Trois maquettes, neuf écrans, après le passage sur appareil qui a montré que /energy/setup décrit autre chose que l'installation. - charges_pilotables_mockup.html : la liste à deux niveaux, avec le bandeau d'arbitrage hérité de /energy/loads (qui disparaît) et la télémétrie sur la carte de chaque charge. - configurer_mecanismes_mockup.html : les six « Configurer » — routeur de relais, modulable, SG-Ready, borne, zones HVAC, sélecteur d'appareils. - raccordement_reseau_mockup.html : compteur réseau et protection de surcharge. Décisions de conception que les images ne portent pas seules : les paliers n'existent que chez le routeur de relais et y sont DÉDUITS des combinaisons ; le mode fixed d'etmvariableload (legacy) n'est plus proposé mais reste lisible, parce qu'ouvrir un écran ne doit pas convertir une config déployée ; SG1/SG2 sont une convention d'app retrouvée par l'encodage standard, la box ne stocke qu'une liste de relais par état ; le compteur par charge est dessiné inactif avec sa raison ; la borne règle un mode par DÉFAUT, pas une commande. CLAUDE.md corrigé contre JSONRPC.Introspect (plugin 1.15.2+etm15) : - Energy.SetChargingMode n'existe pas — le « bug critique » décrivait un appel impossible. Energy.* n'a que cinq méthodes. - Le namespace EnergyPlugin.* n'existe pas : c'est NymeaEnergy.*, 20 méthodes et 14 notifications, toutes listées. - ChargingInfo : chargingMode / targetPercentage / endDateTime (Uint), et non mode / targetSoc / endTime. minCurrent n'existe nulle part. - phasePowerLimit est en AMPÈRES par phase, et 0 coupe toute la recharge intelligente. - AirConditioning expose 8 méthodes, pas 3. Toutes les méthodes citées ont été revérifiées une par une : aucune introuvable. Règle ajoutée : contrôler ce fichier contre Introspect avant chaque lot — trois fois aujourd'hui il a envoyé un agent dans le vide. Et docs/BRIEF_plugin_depuis_maquettes.md : les six points que les maquettes remontent vers le moteur, dont un seul blocage dur (pas de champ de courant minimum pour le mode Min+PV). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
245 lines
11 KiB
Markdown
245 lines
11 KiB
Markdown
# Agent App — `etm_powersync_app`
|
|
> Lire aussi le `CLAUDE.md` du dossier parent avant de commencer.
|
|
|
|
---
|
|
|
|
# ssh banc .75
|
|
Accès banc `.75` : ssh etm@192.168.1.75 (clé déjà installée).
|
|
Ni root ni nymea n'existent. Journal : journalctl -u nymead.
|
|
---
|
|
|
|
## Mon rôle
|
|
Interface utilisateur Flutter du HEMS ETM-PowerSync.
|
|
Je communique **exclusivement via JSON-RPC nymea port 4444**.
|
|
Le gating des features par tier est géré visuellement ici,
|
|
mais la source de vérité du tier est dans `powersync-optimizer`.
|
|
|
|
---
|
|
|
|
## Stack technique
|
|
| Élément | Valeur |
|
|
|---|---|
|
|
| Flutter SDK | ^3.11.0 |
|
|
| State management | `provider ^6.1.2` — `NymeaService` (ChangeNotifier) |
|
|
| Navigation | `go_router ^14.6.3` — ShellRoute + 25+ routes |
|
|
| Transport | WebSocket port 4444 / TCP brut port 2222 |
|
|
| Graphiques | `fl_chart ^0.70.2` |
|
|
| Persistance | `shared_preferences ^2.3.2` |
|
|
| Sécurité installateur | `crypto ^3.0.6` (PIN SHA-256) |
|
|
|
|
---
|
|
|
|
## 🔴 Recharge EV — l'appel réel, vérifié contre la box
|
|
|
|
> **Ce bloc décrivait un bug qui n'existe plus, avec des champs qui n'ont jamais existé.**
|
|
> `Energy.SetChargingMode` **n'est pas une méthode de nymea** — il n'y en a que cinq dans
|
|
> `Energy.*`, et celle-là n'en fait pas partie : l'appel échouerait sur « No such method ».
|
|
> Et le namespace `EnergyPlugin.*` n'existe pas non plus. Corrigé le 2026-08-25 contre
|
|
> `JSONRPC.Introspect` sur `.75` (plugin **1.15.2+etm15**).
|
|
|
|
```dart
|
|
nymeaService.call('NymeaEnergy.SetChargingInfo', {
|
|
'chargingInfo': {
|
|
'evChargerId': chargerId, // Uuid — requis
|
|
'chargingMode': 'ChargingModeEco', // requis, énumération ci-dessous
|
|
'targetPercentage': 80, // o: Uint — charge cible
|
|
'endDateTime': 1786250400, // o: Uint — horodatage, PAS une chaîne
|
|
'repeatDays': [1, 2, 3, 4, 5], // o: Int[]
|
|
'assignedCarId': carId, // o: Uuid
|
|
'spotMarketChargingEnabled': false, // o: Bool
|
|
'dailySpotMarketPercentage': 30, // o: Uint
|
|
}
|
|
});
|
|
```
|
|
|
|
`chargingState` est en **lecture seule** (`r:`) dans la charge utile :
|
|
`ChargingStateIdle` · `ChargingStateSurplusCharging` · `ChargingStateSpotMarketCharging` ·
|
|
`ChargingStateTimeRequirement`.
|
|
|
|
### Énumération `ChargingMode` — les vrais noms
|
|
|
|
`ChargingModeNormal` · `ChargingModeEco` · `ChargingModeEcoWithTargetTime` ·
|
|
`ChargingModeEcoWithMinCurrent` · `ChargingModeEcoMinWithTargetTime`
|
|
|
|
### Modes UI EV — 3 boutons + option échéance
|
|
```
|
|
[PV] [Min+PV] [Boost]
|
|
↓
|
|
Option échéance ? → charge cible (targetPercentage) + heure (endDateTime)
|
|
|
|
PV → ChargingModeEco
|
|
Min+PV → ChargingModeEcoWithMinCurrent
|
|
Boost → ChargingModeNormal
|
|
PV + échéance → ChargingModeEcoWithTargetTime + targetPercentage + endDateTime
|
|
Min+PV + échéance → ChargingModeEcoMinWithTargetTime + targetPercentage + endDateTime
|
|
```
|
|
|
|
> ⚠️ **`minCurrent` n'existe nulle part.** Le mode `ChargingModeEcoWithMinCurrent` est bien
|
|
> dans l'énumération, mais `ChargingInfo` ne porte **aucun champ** de courant minimum. Soit
|
|
> il est fixé ailleurs (paramètre du Thing borne), soit il manque côté plugin. **Ne pas
|
|
> proposer de réglage de courant minimum tant que ce n'est pas tranché** — un champ qui ne
|
|
> part nulle part est pire que pas de champ.
|
|
|
|
---
|
|
|
|
## APIs JSON-RPC consommées
|
|
|
|
### `Energy.*` — nymea-experience-plugin-energy · **5 méthodes**
|
|
| Méthode | Usage | Tier |
|
|
|---|---|---|
|
|
| `GetPowerBalance` | Dashboard temps réel | Community |
|
|
| `GetPowerBalanceLogs(sampleRate, from, to)` | Historique — envoyer les bons `from/to` pour 90j | Community |
|
|
| `GetThingPowerLogs(sampleRate, thingIds[], from, to)` | Historique par device | Community |
|
|
| `GetRootMeter` | Lire le compteur racine configuré | Community |
|
|
| `SetRootMeter(rootMeterThingId)` | Config installateur | Community |
|
|
|
|
> `Energy.*` s'arrête là. **`SetChargingMode` n'existe pas** — ni ici, ni ailleurs.
|
|
|
|
### `NymeaEnergy.*` — powersync-energy-plugin-etm · **20 méthodes / 14 notifications**
|
|
|
|
> ⚠️ Le namespace est **`NymeaEnergy`**, pas `EnergyPlugin`. Ce dernier n'existe pas.
|
|
> Source d'autorité : `energyplugin/nymeaenergyjsonhandler.cpp` et `JSONRPC.Introspect`.
|
|
> **Ce n'est pas dans `OPTIMIZER_PROTOCOL.md`** — ce document décrit le protocole
|
|
> plugin ↔ optimiseur (couche A ↔ couche B), pas la frontière JSON-RPC de l'app.
|
|
|
|
**Charges pilotées**
|
|
| Méthode | Usage | Tier |
|
|
|---|---|---|
|
|
| `GetLoadConfig` | Lire les charges déclarées | Community |
|
|
| `SetLoadConfig(loadConfigs[])` | Écrire — **remplace TOUT l'ensemble**, refus en bloc | Community |
|
|
| `GetLoadTelemetry` | État runtime de l'arbitrage (budget, allocation, verrous, motifs) | Community |
|
|
| `ClearLoadFault(loadId)` | Lever le verrou de défaut — **l'ack ne prouve rien** | Community |
|
|
|
|
**Recharge EV**
|
|
| Méthode | Usage | Tier |
|
|
|---|---|---|
|
|
| `GetChargingInfos(evChargerId)` | Lire config EV | Community |
|
|
| `SetChargingInfo(chargingInfo)` | Écrire config EV — voir le bloc plus haut | Community |
|
|
| `GetChargingSchedules(evChargerId)` | Planning EV | Community |
|
|
| `GetLockOnUnplug` / `SetLockOnUnplug(Bool)` | Verrouillage au débranchement | Community |
|
|
|
|
**Réglages globaux d'installation**
|
|
| Méthode | Usage | Tier |
|
|
|---|---|---|
|
|
| `GetPhasePowerLimit` / `SetPhasePowerLimit(Uint)` | **AMPÈRES par phase**, pas des watts. `0` = désactivé, et coupe toute la recharge intelligente | Community |
|
|
| `GetAcquisitionTolerance` / `SetAcquisitionTolerance(Double)` | Seuil de surplus au démarrage — **unité non documentée par la box** | Community |
|
|
| `GetBatteryLevelConsideration` / `SetBatteryLevelConsideration(Double)` | Part du stockage prise en compte | Community |
|
|
|
|
**Tarifs dynamiques**
|
|
| Méthode | Usage | Tier |
|
|
|---|---|---|
|
|
| `GetAvailableSpotMarketProviders` | Liste providers | Predict AI |
|
|
| `GetSpotMarketConfiguration` / `SetSpotMarketConfiguration(enabled, providerId)` | Config tarif dynamique | Predict AI |
|
|
| `GetSpotMarketScoreEntries(date)` | Cotations aWATTar | Predict AI |
|
|
| `GetEnergyRatios` | Autoconsommation / autonomie | Community |
|
|
|
|
**Notifications** — `LoadConfigChanged` · `LoadTelemetryChanged` · `ChargingInfoAdded/Changed/Removed` ·
|
|
`ChargingSchedulesChanged` · `EnergyRatiosChanged` · `PhasePowerLimitChanged` ·
|
|
`AcquisitionToleranceChanged` · `BatteryLevelConsiderationChanged` · `LockOnUnplugChanged` ·
|
|
`SpotMarketConfigurationChanged` · `SpotMarketStatusChanged` · `SpotMarketScoreEntriesChanged`
|
|
|
|
### `AirConditioning.*` — nymea-experience-plugin-airconditioning · **8 méthodes**
|
|
| Méthode | Usage | Tier |
|
|
|---|---|---|
|
|
| `GetZones` | Afficher zones PAC/thermostat | Auto |
|
|
| `AddZone` / `RemoveZone` | CRUD de zones | Auto |
|
|
| `SetZoneName` | Renommer | Auto |
|
|
| `SetZoneThings(zoneId, thermostats[], valves[], indoorSensors[], outdoorSensors[], windowSensors[], notifications[])` | Rattacher les appareils | Auto |
|
|
| `SetZoneSetpointOverride(zoneId, mode, setpointOverride, minutes)` | Dérogation manuelle | Auto |
|
|
| `SetZoneStandbySetpoint` | Consigne de réduit | Auto |
|
|
| `SetZoneWeekSchedule` | Planning 7 jours | Auto |
|
|
|
|
> `ZoneInfo` est **presque entièrement en lecture** (`r:`) : température, humidité, PM2.5, COV,
|
|
> consigne courante, dérogation et sa fin, listes d'appareils. On écrit par les six `Set*`,
|
|
> jamais par un objet complet.
|
|
|
|
### `Rules.*` — nymea core
|
|
| Méthode | Usage | Tier |
|
|
|---|---|---|
|
|
| `GetRules` | Lecture automatisations | Community |
|
|
| `AddRule` | Créer règle HP/HC, surplus → relais | Community |
|
|
| `RemoveRule / EditRule` | Gérer règles | Community |
|
|
|
|
---
|
|
|
|
## État des écrans
|
|
|
|
| Écran | État | Action requise |
|
|
|---|---|---|
|
|
| Dashboard (Sankey + EV card) | ✅ | Corriger API EV + brancher notifications |
|
|
| EnergyScreen (4 onglets) | ✅ | Ajouter sélecteur plage 90j |
|
|
| ThingsScreen + ThingDetail | ✅ | — |
|
|
| FavoritesScreen | ✅ | Persister dans SharedPreferences |
|
|
| InstallerMode (PIN SHA-256) | ✅ | — |
|
|
| RoleConfigFlow wizard | ⚠️ Stub | Brancher sur vrais RPC |
|
|
| TariffScreen | ⚠️ Stub | Brancher `SetSpotMarketConfiguration` |
|
|
| SchedulerScreen | ⚠️ Stub | Brancher `GetChargingSchedules` |
|
|
| TimelineScreen | ⚠️ Stub | Brancher scheduler réel |
|
|
| AirConditioning zones | ❌ Absent | Créer (Auto) |
|
|
| Rules UI (automatisations) | ❌ Absent | Créer (Community) |
|
|
| DeveloperScreen | ❌ Vide | Créer |
|
|
| AboutScreen | ❌ Vide | Créer |
|
|
|
|
---
|
|
|
|
## Persistance — tout doit survivre au redémarrage
|
|
|
|
| Donnée | État | Action |
|
|
|---|---|---|
|
|
| Adresse serveur, PIN, préférences UI | ✅ SharedPreferences | — |
|
|
| `RoleAssignments` | ❌ Mémoire | Persister SharedPreferences |
|
|
| `FavoriteWidgets` | ❌ Mémoire | Persister SharedPreferences |
|
|
| `TariffConfig` / `HcHpConfig` | ❌ Mémoire | Persister SharedPreferences |
|
|
| `SchedulerConfig` | ❌ Mémoire | Persister SharedPreferences |
|
|
|
|
---
|
|
|
|
## Feature gating par tier
|
|
|
|
```dart
|
|
// TierProvider — à créer, lit le tier depuis le plugin via RPC
|
|
// En attendant : valeur par défaut 'community'
|
|
|
|
if (tierProvider.tier >= Tier.auto) {
|
|
// afficher feature Auto
|
|
}
|
|
|
|
// Utiliser pro_lock_badge.dart (déjà présent) pour verrouiller visuellement
|
|
```
|
|
|
|
### Features par tier
|
|
| Feature | Community | Auto | Predict AI |
|
|
|---|---|---|---|
|
|
| Dashboard temps réel | ✅ | ✅ | ✅ |
|
|
| Config EV (SetChargingInfo) | ✅ | ✅ | ✅ |
|
|
| Tarif HP/HC statique | ✅ | ✅ | ✅ |
|
|
| UI automatisations (Rules.*) | ✅ | ✅ | ✅ |
|
|
| Historique 90 jours | 🔒 | ✅ | ✅ |
|
|
| Wizard onboarding | 🔒 | ✅ | ✅ |
|
|
| Zones PAC/ECS (AirConditioning.*) | 🔒 | ✅ | ✅ |
|
|
| Prévision solaire Open-Meteo | 🔒 | ✅ | ✅ |
|
|
| Notifications d'anomalies | 🔒 | ✅ | ✅ |
|
|
| Accès distant sécurisé | 🔒 | ✅ | ✅ |
|
|
| Tarifs dynamiques aWATTar | 🔒 | 🔒 | ✅ |
|
|
| Accès fonctionnalités beta | 🔒 | 🔒 | ✅ |
|
|
|
|
---
|
|
|
|
## Thème — utiliser `app_theme.dart` systématiquement
|
|
```dart
|
|
primaryGreen solarYellow gridGray homeBlue
|
|
batteryGreen boostRed pvGreen minPvBlue accentTeal
|
|
```
|
|
|
|
---
|
|
|
|
## Règles de modification
|
|
- Tout nouvel écran → valider maquette avec Patrick avant de coder
|
|
- Tout nouvel appel RPC → vérifier dans `INTERFACE.md` que la méthode existe
|
|
- Piloter un EV = `NymeaEnergy.SetChargingInfo` (`Energy.SetChargingMode` n'existe pas)
|
|
- **Vérifier ce fichier contre `JSONRPC.Introspect` avant chaque lot.** Trois fois le
|
|
2026-08-25, la doc locale était en retard sur la machine et a envoyé un agent dans le vide.
|
|
La sonde tient en une ligne : `dart tools/rpc/probe.dart 192.168.1.75 JSONRPC.Introspect`
|
|
- Toujours tester la persistance : killer l'app et vérifier que les données survivent
|
|
- Flavors à configurer : `com.etm-powersync.community` / `.auto` / `.predictai`
|