- 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>
1527 lines
55 KiB
Markdown
1527 lines
55 KiB
Markdown
# 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` (v0–8) pour le contrôle à distance par les applications |
|
||
|
||
### Protocoles utilisés
|
||
|
||
| Protocole | Direction | Usage |
|
||
|---|---|---|
|
||
| **JSON-RPC 2.0 sur WebSocket** | Bidirectionnel | API principale : apps clients ↔ nymea |
|
||
| **HTTP REST** | Sortant | Récupération des prix spot aWATTar (toutes les 60 s) |
|
||
| **Qt Signals/Slots** | Interne | Communication entre managers C++ |
|
||
| **QSettings (INI)** | Local | Persistance configuration (`energy.conf`) |
|
||
| **JSON** | Local | Configuration runtime (`energy-manager-configuration.json`) |
|
||
|
||
### Type de plugin — distinction importante
|
||
|
||
```
|
||
IntegrationPlugin EnergyPlugin (ce plugin)
|
||
│ │
|
||
▼ ▼
|
||
setupThing() init() uniquement
|
||
thingRemoved() Écoute ThingManager
|
||
executeAction() Orchestre les Things
|
||
→ Crée des Things → Utilise des Things
|
||
créées ailleurs
|
||
```
|
||
|
||
---
|
||
|
||
## 2. Structure du repo
|
||
|
||
```
|
||
nymea-energy-plugin-nymea/
|
||
│
|
||
├── energyplugin/ # Code source principal
|
||
│ ├── energypluginnymea.h/.cpp # Point d'entrée : EnergyPlugin → init()
|
||
│ ├── energyplugin.pro # Build qmake (lib partagée)
|
||
│ ├── energyplugin.pri # Config Qt5/Qt6, coverage
|
||
│ ├── plugininfo.h # Catégorie de log : dcNymeaEnergy
|
||
│ ├── energysettings.h/.cpp # QSettings → energy.conf
|
||
│ ├── energymanagerconfiguration.h/.cpp # Config runtime JSON (locks, limites)
|
||
│ │
|
||
│ ├── smartchargingmanager.h/.cpp # ★ Moteur central du smart charging
|
||
│ ├── evcharger.h/.cpp # Wrapper Thing → chargeur VE
|
||
│ ├── rootmeter.h/.cpp # Wrapper Thing → compteur principal
|
||
│ ├── nymeaenergyjsonhandler.h/.cpp # ★ Handler JSON-RPC ("NymeaEnergy")
|
||
│ │
|
||
│ ├── spotmarket/
|
||
│ │ ├── spotmarketdataprovider.h/.cpp # Interface abstraite fournisseur
|
||
│ │ ├── spotmarketdataproviderawattar.h/.cpp # Implémentation aWATTar REST
|
||
│ │ └── spotmarketmanager.h/.cpp # Gestion fournisseur + scheduler
|
||
│ │
|
||
│ ├── types/ # Value types (Q_GADGET, JSON-RPC ready)
|
||
│ │ ├── charginginfo.h/.cpp # Config par chargeur (mode, cible, spot)
|
||
│ │ ├── chargingaction.h/.cpp # Action à appliquer (enabled, A, phases)
|
||
│ │ ├── chargingschedule.h/.cpp # Créneau planifié pour un chargeur
|
||
│ │ ├── chargingprocessinfo.h/.cpp # État interne processus de charge
|
||
│ │ ├── scoreentry.h/.cpp # Créneau spot (TimeFrame + prix + score)
|
||
│ │ ├── smartchargingstate.h/.cpp # État global du smart charging
|
||
│ │ └── timeframe.h/.cpp # Plage horaire (début/fin + helpers)
|
||
│ │
|
||
│ └── translations/ # .ts (de/en_US) → .qm via lrelease
|
||
│
|
||
├── tests/
|
||
│ ├── auto/
|
||
│ │ ├── common/ # EnergyTestBase partagée (nymea-tests)
|
||
│ │ ├── charging/ # → binaire : nymeaenrgytestcharging
|
||
│ │ ├── spotmarket/ # → binaire : nymeaenrgytestspotmarket
|
||
│ │ └── simulation/ # → binaire : nymea-energy-simulation (ENERGY_SIMULATION)
|
||
│ └── mocks/
|
||
│ ├── plugins/energymocks/ # Plugin d'intégration mock (JSON + C++)
|
||
│ └── spotmarketprovider/ # Mock spot market (datasets JSON statiques)
|
||
│
|
||
├── extension/ # ★ NOUVEAU — Fichiers d'extension ETM
|
||
│ ├── heatpump_dhw_thingclasses.json # ThingClasses PAC + ECS
|
||
│ ├── heatpumpmanager.h/.cpp # Gestionnaire PAC + ECS
|
||
│ ├── energyprioritymanager.h # Gestionnaire priorité énergie
|
||
│ ├── nymeaenergyjsonhandler_extension.cpp # Nouvelles méthodes RPC
|
||
│ ├── energypluginnymea_extension_diff.cpp # Diff init()
|
||
│ └── PLAN_EXTENSION.md # Plan détaillé avec UUIDs
|
||
│
|
||
├── integration_app_examples/ # ★ NOUVEAU — Exemples Dart/Flutter
|
||
│ ├── nymea_client.dart # Client WebSocket JSON-RPC complet
|
||
│ └── examples_usage.dart # 6 exemples d'utilisation
|
||
│
|
||
├── debian-qt5/, debian-qt6/ # Packaging Debian
|
||
├── nymea-energy-plugin-nymea.pro # Racine qmake (subdirs)
|
||
├── docker-simulation.sh # Simulations dans Docker
|
||
├── generate-coverage-report.sh # Rapport lcov HTML
|
||
├── doc.md # ★ Ce fichier
|
||
├── README.md
|
||
└── CLAUDE.md
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Things & UUIDs
|
||
|
||
> **Rappel architectural** : ce plugin Energy ne définit **pas** ses propres ThingClasses.
|
||
> Il interagit avec des Things créées par des plugins d'intégration qui implémentent les
|
||
> interfaces nymea : `evcharger`, `energymeter`, `electricvehicle`, `energystorage`.
|
||
> Le plugin mock de test (`a45e07fc-6ccc-40af-b7ad-bac4a003e775`) simule ces appareils.
|
||
|
||
### Identifiants du plugin mock de test
|
||
|
||
| Champ | Valeur |
|
||
|---|---|
|
||
| Plugin ID | `a45e07fc-6ccc-40af-b7ad-bac4a003e775` |
|
||
| Vendor ID | `2062d64d-3232-433c-88bc-0d33c0ba2ba6` |
|
||
| Vendor Name | nymea |
|
||
|
||
---
|
||
|
||
### ThingClass : Meter (Compteur d'énergie)
|
||
|
||
| Champ | Valeur |
|
||
|---|---|
|
||
| **ID** | `2721a051-6e12-471a-baba-21d87c4cebc9` |
|
||
| **Interfaces** | `energymeter`, `connectable` |
|
||
|
||
#### Param de configuration
|
||
|
||
| Nom | UUID | Type | Défaut |
|
||
|---|---|---|---|
|
||
| `port` | `7abcc8a1-08b1-45bc-9116-10f9848359f9` | uint | 6655 |
|
||
|
||
#### States
|
||
|
||
| Nom | UUID | Type | Unité | Writable | Défaut |
|
||
|---|---|---|---|---|---|
|
||
| `connected` | `3b393e45-594d-436a-bbd3-1f9b18ad9cfe` | bool | — | — | false |
|
||
| `voltagePhaseA` | `db018146-0441-4dc0-9834-6d43ebaf8311` | double | Volt | — | 230 |
|
||
| `voltagePhaseB` | `f0bae0af-2cde-4615-a36b-c81d7b233ebe` | double | Volt | — | 230 |
|
||
| `voltagePhaseC` | `fb76f2a8-0ace-4655-b368-1508843a15c6` | double | Volt | — | 230 |
|
||
| `currentPhaseA` | `00668c48-8b12-449a-907b-6744d65b021e` | double | Ampere | — | 0 |
|
||
| `currentPhaseB` | `6cd08b22-3a54-43a8-b828-b6ebb49678bb` | double | Ampere | — | 0 |
|
||
| `currentPhaseC` | `c9e196ec-0b59-43df-9ca6-4e318a63bf0f` | double | Ampere | — | 0 |
|
||
| `currentPower` | `f0490dd9-79ac-41ff-a952-083ed683687d` | double | Watt | — | 0 |
|
||
| `currentPowerPhaseA` | `ae450e65-2bbd-4054-84d6-b8f766b3d7cf` | double | Watt | — | 0 |
|
||
| `currentPowerPhaseB` | `dde0c5cf-9ce7-4e0d-979c-56d52d31eb25` | double | Watt | — | 0 |
|
||
| `currentPowerPhaseC` | `cfdc65bf-8e5a-40dd-acf3-79f3b86fd808` | double | Watt | — | 0 |
|
||
| `totalEnergyConsumed` | `8945c576-1e13-4611-adc8-4123b18d3a70` | double | kWh | — | 0 |
|
||
| `totalEnergyProduced` | `0420b758-e77f-4cf5-a30b-a6e1235b1efd` | double | kWh | — | 0 |
|
||
| `originalPower` *(sim)* | `e9776745-6f43-408d-9a4c-e5d74c711800` | double | Watt | **oui** | 0 |
|
||
|
||
---
|
||
|
||
### ThingClass : Charger (Chargeur VE)
|
||
|
||
| Champ | Valeur |
|
||
|---|---|
|
||
| **ID** | `5a3ae99f-c7da-46df-9104-f477be4606b7` |
|
||
| **Interfaces** | `evcharger`, `smartmeterconsumer`, `connectable` |
|
||
|
||
#### Params de configuration
|
||
|
||
| Nom | UUID | Type | Valeurs possibles | Défaut |
|
||
|---|---|---|---|---|
|
||
| `port` | `652624a2-8f9a-4bc3-b34f-5e3492af4d30` | uint | — | 6656 |
|
||
| `phases` | `facd5c76-d15e-4e29-9929-5e1764ae05dc` | QString | A / B / C / AB / BC / AC / ABC | "A" |
|
||
| `maxChargingCurrentUpperLimit` | `234c6676-1ec0-4eff-bed0-ecee7ce82074` | double | Ampere | 32 |
|
||
|
||
#### States
|
||
|
||
| Nom | UUID | Type | Unité | Writable | Min | Max | Défaut |
|
||
|---|---|---|---|---|---|---|---|
|
||
| `connected` | `6c18e134-0420-41b3-974c-869b5e7125e4` | bool | — | — | — | — | false |
|
||
| `power` | `13672543-9344-4d55-afd6-6393ae052f18` | bool | — | **oui** | — | — | true |
|
||
| `maxChargingCurrent` | `e7566b5b-8258-486e-b0ed-42a1bee332d9` | uint | A | **oui** | 6 | 32 | 6 |
|
||
| `pluggedIn` | `13f8f008-aa70-4772-9fb8-81a9674dd6ad` | bool | — | — | — | — | false |
|
||
| `charging` | `63879844-6342-45ed-8e97-276e0f3092e5` | bool | — | — | — | — | false |
|
||
| `phaseCount` | `728aa4b2-0c90-40da-9a46-6f07ab6a1497` | uint | — | — | 1 | 3 | 1 |
|
||
| `usedPhases` | `cc3abc60-42e1-421d-b32e-37c2c9a113a3` | QString | A…ABC | — | — | — | "A" |
|
||
| `currentPower` | `89d5bab8-3fad-41e4-a3cb-55cd673bbb6c` | double | W | — | — | — | 0 |
|
||
| `totalEnergyConsumed` | `aadf7384-5953-48b3-aedc-5c3835a61639` | double | kWh | — | — | — | 0 |
|
||
| `voltagePhaseA/B/C` | `9eb201d2…` / `06be3d4e…` / `e327de62…` | double | V | — | — | — | 230 |
|
||
| `currentPhaseA/B/C` | `025298dc…` / `ed7ee826…` / `7ab9b93a…` | double | A | — | — | — | 0 |
|
||
| `currentPowerPhaseA/B/C` | `09e9a514…` / `4f27a8f1…` / `859d3a00…` | double | W | — | — | — | 0 |
|
||
|
||
#### Actions
|
||
|
||
| Nom | UUID | Description |
|
||
|---|---|---|
|
||
| `update` | `924174ed-e2ed-4d28-b2de-750cf01e41e3` | Mise à jour manuelle des états (test) |
|
||
|
||
---
|
||
|
||
### ThingClass : ChargerPhaseSwitching (Chargeur avec commutation de phases)
|
||
|
||
| Champ | Valeur |
|
||
|---|---|
|
||
| **ID** | `9208d9f0-280c-469d-a145-106f3277470c` |
|
||
| **Interfaces** | `evcharger`, `smartmeterconsumer`, `connectable` |
|
||
|
||
> Identique à *Charger*, avec un state supplémentaire :
|
||
|
||
| State | UUID | Type | Valeurs | Writable |
|
||
|---|---|---|---|---|
|
||
| `desiredPhaseCount` | `b3c4618a-223f-4c97-80e8-04a2fb490083` | uint | 1, 3 | **oui** |
|
||
|
||
---
|
||
|
||
### ThingClass : SimpleCharger (Chargeur sans mesure)
|
||
|
||
| Champ | Valeur |
|
||
|---|---|
|
||
| **ID** | `29bcf255-b654-4764-be92-399bc26fe7c3` |
|
||
| **Interfaces** | `evcharger`, `connectable` |
|
||
| **States** | `connected` (bool), `power` (bool, writable), `maxChargingCurrent` (uint 6–32 A, writable) |
|
||
|
||
---
|
||
|
||
### ThingClass : Car (Véhicule électrique)
|
||
|
||
| Champ | Valeur |
|
||
|---|---|
|
||
| **ID** | `4513f801-836e-40a7-8784-c02650a9bdc6` |
|
||
| **Interface** | `electricvehicle` |
|
||
|
||
#### Settings (persistants par véhicule)
|
||
|
||
| Nom | UUID | Type | Min | Max | Défaut |
|
||
|---|---|---|---|---|---|
|
||
| `minChargingCurrent` | `1bb8e350-0e9f-4ab5-b814-8fd4ac8900a0` | uint (A) | 4 | 16 | 6 |
|
||
| `capacity` | `0f76f85f-7c53-48ce-9396-ea19c5aa16aa` | uint (kWh) | 5 | 200 | 50 |
|
||
| `phaseCount` | `01978cc7-ceed-4332-87ca-937f366c6d51` | uint | 1 | 3 | 1 |
|
||
| `chargingEnergyLoss` | `08b07382-35a5-4b40-8e43-321e12fbd2ce` | uint (%) | 5 | 35 | 10 |
|
||
|
||
#### States
|
||
|
||
| Nom | UUID | Type | Unité | Writable |
|
||
|---|---|---|---|---|
|
||
| `capacity` | `ce46c9d2-00d9-46f1-bc4f-9f2569393c70` | double | kWh | — |
|
||
| `batteryLevel` | `8aeadf2e-4d5a-4a38-a5a5-299c5b751b9e` | int | % | **oui** |
|
||
| `batteryCritical` | `2d6308d4-6ac1-43af-a374-b0ff79dfcb46` | bool | — | — |
|
||
| `minChargingCurrent` | `2536227c-9b17-462d-8d87-df2fb80eb72c` | uint | A | — |
|
||
| `phaseCount` | `96b9ce94-ed47-46db-bacd-2dcb5333031c` | uint | — | — |
|
||
|
||
---
|
||
|
||
### ThingClass : EnergyStorage (Stockage d'énergie / Batterie)
|
||
|
||
| Champ | Valeur |
|
||
|---|---|
|
||
| **ID** | `d0d5bbf0-249c-46ed-ac6a-5f271b2b0b0f` |
|
||
| **Interface** | `energystorage` |
|
||
|
||
#### States
|
||
|
||
| Nom | UUID | Type | Unité | Writable |
|
||
|---|---|---|---|---|
|
||
| `capacity` | `dfa1f2d2-793e-46a4-8e54-b3ffa40e343e` | double | kWh | — |
|
||
| `batteryLevel` | `f218bb34-913a-4f7f-b811-e9d0324d8d37` | int | % | **oui** |
|
||
| `currentPower` | `7577d0eb-2d4d-41e9-bd59-f750613c265a` | double | W | — |
|
||
| `batteryCritical` | `602902a9-9487-4de8-bffb-58121bebd89a` | bool | — | — |
|
||
|
||
---
|
||
|
||
### ThingClass : Notification
|
||
|
||
| Champ | Valeur |
|
||
|---|---|
|
||
| **ID** | `ee1871b8-46f9-4784-bbe1-e33db16b8753` |
|
||
| **Interface** | `notifications` |
|
||
|
||
#### Action : `notify`
|
||
|
||
| Param | UUID | Type |
|
||
|---|---|---|
|
||
| `title` | `801eb856-9eb3-43f5-9528-ead363bf3fd5` | QString |
|
||
| `body` | `06b8bebc-1ed9-4c19-8146-b9bc032fbcb0` | QString |
|
||
|
||
---
|
||
|
||
## 4. API JSON-RPC — Namespace `NymeaEnergy`
|
||
|
||
Le handler est enregistré sous `"NymeaEnergy"` **versions 0 à 8** via :
|
||
|
||
```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.0–1.0] |
|
||
| `SetAcquisitionTolerance` | `acquisitionTolerance: double` | `energyError` | Modifie le seuil |
|
||
| `GetBatteryLevelConsideration` | — | `batteryLevelConsideration: double` | Part du stockage prise en compte [0.0–1.0] |
|
||
| `SetBatteryLevelConsideration` | `batteryLevelConsideration: double` | `energyError` | Modifie le paramètre |
|
||
| `GetLockOnUnplug` | — | `lockOnUnplug: bool` | Verrou au débranchement |
|
||
| `SetLockOnUnplug` | `lockOnUnplug: bool` | `energyError` | Active/désactive le verrou |
|
||
| `GetChargingInfos` | `o:evChargerId: uuid` | `chargingInfos: []` | Config de charge de tous les chargeurs (ou un seul) |
|
||
| `SetChargingInfo` | `chargingInfo: object` | `energyError` | Modifie la config d'un chargeur (partial update) |
|
||
| `GetChargingSchedules` | — | `chargingSchedules: []` | Plannings de charge calculés |
|
||
|
||
### 4.2 Méthodes — Spot Market
|
||
|
||
| Méthode | Params | Retour | Description |
|
||
|---|---|---|---|
|
||
| `GetAvailableSpotMarketProviders` | — | `providers: []` | Liste des fournisseurs disponibles |
|
||
| `GetSpotMarketConfiguration` | — | `enabled, available, o:providerId` | Config spot market actuelle |
|
||
| `SetSpotMarketConfiguration` | `enabled: bool, o:providerId: uuid` | `energyError` | Active/change le fournisseur |
|
||
| `GetSpotMarketScoreEntries` | — | `spotMarketScoreEntries: []` | Scores pondérés (0=pire → 1=meilleur) |
|
||
|
||
### 4.3 Fournisseurs Spot Market intégrés
|
||
|
||
| Nom | UUID | Pays | URL API |
|
||
|---|---|---|---|
|
||
| aWATTar Austria | `5196b3cc-b2ee-46d6-b63a-7af2cf70ba67` | Autriche | `https://api.awattar.at/v1/marketdata` |
|
||
| aWATTar Germany | `0ca6ad88-e243-438d-a0f8-986cecf61834` | Allemagne | `https://api.awattar.de/v1/marketdata` |
|
||
|
||
### 4.4 Notifications push (WebSocket)
|
||
|
||
| Notification | Payload | Déclencheur |
|
||
|---|---|---|
|
||
| `PhasePowerLimitChanged` | `phasePowerLimit: uint` | Changement limite phase |
|
||
| `AcquisitionToleranceChanged` | `acquisitionTolerance: double` | Changement seuil acquisition |
|
||
| `BatteryLevelConsiderationChanged` | `batteryLevelConsideration: double` | Changement considération batterie |
|
||
| `LockOnUnplugChanged` | `lockOnUnplug: bool` | Changement verrou |
|
||
| `ChargingInfoAdded` | `chargingInfo: object` | Nouveau chargeur détecté |
|
||
| `ChargingInfoRemoved` | `evChargerThingId: uuid` | Chargeur supprimé |
|
||
| `ChargingInfoChanged` | `chargingInfo: object` | Config chargeur modifiée |
|
||
| `ChargingSchedulesChanged` | `chargingSchedules: []` | Plannings recalculés |
|
||
| `SpotMarketConfigurationChanged` | `enabled, available, o:providerId` | Config spot modifiée |
|
||
| `SpotMarketStatusChanged` | `enabled, available` | Statut spot (enabled/available) |
|
||
| `SpotMarketScoreEntriesChanged` | `spotMarketScoreEntries: []` | Nouveaux prix spot reçus |
|
||
|
||
### 4.5 Enums du namespace NymeaEnergy
|
||
|
||
**ChargingMode** :
|
||
|
||
| Valeur | Description |
|
||
|---|---|
|
||
| `ChargingModeNormal` | Charge pleine puissance dès branchement |
|
||
| `ChargingModeEco` | Charge uniquement avec surplus solaire |
|
||
| `ChargingModeEcoWithTargetTime` | Eco + garantie atteinte de la cible à l'heure définie |
|
||
|
||
**ChargingState** :
|
||
|
||
| Valeur | Description |
|
||
|---|---|
|
||
| `ChargingStateIdle` | En attente (aucune décision de charge) |
|
||
| `ChargingStateSurplusCharging` | Charge sur surplus solaire actif |
|
||
| `ChargingStateSpotMarketCharging` | Charge sur créneau spot bon marché |
|
||
| `ChargingStateTimeRequirement` | Charge forcée pour respecter l'heure cible |
|
||
|
||
---
|
||
|
||
## 5. Flow de données
|
||
|
||
### 5.1 Démarrage du plugin
|
||
|
||
```
|
||
Démarrage nymea daemon
|
||
│
|
||
▼
|
||
EnergyPluginNymea::init()
|
||
│
|
||
├─ [1] EnergyManagerConfiguration → lit energy-manager-configuration.json
|
||
│ (locks, limites spot)
|
||
├─ [2] QNetworkAccessManager → client HTTP pour aWATTar
|
||
│
|
||
├─ [3] SpotMarketManager → enregistre aWATTar AT + DE
|
||
│ └─ charge enabled/providerId depuis energy.conf
|
||
│
|
||
├─ [4] SmartChargingManager → s'abonne à ThingManager::thingAdded
|
||
│ └─ charge ChargingInfos depuis energy.conf
|
||
│
|
||
└─ [5] NymeaEnergyJsonHandler → enregistre namespace "NymeaEnergy" v0–8
|
||
```
|
||
|
||
### 5.2 Détection et setup des appareils
|
||
|
||
```
|
||
ThingManager::thingAdded(thing)
|
||
│
|
||
├─ interface == "evcharger" → setupEvCharger() → new EvCharger()
|
||
│ Écoute : pluggedIn, power, charging,
|
||
│ maxChargingCurrent, currentPower
|
||
│
|
||
├─ interface == "energymeter" → setupRootMeter() → new RootMeter()
|
||
│ Écoute : currentPower, currentPhaseA/B/C
|
||
│
|
||
├─ interface == "electricvehicle" → écoute batteryLevel, capacity
|
||
│
|
||
└─ interface == "energystorage" → écoute batteryLevel, currentPower
|
||
```
|
||
|
||
### 5.3 Boucle de décision SmartChargingManager
|
||
|
||
```
|
||
Déclencheurs :
|
||
• currentPowerChanged (compteur) ← puissance réseau modifiée
|
||
• stateValueChanged (chargeur/VE) ← voiture branchée, état changé
|
||
• scoreEntriesUpdated (spot market) ← nouveaux prix reçus
|
||
• Timer interne ← tick périodique
|
||
|
||
▼
|
||
update(currentDateTime)
|
||
│
|
||
├─ prepareInformation()
|
||
│ Collecte : puissance compteur par phase, état chargeurs,
|
||
│ niveau batterie VE, niveau batterie stockage,
|
||
│ scores spot market du jour
|
||
│
|
||
├─ planSpotMarketCharging()
|
||
│ Si spot market activé + données disponibles :
|
||
│ → SpotMarketManager::scheduleChargingTime()
|
||
│ → Calcule les créneaux optimaux (x heures les moins chères)
|
||
│ → Fusionne les micro-créneaux (minimumScheduleDuration)
|
||
│
|
||
├─ planSurplusCharging()
|
||
│ Si mode Eco : surplus = (puissance_produite - consommation_autre)
|
||
│ Si surplus > acquisitionTolerance × puissance_min_charge :
|
||
│ → Calcule courant disponible par phase
|
||
│
|
||
├─ adjustEvChargers()
|
||
│ Pour chaque chargeur :
|
||
│ → Applique ChargingAction (enable/disable, courant, phases)
|
||
│ → Respecte les locks (chargingEnabledLockDuration, chargingCurrentLockDuration)
|
||
│
|
||
└─ verifyOverloadProtection()
|
||
Si currentPhaseX > phasePowerLimit :
|
||
→ Réduit maxChargingCurrent ou désactive le chargeur
|
||
verifyOverloadProtectionRecovery() → restaure dès que possible
|
||
```
|
||
|
||
### 5.4 Flux vers les clients (push notifications)
|
||
|
||
```
|
||
SmartChargingManager
|
||
│ chargingInfoChanged
|
||
│ chargingSchedulesChanged
|
||
▼
|
||
NymeaEnergyJsonHandler
|
||
│ ChargingInfoChanged { chargingInfo }
|
||
│ ChargingSchedulesChanged { chargingSchedules[] }
|
||
▼
|
||
WebSocket clients (apps, UI)
|
||
```
|
||
|
||
### 5.5 États du cycle de vie d'un chargeur
|
||
|
||
```
|
||
┌─────────────────┐ voiture branchée ┌──────────────────┐
|
||
│ IDLE │ ──────────────────────▶│ WAITING │
|
||
│ pluggedIn=false│ │ pluggedIn=true │
|
||
│ charging=false │ │ charging=false │
|
||
└─────────────────┘ └────────┬─────────┘
|
||
▲ │ conditions OK
|
||
│ voiture débranchée ▼
|
||
│ ┌──────────────────┐
|
||
│ │ CHARGING │
|
||
└──────────────────────────────────│ charging=true │
|
||
│ currentPower > 0│
|
||
└──────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Configuration requise
|
||
|
||
### 6.1 Fichier de configuration runtime (optionnel)
|
||
|
||
**Emplacement** : `/var/lib/nymea/energy-manager-configuration.json`
|
||
**Surcharge** : variable d'environnement `NYMEA_ENERGY_MANAGER_CONFIG`
|
||
|
||
```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]
|
||
(v0–8, 15 méthodes) ├─ EnergyPriorityManager [NOUVEAU]
|
||
└─ NymeaEnergyJsonHandler [ÉTENDU]
|
||
(v0–9, 30 méthodes)
|
||
```
|
||
|
||
### 9.2 Nouveaux fichiers à créer
|
||
|
||
| Fichier | Description |
|
||
|---|---|
|
||
| `energyplugin/heatpumpmanager.h/.cpp` | Détection PAC + ECS via ThingManager, wrappers, actions |
|
||
| `energyplugin/energyprioritymanager.h/.cpp` | Allocation de puissance par priorité (5 niveaux) |
|
||
| Plugin intégration ETM `.json` | +2 ThingClasses : `heatPump`, `domesticHotWater` |
|
||
|
||
### 9.3 Nouvelle ThingClass : Pompe à chaleur
|
||
|
||
**ThingClass UUID** : `b5a8f3d2-4c1e-4a9f-8b2d-3e6c0f1d5a7b`
|
||
**Interfaces** : `heatpump`, `connectable`
|
||
**Protocole** : Modbus TCP (port 502 configurable)
|
||
|
||
#### States PAC
|
||
|
||
| Nom | UUID | Type | Unité | Writable | Contraintes |
|
||
|---|---|---|---|---|---|
|
||
| `connected` | `c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f` | bool | — | — | — |
|
||
| `power` | `d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f8a` | bool | — | **oui** | — |
|
||
| `mode` | `e5f6a7b8-c9d0-4e1f-2a3b-4c5d6e7f8a9b` | QString | — | **oui** | heating/cooling/auto/standby |
|
||
| `targetTemperature` | `f6a7b8c9-d0e1-4f2a-3b4c-5d6e7f8a9b0c` | double | °C | **oui** | 15–65 °C |
|
||
| `currentTemperature` | `a7b8c9d0-e1f2-4a3b-4c5d-6e7f8a9b0c1d` | double | °C | — | — |
|
||
| `outdoorTemperature` | `b8c9d0e1-f2a3-4b4c-5d6e-7f8a9b0c1d2e` | double | °C | — | — |
|
||
| `currentPower` | `c9d0e1f2-a3b4-4c5d-6e7f-8a9b0c1d2e3f` | double | W | — | — |
|
||
| `cop` | `d0e1f2a3-b4c5-4d6e-7f8a-9b0c1d2e3f4a` | double | — | — | COP instantané |
|
||
| `defrostActive` | `e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b` | bool | — | — | Dégivrage en cours |
|
||
| `errorCode` | `f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c` | uint | — | — | 0 = pas d'erreur |
|
||
|
||
#### Actions PAC
|
||
|
||
| Action | UUID | Paramètre | Type | Contraintes |
|
||
|---|---|---|---|---|
|
||
| `setMode` | `a3b4c5d6-e7f8-4a9b-0c1d-2e3f4a5b6c7d` | `mode` (QString) | — | heating/cooling/auto/standby |
|
||
| `setTargetTemperature` | `c5d6e7f8-a9b0-4c1d-2e3f-4a5b6c7d8e9f` | `targetTemperature` (double °C) | — | 15–65 °C |
|
||
|
||
#### Events PAC
|
||
|
||
| Event | UUID | Description |
|
||
|---|---|---|
|
||
| `defrostStarted` | `e7f8a9b0-c1d2-4e3f-4a5b-6c7d8e9f0a1b` | Dégivrage automatique démarré |
|
||
| `errorOccurred` | `f8a9b0c1-d2e3-4f4a-5b6c-7d8e9f0a1b2c` | Erreur détectée (param: `errorCode`) |
|
||
|
||
---
|
||
|
||
### 9.4 Nouvelle ThingClass : Eau Chaude Sanitaire (ECS)
|
||
|
||
**ThingClass UUID** : `c7d2e4f1-5b3a-4c0d-9e6f-2a4b7c1d3e5f`
|
||
**Interfaces** : `heating`, `connectable`
|
||
|
||
#### States ECS
|
||
|
||
| Nom | UUID | Type | Unité | Writable | Contraintes |
|
||
|---|---|---|---|---|---|
|
||
| `connected` | `c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f` | bool | — | — | — |
|
||
| `power` | `d2e3f4a5-b6c7-4d8e-9f0a-1b2c3d4e5f6a` | bool | — | **oui** | — |
|
||
| `targetTemperature` | `e3f4a5b6-c7d8-4e9f-0a1b-2c3d4e5f6a7b` | double | °C | **oui** | 40–75 °C |
|
||
| `currentTemperature` | `f4a5b6c7-d8e9-4f0a-1b2c-3d4e5f6a7b8c` | double | °C | — | — |
|
||
| `boostMode` | `a5b6c7d8-e9f0-4a1b-2c3d-4e5f6a7b8c9d` | bool | — | **oui** | Mode chauffe rapide |
|
||
| `currentPower` | `b6c7d8e9-f0a1-4b2c-3d4e-5f6a7b8c9d0e` | double | W | — | — |
|
||
| `legionellaProtectionActive` | `c7d8e9f0-a1b2-4c3d-4e5f-6a7b8c9d0e1f` | bool | — | — | Cycle anti-légionellose |
|
||
|
||
#### Actions ECS
|
||
|
||
| Action | UUID | Paramètre | Type | Contraintes |
|
||
|---|---|---|---|---|
|
||
| `triggerBoost` | `d8e9f0a1-b2c3-4d4e-5f6a-7b8c9d0e1f2a` | `duration` (uint, minutes) | — | 15–120 min |
|
||
|
||
#### Events ECS
|
||
|
||
| Event | UUID | Description |
|
||
|---|---|---|
|
||
| `targetTemperatureReached` | `f0a1b2c3-d4e5-4f6a-7b8c-9d0e1f2a3b4c` | Température cible atteinte |
|
||
| `legionellaProtectionTriggered` | `a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d` | Cycle anti-légionellose déclenché |
|
||
|
||
---
|
||
|
||
### 9.5 Nouvelles méthodes JSON-RPC (NymeaEnergy v9)
|
||
|
||
#### Méthodes PAC
|
||
|
||
| Méthode | Params | Retour | Description |
|
||
|---|---|---|---|
|
||
| `GetHeatPumps` | — | `heatPumps: []` | Liste PAC + tous leurs états |
|
||
| `SetHeatPumpPower` | `thingId, power: bool` | `energyError` | Allumer / éteindre |
|
||
| `SetHeatPumpMode` | `thingId, mode: string` | `energyError` | Mode heating/cooling/auto/standby |
|
||
| `SetHeatPumpTargetTemperature` | `thingId, targetTemperature: double` | `energyError` | Consigne [15–65°C] |
|
||
|
||
#### Méthodes ECS
|
||
|
||
| Méthode | Params | Retour | Description |
|
||
|---|---|---|---|
|
||
| `GetDHWDevices` | — | `dhwDevices: []` | Liste ECS + tous leurs états |
|
||
| `SetDHWPower` | `thingId, power: bool` | `energyError` | Activer / désactiver |
|
||
| `SetDHWTargetTemperature` | `thingId, targetTemperature: double` | `energyError` | Consigne [40–75°C] |
|
||
| `TriggerDHWBoost` | `thingId, duration: uint` | `energyError` | Mode boost 15–120 min |
|
||
|
||
#### Méthodes Gestion d'énergie
|
||
|
||
| Méthode | Params | Retour | Description |
|
||
|---|---|---|---|
|
||
| `GetManagedLoads` | — | `managedLoads: []` | Toutes les charges avec priorité + puissances |
|
||
| `SetLoadPriority` | `thingId, priority: enum` | `energyError` | Modifier la priorité (0–4) |
|
||
| `SetAllocatedPower` | `thingId, allocatedPower: double` | `energyError` | Forcer puissance allouée (W) |
|
||
| `GetTotalAvailablePower` | — | `totalAvailablePower: double` | Puissance totale disponible |
|
||
| `SetTotalAvailablePower` | `totalAvailablePower: double` | `energyError` | Modifier la puissance totale |
|
||
|
||
#### Nouvelles notifications push
|
||
|
||
| Notification | Payload | Déclencheur |
|
||
|---|---|---|
|
||
| `HeatPumpStateChanged` | `thingId, stateName, value` | Changement d'un état PAC |
|
||
| `DHWStateChanged` | `thingId, stateName, value` | Changement d'un état ECS |
|
||
| `ManagedLoadChanged` | `thingId, priority, allocatedPower, active` | Changement de priorité ou allocation |
|
||
| `TotalAvailablePowerChanged` | `totalAvailablePower: double` | Changement puissance disponible |
|
||
|
||
---
|
||
|
||
### 9.6 Gestion de priorité énergétique
|
||
|
||
#### Les 5 niveaux de priorité
|
||
|
||
| Niveau | Valeur | Cas d'usage | Comportement si surcharge |
|
||
|---|---|---|---|
|
||
| `Critical` | 0 | Chauffage PAC hiver, anti-légionellose | **Jamais coupé** |
|
||
| `High` | 1 | Chauffage standard, ECS sécurité | Coupé en dernier recours |
|
||
| `Normal` | 2 | Recharge VE normale, ECS standard | Comportement standard |
|
||
| `Low` | 3 | VE mode Eco, ECS boost, auxiliaires | Interruptible |
|
||
| `Optional` | 4 | VE spot market, confort | Seulement si surplus disponible |
|
||
|
||
#### Algorithme de rééquilibrage
|
||
|
||
```
|
||
rebalance()
|
||
│
|
||
├─ puissanceDisponible = totalAvailablePower - reservedForCritical
|
||
│
|
||
├─ Pour chaque charge, triée par priorité (Critical → Optional) :
|
||
│ │
|
||
│ ├─ Si puissanceDisponible ≥ requestedPower :
|
||
│ │ → allocatedPower = requestedPower, active = true
|
||
│ │ → puissanceDisponible -= requestedPower
|
||
│ │
|
||
│ ├─ Sinon si puissanceDisponible > 0 :
|
||
│ │ → allocatedPower = puissanceDisponible, active = true (throttle)
|
||
│ │ → puissanceDisponible = 0
|
||
│ │
|
||
│ └─ Sinon :
|
||
│ → allocatedPower = 0, active = false ← charge coupée
|
||
│
|
||
└─ Pour chaque charge modifiée : emit managedLoadChanged()
|
||
+ appliquer l'action sur le Thing
|
||
```
|
||
|
||
#### Priorités recommandées par type
|
||
|
||
| Charge | Priorité | Raison |
|
||
|---|---|---|
|
||
| PAC chauffage (hiver) | Critical | Confort thermique impératif |
|
||
| ECS anti-légionellose | Critical | Sécurité sanitaire |
|
||
| PAC chauffage standard | High | Confort prioritaire |
|
||
| ECS normale | Normal | Usage quotidien |
|
||
| Recharge VE mode Normal | Normal | Usage quotidien |
|
||
| Recharge VE mode Eco | Low | Interruptible sans conséquence |
|
||
| ECS mode boost | Low | Confort non-critique |
|
||
| Recharge VE spot market | Optional | Seulement si surplus/prix bas |
|
||
|
||
---
|
||
|
||
### 9.7 Modifications des fichiers existants
|
||
|
||
#### `energyplugin.pri` — ajouts
|
||
|
||
```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*
|