CLAUDE.md portait un exemple de SetChargingInfo qui invitait à l'écriture partielle. Il porte maintenant la règle inverse, avec la preuve machine, et les deux pièges de lecture de GetLoadTelemetry : ne pas sommer allocatedW sans filtrer sur funding, et ne pas lire l'allocation d'une borne comme son état. Retour au moteur dans BRIEF_plugin_depuis_maquettes.md. Un point actionnable chez eux : la description trompeuse vit dans nymeaenergyjsonhandler.cpp:114 et part dans Introspect chez tout client. Elle ne se contente pas de ne pas prévenir, elle autorise explicitement l'erreur — le prochain client la croira. Une ligne à corriger, ou une écriture à rendre réellement partielle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
13 KiB
Agent App — etm_powersync_app
Lire aussi le
CLAUDE.mddu 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.SetChargingModen'est pas une méthode de nymea — il n'y en a que cinq dansEnergy.*, et celle-là n'en fait pas partie : l'appel échouerait sur « No such method ». Et le namespaceEnergyPlugin.*n'existe pas non plus. Corrigé le 2026-08-25 contreJSONRPC.Introspectsur.75(plugin 1.15.2+etm15).
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
}
});
🔴
SetChargingInfon'est PAS une écriture partielle, malgré ce que dit son propreIntrospect(« Only given properties will be set, others will be untouched »).ChargingInfoest reconstruit intégralement à chaque appel : un champ absent du JSON retombe sur le défaut C++, et celui detargetPercentagevaut 80. Muet — réponseEnergyErrorNoError, et laChargingInfoChangedqui suit porte 80 comme si l'utilisateur l'avait choisi. Reproduit sur.75le 2026-08-27 (+etm22) : le champ valait0, un appel portant le seulchargingModel'a laissé à80.Donc : lire (
GetChargingInfos), patcher, réécrire l'objet complet — sanschargingState, qui estr:et ferait rejeter l'appel. Et ne jamais affichertargetPercentagecomme une cible choisie par l'utilisateur tant que l'écran ne l'a pas lui-même écrite : 80 peut être l'empreinte d'une écriture qui ne parlait pas de cible.
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
⚠️
minCurrentn'existe nulle part. Le modeChargingModeEcoWithMinCurrentest bien dans l'énumération, maisChargingInfone 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à.SetChargingModen'existe pas — ni ici, ni ailleurs.
NymeaEnergy.* — powersync-energy-plugin-etm · 20 méthodes / 14 notifications
⚠️ Le namespace est
NymeaEnergy, pasEnergyPlugin. Ce dernier n'existe pas. Source d'autorité :energyplugin/nymeaenergyjsonhandler.cppetJSONRPC.Introspect. Ce n'est pas dansOPTIMIZER_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 |
| — | Depuis +etm22, loads[] contient aussi les bornes (mechanism.kind = "evcharger") et chaque entrée porte o:funding ("surplus" / "grid") |
— |
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 |
ZoneInfoest 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 sixSet*, 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
// 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
primaryGreen solarYellow gridGray homeBlue
batteryGreen boostRed pvGreen minPvBlue accentTeal
Télémétrie d'arbitrage — deux pièges de lecture
1. funding — ne jamais sommer allocatedW sans filtrer. Depuis +etm22 une borne
peut figurer dans loads[] au titre du surplus ou du réseau (échéance, tarif
dynamique). L'identité vérifiable est : somme des allocatedW financés au surplus ==
budget.allocatedW ; une borne financée au réseau vit dans budget.evReservedW. Sommer
sans filtrer donne un écart que rien ne permet d'interpréter. Omis en mode dégradé — sans
plan, pas de financement. Cf. LoadTelemetry.surplusAllocatedW.
2. ⚠️ Pour une BORNE, allocatedW et son motif sont une INTENTION, pas un ordre.
adjustEvChargers() re-décide le surplus après le waterfall et passe outre le refus de
l'arbitre : relevé sur .75, l'arbitre publie « 0 W / BELOW_MIN_POWER » et le proxy
commande ON à 6 A dans le même cycle. L'état réel se lit dans
mechanism.chargingEnabled / mechanism.currentA, qui viennent du Thing. Ne pas écrire
« la borne ne charge pas parce que le budget est sous le plancher » à partir du seul
motif. La réserve ne vaut pas pour l'ECS et la PAC, où l'allocation publiée est
la commande. Elle tombe avec le lot plugin 3g-2 ; côté app, elle est portée par
LoadTelemetryEntry.allocationIsCommand.
Motif BATTERY_RESERVE — params socPercent, reservePercent, withheldW, tous
entiers déjà en pourcentage (ne pas confondre reservePercent = 40 avec
batteryLevelConsideration = 0,4). Le défaut d'usine de ce réglage est passé de 0,9 à
0,2, mais une box déjà déployée garde la valeur qu'elle a persistée.
Règles de modification
- Tout nouvel écran → valider maquette avec Patrick avant de coder
- Tout nouvel appel RPC → vérifier dans
INTERFACE.mdque la méthode existe - Piloter un EV =
NymeaEnergy.SetChargingInfo(Energy.SetChargingModen'existe pas) - Vérifier ce fichier contre
JSONRPC.Introspectavant 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