powersync-energy-plugin-etm/doc.md

60 KiB
Raw Blame History

Documentation — nymea-energy-plugin-nymea

Branche : nymea-energy-plugin-etm Date : 2026-02-23 Auteur : Analyse ETM / Claude Code


Table des matières

  1. Vue d'ensemble
  2. Structure du repo
  3. Things & UUIDs
  4. API JSON-RPC — NymeaEnergy
  5. Flow de données
  6. Configuration requise
  7. Dépendances
  8. Intégration depuis etm_powersync_app
  9. 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 (v08) 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 632 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.01.0]
SetAcquisitionTolerance acquisitionTolerance: double energyError Modifie le seuil
GetBatteryLevelConsideration batteryLevelConsideration: double Part du stockage prise en compte [0.01.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" v08

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

Note : ces valeurs sont pour environnements de test. Ne pas modifier en production.

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

Extrait — Connexion et authentification

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)

Extrait — Lire et surveiller l'état d'un chargeur

// 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}');
});

Extrait — Configurer le smart charging

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,
);

Extrait — Consulter les prix spot

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]
    (v08, 15 méthodes)              ├─ EnergyPriorityManager  [NOUVEAU]
                                     └─ NymeaEnergyJsonHandler [ÉTENDU]
                                        (v09, 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 1565 °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) 1565 °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 4075 °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) 15120 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 [1565°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 [4075°C]
TriggerDHWBoost thingId, duration: uint energyError Mode boost 15120 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é (04)
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.cppinit() é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 :
    1. Filtrer les slots avant deadline (non overridés)
    2. Trier par score composite : priceScore + solarBonus
      • Score bas = meilleur slot (prix bas + surplus solaire)
    3. 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 :

  1. 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;
    };
    
  2. 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
  3. Ajouter les fichiers à energyplugin.pri (HEADERS + SOURCES)

  4. Enregistrer dans SchedulerManager :

    // Dans schedulermanager.cpp, constructeur :
    registerStrategy(new MyStrategy(this));
    
  5. Activer via JSON-RPC :

    { "method": "NymeaEnergy.SetSchedulerStrategy", "params": { "strategyId": "my-strategy" } }
    

10.5 Guide — Ajouter un nouveau consommateur flexible

4 étapes :

  1. 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)
  2. 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();
    
  3. Enregistrer auprès du SchedulerManager :

    m_schedulerManager->updateLoad(load);
    

    Appeler updateLoad() à chaque changement d'état (SOC, deadline, connexion).

  4. 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. ManualStrategy — Community Tier

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


Section 12 — Installer Setup (LoadAdapterRegistry, v12)

12.1 Vue d'ensemble

L'Installer Setup permet à un installateur de mapper des rôles énergétiques (EVCharger, DHW, HeatPump, Battery, SolarMeter, GridMeter) à des Things nymea concrètes. Le LoadAdapterRegistry crée ensuite l'adaptateur approprié (relay, SG-Ready, evcharger, battery) et transmet les consignes du Scheduler au matériel réel.

Flux en 3 étapes :

  1. Activer le rôle → SetRoleEnabled(role, true)
  2. Choisir la Thing → AssignThingToRole(role, thingId)
  3. Tester la connexion → TestRoleConnection(role) → notification ConnectionTestResult

12.2 Tableau de compatibilité (rôle → interfaces requises)

Rôle Interfaces nymea requises
EVCharger evcharger
DHW relay ou smartmeterconsumer
HeatPump heating (SG-Ready) ou relay
Battery energystorage
SolarMeter energymeter
GridMeter energymeter

12.3 Détection automatique du type d'adaptateur

Rôle Interface détectée Adaptateur créé
EVCharger evcharger EvChargerAdapter
DHW relay RelayAdapter
HeatPump heating SgReadyAdapter (2 relais)
HeatPump relay RelayAdapter
Battery energystorage BatteryAdapter
SolarMeter energymeter read-only (pas d'adaptateur)
GridMeter energymeter read-only (pas d'adaptateur)

12.4 Référence API JSON-RPC v12

GetSetupStatus{ setupStatus: object }

Retourne l'état de tous les rôles.

{
  "method": "NymeaEnergy.GetSetupStatus",
  "params": {}
}
// Réponse:
{
  "setupStatus": {
    "roles": [
      {
        "role": "DHW",
        "enabled": true,
        "assigned": true,
        "assignedThingId": "…uuid…",
        "assignedThingName": "Ballon ECS",
        "reachable": true,
        "adapterType": "relay",
        "lastError": ""
      }
    ],
    "allEnabledRolesOk": true,
    "configuredCount": 2,
    "errorCount": 0
  }
}

GetCompatibleThings{ role, things: [] }

{ "method": "NymeaEnergy.GetCompatibleThings", "params": { "role": "DHW" } }

AssignThingToRole{ energyError, adapterType, detectedParams }

{
  "method": "NymeaEnergy.AssignThingToRole",
  "params": {
    "role":    "HeatPump",
    "thingId": "…relay1-uuid…",
    "params":  { "relay1ThingId": "…", "relay2ThingId": "…", "normalPowerW": 1500 }
  }
}

UnassignRole{ energyError }

{ "method": "NymeaEnergy.UnassignRole", "params": { "role": "Battery" } }

SetRoleEnabled{ energyError }

{ "method": "NymeaEnergy.SetRoleEnabled", "params": { "role": "DHW", "enabled": true } }

TestRoleConnection{ energyError } + notification ConnectionTestResult

{ "method": "NymeaEnergy.TestRoleConnection", "params": { "role": "DHW" } }
// Notification:
{ "notification": "NymeaEnergy.ConnectionTestResult",
  "params": { "role": "DHW", "success": true, "message": "Connection test OK" } }

12.5 Notifications push

Notification Payload Déclencheur
SetupStatusChanged { setupStatus } Assignment, enable, reachability
ConnectionTestResult { role, success, message } Fin de TestRoleConnection
ThingBecameCompatible { role, thing } Nouvelle Thing ajoutée dans nymea

12.6 Gestion des erreurs — Thing disparue

Si une Thing assignée disparaît au redémarrage du daemon :

  • L'entrée dans adapters.conf est conservée
  • RoleStatus.reachable = false, lastError = "Thing not found: <uuid>"
  • SetupStatus.allEnabledRolesOk = false
  • Notification SetupStatusChanged émise avec l'erreur
  • Dès que la Thing réapparaît (ThingManager::thingAdded), notification ThingBecameCompatible

12.7 Exemple complet — EV charger hebdomadaire via Installer Setup

// 1. Activer le rôle
{ "method": "NymeaEnergy.SetRoleEnabled",
  "params": { "role": "EVCharger", "enabled": true } }

// 2. Lister les Things compatibles
{ "method": "NymeaEnergy.GetCompatibleThings",
  "params": { "role": "EVCharger" } }

// 3. Assigner la Thing choisie
{ "method": "NymeaEnergy.AssignThingToRole",
  "params": { "role": "EVCharger", "thingId": "…uuid…", "params": { "phases": 1 } } }

// 4. Tester
{ "method": "NymeaEnergy.TestRoleConnection",
  "params": { "role": "EVCharger" } }
// → ConnectionTestResult { "success": true, "message": "EV charger connected" }

// 5. Configurer un créneau manuel EV
{ "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 }
  }
}

12.8 Persistance

Les assignements sont sauvegardés dans :

NymeaSettings::settingsPath() + "/adapters.conf"

Format INI, identique à scheduler.conf. Chargés automatiquement au démarrage du daemon.


Section 12 ajoutée le 2026-02-24 — Installer Setup LoadAdapterRegistry v12