96 lines
5.7 KiB
JavaScript
96 lines
5.7 KiB
JavaScript
#!/usr/bin/env node
|
|
// arbitrage.js — ARBITRE ENCADRÉ (08/10/2026, « GO arbitre », contrat d'EVCS, commit dea9c88).
|
|
// EVCS publie sensor.evcs_arbitrage (état question | repondu | aucun ; en question : id, question, choix
|
|
// [{cle, libelle}]). Le concierge (detect) envoie la question à Thierry UNE fois par id ; sa réponse
|
|
// passe par CE script, seule écriture : input_text.evcs_arbitrage_reponse =
|
|
// {"id","choix","source":"whatsapp","confirmed_at"}. EVCS vérifie et n'applique que l'ordre du choix.
|
|
// node arbitrage.js question → texte de la question ouverte (ou rien) [appelé par detect]
|
|
// node arbitrage.js repondre <cle> → écrit la réponse à la question OUVERTE, attend le verdict d'EVCS
|
|
// node arbitrage.js application → verdict « repondu » pas encore relayé (ou rien) [appelé par detect]
|
|
// Jamais d'id fourni par l'agent : l'id est relu dans HA au moment d'écrire. Jamais d'autre entité.
|
|
// Simulation : fichier `.simulation` → aucune écriture ; ARBITRAGE_SIMULATION='{"arbitrage":{état+attributs}}'
|
|
// → HA non lu non plus (tests).
|
|
'use strict';
|
|
const fs = require('fs'); const path = require('path');
|
|
const HA_BASE = process.env.HA_BASE || 'http://10.10.10.17:8123';
|
|
const token = () => fs.readFileSync(path.join(__dirname, '.ha-token'), 'utf8').trim();
|
|
const SENSOR = 'sensor.evcs_arbitrage';
|
|
const REPONSE = 'input_text.evcs_arbitrage_reponse';
|
|
const STATE = path.join(__dirname, 'concierge-arbitrage-state.json'); // {questionId, appliqueId}
|
|
const SIMENV = process.env.ARBITRAGE_SIMULATION ? JSON.parse(process.env.ARBITRAGE_SIMULATION) : null;
|
|
const SIMULATION = !!SIMENV || fs.existsSync(path.join(__dirname, '.simulation'));
|
|
|
|
async function ha(method, url, body) {
|
|
const ac = new AbortController(); const t = setTimeout(() => ac.abort(), 10000);
|
|
try {
|
|
const r = await fetch(`${HA_BASE}${url}`, { method, signal: ac.signal,
|
|
headers: { Authorization: `Bearer ${token()}`, 'Content-Type': 'application/json' },
|
|
body: body ? JSON.stringify(body) : undefined });
|
|
if (!r.ok) throw new Error(`HA ${r.status} sur ${url}`);
|
|
return r.json();
|
|
} finally { clearTimeout(t); }
|
|
}
|
|
const lireArbitrage = async () => SIMENV ? (SIMENV.arbitrage || { state: 'aucun', attributes: {} })
|
|
: ha('GET', `/api/states/${SENSOR}`);
|
|
const loadState = () => { if (SIMENV) return SIMENV.etat || {}; try { return JSON.parse(fs.readFileSync(STATE, 'utf8')); } catch { return {}; } };
|
|
const saveState = (o) => { if (SIMULATION) return; fs.writeFileSync(STATE, JSON.stringify(o, null, 2)); };
|
|
const choixDe = (a) => (Array.isArray(a.choix) ? a.choix : []).filter((c) => c && c.cle != null && c.libelle);
|
|
|
|
function texteQuestion(a) {
|
|
const ch = choixDe(a).map((c) => `${c.cle}. ${c.libelle}`).join('\n');
|
|
return `⚖️ ${String(a.question).trim()}\n${ch}\nRéponds par le numéro.`;
|
|
}
|
|
|
|
async function question() {
|
|
const s = await lireArbitrage(); const a = s.attributes || {};
|
|
if (s.state !== 'question' || !a.id || !a.question || choixDe(a).length < 2) return '';
|
|
const st = loadState();
|
|
if (st.questionId === a.id) return ''; // déjà envoyée
|
|
saveState({ ...st, questionId: a.id });
|
|
return texteQuestion(a);
|
|
}
|
|
|
|
async function application() {
|
|
const s = await lireArbitrage(); const a = s.attributes || {};
|
|
if (s.state !== 'repondu' || !a.id || !a.application) return '';
|
|
const st = loadState();
|
|
if (st.appliqueId === a.id || st.questionId !== a.id) return ''; // déjà relayé, ou arbitrage jamais posé par nous
|
|
saveState({ ...st, appliqueId: a.id });
|
|
return `⚖️ ${String(a.application).trim()}`;
|
|
}
|
|
|
|
async function repondre(cleArg) {
|
|
const cle = String(cleArg ?? '').trim();
|
|
if (!/^\d{1,2}$/.test(cle)) return `Rien n'est transmis : « ${cle} » n'est pas un numéro de choix. Redemande le numéro.`;
|
|
const s = await lireArbitrage(); const a = s.attributes || {};
|
|
if (s.state !== 'question' || !a.id) return "Rien n'est transmis : aucun arbitrage n'est ouvert.";
|
|
const c = choixDe(a).find((x) => String(x.cle) === cle);
|
|
if (!c) return `Rien n'est transmis : le choix ${cle} n'existe pas (${choixDe(a).map((x) => x.cle).join(' ou ')}). Redemande le numéro.`;
|
|
const valeur = JSON.stringify({ id: a.id, choix: String(c.cle), source: 'whatsapp', confirmed_at: new Date().toISOString() });
|
|
if (valeur.length > 255) return "Rien n'est transmis : réponse trop longue pour HA (id d'arbitrage anormal).";
|
|
if (SIMULATION) return `[SIMULATION : rien écrit — ${REPONSE} = ${valeur}] Choix ${c.cle} (${c.libelle}).`;
|
|
await ha('POST', '/api/services/input_text/set_value', { entity_id: REPONSE, value: valeur });
|
|
// Verdict d'EVCS : « repondu » avec application ; on attend jusqu'à 90 s, sinon detect le relaiera.
|
|
for (let i = 0; i < 18; i++) {
|
|
await new Promise((r) => setTimeout(r, 5000));
|
|
let v; try { v = await lireArbitrage(); } catch { continue; }
|
|
const va = v.attributes || {};
|
|
if (v.state === 'repondu' && va.id === a.id && va.application) {
|
|
const st = loadState(); saveState({ ...st, questionId: a.id, appliqueId: a.id });
|
|
return `Choix ${c.cle} transmis. ⚖️ ${String(va.application).trim()}`;
|
|
}
|
|
}
|
|
return `Choix ${c.cle} transmis (${c.libelle}). EVCS n'a pas encore confirmé ce qu'il a fait : sa réponse arrivera par WhatsApp.`;
|
|
}
|
|
|
|
const [cmd, arg] = process.argv.slice(2);
|
|
(async () => {
|
|
let out = '';
|
|
if (cmd === 'question') out = await question();
|
|
else if (cmd === 'application') out = await application();
|
|
else if (cmd === 'repondre') {
|
|
try { out = await repondre(arg); } catch (e) { out = `Rien n'est transmis : erreur (${e.message}).`; }
|
|
} else { console.error('usage : arbitrage.js question | repondre <numéro> | application'); process.exit(2); }
|
|
if (out) console.log(out);
|
|
})().catch((e) => { console.error('[arbitrage]', e.message); process.exit(1); });
|