Chemin d ecriture du chien de garde demontre, pas seulement simule

Sonde de 777 W reellement publiee sur le topic de consigne du GX,
relue, puis valeur d origine remise par le script. Le dry-run supprimait
exactement ce maillon.

Et la fin de session s est deroulee dans le bon ordre sans coordination :
consigne retombee a 50 W a 03:31:30 avec l etat de fin de charge, puis
contacteur ouvert 34 s plus tard. La demande baisse avant que le reseau
parte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKzkkVTG6KXnGXFFC7X9XJ
This commit is contained in:
Tallagrand Thierry
2026-09-01 03:36:09 +02:00
parent 32de4ffa35
commit a7ebaabb8b
2 changed files with 121 additions and 0 deletions

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Preuve de bout en bout du CHEMIN D'ÉCRITURE du chien de garde.
*
* Le `--dry-run` valide la détection, le verrou et le journal, mais il
* supprime précisément ce qui compte le jour d'une panne : la publication
* MQTT vers le GX. Si le broker refusait les topics `W/`, le chien de garde
* serait décoratif — et on ne l'apprendrait qu'au pire moment. Ce test écrit
* réellement, relit, puis REMET la valeur d'origine lui-même.
*
* Sûreté : il n'écrit que sur la consigne de puissance réseau, il mémorise la
* valeur trouvée avant de la modifier et la restaure quoi qu'il arrive
* (y compris sur erreur ou Ctrl-C). La valeur sonde est volontairement
* absurde (777 W) pour être reconnaissable dans un journal.
*
* Usage : node tools/watchdog-selftest.mjs
*/
import mqtt from 'mqtt';
const HOST = process.env.GX_MQTT_HOST || '10.10.10.9';
const PORT = Number(process.env.GX_MQTT_PORT || 1883);
const P = process.env.GX_PORTAL_ID || 'c0619ab34ac5';
const EV = process.env.GX_EVCHARGER_INSTANCE || '40';
const PROBE_W = 777;
const SETPOINT = 'settings/0/Settings/CGwacs/AcPowerSetPoint';
const STARTSTOP = `evcharger/${EV}/StartStop`;
const ACIN = 'system/0/Ac/Grid/L1/Power';
const cache = new Map();
const c = mqtt.connect(`mqtt://${HOST}:${PORT}`, { connectTimeout: 8000 });
const read = (p) => cache.get(`N/${P}/${p}`);
const write = (p, value) => c.publish(`W/${P}/${p}`, JSON.stringify({ value }));
const wait = (ms) => new Promise(r => setTimeout(r, ms));
let original = null;
async function restore(why) {
if (original === null) return;
write(SETPOINT, original);
await wait(1500);
console.log(` restauration (${why}) → consigne ${read(SETPOINT)} W`);
}
process.on('SIGINT', async () => { await restore('interruption'); process.exit(1); });
c.on('connect', async () => {
for (const t of [SETPOINT, STARTSTOP, ACIN]) c.subscribe(`N/${P}/${t}`);
c.publish(`R/${P}/keepalive`, '');
await wait(2500);
original = read(SETPOINT);
console.log(`GX ${HOST}:${PORT} — connecté`);
console.log(`état trouvé : consigne ${original} W · borne StartStop=${read(STARTSTOP)} · réseau ${read(ACIN)} W\n`);
if (typeof original !== 'number') {
console.log('❌ consigne illisible : le test ne peut pas garantir la restauration. ABANDON.');
c.end(); process.exit(1);
}
try {
console.log(`1) écriture réelle de la sonde : ${PROBE_W} W`);
write(SETPOINT, PROBE_W);
await wait(3000);
const seen = read(SETPOINT);
console.log(` relecture : ${seen} W`);
const ok = seen === PROBE_W;
console.log(ok
? ' ✅ le GX accepte les écritures du chien de garde\n'
: ` ❌ écriture SANS EFFET (attendu ${PROBE_W}) — le chien de garde serait décoratif\n`);
console.log(`2) remise de la valeur d'origine : ${original} W`);
write(SETPOINT, original);
await wait(3000);
const back = read(SETPOINT);
console.log(` relecture : ${back} W ${back === original ? '✅' : '❌'}\n`);
console.log(ok && back === original
? 'VERDICT : chemin d\'écriture DÉMONTRÉ, état d\'origine rétabli.'
: 'VERDICT : ANOMALIE — voir ci-dessus.');
process.exitCode = ok && back === original ? 0 : 1;
} catch (e) {
console.error('erreur :', e.message);
await restore('erreur');
process.exitCode = 1;
}
c.end();
setTimeout(() => process.exit(process.exitCode ?? 0), 500);
});
c.on('message', (t, m) => { try { const v = JSON.parse(m.toString()); if (v && 'value' in v) cache.set(t, v.value); } catch {} });
c.on('error', (e) => { console.error('MQTT:', e.message); process.exit(1); });
setTimeout(() => { console.error('délai dépassé'); process.exit(1); }, 30000);