[3c-5] watchdog L2 : QTimer fraîcheur compteur + mode dégradé conservateur
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>
This commit is contained in:
parent
0615e5f39d
commit
312a2484ae
@ -100,7 +100,11 @@ pour chaque LoadContext lc où lc.adapter == "relay-stages" :
|
||||
- Mock JSON : ThingClass `mockPowerSwitch` (état `power` bool, état `currentPower` double)
|
||||
- `energytestbase.h` : `mockPowerSwitchThingClassId`
|
||||
- `testEcsSurplusPV` : cas normal + cas "ECS déjà au palier 1, surplus stable → Y RESTE" (anti-clignotement)
|
||||
- `testMeterSilentFallback` : compteur muet 90 s → mode dégradé → ECS off
|
||||
- `testMeterSilentFallback` : compteur muet 90 s → mode dégradé → ECS off ; **vérifier que
|
||||
l'ECS RESTE à 0 sur plusieurs cycles `update()` pendant le silence** (planification
|
||||
suspendue, pas de rallumage sur cache mort) ; dégel → reprise normale + `degradedMode=false`.
|
||||
Repli EV : seul un VE *déjà en charge* est clampé au minimum (pas d'activation forcée).
|
||||
Seam de test : piloter `onMeterWatchdogTick()` / `m_lastMeterUpdate` sans 90 s d'horloge réelle.
|
||||
|
||||
### Arithmétique du budget (corrections A + B)
|
||||
|
||||
|
||||
@ -52,20 +52,36 @@ 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 :
|
||||
À 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 (pluggedIn) | Courant minimum autorisé par la borne (`maxChargingCurrentMinValue`) |
|
||||
| EV (non pluggedIn) | Désactivé |
|
||||
| ECS | Relais coupé (palier 0) |
|
||||
| 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)"`.
|
||||
|
||||
**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).
|
||||
**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).
|
||||
|
||||
@ -8,9 +8,20 @@
|
||||
#include "types/surpluscontext.h"
|
||||
#include "types/plan.h"
|
||||
#include "../rootmeter.h"
|
||||
#include "../evcharger.h"
|
||||
|
||||
#include "plugininfo.h"
|
||||
|
||||
#include <energymanager.h>
|
||||
#include <QTimer>
|
||||
|
||||
namespace {
|
||||
//! Période du watchdog L2 (SAFETY.md §L2) : tick indépendant des signaux compteur.
|
||||
constexpr int MeterWatchdogPeriodMs = 30 * 1000; // 30 s
|
||||
//! Seuil de silence compteur au-delà duquel le mode dégradé L2 est déclenché.
|
||||
constexpr int MeterSilenceThresholdS = 90; // 90 s
|
||||
}
|
||||
|
||||
EnergyArbitrator::EnergyArbitrator(EnergyManager *em, ThingManager *tm,
|
||||
SpotMarketManager *sm,
|
||||
EnergyManagerConfiguration *conf,
|
||||
@ -18,6 +29,23 @@ EnergyArbitrator::EnergyArbitrator(EnergyManager *em, ThingManager *tm,
|
||||
: SmartChargingManager(em, tm, sm, conf, parent)
|
||||
, m_scheduler(new RuleBasedScheduler(this, this))
|
||||
{
|
||||
// --- L2 : watchdog fraîcheur compteur (SAFETY.md §L2) ---
|
||||
// Fraîcheur picotée sur powerBalanceChanged (en plus de la connexion amont L4) ;
|
||||
// grâce au démarrage : on initialise le timestamp pour éviter un dégradé immédiat.
|
||||
m_lastMeterUpdate = QDateTime::currentDateTime();
|
||||
connect(em, &EnergyManager::powerBalanceChanged, this, [this]() {
|
||||
m_lastMeterUpdate = QDateTime::currentDateTime();
|
||||
if (m_degradedMode) {
|
||||
qCInfo(dcNymeaEnergy()) << "[Arbitre] Compteur de nouveau actif — sortie du mode dégradé L2.";
|
||||
m_degradedMode = false;
|
||||
}
|
||||
});
|
||||
// QTimer (et non signal) : doit rester actif quand le compteur est muet.
|
||||
m_meterWatchdog = new QTimer(this);
|
||||
m_meterWatchdog->setInterval(MeterWatchdogPeriodMs);
|
||||
connect(m_meterWatchdog, &QTimer::timeout, this, &EnergyArbitrator::onMeterWatchdogTick);
|
||||
m_meterWatchdog->start();
|
||||
|
||||
qCDebug(dcNymeaEnergy()) << "[EnergyArbitrator] Arbitre ETM initialisé.";
|
||||
}
|
||||
|
||||
@ -80,6 +108,15 @@ void EnergyArbitrator::update(const QDateTime ¤tDateTime)
|
||||
verifyOverloadProtection(currentDateTime);
|
||||
verifyOverloadProtectionRecovery(currentDateTime);
|
||||
|
||||
// Mode dégradé L2 : la sécurité (L4 ci-dessus) reste active, mais on SUSPEND la
|
||||
// planification et le dispatch. Replanifier sur le cache d'un compteur mort
|
||||
// rallumerait les charges que le watchdog vient de couper → oscillation. Les
|
||||
// consignes de repli (posées à la transition) tiennent jusqu'au retour du compteur.
|
||||
if (m_degradedMode) {
|
||||
qCDebug(dcNymeaEnergy()) << "[Arbitre] Mode dégradé L2 actif — planification suspendue.";
|
||||
return;
|
||||
}
|
||||
|
||||
// ETM-only : sync adapters + proxy planification → log [Arbitre]
|
||||
// getPlan() appelle planSpotMarketCharging() + planSurplusCharging() (position 5-6 amont).
|
||||
syncAdapters();
|
||||
@ -164,3 +201,52 @@ void EnergyArbitrator::applyActionsToAdapters(const Slot &slot)
|
||||
adapter->applyAction(action);
|
||||
}
|
||||
}
|
||||
|
||||
void EnergyArbitrator::onMeterWatchdogTick()
|
||||
{
|
||||
if (!m_lastMeterUpdate.isValid())
|
||||
return; // Aucune mesure reçue (démarrage) — pas de dégradé (invariant root meter absent).
|
||||
|
||||
const QDateTime now = QDateTime::currentDateTime();
|
||||
const qint64 silentS = m_lastMeterUpdate.secsTo(now);
|
||||
if (silentS <= MeterSilenceThresholdS)
|
||||
return;
|
||||
|
||||
if (m_degradedMode)
|
||||
return; // Déjà en repli — les consignes tiennent, pas de ré-émission (anti-oscillation).
|
||||
|
||||
qCWarning(dcNymeaEnergy()) << "[Arbitre] Compteur muet depuis" << silentS
|
||||
<< "s (>" << MeterSilenceThresholdS << "s) — mode dégradé L2.";
|
||||
applyDegradedMode(now);
|
||||
}
|
||||
|
||||
void EnergyArbitrator::applyDegradedMode(const QDateTime &now)
|
||||
{
|
||||
m_degradedMode = true;
|
||||
const QString reason =
|
||||
QStringLiteral("Compteur muet depuis >90 s — consigne de repli (L2 watchdog)");
|
||||
|
||||
// ECS : tous paliers coupés (stage 0), force=true → bypass des verrous anti-rebond.
|
||||
for (EcsRelayAdapter *adapter : m_ecsAdapters) {
|
||||
LoadAction la;
|
||||
la.loadId = adapter->descriptor().id;
|
||||
la.kind = LoadAction::Stage;
|
||||
la.stage = 0;
|
||||
la.force = true;
|
||||
la.reason = reason;
|
||||
adapter->applyAction(la);
|
||||
}
|
||||
|
||||
// EV : repli CONSERVATEUR — n'initie aucune charge. On clampe seulement une charge
|
||||
// DÉJÀ en cours au courant minimum (force=true, bypass lock). Une borne branchée mais
|
||||
// non chargeante reste off (off volontaire possible : HC/spot à venir) ; débranchée →
|
||||
// aucune action. La garantie "jamais 0 A si branché" relève du failsafe L1 de la borne.
|
||||
for (auto it = internalEvChargers().constBegin(); it != internalEvChargers().constEnd(); ++it) {
|
||||
EvCharger *ev = it.value();
|
||||
if (ev->available() && ev->charging())
|
||||
ev->setMaxChargingCurrent(ev->maxChargingCurrentMinValue(), now, true);
|
||||
}
|
||||
|
||||
// SG-Ready (état 1) / Batterie (aucune charge réseau) : repli ajouté avec leurs
|
||||
// adaptateurs (3e/3f). Le flag degradedMode + notification client arrivent en 3c-6.
|
||||
}
|
||||
|
||||
@ -7,6 +7,10 @@
|
||||
#include "types/surpluscontext.h"
|
||||
#include "types/plan.h"
|
||||
|
||||
#include <QDateTime>
|
||||
|
||||
class QTimer;
|
||||
|
||||
class EvAdapter;
|
||||
class EcsRelayAdapter;
|
||||
class RuleBasedScheduler;
|
||||
@ -88,6 +92,7 @@ protected:
|
||||
* 1. updateManualSoCsWithoutMeter()
|
||||
* 2. prepareInformation()
|
||||
* 3. verifyOverloadProtection() + verifyOverloadProtectionRecovery()
|
||||
* (si \c m_degradedMode actif : retour immédiat — planification/dispatch suspendus, L2)
|
||||
* 4. m_scheduler->getPlan() → log des decisionReason
|
||||
* 5. applyActionsToAdapters() (ECS Stage) + adjustEvChargers() (EV) → dispatch matériel
|
||||
*
|
||||
@ -124,7 +129,38 @@ private:
|
||||
*/
|
||||
void applyActionsToAdapters(const Slot &slot);
|
||||
|
||||
/*!
|
||||
* \brief Tick du watchdog L2 (SAFETY.md §L2) — piloté par \c m_meterWatchdog (QTimer 30 s).
|
||||
*
|
||||
* Si \c QDateTime::currentDateTime() − \c m_lastMeterUpdate dépasse 90 s, déclenche
|
||||
* \c applyDegradedMode(). Indépendant des signaux compteur : reste actif précisément
|
||||
* quand le compteur est muet (le signal \c powerBalanceChanged ne fire plus).
|
||||
* \note Tant que \c m_lastMeterUpdate est invalide (aucune mesure reçue depuis le
|
||||
* démarrage), aucun mode dégradé n'est déclenché (invariant root meter absent).
|
||||
*/
|
||||
void onMeterWatchdogTick();
|
||||
|
||||
/*!
|
||||
* \brief Applique les consignes de repli L2 (SAFETY.md §L2, Variante B).
|
||||
*
|
||||
* Repli CONSERVATEUR (n'initie aucune charge) : ECS → palier 0 \c force=true (bypass
|
||||
* anti-rebond) ; EV en charge → clamp courant minimum borne ; EV branché non chargeant
|
||||
* ou débranché → aucune action (planification en cours respectée ; "jamais 0 A si
|
||||
* branché" relève du failsafe L1). SG-Ready/Batterie : repli ajouté à l'arrivée de
|
||||
* leurs adaptateurs (3e/3f). Positionne \c m_degradedMode.
|
||||
*
|
||||
* \note Appelé une seule fois à la TRANSITION vers le mode dégradé. Ensuite \c update()
|
||||
* suspend la planification, donc les consignes tiennent sans ré-émission par tick.
|
||||
* \param now Instant courant (locks anti-rebond des bornes EV).
|
||||
*/
|
||||
void applyDegradedMode(const QDateTime &now);
|
||||
|
||||
RuleBasedScheduler *m_scheduler = nullptr;
|
||||
QHash<QString, EvAdapter *> m_adapters; //!< loadId (ThingId string) → EvAdapter*.
|
||||
QHash<QString, EcsRelayAdapter *> m_ecsAdapters; //!< loadId → EcsRelayAdapter*.
|
||||
|
||||
// --- L2 watchdog fraîcheur compteur (SAFETY.md §L2) ---
|
||||
QTimer *m_meterWatchdog = nullptr; //!< Tick 30 s, indépendant des signaux compteur.
|
||||
QDateTime m_lastMeterUpdate; //!< Horodatage du dernier powerBalanceChanged.
|
||||
bool m_degradedMode = false; //!< Vrai si les consignes de repli L2 sont actives.
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user