[3c-6] degradedMode() + notification ChargingSchedulesChanged + invariant zéro-cloud

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>
This commit is contained in:
Patrick Schurig 2026-06-08 17:04:09 +02:00
parent 312a2484ae
commit f71e0405b4
7 changed files with 64 additions and 6 deletions

View File

@ -181,6 +181,12 @@ Règles absolues :
après pilotage.
9. **Aucun composant propriétaire ici** (Héos = repo privé `etm-powersync-optimizer`).
Ce repo doit compiler et tourner seul, GPL pur.
10. **ZÉRO cloud** — aucun appel réseau sortant vers un service distant (ni n8n, ni mail,
ni push tiers). Le système fonctionne sans internet (autoconsommation, local-first).
Toute alerte est **locale** : notification nymea in-app + signalisation physique
(buzzer/relais via règle nymea). Le moteur expose l'état, il ne contacte personne.
Exception : le plugin est CLIENT d'un optimiseur sur socket local/LAN (OPTIMIZER_PROTOCOL,
`unix://` ou `tcp://` du réseau de l'installation) — jamais un service cloud externe.
## RÉPONSES FIGÉES (ne plus poser ces questions)

View File

@ -453,10 +453,18 @@ S'abonner via `JSONRPC.SetNotificationStatus` avec le namespace `"NymeaEnergy"`.
---
### `NymeaEnergy.ChargingSchedulesChanged`
Émis à chaque recalcul du planning (cycle ~1 min).
Émis à chaque recalcul du planning (cycle ~1 min), **et** à chaque transition du mode
dégradé L2 (watchdog fraîcheur compteur).
```json
{ "chargingSchedules": [ ... ] }
{
"chargingSchedules": [ ... ],
"o:degradedMode": false
}
```
- `degradedMode` *(bool, optionnel — [ETM])* : `true` quand le compteur est muet depuis
> 90 s et que les consignes de repli L2 sont actives (planification suspendue, ECS coupé,
EV en charge clampé au minimum). Repasse à `false` au retour du compteur. Champ additif :
les clients antérieurs l'ignorent. Détail : `docs/SAFETY.md` §L2.
---

View File

@ -88,13 +88,35 @@ indépendante de la défaillance du contrôleur est le rôle de L3 (watchdog sys
### Notification client (dans ce repo)
- Une notification nymea `EnergyManagerChanged` est émise avec un flag `degradedMode: true`.
- 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"*.
### Alertes externes (hors de ce repo)
### Limite — détection par fraîcheur uniquement
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.
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é
@ -169,6 +191,10 @@ déclenchée sur ces événements — configuration installation, documentée da
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

View File

@ -38,6 +38,7 @@ EnergyArbitrator::EnergyArbitrator(EnergyManager *em, ThingManager *tm,
if (m_degradedMode) {
qCInfo(dcNymeaEnergy()) << "[Arbitre] Compteur de nouveau actif — sortie du mode dégradé L2.";
m_degradedMode = false;
emit chargingSchedulesChanged(); // pousse degradedMode=false (planif reprend au cycle suivant)
}
});
// QTimer (et non signal) : doit rester actif quand le compteur est muet.
@ -223,6 +224,7 @@ void EnergyArbitrator::onMeterWatchdogTick()
void EnergyArbitrator::applyDegradedMode(const QDateTime &now)
{
m_degradedMode = true;
emit chargingSchedulesChanged(); // pousse degradedMode=true (notification client L2)
const QString reason =
QStringLiteral("Compteur muet depuis >90 s — consigne de repli (L2 watchdog)");

View File

@ -84,6 +84,14 @@ public:
*/
void registerEcsAdapter(EcsRelayAdapter *adapter);
/*!
* \brief Mode dégradé L2 actif (compteur muet > 90 s) override de SmartChargingManager.
* \return \c true tant que les consignes de repli L2 tiennent ; \c false en régime normal.
* \note Exposé dans la notification \c NymeaEnergy.ChargingSchedulesChanged (champ
* \c degradedMode), émise aussi aux transitions de ce flag.
*/
bool degradedMode() const override { return m_degradedMode; }
protected:
/*!
* \brief Boucle principale ETM surcharge SmartChargingManager::update().

View File

@ -197,6 +197,9 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
params.clear();
description = "Emitted whenever the planed charging schedules have changed.";
params.insert("chargingSchedules", QVariantList() << objectRef<ChargingSchedule>());
// [ETM] degradedMode : true quand le watchdog L2 a basculé en repli (compteur muet).
// Optionnel (additif, rétro-compatible) ; émis aussi aux transitions du mode dégradé.
params.insert("o:degradedMode", enumValueName(Bool));
registerNotification("ChargingSchedulesChanged", description, params);
// Charing manager
@ -223,6 +226,7 @@ NymeaEnergyJsonHandler::NymeaEnergyJsonHandler(SpotMarketManager *spotMarketMana
schedules << pack<ChargingSchedule>(schedule);
}
params.insert("chargingSchedules", schedules);
params.insert("degradedMode", m_smartChargingManager->degradedMode()); // [ETM] L2
emit ChargingSchedulesChanged(params);
});

View File

@ -72,6 +72,10 @@ public:
ChargingSchedules chargingSchedules() const;
// [ETM] Mode dégradé L2 (watchdog fraîcheur compteur). Base = false ;
// overridé dans EnergyArbitrator. Exposé pour la notification JSON-RPC.
virtual bool degradedMode() const { return false; }
SpotMarketManager *spotMarketManager() const;
#ifdef ENERGY_SIMULATION