airtria-ha-read: age = fait neutre, pas de verdict "fige" (EVCS)

EVCS a mesure que "FIGE >15 min" crie au loup : un systeme stable a des entites metier
IMMOBILES par construction (voiture branchee depuis 12 h, mode "Bloque" inchange), et
last_reported == last_changed -- aucun champ de l'API ne distingue un ecrivain mort d'une
reecriture identique. L'age n'est donc pas une preuve de panne.

- airtria-ha-read.js : "inchange depuis X" (fait neutre), plus de marqueur FIGE.
- AGENTS.md 1.7 : l'age d'une entite metier n'est pas une panne ; la vivacite se lit sur le
  battement de coeur seul ; entite metier lue pour sa VALEUR, pas sa fraicheur. La vraie
  erreur du "12,1 kWh" = le perimetre (compteur de vie), pas l'age.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RZsbhyGmRwZogdoBwT6C6y
This commit is contained in:
2026-09-16 08:19:34 +02:00
parent 78577eda85
commit 04c1e19bf3
2 changed files with 32 additions and 13 deletions

View File

@@ -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, 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. 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 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 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 (« 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 silencieux est pire qu'un « je ne sais pas » : il change une surveillance en fausse
réassurance. réassurance.
- **Vérifier `last_changed`** (donné par `/api/states/<entité>`) avant de citer une valeur - **L'âge d'une entité métier n'est PAS une preuve de panne.** Un système stable produit des
comme ACTUELLE : figée depuis des heures ≠ « maintenant » / « cette nuit ». 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 - **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. 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 - **Ne jamais FABRIQUER une panne ni une cause.** Si une lecture échoue, rapporter l'erreur

View File

@@ -2,10 +2,13 @@
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// Airtria doit LIRE l'état via ce script (exec), jamais raisonner sur ce qu'une valeur // 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é, // « 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 // et `last_changed` converti en heure de Paris + âge comme FAIT neutre (« inchangé depuis
// d'échec, imprime le VRAI code HTTP / la vraie erreur — pour que la panne rapportée soit // X »), JAMAIS un verdict « figé » : un système stable a des entités immobiles (voiture
// réelle, pas inventée (le 16/09 Airtria a affirmé un 401 alors que le jeton renvoyait 200). // branchée depuis 12 h, mode inchangé) et aucun champ de l'API ne distingue un écrivain mort
// Rappel EVCS 16/09 : les horodatages /api/states sont en UTC → toujours convertir. // 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'; 'use strict';
const fs = require('fs'); 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( const token = () => fs.readFileSync(
process.env.HA_TOKEN_FILE || path.join(__dirname, '.ha-token'), 'utf8').trim(); 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 '?'; if (!iso) return '?';
const d = new Date(iso); const d = new Date(iso);
const t = d.toLocaleString('fr-FR', const t = d.toLocaleString('fr-FR',
{ timeZone: 'Europe/Paris', dateStyle: 'short', timeStyle: 'medium' }); { timeZone: 'Europe/Paris', dateStyle: 'short', timeStyle: 'short' });
const ageMin = Math.round((Date.now() - d.getTime()) / 60000); const min = Math.round((Date.now() - d.getTime()) / 60000);
const stale = ageMin > 15 ? ' ⚠️ FIGÉ (>15 min, PAS une mesure actuelle)' : ''; let age;
return `${t} (Paris, il y a ${ageMin} min)${stale}`; 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) { async function readOne(entity) {
@@ -49,7 +60,7 @@ async function readOne(entity) {
const j = await r.json(); const j = await r.json();
const unit = j.attributes && j.attributes.unit_of_measurement const unit = j.attributes && j.attributes.unit_of_measurement
? ` ${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 () => { (async () => {