Documentation — nymea-energy-plugin-nymea
Branche : nymea-energy-plugin-etm
Date : 2026-02-23
Auteur : Analyse ETM / Claude Code
Table des matières
- Vue d'ensemble
- Structure du repo
- Things & UUIDs
- API JSON-RPC — NymeaEnergy
- Flow de données
- Configuration requise
- Dépendances
- Intégration depuis etm_powersync_app
- Plan d'extension — PAC / ECS / Gestion d'énergie
1. Vue d'ensemble
Objectif du plugin
nymea-energy-plugin-nymea est un Energy Plugin pour la plateforme nymea. Il ne crée pas lui-même des appareils (Things) — il orchestre les appareils existants (chargeurs VE, compteurs d'énergie, véhicules électriques, stockages) pour offrir des fonctionnalités avancées de gestion d'énergie :
| Fonctionnalité |
Description |
| Smart Charging |
Optimisation de la charge VE selon le surplus solaire, le tarif spot, ou une heure cible |
| Spot Market Charging |
Planification aux heures les moins chères (aWATTar API — Autriche & Allemagne) |
| Overload Protection |
Limitation automatique du courant pour ne pas dépasser la limite de puissance par phase |
| API JSON-RPC |
Namespace NymeaEnergy (v0–8) pour le contrôle à distance par les applications |
Protocoles utilisés
| Protocole |
Direction |
Usage |
| JSON-RPC 2.0 sur WebSocket |
Bidirectionnel |
API principale : apps clients ↔ nymea |
| HTTP REST |
Sortant |
Récupération des prix spot aWATTar (toutes les 60 s) |
| Qt Signals/Slots |
Interne |
Communication entre managers C++ |
| QSettings (INI) |
Local |
Persistance configuration (energy.conf) |
| JSON |
Local |
Configuration runtime (energy-manager-configuration.json) |
Type de plugin — distinction importante
IntegrationPlugin EnergyPlugin (ce plugin)
│ │
▼ ▼
setupThing() init() uniquement
thingRemoved() Écoute ThingManager
executeAction() Orchestre les Things
→ Crée des Things → Utilise des Things
créées ailleurs
2. Structure du repo
nymea-energy-plugin-nymea/
│
├── energyplugin/ # Code source principal
│ ├── energypluginnymea.h/.cpp # Point d'entrée : EnergyPlugin → init()
│ ├── energyplugin.pro # Build qmake (lib partagée)
│ ├── energyplugin.pri # Config Qt5/Qt6, coverage
│ ├── plugininfo.h # Catégorie de log : dcNymeaEnergy
│ ├── energysettings.h/.cpp # QSettings → energy.conf
│ ├── energymanagerconfiguration.h/.cpp # Config runtime JSON (locks, limites)
│ │
│ ├── smartchargingmanager.h/.cpp # ★ Moteur central du smart charging
│ ├── evcharger.h/.cpp # Wrapper Thing → chargeur VE
│ ├── rootmeter.h/.cpp # Wrapper Thing → compteur principal
│ ├── nymeaenergyjsonhandler.h/.cpp # ★ Handler JSON-RPC ("NymeaEnergy")
│ │
│ ├── spotmarket/
│ │ ├── spotmarketdataprovider.h/.cpp # Interface abstraite fournisseur
│ │ ├── spotmarketdataproviderawattar.h/.cpp # Implémentation aWATTar REST
│ │ └── spotmarketmanager.h/.cpp # Gestion fournisseur + scheduler
│ │
│ ├── types/ # Value types (Q_GADGET, JSON-RPC ready)
│ │ ├── charginginfo.h/.cpp # Config par chargeur (mode, cible, spot)
│ │ ├── chargingaction.h/.cpp # Action à appliquer (enabled, A, phases)
│ │ ├── chargingschedule.h/.cpp # Créneau planifié pour un chargeur
│ │ ├── chargingprocessinfo.h/.cpp # État interne processus de charge
│ │ ├── scoreentry.h/.cpp # Créneau spot (TimeFrame + prix + score)
│ │ ├── smartchargingstate.h/.cpp # État global du smart charging
│ │ └── timeframe.h/.cpp # Plage horaire (début/fin + helpers)
│ │
│ └── translations/ # .ts (de/en_US) → .qm via lrelease
│
├── tests/
│ ├── auto/
│ │ ├── common/ # EnergyTestBase partagée (nymea-tests)
│ │ ├── charging/ # → binaire : nymeaenrgytestcharging
│ │ ├── spotmarket/ # → binaire : nymeaenrgytestspotmarket
│ │ └── simulation/ # → binaire : nymea-energy-simulation (ENERGY_SIMULATION)
│ └── mocks/
│ ├── plugins/energymocks/ # Plugin d'intégration mock (JSON + C++)
│ └── spotmarketprovider/ # Mock spot market (datasets JSON statiques)
│
├── extension/ # ★ NOUVEAU — Fichiers d'extension ETM
│ ├── heatpump_dhw_thingclasses.json # ThingClasses PAC + ECS
│ ├── heatpumpmanager.h/.cpp # Gestionnaire PAC + ECS
│ ├── energyprioritymanager.h # Gestionnaire priorité énergie
│ ├── nymeaenergyjsonhandler_extension.cpp # Nouvelles méthodes RPC
│ ├── energypluginnymea_extension_diff.cpp # Diff init()
│ └── PLAN_EXTENSION.md # Plan détaillé avec UUIDs
│
├── integration_app_examples/ # ★ NOUVEAU — Exemples Dart/Flutter
│ ├── nymea_client.dart # Client WebSocket JSON-RPC complet
│ └── examples_usage.dart # 6 exemples d'utilisation
│
├── debian-qt5/, debian-qt6/ # Packaging Debian
├── nymea-energy-plugin-nymea.pro # Racine qmake (subdirs)
├── docker-simulation.sh # Simulations dans Docker
├── generate-coverage-report.sh # Rapport lcov HTML
├── doc.md # ★ Ce fichier
├── README.md
└── CLAUDE.md
3. Things & UUIDs
Rappel architectural : ce plugin Energy ne définit pas ses propres ThingClasses.
Il interagit avec des Things créées par des plugins d'intégration qui implémentent les
interfaces nymea : evcharger, energymeter, electricvehicle, energystorage.
Le plugin mock de test (a45e07fc-6ccc-40af-b7ad-bac4a003e775) simule ces appareils.
Identifiants du plugin mock de test
| Champ |
Valeur |
| Plugin ID |
a45e07fc-6ccc-40af-b7ad-bac4a003e775 |
| Vendor ID |
2062d64d-3232-433c-88bc-0d33c0ba2ba6 |
| Vendor Name |
nymea |
ThingClass : Meter (Compteur d'énergie)
| Champ |
Valeur |
| ID |
2721a051-6e12-471a-baba-21d87c4cebc9 |
| Interfaces |
energymeter, connectable |
Param de configuration
| Nom |
UUID |
Type |
Défaut |
port |
7abcc8a1-08b1-45bc-9116-10f9848359f9 |
uint |
6655 |
States
| Nom |
UUID |
Type |
Unité |
Writable |
Défaut |
connected |
3b393e45-594d-436a-bbd3-1f9b18ad9cfe |
bool |
— |
— |
false |
voltagePhaseA |
db018146-0441-4dc0-9834-6d43ebaf8311 |
double |
Volt |
— |
230 |
voltagePhaseB |
f0bae0af-2cde-4615-a36b-c81d7b233ebe |
double |
Volt |
— |
230 |
voltagePhaseC |
fb76f2a8-0ace-4655-b368-1508843a15c6 |
double |
Volt |
— |
230 |
currentPhaseA |
00668c48-8b12-449a-907b-6744d65b021e |
double |
Ampere |
— |
0 |
currentPhaseB |
6cd08b22-3a54-43a8-b828-b6ebb49678bb |
double |
Ampere |
— |
0 |
currentPhaseC |
c9e196ec-0b59-43df-9ca6-4e318a63bf0f |
double |
Ampere |
— |
0 |
currentPower |
f0490dd9-79ac-41ff-a952-083ed683687d |
double |
Watt |
— |
0 |
currentPowerPhaseA |
ae450e65-2bbd-4054-84d6-b8f766b3d7cf |
double |
Watt |
— |
0 |
currentPowerPhaseB |
dde0c5cf-9ce7-4e0d-979c-56d52d31eb25 |
double |
Watt |
— |
0 |
currentPowerPhaseC |
cfdc65bf-8e5a-40dd-acf3-79f3b86fd808 |
double |
Watt |
— |
0 |
totalEnergyConsumed |
8945c576-1e13-4611-adc8-4123b18d3a70 |
double |
kWh |
— |
0 |
totalEnergyProduced |
0420b758-e77f-4cf5-a30b-a6e1235b1efd |
double |
kWh |
— |
0 |
originalPower (sim) |
e9776745-6f43-408d-9a4c-e5d74c711800 |
double |
Watt |
oui |
0 |
ThingClass : Charger (Chargeur VE)
| Champ |
Valeur |
| ID |
5a3ae99f-c7da-46df-9104-f477be4606b7 |
| Interfaces |
evcharger, smartmeterconsumer, connectable |
Params de configuration
| Nom |
UUID |
Type |
Valeurs possibles |
Défaut |
port |
652624a2-8f9a-4bc3-b34f-5e3492af4d30 |
uint |
— |
6656 |
phases |
facd5c76-d15e-4e29-9929-5e1764ae05dc |
QString |
A / B / C / AB / BC / AC / ABC |
"A" |
maxChargingCurrentUpperLimit |
234c6676-1ec0-4eff-bed0-ecee7ce82074 |
double |
Ampere |
32 |
States
| Nom |
UUID |
Type |
Unité |
Writable |
Min |
Max |
Défaut |
connected |
6c18e134-0420-41b3-974c-869b5e7125e4 |
bool |
— |
— |
— |
— |
false |
power |
13672543-9344-4d55-afd6-6393ae052f18 |
bool |
— |
oui |
— |
— |
true |
maxChargingCurrent |
e7566b5b-8258-486e-b0ed-42a1bee332d9 |
uint |
A |
oui |
6 |
32 |
6 |
pluggedIn |
13f8f008-aa70-4772-9fb8-81a9674dd6ad |
bool |
— |
— |
— |
— |
false |
charging |
63879844-6342-45ed-8e97-276e0f3092e5 |
bool |
— |
— |
— |
— |
false |
phaseCount |
728aa4b2-0c90-40da-9a46-6f07ab6a1497 |
uint |
— |
— |
1 |
3 |
1 |
usedPhases |
cc3abc60-42e1-421d-b32e-37c2c9a113a3 |
QString |
A…ABC |
— |
— |
— |
"A" |
currentPower |
89d5bab8-3fad-41e4-a3cb-55cd673bbb6c |
double |
W |
— |
— |
— |
0 |
totalEnergyConsumed |
aadf7384-5953-48b3-aedc-5c3835a61639 |
double |
kWh |
— |
— |
— |
0 |
voltagePhaseA/B/C |
9eb201d2… / 06be3d4e… / e327de62… |
double |
V |
— |
— |
— |
230 |
currentPhaseA/B/C |
025298dc… / ed7ee826… / 7ab9b93a… |
double |
A |
— |
— |
— |
0 |
currentPowerPhaseA/B/C |
09e9a514… / 4f27a8f1… / 859d3a00… |
double |
W |
— |
— |
— |
0 |
Actions
| Nom |
UUID |
Description |
update |
924174ed-e2ed-4d28-b2de-750cf01e41e3 |
Mise à jour manuelle des états (test) |
ThingClass : ChargerPhaseSwitching (Chargeur avec commutation de phases)
| Champ |
Valeur |
| ID |
9208d9f0-280c-469d-a145-106f3277470c |
| Interfaces |
evcharger, smartmeterconsumer, connectable |
Identique à Charger, avec un state supplémentaire :
| State |
UUID |
Type |
Valeurs |
Writable |
desiredPhaseCount |
b3c4618a-223f-4c97-80e8-04a2fb490083 |
uint |
1, 3 |
oui |
ThingClass : SimpleCharger (Chargeur sans mesure)
| Champ |
Valeur |
| ID |
29bcf255-b654-4764-be92-399bc26fe7c3 |
| Interfaces |
evcharger, connectable |
| States |
connected (bool), power (bool, writable), maxChargingCurrent (uint 6–32 A, writable) |
ThingClass : Car (Véhicule électrique)
| Champ |
Valeur |
| ID |
4513f801-836e-40a7-8784-c02650a9bdc6 |
| Interface |
electricvehicle |
Settings (persistants par véhicule)
| Nom |
UUID |
Type |
Min |
Max |
Défaut |
minChargingCurrent |
1bb8e350-0e9f-4ab5-b814-8fd4ac8900a0 |
uint (A) |
4 |
16 |
6 |
capacity |
0f76f85f-7c53-48ce-9396-ea19c5aa16aa |
uint (kWh) |
5 |
200 |
50 |
phaseCount |
01978cc7-ceed-4332-87ca-937f366c6d51 |
uint |
1 |
3 |
1 |
chargingEnergyLoss |
08b07382-35a5-4b40-8e43-321e12fbd2ce |
uint (%) |
5 |
35 |
10 |
States
| Nom |
UUID |
Type |
Unité |
Writable |
capacity |
ce46c9d2-00d9-46f1-bc4f-9f2569393c70 |
double |
kWh |
— |
batteryLevel |
8aeadf2e-4d5a-4a38-a5a5-299c5b751b9e |
int |
% |
oui |
batteryCritical |
2d6308d4-6ac1-43af-a374-b0ff79dfcb46 |
bool |
— |
— |
minChargingCurrent |
2536227c-9b17-462d-8d87-df2fb80eb72c |
uint |
A |
— |
phaseCount |
96b9ce94-ed47-46db-bacd-2dcb5333031c |
uint |
— |
— |
ThingClass : EnergyStorage (Stockage d'énergie / Batterie)
| Champ |
Valeur |
| ID |
d0d5bbf0-249c-46ed-ac6a-5f271b2b0b0f |
| Interface |
energystorage |
States
| Nom |
UUID |
Type |
Unité |
Writable |
capacity |
dfa1f2d2-793e-46a4-8e54-b3ffa40e343e |
double |
kWh |
— |
batteryLevel |
f218bb34-913a-4f7f-b811-e9d0324d8d37 |
int |
% |
oui |
currentPower |
7577d0eb-2d4d-41e9-bd59-f750613c265a |
double |
W |
— |
batteryCritical |
602902a9-9487-4de8-bffb-58121bebd89a |
bool |
— |
— |
ThingClass : Notification
| Champ |
Valeur |
| ID |
ee1871b8-46f9-4784-bbe1-e33db16b8753 |
| Interface |
notifications |
Action : notify
| Param |
UUID |
Type |
title |
801eb856-9eb3-43f5-9528-ead363bf3fd5 |
QString |
body |
06b8bebc-1ed9-4c19-8146-b9bc032fbcb0 |
QString |
4. API JSON-RPC — Namespace NymeaEnergy
Le handler est enregistré sous "NymeaEnergy" versions 0 à 8 via :
jsonRpcServer()->registerExperienceHandler(handler, 0, 8);
4.1 Méthodes — Smart Charging
| Méthode |
Params |
Retour |
Description |
GetPhasePowerLimit |
— |
phasePowerLimit: uint |
Limite courant par phase (A). 0 = désactivé |
SetPhasePowerLimit |
phasePowerLimit: uint |
energyError |
Définit la limite (désactive le smart charging si 0) |
GetAcquisitionTolerance |
— |
acquisitionTolerance: double |
Seuil surplus pour démarrer la charge [0.0–1.0] |
SetAcquisitionTolerance |
acquisitionTolerance: double |
energyError |
Modifie le seuil |
GetBatteryLevelConsideration |
— |
batteryLevelConsideration: double |
Part du stockage prise en compte [0.0–1.0] |
SetBatteryLevelConsideration |
batteryLevelConsideration: double |
energyError |
Modifie le paramètre |
GetLockOnUnplug |
— |
lockOnUnplug: bool |
Verrou au débranchement |
SetLockOnUnplug |
lockOnUnplug: bool |
energyError |
Active/désactive le verrou |
GetChargingInfos |
o:evChargerId: uuid |
chargingInfos: [] |
Config de charge de tous les chargeurs (ou un seul) |
SetChargingInfo |
chargingInfo: object |
energyError |
Modifie la config d'un chargeur (partial update) |
GetChargingSchedules |
— |
chargingSchedules: [] |
Plannings de charge calculés |
4.2 Méthodes — Spot Market
| Méthode |
Params |
Retour |
Description |
GetAvailableSpotMarketProviders |
— |
providers: [] |
Liste des fournisseurs disponibles |
GetSpotMarketConfiguration |
— |
enabled, available, o:providerId |
Config spot market actuelle |
SetSpotMarketConfiguration |
enabled: bool, o:providerId: uuid |
energyError |
Active/change le fournisseur |
GetSpotMarketScoreEntries |
— |
spotMarketScoreEntries: [] |
Scores pondérés (0=pire → 1=meilleur) |
4.3 Fournisseurs Spot Market intégrés
| Nom |
UUID |
Pays |
URL API |
| aWATTar Austria |
5196b3cc-b2ee-46d6-b63a-7af2cf70ba67 |
Autriche |
https://api.awattar.at/v1/marketdata |
| aWATTar Germany |
0ca6ad88-e243-438d-a0f8-986cecf61834 |
Allemagne |
https://api.awattar.de/v1/marketdata |
4.4 Notifications push (WebSocket)
| Notification |
Payload |
Déclencheur |
PhasePowerLimitChanged |
phasePowerLimit: uint |
Changement limite phase |
AcquisitionToleranceChanged |
acquisitionTolerance: double |
Changement seuil acquisition |
BatteryLevelConsiderationChanged |
batteryLevelConsideration: double |
Changement considération batterie |
LockOnUnplugChanged |
lockOnUnplug: bool |
Changement verrou |
ChargingInfoAdded |
chargingInfo: object |
Nouveau chargeur détecté |
ChargingInfoRemoved |
evChargerThingId: uuid |
Chargeur supprimé |
ChargingInfoChanged |
chargingInfo: object |
Config chargeur modifiée |
ChargingSchedulesChanged |
chargingSchedules: [] |
Plannings recalculés |
SpotMarketConfigurationChanged |
enabled, available, o:providerId |
Config spot modifiée |
SpotMarketStatusChanged |
enabled, available |
Statut spot (enabled/available) |
SpotMarketScoreEntriesChanged |
spotMarketScoreEntries: [] |
Nouveaux prix spot reçus |
4.5 Enums du namespace NymeaEnergy
ChargingMode :
| Valeur |
Description |
ChargingModeNormal |
Charge pleine puissance dès branchement |
ChargingModeEco |
Charge uniquement avec surplus solaire |
ChargingModeEcoWithTargetTime |
Eco + garantie atteinte de la cible à l'heure définie |
ChargingState :
| Valeur |
Description |
ChargingStateIdle |
En attente (aucune décision de charge) |
ChargingStateSurplusCharging |
Charge sur surplus solaire actif |
ChargingStateSpotMarketCharging |
Charge sur créneau spot bon marché |
ChargingStateTimeRequirement |
Charge forcée pour respecter l'heure cible |
5. Flow de données
5.1 Démarrage du plugin
Démarrage nymea daemon
│
▼
EnergyPluginNymea::init()
│
├─ [1] EnergyManagerConfiguration → lit energy-manager-configuration.json
│ (locks, limites spot)
├─ [2] QNetworkAccessManager → client HTTP pour aWATTar
│
├─ [3] SpotMarketManager → enregistre aWATTar AT + DE
│ └─ charge enabled/providerId depuis energy.conf
│
├─ [4] SmartChargingManager → s'abonne à ThingManager::thingAdded
│ └─ charge ChargingInfos depuis energy.conf
│
└─ [5] NymeaEnergyJsonHandler → enregistre namespace "NymeaEnergy" v0–8
5.2 Détection et setup des appareils
ThingManager::thingAdded(thing)
│
├─ interface == "evcharger" → setupEvCharger() → new EvCharger()
│ Écoute : pluggedIn, power, charging,
│ maxChargingCurrent, currentPower
│
├─ interface == "energymeter" → setupRootMeter() → new RootMeter()
│ Écoute : currentPower, currentPhaseA/B/C
│
├─ interface == "electricvehicle" → écoute batteryLevel, capacity
│
└─ interface == "energystorage" → écoute batteryLevel, currentPower
5.3 Boucle de décision SmartChargingManager
Déclencheurs :
• currentPowerChanged (compteur) ← puissance réseau modifiée
• stateValueChanged (chargeur/VE) ← voiture branchée, état changé
• scoreEntriesUpdated (spot market) ← nouveaux prix reçus
• Timer interne ← tick périodique
▼
update(currentDateTime)
│
├─ prepareInformation()
│ Collecte : puissance compteur par phase, état chargeurs,
│ niveau batterie VE, niveau batterie stockage,
│ scores spot market du jour
│
├─ planSpotMarketCharging()
│ Si spot market activé + données disponibles :
│ → SpotMarketManager::scheduleChargingTime()
│ → Calcule les créneaux optimaux (x heures les moins chères)
│ → Fusionne les micro-créneaux (minimumScheduleDuration)
│
├─ planSurplusCharging()
│ Si mode Eco : surplus = (puissance_produite - consommation_autre)
│ Si surplus > acquisitionTolerance × puissance_min_charge :
│ → Calcule courant disponible par phase
│
├─ adjustEvChargers()
│ Pour chaque chargeur :
│ → Applique ChargingAction (enable/disable, courant, phases)
│ → Respecte les locks (chargingEnabledLockDuration, chargingCurrentLockDuration)
│
└─ verifyOverloadProtection()
Si currentPhaseX > phasePowerLimit :
→ Réduit maxChargingCurrent ou désactive le chargeur
verifyOverloadProtectionRecovery() → restaure dès que possible
5.4 Flux vers les clients (push notifications)
SmartChargingManager
│ chargingInfoChanged
│ chargingSchedulesChanged
▼
NymeaEnergyJsonHandler
│ ChargingInfoChanged { chargingInfo }
│ ChargingSchedulesChanged { chargingSchedules[] }
▼
WebSocket clients (apps, UI)
5.5 États du cycle de vie d'un chargeur
┌─────────────────┐ voiture branchée ┌──────────────────┐
│ IDLE │ ──────────────────────▶│ WAITING │
│ pluggedIn=false│ │ pluggedIn=true │
│ charging=false │ │ charging=false │
└─────────────────┘ └────────┬─────────┘
▲ │ conditions OK
│ voiture débranchée ▼
│ ┌──────────────────┐
│ │ CHARGING │
└──────────────────────────────────│ charging=true │
│ currentPower > 0│
└──────────────────┘
6. Configuration requise
6.1 Fichier de configuration runtime (optionnel)
Emplacement : /var/lib/nymea/energy-manager-configuration.json
Surcharge : variable d'environnement NYMEA_ENERGY_MANAGER_CONFIG
{
"chargingEnabledLockDuration": 300,
"chargingCurrentLockDuration": 10,
"minimumScheduleDuration": 15,
"spotMarketChargePredictableEnergyPercentage": 0.5
}
| Paramètre |
Défaut |
Description |
chargingEnabledLockDuration |
300 s |
Durée de verrou après pause/reprise charge (évite les cycles rapides) |
chargingCurrentLockDuration |
10 s |
Durée de verrou après changement de courant (spec OCPP : min 10 s) |
minimumScheduleDuration |
15 min |
Durée minimum d'un créneau planifié (évite les créneaux de 1 min) |
spotMarketChargePredictableEnergyPercentage |
0.5 |
% d'énergie planifié si les prix du lendemain ne sont pas encore disponibles |
6.2 Connexion réseau
| Service |
Protocol |
URL / Port |
Description |
| nymea daemon |
WebSocket |
ws://[host]:2222 |
API JSON-RPC clients |
| aWATTar Austria |
HTTPS REST |
https://api.awattar.at/v1/marketdata |
Prix spot Autriche |
| aWATTar Germany |
HTTPS REST |
https://api.awattar.de/v1/marketdata |
Prix spot Allemagne |
6.3 Persistance — energy.conf
Fichier QSettings ({NymeaSettings::settingsPath()}/energy.conf) :
[SpotMarket]
enabled=true
providerId=5196b3cc-b2ee-46d6-b63a-7af2cf70ba67
[ChargingInfos]
[ChargingInfos/{uuid-chargeur}]
assignedCarId={uuid-voiture}
chargingMode=ChargingModeEcoWithTargetTime
endDateTime=2026-02-24T07:00:00
repeatDays=1,2,3,4,5
targetPercentage=80
spotMarketChargingEnabled=true
dailySpotMarketPercentage=80
7. Dépendances
7.1 Bibliothèques système (pkgconfig)
| Bibliothèque |
Usage |
Contexte |
nymea |
Thing, ThingManager, JsonHandler, NymeaSettings, Electricity |
Plugin + Tests |
nymea-energy |
EnergyPlugin, EnergyManager, EnergyLogs |
Plugin + Tests |
nymea-core |
Noyau nymea |
Tests uniquement |
nymea-tests |
EnergyTestBase, framework de test intégration |
Tests uniquement |
7.2 Modules Qt
| Module |
Usage |
Contexte |
Qt::Core |
QObject, QTimer, QDateTime, QVariant, QSettings, QUuid |
Plugin |
Qt::Network |
QNetworkAccessManager, QNetworkReply, QUrl, QUrlQuery |
Plugin |
Qt::Sql |
Base SQLite des energy logs |
Tests simulation |
Qt::WebSockets |
WebSocket test |
Tests |
Qt::DBus |
D-Bus test |
Tests |
Qt::Test |
QTest framework |
Tests unitaires |
7.3 Compatibilité Qt / C++
| Version Qt |
Standard C++ |
Note |
| Qt 5.x |
C++11 |
DEFINES += QT_DISABLE_DEPRECATED_UP_TO=0x050F00 |
| Qt 6.x |
C++17 |
Détection automatique dans energyplugin.pri |
8. Intégration depuis etm_powersync_app
8.1 Protocole de connexion
nymea expose une API JSON-RPC 2.0 sur WebSocket (pas de REST, pas de MQTT).
etm_powersync_app
│
│ ws://[ip-nymea]:2222
│ Format : {"id": N, "method": "NS.Method", "params": {...}, "token": "..."}
▼
nymea daemon
│
├── Integrations.* → Lecture/écriture des States, Actions
├── NymeaEnergy.* → Smart charging, spot market
└── JSONRPC.* → Auth, notifications, introspection
8.2 Séquence d'authentification
App nymea
│ │
│──── WebSocket connect ───────────────▶│
│◀─── Hello (auto) { serverName, ... } ─│
│ │
│──── JSONRPC.CreateUser ───────────────▶│ (1ère fois seulement)
│◀─── { success: true } ────────────────│
│ │
│──── JSONRPC.Authenticate ─────────────▶│
│ { username, password, deviceName }│
│◀─── { success: true, token: "abc..." }─│
│ │
│ ← Token à stocker (FlutterSecureStorage)
│ │
│──── JSONRPC.SetNotificationStatus ────▶│
│ { namespaces: ["NymeaEnergy", │
│ "Integrations"] } │
│◀─── { success: true } ────────────────│
│ │
│ Toutes les requêtes suivantes : │
│──── { id: N, method: ..., ──────────▶│
│ token: "abc..." } │
8.3 Exemples JSON-RPC complets
Récupérer tous les chargeurs VE
// Requête
{
"id": 10,
"method": "Integrations.GetThings",
"params": {},
"token": "abc123..."
}
// Réponse (extrait)
{
"id": 10,
"status": "success",
"params": {
"things": [
{
"id": "f1e2d3c4-...",
"name": "Chargeur Garage",
"thingClassId": "5a3ae99f-c7da-46df-9104-f477be4606b7",
"pluginId": "...",
"states": [
{ "stateTypeId": "13f8f008-...", "value": true },
{ "stateTypeId": "63879844-...", "value": false }
]
}
]
}
}
Lire un State spécifique (voiture branchée ?)
// Requête
{
"id": 11,
"method": "Integrations.GetStateValue",
"params": {
"thingId": "f1e2d3c4-...",
"stateTypeId": "13f8f008-aa70-4772-9fb8-81a9674dd6ad"
},
"token": "abc123..."
}
// Réponse
{
"id": 11,
"status": "success",
"params": { "value": true }
}
Configurer le mode de charge (Eco avec heure cible)
// Requête
{
"id": 12,
"method": "NymeaEnergy.SetChargingInfo",
"params": {
"chargingInfo": {
"evChargerId": "f1e2d3c4-...",
"chargingMode": "ChargingModeEcoWithTargetTime",
"targetPercentage": 80,
"endDateTime": "2026-02-24T07:00:00",
"spotMarketChargingEnabled": true,
"dailySpotMarketPercentage": 80
}
},
"token": "abc123..."
}
// Réponse
{
"id": 12,
"status": "success",
"params": { "energyError": "EnergyErrorNoError" }
}
Activer le spot market (aWATTar Autriche)
{
"id": 13,
"method": "NymeaEnergy.SetSpotMarketConfiguration",
"params": {
"enabled": true,
"providerId": "5196b3cc-b2ee-46d6-b63a-7af2cf70ba67"
},
"token": "abc123..."
}
S'abonner et recevoir une notification push
// Côté app : abonnement (fait une seule fois après auth)
{
"id": 5,
"method": "JSONRPC.SetNotificationStatus",
"params": { "namespaces": ["NymeaEnergy", "Integrations"] }
}
// Réception asynchrone depuis nymea (aucun id de requête) :
{
"notification": "NymeaEnergy.ChargingInfoChanged",
"params": {
"chargingInfo": {
"evChargerId": "f1e2d3c4-...",
"chargingMode": "ChargingModeEcoWithTargetTime",
"chargingState": "ChargingStateSpotMarketCharging",
"targetPercentage": 80
}
}
}
// Autre exemple — nouveaux prix spot reçus :
{
"notification": "NymeaEnergy.SpotMarketScoreEntriesChanged",
"params": {
"spotMarketScoreEntries": [
{
"startDateTime": "2026-02-24T02:00:00",
"endDateTime": "2026-02-24T03:00:00",
"value": 48.5,
"weighting": 0.92
}
]
}
}
8.4 Exemples Dart/Flutter (etm_powersync_app)
Les fichiers complets se trouvent dans integration_app_examples/ :
| Fichier |
Contenu |
nymea_client.dart |
Classe NymeaClient : connexion WS, auth, toutes les méthodes NymeaEnergy.*, streams de notifications |
examples_usage.dart |
6 exemples autonomes : connect, getEvChargers, readState, executeActions, listenEvents, spotMarket |
final client = NymeaClient(host: '192.168.1.100', port: 2222);
await client.connect();
final token = await client.authenticate(
'admin@etm.local',
'MonMotDePasse!',
'etm_powersync_app',
);
// Persister le token : FlutterSecureStorage.write(key: 'nymea_token', value: token)
// Lecture ponctuelle
final pluggedIn = await client.getStateValue(
chargerId,
'13f8f008-aa70-4772-9fb8-81a9674dd6ad', // stateTypeId pluggedIn
);
// Écoute en temps réel (WebSocket push)
await client.subscribeToNotifications(['NymeaEnergy', 'Integrations']);
client.onChargingInfoChanged.listen((info) {
print('Chargeur ${info.evChargerId} → état : ${info.chargingState}');
});
await client.setChargingInfo(
evChargerId: chargerId,
mode: ChargingMode.ecoWithTargetTime,
targetPercentage: 80,
endDateTime: DateTime.now()
.add(const Duration(days: 1))
.copyWith(hour: 7, minute: 0)
.toIso8601String(),
spotMarketEnabled: true,
dailySpotMarketPercentage: 80,
);
final scores = await client.getSpotMarketScoreEntries();
scores.sort((a, b) => b.weighting.compareTo(a.weighting));
// Meilleur créneau de la journée
final best = scores.first;
print('Meilleur créneau : ${best.startDateTime.hour}h'
' — ${best.value.toStringAsFixed(1)} €/MWh'
' (score: ${(best.weighting * 100).round()}%)');
9. Plan d'extension — PAC / ECS / Gestion d'énergie
9.1 Vue d'ensemble de l'extension
AVANT (existant) APRÈS (extension ETM)
───────────────── ─────────────────────────────────
EnergyPluginNymea::init() EnergyPluginNymea::init()
├─ SmartChargingManager ├─ SmartChargingManager (inchangé)
├─ SpotMarketManager ├─ SpotMarketManager (inchangé)
└─ NymeaEnergyJsonHandler ├─ HeatPumpManager [NOUVEAU]
(v0–8, 15 méthodes) ├─ EnergyPriorityManager [NOUVEAU]
└─ NymeaEnergyJsonHandler [ÉTENDU]
(v0–9, 30 méthodes)
9.2 Nouveaux fichiers à créer
| Fichier |
Description |
energyplugin/heatpumpmanager.h/.cpp |
Détection PAC + ECS via ThingManager, wrappers, actions |
energyplugin/energyprioritymanager.h/.cpp |
Allocation de puissance par priorité (5 niveaux) |
Plugin intégration ETM .json |
+2 ThingClasses : heatPump, domesticHotWater |
9.3 Nouvelle ThingClass : Pompe à chaleur
ThingClass UUID : b5a8f3d2-4c1e-4a9f-8b2d-3e6c0f1d5a7b
Interfaces : heatpump, connectable
Protocole : Modbus TCP (port 502 configurable)
States PAC
| Nom |
UUID |
Type |
Unité |
Writable |
Contraintes |
connected |
c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f |
bool |
— |
— |
— |
power |
d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a |
bool |
— |
oui |
— |
mode |
e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b |
QString |
— |
oui |
heating/cooling/auto/standby |
targetTemperature |
f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c |
double |
°C |
oui |
15–65 °C |
currentTemperature |
a7b8c9d0-e1f2-4a3b-4c5d-6e7f8a9b0c1d |
double |
°C |
— |
— |
outdoorTemperature |
b8c9d0e1-f2a3-4b4c-5d6e-7f8a9b0c1d2e |
double |
°C |
— |
— |
currentPower |
c9d0e1f2-a3b4-4c5d-6e7f-8a9b0c1d2e3f |
double |
W |
— |
— |
cop |
d0e1f2a3-b4c5-4d6e-7f8a-9b0c1d2e3f4a |
double |
— |
— |
COP instantané |
defrostActive |
e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b |
bool |
— |
— |
Dégivrage en cours |
errorCode |
f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c |
uint |
— |
— |
0 = pas d'erreur |
Actions PAC
| Action |
UUID |
Paramètre |
Type |
Contraintes |
setMode |
a3b4c5d6-e7f8-4a9b-0c1d-2e3f4a5b6c7d |
mode (QString) |
— |
heating/cooling/auto/standby |
setTargetTemperature |
c5d6e7f8-a9b0-4c1d-2e3f-4a5b6c7d8e9f |
targetTemperature (double °C) |
— |
15–65 °C |
Events PAC
| Event |
UUID |
Description |
defrostStarted |
e7f8a9b0-c1d2-4e3f-4a5b-6c7d8e9f0a1b |
Dégivrage automatique démarré |
errorOccurred |
f8a9b0c1-d2e3-4f4a-5b6c-7d8e9f0a1b2c |
Erreur détectée (param: errorCode) |
9.4 Nouvelle ThingClass : Eau Chaude Sanitaire (ECS)
ThingClass UUID : c7d2e4f1-5b3a-4c0d-9e6f-2a4b7c1d3e5f
Interfaces : heating, connectable
States ECS
| Nom |
UUID |
Type |
Unité |
Writable |
Contraintes |
connected |
c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f |
bool |
— |
— |
— |
power |
d2e3f4a5-b6c7-4d8e-9f0a-1b2c3d4e5f6a |
bool |
— |
oui |
— |
targetTemperature |
e3f4a5b6-c7d8-4e9f-0a1b-2c3d4e5f6a7b |
double |
°C |
oui |
40–75 °C |
currentTemperature |
f4a5b6c7-d8e9-4f0a-1b2c-3d4e5f6a7b8c |
double |
°C |
— |
— |
boostMode |
a5b6c7d8-e9f0-4a1b-2c3d-4e5f6a7b8c9d |
bool |
— |
oui |
Mode chauffe rapide |
currentPower |
b6c7d8e9-f0a1-4b2c-3d4e-5f6a7b8c9d0e |
double |
W |
— |
— |
legionellaProtectionActive |
c7d8e9f0-a1b2-4c3d-4e5f-6a7b8c9d0e1f |
bool |
— |
— |
Cycle anti-légionellose |
Actions ECS
| Action |
UUID |
Paramètre |
Type |
Contraintes |
triggerBoost |
d8e9f0a1-b2c3-4d4e-5f6a-7b8c9d0e1f2a |
duration (uint, minutes) |
— |
15–120 min |
Events ECS
| Event |
UUID |
Description |
targetTemperatureReached |
f0a1b2c3-d4e5-4f6a-7b8c-9d0e1f2a3b4c |
Température cible atteinte |
legionellaProtectionTriggered |
a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d |
Cycle anti-légionellose déclenché |
9.5 Nouvelles méthodes JSON-RPC (NymeaEnergy v9)
Méthodes PAC
| Méthode |
Params |
Retour |
Description |
GetHeatPumps |
— |
heatPumps: [] |
Liste PAC + tous leurs états |
SetHeatPumpPower |
thingId, power: bool |
energyError |
Allumer / éteindre |
SetHeatPumpMode |
thingId, mode: string |
energyError |
Mode heating/cooling/auto/standby |
SetHeatPumpTargetTemperature |
thingId, targetTemperature: double |
energyError |
Consigne [15–65°C] |
Méthodes ECS
| Méthode |
Params |
Retour |
Description |
GetDHWDevices |
— |
dhwDevices: [] |
Liste ECS + tous leurs états |
SetDHWPower |
thingId, power: bool |
energyError |
Activer / désactiver |
SetDHWTargetTemperature |
thingId, targetTemperature: double |
energyError |
Consigne [40–75°C] |
TriggerDHWBoost |
thingId, duration: uint |
energyError |
Mode boost 15–120 min |
Méthodes Gestion d'énergie
| Méthode |
Params |
Retour |
Description |
GetManagedLoads |
— |
managedLoads: [] |
Toutes les charges avec priorité + puissances |
SetLoadPriority |
thingId, priority: enum |
energyError |
Modifier la priorité (0–4) |
SetAllocatedPower |
thingId, allocatedPower: double |
energyError |
Forcer puissance allouée (W) |
GetTotalAvailablePower |
— |
totalAvailablePower: double |
Puissance totale disponible |
SetTotalAvailablePower |
totalAvailablePower: double |
energyError |
Modifier la puissance totale |
Nouvelles notifications push
| Notification |
Payload |
Déclencheur |
HeatPumpStateChanged |
thingId, stateName, value |
Changement d'un état PAC |
DHWStateChanged |
thingId, stateName, value |
Changement d'un état ECS |
ManagedLoadChanged |
thingId, priority, allocatedPower, active |
Changement de priorité ou allocation |
TotalAvailablePowerChanged |
totalAvailablePower: double |
Changement puissance disponible |
9.6 Gestion de priorité énergétique
Les 5 niveaux de priorité
| Niveau |
Valeur |
Cas d'usage |
Comportement si surcharge |
Critical |
0 |
Chauffage PAC hiver, anti-légionellose |
Jamais coupé |
High |
1 |
Chauffage standard, ECS sécurité |
Coupé en dernier recours |
Normal |
2 |
Recharge VE normale, ECS standard |
Comportement standard |
Low |
3 |
VE mode Eco, ECS boost, auxiliaires |
Interruptible |
Optional |
4 |
VE spot market, confort |
Seulement si surplus disponible |
Algorithme de rééquilibrage
rebalance()
│
├─ puissanceDisponible = totalAvailablePower - reservedForCritical
│
├─ Pour chaque charge, triée par priorité (Critical → Optional) :
│ │
│ ├─ Si puissanceDisponible ≥ requestedPower :
│ │ → allocatedPower = requestedPower, active = true
│ │ → puissanceDisponible -= requestedPower
│ │
│ ├─ Sinon si puissanceDisponible > 0 :
│ │ → allocatedPower = puissanceDisponible, active = true (throttle)
│ │ → puissanceDisponible = 0
│ │
│ └─ Sinon :
│ → allocatedPower = 0, active = false ← charge coupée
│
└─ Pour chaque charge modifiée : emit managedLoadChanged()
+ appliquer l'action sur le Thing
Priorités recommandées par type
| Charge |
Priorité |
Raison |
| PAC chauffage (hiver) |
Critical |
Confort thermique impératif |
| ECS anti-légionellose |
Critical |
Sécurité sanitaire |
| PAC chauffage standard |
High |
Confort prioritaire |
| ECS normale |
Normal |
Usage quotidien |
| Recharge VE mode Normal |
Normal |
Usage quotidien |
| Recharge VE mode Eco |
Low |
Interruptible sans conséquence |
| ECS mode boost |
Low |
Confort non-critique |
| Recharge VE spot market |
Optional |
Seulement si surplus/prix bas |
9.7 Modifications des fichiers existants
energyplugin.pri — ajouts
HEADERS += \
$$PWD/heatpumpmanager.h \
$$PWD/energyprioritymanager.h \
SOURCES += \
$$PWD/heatpumpmanager.cpp \
$$PWD/energyprioritymanager.cpp \
energypluginnymea.cpp — init() étendu
void EnergyPluginNymea::init()
{
EnergyManagerConfiguration *configuration = new EnergyManagerConfiguration(this);
QNetworkAccessManager *networkManager = new QNetworkAccessManager(this);
SpotMarketManager *spotMarketManager = new SpotMarketManager(networkManager, this);
SmartChargingManager *chargingManager = new SmartChargingManager(
energyManager(), thingManager(), spotMarketManager, configuration, this);
// [NOUVEAU]
HeatPumpManager *heatPumpManager = new HeatPumpManager(
energyManager(), thingManager(), this);
EnergyPriorityManager *priorityManager = new EnergyPriorityManager(
energyManager(), thingManager(), this);
// Handler étendu — version 9
jsonRpcServer()->registerExperienceHandler(
new NymeaEnergyJsonHandler(
spotMarketManager, chargingManager,
heatPumpManager, priorityManager, this),
0, 9);
}
9.8 Exemples d'utilisation depuis l'app (extension)
Contrôler une pompe à chaleur
// Passer en mode chauffage à 22°C
{
"id": 20,
"method": "NymeaEnergy.SetHeatPumpMode",
"params": { "thingId": "<uuid-pac>", "mode": "heating" },
"token": "..."
}
{
"id": 21,
"method": "NymeaEnergy.SetHeatPumpTargetTemperature",
"params": { "thingId": "<uuid-pac>", "targetTemperature": 22.0 },
"token": "..."
}
Déclencher un boost ECS
{
"id": 22,
"method": "NymeaEnergy.TriggerDHWBoost",
"params": { "thingId": "<uuid-ecs>", "duration": 45 },
"token": "..."
}
Configurer les priorités énergétiques
// Définir la puissance totale disponible (3 x 16A x 230V ≈ 11 kW)
{
"id": 23,
"method": "NymeaEnergy.SetTotalAvailablePower",
"params": { "totalAvailablePower": 11040 },
"token": "..."
}
// Mettre le chargeur VE en priorité basse
{
"id": 24,
"method": "NymeaEnergy.SetLoadPriority",
"params": { "thingId": "<uuid-chargeur>", "priority": "LoadPriorityLow" },
"token": "..."
}
// Mettre la PAC en priorité critique (ne jamais couper)
{
"id": 25,
"method": "NymeaEnergy.SetLoadPriority",
"params": { "thingId": "<uuid-pac>", "priority": "LoadPriorityCritical" },
"token": "..."
}
Documentation mise à jour le 2026-02-23 — Branche : nymea-energy-plugin-etm
Sections générées : analyse du code source, intégration app, plan d'extension ETM
10. SchedulerManager — Planification énergétique multi-stratégie
10.1 Architecture globale
energypluginnymea.cpp::init()
├── SmartChargingManager (VE — surplus + spot)
├── SchedulerManager (NOUVEAU — orchestrateur 24/48h)
│ ├── ISchedulingStrategy (interface)
│ │ ├── RuleBasedStrategy (défaut — 3 passes déterministes)
│ │ └── AIStrategy (stub — extension future ONNX/HTTP)
│ ├── SchedulerSettings (persistance QSettings)
│ └── PredictionProvider (stub — remplacé par vrai prédicteur Phase 2)
└── NymeaEnergyJsonHandler (étendu v10 — nouvelles méthodes RPC)
Flux complet d'une recompute (toutes les 15 min)
SchedulerManager::forceRecompute()
│
├─ 1. buildForecast()
│ └─ Génère N slots horaires (planningHorizonHours)
│ Remplit electricityPrice depuis SpotMarketManager
│ solarForecastW / baseConsumptionW = 0 (stub Phase 1)
│
├─ 2. collectLoads()
│ └─ Retourne les FlexibleLoad enregistrés (updateLoad() par les managers)
│
├─ 3. strategy->computeSchedule(forecast, loads, config)
│ └─ Retourne timeline annotée (allocations + decisionReason non vide)
│
├─ 4. Réinjection des overrides manuels (non modifiés)
│
├─ 5. applyCurrentSlot() — applique le slot COURANT aux managers hardware
│
├─ 6. scheduleNextSlotTimer() — arme un QTimer pour le prochain slot
│
└─ 7. emit timelineUpdated() → JSON-RPC TimelineUpdated notification
10.2 RuleBasedStrategy — 3 passes déterministes
Passe 1 — Charges critiques / inflexibles
- Source : FlexibleLoad avec
type = Inflexible ET priority >= 0.9
- Action : réserve leur
currentPowerW dans tous les slots sans exception
- Exemple : PAC chauffage en mode critique hiver, ECS anti-légionellose
decisionReason : "Chauffage critique — PAC toujours active"
Passe 2 — Stockages (batterie, ballon ECS)
- Source : FlexibleLoad avec
type = Storage
- Condition de charge pour un slot :
- Surplus solaire :
solarForecastW - baseConsumptionW > solarSurplusThresholdW (200 W) OU
- Prix bas :
electricityPrice < chargePriceThreshold (0.08 €/kWh)
- Puissance allouée :
min(maxPowerW, max(minPowerW, surplus))
decisionReason exemples :
"Surplus solaire +2.3 kW — recharge batterie"
"Prix spot 0.05€/kWh (seuil 0.08€) — charge batterie"
Passe 3 — Charges flexibles (VE, lave-linge)
- Source : FlexibleLoad avec
type = Shiftable
- Sélection des slots :
- Filtrer les slots avant
deadline (non overridés)
- Trier par score composite :
priceScore + solarBonus
- Score bas = meilleur slot (prix bas + surplus solaire)
- Choisir les
slotsNeeded meilleurs slots
decisionReason exemples :
"Surplus solaire prévu +1.6 kW — recharge VE gratuite"
"Prix spot 0.05€/kWh (seuil 0.08€) — recharge VE planifiée"
"Créneau optimal avant deadline — recharge VE planifiée (0.07€/kWh)"
Invariant : decisionReason non vide
Toute décision doit avoir un decisionReason non vide. La méthode
fillMissingReasons() garantit cet invariant même pour les slots passifs :
"Surplus solaire +1.2 kW — aucune charge flexible disponible"
"Prix très bas 0.04€/kWh — aucune charge flexible configurée"
"Créneau passif — toutes les charges flexibles satisfaites"
10.3 Méthodes JSON-RPC NymeaEnergy v10
| Méthode |
Direction |
Description |
GetEnergyTimeline |
Request |
Timeline 24h avec toutes les allocations et raisons |
GetFlexibleLoads |
Request |
Liste des charges flexibles connues |
GetSchedulerStatus |
Request |
Stratégie active, santé du plan, overrides |
SetSchedulerStrategy |
Request |
Changer la stratégie active |
SetSchedulerConfig |
Request |
Modifier les seuils et l'horizon |
SetLoadConfig |
Request |
Modifier priorité/deadline/cible d'une charge |
OverrideSlot |
Request |
Override manuel d'un slot |
TimelineUpdated |
Notification |
Timeline recomputée |
SlotActivated |
Notification |
Slot courant appliqué au hardware |
OverrideConflict |
Notification |
Override empêche une décision optimale |
Exemple GetEnergyTimeline (24h)
// Requête
{ "id": 30, "method": "NymeaEnergy.GetEnergyTimeline", "params": { "hours": 24 }, "token": "..." }
// Réponse
{
"params": {
"timeline": [
{
"start": "2026-02-23T14:00:00.000Z",
"end": "2026-02-23T15:00:00.000Z",
"solarForecastW": 2800,
"baseConsumptionW": 1200,
"electricityPrice": 0.092,
"allocations": { "ev": 1200, "heatpump": 800, "dhw": 0, "battery": 600, "feedin": 0 },
"netGridPowerW": -200,
"estimatedCostEUR": -0.018,
"selfSufficiencyPct": 116,
"decisionReason": "Surplus solaire +1.6 kW — recharge VE + batterie, léger export",
"decisionRules": ["SolarSurplus", "EVDeadlineOk", "BatteryBelow80Pct"]
}
],
"summary": {
"totalEstimatedCostEUR": 1.24,
"totalSelfSufficiencyPct": 68,
"totalSolarProductionKwh": 12.4,
"totalGridImportKwh": 4.2,
"totalGridExportKwh": 1.8
},
"activeStrategy": "rule-based",
"lastComputedAt": "2026-02-23T13:47:00.000Z"
}
}
10.4 Guide — Ajouter une nouvelle stratégie de scheduling
5 étapes :
-
Créer la classe dans energyplugin/schedulingstrategies/ :
class MyStrategy : public ISchedulingStrategy {
Q_OBJECT
public:
QString strategyId() const override { return "my-strategy"; }
QString displayName() const override { return "Ma Stratégie"; }
QString description() const override { return "Description..."; }
QList<EnergyTimeSlot> computeSchedule(
const QList<EnergyTimeSlot> &forecast,
const QList<FlexibleLoad> &loads,
const SchedulerConfig &config) override;
QString explainDecision(const EnergyTimeSlot &, const FlexibleLoad &) const override;
};
-
Implémenter computeSchedule() :
- Partir d'une copie du forecast (préserve les prédictions)
- Respecter
slot.manualOverride == true (ne pas modifier ces slots)
- Remplir toutes les allocations ET
decisionReason non vide pour tout slot actif
-
Ajouter les fichiers à energyplugin.pri (HEADERS + SOURCES)
-
Enregistrer dans SchedulerManager :
// Dans schedulermanager.cpp, constructeur :
registerStrategy(new MyStrategy(this));
-
Activer via JSON-RPC :
{ "method": "NymeaEnergy.SetSchedulerStrategy", "params": { "strategyId": "my-strategy" } }
10.5 Guide — Ajouter un nouveau consommateur flexible
4 étapes :
-
Choisir le LoadType et LoadSource :
Inflexible → toujours actif, jamais géré (base load)
Shiftable → doit finir avant deadline (VE, lave-linge)
Modulable → puissance ajustable (PAC, clim)
Storage → bidirectionnel (batterie, ballon ECS)
-
Créer un FlexibleLoad dans le manager responsable :
FlexibleLoad load;
load.thingId = thing->id();
load.displayName = thing->name();
load.type = LoadType::Shiftable;
load.source = LoadSource::SmartCharging;
load.minPowerW = 1400;
load.maxPowerW = 7400;
load.priority = 0.7;
load.deadline = chargingInfo.endDateTime();
load.targetValue = chargingInfo.targetPercentage();
load.currentValue= car->batteryLevel();
-
Enregistrer auprès du SchedulerManager :
m_schedulerManager->updateLoad(load);
Appeler updateLoad() à chaque changement d'état (SOC, deadline, connexion).
-
Réagir aux décisions du Scheduler :
connect(m_schedulerManager, &SchedulerManager::timelineUpdated,
this, [this](const QList<EnergyTimeSlot> &timeline) {
QDateTime now = QDateTime::currentDateTimeUtc();
foreach (const EnergyTimeSlot &slot, timeline) {
if (slot.isActive(now)) {
// Lire slot.allocatedToEV / allocatedToHP / etc.
applyAllocation(slot.allocatedToEV);
break;
}
}
});
10.6 Exemples de decisionReason par cas
| Situation |
decisionReason |
decisionRules |
| Surplus solaire, recharge VE |
"Surplus solaire prévu +2.3 kW — recharge VE gratuite" |
["SolarSurplus", "EVDeadlineOk"] |
| Prix spot bas, batterie |
"Prix spot 0.05€/kWh (seuil 0.08€) — charge batterie" |
["PriceBelow0.08"] |
| Pic de prix, décharge batterie |
"Prix peak 0.18€/kWh — VE suspendu, batterie en décharge" |
["PricePeak"] |
| Override manuel |
"Décision manuelle : Départ annulé — ne pas charger ce soir" |
["ManualOverride"] |
| PAC critique |
"Chauffage critique — PAC toujours active" |
["CriticalHeating"] |
| Créneau passif |
"Créneau passif — toutes les charges flexibles satisfaites" |
[] |
| AI non chargée |
"Modèle IA non chargé — basculement vers RuleBasedStrategy recommandé" |
["AIModelNotLoaded"] |
Section 10 ajoutée le 2026-02-23 — SchedulerManager Phase 1 (stubs prédiction)
11.1 Vue d'ensemble
ManualStrategy (strategyId = "manual") est la stratégie de niveau Community.
Elle donne à l'utilisateur un contrôle total : chaque créneau horaire est piloté
par une ManualSlotConfig explicitement définie. Aucune optimisation automatique.
Cas d'usage typique : utilisateur technique qui sait exactement quand et à quelle
puissance charger son VE, sans déléguer la décision à un algorithme.
11.2 Comportement par cas
| Situation |
Résultat |
decisionRules |
| Slot dans une config active |
Allocations appliquées exactement |
["ManualSlot"] |
| Slot sans config |
Charges inflexibles/critiques uniquement |
["ManualDefault"] ou ["CriticalHeating"] |
| Config existante mais expirée |
Charges critiques uniquement |
["ExpiredSlot"] |
| Slot en override manuel |
Préservé tel quel |
["ManualOverride"] |
Invariant : decisionReason n'est jamais vide (contrat ISchedulingStrategy).
11.3 Type ManualSlotConfig
struct ManualSlotConfig {
QDateTime start;
QDateTime end;
QMap<LoadSource, double> powerAllocations; // "ev"→2000W, "battery"→1000W, ...
QString label; // affiché dans l'UI, ex. "Recharge VE nuit"
bool repeating; // si true : récurrence hebdomadaire (même jour/heure)
QDateTime expiresAt; // optionnel — ignoré après cette date
};
Pour les slots répétables (repeating=true) : la récurrence est calculée en
minutes-de-semaine (jour_semaine × 1440 + heure × 60 + minute), ce qui gère
correctement les slots overnight (ex. Lun 22:00 → Mar 06:00).
11.4 JSON-RPC — NymeaEnergy v11
GetManualSlots
→ {}
← { "slots": [ { ManualSlotConfig }, ... ] }
SetManualSlot
→ {
"start": "2026-02-24T22:00:00.000Z",
"end": "2026-02-25T06:00:00.000Z",
"label": "Recharge VE nuit",
"repeating": false,
"expiresAt": "2026-03-01T00:00:00.000Z",
"allocations": { "ev": 2000, "battery": 1000, "heatpump": 0, "dhw": 0 }
}
← { "energyError": "EnergyErrorNoError" }
RemoveManualSlot
→ { "start": "2026-02-24T22:00:00.000Z" }
← { "energyError": "EnergyErrorNoError" }
ClearManualSlots
→ {}
← { "energyError": "EnergyErrorNoError" }
ManualSlotActivated (push notification)
{
"slot": { /* ManualSlotConfig */ },
"appliedAllocations": { "ev": 2000, "battery": 1000, "heatpump": 0, "dhw": 0, "feedin": 0 },
"reason": "Créneau manuel 'Recharge VE nuit' activé"
}
11.5 Persistance
Les ManualSlotConfig sont persistées dans :
NymeaSettings::settingsPath() + "/scheduler.conf" [section: manualSlots]
- Chargement : au démarrage, dans
SchedulerManager::registerStrategy() lorsque
ManualStrategy est enregistrée. Les slots expirés sont ignorés à la lecture.
- Sauvegarde : à chaque
SetManualSlot / RemoveManualSlot / ClearManualSlots.
11.6 Guide d'intégration — créneau EV hebdomadaire
Étape 1 — Activer ManualStrategy :
{ "method": "NymeaEnergy.SetSchedulerStrategy", "params": { "strategyId": "manual" } }
Étape 2 — Configurer un créneau VE chaque lundi nuit (22:00→06:00), 2 kW :
{
"method": "NymeaEnergy.SetManualSlot",
"params": {
"start": "2026-02-23T22:00:00.000Z",
"end": "2026-02-24T06:00:00.000Z",
"label": "Recharge hebdo VE",
"repeating": true,
"allocations": { "ev": 2000 }
}
}
Étape 3 — S'abonner à la notification pour confirmation :
{ "method": "JSONRPC.SetNotificationStatus",
"params": { "namespaces": ["NymeaEnergy"] } }
// → ManualSlotActivated émis à chaque lundi 22:00
Étape 4 — Retirer le créneau si besoin :
{ "method": "NymeaEnergy.RemoveManualSlot",
"params": { "start": "2026-02-23T22:00:00.000Z" } }
11.7 Clés d'allocation (JSON)
| Clé JSON |
LoadSource interne |
"ev" |
LoadSource::SmartCharging |
"battery" |
LoadSource::Battery |
"dhw" |
LoadSource::DHW |
"heatpump" |
LoadSource::HeatPump |
"feedin" |
LoadSource::FeedIn |
Section 11 ajoutée le 2026-02-24 — ManualStrategy Community Tier