[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>
This commit is contained in:
Patrick Schurig 2026-06-09 23:25:09 +02:00
parent 093fa09b5e
commit b06ac15714
3 changed files with 58 additions and 15 deletions

View File

@ -61,7 +61,7 @@ repli **conservatrice** : le repli n'INITIE rien, il borne ce qui tourne déjà.
| 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) |
| 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.

View File

@ -4,6 +4,7 @@
#include "energyarbitrator.h"
#include "adapters/evadapter.h"
#include "adapters/ecsrelayadapter.h"
#include "adapters/sgreadyadapter.h"
#include "scheduler/rulebasedscheduler.h"
#include "types/surpluscontext.h"
#include "types/plan.h"
@ -96,6 +97,18 @@ void EnergyArbitrator::registerEcsAdapter(EcsRelayAdapter *adapter)
qCDebug(dcNymeaEnergy()) << "[EnergyArbitrator] EcsRelayAdapter enregistré:" << adapter->descriptor().label;
}
void EnergyArbitrator::registerSgReadyAdapter(SgReadyAdapter *adapter)
{
const QString id = adapter->descriptor().id;
if (m_sgReadyAdapters.contains(id)) {
qCWarning(dcNymeaEnergy()) << "[EnergyArbitrator] SgReadyAdapter déjà enregistré:" << id;
return;
}
adapter->setParent(this);
m_sgReadyAdapters[id] = adapter;
qCDebug(dcNymeaEnergy()) << "[EnergyArbitrator] SgReadyAdapter enregistré:" << adapter->descriptor().label;
}
void EnergyArbitrator::update(const QDateTime &currentDateTime)
{
qCDebug(dcNymeaEnergy()) << "Updating smart charging";
@ -170,6 +183,10 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const
for (auto it = m_ecsAdapters.constBegin(); it != m_ecsAdapters.constEnd(); ++it)
ctx.loads.append(it.value()->toLoadContext(now));
// --- loads[] : SG-Ready adapters (PAC) ---
for (auto it = m_sgReadyAdapters.constBegin(); it != m_sgReadyAdapters.constEnd(); ++it)
ctx.loads.append(it.value()->toLoadContext(now));
return ctx;
}
@ -191,17 +208,22 @@ void EnergyArbitrator::syncAdapters()
void EnergyArbitrator::applyActionsToAdapters(const Slot &slot, const QDateTime &now)
{
for (const LoadAction &action : slot.actions) {
// EV (Setpoint) : dispatché par adjustEvChargers() amont jusqu'à 3g.
if (action.kind != LoadAction::Stage)
continue;
// L'adaptateur applique, écrête et verrouille — il ne décide pas (règle 2).
if (action.kind == LoadAction::Stage) {
EcsRelayAdapter *adapter = m_ecsAdapters.value(action.loadId);
if (adapter)
adapter->applyAction(action, now);
else
qCWarning(dcNymeaEnergy()) << "[Arbitre] action Stage sans adaptateur ECS:" << action.loadId;
EcsRelayAdapter *adapter = m_ecsAdapters.value(action.loadId);
if (!adapter) {
qCWarning(dcNymeaEnergy()) << "[Arbitre] action Stage sans adaptateur ECS:" << action.loadId;
continue;
} else if (action.kind == LoadAction::State) {
SgReadyAdapter *adapter = m_sgReadyAdapters.value(action.loadId);
if (adapter)
adapter->applyAction(action, now);
else
qCWarning(dcNymeaEnergy()) << "[Arbitre] action State sans adaptateur SG-Ready:" << action.loadId;
}
// L'adaptateur applique, écrête et verrouille (anti-rebond) — il ne décide pas.
adapter->applyAction(action, now);
// EV (Setpoint) : dispatché par adjustEvChargers() amont jusqu'à 3g.
}
}
@ -266,6 +288,18 @@ void EnergyArbitrator::applyDegradedMode(const QDateTime &now)
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.
// SG-Ready (PAC) : repli en état 2 (NORMAL — mains off), JAMAIS état 1 (blocage).
// Sous compteur muet on cesse de piloter : la PAC chauffe selon son propre thermostat
// (la bloquer = maison qui ne chauffe plus sans raison visible). force=true → bypass minStateHold.
for (SgReadyAdapter *adapter : m_sgReadyAdapters) {
LoadAction la;
la.loadId = adapter->descriptor().id;
la.kind = LoadAction::State;
la.state = 2;
la.force = true;
la.reason = reason;
adapter->applyAction(la, now);
}
// Batterie (aucune charge réseau) : repli ajouté avec son adaptateur (3f).
}

View File

@ -13,6 +13,7 @@ class QTimer;
class EvAdapter;
class EcsRelayAdapter;
class SgReadyAdapter;
class RuleBasedScheduler;
/*!
@ -84,6 +85,13 @@ public:
*/
void registerEcsAdapter(EcsRelayAdapter *adapter);
/*!
* \brief Enregistre un SgReadyAdapter (PAC) pour inclusion dans le contexte et le dispatch.
* \param adapter Adaptateur à enregistrer ; son \c descriptor().id doit être unique.
* Adopté comme enfant Qt de l'arbitre. Appelé par le test (setup) ou la config production.
*/
void registerSgReadyAdapter(SgReadyAdapter *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.
@ -125,7 +133,7 @@ protected:
* 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
* 5. applyActionsToAdapters() (ECS Stage + SG-Ready State) + adjustEvChargers() (EV) dispatch
*
* \param currentDateTime Instant courant (timer ou simulation).
*/
@ -189,8 +197,9 @@ private:
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*.
QHash<QString, EvAdapter *> m_adapters; //!< loadId (ThingId string) → EvAdapter*.
QHash<QString, EcsRelayAdapter *> m_ecsAdapters; //!< loadId → EcsRelayAdapter*.
QHash<QString, SgReadyAdapter *> m_sgReadyAdapters; //!< loadId → SgReadyAdapter* (PAC).
// --- L2 watchdog fraîcheur compteur (SAFETY.md §L2) ---
QTimer *m_meterWatchdog = nullptr; //!< Tick 30 s, indépendant des signaux compteur.