// 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 };