refactor(etm): retirer le kind Stage (ECS→Setpoint), chemin State PAC conservé
Contrat etmvariableload rév. 2 §6 : LoadAction garde DEUX kinds (Setpoint W + état
SG-Ready). On supprime UNIQUEMENT le kind Stage (index de palier ECS) ; l'ECS passera
en etmvariableload (Setpoint W) en T3.
- LoadAction : enum Kind { Setpoint, State, Constraint } (retrait Stage + champ stage).
- SurplusContext : retrait stage/minStage/maxStage de LoadContextTelemetry. state/minState/
maxState (PAC) CONSERVÉS.
- LoadDescriptor : retrait du champ stages (relay-stages orphelin).
- RuleBasedScheduler : retrait buildEcsStageAction ; waterfall = sg-ready uniquement.
- EnergyArbitrator : retrait registerEcsAdapter / m_ecsAdapters / dispatch Stage /
boucle contexte ECS / repli L2 ECS. Chemin State (PAC) intact.
- Suppression de EcsRelayAdapter ; retrait du registre en dur ECS (energypluginnymea).
NB : entre ce commit et T4 le repli L2 charge pilotée est absent (recâblé en T4) —
commit intermédiaire d'une branche feature, jamais déployé tel quel.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b7bfd58139
commit
51006745ed
@ -31,7 +31,6 @@
|
||||
// [ETM] BEGIN — EnergyArbitrator flip. Remove block to revert to upstream SmartChargingManager.
|
||||
#ifdef ETM_ARBITRATOR
|
||||
#include "etm/energyarbitrator.h"
|
||||
#include "etm/adapters/ecsrelayadapter.h"
|
||||
#include "etm/adapters/sgreadyadapter.h"
|
||||
#endif
|
||||
// [ETM] END
|
||||
@ -55,20 +54,11 @@ void EnergyPluginNymea::init()
|
||||
qCDebug(dcNymeaEnergy()) << "ETM_ARBITRATOR actif — EnergyArbitrator chargé.";
|
||||
EnergyArbitrator *chargingManager = new EnergyArbitrator(energyManager(), thingManager(), spotMarketManager, configuration, this);
|
||||
|
||||
// §1.d — Banc hems : adaptateurs ECS 3 relais + SG-Ready PAC (ThingIds briefing terrain).
|
||||
// §1.d — Banc hems : SG-Ready PAC (ThingIds briefing terrain).
|
||||
// NB (T2) : l'ECS banc (ex-EcsRelayAdapter relay-stages) est retiré — l'ECS passe en
|
||||
// etmvariableload (Setpoint W) et sera construit depuis LoadConfig en T4. La PAC reste à états.
|
||||
{
|
||||
ThingManager *tm = thingManager();
|
||||
const QString R500 = "{8538782f-2c8c-4a30-bfce-c8140f791c9b}";
|
||||
const QString R1000 = "{2ebe6bef-829b-4695-9d11-ddb4c16c5448}";
|
||||
const QString R2000 = "{b033b212-1adb-4df0-ba2b-8fa477de52a2}";
|
||||
auto *ecs = new EcsRelayAdapter(
|
||||
tm, "ecs-terrain", "ECS banc",
|
||||
QList<int>({0, 500, 1000, 1500, 2000, 2500, 3000, 3500}),
|
||||
QList<QList<QString>>({ {}, {R500}, {R1000}, {R500,R1000},
|
||||
{R2000}, {R500,R2000}, {R1000,R2000}, {R500,R1000,R2000} }),
|
||||
60, 60, 1, chargingManager);
|
||||
chargingManager->registerEcsAdapter(ecs);
|
||||
|
||||
const QString K1 = "{beaf92e1-aedc-4b84-9ce4-e423648638cc}";
|
||||
const QString K2 = "{bf236e64-5ae7-4bf1-82a6-4ede03de75a6}";
|
||||
auto *pac = new SgReadyAdapter(
|
||||
|
||||
@ -1,217 +0,0 @@
|
||||
// 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;
|
||||
|
||||
// Source de currentPowerW (consommée par le recrédit anti-clignotement du waterfall,
|
||||
// RuleBasedScheduler::buildEcsStageAction — correction B) :
|
||||
// - MESURÉE dès qu'au moins un relais du stage expose un state "currentPower" :
|
||||
// somme des mesures. Un thermostat interne coupé (relais ON mais 0 W réel) renvoie
|
||||
// alors 0 → recrédit 0, jamais de puissance fantôme.
|
||||
// - DÉCLARÉE (repli) : stages[stage] UNIQUEMENT si aucun relais actif ne mesure
|
||||
// (relais nus / mock sans powermetering). Approximation = palier à sa nominale.
|
||||
double power = 0;
|
||||
bool metered = false;
|
||||
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)
|
||||
continue;
|
||||
if (!t2->thingClass().stateTypes().findByName("currentPower").id().isNull()) {
|
||||
metered = true;
|
||||
power += t2->stateValue("currentPower").toDouble();
|
||||
}
|
||||
}
|
||||
// Repli déclaré : seulement si la mesure n'est pas disponible (pas si elle vaut 0).
|
||||
if (!metered && m_currentStage > 0 && m_currentStage < m_stages.size())
|
||||
power = m_stages.at(m_currentStage);
|
||||
|
||||
t.currentPowerW = power;
|
||||
return t;
|
||||
}
|
||||
|
||||
LoadContext EcsRelayAdapter::toLoadContext(const QDateTime &now) 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;
|
||||
// Fenêtre de verrou évaluée au temps de cycle : le scheduler y borne son choix
|
||||
// (plancher = puissance engagée non-coupable sous minOn ; plafond = redémarrage minOff).
|
||||
lockWindow(now, ctx.telemetry.minStage, ctx.telemetry.maxStage);
|
||||
return ctx;
|
||||
}
|
||||
|
||||
LoadAction EcsRelayAdapter::applyAction(const LoadAction &action, const QDateTime &now)
|
||||
{
|
||||
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 évalués au temps de cycle (même fenêtre que le scheduler) —
|
||||
// bypassés si force == true (L2 watchdog).
|
||||
if (!action.force && lockActive(newStage, now)) {
|
||||
qCDebug(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
|
||||
<< "— verrou anti-rebond actif, stage" << newStage << "ignoré.";
|
||||
return action;
|
||||
}
|
||||
|
||||
qCInfo(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 = now; // estampille au temps de cycle (cohérent avec la fenêtre de verrou)
|
||||
m_lastActionAt = now;
|
||||
|
||||
LoadAction applied = action;
|
||||
applied.stage = newStage;
|
||||
applied.estimatedPowerW = m_stages.at(newStage);
|
||||
return applied;
|
||||
}
|
||||
|
||||
// ---- privé ---------------------------------------------------------------
|
||||
|
||||
void EcsRelayAdapter::lockWindow(const QDateTime &now, int &minStage, int &maxStage) const
|
||||
{
|
||||
const int topStage = m_stages.size() - 1;
|
||||
const bool valid = m_lastSwitch.isValid();
|
||||
const qint64 elapsed = valid ? m_lastSwitch.secsTo(now) : 0;
|
||||
|
||||
// Plancher : si ON et minOn non écoulé → interdit de descendre sous le palier courant
|
||||
// (puissance engagée non-coupable, protection compresseur / anti court-cycling).
|
||||
minStage = (m_currentStage > 0 && valid && elapsed < m_minOnS) ? m_currentStage : 0;
|
||||
|
||||
// Plafond : si à l'arrêt et minOff non écoulé → interdit de redémarrer.
|
||||
maxStage = (m_currentStage == 0 && valid && elapsed < m_minOffS) ? 0 : topStage;
|
||||
}
|
||||
|
||||
bool EcsRelayAdapter::lockActive(int newStage, const QDateTime &now) const
|
||||
{
|
||||
// MÊME calcul que la fenêtre exposée au scheduler → décision et exécution coïncident.
|
||||
int minStage, maxStage;
|
||||
lockWindow(now, minStage, maxStage);
|
||||
return newStage < minStage || newStage > maxStage;
|
||||
}
|
||||
|
||||
void EcsRelayAdapter::applyRelayStage(int stage)
|
||||
{
|
||||
// Set de relais CIBLE du palier (delta complet : chaque relais connu est amené à son
|
||||
// état cible on/off, pas d'ajout incrémental). Gère les mappings NON-CASCADÉS où monter
|
||||
// d'un palier éteint des relais (ex. 3 résistances 500/1000/2000 W : 1500=[r500,r1000]
|
||||
// → 2000=[r2000] commute 3 relais).
|
||||
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;
|
||||
}();
|
||||
|
||||
QSet<QString> allRelays;
|
||||
for (const auto &list : m_relayMapping)
|
||||
for (const QString &id : list)
|
||||
allRelays.insert(id);
|
||||
|
||||
auto writeRelay = [&](const QString &thingId, bool on) {
|
||||
Thing *relay = m_thingManager->findConfiguredThing(ThingId(thingId));
|
||||
if (!relay) {
|
||||
qCWarning(dcNymeaEnergy()) << "[EcsRelayAdapter]" << m_label
|
||||
<< "— relais non trouvé:" << thingId;
|
||||
return;
|
||||
}
|
||||
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(), on));
|
||||
m_thingManager->executeAction(powerAction);
|
||||
} else {
|
||||
relay->setStateValue("power", on); // repli mock
|
||||
}
|
||||
};
|
||||
|
||||
// Intention (résistif : transitoire inoffensif, mais portée comme SG-Ready) : COUPER
|
||||
// d'abord les relais hors-cible, PUIS enclencher ceux de la cible → pas de sur-puissance
|
||||
// transitoire (somme des deux paliers) sur une transition non-cascadée.
|
||||
for (const QString &id : allRelays)
|
||||
if (!wantOn.contains(id))
|
||||
writeRelay(id, false);
|
||||
for (const QString &id : wantOn)
|
||||
writeRelay(id, true);
|
||||
}
|
||||
@ -1,123 +0,0 @@
|
||||
// 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 Rang dans le waterfall (OPTIMIZER_PROTOCOL §5) : valeur plus BASSE
|
||||
* = servi en premier (rang 1 = premier servi).
|
||||
* \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.
|
||||
* \param now Temps de cycle (\c ctx.timestamp) — source unique pour minStage/maxStage.
|
||||
* \return LoadContext incluant declared, limits et télémétrie ECS (stage, currentPowerW,
|
||||
* lastSwitch, et la fenêtre de verrou \c minStage/maxStage évaluée à \p now).
|
||||
*/
|
||||
LoadContext toLoadContext(const QDateTime &now) const override;
|
||||
|
||||
/*!
|
||||
* \brief Applique un changement de palier sur les relais.
|
||||
*
|
||||
* \param action LoadAction de kind Stage. Autres kinds : retour sans effet.
|
||||
* \param now Temps de cycle (\c ctx.timestamp) — MÊME source que toLoadContext(),
|
||||
* utilisée pour l'évaluation des verrous et l'estampille \c m_lastSwitch.
|
||||
* \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 (= \p now).
|
||||
*/
|
||||
LoadAction applyAction(const LoadAction &action, const QDateTime &now) override;
|
||||
|
||||
/*! \brief Stage courant (0 = off). */
|
||||
int currentStage() const { return m_currentStage; }
|
||||
|
||||
private:
|
||||
/*!
|
||||
* \brief Fenêtre de paliers autorisée à l'instant \p now par les verrous minOn/minOff.
|
||||
* \param now Temps de cycle.
|
||||
* \param[out] minStage Plancher : palier courant si minOn non écoulé (puissance engagée
|
||||
* non-coupable), sinon 0.
|
||||
* \param[out] maxStage Plafond : 0 si à l'arrêt et minOff non écoulé (redémarrage interdit),
|
||||
* sinon le palier le plus haut.
|
||||
*/
|
||||
void lockWindow(const QDateTime &now, int &minStage, int &maxStage) const;
|
||||
|
||||
bool lockActive(int newStage, const QDateTime &now) 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,7 +3,6 @@
|
||||
|
||||
#include "energyarbitrator.h"
|
||||
#include "adapters/evadapter.h"
|
||||
#include "adapters/ecsrelayadapter.h"
|
||||
#include "adapters/sgreadyadapter.h"
|
||||
#include "scheduler/rulebasedscheduler.h"
|
||||
#include "types/surpluscontext.h"
|
||||
@ -85,18 +84,6 @@ 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::registerSgReadyAdapter(SgReadyAdapter *adapter)
|
||||
{
|
||||
const QString id = adapter->descriptor().id;
|
||||
@ -150,7 +137,7 @@ void EnergyArbitrator::update(const QDateTime ¤tDateTime)
|
||||
}
|
||||
|
||||
// 7 : dispatch matériel (même position que l'amont — m_chargingActions rempli par getPlan())
|
||||
applyActionsToAdapters(slot, currentDateTime); // ECS (kind==Stage) → m_ecsAdapters
|
||||
applyActionsToAdapters(slot, currentDateTime); // PAC (kind==State) → m_sgReadyAdapters
|
||||
adjustEvChargers(currentDateTime); // EV (kind==Setpoint) → proxy amont jusqu'à 3g
|
||||
}
|
||||
|
||||
@ -179,9 +166,7 @@ SurplusContext EnergyArbitrator::buildContext(const QDateTime &now) const
|
||||
for (auto it = m_adapters.constBegin(); it != m_adapters.constEnd(); ++it)
|
||||
ctx.loads.append(it.value()->toLoadContext(now));
|
||||
|
||||
// --- loads[] : ECS relay adapters ---
|
||||
for (auto it = m_ecsAdapters.constBegin(); it != m_ecsAdapters.constEnd(); ++it)
|
||||
ctx.loads.append(it.value()->toLoadContext(now));
|
||||
// --- loads[] : etmvariableload adapters (ECS/routeur) → câblés en T4 (config) ---
|
||||
|
||||
// --- loads[] : SG-Ready adapters (PAC) ---
|
||||
for (auto it = m_sgReadyAdapters.constBegin(); it != m_sgReadyAdapters.constEnd(); ++it)
|
||||
@ -209,14 +194,7 @@ void EnergyArbitrator::applyActionsToAdapters(const Slot &slot, const QDateTime
|
||||
{
|
||||
for (const LoadAction &action : slot.actions) {
|
||||
// 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;
|
||||
|
||||
} else if (action.kind == LoadAction::State) {
|
||||
if (action.kind == LoadAction::State) {
|
||||
SgReadyAdapter *adapter = m_sgReadyAdapters.value(action.loadId);
|
||||
if (adapter)
|
||||
adapter->applyAction(action, now);
|
||||
@ -224,6 +202,7 @@ void EnergyArbitrator::applyActionsToAdapters(const Slot &slot, const QDateTime
|
||||
qCWarning(dcNymeaEnergy()) << "[Arbitre] action State sans adaptateur SG-Ready:" << action.loadId;
|
||||
}
|
||||
// EV (Setpoint) : dispatché par adjustEvChargers() amont jusqu'à 3g.
|
||||
// etmvariableload (Setpoint W) : dispatch via m_etmVariableLoadAdapters → câblé en T3/T4.
|
||||
}
|
||||
}
|
||||
|
||||
@ -267,16 +246,8 @@ void EnergyArbitrator::applyDegradedMode(const QDateTime &now)
|
||||
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, now); // force=true → bypass verrous ; now = temps de cycle
|
||||
}
|
||||
// etmvariableload (ECS/routeur) : repli setPowerSetpoint(0) force=true (contrat rév. 2 §9) —
|
||||
// câblé avec m_etmVariableLoadAdapters en T4 (config). En attendant, EV + PAC ci-dessous.
|
||||
|
||||
// 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
|
||||
|
||||
@ -12,7 +12,6 @@
|
||||
class QTimer;
|
||||
|
||||
class EvAdapter;
|
||||
class EcsRelayAdapter;
|
||||
class SgReadyAdapter;
|
||||
class RuleBasedScheduler;
|
||||
|
||||
@ -76,15 +75,6 @@ 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);
|
||||
|
||||
/*!
|
||||
* \brief Enregistre un SgReadyAdapter (PAC) pour inclusion dans le contexte et le dispatch.
|
||||
* \param adapter Adaptateur à enregistrer ; son \c descriptor().id doit être unique.
|
||||
@ -133,7 +123,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 + SG-Ready State) + adjustEvChargers() (EV) → dispatch
|
||||
* 5. applyActionsToAdapters() (SG-Ready State) + adjustEvChargers() (EV) → dispatch
|
||||
*
|
||||
* \param currentDateTime Instant courant (timer ou simulation).
|
||||
*/
|
||||
@ -141,32 +131,32 @@ protected:
|
||||
|
||||
private:
|
||||
/*!
|
||||
* \brief Construit le SurplusContext §5 : meter brut + loads EV + ECS + SG-Ready.
|
||||
* \brief Construit le SurplusContext §5 : meter brut + loads EV + SG-Ready.
|
||||
*
|
||||
* \c ctx.meter.exportW = mesure brute du compteur (AGENTS invariant 8 — aucune
|
||||
* déduction interne). La déduction evReservedW est faite dans le scheduler.
|
||||
* \param now Temps de cycle (\c ctx.timestamp) : pose \c ctx.timestamp et sert de SOURCE
|
||||
* UNIQUE aux fenêtres de verrou (\c minStage/maxStage, \c minState/maxState) calculées
|
||||
* par chaque adaptateur — cohérence décision (scheduler) / exécution (applyAction).
|
||||
* UNIQUE à la fenêtre de verrou PAC (\c minState/maxState) calculée par le SgReadyAdapter —
|
||||
* cohérence décision (scheduler) / exécution (applyAction). Les charges etmvariableload
|
||||
* (ECS/routeur) n'ont pas de fenêtre côté moteur (verrous dans le thing).
|
||||
*/
|
||||
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().
|
||||
* \note Découverte etmvariableload/SG-Ready via config — câblé en T4.
|
||||
*/
|
||||
void syncAdapters();
|
||||
|
||||
/*!
|
||||
* \brief Applique les actions d'un Slot aux LoadAdapters non-EV (ECS + SG-Ready).
|
||||
* \brief Applique les actions d'un Slot aux LoadAdapters non-EV.
|
||||
*
|
||||
* Itère \c slot.actions et dispatche selon le kind : \c Stage → \c m_ecsAdapters
|
||||
* (EcsRelayAdapter) ; \c State → \c m_sgReadyAdapters (SgReadyAdapter). Les actions EV
|
||||
* (\c Setpoint) restent dispatchées par \c adjustEvChargers() amont jusqu'à 3g.
|
||||
* L'adaptateur écrête/verrouille lui-même (anti-rebond) et ignore toute action sans
|
||||
* \c reason ou de kind non supporté — aucune décision ici (règle 2).
|
||||
* Itère \c slot.actions et dispatche selon le kind : \c State → \c m_sgReadyAdapters
|
||||
* (SgReadyAdapter, PAC). Les actions EV (\c Setpoint) restent dispatchées par
|
||||
* \c adjustEvChargers() amont jusqu'à 3g ; les charges etmvariableload (\c Setpoint W)
|
||||
* seront dispatchées via leur adaptateur en T3/T4. L'adaptateur écrête/verrouille lui-même
|
||||
* et ignore toute action sans \c reason ou de kind non supporté — aucune décision ici (règle 2).
|
||||
* \param slot Créneau courant retourné par le scheduler.
|
||||
* \param now Temps de cycle (\c ctx.timestamp) — transmis aux adaptateurs pour une
|
||||
* évaluation des verrous cohérente avec celle vue par le scheduler.
|
||||
@ -202,7 +192,6 @@ private:
|
||||
|
||||
RuleBasedScheduler *m_scheduler = nullptr;
|
||||
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) ---
|
||||
|
||||
@ -6,7 +6,6 @@ HEADERS += \
|
||||
$$PWD/adapters/iloadadapter.h \
|
||||
$$PWD/scheduler/ischeduler.h \
|
||||
$$PWD/adapters/evadapter.h \
|
||||
$$PWD/adapters/ecsrelayadapter.h \
|
||||
$$PWD/adapters/sgreadyadapter.h \
|
||||
$$PWD/adapters/etmvariableloadadapter.h \
|
||||
$$PWD/scheduler/rulebasedscheduler.h \
|
||||
@ -14,7 +13,6 @@ HEADERS += \
|
||||
|
||||
SOURCES += \
|
||||
$$PWD/adapters/evadapter.cpp \
|
||||
$$PWD/adapters/ecsrelayadapter.cpp \
|
||||
$$PWD/adapters/sgreadyadapter.cpp \
|
||||
$$PWD/adapters/etmvariableloadadapter.cpp \
|
||||
$$PWD/scheduler/rulebasedscheduler.cpp \
|
||||
|
||||
@ -96,23 +96,22 @@ Plan RuleBasedScheduler::getPlan(const SurplusContext &ctx)
|
||||
// transitoire sous minOn est géré par le verrou de l'adaptateur, pas par le budget.)
|
||||
double remainingSurplusW = (ctx.meter.exportW - ctx.meter.importW) - evReservedW;
|
||||
|
||||
// Charges pilotables non-EV (ECS paliers + SG-Ready) triées par priorité ASCENDANTE :
|
||||
// rang 1 = premier servi (OPTIMIZER_PROTOCOL §5 + annexe C — la priorité est un rang).
|
||||
// Le budget de surplus est UNIQUE et cascade à travers TOUTES ces charges par priorité.
|
||||
// Charges pilotables non-EV triées par priorité ASCENDANTE : rang 1 = premier servi
|
||||
// (OPTIMIZER_PROTOCOL §5 + annexe C — la priorité est un rang). Le budget de surplus est
|
||||
// UNIQUE et cascade à travers ces charges par priorité.
|
||||
//
|
||||
// T2 : seule la PAC SG-Ready (kind State) est traitée ici. Les charges etmvariableload
|
||||
// (ECS/routeur, kind Setpoint W) rejoindront ce waterfall avec la règle d'arrondi en T3.
|
||||
QList<LoadContext> nonEvLoads;
|
||||
for (const LoadContext &lc : ctx.loads) {
|
||||
if (lc.adapter == QStringLiteral("relay-stages") || lc.adapter == QStringLiteral("sg-ready"))
|
||||
if (lc.adapter == QStringLiteral("sg-ready"))
|
||||
nonEvLoads.append(lc);
|
||||
}
|
||||
std::sort(nonEvLoads.begin(), nonEvLoads.end(),
|
||||
[](const LoadContext &a, const LoadContext &b) { return a.priority < b.priority; });
|
||||
|
||||
for (const LoadContext &lc : nonEvLoads) {
|
||||
if (lc.adapter == QStringLiteral("sg-ready"))
|
||||
slot.actions.append(buildSgReadyStateAction(lc, remainingSurplusW));
|
||||
else
|
||||
slot.actions.append(buildEcsStageAction(lc, remainingSurplusW));
|
||||
}
|
||||
for (const LoadContext &lc : nonEvLoads)
|
||||
slot.actions.append(buildSgReadyStateAction(lc, remainingSurplusW));
|
||||
|
||||
// Grid funding (ECS/PAC) : dormant jusqu'à 3f (waterfall réseau) — non implémenté ici.
|
||||
|
||||
@ -171,58 +170,6 @@ LoadAction RuleBasedScheduler::buildIdleAction(EvCharger *ev) const
|
||||
return la;
|
||||
}
|
||||
|
||||
LoadAction RuleBasedScheduler::buildEcsStageAction(const LoadContext &lc,
|
||||
double &remainingSurplusW) const
|
||||
{
|
||||
// Correction B (anti-clignotement) : on recrédite la conso actuelle de l'ECS au budget.
|
||||
// Sans ce recrédit, allumer un palier ferait chuter l'export mesuré → palier 0 → oscillation.
|
||||
const double budgetChargeW = remainingSurplusW + lc.telemetry.currentPowerW;
|
||||
|
||||
// Palier déclaré le plus haut qui tient dans le budget. stages[0] == 0 par construction
|
||||
// (Q_ASSERT EcsRelayAdapter).
|
||||
const QList<int> &stages = lc.declared.stages;
|
||||
const int topStage = stages.size() - 1;
|
||||
int budgetStage = 0;
|
||||
for (int i = 0; i < stages.size(); ++i) {
|
||||
if (stages.at(i) <= budgetChargeW)
|
||||
budgetStage = i;
|
||||
}
|
||||
|
||||
// Verrou (minOn/minOff) : le palier choisi est borné par la fenêtre déclarée par
|
||||
// l'adaptateur (calculée au MÊME ctx.timestamp). Une charge verrouillée ON garde son
|
||||
// palier (puissance engagée non-coupable) ; à l'arrêt sous minOff elle ne redémarre pas.
|
||||
const int lo = qBound(0, lc.telemetry.minStage, topStage);
|
||||
const int hi = qBound(lo, lc.telemetry.maxStage, topStage);
|
||||
const int bestStage = qBound(lo, budgetStage, hi);
|
||||
|
||||
LoadAction la;
|
||||
la.loadId = lc.id;
|
||||
la.kind = LoadAction::Stage;
|
||||
la.funding = LoadAction::Surplus;
|
||||
la.stage = bestStage;
|
||||
la.estimatedPowerW = stages.at(bestStage);
|
||||
|
||||
if (bestStage > budgetStage)
|
||||
// Maintenu au-dessus du budget : protection compresseur (minOn non écoulé).
|
||||
la.reason = QStringLiteral("Verrou minOn — %1 maintenu palier %2 (%3 W, surplus %4 W)")
|
||||
.arg(lc.label).arg(bestStage).arg(stages.at(bestStage)).arg(qRound(budgetChargeW));
|
||||
else if (bestStage < budgetStage)
|
||||
// Empêché de monter/redémarrer par minOff malgré le surplus.
|
||||
la.reason = QStringLiteral("Verrou minOff — %1 maintenu palier %2 (redémarrage trop tôt)")
|
||||
.arg(lc.label).arg(bestStage);
|
||||
else if (bestStage > 0)
|
||||
la.reason = QStringLiteral("Surplus PV %1 W — %2 palier %3 (%4 W)")
|
||||
.arg(qRound(budgetChargeW)).arg(lc.label)
|
||||
.arg(bestStage).arg(stages.at(bestStage));
|
||||
else
|
||||
la.reason = QStringLiteral("Surplus insuffisant (%1 W) — %2 éteint")
|
||||
.arg(qRound(budgetChargeW)).arg(lc.label);
|
||||
|
||||
// Budget restant pour les charges ECS suivantes (rang supérieur / priorité plus basse).
|
||||
remainingSurplusW = budgetChargeW - la.estimatedPowerW;
|
||||
return la;
|
||||
}
|
||||
|
||||
LoadAction RuleBasedScheduler::buildSgReadyStateAction(const LoadContext &lc,
|
||||
double &remainingSurplusW) const
|
||||
{
|
||||
|
||||
@ -10,7 +10,7 @@ class ChargingAction;
|
||||
class EnergyArbitrator;
|
||||
|
||||
/*!
|
||||
* \brief Planificateur règles GPL : EV (proxy amont) + waterfall surplus ECS / SG-Ready.
|
||||
* \brief Planificateur règles GPL : EV (proxy amont) + waterfall surplus SG-Ready (PAC).
|
||||
*
|
||||
* \c getPlan() produit un plan à 1 créneau en DEUX temps :
|
||||
*
|
||||
@ -19,16 +19,17 @@ class EnergyArbitrator;
|
||||
* relit \c m_chargingActions et les reformate en LoadAction(Setpoint) annotées d'un
|
||||
* \c reason français. Le dispatch EV réel reste dans \c adjustEvChargers() amont.
|
||||
*
|
||||
* 2. **Waterfall non-EV (3c/3e)** : un budget de surplus UNIQUE — net SIGNÉ
|
||||
* 2. **Waterfall non-EV** : un budget de surplus UNIQUE — net SIGNÉ
|
||||
* \c (exportW − importW) − evReservedW — cascade par **priorité ASC** (rang, 1 = premier
|
||||
* servi) à travers les charges \c relay-stages (ECS, \c buildEcsStageAction) ET
|
||||
* \c sg-ready (PAC, \c buildSgReadyStateAction). Anti-clignotement par **recrédit** de la
|
||||
* conso allouée ; **clamp lock-aware** \c minStage/maxStage et \c minState/maxState (verrous
|
||||
* minOn/minOff/minStateHold, protection compresseur) — la fenêtre est calculée au MÊME
|
||||
* \c ctx.timestamp que l'exécution (cf. \c ILoadAdapter). Ces LoadAction sont RÉELLEMENT
|
||||
* dispatchées par \c EnergyArbitrator::applyActionsToAdapters().
|
||||
* servi). En T2, seule la PAC \c sg-ready (\c buildSgReadyStateAction, kind State) y est
|
||||
* traitée : anti-clignotement par **recrédit** de la conso allouée, **clamp lock-aware**
|
||||
* \c minState/maxState (verrou \c minStateHold, protection court-cycling compresseur) calculé
|
||||
* au MÊME \c ctx.timestamp que l'exécution (cf. \c ILoadAdapter). Ces LoadAction sont
|
||||
* RÉELLEMENT dispatchées par \c EnergyArbitrator::applyActionsToAdapters().
|
||||
*
|
||||
* À partir de **3g** : l'EV rejoindra le waterfall unifié (toutes charges classables ensemble).
|
||||
* Les charges \c etmvariableload (ECS/routeur, kind Setpoint W) rejoindront ce waterfall avec
|
||||
* la règle d'arrondi (contrat rév. 2 §5) en **T3**. À partir de **3g**, l'EV rejoint aussi le
|
||||
* waterfall unifié (toutes charges classables ensemble).
|
||||
*
|
||||
* \invariant getPlan() retourne IMMÉDIATEMENT (AGENTS invariant 5) et toujours un Plan valide.
|
||||
* \invariant Toute LoadAction a un \c reason non vide, en français.
|
||||
@ -51,8 +52,8 @@ public:
|
||||
*
|
||||
* Étape 1 (EV) : \c runSpotMarketPlanning() + \c runSurplusPlanning() puis reformatage
|
||||
* de \c scheduledActions() en LoadAction(Setpoint) — log [Arbitre], dispatch amont.
|
||||
* Étape 2 (non-EV) : waterfall surplus sur les charges \c relay-stages / \c sg-ready
|
||||
* triées par priorité — LoadAction(Stage/State) réellement dispatchées.
|
||||
* Étape 2 (non-EV) : waterfall surplus sur la PAC \c sg-ready triée par priorité —
|
||||
* LoadAction(State) réellement dispatchées (etmvariableload ECS/routeur arrive en T3).
|
||||
*
|
||||
* \param ctx SurplusContext courant. Utilisé : \c ctx.timestamp (temps de cycle / verrous),
|
||||
* \c ctx.meter (surplus net signé), \c ctx.loads (déclarés, télémétrie, fenêtres de verrou).
|
||||
@ -83,21 +84,6 @@ private:
|
||||
*/
|
||||
LoadAction buildIdleAction(EvCharger *ev) const;
|
||||
|
||||
/*!
|
||||
* \brief Construit un LoadAction "stage" ECS par cascade de surplus (waterfall §6).
|
||||
*
|
||||
* Retient le palier déclaré le plus haut dont la puissance tient dans le budget courant.
|
||||
* Correction B (anti-clignotement) : recrédite d'abord \c lc.telemetry.currentPowerW au
|
||||
* budget (la conso actuelle de l'ECS est déjà soustraite de l'export mesuré), puis
|
||||
* décrémente \c remainingSurplusW de la puissance du palier retenu.
|
||||
*
|
||||
* \param lc Charge ECS (adapter == "relay-stages") du SurplusContext.
|
||||
* \param[in,out] remainingSurplusW Budget de surplus restant (W) ; mis à jour pour la
|
||||
* charge ECS suivante (priorité inférieure / rang supérieur).
|
||||
* \return LoadAction kind=Stage, funding=Surplus, \c reason français non vide.
|
||||
*/
|
||||
LoadAction buildEcsStageAction(const LoadContext &lc, double &remainingSurplusW) const;
|
||||
|
||||
/*!
|
||||
* \brief Construit un LoadAction "state" SG-Ready (PAC) par mapping SÉMANTIQUE du surplus.
|
||||
*
|
||||
|
||||
@ -16,8 +16,10 @@
|
||||
* \c currentA, \c phaseCount sont significatifs.
|
||||
*/
|
||||
struct LoadAction {
|
||||
/*! \brief Type d'action : consigne continue, palier discret, état ou contrainte. */
|
||||
enum Kind { Setpoint, Stage, State, Constraint };
|
||||
/*! \brief Type d'action : consigne en watts (etmvariableload), état SG-Ready ou contrainte.
|
||||
* \note Le kind \c Stage (index de palier ECS) a été RETIRÉ (contrat etmvariableload rév. 2
|
||||
* §6) : l'ECS est piloté en watts via \c Setpoint, l'index de palier vit dans le thing. */
|
||||
enum Kind { Setpoint, State, Constraint };
|
||||
/*! \brief Financement interne : Surplus (PV) ou Grid (réseau). Non sérialisé. */
|
||||
enum Funding { Surplus, Grid };
|
||||
/*! \brief Source d'énergie pour Setpoint batterie : "solar" ou "grid". */
|
||||
@ -38,9 +40,6 @@ struct LoadAction {
|
||||
double powerW = 0;
|
||||
Source source = Solar;
|
||||
|
||||
// --- Stage relay-stages (ECS) ---
|
||||
int stage = 0; //!< Index de palier (0 = off, 1 = 1er palier, ...).
|
||||
|
||||
// --- State sg-ready ---
|
||||
int state = 0; //!< État SG-Ready (1-4).
|
||||
|
||||
|
||||
@ -19,10 +19,6 @@ struct LoadDeclared {
|
||||
double maxA = 0; //!< Courant maximum (A).
|
||||
int phases = 0; //!< Nombre de phases disponibles (1 ou 3).
|
||||
|
||||
// --- relay-stages (ECS) ---
|
||||
//! Puissances en W par palier : [0, 1200, 2400] — index 0 = off.
|
||||
QList<int> stages;
|
||||
|
||||
// --- etmvariableload (charge à puissance pilotable : EV / ECS résistif / routeur PV) ---
|
||||
//! Paliers atteignables en W (contrat etmvariableload §3), triés croissants, \c 0 inclus.
|
||||
//! Présent en mode *fixed*. Vide ⇒ modulation continue (*dynamic*, borne \c maxPowerW).
|
||||
@ -86,7 +82,7 @@ struct LoadNeeds {
|
||||
struct LoadDescriptor {
|
||||
QString id; //!< ThingId de la charge (string).
|
||||
QString label; //!< Nom lisible (affiché dans les logs).
|
||||
//! Type d'adaptateur : "evcharger"|"relay-stages"|"sg-ready"|"battery".
|
||||
//! Type d'adaptateur : "evcharger"|"etmvariableload"|"sg-ready"|"battery".
|
||||
QString adapter;
|
||||
int priority = 0;
|
||||
LoadDeclared declared;
|
||||
|
||||
@ -59,13 +59,9 @@ struct LoadContextTelemetry {
|
||||
bool pluggedIn = false;
|
||||
bool charging = false;
|
||||
double sessionWh = 0; //!< Énergie chargée dans la session courante (Wh).
|
||||
// --- relay-stages ---
|
||||
int stage = 0;
|
||||
//! Fenêtre de paliers autorisée MAINTENANT par les verrous (calculée au temps de cycle).
|
||||
//! Le scheduler y borne son choix : minStage = plancher (verrou minOn, puissance
|
||||
//! engagée non-coupable) ; maxStage = plafond (verrou minOff, redémarrage interdit).
|
||||
int minStage = 0;
|
||||
int maxStage = 0;
|
||||
// --- etmvariableload (EV/ECS/routeur) : pas de fenêtre de verrou côté moteur —
|
||||
// l'anti-rebond (minOn/minOff) vit dans le thing (contrat rév. 2 §3). Seul
|
||||
// currentPowerW (ci-dessus) sert au résidu de la règle d'arrondi (contrat §5).
|
||||
// --- sg-ready ---
|
||||
int state = 0;
|
||||
//! Fenêtre d'états autorisée MAINTENANT par le verrou minStateHold (protection
|
||||
@ -91,7 +87,7 @@ struct LoadLearned {
|
||||
*/
|
||||
struct LoadContext {
|
||||
QString id;
|
||||
QString adapter; //!< "evcharger"|"relay-stages"|"sg-ready"|"battery".
|
||||
QString adapter; //!< "evcharger"|"etmvariableload"|"sg-ready"|"battery".
|
||||
QString label;
|
||||
int priority = 0;
|
||||
LoadDeclared declared;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user