Patrick Schurig c3fedfe36b [3b] décision B + modèle sécurité (AGENTS + SAFETY.md) + Doxygen proxy/inactif
- AGENTS.md : nouvelle entrée "3b révisé — délégation EV à l'amont" (beta hybride
  assumée, ETM réel en 3c, transplantation EV en 3g) ; modèle sécurité L0-L4
  avec double déclenchement verifyOverloadProtection documenté (signal ligne 127 +
  appel cyclique ligne 313 SCM.cpp).
- docs/SAFETY.md : document normatif 5 couches + signalisation locale optionnelle ;
  Variante B confirmée pour le repli L2 (EV au minimum + notification nymea +
  risque 1,4 kW accepté) ; table défaillances/couches corrigée (L1 ne couvre pas
  compteur hors ligne).
- energyarbitrator.cpp update() : commentaire explicitant la correspondance exacte
  avec l'ordre SCM (1-4 parent, ETM entre 4 et 7, planSpot+planSurplus via getPlan).
- rulebasedscheduler.h : Doxygen getPlan() marqué "PROXY AMONT POUR L'EV (beta)".
- evadapter.h : Doxygen applyAction() marqué "Inactif jusqu'à 3g".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 07:41:12 +02:00

168 lines
6.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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**)
Toutes les charges pilotées reçoivent une consigne de repli immédiate :
| Charge | Consigne de repli |
|--------|------------------|
| EV (pluggedIn) | Courant minimum autorisé par la borne (`maxChargingCurrentMinValue`) |
| EV (non pluggedIn) | Désactivé |
| ECS | Relais coupé (palier 0) |
| SG-Ready PAC | État 1 (normal) |
| Batterie | Aucune charge réseau (surplus uniquement, plafonné à 0 si compteur muet) |
Chaque consigne porte un `decisionReason` explicite :
`"Compteur muet depuis >90 s — consigne de repli (L2 watchdog)"`.
**Risque accepté** : ~1,4 kW de tirage EV non supervisé (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) |