// concierge-health.js — santé du concierge véhicule, publiée dans Home Assistant. // ----------------------------------------------------------------------------- // Même patron que le moniteur LLM (llm-monitor.js), appliqué au cycle `detect` : // - BATTEMENT DE CŒUR à CHAQUE passage (horodatage sur input_datetime) : sans lui, un // concierge mort — cron arrêté, conteneur tombé, auth qui échoue au chargement — est // indiscernable d'un système sain. HA peut alors crier quand l'horodatage se fige. // Valeur qui CHANGE à chaque fois (horodatage, pas « ok ») : HA ne rafraîchit pas le // last_changed d'un helper réécrit à l'identique. // - ALERTE WhatsApp UNIQUEMENT sur TRANSITION (ok↔erreur), jamais un rappel périodique // (« events not states ») : une panne d'auth qui dure ne spamme pas le canal. // - Chemin HA INDÉPENDANT de PostgreSQL : l'alerte « auth KO » part précisément quand // c'est PG/l'auth qui tombe — le signal de vie n'emprunte pas le chemin qui tombe. // - Best-effort : ne fait JAMAIS échouer le concierge (aucune entité requise, aucun jet). // - Interrupteur HA : OFF → pas de WhatsApp, mais l'affichage reste à jour. // ----------------------------------------------------------------------------- 'use strict'; const fs = require('fs'); const path = require('path'); const STATE = process.env.CONCIERGE_HEALTH_STATE || path.join(__dirname, 'concierge-health-state.json'); const HA_BASE = process.env.HA_BASE || 'http://10.10.10.17:8123'; const HA_ENABLED = 'input_boolean.concierge_monitor_enabled'; // interrupteur alertes on/off const HA_STATUS = 'input_text.concierge_status'; // libellé d'état (max 255) const HA_LASTRUN = 'input_datetime.concierge_last_run'; // battement de cœur (chaque cycle) const load = (f, d) => { try { return JSON.parse(fs.readFileSync(f, 'utf8')); } catch { return d; } }; const save = (f, o) => { try { fs.writeFileSync(f, JSON.stringify(o, null, 2)); } catch { /* best-effort */ } }; const haToken = () => { try { return fs.readFileSync(process.env.HA_TOKEN_FILE || path.join(__dirname, '.ha-token'), 'utf8').trim(); } catch { return null; } }; async function haGet(entity) { const tok = haToken(); if (!tok) return null; try { const r = await fetch(`${HA_BASE}/api/states/${entity}`, { headers: { Authorization: `Bearer ${tok}` } }); return r.ok ? r.json() : null; } catch { return null; } } async function haCall(domain, service, data) { const tok = haToken(); if (!tok) return; try { await fetch(`${HA_BASE}/api/services/${domain}/${service}`, { method: 'POST', headers: { Authorization: `Bearer ${tok}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); } catch { /* best-effort */ } } // Publie la santé du cycle et renvoie le message de TRANSITION à envoyer par WhatsApp // (ou '' si pas de transition, ou si l'interrupteur est OFF). `ok` = le cycle detect a // réussi (lecture PostgreSQL comprise) ; `detail` = message d'erreur si !ok. async function publish({ ok, detail }) { const prev = load(STATE, { status: 'ok' }); const status = ok ? 'ok' : 'alert'; const hhmm = new Date().toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit', timeZone: 'Europe/Paris' }); // Battement de cœur À CHAQUE passage + statut live (best-effort, chemin indépendant de PG). await haCall('input_datetime', 'set_datetime', { entity_id: HA_LASTRUN, timestamp: Math.floor(Date.now() / 1000) }); const label = (ok ? 'OK' : `⚠️ ${detail || 'erreur détection'}`).slice(0, 240); await haCall('input_text', 'set_value', { entity_id: HA_STATUS, value: `${label} · ${hhmm}` }); // Alerte seulement sur la bascule d'état. let out = ''; if (status === 'alert' && prev.status !== 'alert') { out = `⚠️ Concierge véhicule en panne : ${detail || 'erreur de détection'}. ` + `Il ne peut plus poser la question du véhicule tant que ce n'est pas corrigé.`; } else if (status === 'ok' && prev.status === 'alert') { out = '✅ Concierge véhicule de nouveau opérationnel.'; } save(STATE, { status, at: new Date().toISOString(), detail: ok ? null : (detail || null) }); if (!out) return ''; const enabled = await haGet(HA_ENABLED); if (enabled && enabled.state === 'off') return ''; // muet : affichage à jour, pas de WhatsApp return out; } module.exports = { publish, HA_ENABLED, HA_STATUS, HA_LASTRUN };