Le chien de garde arretait la charge qu il devait proteger
Nuit du 16 au 17/09 : neuf arrets de la charge nuit de la VW, de 30 s a 3 min, sans que le controleur EVCS ait bouge — sa table de decision ne porte que deux lignes sur toute la periode. Le journal de bord de Home Assistant montre le switch de charge passant a off SANS contexte de service, donc commande depuis le GX, puis le controleur qui reessaie au cycle suivant. Correlation un pour un avec les actions du chien de garde, trois secondes avant chacune, et aucune coupure sans action. Son message porte la signature : « CONTROLEUR MUET — derniere execution il y a NaNs ». Cause : n8n inscrit une execution AVANT de la demarrer, avec status "new" et startedAt null. Date.parse(null) vaut NaN, et le test !(ageSec <= 600) — forme choisie pour echouer du cote sur — declare alors mort le controleur qui vient precisement de demarrer un cycle. Le signal de vie retourne contre lui-meme. La fenetre dure moins d une seconde : 3 sondages sur 968 y tombent, 0,3 %. Ce qui a change le 16/09 n est pas le code mais la PHASE du chien de garde, qui derive d environ vingt secondes par jour et est venue se poser exactement sur les instants de declenchement du controleur (:00 et :30) — 34 evenements ce jour-la contre 1 a 2 les jours precedents. Le defaut dormait depuis le 01/09. Correction a la session precedente : la nuit du 15 au 16, presentee comme irreprochable, portait deja trois coupures dont une de trois minutes. Ce que j avais verifie — consigne a 6 A inchangee pendant 153 min — restait exact et ne prouvait rien : la consigne ne bouge pas quand la borne s arrete, seule la puissance tombe. Second defaut, latent et plus grave : CFG.idleSetpointW valait 50 W quand le controleur est passe a 700 W le 06/09, donc dangerousSetpoint etait vrai en permanence et chaque fausse alerte ecrasait aussi la consigne reseau a 50 W. Sans consequence cette nuit — EDF etait ouvert — mais pendant une nuit ou le reseau porte la maison, c est le defaut du 04/09 reintroduit. Trois correctifs : un horodatage manquant n est plus une panne (on prend le plus recent startedAt exploitable, repli cote sur si aucun) ; valeur de repos portee a 700 W avec le commentaire qui dit ou vit l autre exemplaire ; et une panne doit DURER — deux lectures consecutives avant d agir, meme doctrine que la tolerance batterie et le relachement du contacteur. Verification par REJEU et non par absence de symptome : le redemarrage a remis la phase a :07, donc une nuit calme ne prouverait rien. Un serveur local rejouant la charge JSON exacte capturee a 02:07:01 donne, code d avant, « MUET — il y a NaNs » et les deux ecritures ; code corrige, « controleur vivant (execution il y a 36s) » et aucune ecriture. Le filet mord toujours sur une panne soutenue : 1/2 puis alerte et StartStop = 0 au second cycle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GwpFpy2aQ2rJok6tNQSbVj
This commit is contained in:
@@ -81,8 +81,18 @@ const CFG = {
|
||||
livenessMaxAgeSec: Number(process.env.WATCHDOG_MAX_AGE_SEC || 600),
|
||||
periodSec: Number(process.env.WATCHDOG_PERIOD_SEC || 60),
|
||||
|
||||
// Valeur de repos de la consigne réseau : le défaut Victron, pas un choix.
|
||||
idleSetpointW: Number(process.env.WATCHDOG_IDLE_SETPOINT_W || 50),
|
||||
// Valeur de repos de la consigne reseau. ⚠️ ELLE DOIT ETRE CELLE DU
|
||||
// CONTROLEUR, sans quoi le chien de garde juge « laissee haute » une consigne
|
||||
// parfaitement normale. C'etait le cas depuis le 06/09/2026 : le controleur
|
||||
// est passe de 200 a 700 W (`gridSetpointIdleW` dans INPUT_PARSE, helper
|
||||
// optionnel `input_number.evcs_grid_setpoint_idle_w`) et cette valeur-ci est
|
||||
// restee a 50, si bien que `dangerousSetpoint` etait VRAI en permanence — et
|
||||
// que chaque fausse alerte ecrasait la consigne a 50 W. Sur une nuit ou le
|
||||
// reseau porte la maison, cela lui fait reprendre la batterie (cf. §43).
|
||||
// Elle n'est pas lue dans Home Assistant a dessein : le chien de garde doit
|
||||
// fonctionner quand HA est mort. A tenir a jour a la main, ici et dans
|
||||
// deploy/watchdog/watchdog.env.
|
||||
idleSetpointW: Number(process.env.WATCHDOG_IDLE_SETPOINT_W || 700),
|
||||
// Au-delà de quoi la consigne est jugée « laissée haute ». Un pas de courant
|
||||
// EVCS à 230 V, la granularité à laquelle le contrôleur déplace la puissance.
|
||||
setpointSlackW: Number(process.env.WATCHDOG_SETPOINT_SLACK_W || 230),
|
||||
@@ -149,7 +159,32 @@ async function controllerLiveness() {
|
||||
const runs = (await r.json()).data || [];
|
||||
if (!runs.length) return { alive: false, ageSec: null, detail: 'aucune exécution connue' };
|
||||
|
||||
const ageSec = (Date.now() - Date.parse(runs[0].startedAt)) / 1000;
|
||||
// ⚠️ UNE EXECUTION PAS ENCORE DEMARREE N'EST PAS UNE PANNE, C'EST LE
|
||||
// CONTRAIRE. n8n insere la ligne d'execution AVANT de la lancer : pendant
|
||||
// quelques centaines de millisecondes elle porte `status: "new"` et
|
||||
// `startedAt: null`. `Date.parse(null)` vaut NaN, et la forme `!(NaN <= X)`
|
||||
// — choisie pour echouer du cote sur — declarait alors le controleur mort
|
||||
// alors qu'il venait precisement de demarrer un cycle.
|
||||
//
|
||||
// Mesure du 17/09/2026 : 3 sondages sur 968 tombent dans cette fenetre,
|
||||
// soit 0,3 %. Mais le chien de garde ne sonde qu'une fois par minute et sa
|
||||
// phase DERIVE d'environ vingt secondes par jour ; quand elle vient se
|
||||
// poser sur les instants de declenchement du controleur (:00 et :30), il
|
||||
// tombe dedans plusieurs fois par heure. Nuit du 16 au 17/09 : neuf arrets
|
||||
// de la charge de la VW, chacun trois secondes apres une action du chien
|
||||
// de garde, correlation un pour un.
|
||||
//
|
||||
// On prend donc le plus recent horodatage EXPLOITABLE parmi les executions
|
||||
// renvoyees : une ligne sans `startedAt` est ignoree comme reference, mais
|
||||
// elle ne vaut jamais « mort ». Si AUCUNE des trois n'est horodatee, on
|
||||
// retombe cote sur.
|
||||
const horodatees = runs
|
||||
.map((e) => Date.parse(e.startedAt))
|
||||
.filter((t) => Number.isFinite(t));
|
||||
if (!horodatees.length) {
|
||||
return { alive: false, ageSec: null, detail: 'aucune exécution horodatée' };
|
||||
}
|
||||
const ageSec = (Date.now() - Math.max(...horodatees)) / 1000;
|
||||
if (!(ageSec <= CFG.livenessMaxAgeSec)) {
|
||||
return { alive: false, ageSec, detail: `dernière exécution il y a ${Math.round(ageSec)}s` };
|
||||
}
|
||||
@@ -224,6 +259,16 @@ function gxWrite(p, value) {
|
||||
let armed = true; // faux = action déjà faite, on attend un signe de vie
|
||||
let lastAliveLog = 0;
|
||||
|
||||
// ⚠️ UNE PANNE DOIT DURER POUR COMPTER. Meme doctrine que la tolerance
|
||||
// temporelle de la politique batterie (HANDOFF §35) et que le relachement du
|
||||
// contacteur (§57) : un franchissement de seuil vu sur UN echantillon est un
|
||||
// tirage, pas un fait. Le seuil de silence vaut deja 600 s ; exiger deux
|
||||
// lectures « mort » consecutives ne retarde la reaction que d'une periode
|
||||
// (60 s) et supprime toute cette classe de faux positif, y compris un hoquet
|
||||
// reseau isole entre les deux NAS.
|
||||
const CONFIRMATIONS_REQUISES = 2;
|
||||
let mortsConsecutives = 0;
|
||||
|
||||
async function cycle() {
|
||||
gxKeepalive();
|
||||
const live = await controllerLiveness();
|
||||
@@ -231,6 +276,7 @@ async function cycle() {
|
||||
if (live.alive) {
|
||||
if (!armed) log('INFO', `contrôleur revenu (${live.detail}) — chien de garde réarmé`);
|
||||
armed = true;
|
||||
mortsConsecutives = 0;
|
||||
// Trace discrète : une ligne par heure suffit à prouver qu'il veille.
|
||||
if (Date.now() - lastAliveLog > 3600e3) {
|
||||
lastAliveLog = Date.now();
|
||||
@@ -239,6 +285,13 @@ async function cycle() {
|
||||
return;
|
||||
}
|
||||
|
||||
mortsConsecutives += 1;
|
||||
if (mortsConsecutives < CONFIRMATIONS_REQUISES) {
|
||||
log('WARN', `contrôleur peut-être muet (${live.detail}) — ` +
|
||||
`${mortsConsecutives}/${CONFIRMATIONS_REQUISES}, on confirme au cycle suivant avant d'agir`);
|
||||
return;
|
||||
}
|
||||
|
||||
const setpointW = gxRead(T.setpoint);
|
||||
const evPowerW = gxRead(T.evPower);
|
||||
const evOn = gxRead(T.evStartStop);
|
||||
|
||||
Reference in New Issue
Block a user