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:
31
HANDOFF.md
31
HANDOFF.md
@@ -2321,3 +2321,34 @@ réellement présente — c'est cette colonne-là qui compte.
|
|||||||
2. **L'historique HA** (4,4 Go) : perdu en cas de restauration. Conséquence
|
2. **L'historique HA** (4,4 Go) : perdu en cas de restauration. Conséquence
|
||||||
réelle pour le chantier du bilan énergétique, qui s'appuie sur les
|
réelle pour le chantier du bilan énergétique, qui s'appuie sur les
|
||||||
statistiques long terme. À traiter séparément, en hebdomadaire.
|
statistiques long terme. À traiter séparément, en hebdomadaire.
|
||||||
|
|
||||||
|
### Validation de bout en bout, nuit du 01/09 (fin de session)
|
||||||
|
|
||||||
|
**Chemin d'écriture du chien de garde DÉMONTRÉ**, pas seulement simulé
|
||||||
|
(`tools/watchdog-selftest.mjs`, rejouable) : écriture réelle d'une sonde de
|
||||||
|
777 W sur `W/<portail>/settings/0/Settings/CGwacs/AcPowerSetPoint`, relecture
|
||||||
|
conforme, remise de la valeur d'origine par le script lui-même. Le `--dry-run`
|
||||||
|
supprimait justement ce maillon-là ; sans cette preuve, le chien de garde
|
||||||
|
n'était que probablement fonctionnel. Test fait EDF ouvert, donc consigne
|
||||||
|
inerte : risque nul.
|
||||||
|
|
||||||
|
**Et la séquence de fin de session s'est déroulée dans le bon ordre**, sans
|
||||||
|
que personne l'ait orchestrée :
|
||||||
|
|
||||||
|
| 03:31:30 | fin de charge → `night_recovery_done_idle`, et **la consigne retombe à 50 W dans le même cycle** |
|
||||||
|
| 03:32:04 | 34 s plus tard, l'hystérésis ouvre le contacteur (`turn_off_recovery_done`) |
|
||||||
|
|
||||||
|
La demande baisse AVANT que le réseau parte — le principe de la séquence
|
||||||
|
d'ouverture (§30), obtenu ici par construction et non par coordination. C'est
|
||||||
|
la garantie que le risque introduit en §31 (consigne laissée haute) ne se
|
||||||
|
matérialise pas en fonctionnement normal ; le chien de garde ne couvre plus que
|
||||||
|
le cas de la panne.
|
||||||
|
|
||||||
|
**Bilan de la nuit HC** : charge démarrée à 01:58 après correction, menée à son
|
||||||
|
terme à 03:31 sans jamais entamer la réserve (batterie tenue entre −2 et +1 A
|
||||||
|
pendant toute la session), SOC stable à 49 %.
|
||||||
|
|
||||||
|
**Clé API n8n** : désormais aussi dans `.env` à la racine (convention du
|
||||||
|
propriétaire), en plus de `~/.config/opencode/opencode.json`. Le repli du chien
|
||||||
|
de garde sur `.env` est vérifié — utile pour tout déploiement hors poste de
|
||||||
|
travail.
|
||||||
|
|||||||
90
tools/watchdog-selftest.mjs
Normal file
90
tools/watchdog-selftest.mjs
Normal 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);
|
||||||
Reference in New Issue
Block a user