From b8e882616bfab91e672dfbd84c1480d51af5bb7c Mon Sep 17 00:00:00 2001 From: Patrick Schurig Date: Sat, 6 Jun 2026 09:07:55 +0200 Subject: [PATCH] docs: document internal data flow and EnergyManagerConfiguration in INTERFACE.md Co-Authored-By: Claude Sonnet 4.6 --- INTERFACE.md | 121 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 120 insertions(+), 1 deletion(-) diff --git a/INTERFACE.md b/INTERFACE.md index 80cad2c..e4810a6 100644 --- a/INTERFACE.md +++ b/INTERFACE.md @@ -495,11 +495,129 @@ 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` | — | +| `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/`. --- @@ -510,4 +628,5 @@ Le plugin détecte les appareils **par interface**, jamais par ThingClassId. - `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).