diff --git a/energyplugin/energyplugin.pri b/energyplugin/energyplugin.pri index 0304b4d..216967a 100644 --- a/energyplugin/energyplugin.pri +++ b/energyplugin/energyplugin.pri @@ -37,6 +37,8 @@ HEADERS += \ $$PWD/types/smartchargingstate.h \ $$PWD/types/timeframe.h \ +include($$PWD/etm/etm.pri) + SOURCES += \ $$PWD/energymanagerconfiguration.cpp \ $$PWD/energysettings.cpp \ diff --git a/energyplugin/etm/adapters/iloadadapter.h b/energyplugin/etm/adapters/iloadadapter.h new file mode 100644 index 0000000..5aa1cac --- /dev/null +++ b/energyplugin/etm/adapters/iloadadapter.h @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#pragma once + +#include +#include "../types/loadaction.h" +#include "../types/loaddescriptor.h" +#include "../types/surpluscontext.h" + +// Vue runtime minimale qu'un adaptateur expose à l'arbitre. +struct LoadTelemetry { + double currentPowerW = 0; + bool available = true; // faux si l'appareil nymea est absent/erreur + QDateTime lastActionAt; +}; + +// Interface pure — les implémentations concrètes héritent de QObject + ILoadAdapter. +// Signaux (telemetryChanged, descriptorChanged) déclarés dans les classes concrètes. +class ILoadAdapter { +public: + virtual ~ILoadAdapter() = default; + + // Déclaration statique : capacités, limites, priorité, needs. + virtual LoadDescriptor descriptor() const = 0; + + // Télémétrie runtime (courant, disponibilité, dernière action). + virtual LoadTelemetry telemetry() const = 0; + + // Construit l'entrée §5 loads[] pour SurplusContext. + // Inclut declared, telemetry type-spécifique, learned courant. + virtual LoadContext toLoadContext() const = 0; + + // Applique l'action. Retourne ce qui a réellement été appliqué + // (après écrêtage matériel). L'arbitre a déjà écrêté selon les limites + // et le budget — c'est le second filet. + virtual LoadAction applyAction(const LoadAction &action) = 0; +}; diff --git a/energyplugin/etm/etm.pri b/energyplugin/etm/etm.pri new file mode 100644 index 0000000..561d156 --- /dev/null +++ b/energyplugin/etm/etm.pri @@ -0,0 +1,7 @@ +HEADERS += \ + $$PWD/types/loadaction.h \ + $$PWD/types/loaddescriptor.h \ + $$PWD/types/surpluscontext.h \ + $$PWD/types/plan.h \ + $$PWD/adapters/iloadadapter.h \ + $$PWD/scheduler/ischeduler.h \ diff --git a/energyplugin/etm/scheduler/ischeduler.h b/energyplugin/etm/scheduler/ischeduler.h new file mode 100644 index 0000000..a679560 --- /dev/null +++ b/energyplugin/etm/scheduler/ischeduler.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#pragma once + +#include "../types/surpluscontext.h" +#include "../types/plan.h" + +// Interface pure — implémentations concrètes héritent de QObject + IScheduler. +// +// Règle : getPlan() DOIT retourner immédiatement (modèle cache §AGENTS invariant 5). +// SocketScheduler renvoie son dernier plan en cache et recalcule en fond. +// SocketScheduler embarque un RuleBasedScheduler comme fallback et renvoie +// TOUJOURS un Plan exploitable — l'abstain du protocole n'atteint jamais l'arbitre. +class IScheduler { +public: + virtual ~IScheduler() = default; + + virtual Plan getPlan(const SurplusContext &ctx) = 0; +}; diff --git a/energyplugin/etm/types/loadaction.h b/energyplugin/etm/types/loadaction.h new file mode 100644 index 0000000..b2c4456 --- /dev/null +++ b/energyplugin/etm/types/loadaction.h @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#pragma once + +#include + +// Noms de champs : identiques à OPTIMIZER_PROTOCOL.md §6 (font autorité). +// `funding` est interne à l'arbitre — absent du JSON protocole. +struct LoadAction { + enum Kind { Setpoint, Stage, State, Constraint }; + enum Funding { Surplus, Grid }; // interne, non sérialisé + enum Source { Solar, GridSource }; // "solar"|"grid" — Setpoint battery + enum Permission { Allow, Forbid }; // "allow"|"forbid" — Constraint + + QString loadId; + Kind kind = Setpoint; + Funding funding = Surplus; + + // Setpoint — evcharger + bool chargingEnabled = false; + double currentA = 0; + uint phaseCount = 0; + // Setpoint — battery + double powerW = 0; + Source source = Solar; + // Stage — relay-stages (ECS) + int stage = 0; + // State — sg-ready + int state = 0; + // Constraint — battery v1 + Permission charge = Allow; + Permission discharge = Allow; + + QString reason; // obligatoire, non vide, français (AGENTS invariant 7) + double estimatedPowerW = 0; // hint pour comptabilité arbitre (rempli par le scheduler) +}; diff --git a/energyplugin/etm/types/loaddescriptor.h b/energyplugin/etm/types/loaddescriptor.h new file mode 100644 index 0000000..f4ebb2b --- /dev/null +++ b/energyplugin/etm/types/loaddescriptor.h @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#pragma once + +#include +#include +#include +#include "loadaction.h" + +// Capacités déclarées par l'installateur (plaque signalétique, câblage). +// Noms de champs : OPTIMIZER_PROTOCOL.md §5 loads[].declared +struct LoadDeclared { + // evcharger + double minA = 0; + double maxA = 0; + int phases = 0; + // relay-stages (ECS) — puissances en W : [0, 1200, 2400] + QList stages; + // battery + double maxChargeW = 0; + double maxDischargeW = 0; + double capacityWh = 0; + int reserveSocPercent = 0; + // sg-ready : états toujours 1-4, pas de déclaration nécessaire +}; + +// OPTIMIZER_PROTOCOL.md §5 loads[].limits +struct LoadLimits { + int minOnS = 0; + int minOffS = 0; + int chargingEnabledLockS = 0; // evcharger + int currentChangeLockS = 0; // evcharger + int minStateHoldS = 0; // sg-ready +}; + +// OPTIMIZER_PROTOCOL.md §5 loads[].needs +struct LoadNeeds { + int targetSocPercent = 0; + QDateTime deadline; + QString dailyDeadline; // "18:00" + int minEnergyWhPerDay = 0; +}; + +// Déclaration statique qu'un ILoadAdapter expose à l'arbitre. +struct LoadDescriptor { + QString id; + QString label; + QString adapter; // "evcharger"|"relay-stages"|"sg-ready"|"battery" + int priority = 0; + LoadDeclared declared; + LoadLimits limits; + LoadNeeds needs; + QList supportedKinds; +}; diff --git a/energyplugin/etm/types/plan.h b/energyplugin/etm/types/plan.h new file mode 100644 index 0000000..a349505 --- /dev/null +++ b/energyplugin/etm/types/plan.h @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#pragma once + +#include +#include +#include +#include "loadaction.h" + +// OPTIMIZER_PROTOCOL.md §6 (noms de champs identiques). + +struct Slot { + QDateTime from; + QDateTime to; + QList actions; // ordonnées par priorité de LoadDescriptor +}; + +struct Plan { + QString planId; + QString strategy; + QList slots; + + // Créneau couvrant dt — créneau vide (from==to==invalid) si aucun. + Slot slotCovering(const QDateTime &dt) const { + for (const Slot &s : slots) { + if (dt >= s.from && dt < s.to) + return s; + } + return {}; + } + + bool isValid() const { return !slots.isEmpty(); } +}; diff --git a/energyplugin/etm/types/surpluscontext.h b/energyplugin/etm/types/surpluscontext.h new file mode 100644 index 0000000..5b44e49 --- /dev/null +++ b/energyplugin/etm/types/surpluscontext.h @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +// Copyright (C) 2025 - 2026, Patrick Schurig / ETM PowerSync +#pragma once + +#include +#include +#include +#include "loaddescriptor.h" + +// Structures miroirs de OPTIMIZER_PROTOCOL.md §5 (noms de champs identiques). + +struct SurplusSite { + double contractedPowerW = 0; + QList phaseLimitA; // [63.0, 63.0, 63.0] +}; + +struct SurplusMeter { + double importW = 0; + double exportW = 0; + QList perPhaseA; +}; + +struct SurplusPv { + double currentW = 0; +}; + +struct SurplusBattery { + bool present = false; + double socPercent = 0; + double powerW = 0; + double capacityWh = 0; + int reserveSocPercent = 0; + double maxChargeW = 0; + double maxDischargeW = 0; +}; + +struct TariffEntry { + QDateTime from; + QString label; + double priceCtkWh = 0; +}; + +struct SurplusTariff { + QString provider; + TariffEntry current; + QList next; +}; + +// Télémétrie d'une charge dans le contexte §5 loads[].telemetry +struct LoadContextTelemetry { + double currentPowerW = 0; + // evcharger + bool pluggedIn = false; + bool charging = false; + double sessionWh = 0; + // relay-stages + int stage = 0; + // sg-ready + int state = 0; + // battery / electricvehicle + double socPercent = 0; + QDateTime lastSwitch; +}; + +// Données apprises par l'optimiseur §8 (renvoyées pour persistance) +struct LoadLearned { + double dailyEnergyWh = 0; + double confidence = 0.0; // 0–1 ; < 0.7 → "profil en apprentissage" +}; + +// Entrée loads[] dans le SurplusContext envoyé au scheduler. +// Construit par l'arbitre depuis ILoadAdapter::descriptor() + telemetry(). +struct LoadContext { + QString id; + QString adapter; // "evcharger"|"relay-stages"|"sg-ready"|"battery" + QString label; + int priority = 0; + LoadDeclared declared; + LoadLearned learned; + LoadContextTelemetry telemetry; + LoadNeeds needs; + LoadLimits limits; +}; + +// OPTIMIZER_PROTOCOL.md §5 — transmis au scheduler à chaque cycle. +struct SurplusContext { + QDateTime timestamp; + SurplusSite site; + SurplusMeter meter; + SurplusPv pv; + SurplusBattery battery; + QList loads; + SurplusTariff tariff; + // forecast : opaque, transmis tel quel si présent — réservé V2 +};