feat(etm): adaptateur etmvariableload (interface charge pilotée, Setpoint W)
Côté energymanager : EtmVariableLoadAdapter consomme l'interface etmvariableload (states currentPowerW read / powerSetpoint write) pour les charges à puissance pilotable (EV / ECS résistif / routeur PV). La combinatoire matérielle, l'anti-rebond et les phases vivent dans le thing (contrat §1/§3) — l'adaptateur n'écrit qu'un setpoint W et relit currentPowerW. - LoadDescriptor : champs additifs powerLevels / maxPowerW (déclaration §3/§5). - Contrat partagé versionné : docs/INTERFACE_etmvariableload.md (rév. 2). - La déclaration de l'interface nymee + le driver thing relèvent d'une session device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c43449cafe
commit
b7bfd58139
169
docs/INTERFACE_etmvariableload.md
Normal file
169
docs/INTERFACE_etmvariableload.md
Normal file
@ -0,0 +1,169 @@
|
||||
# INTERFACE `etmvariableload` — contrat charge pilotée · **rév. 2**
|
||||
|
||||
> **Source de vérité unique.** Déposé **à l'identique dans les deux repos**
|
||||
> (`etm-powersync-app` et `etm-powersync-energy-plugin-etm`). Toute divergence se tranche **ici**,
|
||||
> pas dans le code. Un agent qui a besoin d'un champ absent met à jour ce doc **d'abord**, dans
|
||||
> les deux repos.
|
||||
>
|
||||
> **rév. 2 — corrige la rév. 1 sur un point structurant :** `etmvariableload` ne couvre **que les
|
||||
> charges à puissance pilotable** (EV, ECS résistif, routeur PV). **La PAC SG-Ready en est exclue** :
|
||||
> elle reste un modèle **à états**, interface séparée et conservée (voir §8). `LoadAction` garde
|
||||
> donc **deux** kinds (`Setpoint` ET l'état SG-Ready), pas un seul.
|
||||
|
||||
---
|
||||
|
||||
## 1. Principe & frontière
|
||||
|
||||
Trois niveaux, trois responsables. **Aucune notion de matériel ne remonte au-dessus du thing.**
|
||||
|
||||
| Niveau | Où | Quoi |
|
||||
|---|---|---|
|
||||
| **Câblage** | thing / plugin device (wizard Things) | GPIO, registres Modbus, quelle résistance sur quelle phase |
|
||||
| **Déclaration** | config charge pilotée (app) | mode, paliers atteignables **en W**, priorité, besoins |
|
||||
| **Runtime** | interface `etmvariableload` | setpoint W envoyé ↔ puissance réelle remontée |
|
||||
|
||||
> **Invariant directeur.** L'energymanager raisonne **uniquement en watts**. Aucune résistance,
|
||||
> aucun relais, aucun index de palier matériel ne franchit l'app ni n'atteint l'arbitre.
|
||||
> **Le thing décide du matériel ; l'energymanager décide de la puissance.**
|
||||
|
||||
---
|
||||
|
||||
## 2. Portée de `etmvariableload`
|
||||
|
||||
**EST une `etmvariableload`** (charge à puissance pilotable) :
|
||||
- **EV charger** (setpoint courant/puissance),
|
||||
- **ECS résistif** — classique (on/off) ou multipalier,
|
||||
- **Routeur PV / triac** (modulation continue).
|
||||
|
||||
**N'EST PAS une `etmvariableload`** :
|
||||
- **PAC SG-Ready** → modèle à **états** (2 bits, hystérésis, `minStateHold`). Interface distincte,
|
||||
conservée telle quelle. Voir §8. Un signal SG-Ready est une *suggestion d'état* à la PAC, pas
|
||||
une consigne de puissance en watts — le forcer en setpoint W serait faux.
|
||||
|
||||
---
|
||||
|
||||
## 3. Interface device `etmvariableload`
|
||||
|
||||
Implémentée par le thing (ECS / EV / routeur). **Côté moteur, seul l'`EtmVariableLoadAdapter`
|
||||
consommateur est livré** ; la déclaration de l'interface nymea + le driver (résistances/triac/
|
||||
anti-rebond/phases) sont une **session device dédiée, hors repo moteur**.
|
||||
|
||||
| State | Accès | Type | Unité | Sémantique |
|
||||
|---|---|---|---|---|
|
||||
| `maxPowerW` | read | uint | W | Plafond physique de la charge. |
|
||||
| `powerLevels` | read | list<uint> | W | Paliers atteignables, triés croissants, **`0` inclus**. Présent en *fixed*. Vide/absent ⇒ modulation continue. |
|
||||
| `powerSetpoint` | **write** | uint | W | Consigne demandée par l'energymanager. |
|
||||
| `currentPowerW` | read | uint | W | Puissance **réellement** appliquée. Ferme la boucle de quantification. |
|
||||
|
||||
**Le thing, en recevant un `powerSetpoint` :** choisit la combinaison matérielle concrète,
|
||||
applique ses **verrous anti-rebond** (minOn/minOff) et son **équilibrage de phases**, puis publie
|
||||
`currentPowerW`. L'energymanager n'a aucune visibilité là-dessus — c'est voulu.
|
||||
|
||||
---
|
||||
|
||||
## 4. `LoadDescriptor` — config app → energymanager
|
||||
|
||||
Émis par l'écran « Configurer charge pilotée » (sous-ensemble de `OPTIMIZER_PROTOCOL.md §5`,
|
||||
noms identiques) :
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"id": "uuid-thing-ecs",
|
||||
"label": "Chauffe-eau",
|
||||
"adapter": "etmvariableload",
|
||||
"mode": "fixed", // "fixed" | "dynamic"
|
||||
"powerLevels": [0, 600, 1200, 1800, 2400], // REQUIS si fixed — absent si dynamic
|
||||
"maxPowerW": 3000, // REQUIS si dynamic ; plafond aussi en fixed
|
||||
"priority": 2, // rang ; 1 = servi en premier
|
||||
"enabled": true,
|
||||
"needs": { "dailyDeadline": "06:00", "minEnergyWhPerDay": 4000 }
|
||||
}
|
||||
```
|
||||
|
||||
- `powerLevels` saisi par l'installateur — directement ou via l'**assistant résistances** (UI-only :
|
||||
additionne les unités, pré-remplit, puis disparaît ; **aucune résistance n'est stockée ni envoyée**).
|
||||
- Config **persistée côté plugin** (`/var/lib/nymea/energy-load-configuration.json`), jamais en
|
||||
SharedPreferences. App = éditeur : `NymeaEnergy.GetLoadConfig` / `SetLoadConfig`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Règle d'arrondi (energymanager)
|
||||
|
||||
À chaque cycle, pour une charge dont c'est le tour dans la priorité :
|
||||
|
||||
- **Fixed** : `level = max( l ∈ powerLevels | l ≤ budget )` puis `setPowerSetpoint(level)`.
|
||||
L'arbitre connaît la **granularité déclarée** → pas de sur-allocation, **zéro cycle de retard**.
|
||||
- **Dynamic** : `setPowerSetpoint( clamp(budget, 0, maxPowerW) )`.
|
||||
- **Résidu** : lire le `currentPowerW` **de début de cycle** (télémétrie de l'adapter, comme la
|
||||
Correction B — **PAS** une relecture post-setpoint : invariant 8, aucune boucle de feedback).
|
||||
Le résidu `budget − currentPowerW` repart vers la charge suivante de la priorité **du même cycle**.
|
||||
|
||||
**Déclaré vs réel :** la **déclaration** (`powerLevels`/`maxPowerW`) est la référence de
|
||||
**planification**. `currentPowerW` est le **juge de paix runtime**. Si le réel plafonne durablement
|
||||
sous le déclaré → erreur de déclaration, l'installateur corrige. L'energymanager **n'écrase jamais**
|
||||
la déclaration.
|
||||
|
||||
---
|
||||
|
||||
## 6. `LoadAction` — **deux** kinds
|
||||
|
||||
| Kind | Pour | Porte |
|
||||
|---|---|---|
|
||||
| `Setpoint` | EV / ECS / routeur (`etmvariableload`) | `powerW` |
|
||||
| *état SG-Ready* | PAC (interface à états, §8) | `state` (1–4) |
|
||||
| `Constraint` | batterie (déféré 3f) | conservé |
|
||||
|
||||
- **Supprimer `Stage`** (ECS → `Setpoint`). Retirer `stage/minStage/maxStage` de
|
||||
`LoadContextTelemetry` (le verrou ECS redescend dans le thing).
|
||||
- **Conserver le kind d'état SG-Ready** et `state/minState/maxState` pour la PAC — son
|
||||
`minStateHold` (protection court-cycling compresseur) reste dans son chemin, **inchangé**.
|
||||
- `reason` reste porté par chaque `LoadAction` (déjà le cas).
|
||||
|
||||
---
|
||||
|
||||
## 7. Expo du `reason` — tableau additif `loadActions[]`
|
||||
|
||||
Sérialiser dans `GetChargingSchedules` / `ChargingSchedulesChanged` un **champ additif**
|
||||
(rétro-compatible, ne touche pas la structure EV `ChargingAction`) :
|
||||
|
||||
```jsonc
|
||||
"loadActions": [
|
||||
{ "loadId": "uuid-ecs", "setpointW": 1200, "currentPowerW": 1180, "reason": "Surplus PV 1.3 kW — ECS rang 2" },
|
||||
{ "loadId": "uuid-pac", "state": 3, "reason": "Surplus PV — PAC en état 3" }
|
||||
]
|
||||
```
|
||||
|
||||
- Couvre **toutes** les charges, y compris la PAC. `setpointW`/`currentPowerW` **optionnels**
|
||||
(absents pour une charge à états) ; `loadId` + `reason` **toujours** présents.
|
||||
- Nécessite que l'arbitre **retienne le dernier plan** (aujourd'hui loggé puis jeté,
|
||||
`energyarbitrator.cpp:140-153`) — petite addition d'état.
|
||||
- C'est le canal que consomme la **decision card** de l'app : « pour chaque charge, le pourquoi ».
|
||||
|
||||
---
|
||||
|
||||
## 8. PAC SG-Ready — modèle à états (conservé)
|
||||
|
||||
Hors `etmvariableload`. Le `SgReadyAdapter` existant (états 1–4, combos K1/K2, hystérésis,
|
||||
`minStateHold`) **n'est pas réécrit**. L'arbitre lui envoie un **état** (kind §6), pas un setpoint W.
|
||||
Repli L2 : état 2 (normal, mains off), jamais état 1 (blocage) — comportement actuel conservé.
|
||||
|
||||
> Une PAC pilotée par Modbus qui exposerait une vraie puissance pilotable pourra, *plus tard*,
|
||||
> implémenter `etmvariableload`. Tant qu'on pilote par signal SG-Ready, c'est un modèle à états.
|
||||
|
||||
---
|
||||
|
||||
## 9. Sécurité (déjà en place)
|
||||
|
||||
- **Watchdog L2** (`evaluateMeterFreshness`, 90 s, `applyDegradedMode`) existant. En dégradé,
|
||||
repli charge pilotée = **`setPowerSetpoint(0)` `force=true`** (remplace l'ECS stage-0 actuel) ;
|
||||
PAC → état 2. Conserver l'ordre `update()` (sécurité avant planif) intact.
|
||||
- `enabled:false` → rôle déclaré mais **exclu** de l'arbitrage (aucune action envoyée).
|
||||
|
||||
---
|
||||
|
||||
## 10. Dépendances liées (à coder en même temps, côté plugin)
|
||||
|
||||
- Handler `nymeaenergyjsonhandler` : `Get/SetLoadConfig` + persistance.
|
||||
- `loadActions[]` dans `GetChargingSchedules` / `ChargingSchedulesChanged`.
|
||||
- Câbler l'arbitre pour construire ses adapters depuis `LoadConfig` au lieu du registre en dur
|
||||
(`energypluginnymea.cpp:58-80` — le « 3g »).
|
||||
138
energyplugin/etm/adapters/etmvariableloadadapter.cpp
Normal file
138
energyplugin/etm/adapters/etmvariableloadadapter.cpp
Normal file
@ -0,0 +1,138 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync
|
||||
|
||||
#include "etmvariableloadadapter.h"
|
||||
#include "plugininfo.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <algorithm>
|
||||
#include <integrations/thingmanager.h>
|
||||
#include <integrations/thing.h>
|
||||
#include <types/action.h>
|
||||
#include <types/param.h>
|
||||
|
||||
EtmVariableLoadAdapter::EtmVariableLoadAdapter(ThingManager *thingManager,
|
||||
const QString &id,
|
||||
const QString &label,
|
||||
const QList<int> &powerLevels,
|
||||
int maxPowerW,
|
||||
int priority,
|
||||
const LoadNeeds &needs,
|
||||
QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_thingManager(thingManager)
|
||||
, m_id(id)
|
||||
, m_label(label)
|
||||
, m_powerLevels(powerLevels)
|
||||
, m_maxPowerW(maxPowerW)
|
||||
, m_priority(priority)
|
||||
, m_needs(needs)
|
||||
{
|
||||
// Re-tri par sécurité : l'UI émet trié, l'energymanager re-trie (contrat §6 cas limites).
|
||||
std::sort(m_powerLevels.begin(), m_powerLevels.end());
|
||||
// Contrat §2 : les paliers incluent 0. En *fixed* (liste non vide), 0 doit être présent.
|
||||
Q_ASSERT(m_powerLevels.isEmpty() || m_powerLevels.first() == 0);
|
||||
}
|
||||
|
||||
LoadDescriptor EtmVariableLoadAdapter::descriptor() const
|
||||
{
|
||||
LoadDescriptor d;
|
||||
d.id = m_id;
|
||||
d.label = m_label;
|
||||
d.adapter = QStringLiteral("etmvariableload");
|
||||
d.priority = m_priority;
|
||||
|
||||
d.declared.powerLevels = m_powerLevels;
|
||||
d.declared.maxPowerW = m_maxPowerW;
|
||||
d.needs = m_needs;
|
||||
|
||||
d.supportedKinds = { LoadAction::Setpoint };
|
||||
return d;
|
||||
}
|
||||
|
||||
LoadTelemetry EtmVariableLoadAdapter::telemetry() const
|
||||
{
|
||||
LoadTelemetry t;
|
||||
Thing *thing = m_thingManager->findConfiguredThing(ThingId(m_id));
|
||||
t.available = (thing != nullptr);
|
||||
t.currentPowerW = readCurrentPowerW(); // juge runtime (contrat §4)
|
||||
t.lastActionAt = m_lastActionAt;
|
||||
return t;
|
||||
}
|
||||
|
||||
LoadContext EtmVariableLoadAdapter::toLoadContext(const QDateTime &now) const
|
||||
{
|
||||
Q_UNUSED(now) // aucune fenêtre de verrou côté moteur : l'anti-rebond vit dans le thing.
|
||||
|
||||
LoadContext ctx;
|
||||
ctx.id = m_id;
|
||||
ctx.adapter = QStringLiteral("etmvariableload");
|
||||
ctx.label = m_label;
|
||||
ctx.priority = m_priority;
|
||||
ctx.declared = descriptor().declared;
|
||||
|
||||
ctx.telemetry.currentPowerW = readCurrentPowerW();
|
||||
return ctx;
|
||||
}
|
||||
|
||||
LoadAction EtmVariableLoadAdapter::applyAction(const LoadAction &action, const QDateTime &now)
|
||||
{
|
||||
if (action.kind != LoadAction::Setpoint)
|
||||
return action;
|
||||
|
||||
if (action.reason.isEmpty()) {
|
||||
qCWarning(dcNymeaEnergy()) << "[EtmVariableLoadAdapter]" << m_label
|
||||
<< "— LoadAction sans reason rejetée.";
|
||||
return action;
|
||||
}
|
||||
|
||||
// Second filet (invariant ILoadAdapter) : borner la consigne au plafond physique.
|
||||
// L'arrondi au powerLevels (mode *fixed*) est fait par le scheduler (contrat §4, T3).
|
||||
const double setpointW = qBound(0.0, action.powerW, static_cast<double>(m_maxPowerW));
|
||||
|
||||
qCInfo(dcNymeaEnergy()) << "[EtmVariableLoadAdapter]" << m_label
|
||||
<< "→ setpoint" << qRound(setpointW) << "W"
|
||||
<< (action.force ? "(force)" : "")
|
||||
<< "|" << action.reason;
|
||||
|
||||
writeSetpoint(setpointW);
|
||||
|
||||
m_currentSetpointW = setpointW;
|
||||
m_lastActionAt = now;
|
||||
|
||||
LoadAction applied = action;
|
||||
applied.powerW = setpointW;
|
||||
applied.estimatedPowerW = setpointW;
|
||||
return applied;
|
||||
}
|
||||
|
||||
// ---- privé ---------------------------------------------------------------
|
||||
|
||||
double EtmVariableLoadAdapter::readCurrentPowerW() const
|
||||
{
|
||||
Thing *thing = m_thingManager->findConfiguredThing(ThingId(m_id));
|
||||
if (!thing)
|
||||
return 0.0;
|
||||
if (thing->thingClass().stateTypes().findByName("currentPowerW").id().isNull())
|
||||
return 0.0;
|
||||
return thing->stateValue("currentPowerW").toDouble();
|
||||
}
|
||||
|
||||
void EtmVariableLoadAdapter::writeSetpoint(double powerW)
|
||||
{
|
||||
Thing *thing = m_thingManager->findConfiguredThing(ThingId(m_id));
|
||||
if (!thing) {
|
||||
qCWarning(dcNymeaEnergy()) << "[EtmVariableLoadAdapter]" << m_label
|
||||
<< "— thing non trouvé:" << m_id;
|
||||
return;
|
||||
}
|
||||
|
||||
StateType setpointStateType = thing->thingClass().stateTypes().findByName("powerSetpoint");
|
||||
if (!setpointStateType.id().isNull()) {
|
||||
Action setpointAction(setpointStateType.id(), thing->id(), Action::TriggeredByRule);
|
||||
setpointAction.setParams(ParamList() << Param(setpointStateType.id(), powerW));
|
||||
m_thingManager->executeAction(setpointAction);
|
||||
} else {
|
||||
thing->setStateValue("powerSetpoint", powerW); // repli mock
|
||||
}
|
||||
}
|
||||
130
energyplugin/etm/adapters/etmvariableloadadapter.h
Normal file
130
energyplugin/etm/adapters/etmvariableloadadapter.h
Normal file
@ -0,0 +1,130 @@
|
||||
// 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 charge à puissance pilotable — interface \c etmvariableload (contrat
|
||||
* INTERFACE_etmvariableload.md). Action \c kind:Setpoint en WATTS.
|
||||
*
|
||||
* Couvre les charges dont la consigne est une puissance : ECS multipalier, routeur PV /
|
||||
* triac, résistance modulable. \b Pas la PAC SG-Ready (modèle à états, \c SgReadyAdapter).
|
||||
*
|
||||
* \par Frontière (contrat §1 / §5) — non négociable
|
||||
* L'energymanager raisonne UNIQUEMENT en watts. \b Aucune résistance, relais, combinaison
|
||||
* ni index de palier matériel ne traverse cet adaptateur. La connaissance matérielle (quelle
|
||||
* résistance, quel triac, sur quelle phase), l'anti-rebond (minOn/minOff) et l'équilibrage
|
||||
* de phases vivent ENTIÈREMENT dans le \b thing qui implémente l'interface. Cet adaptateur
|
||||
* se contente d'écrire une consigne \c powerSetpoint (W) et de relire \c currentPowerW.
|
||||
*
|
||||
* \par States nymea consommés sur le thing cible (\c m_id)
|
||||
* - \c currentPowerW (read) : puissance réellement appliquée — juge runtime (contrat §4).
|
||||
* - \c powerSetpoint (write) : consigne demandée par l'energymanager.
|
||||
* - \c maxPowerW / \c powerLevels (read) : déclarés par le thing pour le wizard ; la
|
||||
* planification utilise la DÉCLARATION de config (passée au constructeur, contrat §4),
|
||||
* pas ces states — l'adaptateur n'écrase jamais la déclaration.
|
||||
*
|
||||
* \invariant applyAction() rejette silencieusement toute action dont \c reason est vide.
|
||||
* \invariant Seul le kind \c Setpoint est traité ; les autres kinds retournent sans effet.
|
||||
* \invariant La consigne est écrêtée à \c [0, maxPowerW] avant envoi matériel (second filet
|
||||
* après l'écrêtage de l'arbitre). L'arrondi au \c powerLevels (mode *fixed*) est la
|
||||
* responsabilité du SCHEDULER (contrat §4), pas de l'adaptateur.
|
||||
* \invariant **Temps = paramètre, jamais l'horloge** (cf. \c ILoadAdapter) : aucune logique
|
||||
* temporelle ici (les verrous vivent dans le thing) — \c now sert uniquement aux estampilles.
|
||||
*
|
||||
* \note T1 (squelette) : l'adaptateur consomme l'interface côté moteur. La déclaration de
|
||||
* l'interface nymea \c etmvariableload et le driver thing relèvent d'une session device dédiée.
|
||||
*/
|
||||
class EtmVariableLoadAdapter : public QObject, public ILoadAdapter
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
/*!
|
||||
* \brief Constructeur.
|
||||
* \param thingManager Gestionnaire nymea pour résoudre \p id en Thing.
|
||||
* \param id ThingId (string) du thing implémentant \c etmvariableload, et
|
||||
* identifiant logique de la charge.
|
||||
* \param label Nom lisible (logs, app).
|
||||
* \param powerLevels Paliers atteignables en W (DÉCLARÉS, triés croissants, \c 0 inclus)
|
||||
* en mode *fixed* ; liste vide ⇒ *dynamic* (modulation 0..maxPowerW).
|
||||
* \param maxPowerW Plafond physique (W) — contrat §2.
|
||||
* \param priority Rang dans le waterfall (contrat §5 / OPTIMIZER_PROTOCOL §5) : valeur
|
||||
* plus BASSE = servi en premier (rang 1 = premier servi).
|
||||
* \param needs Besoins déclarés (échéances, énergie min) — exposés au scheduler
|
||||
* via \c descriptor().needs. Vide par défaut.
|
||||
* \param parent Propriétaire Qt.
|
||||
*/
|
||||
explicit EtmVariableLoadAdapter(ThingManager *thingManager,
|
||||
const QString &id,
|
||||
const QString &label,
|
||||
const QList<int> &powerLevels,
|
||||
int maxPowerW,
|
||||
int priority,
|
||||
const LoadNeeds &needs = LoadNeeds(),
|
||||
QObject *parent = nullptr);
|
||||
|
||||
/*!
|
||||
* \brief Description statique : adapter="etmvariableload", powerLevels, maxPowerW, priority.
|
||||
* \return LoadDescriptor déclaratif (référence de planification, contrat §4).
|
||||
*/
|
||||
LoadDescriptor descriptor() const override;
|
||||
|
||||
/*!
|
||||
* \brief Télémétrie runtime. \c currentPowerW = state \c currentPowerW MESURÉ du thing
|
||||
* (juge de quantification, contrat §4) ; \c available faux si le thing est absent.
|
||||
*/
|
||||
LoadTelemetry telemetry() const override;
|
||||
|
||||
/*!
|
||||
* \brief Construit l'entrée loads[] §5 (adapter="etmvariableload").
|
||||
* \param now Temps de cycle (\c ctx.timestamp) — estampille uniquement ; aucune fenêtre de
|
||||
* verrou côté moteur (l'anti-rebond vit dans le thing, contrat §2).
|
||||
* \return LoadContext incluant declared (powerLevels/maxPowerW) et \c currentPowerW.
|
||||
*/
|
||||
LoadContext toLoadContext(const QDateTime &now) const override;
|
||||
|
||||
/*!
|
||||
* \brief Écrit la consigne \c powerSetpoint (W) sur le thing.
|
||||
* \param action LoadAction de kind \c Setpoint ; consigne lue dans \c action.powerW. Autres
|
||||
* kinds : retour sans effet.
|
||||
* \param now Temps de cycle (\c ctx.timestamp) — estampille \c m_lastActionAt.
|
||||
* \return L'action après écrêtage matériel (\c powerW borné à [0, maxPowerW]).
|
||||
*
|
||||
* \invariant Si \c action.reason est vide → retour sans effet (log warning).
|
||||
* \invariant \c action.force == true (repli L2 : \c setPowerSetpoint(0)) est transmis tel
|
||||
* quel ; le bypass anti-rebond est honoré par le thing, pas ici.
|
||||
*/
|
||||
LoadAction applyAction(const LoadAction &action, const QDateTime &now) override;
|
||||
|
||||
/*! \brief Dernière consigne (W) effectivement écrite (avant écrêtage thing). */
|
||||
double currentSetpointW() const { return m_currentSetpointW; }
|
||||
|
||||
private:
|
||||
//! Vrai si aucun palier déclaré n'est fourni ⇒ modulation continue (contrat §2).
|
||||
bool isDynamic() const { return m_powerLevels.isEmpty(); }
|
||||
|
||||
//! Écrit le state \c powerSetpoint (W) via executeAction (repli \c setStateValue pour mock).
|
||||
void writeSetpoint(double powerW);
|
||||
|
||||
//! Lit le state \c currentPowerW du thing cible (0 si absent / non exposé).
|
||||
double readCurrentPowerW() const;
|
||||
|
||||
ThingManager *m_thingManager;
|
||||
QString m_id;
|
||||
QString m_label;
|
||||
QList<int> m_powerLevels; //!< Paliers W déclarés (vide = dynamic).
|
||||
int m_maxPowerW;
|
||||
int m_priority;
|
||||
LoadNeeds m_needs;
|
||||
|
||||
double m_currentSetpointW = 0; //!< Dernière consigne écrite (W).
|
||||
QDateTime m_lastActionAt;
|
||||
};
|
||||
@ -8,6 +8,7 @@ HEADERS += \
|
||||
$$PWD/adapters/evadapter.h \
|
||||
$$PWD/adapters/ecsrelayadapter.h \
|
||||
$$PWD/adapters/sgreadyadapter.h \
|
||||
$$PWD/adapters/etmvariableloadadapter.h \
|
||||
$$PWD/scheduler/rulebasedscheduler.h \
|
||||
$$PWD/energyarbitrator.h \
|
||||
|
||||
@ -15,5 +16,6 @@ SOURCES += \
|
||||
$$PWD/adapters/evadapter.cpp \
|
||||
$$PWD/adapters/ecsrelayadapter.cpp \
|
||||
$$PWD/adapters/sgreadyadapter.cpp \
|
||||
$$PWD/adapters/etmvariableloadadapter.cpp \
|
||||
$$PWD/scheduler/rulebasedscheduler.cpp \
|
||||
$$PWD/energyarbitrator.cpp \
|
||||
|
||||
@ -23,6 +23,16 @@ struct LoadDeclared {
|
||||
//! 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).
|
||||
//! C'est la DÉCLARATION installateur (référence de planification, contrat §5) — distincte
|
||||
//! de la conso runtime \c currentPowerW.
|
||||
QList<int> powerLevels;
|
||||
//! Plafond physique de la charge (W) — contrat etmvariableload §2 \c maxPowerW. REQUIS en
|
||||
//! *dynamic* ; sert aussi de plafond en *fixed*.
|
||||
double maxPowerW = 0;
|
||||
|
||||
// --- battery ---
|
||||
double maxChargeW = 0; //!< Puissance max de charge (W).
|
||||
double maxDischargeW = 0; //!< Puissance max de décharge (W).
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user