diff --git a/workspace/AGENTS.md b/workspace/AGENTS.md index 6ddf077..1218b6a 100644 --- a/workspace/AGENTS.md +++ b/workspace/AGENTS.md @@ -60,7 +60,7 @@ Pour vérifier qu'un PNG est correct, utiliser **`du -b`** (taille en octets) vi Ne JAMAIS utiliser `view_image` ni `read` sur un `.png` : c'est lent, coûteux en contexte, et inutile. Un PNG de graphe valide fait > 5000 octets. -### 1.7 Mesures : ne JAMAIS substituer une grandeur absente, vérifier `last_changed` +### 1.7 Mesures : ne pas substituer, ne pas fabriquer, ne pas confondre âge et panne Pour toute question chiffrée (énergie, SOC, puissance, prix). Ces règles ont manqué le 16/09 (« SOC de la VW ? » : la grandeur n'existe pas ; Airtria l'a dit, puis a servi les @@ -71,8 +71,16 @@ grandeurs voisines comme si elles répondaient → 5 affirmations fausses sur 6) (« je n'ai pas X ; j'ai Y, qui est différent »), jamais comme la réponse. Le remplacement silencieux est pire qu'un « je ne sais pas » : il change une surveillance en fausse réassurance. -- **Vérifier `last_changed`** (donné par `/api/states/`) avant de citer une valeur - comme ACTUELLE : figée depuis des heures ≠ « maintenant » / « cette nuit ». +- **L'âge d'une entité métier n'est PAS une preuve de panne.** Un système stable produit des + entités IMMOBILES (voiture branchée depuis 12 h, mode « Bloqué » inchangé) : c'est normal. + Aucun champ de l'API ne distingue un écrivain mort d'un écrivain qui réécrit la même valeur + (`last_reported` = `last_changed`). Donc : lire une entité métier pour sa VALEUR, jamais + pour sa fraîcheur, et ne jamais dire « figé » d'une valeur ancienne. La VIVACITÉ (« le + contrôleur est-il muet ? ») se lit UNIQUEMENT sur le battement de cœur (état = horodatage + qui change forcément à chaque écriture). +- **Ne pas attribuer une valeur à une fenêtre qu'elle ne couvre pas** : un compteur de vie + (kWh cumulés) n'est pas « cette nuit » — la vraie erreur du « 12,1 kWh », c'est le + périmètre, pas l'âge. - **Grandeur directionnelle** (une batterie monte/descend) : ne jamais publier la valeur sans le SIGNE de la puissance associée, sinon on affirme « ça monte » quand ça descend. - **Ne jamais FABRIQUER une panne ni une cause.** Si une lecture échoue, rapporter l'erreur diff --git a/workspace/airtria-ha-read.js b/workspace/airtria-ha-read.js index a2e835a..c5a6451 100644 --- a/workspace/airtria-ha-read.js +++ b/workspace/airtria-ha-read.js @@ -2,10 +2,13 @@ // ----------------------------------------------------------------------------- // 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 (marqué FIGÉ au-delà de 15 min). En cas -// d'échec, imprime le VRAI code HTTP / la vraie erreur — pour que la panne rapportée soit -// réelle, pas inventée (le 16/09 Airtria a affirmé un 401 alors que le jeton renvoyait 200). -// Rappel EVCS 16/09 : les horodatages /api/states sont en UTC → toujours convertir. +// 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'); @@ -15,14 +18,22 @@ 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(); -function parisAge(iso) { +// Â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: 'medium' }); - const ageMin = Math.round((Date.now() - d.getTime()) / 60000); - const stale = ageMin > 15 ? ' ⚠️ FIGÉ (>15 min, PAS une mesure actuelle)' : ''; - return `${t} (Paris, il y a ${ageMin} min)${stale}`; + { 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) { @@ -49,7 +60,7 @@ async function readOne(entity) { 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} | maj ${parisAge(j.last_changed)}`); + console.log(`${entity} = ${j.state}${unit} | ${since(j.last_changed)}`); } (async () => {