Patrick Schurig 312a2484ae [3c-5] watchdog L2 : QTimer fraîcheur compteur + mode dégradé conservateur
QTimer 30s indépendant des signaux ; m_lastMeterUpdate picoté sur powerBalanceChanged.
Silence >90s → mode dégradé (appliqué à la TRANSITION uniquement) :
  - ECS palier 0 force=true ;
  - EV : clamp courant minimum SEULEMENT si déjà en charge (pas d'activation forcée ;
    "jamais 0 A si branché" relève du failsafe L1, pas du repli logiciel).
update() suspend la planification + le dispatch tant que m_degradedMode (sécurité L4
en position 3 reste active) → pas de rallumage sur le cache d'un compteur mort, pas
d'oscillation. Reprise au retour du compteur.

SAFETY.md §L2 : nuance maintenu/démarré + suspension planification. AGENTS.md morceau 7 :
exiger ECS reste à 0 sur plusieurs cycles. SG-Ready/Batterie déférés 3e/3f ;
flag degradedMode exposé en 3c-6. Build 0 erreur / 0 warning.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 16:43:28 +02:00

7.9 KiB
Raw Blame History

SAFETY.md — Modèle de sécurité ETM PowerSync Energy

Décision Patrick Schurig, validée en session 2026-06-08. Ce document est normatif.

Principes

Cinq couches indépendantes + signalisation locale optionnelle. Une couche supérieure peut tomber sans impacter les couches inférieures. Jamais de contournement logiciel d'une couche matérielle.


L0 — Disjoncteur (matériel)

Protège contre : surintensité physique, défaut d'isolement.
Responsable : installateur électricien, norme C15-100.
ETM ne touche pas à cette couche. Elle fonctionne que nymead soit vivant ou non.


L1 — Failsafe natif des bornes EV et charges pilotées

Protège contre : perte de communication borne ↔ nymead.
Ne couvre PAS : perte de communication compteur ↔ nymead (c'est le rôle de L2).
Responsable : configuration installateur sur chaque borne/appareil.

Checklist ETM à la mise en service :

  • Borne EV : configurer le timeout de communication → repli sur courant minimum défini par l'installateur (jamais 0 A si le VE est branché, jamais maximum abonnement).
  • Relais ECS : désactivation si absence de commande > N minutes (selon chauffe-eau).
  • SG-Ready PAC : état 1 (normal, non piloté) si silence > N minutes.

Cette configuration est documentée dans la checklist de déploiement (etm-powersync-deploy), pas ici.


L2 — Watchdog fraîcheur compteur (à implémenter en phase 3c)

Protège contre : compteur hors ligne, plugin gelé, nymead bloqué.
Responsable : code ETM dans EnergyArbitrator.

Comportement

Un QTimer (pas un signal meterChanged) tourne en permanence avec une période de 30 s.
À chaque tick : si QDateTime::currentDateTime() - m_lastMeterUpdate > 90 smode dégradé déclenché.

Choix de conception : QTimer et non signal powerBalanceChanged, car si le compteur est muet, le signal ne fire plus — le watchdog doit rester actif précisément dans ce cas.

Mode dégradé — consignes de repli (Variante B — décision Patrick)

À la transition vers le mode dégradé, les charges pilotées reçoivent une consigne de repli conservatrice : le repli n'INITIE rien, il borne ce qui tourne déjà.

Charge Consigne de repli
EV en charge Clamp au courant minimum borne (maxChargingCurrentMinValue)
EV branché mais pas en charge Inchangé — reste off (off possiblement volontaire : HC/spot à venir)
EV débranché Aucune action
ECS Relais coupé (palier 0, force=true)
SG-Ready PAC État 1 (normal)
Batterie Aucune charge réseau (surplus uniquement, plafonné à 0 si compteur muet)

« Maintenu » ≠ « démarré » : le mode dégradé ne force jamais l'activation d'une charge. La garantie "jamais 0 A si le VE est branché" appartient au failsafe L1 de la borne (timeout communication → courant minimum installateur), pas au repli logiciel.

Chaque consigne porte un decisionReason explicite : "Compteur muet depuis >90 s — consigne de repli (L2 watchdog)".

Suspension de la planification : tant que le mode dégradé est actif, update() exécute la sécurité L4 (position 3, intouchable) puis retourne immédiatement — ni getPlan() ni dispatch. Sinon update() replanifierait sur le cache d'un compteur mort et rallumerait les charges que le watchdog vient de couper, que le tick suivant recouperait 30 s plus tard (oscillation). Le repli est donc appliqué une seule fois à la transition ; les consignes tiennent jusqu'au retour du compteur.

Risque accepté : ~1,4 kW de tirage EV non supervisé pour un VE déjà en charge au moment de la bascule (minimum borne typique ≈ 6 A × 230 V × 1 phase). Ce tirage est atténué par L0 (disjoncteur) et L1 (failsafe borne). Le client est informé (voir Notification ci-dessous).

Limite : la notification part du contrôleur dégradé lui-même (best-effort). L'alerte indépendante de la défaillance du contrôleur est le rôle de L3 (watchdog systemd).

Notification client (dans ce repo)

  • Une notification nymea EnergyManagerChanged est émise avec un flag degradedMode: true.
  • L'application affiche : "Supervision compteur perdue — charge EV maintenue au minimum".

Alertes externes (hors de ce repo)

Les notifications push/mail/SMS sont déclenchées via l'infra ETM (n8n) sur réception de l'événement nymea. Aucun code de notification externe dans ce plugin.

Sortie du mode dégradé

Dès que le compteur fournit une nouvelle mesure (m_lastMeterUpdate remis à jour) → reprise normale au cycle suivant, degradedMode: false.

Scénario de simulation obligatoire (DoD 3c)

Scénario testMeterSilentFallback dans tests/auto/simulation/ :

  1. Charger branché, surplus PV → charge en cours.
  2. Geler les updates du compteur (mock).
  3. Vérifier qu'après 90 s simulés, les consignes de repli sont émises avec le bon reason et que degradedMode est à true.
  4. Dégeler le compteur → vérifier la reprise normale et degradedMode: false.

L3 — Watchdog systemd sur nymead

Protège contre : crash de nymead, deadlock process, défaillance de L2.
Responsable : etm-powersync-deploy (hors scope de ce repo).
Nymead enregistre sd_notify(WATCHDOG=1) ; systemd le relance si absent > timeout.
À la reprise, L1 garantit la sécurité matérielle pendant le redémarrage.


L4 — Logique signal-driven existante (amont)

Protège contre : surcharge transitoire sur phases.
Responsable : SmartChargingManager::verifyOverloadProtection().

Double déclenchement (code amont vérifié)

verifyOverloadProtection() est déclenchée par deux mécanismes distincts :

  1. Signal temps réel (smartchargingmanager.cpp ligne 127) :

    connect(m_energyManager, &EnergyManager::powerBalanceChanged, this, [this]() {
        verifyOverloadProtection(QDateTime::currentDateTime());
    });
    

    C'est le mécanisme principal : déclenché à chaque nouveau bilan de puissance émis par le compteur, sans attendre le prochain cycle update().

  2. Appel cyclique (smartchargingmanager.cpp ligne 313, dans update()) :

    verifyOverloadProtection(currentDateTime);   // position 3 du cycle
    

    Filet périodique — garantit une vérification même si le signal précède la mise à jour des états internes.

Note : en mode simulation, simulationCallUpdate() (ligne 295-299) appelle update() puis verifyOverloadProtection() une seconde fois — double appel intentionnel dans les tests, pas dans la production.

Règle de code ETM

EnergyArbitrator::update() appelle verifyOverloadProtection() en position 3, identique à l'amont. INTERDIT de déplacer cet appel ou de le conditionner. La connexion signal est héritée via le constructeur parent — aucune reconnexion ETM.


Signalisation locale (optionnelle — hors de ce repo)

Protège contre : alarme silencieuse non détectée, exigeant une action humaine immédiate.
Ce plugin émet les événements (degradedMode, alarmes overload, etc.).
La signalisation est une Thing nymea (buzzer GPIO ou canal relais) + une Règle déclenchée sur ces événements — configuration installation, documentée dans etm-powersync-deploy.

Aucun code buzzer/relais dans ce repo. Principe : le moteur émet, la configuration d'installation décide quoi signaler.


Correspondance couches / scénarios de défaillance

Défaillance L0 L1 L2 L3 L4 Signal local
Surintensité réseau optionnel
Borne perd le réseau IP
Compteur hors ligne > 90 s optionnel
Crash nymead
Bug dans update()
Optimiseur externe mort repli rule-based (AGENTS §6)