pakutz79 0797f37c78 feat: ManualStrategy — community tier scheduling with full manual control
- Add ManualSlotConfig type with weekly repeating support and auto-expiry
- Add ManualStrategy (strategyId="manual"): applies user-defined allocations
  exactly; expired slots logged and skipped; inflexible/critical loads always
  applied as safety fallback; decisionReason never empty
- Extend SchedulerSettings with manualSlots persistence section (INI array)
- Extend SchedulerManager with setManualSlot/removeManualSlot/clearManualSlots
  methods; hydrates ManualStrategy from settings on registerStrategy()
- Add JSON-RPC v11 methods: GetManualSlots, SetManualSlot, RemoveManualSlot,
  ClearManualSlots + ManualSlotActivated push notification
- Register ManualStrategy in energypluginnymea.cpp::init() (no feature flag)
- Add 5 unit tests: basicSlot, noConfig_fallback, expiredSlot, repeatingSlot,
  persistence (JSON round-trip)
- Update doc.md section 11

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 06:34:31 +01:00

1527 lines
55 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](#1-vue-densemble)
2. [Structure du repo](#2-structure-du-repo)
3. [Things & UUIDs](#3-things--uuids)
4. [API JSON-RPC — NymeaEnergy](#4-api-json-rpc--nymeaenergy)
5. [Flow de données](#5-flow-de-données)
6. [Configuration requise](#6-configuration-requise)
7. [Dépendances](#7-dépendances)
8. [Intégration depuis etm_powersync_app](#8-intégration-depuis-etm_powersync_app)
9. [Plan d'extension — PAC / ECS / Gestion d'énergie](#9-plan-dextension--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 :
```cpp
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`
```json
{
"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`) :
```ini
[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
```json
// 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 ?)
```json
// 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)
```json
// 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)
```json
{
"id": 13,
"method": "NymeaEnergy.SetSpotMarketConfiguration",
"params": {
"enabled": true,
"providerId": "5196b3cc-b2ee-46d6-b63a-7af2cf70ba67"
},
"token": "abc123..."
}
```
#### S'abonner et recevoir une notification push
```json
// 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
```dart
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
```dart
// 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
```dart
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
```dart
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
```qmake
HEADERS += \
$$PWD/heatpumpmanager.h \
$$PWD/energyprioritymanager.h \
SOURCES += \
$$PWD/heatpumpmanager.cpp \
$$PWD/energyprioritymanager.cpp \
```
#### `energypluginnymea.cpp` — `init()` étendu
```cpp
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
```json
// 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
```json
{
"id": 22,
"method": "NymeaEnergy.TriggerDHWBoost",
"params": { "thingId": "<uuid-ecs>", "duration": 45 },
"token": "..."
}
```
#### Configurer les priorités énergétiques
```json
// 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)
```json
// 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/` :
```cpp
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`** :
```cpp
// Dans schedulermanager.cpp, constructeur :
registerStrategy(new MyStrategy(this));
```
5. **Activer via JSON-RPC** :
```json
{ "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 :
```cpp
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** :
```cpp
m_schedulerManager->updateLoad(load);
```
Appeler `updateLoad()` à chaque changement d'état (SOC, deadline, connexion).
4. **Réagir aux décisions du Scheduler** :
```cpp
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
```cpp
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
```json
→ {}
← { "slots": [ { ManualSlotConfig }, ... ] }
```
#### SetManualSlot
```json
→ {
"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
```json
→ { "start": "2026-02-24T22:00:00.000Z" }
← { "energyError": "EnergyErrorNoError" }
```
#### ClearManualSlots
```json
→ {}
← { "energyError": "EnergyErrorNoError" }
```
#### ManualSlotActivated (push notification)
```json
{
"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 :
```json
{ "method": "NymeaEnergy.SetSchedulerStrategy", "params": { "strategyId": "manual" } }
```
**Étape 2** — Configurer un créneau VE chaque lundi nuit (22:00→06:00), 2 kW :
```json
{
"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 :
```json
{ "method": "JSONRPC.SetNotificationStatus",
"params": { "namespaces": ["NymeaEnergy"] } }
// → ManualSlotActivated émis à chaque lundi 22:00
```
**Étape 4** — Retirer le créneau si besoin :
```json
{ "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*