// airtria-ha-read.js [ …] — lecture Home Assistant fiable, UNE vérité. // ----------------------------------------------------------------------------- // Airtria doit LIRE l'état via ce script (exec), jamais raisonner sur ce qu'une valeur // « devrait » être, jamais fabriquer une erreur. Pour chaque entité, imprime : état, unité, // et `last_changed` converti en heure de Paris + âge comme FAIT neutre (« inchangé depuis // X »), JAMAIS un verdict « figé » : un système stable a des entités immobiles (voiture // branchée depuis 12 h, mode inchangé) et aucun champ de l'API ne distingue un écrivain mort // d'une réécriture identique (EVCS 16/09) — la vivacité se lit sur le battement de cœur, pas // sur l'âge d'une entité métier. En cas d'échec, imprime le VRAI code HTTP / la vraie erreur // (le 16/09 Airtria a affirmé un 401 alors que le jeton renvoyait 200 : ne rien inventer). // Rappel EVCS : les horodatages /api/states sont en UTC → toujours convertir. // ----------------------------------------------------------------------------- '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( process.env.HA_TOKEN_FILE || path.join(__dirname, '.ha-token'), 'utf8').trim(); // Âge = FAIT neutre, jamais un verdict de panne (cf. en-tête). La péremption d'une valeur // ne se déduit PAS de son âge : ancienneté ≠ panne pour une entité métier. function since(iso) { if (!iso) return '?'; const d = new Date(iso); const t = d.toLocaleString('fr-FR', { timeZone: 'Europe/Paris', dateStyle: 'short', timeStyle: 'short' }); const min = Math.round((Date.now() - d.getTime()) / 60000); let age; if (min < 1) age = "à l'instant"; else if (min < 90) age = `inchangé depuis ${min} min`; else { const h = Math.floor(min / 60), m = min % 60; age = `inchangé depuis ${h} h${m ? ' ' + String(m).padStart(2, '0') : ''}`; } return `${t} (Paris) · ${age}`; } async function readOne(entity) { const ac = new AbortController(); const to = setTimeout(() => ac.abort(), 10000); let r; try { r = await fetch(`${HA_BASE}/api/states/${entity}`, { headers: { Authorization: `Bearer ${token()}` }, signal: ac.signal }); } catch (e) { clearTimeout(to); console.log(`${entity} : ERREUR réseau (${e.name}) — ne rien inventer`); return; } clearTimeout(to); if (r.status === 404) { console.log(`${entity} : INEXISTANT (HTTP 404) — cette entité n'existe pas, ne pas substituer`); return; } if (!r.ok) { console.log(`${entity} : ERREUR HTTP ${r.status} — rapporter ce code, ne rien inventer`); return; } const j = await r.json(); const unit = j.attributes && j.attributes.unit_of_measurement ? ` ${j.attributes.unit_of_measurement}` : ''; console.log(`${entity} = ${j.state}${unit} | ${since(j.last_changed)}`); // Attributs utiles (le module d'une cellule, le statut complet/partiel, l'explication // ou la raison du contrôleur sont des ATTRIBUTS, pas l'état — les imprimer sinon // l'agent ne peut pas les relayer). On masque le bruit de config. const NOISE = new Set(['unit_of_measurement', 'friendly_name', 'icon', 'device_class', 'state_class', 'supported_features', 'attribution', 'entity_picture', 'assumed_state', 'editable', 'min', 'max', 'step', 'mode', 'initial', 'options', 'last_reset']); if (j.attributes) { for (const [k, v] of Object.entries(j.attributes)) { if (NOISE.has(k)) continue; const val = (v !== null && typeof v === 'object') ? JSON.stringify(v) : String(v); console.log(` · ${k} = ${val}`); } } } (async () => { const entities = process.argv.slice(2); if (!entities.length) { console.error('usage: airtria-ha-read.js [entity_id ...]'); process.exit(2); } for (const e of entities) await readOne(e); })().catch(e => { console.error('[ha-read]', e.message); process.exit(1); });