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>
184 lines
7.9 KiB
Markdown
184 lines
7.9 KiB
Markdown
# 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)
|
||
|
||
- 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) :
|
||
```cpp
|
||
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()`) :
|
||
```cpp
|
||
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) |
|