# 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": "", "mode": "heating" }, "token": "..." } { "id": 21, "method": "NymeaEnergy.SetHeatPumpTargetTemperature", "params": { "thingId": "", "targetTemperature": 22.0 }, "token": "..." } ``` #### Déclencher un boost ECS ```json { "id": 22, "method": "NymeaEnergy.TriggerDHWBoost", "params": { "thingId": "", "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": "", "priority": "LoadPriorityLow" }, "token": "..." } // Mettre la PAC en priorité critique (ne jamais couper) { "id": 25, "method": "NymeaEnergy.SetLoadPriority", "params": { "thingId": "", "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 computeSchedule( const QList &forecast, const QList &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 &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 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* --- ## Section 12 — Installer Setup (LoadAdapterRegistry, v12) ### 12.1 Vue d'ensemble L'**Installer Setup** permet à un installateur de mapper des rôles énergétiques (`EVCharger`, `DHW`, `HeatPump`, `Battery`, `SolarMeter`, `GridMeter`) à des Things nymea concrètes. Le `LoadAdapterRegistry` crée ensuite l'adaptateur approprié (relay, SG-Ready, evcharger, battery) et transmet les consignes du Scheduler au matériel réel. **Flux en 3 étapes :** 1. Activer le rôle → `SetRoleEnabled(role, true)` 2. Choisir la Thing → `AssignThingToRole(role, thingId)` 3. Tester la connexion → `TestRoleConnection(role)` → notification `ConnectionTestResult` ### 12.2 Tableau de compatibilité (rôle → interfaces requises) | Rôle | Interfaces nymea requises | |---|---| | `EVCharger` | `evcharger` | | `DHW` | `relay` ou `smartmeterconsumer` | | `HeatPump` | `heating` (SG-Ready) ou `relay` | | `Battery` | `energystorage` | | `SolarMeter` | `energymeter` | | `GridMeter` | `energymeter` | ### 12.3 Détection automatique du type d'adaptateur | Rôle | Interface détectée | Adaptateur créé | |---|---|---| | `EVCharger` | `evcharger` | `EvChargerAdapter` | | `DHW` | `relay` | `RelayAdapter` | | `HeatPump` | `heating` | `SgReadyAdapter` (2 relais) | | `HeatPump` | `relay` | `RelayAdapter` | | `Battery` | `energystorage` | `BatteryAdapter` | | `SolarMeter` | `energymeter` | read-only (pas d'adaptateur) | | `GridMeter` | `energymeter` | read-only (pas d'adaptateur) | ### 12.4 Référence API JSON-RPC v12 #### `GetSetupStatus` → `{ setupStatus: object }` Retourne l'état de tous les rôles. ```json { "method": "NymeaEnergy.GetSetupStatus", "params": {} } // Réponse: { "setupStatus": { "roles": [ { "role": "DHW", "enabled": true, "assigned": true, "assignedThingId": "…uuid…", "assignedThingName": "Ballon ECS", "reachable": true, "adapterType": "relay", "lastError": "" } ], "allEnabledRolesOk": true, "configuredCount": 2, "errorCount": 0 } } ``` #### `GetCompatibleThings` → `{ role, things: [] }` ```json { "method": "NymeaEnergy.GetCompatibleThings", "params": { "role": "DHW" } } ``` #### `AssignThingToRole` → `{ energyError, adapterType, detectedParams }` ```json { "method": "NymeaEnergy.AssignThingToRole", "params": { "role": "HeatPump", "thingId": "…relay1-uuid…", "params": { "relay1ThingId": "…", "relay2ThingId": "…", "normalPowerW": 1500 } } } ``` #### `UnassignRole` → `{ energyError }` ```json { "method": "NymeaEnergy.UnassignRole", "params": { "role": "Battery" } } ``` #### `SetRoleEnabled` → `{ energyError }` ```json { "method": "NymeaEnergy.SetRoleEnabled", "params": { "role": "DHW", "enabled": true } } ``` #### `TestRoleConnection` → `{ energyError }` + notification `ConnectionTestResult` ```json { "method": "NymeaEnergy.TestRoleConnection", "params": { "role": "DHW" } } // Notification: { "notification": "NymeaEnergy.ConnectionTestResult", "params": { "role": "DHW", "success": true, "message": "Connection test OK" } } ``` ### 12.5 Notifications push | Notification | Payload | Déclencheur | |---|---|---| | `SetupStatusChanged` | `{ setupStatus }` | Assignment, enable, reachability | | `ConnectionTestResult` | `{ role, success, message }` | Fin de `TestRoleConnection` | | `ThingBecameCompatible` | `{ role, thing }` | Nouvelle Thing ajoutée dans nymea | ### 12.6 Gestion des erreurs — Thing disparue Si une Thing assignée disparaît au redémarrage du daemon : - L'entrée dans `adapters.conf` est conservée - `RoleStatus.reachable = false`, `lastError = "Thing not found: "` - `SetupStatus.allEnabledRolesOk = false` - Notification `SetupStatusChanged` émise avec l'erreur - Dès que la Thing réapparaît (`ThingManager::thingAdded`), notification `ThingBecameCompatible` ### 12.7 Exemple complet — EV charger hebdomadaire via Installer Setup ```json // 1. Activer le rôle { "method": "NymeaEnergy.SetRoleEnabled", "params": { "role": "EVCharger", "enabled": true } } // 2. Lister les Things compatibles { "method": "NymeaEnergy.GetCompatibleThings", "params": { "role": "EVCharger" } } // 3. Assigner la Thing choisie { "method": "NymeaEnergy.AssignThingToRole", "params": { "role": "EVCharger", "thingId": "…uuid…", "params": { "phases": 1 } } } // 4. Tester { "method": "NymeaEnergy.TestRoleConnection", "params": { "role": "EVCharger" } } // → ConnectionTestResult { "success": true, "message": "EV charger connected" } // 5. Configurer un créneau manuel EV { "method": "NymeaEnergy.SetManualSlot", "params": { "start": "2026-02-23T22:00:00.000Z", "end": "2026-02-24T06:00:00.000Z", "label": "Recharge hebdo VE", "repeating": true, "allocations": { "ev": 2000 } } } ``` ### 12.8 Persistance Les assignements sont sauvegardés dans : ``` NymeaSettings::settingsPath() + "/adapters.conf" ``` Format INI, identique à `scheduler.conf`. Chargés automatiquement au démarrage du daemon. --- *Section 12 ajoutée le 2026-02-24 — Installer Setup LoadAdapterRegistry v12*