Concierge vehicule : code (detection, live, write + test de portee)
Complete le commit doc precedent : ajoute le code JS du concierge, exclu jusqu'ici par la liste blanche workspace/*. Exception ciblee !workspace/concierge-*.js dans .gitignore (aucun secret : les tokens sont lus depuis .ha-token/.pg-airtria, ignores). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RZsbhyGmRwZogdoBwT6C6y
This commit is contained in:
61
workspace/concierge-write.js
Normal file
61
workspace/concierge-write.js
Normal file
@@ -0,0 +1,61 @@
|
||||
// concierge-write.js — Concierge véhicule Airtria, ÉTAGE ÉCRITURE (canal HA restreint)
|
||||
// -----------------------------------------------------------------------------
|
||||
// SEULE écriture HA d'Airtria. La restriction à cette entité est CONVENTIONNELLE
|
||||
// (le token HA peut techniquement tout écrire) : elle ne tient que par ce code.
|
||||
// Donc, invariants tenus ici et vérifiés par concierge-write.test.js :
|
||||
// - l'entité écrite est une CONSTANTE EN DUR, jamais interpolée ni construite ;
|
||||
// - une seule écriture, via input_text.set_value, aucun actionneur ;
|
||||
// - relecture obligatoire après écriture (détecte un rejet silencieux HA).
|
||||
// Voir airtria-concierge-vehicule-SPEC.md.
|
||||
// -----------------------------------------------------------------------------
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// ⚠️ Constante unique. Ne JAMAIS la rendre dynamique ni en ajouter d'autres.
|
||||
const HELPER_ENTITY = 'input_text.airtria_vehicle_label';
|
||||
const HA_BASE = process.env.HA_BASE || 'http://10.10.10.17:8123';
|
||||
const MAX_LEN = 255; // le helper DOIT être déclaré max:255 côté HA (défaut = 100)
|
||||
|
||||
function haToken() {
|
||||
return fs.readFileSync(
|
||||
process.env.HA_TOKEN_FILE || path.join(__dirname, '.ha-token'), 'utf8').trim();
|
||||
}
|
||||
|
||||
// Publie le tuple {sessions[], label, confirmed_at, source} pour un branchement.
|
||||
// sessions = clefs `debut` VERBATIM (ms comprises). Le service EVCS joint sur ±1 s.
|
||||
async function publishLabel(sessions, label, source = 'whatsapp') {
|
||||
if (!Array.isArray(sessions) || sessions.length === 0) {
|
||||
throw new Error('sessions doit être une liste non vide');
|
||||
}
|
||||
if (!label || typeof label !== 'string') throw new Error('label invalide');
|
||||
const payload = JSON.stringify({
|
||||
sessions, label, confirmed_at: new Date().toISOString(), source,
|
||||
});
|
||||
if (payload.length > MAX_LEN) {
|
||||
throw new Error(`payload ${payload.length} > ${MAX_LEN} : scinder le groupe ` +
|
||||
`en deux publications espacées de > 90 s`);
|
||||
}
|
||||
const token = haToken();
|
||||
const headers = { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' };
|
||||
|
||||
// Écriture : service input_text.set_value sur la CONSTANTE en dur.
|
||||
const res = await fetch(`${HA_BASE}/api/services/input_text/set_value`, {
|
||||
method: 'POST', headers,
|
||||
body: JSON.stringify({ entity_id: HELPER_ENTITY, value: payload }),
|
||||
});
|
||||
if (!res.ok) throw new Error(`set_value HTTP ${res.status}`);
|
||||
|
||||
// Relecture OBLIGATOIRE : seul moyen de voir un rejet silencieux (max trop court).
|
||||
const back = await fetch(`${HA_BASE}/api/states/${HELPER_ENTITY}`, { headers });
|
||||
if (!back.ok) throw new Error(`relecture HTTP ${back.status}`);
|
||||
const state = (await back.json()).state;
|
||||
if (state !== payload) {
|
||||
throw new Error('relecture != écrit — rejet silencieux probable ' +
|
||||
'(vérifier max:255 sur le helper)');
|
||||
}
|
||||
return { entity: HELPER_ENTITY, payload };
|
||||
}
|
||||
|
||||
module.exports = { publishLabel, HELPER_ENTITY, MAX_LEN };
|
||||
Reference in New Issue
Block a user