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

@@ -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 () => {