# INTERFACE.md — API JSON-RPC du plugin `nymea-energy-plugin-nymea`

> Ce fichier fait autorité sur l'interface exposée par le plugin.
> Mettre à jour ce fichier à chaque modification de l'API.
>
> **Namespace :** `NymeaEnergy`
> **Versions enregistrées :** 0–8 (`registerExperienceHandler(..., 0, 8)`)
> **Transport :** WebSocket JSON-RPC 2.0, port 4444 (nymea standard)

---

## Enums

### `ChargingMode`
| Valeur | Description |
|---|---|
| `ChargingModeNormal` | Recharge immédiate au maximum disponible |
| `ChargingModeEco` | Recharge sur surplus solaire uniquement |
| `ChargingModeEcoWithTargetTime` | Eco + deadline de fin (`endDateTime` + `targetPercentage`) |
| `ChargingModeEcoWithMinCurrent` | Eco + courant minimum garanti (6 A) si pas de surplus |
| `ChargingModeEcoMinWithTargetTime` | Eco + courant minimum + deadline |

### `ChargingState` (lecture seule — calculé par le manager)
| Valeur | Description |
|---|---|
| `ChargingStateIdle` | Pas de recharge active |
| `ChargingStateSurplusCharging` | Recharge sur surplus solaire en cours |
| `ChargingStateSpotMarketCharging` | Recharge sur créneau spot market en cours |
| `ChargingStateTimeRequirement` | Recharge forcée pour atteindre la deadline |

### `ChargingActionIssuer`
| Valeur | Description |
|---|---|
| `ChargingActionIssuerUnknown` | Émetteur non identifié |
| `ChargingActionIssuerSurplusCharging` | Action émise par le surplus manager |
| `ChargingActionIssuerSpotMarketCharging` | Action émise par le spot market manager |
| `ChargingActionIssuerTimeRequirement` | Action émise pour honorer la deadline |
| `ChargingActionIssuerOverloadProtection` | Action émise par la protection surcharge |

### `EnergyError`
| Valeur | Description |
|---|---|
| `EnergyErrorNoError` | Succès |
| `EnergyErrorMissingParameter` | Paramètre obligatoire absent |
| `EnergyErrorInvalidParameter` | Valeur de paramètre invalide |

---

## Types

### `ChargingInfo`
Configuration d'une borne EV. Seuls les champs marqués `(writable)` peuvent être modifiés via `SetChargingInfo` — les autres sont ignorés ou read-only.

| Champ | Type | Writable | Description |
|---|---|---|---|
| `evChargerId` | `Uuid` | — | Identifiant de la borne (obligatoire dans SetChargingInfo) |
| `assignedCarId` | `Uuid` | ✓ | Véhicule associé (optionnel) |
| `chargingMode` | `ChargingMode` | ✓ | Mode de recharge |
| `endDateTime` | `DateTime` | ✓ | Deadline cible (EcoWithTargetTime / EcoMinWithTargetTime) |
| `repeatDays` | `[Int]` | ✓ | Jours de répétition (0=Lun … 6=Dim, liste vide = pas de répétition) |
| `targetPercentage` | `Uint` | ✓ | Niveau batterie cible en % (défaut : 80) |
| `chargingState` | `ChargingState` | — | État calculé (lecture seule) |
| `spotMarketChargingEnabled` | `Bool` | ✓ | Activer la recharge sur spot market |
| `dailySpotMarketPercentage` | `Uint` | ✓ | % de la charge quotidienne à faire en heures spot bon marché |

### `ChargingAction`
Action envoyée au charger physique (présente dans les schedules).

| Champ | Type | Description |
|---|---|---|
| `chargingEnabled` | `Bool` | Activer/désactiver la recharge |
| `maxChargingCurrent` | `Double` | Courant max en Ampères |
| `desiredPhaseCount` | `Uint` | Nombre de phases souhaitées (1 ou 3) |
| `issuer` | `ChargingActionIssuer` | Origine de la décision |

### `ChargingSchedule`
Créneau de recharge planifié (résultat du calcul du manager).

| Champ | Type | Description |
|---|---|---|
| `evChargerId` | `Uuid` | Borne concernée |
| `startDateTime` | `DateTime` | Début du créneau |
| `endDateTime` | `DateTime` | Fin du créneau |
| `action` | `ChargingAction` | Action prévue sur ce créneau |

### `ScoreEntry`
Entrée de cotation horaire du spot market, avec pondération relative.

| Champ | Type | Description |
|---|---|---|
| `startDateTime` | `DateTime` | Début du créneau tarifaire |
| `endDateTime` | `DateTime` | Fin du créneau tarifaire |
| `value` | `Float` | Prix brut (€/MWh, selon provider) |
| `weighting` | `Float` | Pondération relative : 0.0 = le plus cher, 1.0 = le moins cher |

### `SpotMarketProviderInfo`
Fournisseur de données spot market disponible.

| Champ | Type | Description |
|---|---|---|
| `providerId` | `Uuid` | Identifiant du provider |
| `name` | `String` | Nom affiché |
| `country` | `Uint` | Pays (`QLocale::Country`) |
| `website` | `String` | URL du site du provider |

**Providers enregistrés :**
| Nom | `providerId` |
|---|---|
| aWATTar AT | `5196b3cc-b2ee-46d6-b63a-7af2cf70ba67` |
| aWATTar DE | `0ca6ad88-e243-438d-a0f8-986cecf61834` |

---

## Méthodes

> Notation : `o:` = paramètre optionnel. Tous les appels requièrent un token d'auth nymea.

---

### Overload protection

#### `NymeaEnergy.GetPhasePowerLimit`
Retourne la limite de courant par phase configurée.

**Params :** aucun

**Returns :**
```json
{ "phasePowerLimit": 25 }
```
> `0` signifie non configuré — tout le smart charging est désactivé.

---

#### `NymeaEnergy.SetPhasePowerLimit`
Définit la limite de courant par phase (en Ampères). `0` désactive le smart charging.

**Params :**
```json
{ "phasePowerLimit": 25 }
```

**Returns :**
```json
{ "energyError": "EnergyErrorNoError" }
```

---

### Paramètres de recharge

#### `NymeaEnergy.GetAcquisitionTolerance`
Retourne le seuil de surplus nécessaire pour démarrer une recharge Eco.

**Params :** aucun

**Returns :**
```json
{ "acquisitionTolerance": 0.5 }
```
> Valeur entre 0.0 et 1.0. Plus la valeur est haute, plus il faut de surplus.

---

#### `NymeaEnergy.SetAcquisitionTolerance`
**Params :**
```json
{ "acquisitionTolerance": 0.5 }
```

**Returns :**
```json
{ "energyError": "EnergyErrorNoError" }
```
> Retourne `EnergyErrorInvalidParameter` si hors de `[0.0, 1.0]`.

---

#### `NymeaEnergy.GetBatteryLevelConsideration`
Retourne le facteur de prise en compte du stockage batterie dans le calcul du surplus.

**Params :** aucun

**Returns :**
```json
{ "batteryLevelConsideration": 0.9 }
```
> 0.0 = stockage ignoré, 1.0 = surplus réduit intégralement de la puissance de charge batterie.

---

#### `NymeaEnergy.SetBatteryLevelConsideration`
**Params :**
```json
{ "batteryLevelConsideration": 0.9 }
```

**Returns :**
```json
{ "energyError": "EnergyErrorNoError" }
```
> Retourne `EnergyErrorInvalidParameter` si hors de `[0.0, 1.0]`.

---

#### `NymeaEnergy.GetLockOnUnplug`
Retourne si la borne doit être verrouillée à la déconnexion du véhicule.

**Params :** aucun

**Returns :**
```json
{ "lockOnUnplug": false }
```

---

#### `NymeaEnergy.SetLockOnUnplug`
**Params :**
```json
{ "lockOnUnplug": true }
```

**Returns :**
```json
{ "energyError": "EnergyErrorNoError" }
```

---

### Configuration EV

#### `NymeaEnergy.GetChargingInfos`
Retourne la configuration de recharge de toutes les bornes, ou d'une seule.

**Params :**
```json
{ "o:evChargerId": "uuid-de-la-borne" }
```

**Returns :**
```json
{
  "chargingInfos": [
    {
      "evChargerId": "uuid-de-la-borne",
      "assignedCarId": "uuid-du-car",
      "chargingMode": "ChargingModeEco",
      "chargingState": "ChargingStateSurplusCharging",
      "endDateTime": "2025-06-15T08:00:00+02:00",
      "repeatDays": [1, 2, 3, 4, 5],
      "targetPercentage": 80,
      "spotMarketChargingEnabled": false,
      "dailySpotMarketPercentage": 0
    }
  ]
}
```

---

#### `NymeaEnergy.SetChargingInfo`
Met à jour la configuration d'une borne. Seuls les champs `USER true` sont modifiables (voir type `ChargingInfo`). Les autres champs sont ignorés.

**Params :**
```json
{
  "chargingInfo": {
    "evChargerId": "uuid-de-la-borne",
    "chargingMode": "ChargingModeEcoWithTargetTime",
    "endDateTime": "2025-06-15T08:00:00+02:00",
    "targetPercentage": 80
  }
}
```

**Returns :**
```json
{ "energyError": "EnergyErrorNoError" }
```

---

#### `NymeaEnergy.GetChargingSchedules`
Retourne le planning de recharge calculé (toutes les bornes, horizon ~24h).

**Params :** aucun

**Returns :**
```json
{
  "chargingSchedules": [
    {
      "evChargerId": "uuid-de-la-borne",
      "startDateTime": "2025-06-15T02:00:00+02:00",
      "endDateTime": "2025-06-15T04:30:00+02:00",
      "action": {
        "chargingEnabled": true,
        "maxChargingCurrent": 16.0,
        "desiredPhaseCount": 1,
        "issuer": "ChargingActionIssuerSpotMarketCharging"
      }
    }
  ]
}
```

---

### Spot market

#### `NymeaEnergy.GetAvailableSpotMarketProviders`
Retourne la liste des providers spot market disponibles.

**Params :** aucun

**Returns :**
```json
{
  "providers": [
    {
      "providerId": "5196b3cc-b2ee-46d6-b63a-7af2cf70ba67",
      "name": "aWATTar AT",
      "country": 14,
      "website": "https://www.awattar.at"
    },
    {
      "providerId": "0ca6ad88-e243-438d-a0f8-986cecf61834",
      "name": "aWATTar DE",
      "country": 82,
      "website": "https://www.awattar.de"
    }
  ]
}
```

---

#### `NymeaEnergy.GetSpotMarketConfiguration`
Retourne la configuration spot market courante.

**Params :** aucun

**Returns :**
```json
{
  "enabled": true,
  "available": true,
  "o:providerId": "5196b3cc-b2ee-46d6-b63a-7af2cf70ba67"
}
```
> `providerId` est absent si `enabled` est `false`.

---

#### `NymeaEnergy.SetSpotMarketConfiguration`
Active ou désactive le spot market et sélectionne le provider.

**Params :**
```json
{
  "enabled": true,
  "o:providerId": "5196b3cc-b2ee-46d6-b63a-7af2cf70ba67"
}
```
> Si `enabled: true`, `providerId` est obligatoire et doit être un UUID valide.

**Returns :**
```json
{ "energyError": "EnergyErrorNoError" }
```
> Retourne `EnergyErrorInvalidParameter` si `enabled: true` sans `providerId`, ou `providerId` inconnu.

---

#### `NymeaEnergy.GetSpotMarketScoreEntries`
Retourne les cotations pondérées du provider actif. Liste vide si désactivé ou non disponible.

**Params :** aucun

**Returns :**
```json
{
  "spotMarketScoreEntries": [
    {
      "startDateTime": "2025-06-15T00:00:00+02:00",
      "endDateTime": "2025-06-15T01:00:00+02:00",
      "value": 45.2,
      "weighting": 0.85
    }
  ]
}
```

---

## Notifications

S'abonner via `JSONRPC.SetNotificationStatus` avec le namespace `"NymeaEnergy"`.

---

### `NymeaEnergy.PhasePowerLimitChanged`
```json
{ "phasePowerLimit": 25 }
```

---

### `NymeaEnergy.AcquisitionToleranceChanged`
```json
{ "acquisitionTolerance": 0.5 }
```

---

### `NymeaEnergy.BatteryLevelConsiderationChanged`
```json
{ "batteryLevelConsideration": 0.9 }
```

---

### `NymeaEnergy.LockOnUnplugChanged`
```json
{ "lockOnUnplug": true }
```

---

### `NymeaEnergy.ChargingInfoAdded`
Émis quand une nouvelle borne est détectée et sa `ChargingInfo` créée.
```json
{ "chargingInfo": { ... } }
```

---

### `NymeaEnergy.ChargingInfoRemoved`
Émis quand une borne est supprimée.
```json
{ "evChargerThingId": "uuid-de-la-borne" }
```

---

### `NymeaEnergy.ChargingInfoChanged`
Émis à chaque modification de la `ChargingInfo` (config ou état).
```json
{ "chargingInfo": { ... } }
```

---

### `NymeaEnergy.ChargingSchedulesChanged`
Émis à chaque recalcul du planning (cycle ~1 min), **et** à chaque transition du mode
dégradé L2 (watchdog fraîcheur compteur).
```json
{
  "chargingSchedules": [ ... ],
  "o:degradedMode": false
}
```
- `degradedMode` *(bool, optionnel — [ETM])* : `true` quand le compteur est muet depuis
  > 90 s et que les consignes de repli L2 sont actives (planification suspendue, ECS coupé,
  EV en charge clampé au minimum). Repasse à `false` au retour du compteur. Champ additif :
  les clients antérieurs l'ignorent. Détail : `docs/SAFETY.md` §L2.

---

### `NymeaEnergy.SpotMarketConfigurationChanged`
Émis quand le provider actif, l'état enabled ou l'état available change.
```json
{
  "enabled": true,
  "available": true,
  "o:providerId": "5196b3cc-b2ee-46d6-b63a-7af2cf70ba67"
}
```

---

### `NymeaEnergy.SpotMarketStatusChanged`
Émis quand `enabled` ou `available` change (sans les détails du provider).
```json
{ "enabled": true, "available": true }
```

---

### `NymeaEnergy.SpotMarketScoreEntriesChanged`
Émis quand les cotations sont mises à jour par le provider.
```json
{ "spotMarketScoreEntries": [ ... ] }
```

---

## Interfaces nymea consommées

Le plugin détecte les appareils **par interface**, jamais par ThingClassId.

| Interface | États lus | Actions envoyées |
|---|---|---|
| `evcharger` | `chargingEnabled`, `maxChargingCurrent`, `pluggedIn`, `charging`, `currentPhaseA/B/C`, `currentPowerPhaseA/B/C` | `setChargingEnabled`, `setMaxChargingCurrent` |
| `electricvehicle` | `batteryLevel`, `maxChargingCurrent`, `capacity`, `minChargingCurrent`, `phaseCount` | — |
| `rootmeter` / `energymeter` | `currentPowerPhaseA/B/C`, `currentPhaseA/B/C` | — |
| `energystorage` | `currentPower`, `batteryLevel` | — |

**Déclencheur du cycle :** signal `PowerBalanceEntryAdded` de `nymea-experience-plugin-energy` (~1 min).
**Déclencheur overload :** signal `EnergyManager::powerBalanceChanged` (temps réel, découplé du cycle).

---

## EnergyManagerConfiguration

Paramètres de tuning chargés **une seule fois au démarrage** depuis un fichier JSON.  
Pas de setters — un changement nécessite un redémarrage du daemon nymea.

**Chemin (ordre de priorité) :**
1. `$NYMEA_ENERGY_MANAGER_CONFIG` (variable d'environnement)
2. `/var/lib/nymea/energy-manager-configuration.json`
3. Valeurs par défaut si aucun fichier trouvé

**Format JSON :**
```json
{
  "chargingEnabledLockDuration": 300,
  "chargingCurrentLockDuration": 10,
  "minimumScheduleDuration": 15,
  "spotMarketChargePredictableEnergyPercentage": 0.5
}
```

| Paramètre | Défaut | Unité | Rôle |
|---|---|---|---|
| `chargingEnabledLockDuration` | 300 | secondes | Anti-flapping : durée de verrouillage après changement ON/OFF |
| `chargingCurrentLockDuration` | 10 | secondes | Anti-flapping : durée de verrouillage après changement de courant |
| `minimumScheduleDuration` | 15 | minutes | Durée minimale d'un créneau spot market planifié |
| `spotMarketChargePredictableEnergyPercentage` | 0.5 | ratio [0–1] | Fraction de l'énergie spot considérée "prédictible" dans le planning |

---

## Flux interne — `SmartChargingManager`

### Entrées de données

```
EnergyManager::logs()::powerBalanceEntryAdded (SampleRate1Min)
    └─→ update(now)                    ← cycle principal ~1/min

EnergyManager::powerBalanceChanged             ← temps réel
    └─→ verifyOverloadProtection(now)  ← safety loop immédiate, découplée du cycle

RootMeter (wraps Thing interface=rootmeter/energymeter)
    ├── currentPower()            ← total W (négatif = surplus / export)
    ├── currentPowerPhaseA/B/C()  ← W par phase
    └── currentPhaseA/B/C()       ← A par phase

ThingManager → interface "energystorage"
    ├── batteryLevel  ← % (moyenne de tous les stockages)
    └── currentPower  ← W total (+ = charge, − = décharge)

EvCharger → interface "evcharger" + "electricvehicle"
    ├── currentPower(), maxChargingCurrent(), phaseCount()
    ├── meteredPhases()   ← phases réelles via currentPhaseA/B/C live
    └── car: batteryLevel, capacity, minChargingCurrent, phaseCount
```

### Pipeline `update()` — exécuté à chaque cycle

```
update(currentDateTime)
  │
  ├─ 1. updateManualSoCsWithoutMeter()
  │      Estime le SoC à partir de l'énergie intégrée si pas de compteur sur le VE.
  │
  ├─ 2. prepareInformation()
  │      - Filtre les EV chargers actifs (plugged, car assignée, mode ≠ Normal)
  │      - Calcule par charger : phases effectives, SoC, temps restant, phaseLimitPower
  │      - Reset m_chargingActions à OFF/minCurrent pour les 3 issuers
  │
  ├─ 3. verifyOverloadProtection()
  │      - Lit rootMeter->currentPowerPhaseA/B/C()
  │      - Si une phase dépasse phasePowerLimit × 230 W → throttle immédiat (issuer=OverloadProtection)
  │
  ├─ 4. verifyOverloadProtectionRecovery()
  │      - Ré-active les chargers throttlés si la marge est suffisante
  │
  ├─ 5. planSpotMarketCharging()
  │      - SpotMarketManager::scheduleChargingTime() → TimeFrames (créneaux bon marché)
  │      - Remplit m_chargingSchedules + m_chargingActions[SpotMarket] = ON
  │
  ├─ 6. planSurplusCharging()
  │      - currentLoad = rootMeter->currentPower()
  │          + correction batteries (fromBatteries)
  │          + puissance ajoutée dans ce cycle (addedPower)
  │      - allowanceInAmpere = −currentLoad / 230
  │      - Si allowance ≥ minCurrent × acquisitionTolerance → chargingActions[Surplus] = ON
  │
  └─ 7. adjustEvChargers()
         Applique la décision finale par priorité décroissante :
         1. TimeRequirement  → ON au max courant disponible (deadline imminente)
         2. SurplusCharging  → ON au courant surplus calculé
         3. SpotMarketCharging → ON au courant max dans le créneau
         4. EcoWithMinCurrent fallback → ON à 6 A (EcoMinChargingCurrent)
         5. Idle             → OFF
              │
              ├── executeChargingAction()
              │     └─→ Thing::executeAction(setChargingEnabled, setMaxChargingCurrent)
              │
              ├── emit chargingInfoChanged()      ← met à jour ChargingState (lu par JSON-RPC)
              └── emit chargingSchedulesChanged() ← planning rafraîchi (lu par JSON-RPC)
```

### Persistance des settings utilisateur

| Données | Fichier |
|---|---|
| `phasePowerLimit`, `acquisitionTolerance`, `batteryLevelConsideration` | `EnergySettings` (QSettings INI, `energy.conf`) |
| `ChargingInfo` par charger (mode, endDateTime, repeatDays, etc.) | même `EnergySettings`, groupe `ChargingInfos/` |
| `lockOnUnplug` | `/var/lib/nymea/energy.conf` (QSettings séparé) |
| SpotMarket `enabled`, `providerId` | `EnergySettings` (géré par `SpotMarketManager`) |

### Point d'injection pour `powersync-optimizer`

Le point naturel est **entre `prepareInformation()` et `adjustEvChargers()`**.  
L'optimizer reçoit les données de contexte (`SurplusData`) et retourne une `ChargingAction` qui remplace ou complète les actions calculées localement. Voir `PowerSyncClient::requestOptimization()` dans `etm/`.

---

## Notes d'intégration

- Les timestamps sont des `QDateTime` sérialisés ISO 8601 avec timezone.
- Les puissances sont en **Watts**, les courants en **Ampères**.
- `phasePowerLimit` est en **Ampères** (par phase), pas en Watts.
- `weighting` des `ScoreEntry` : 1.0 = créneau le moins cher, 0.0 = le plus cher.
- `SetChargingInfo` est partiel : seuls les champs présents sont appliqués, sauf `evChargerId` qui est toujours requis.
- `currentPower` du root meter est **négatif** quand il y a surplus solaire (export réseau).
- Le plugin fonctionne sans `powersync-optimizer` (mode Community, dégradé proprement).
