// 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)}`); } (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); });