virtual degradedMode() dans SmartChargingManager (base false, [ETM] additif), override EnergyArbitrator. Champ o:degradedMode (additif) dans la notification NymeaEnergy.ChargingSchedulesChanged, émise aussi aux transitions du mode dégradé (planif suspendue → push du flag via emit chargingSchedulesChanged()). INTERFACE.md : champ degradedMode documenté. SAFETY.md : notification réconciliée (ChargingSchedulesChanged, pas EnergyManagerChanged) + limite "valeur figée non détectée". Correction ZÉRO CLOUD : suppression de la section "Alertes externes" / mécanisme n8n, remplacée par une signalisation 100% locale (notification nymea in-app + buzzer/relais via règle nymea, aucun canal réseau sortant). Invariant 10 "ZÉRO cloud" gravé dans AGENTS.md. Build 0 erreur / 0 warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.6 KiB
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 s
→ mode 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)
- La notification JSON-RPC
NymeaEnergy.ChargingSchedulesChangedporte un champ additifdegradedMode(bool). Elle est émise aux transitions du mode dégradé (entrée/sortie), en plus des recalculs de planning. VoirINTERFACE.md. - L'application affiche : "Supervision compteur perdue — charge EV maintenue au minimum".
Limite — détection par fraîcheur uniquement
Le watchdog L2 détecte l'absence de mise à jour du compteur (plus de signal
powerBalanceChanged depuis > 90 s), pas une valeur figée. Un compteur qui continue
d'émettre une valeur strictement constante (capteur bloqué mais lien vivant) n'est pas
détecté par cette couche — m_lastMeterUpdate reste frais. Détecter une valeur figée
(variance nulle sur fenêtre) est hors scope L2 ; le cas est couvert au niveau matériel/L0
et par la supervision externe.
Signalisation locale (zéro cloud)
ETM PowerSync est 100 % autonome, zéro cloud : aucune alerte ne sort vers un service
distant (ni n8n, ni mail, ni push tiers). Le système est conçu pour fonctionner sans
internet (argument produit : autoconsommation, local-first). Le degradedMode est
signalé par deux canaux strictement locaux :
- Notification nymea in-app (déjà implémentée : champ
degradedMode) — canal principal vers le client connecté à l'application. - Signal sonore local optionnel (buzzer GPIO ou canal relais) piloté par une Règle
nymea déclenchée sur
degradedMode— pour le client sur place, sans application. Aucun code buzzer dans ce repo : le moteur expose l'état, la signalisation est une Thing nymea + une règle (configuration d'installation, cf.## Signalisation locale).
Aucun canal sortant réseau. Voir l'invariant « ZÉRO cloud » dans AGENTS.md.
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/ :
- Charger branché, surplus PV → charge en cours.
- Geler les updates du compteur (mock).
- Vérifier qu'après 90 s simulés, les consignes de repli sont émises avec le bon
reasonet quedegradedModeest àtrue. - 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 :
-
Signal temps réel (
smartchargingmanager.cppligne 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(). -
Appel cyclique (
smartchargingmanager.cppligne 313, dansupdate()) :verifyOverloadProtection(currentDateTime); // position 3 du cycleFilet 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.
Zéro cloud : toute la signalisation est locale (notification nymea in-app +
signalisation physique). Aucun appel réseau sortant vers un service distant — le système
fonctionne sans internet. Invariant gravé dans AGENTS.md.
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) |