# 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*
