[wip] 3c morceaux 0-2 compilés + plan 3c validé dans AGENTS.md
Morceaux 0-2 implémentés et compilés (0 erreur / 0 warning) : - M0 : LoadAction.force=false (bypass verrous anti-rebond sécurité) - M1 : EcsRelayAdapter (.h+.cpp) — N paliers powerswitch, anti-rebond, etm.pri - M2 : buildContext() — SurplusMeter brut, loads EV+ECS, registerEcsAdapter() AGENTS.md : section PLAN 3C ajoutée avec corrections A+B intégrées. Corrections A (déduction EV unique dans scheduler) et B (recrédit conso propre anti-clignotement) documentées avant implémentation morceau 3. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
19951a1e3e
commit
7709057335
89
AGENTS.md
89
AGENTS.md
@ -17,6 +17,7 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie).
|
||||
| 2 — design arbitre validé | ✅ FAITE | `074fa71` |
|
||||
| 3a — structs protocole + interfaces | ✅ FAITE | `4ae1939` |
|
||||
| 3b — EnergyArbitrator + scheduler + adapter | ✅ FAITE — iso-fonctionnalité prouvée | `5f49e4c`, `d8ebd65`, `[3b-iv]` |
|
||||
| 3c — EcsRelayAdapter + waterfall ECS | 🔄 EN COURS | (wip) |
|
||||
|
||||
**Détail 3b** :
|
||||
- `EnergyArbitrator : public SmartChargingManager` — justification dans `## DÉCISIONS DE DESIGN`
|
||||
@ -28,11 +29,17 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie).
|
||||
- Tests charging : 57 lignes décisions identiques, diff = 0 ; 46/46 PASS ref ET ETM
|
||||
- [Arbitre] présents avec raisons françaises pour les 4 cas (idle, surplus PV, aWATTar, deadline)
|
||||
|
||||
**PROCHAINE ACTION — 3c** :
|
||||
- `EcsRelayAdapter` (paliers 0/1/2) — premier adaptateur non-EV, premier `applyAction()` vivant
|
||||
- Pipeline ETM waterfall (budget surplus → ECS, déduction `addedPower` EV) dans `EnergyArbitrator::update()`
|
||||
- Watchdog L2 : `QTimer` 30 s, mode dégradé variante B, notification `degradedMode` (cf. SAFETY.md §L2)
|
||||
- Scénarios simulation : `testEcsSurplusPV` (chauffe-eau sur surplus PV) + `testMeterSilentFallback` (compteur muet → repli)
|
||||
**Détail 3c (morceaux déjà compilés, 0 erreur / 0 warning)** :
|
||||
- **Morceau 0** — `LoadAction.force=false` (bypass verrous sécurité) ✅
|
||||
- **Morceau 1** — `EcsRelayAdapter` (.h + .cpp) : pilote N Things powerswitch,
|
||||
`applyRelayStage()`, verrous `minOnS/minOffS`, bypass si `force==true` ✅
|
||||
- Enregistrement explicite via `EnergyArbitrator::registerEcsAdapter()` (tests + config)
|
||||
- **Morceau 2** — `buildContext()` : `SurplusMeter` brut (`exportW = max(0, -meter->currentPower())`),
|
||||
`loads[]` EV + ECS, `SurpusPv` déféré 3d ✅
|
||||
|
||||
**PROCHAINE ACTION — suite 3c** :
|
||||
- Morceau 3 : waterfall ECS dans `RuleBasedScheduler::getPlan()` (voir PLAN 3C ci-dessous)
|
||||
- Morceaux 4-7 : voir PLAN 3C
|
||||
|
||||
**Remotes git** :
|
||||
- `origin` (`https://git.etm-powersync.fr/...`) = remote de travail — push normal
|
||||
@ -41,6 +48,78 @@ vers un gestionnaire d'énergie complet (EV, ECS, PAC SG-Ready, batterie).
|
||||
|
||||
---
|
||||
|
||||
## PLAN 3C (validé, morceaux 0-2 déjà compilés)
|
||||
|
||||
Plan approuvé par Patrick. Corrections A (double déduction EV) et B (anti-clignotement ECS)
|
||||
**intégrées** dans le design ci-dessous.
|
||||
|
||||
### Morceaux déjà compilés (0 erreur/warning)
|
||||
|
||||
| # | Fichier(s) | Ce qui a été fait |
|
||||
|---|-----------|-------------------|
|
||||
| 0 | `types/loadaction.h` | `bool force = false` — bypass verrous sécurité |
|
||||
| 1 | `adapters/ecsrelayadapter.h/.cpp` | Adaptateur N-paliers powerswitch, anti-rebond, `applyRelayStage()`, `etm.pri` |
|
||||
| 2 | `energyarbitrator.h/.cpp` | `buildContext()` : `SurplusMeter` brut, `loads[]` EV+ECS, `registerEcsAdapter()`, `m_ecsAdapters` |
|
||||
|
||||
### Morceaux à venir
|
||||
|
||||
**3 — Waterfall ECS dans `RuleBasedScheduler::getPlan()`**
|
||||
|
||||
Après la boucle EV proxy, ajouter :
|
||||
|
||||
```
|
||||
// Déduction unique (correction A) — ctx.meter.exportW = mesure brute
|
||||
evReservedW = Σ EV en charge dans slot.actions : max(0, commandedA×phases×230 − ev->currentPower())
|
||||
remainingSurplusW = max(0, ctx.meter.exportW − evReservedW)
|
||||
|
||||
// Tri loads ECS par priorité DESC (200 = servi en premier)
|
||||
pour chaque LoadContext lc où lc.adapter == "relay-stages" :
|
||||
budgetCharge = remainingSurplusW + lc.telemetry.currentPowerW // correction B anti-clignotement
|
||||
bestStage = palier le plus haut dont stages[i] ≤ budgetCharge
|
||||
reason = "Surplus PV — ECS palier N (W)" ou "Surplus insuffisant — ECS éteint"
|
||||
remainingSurplusW = remainingSurplusW + lc.telemetry.currentPowerW − stages[bestStage]
|
||||
// Grid funding : dormant jusqu'à 3f (commente, n'implémente pas)
|
||||
```
|
||||
|
||||
**4 — `syncAdapters()` extension + `applyActionsToAdapters(Slot)` dans `update()`**
|
||||
- `syncAdapters()` : commentaire "découverte ECS via interface 'ecsrelay' déférée 3g"
|
||||
- `applyActionsToAdapters(Slot)` : itère slot.actions, dispatche via `m_ecsAdapters` pour kind==Stage
|
||||
|
||||
**5 — Watchdog L2** (cf. SAFETY.md §L2)
|
||||
- `QTimer m_meterWatchdog` 30 s — picoté sur `powerBalanceChanged` SIGNAL pour `m_lastMeterUpdate`
|
||||
- `onMeterWatchdogTick()` : si `now − m_lastMeterUpdate > 90 s` → `applyDegradedMode()`
|
||||
- `applyDegradedMode()` : ECS stage 0 force=true + EV courant minimum, reason="Compteur muet..."
|
||||
|
||||
**6 — `degradedMode()` + notification + INTERFACE.md**
|
||||
- `virtual bool degradedMode() const` dans `SmartChargingManager` (retourne false, `// [ETM]`)
|
||||
- Override dans `EnergyArbitrator`
|
||||
- Champ `"degradedMode": bool` dans `ChargingSchedulesChanged` (additif, rétro-compatible)
|
||||
- Mise à jour `docs/INTERFACE.md` + limite dans `docs/SAFETY.md` : "valeur strictement constante non détectée"
|
||||
|
||||
**7 — Mock powerswitch + tests**
|
||||
- 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
|
||||
|
||||
### Arithmétique du budget (corrections A + B)
|
||||
|
||||
**Correction A — déduction EV unique, dans le scheduler** :
|
||||
```
|
||||
exportW dans ctx.meter = mesure brute (invariant 8, protocole §5)
|
||||
evReservedW = déduction dans getPlan() APRÈS proxy EV, avec m_chargingActions fraîches
|
||||
Pas de déduction dans buildContext().
|
||||
```
|
||||
Exemple : PV 9 kW, EV stable 7360 W, export mesuré 1140 W → evReservedW=0, budget ECS=1140 W ✓
|
||||
|
||||
**Correction B — anti-clignotement par recrédit de la conso actuelle** :
|
||||
```
|
||||
budgetCharge = remainingSurplusW + lc.telemetry.currentPowerW
|
||||
```
|
||||
Identique à SCM ~l.1245 pour l'EV. Sans ce recrédit : ECS palier 1 → export chute → palier 0 → oscillation.
|
||||
|
||||
---
|
||||
|
||||
> ⚠️ Tout plan antérieur mentionnant « créer etm/ avec PowerSyncClient et
|
||||
> StaticHcHpProvider comme première étape » ou « injecter l'optimiseur dans
|
||||
> SmartChargingManager » est **INVALIDE et ABANDONNÉ**. Ne pas le reprendre,
|
||||
|
||||
191
energyplugin/etm/adapters/ecsrelayadapter.cpp
Normal file
191
energyplugin/etm/adapters/ecsrelayadapter.cpp
Normal file
@ -0,0 +1,191 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||
|
||||
#include "ecsrelayadapter.h"
|
||||
#include "plugininfo.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <integrations/thingmanager.h>
|
||||
#include <integrations/thing.h>
|
||||
#include <types/action.h>
|
||||
#include <types/param.h>
|
||||
|
||||
EcsRelayAdapter::EcsRelayAdapter(ThingManager *thingManager,
|
||||
const QString &id,
|
||||
const QString &label,
|
||||
const QList<int> &stages,
|
||||
const QList<QList<QString>> &relayMapping,
|
||||
int minOnS,
|
||||
int minOffS,
|
||||
int priority,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_thingManager(thingManager)
|
||||
, m_id(id)
|
||||
, m_label(label)
|
||||
, m_stages(stages)
|
||||
, m_relayMapping(relayMapping)
|
||||
, m_minOnS(minOnS)
|
||||
, m_minOffS(minOffS)
|
||||
, m_priority(priority)
|
||||
{
|
||||
Q_ASSERT(!m_stages.isEmpty() && m_stages.first() == 0);
|
||||
Q_ASSERT(m_relayMapping.size() == m_stages.size());
|
||||
}
|
||||
|
||||
LoadDescriptor EcsRelayAdapter::descriptor() const
|
||||
{
|
||||
LoadDescriptor d;
|
||||
d.id = m_id;
|
||||
d.label = m_label;
|
||||
d.adapter = QStringLiteral("relay-stages");
|
||||
d.priority = m_priority;
|
||||
|
||||
d.declared.stages = m_stages;
|
||||
d.limits.minOnS = m_minOnS;
|
||||
d.limits.minOffS = m_minOffS;
|
||||
|
||||
d.supportedKinds = { LoadAction::Stage };
|
||||
return d;
|
||||
}
|
||||
|
||||
LoadTelemetry EcsRelayAdapter::telemetry() const
|
||||
{
|
||||
LoadTelemetry t;
|
||||
t.available = true;
|
||||
t.lastActionAt = m_lastActionAt;
|
||||
|
||||
// Puissance mesurée = somme des relais actifs pour le stage courant
|
||||
double power = 0;
|
||||
const QList<QString> &activeRelays = m_currentStage < m_relayMapping.size()
|
||||
? m_relayMapping.at(m_currentStage)
|
||||
: QList<QString>();
|
||||
for (const QString &thingId : activeRelays) {
|
||||
Thing *t2 = m_thingManager->findConfiguredThing(ThingId(thingId));
|
||||
if (t2)
|
||||
power += t2->stateValue("currentPower").toDouble();
|
||||
}
|
||||
// Si pas de powermetering dans le mock, on estime depuis les stages déclarés
|
||||
if (power == 0 && m_currentStage > 0 && m_currentStage < m_stages.size())
|
||||
power = m_stages.at(m_currentStage);
|
||||
|
||||
t.currentPowerW = power;
|
||||
return t;
|
||||
}
|
||||
|
||||
LoadContext EcsRelayAdapter::toLoadContext() const
|
||||
{
|
||||
LoadContext ctx;
|
||||
ctx.id = m_id;
|
||||
ctx.adapter = QStringLiteral("relay-stages");
|
||||
ctx.label = m_label;
|
||||
ctx.priority = m_priority;
|
||||
ctx.declared = descriptor().declared;
|
||||
ctx.limits = descriptor().limits;
|
||||
|
||||
const LoadTelemetry tel = telemetry();
|
||||
ctx.telemetry.currentPowerW = tel.currentPowerW;
|
||||
ctx.telemetry.stage = m_currentStage;
|
||||
ctx.telemetry.lastSwitch = m_lastSwitch;
|
||||
return ctx;
|
||||
}
|
||||
|
||||
LoadAction EcsRelayAdapter::applyAction(const LoadAction &action)
|
||||
{
|
||||
if (action.kind != LoadAction::Stage)
|
||||
return action;
|
||||
|
||||
if (action.reason.isEmpty()) {
|
||||
qCWarning(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
|
||||
<< "— LoadAction sans reason rejetée.";
|
||||
return action;
|
||||
}
|
||||
|
||||
const int newStage = qBound(0, action.stage, m_stages.size() - 1);
|
||||
|
||||
if (newStage == m_currentStage)
|
||||
return action; // Aucun changement → idempotent
|
||||
|
||||
// Verrous anti-rebond — bypassés si force == true (L2 watchdog)
|
||||
if (!action.force && lockActive(newStage)) {
|
||||
qCDebug(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
|
||||
<< "— verrou anti-rebond actif, stage" << newStage << "ignoré.";
|
||||
return action;
|
||||
}
|
||||
|
||||
qCDebug(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
|
||||
<< "→ stage" << newStage
|
||||
<< "(" << (m_currentStage < m_stages.size() ? m_stages.at(m_currentStage) : 0) << "W"
|
||||
<< "→" << m_stages.at(newStage) << "W)"
|
||||
<< "|" << action.reason;
|
||||
|
||||
applyRelayStage(newStage);
|
||||
|
||||
m_currentStage = newStage;
|
||||
m_lastSwitch = QDateTime::currentDateTime();
|
||||
m_lastActionAt = m_lastSwitch;
|
||||
|
||||
LoadAction applied = action;
|
||||
applied.stage = newStage;
|
||||
applied.estimatedPowerW = m_stages.at(newStage);
|
||||
return applied;
|
||||
}
|
||||
|
||||
// ---- privé ---------------------------------------------------------------
|
||||
|
||||
bool EcsRelayAdapter::lockActive(int newStage) const
|
||||
{
|
||||
if (!m_lastSwitch.isValid())
|
||||
return false;
|
||||
|
||||
const int elapsed = static_cast<int>(m_lastSwitch.secsTo(QDateTime::currentDateTime()));
|
||||
|
||||
if (newStage > m_currentStage) {
|
||||
// Passage à un palier supérieur : minOffS si l'on quitte l'état OFF (stage 0→n)
|
||||
// ou simplement le délai de stabilisation
|
||||
if (m_currentStage == 0 && elapsed < m_minOffS)
|
||||
return true;
|
||||
} else {
|
||||
// Réduction de palier : minOnS depuis le dernier ON
|
||||
if (m_currentStage > 0 && elapsed < m_minOnS)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void EcsRelayAdapter::applyRelayStage(int stage)
|
||||
{
|
||||
// Ensemble des relais ON pour le nouveau stage
|
||||
const QSet<QString> wantOn = [&]() {
|
||||
QSet<QString> s;
|
||||
if (stage < m_relayMapping.size())
|
||||
for (const QString &id : m_relayMapping.at(stage))
|
||||
s.insert(id);
|
||||
return s;
|
||||
}();
|
||||
|
||||
// Union de tous les relais connus
|
||||
QSet<QString> allRelays;
|
||||
for (const auto &list : m_relayMapping)
|
||||
for (const QString &id : list)
|
||||
allRelays.insert(id);
|
||||
|
||||
for (const QString &thingId : allRelays) {
|
||||
Thing *relay = m_thingManager->findConfiguredThing(ThingId(thingId));
|
||||
if (!relay) {
|
||||
qCWarning(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
|
||||
<< "— relais non trouvé:" << thingId;
|
||||
continue;
|
||||
}
|
||||
const bool targetOn = wantOn.contains(thingId);
|
||||
StateType powerStateType = relay->thingClass().stateTypes().findByName("power");
|
||||
if (!powerStateType.id().isNull()) {
|
||||
Action powerAction(powerStateType.id(), relay->id(), Action::TriggeredByRule);
|
||||
powerAction.setParams(ParamList() << Param(powerStateType.id(), targetOn));
|
||||
m_thingManager->executeAction(powerAction);
|
||||
} else {
|
||||
// Fallback mock : setStateValue direct (Things sans actionType "power")
|
||||
relay->setStateValue("power", targetOn);
|
||||
}
|
||||
}
|
||||
}
|
||||
108
energyplugin/etm/adapters/ecsrelayadapter.h
Normal file
108
energyplugin/etm/adapters/ecsrelayadapter.h
Normal file
@ -0,0 +1,108 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
#include <QDateTime>
|
||||
#include <QList>
|
||||
#include <QString>
|
||||
#include "iloadadapter.h"
|
||||
|
||||
class Thing;
|
||||
class ThingManager;
|
||||
|
||||
/*!
|
||||
* \brief Adaptateur pour chauffe-eau ou tout relais N paliers (interface relay-stages).
|
||||
*
|
||||
* Pilote en production N Things powerswitch nymea : \c m_relayMapping[stage] contient
|
||||
* la liste des ThingIds à mettre ON pour ce palier (les autres sont mis OFF).
|
||||
*
|
||||
* Exemple — chauffe-eau 2400W, 2 résistances Waveshare :
|
||||
* stage 0 : {} → A=OFF, B=OFF
|
||||
* stage 1 : {"thingId-A"} → A=ON, B=OFF (1200 W)
|
||||
* stage 2 : {"thingId-A", "thingId-B"} → A=ON, B=ON (2400 W)
|
||||
*
|
||||
* \invariant applyAction() rejette silencieusement toute action dont \c reason est vide.
|
||||
* \invariant applyAction() applique les verrous anti-rebond \c minOnS / \c minOffS
|
||||
* SAUF si \c action.force == true (réservé L2 watchdog).
|
||||
* \invariant Le stage est écrêté à [0, stages().size()-1] avant envoi matériel.
|
||||
* \invariant Seul le kind Stage est traité ; les autres kinds retournent sans effet.
|
||||
*/
|
||||
class EcsRelayAdapter : public QObject, public ILoadAdapter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/*!
|
||||
* \brief Constructeur.
|
||||
* \param thingManager Gestionnaire nymea pour résoudre les ThingIds en Things.
|
||||
* \param id Identifiant logique de la charge (ThingId de l'objet ECS dans nymea,
|
||||
* ou identifiant arbitraire unique pour le mock).
|
||||
* \param label Nom lisible affiché dans les logs et l'app.
|
||||
* \param stages Puissances en W par palier, index 0 = off : [0, 1200, 2400].
|
||||
* \param relayMapping relayMapping[i] = liste de ThingIds powerswitch ON pour le palier i.
|
||||
* \param minOnS Durée minimale ON (s) — anti-rebond.
|
||||
* \param minOffS Durée minimale OFF (s) — anti-rebond.
|
||||
* \param priority Priorité dans le waterfall (200=deadline, 100=normal, 0=différable).
|
||||
* \param parent Propriétaire Qt.
|
||||
*/
|
||||
explicit EcsRelayAdapter(ThingManager *thingManager,
|
||||
const QString &id,
|
||||
const QString &label,
|
||||
const QList<int> &stages,
|
||||
const QList<QList<QString>> &relayMapping,
|
||||
int minOnS,
|
||||
int minOffS,
|
||||
int priority,
|
||||
QObject *parent = nullptr);
|
||||
|
||||
/*!
|
||||
* \brief Description statique de la charge.
|
||||
* \return LoadDescriptor avec adapter="relay-stages", stages, minOnS/minOffS, priority.
|
||||
*/
|
||||
LoadDescriptor descriptor() const override;
|
||||
|
||||
/*!
|
||||
* \brief Télémétrie runtime : puissance mesurée, stage courant, lastSwitch.
|
||||
* \return LoadTelemetry avec currentPowerW issu de la somme des Things actifs.
|
||||
*/
|
||||
LoadTelemetry telemetry() const override;
|
||||
|
||||
/*!
|
||||
* \brief Construit l'entrée loads[] §5 du SurplusContext.
|
||||
* \return LoadContext incluant declared, limits et télémétrie ECS (stage, currentPowerW, lastSwitch).
|
||||
*/
|
||||
LoadContext toLoadContext() const override;
|
||||
|
||||
/*!
|
||||
* \brief Applique un changement de palier sur les relais.
|
||||
*
|
||||
* \param action LoadAction de kind Stage. Autres kinds : retour sans effet.
|
||||
* \return L'action après écrêtage (stage borné à [0, stages.size()-1]).
|
||||
*
|
||||
* \invariant Si \c action.reason est vide → retour sans effet (log warning).
|
||||
* \invariant Si verrous anti-rebond actifs ET \c action.force == false → retour sans effet (log).
|
||||
* \invariant Si \c action.force == true → bypass verrous (L2 watchdog uniquement).
|
||||
* \invariant Toute modification de stage met à jour \c m_lastSwitch.
|
||||
*/
|
||||
LoadAction applyAction(const LoadAction &action) override;
|
||||
|
||||
/*! \brief Stage courant (0 = off). */
|
||||
int currentStage() const { return m_currentStage; }
|
||||
|
||||
private:
|
||||
bool lockActive(int newStage) const;
|
||||
void applyRelayStage(int stage);
|
||||
|
||||
ThingManager *m_thingManager;
|
||||
QString m_id;
|
||||
QString m_label;
|
||||
QList<int> m_stages; //!< Puissances W par palier, [0]=off.
|
||||
QList<QList<QString>> m_relayMapping; //!< ThingIds ON par palier.
|
||||
int m_minOnS;
|
||||
int m_minOffS;
|
||||
int m_priority;
|
||||
|
||||
int m_currentStage = 0;
|
||||
QDateTime m_lastSwitch; //!< Dernier changement de palier (null = jamais).
|
||||
QDateTime m_lastActionAt;
|
||||
};
|
||||
@ -3,9 +3,11 @@
|
||||
|
||||
#include "energyarbitrator.h"
|
||||
#include "adapters/evadapter.h"
|
||||
#include "adapters/ecsrelayadapter.h"
|
||||
#include "scheduler/rulebasedscheduler.h"
|
||||
#include "types/surpluscontext.h"
|
||||
#include "types/plan.h"
|
||||
#include "../rootmeter.h"
|
||||
|
||||
#include "plugininfo.h"
|
||||
|
||||
@ -51,6 +53,18 @@ RootMeter *EnergyArbitrator::registeredRootMeter() const
|
||||
return internalRootMeter();
|
||||
}
|
||||
|
||||
void EnergyArbitrator::registerEcsAdapter(EcsRelayAdapter *adapter)
|
||||
{
|
||||
const QString id = adapter->descriptor().id;
|
||||
if (m_ecsAdapters.contains(id)) {
|
||||
qCWarning(dcNymeaEnergy()) << "[EnergyArbitrator] EcsRelayAdapter déjà enregistré:" << id;
|
||||
return;
|
||||
}
|
||||
adapter->setParent(this);
|
||||
m_ecsAdapters[id] = adapter;
|
||||
qCDebug(dcNymeaEnergy()) << "[EnergyArbitrator] EcsRelayAdapter enregistré:" << adapter->descriptor().label;
|
||||
}
|
||||
|
||||
void EnergyArbitrator::update(const QDateTime ¤tDateTime)
|
||||
{
|
||||
qCDebug(dcNymeaEnergy()) << "Updating smart charging";
|
||||
@ -88,10 +102,33 @@ void EnergyArbitrator::update(const QDateTime ¤tDateTime)
|
||||
|
||||
SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const
|
||||
{
|
||||
// 3b stub — seul le timestamp est renseigné.
|
||||
// Contexte §5 complet (site/meter/pv/battery/loads) prévu en 3d.
|
||||
SurplusContext ctx;
|
||||
ctx.timestamp = now;
|
||||
|
||||
// --- Compteur principal (AGENTS invariant 8 : mesure brute, aucune déduction) ---
|
||||
RootMeter *meter = internalRootMeter();
|
||||
if (meter) {
|
||||
// currentPower() < 0 → export ; > 0 → import (convention amont SCM l.1141)
|
||||
const double p = meter->currentPower();
|
||||
ctx.meter.importW = qMax(0.0, p);
|
||||
ctx.meter.exportW = qMax(0.0, -p);
|
||||
ctx.meter.perPhaseA = {
|
||||
meter->currentPhaseA(),
|
||||
meter->currentPhaseB(),
|
||||
meter->currentPhaseC()
|
||||
};
|
||||
}
|
||||
// SurplusPv : interface inverter — déféré (remplissage prévu en 3d)
|
||||
// SurplusBattery : déféré 3f
|
||||
|
||||
// --- loads[] : EV adapters ---
|
||||
for (auto it = m_adapters.constBegin(); it != m_adapters.constEnd(); ++it)
|
||||
ctx.loads.append(it.value()->toLoadContext());
|
||||
|
||||
// --- loads[] : ECS relay adapters ---
|
||||
for (auto it = m_ecsAdapters.constBegin(); it != m_ecsAdapters.constEnd(); ++it)
|
||||
ctx.loads.append(it.value()->toLoadContext());
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
#include "types/plan.h"
|
||||
|
||||
class EvAdapter;
|
||||
class EcsRelayAdapter;
|
||||
class RuleBasedScheduler;
|
||||
|
||||
/*!
|
||||
@ -70,6 +71,15 @@ public:
|
||||
*/
|
||||
RootMeter *registeredRootMeter() const;
|
||||
|
||||
/*!
|
||||
* \brief Enregistre un EcsRelayAdapter pour inclusion dans le contexte et le dispatch.
|
||||
*
|
||||
* Appelé par le test (setup) ou la configuration de production.
|
||||
* L'adaptateur est adopté comme enfant Qt de l'arbitre.
|
||||
* \param adapter Adaptateur à enregistrer. Son \c descriptor().id doit être unique.
|
||||
*/
|
||||
void registerEcsAdapter(EcsRelayAdapter *adapter);
|
||||
|
||||
protected:
|
||||
/*!
|
||||
* \brief Boucle principale ETM — surcharge SmartChargingManager::update().
|
||||
@ -87,17 +97,22 @@ protected:
|
||||
|
||||
private:
|
||||
/*!
|
||||
* \brief Construit un SurplusContext minimal (3b stub — timestamp seul).
|
||||
* \note Contexte complet prévu en 3d (§5 complet avec site/meter/pv/battery/loads).
|
||||
* \brief Construit le SurplusContext §5 : meter brut + loads EV + loads ECS.
|
||||
*
|
||||
* \c ctx.meter.exportW = mesure brute du compteur (AGENTS invariant 8 — aucune
|
||||
* déduction interne). La déduction evReservedW est faite dans le scheduler.
|
||||
*/
|
||||
SurplusContext buildContext(const QDateTime &now) const;
|
||||
|
||||
/*!
|
||||
* \brief Synchronise m_adapters avec les EvCharger actuellement enregistrés.
|
||||
* Crée les adapters manquants, supprime les adapters obsolètes.
|
||||
* \note Découverte ECS via interface 'ecsrelay' ThingManager — déféré 3g config.
|
||||
* En beta, les EcsRelayAdapters sont enregistrés via registerEcsAdapter().
|
||||
*/
|
||||
void syncAdapters();
|
||||
|
||||
RuleBasedScheduler *m_scheduler = nullptr;
|
||||
QHash<QString, EvAdapter *> m_adapters; //!< loadId (ThingId string) → EvAdapter*.
|
||||
RuleBasedScheduler *m_scheduler = nullptr;
|
||||
QHash<QString, EvAdapter *> m_adapters; //!< loadId (ThingId string) → EvAdapter*.
|
||||
QHash<QString, EcsRelayAdapter *> m_ecsAdapters; //!< loadId → EcsRelayAdapter*.
|
||||
};
|
||||
|
||||
@ -6,10 +6,12 @@ HEADERS += \
|
||||
$$PWD/adapters/iloadadapter.h \
|
||||
$$PWD/scheduler/ischeduler.h \
|
||||
$$PWD/adapters/evadapter.h \
|
||||
$$PWD/adapters/ecsrelayadapter.h \
|
||||
$$PWD/scheduler/rulebasedscheduler.h \
|
||||
$$PWD/energyarbitrator.h \
|
||||
|
||||
SOURCES += \
|
||||
$$PWD/adapters/evadapter.cpp \
|
||||
$$PWD/adapters/ecsrelayadapter.cpp \
|
||||
$$PWD/scheduler/rulebasedscheduler.cpp \
|
||||
$$PWD/energyarbitrator.cpp \
|
||||
|
||||
@ -59,4 +59,14 @@ struct LoadAction {
|
||||
* Rempli par le scheduler ; peut être 0 si inconnu.
|
||||
*/
|
||||
double estimatedPowerW = 0;
|
||||
|
||||
/*!
|
||||
* \brief Forçage sécurité — bypasse les verrous anti-rebond (minOn/minOff).
|
||||
*
|
||||
* Positionné à \c true uniquement par \c applyDegradedMode() (L2 watchdog)
|
||||
* et les contraintes de sécurité. Les adaptateurs doivent appliquer l'action
|
||||
* immédiatement sans vérifier les verrous temporels.
|
||||
* \warning Réservé à la sécurité. Ne jamais mettre à \c true dans un scheduler.
|
||||
*/
|
||||
bool force = false;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user