[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` |
|
| 2 — design arbitre validé | ✅ FAITE | `074fa71` |
|
||||||
| 3a — structs protocole + interfaces | ✅ FAITE | `4ae1939` |
|
| 3a — structs protocole + interfaces | ✅ FAITE | `4ae1939` |
|
||||||
| 3b — EnergyArbitrator + scheduler + adapter | ✅ FAITE — iso-fonctionnalité prouvée | `5f49e4c`, `d8ebd65`, `[3b-iv]` |
|
| 3b — EnergyArbitrator + scheduler + adapter | ✅ FAITE — iso-fonctionnalité prouvée | `5f49e4c`, `d8ebd65`, `[3b-iv]` |
|
||||||
|
| 3c — EcsRelayAdapter + waterfall ECS | 🔄 EN COURS | (wip) |
|
||||||
|
|
||||||
**Détail 3b** :
|
**Détail 3b** :
|
||||||
- `EnergyArbitrator : public SmartChargingManager` — justification dans `## DÉCISIONS DE DESIGN`
|
- `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
|
- 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)
|
- [Arbitre] présents avec raisons françaises pour les 4 cas (idle, surplus PV, aWATTar, deadline)
|
||||||
|
|
||||||
**PROCHAINE ACTION — 3c** :
|
**Détail 3c (morceaux déjà compilés, 0 erreur / 0 warning)** :
|
||||||
- `EcsRelayAdapter` (paliers 0/1/2) — premier adaptateur non-EV, premier `applyAction()` vivant
|
- **Morceau 0** — `LoadAction.force=false` (bypass verrous sécurité) ✅
|
||||||
- Pipeline ETM waterfall (budget surplus → ECS, déduction `addedPower` EV) dans `EnergyArbitrator::update()`
|
- **Morceau 1** — `EcsRelayAdapter` (.h + .cpp) : pilote N Things powerswitch,
|
||||||
- Watchdog L2 : `QTimer` 30 s, mode dégradé variante B, notification `degradedMode` (cf. SAFETY.md §L2)
|
`applyRelayStage()`, verrous `minOnS/minOffS`, bypass si `force==true` ✅
|
||||||
- Scénarios simulation : `testEcsSurplusPV` (chauffe-eau sur surplus PV) + `testMeterSilentFallback` (compteur muet → repli)
|
- 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** :
|
**Remotes git** :
|
||||||
- `origin` (`https://git.etm-powersync.fr/...`) = remote de travail — push normal
|
- `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
|
> ⚠️ Tout plan antérieur mentionnant « créer etm/ avec PowerSyncClient et
|
||||||
> StaticHcHpProvider comme première étape » ou « injecter l'optimiseur dans
|
> StaticHcHpProvider comme première étape » ou « injecter l'optimiseur dans
|
||||||
> SmartChargingManager » est **INVALIDE et ABANDONNÉ**. Ne pas le reprendre,
|
> 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 "energyarbitrator.h"
|
||||||
#include "adapters/evadapter.h"
|
#include "adapters/evadapter.h"
|
||||||
|
#include "adapters/ecsrelayadapter.h"
|
||||||
#include "scheduler/rulebasedscheduler.h"
|
#include "scheduler/rulebasedscheduler.h"
|
||||||
#include "types/surpluscontext.h"
|
#include "types/surpluscontext.h"
|
||||||
#include "types/plan.h"
|
#include "types/plan.h"
|
||||||
|
#include "../rootmeter.h"
|
||||||
|
|
||||||
#include "plugininfo.h"
|
#include "plugininfo.h"
|
||||||
|
|
||||||
@ -51,6 +53,18 @@ RootMeter *EnergyArbitrator::registeredRootMeter() const
|
|||||||
return internalRootMeter();
|
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)
|
void EnergyArbitrator::update(const QDateTime ¤tDateTime)
|
||||||
{
|
{
|
||||||
qCDebug(dcNymeaEnergy()) << "Updating smart charging";
|
qCDebug(dcNymeaEnergy()) << "Updating smart charging";
|
||||||
@ -88,10 +102,33 @@ void EnergyArbitrator::update(const QDateTime ¤tDateTime)
|
|||||||
|
|
||||||
SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const
|
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;
|
SurplusContext ctx;
|
||||||
ctx.timestamp = now;
|
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;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@
|
|||||||
#include "types/plan.h"
|
#include "types/plan.h"
|
||||||
|
|
||||||
class EvAdapter;
|
class EvAdapter;
|
||||||
|
class EcsRelayAdapter;
|
||||||
class RuleBasedScheduler;
|
class RuleBasedScheduler;
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
@ -70,6 +71,15 @@ public:
|
|||||||
*/
|
*/
|
||||||
RootMeter *registeredRootMeter() const;
|
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:
|
protected:
|
||||||
/*!
|
/*!
|
||||||
* \brief Boucle principale ETM — surcharge SmartChargingManager::update().
|
* \brief Boucle principale ETM — surcharge SmartChargingManager::update().
|
||||||
@ -87,17 +97,22 @@ protected:
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
/*!
|
/*!
|
||||||
* \brief Construit un SurplusContext minimal (3b stub — timestamp seul).
|
* \brief Construit le SurplusContext §5 : meter brut + loads EV + loads ECS.
|
||||||
* \note Contexte complet prévu en 3d (§5 complet avec site/meter/pv/battery/loads).
|
*
|
||||||
|
* \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;
|
SurplusContext buildContext(const QDateTime &now) const;
|
||||||
|
|
||||||
/*!
|
/*!
|
||||||
* \brief Synchronise m_adapters avec les EvCharger actuellement enregistrés.
|
* \brief Synchronise m_adapters avec les EvCharger actuellement enregistrés.
|
||||||
* Crée les adapters manquants, supprime les adapters obsolètes.
|
* 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();
|
void syncAdapters();
|
||||||
|
|
||||||
RuleBasedScheduler *m_scheduler = nullptr;
|
RuleBasedScheduler *m_scheduler = nullptr;
|
||||||
QHash<QString, EvAdapter *> m_adapters; //!< loadId (ThingId string) → EvAdapter*.
|
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/adapters/iloadadapter.h \
|
||||||
$$PWD/scheduler/ischeduler.h \
|
$$PWD/scheduler/ischeduler.h \
|
||||||
$$PWD/adapters/evadapter.h \
|
$$PWD/adapters/evadapter.h \
|
||||||
|
$$PWD/adapters/ecsrelayadapter.h \
|
||||||
$$PWD/scheduler/rulebasedscheduler.h \
|
$$PWD/scheduler/rulebasedscheduler.h \
|
||||||
$$PWD/energyarbitrator.h \
|
$$PWD/energyarbitrator.h \
|
||||||
|
|
||||||
SOURCES += \
|
SOURCES += \
|
||||||
$$PWD/adapters/evadapter.cpp \
|
$$PWD/adapters/evadapter.cpp \
|
||||||
|
$$PWD/adapters/ecsrelayadapter.cpp \
|
||||||
$$PWD/scheduler/rulebasedscheduler.cpp \
|
$$PWD/scheduler/rulebasedscheduler.cpp \
|
||||||
$$PWD/energyarbitrator.cpp \
|
$$PWD/energyarbitrator.cpp \
|
||||||
|
|||||||
@ -59,4 +59,14 @@ struct LoadAction {
|
|||||||
* Rempli par le scheduler ; peut être 0 si inconnu.
|
* Rempli par le scheduler ; peut être 0 si inconnu.
|
||||||
*/
|
*/
|
||||||
double estimatedPowerW = 0;
|
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