Patrick Schurig b06ac15714 [3e-4] arbitre : registerSgReadyAdapter + dispatch State + mode dégradé → état 2
registerSgReadyAdapter + m_sgReadyAdapters ; buildContext inclut les PAC ;
applyActionsToAdapters dispatche kind==State → m_sgReadyAdapters. Mode dégradé L2 :
SG-Ready → état 2 (NORMAL, mains off, force=true), JAMAIS état 1 (blocage). SAFETY.md
table L2 corrigée (état 2, pas 1). Build 0/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 23:25:09 +02:00

9.7 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 2 (normal — mains off, la PAC chauffe selon son thermostat). JAMAIS état 1 (blocage) : bloquer une PAC sous compteur muet = maison qui ne chauffe plus sans raison visible.
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.ChargingSchedulesChanged porte un champ additif degradedMode (bool). Elle est émise aux transitions du mode dégradé (entrée/sortie), en plus des recalculs de planning. Voir INTERFACE.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/ :

  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.

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)