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