From 7d71bea527cc8669562dcf79f4e9e102f8ccad6c Mon Sep 17 00:00:00 2001 From: Patrick Schurig ETM-Schurig Date: Fri, 29 May 2026 22:10:50 +0200 Subject: [PATCH] =?UTF-8?q?chore:=20mise=20=C3=A0=20jour=20CLAUDE.md=20?= =?UTF-8?q?=E2=80=94=20brief=20refonte=20UI=20agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remplace le CLAUDE.md générique par le brief complet de la session : APIs JSON-RPC consommées, état des écrans, persistance, feature gating, thème EtmTokens, règles de modification. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 284 +++++++++++++++++++----------------------------------- 1 file changed, 99 insertions(+), 185 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 33bb194..d8be544 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,106 +1,30 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Contexte général - -Dossier parent de tous les projets : `/home/etm/Projects/` - -- **`etm_powersync_app/`** (ce projet) — application Flutter HEMS, interface utilisateur -- **`etm-nymea/`** — serveur nymea et plugins associés (C++/Qt), le backend - -Ce projet est le **client Flutter** qui communique avec le serveur nymea via JSON-RPC 2.0. -Il n'a aucun accès direct aux plugins — tout passe par les APIs JSON-RPC. - -Lire aussi le `CLAUDE.md` du dossier parent (`/home/etm/Projects/CLAUDE.md`) pour le contexte global. +# Agent App — `etm_powersync_app` +> Lire aussi le `CLAUDE.md` du dossier parent avant de commencer. --- -## Commandes +## 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`. -```bash -flutter pub get # installer/mettre à jour les dépendances -flutter run # lancer sur device/émulateur connecté -flutter build apk # build Android release -flutter analyze # analyse statique Dart -flutter test # lancer les tests -``` +--- ## Stack technique - | Élément | Valeur | |---|---| | Flutter SDK | ^3.11.0 | -| State management | `provider ^6.1.2` — `NymeaService` (ChangeNotifier unique) | +| 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 locale | `shared_preferences ^2.3.2` | -| Sécurité installateur | `crypto ^3.0.6` (PIN SHA-256, lock 30s, auto-lock 10 min) | +| Persistance | `shared_preferences ^2.3.2` | +| Sécurité installateur | `crypto ^3.0.6` (PIN SHA-256) | --- -## Architecture - -### State management — un seul Provider - -`NymeaService` (`lib/services/nymea_service.dart`) est le seul `ChangeNotifier`. Il est fourni à la racine dans `main.dart` via `ChangeNotifierProvider`. Tous les écrans le consomment avec `context.watch()` ou `context.read()`. Il n'y a aucune autre couche de state management. - -### Navigation - -`MainShell` dans `main.dart` utilise un `IndexedStack` de 5 écrans (l'état est préservé entre les onglets) : - -| Index | Écran | Fichier | -|-------|-------|---------| -| 0 | Dashboard | `lib/screens/dashboard_screen.dart` | -| 1 | Énergie | `lib/screens/energy_screen.dart` | -| 2 | Things | `lib/screens/things_screen.dart` | -| 3 | A/C | `lib/screens/ac_screen.dart` | -| 4 | Favoris | `lib/screens/favorites_screen.dart` | - -### Communication avec nymea - -`NymeaService.connect()` supporte deux transports : - -- **TCP brut** port 2222 (défaut) : nymea parle **en premier** (JSON délimité par `\n`). La fragmentation est gérée via `StringBuffer`. -- **WebSocket** port 4444 : c'est le **client qui parle en premier** en envoyant `JSONRPC.Hello`. - -Les requêtes utilisent des IDs auto-incrémentés stockés dans `_pendingRequests: Map`. Timeout global : 15 secondes. Les notifications push nymea (pas de champ `id`, ont un champ `notification`) sont dispatchées dans `_handleNotification()`. - -**Convention nymea énergie importante** : `currentPowerProduction` est **négatif** (producteur = négatif). Le service convertit en positif dans `_parsePowerBalance()`. Puissances en **W**, énergies journalières en **Wh**, totaux API nymea en **kWh** (× 1000 dans le service). Timestamps : **secondes** dans toutes les APIs energy. - -### Mode simulation - -`NymeaService.startSimulation()` déconnecte toute connexion live, active `_isSimulation = true`, génère des things fictifs et une production PV simulée en sinus (timer 2 s). Le dashboard démarre automatiquement la simulation au premier affichage si non connecté. Toutes les méthodes du service font un garde `if (_isSimulation) return …` pour éviter les vrais appels RPC. - -### Modèles de données - -- **`EnergyData`** (`lib/models/energy_data.dart`) — objet immutable avec `copyWith`, regroupe toutes les métriques énergie temps-réel et journalières + état de la borne EV. -- **`NymeaThing` / `NymeaThingClass` / `NymeaStateType` / `NymeaActionType`** (`lib/models/nymea_models.dart`) — miroir de l'API Integrations nymea. `NymeaThingClass.primaryStateType` détermine l'état affiché sur les cards. -- **`ThingCategory` + `interfaceToCategoryMap`** (`lib/models/thing_category.dart`) — mappe les interfaces nymea (ex. `"evcharger"`, `"solarinverter"`) vers des catégories d'affichage utilisées par `ThingsScreen`. -- **`FavoriteWidget`** (`lib/models/nymea_models.dart`) — descripteurs de widgets stockés en mémoire dans `NymeaService._favoriteWidgets`. - -### Thème - -`AppTheme` (`lib/theme/app_theme.dart`) expose des constantes `Color` nommées. Toujours utiliser ces constantes plutôt que des valeurs hex brutes : - -```dart -primaryGreen // principal -solarYellow // production PV -gridGray // réseau -homeBlue // consommation maison -batteryGreen // batterie -boostRed // mode boost -pvGreen // surplus PV -minPvBlue // mode minPV -accentTeal // accent -``` - ---- - -## 🔴 Bug critique — priorité absolue - -`EVChargingCard` appelle `Energy.SetChargingMode` — cette méthode appartient à l'experience plugin et concerne le comptage du rootmeter, **pas le pilotage EV**. Le SmartChargingManager n'est jamais déclenché depuis l'app dans l'état actuel. +## 🔴 Bug critique — à corriger en priorité absolue ```dart // ❌ FAUX — ne déclenche pas le SmartChargingManager @@ -110,64 +34,65 @@ nymeaService.call('Energy.SetChargingMode', {'mode': 'pv'}); nymeaService.call('EnergyPlugin.SetChargingInfo', { 'chargingInfo': { 'evChargerId': chargerId, - 'mode': 'Eco', // Normal | Eco | EcoWithTargetTime - 'targetSoc': 80, - 'endTime': null, + 'mode': 'Eco', // Eco | EcoWithMinCurrent | Normal + // EcoWithTargetTime | EcoMinWithTargetTime + 'minCurrent': 6, // requis pour EcoWithMinCurrent/* + 'targetSoc': 80, // requis pour *WithTargetTime + 'endTime': null, // requis pour *WithTargetTime } }); ``` +### Modes UI EV — 3 boutons + option deadline +``` +[PV] [Min+PV] [Boost] + ↓ + Option deadline ? → afficher champs SOC cible + heure d'arrivée + +PV → mode: Eco +Min+PV → mode: EcoWithMinCurrent, minCurrent: 6 +Boost → mode: Normal +PV + deadline → mode: EcoWithTargetTime, targetSoc, endTime +Min+PV+deadline → mode: EcoMinWithTargetTime, minCurrent, targetSoc, endTime +``` + --- ## APIs JSON-RPC consommées -### `Energy.*` (nymea-experience-plugin-energy) +### `Energy.*` — nymea-experience-plugin-energy +| 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 | +| `SetRootMeter(rootMeterThingId)` | Config installateur | Community | -| Méthode | Usage | -|---|---| -| `GetPowerBalance` | Dashboard temps réel | -| `GetPowerBalanceLogs(sampleRate, from, to)` | Historique — envoyer les bons `from/to` pour les 90j | -| `GetThingPowerLogs(sampleRate, thingIds[], from, to)` | Historique par device | -| `SetRootMeter(rootMeterThingId)` | Config installateur | +### `EnergyPlugin.*` — powersync-energy-plugin-etm +| Méthode | Usage | Tier | +|---|---|---| +| `GetChargingInfos(evChargerId)` | Lire config EV | Community | +| `SetChargingInfo(chargingInfo)` | **⚠️ CORRECTION CRITIQUE** | Community | +| `GetChargingSchedules(evChargerId)` | Planning EV | Community | +| `GetAvailableSpotMarketProviders()` | Liste providers | Community | +| `SetSpotMarketConfiguration(enabled, providerId)` | Config tarif dynamique | Predict AI | +| `GetSpotMarketScoreEntries(date)` | Cotations aWATTar | Predict AI | +| `SetPhasePowerLimit(Uint)` | Config installateur | Community | +| `SetAcquisitionTolerance(Double)` | Seuil surplus | Community | -**Notifications push :** `PowerBalanceChanged`, `PowerBalanceLogEntryAdded`, `ThingPowerLogEntryAdded`, `RootMeterChanged` +### `AirConditioning.*` — nymea-experience-plugin-airconditioning +| Méthode | Usage | Tier | +|---|---|---| +| `GetZones` | Afficher zones PAC/thermostat | Auto | +| `SetZoneSetpointOverride` | Pilotage manuel | Auto | +| `SetZoneWeekSchedule` | Planning 7 jours | Auto | -### `EnergyPlugin.*` (powersync-energy-plugin-etm) - -| Méthode | Usage | -|---|---| -| `GetChargingInfos(o:evChargerId)` | Lire config recharge EV | -| `SetChargingInfo(chargingInfo)` | **Piloter le smart charging EV** (Eco / EcoWithTargetTime / Normal) | -| `GetChargingSchedules(o:evChargerId)` | Afficher le planning EV calculé | -| `GetAvailableSpotMarketProviders()` | Liste des fournisseurs tarifs disponibles | -| `SetSpotMarketConfiguration(enabled, providerId)` | Activer/choisir fournisseur spot market | -| `GetSpotMarketScoreEntries(o:date)` | Cotations horaires aWATTar | -| `SetPhasePowerLimit(Uint)` | Config installateur — protection surcharge (A/phase) | -| `SetAcquisitionTolerance(Double)` | Config seuil surplus déclenchant la recharge | -| `SetBatteryLevelConsideration(Double)` | Facteur batterie dans le calcul surplus | - -**Notifications push :** `ChargingInfoAdded/Removed/Changed`, `ChargingSchedulesChanged`, `SpotMarketConfigurationChanged`, `SpotMarketScoreEntriesChanged`, `PhasePowerLimitChanged` - -### `AirConditioning.*` (nymea-experience-plugin-airconditioning) - -| Méthode | Usage | -|---|---| -| `GetZones` | Afficher les zones PAC/thermostat | -| `AddZone` / `RemoveZone` | Gestion des zones | -| `SetZoneName` / `SetZoneThings` | Configuration zone | -| `SetZoneStandbySetpoint` | Consigne hors-horaire | -| `SetZoneSetpointOverride` | Pilotage manuel zone | -| `SetZoneWeekSchedule` | Planning 7 jours | - -**Notifications push :** `ZoneAdded`, `ZoneRemoved`, `ZoneChanged` - -### `Integrations.*` / `Rules.*` / `Logging.*` - -- `GetThings`, `GetThingClasses`, `ExecuteAction`, `SetStateValue`, `SetThingSettings`, `GetStateValue` — ✅ déjà implémentés -- `DiscoverThings`, `AddThing`, `RemoveThing`, `EditThing` — ✅ déjà implémentés -- `GetRules` — ✅ lecture seule déjà implémentée -- `AddRule`, `RemoveRule`, `EditRule` — ❌ à implémenter (UI automatisations Community) -- `Logging.GetLogEntries` — ✅ implémenté (historique SOC batterie, température) +### `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 | --- @@ -175,87 +100,76 @@ nymeaService.call('EnergyPlugin.SetChargingInfo', { | Écran | État | Action requise | |---|---|---| -| Dashboard (Sankey + gains + EV card) | ✅ | Icône notifications à brancher | -| EnergyScreen (4 onglets + charts) | ✅ | Ajouter sélecteur plage 90j | -| ThingsScreen + ThingDetailScreen | ✅ | — | -| FavoritesScreen | ✅ | **Persister dans SharedPreferences** | +| 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 Step 3 sur les vrais RPC | -| TariffScreen | ⚠️ Stub | Brancher sur `EnergyPlugin.SetSpotMarketConfiguration` | -| SchedulerScreen | ⚠️ Stub | `setStrategy()` / `forceRecalc()` = fonctions vides | -| TimelineScreen | ⚠️ Stub | Données simulées → brancher scheduler réel | -| AirConditioning zones | ❌ Absent | À créer (tier Auto) | -| DeveloperScreen | ❌ Vide | À créer | -| AboutScreen | ❌ Vide | À créer | +| 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 +## Persistance — tout doit survivre au redémarrage -| Donnée | Actuellement | À faire | +| Donnée | État | Action | |---|---|---| | Adresse serveur, PIN, préférences UI | ✅ SharedPreferences | — | -| `RoleAssignments` (configuration EMS) | ❌ En mémoire | Persister SharedPreferences | -| `FavoriteWidgets` | ❌ En mémoire | Persister SharedPreferences | -| `TariffConfig` / `HcHpConfig` | ❌ En mémoire | Persister SharedPreferences | -| `SchedulerConfig` | ❌ En mémoire | Persister 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 -Le tier actif est lu depuis `/etc/powersync/tier.conf` via un futur RPC. -En attendant, utiliser `TierProvider` (classe à créer) avec valeur par défaut `community`. - -Utiliser `pro_lock_badge.dart` (déjà présent) pour verrouiller visuellement les features non disponibles. **Ne jamais hardcoder le tier** — toujours passer par `TierProvider` : - ```dart -// Toujours via TierProvider — jamais de valeur hardcodée +// 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 | ✅ | ✅ | ✅ | -| Historique journée en cours | ✅ | ✅ | ✅ | -| Config EV (`EnergyPlugin.SetChargingInfo`) | ✅ | ✅ | ✅ | +| Config EV (SetChargingInfo) | ✅ | ✅ | ✅ | | Tarif HP/HC statique | ✅ | ✅ | ✅ | -| UI automatisations (`Rules.*`) | ✅ | ✅ | ✅ | +| UI automatisations (Rules.*) | ✅ | ✅ | ✅ | | Historique 90 jours | 🔒 | ✅ | ✅ | -| Wizard onboarding guidé | 🔒 | ✅ | ✅ | -| Zones PAC/ECS (`AirConditioning.*`) | 🔒 | ✅ | ✅ | +| Wizard onboarding | 🔒 | ✅ | ✅ | +| Zones PAC/ECS (AirConditioning.*) | 🔒 | ✅ | ✅ | | Prévision solaire Open-Meteo | 🔒 | ✅ | ✅ | | Notifications d'anomalies | 🔒 | ✅ | ✅ | | Accès distant sécurisé | 🔒 | ✅ | ✅ | -| Tarifs dynamiques aWATTar / spot market | 🔒 | 🔒 | ✅ | -| ENTSO-E / Tibber | 🔒 | 🔒 | ✅ | +| Tarifs dynamiques aWATTar | 🔒 | 🔒 | ✅ | | Accès fonctionnalités beta | 🔒 | 🔒 | ✅ | -Flavors Android/iOS à configurer : `com.etm-powersync.community` / `.auto` / `.predictai` - --- -## Conventions - -- **Code** : anglais — **Commentaires** : français -- **Nommage** : camelCase variables, PascalCase classes -- Les écrans utilisent `Consumer` ou `context.watch` — ne jamais dupliquer l'état du service dans un `State` -- **Unités** : Watts (W) pour les puissances instantanées, Wh pour les énergies journalières. L'API nymea retourne des totaux en kWh que le service multiplie par 1000. +## 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 la maquette avec Patrick avant de coder -- Tout nouvel appel RPC → vérifier dans `docs/nymea_api.md` que la méthode existe -- Ne **jamais** appeler `Energy.SetChargingMode` pour piloter un EV -- Toujours tester la persistance : killer l'app et vérifier que les données survivent au redémarrage - ---- - -## Documentation API - -- `docs/nymea_api.md` — introspection complète de l'API nymea -- `docs/nymea_integrations.md` — namespace Integrations en détail +- Tout nouvel écran → valider maquette avec Patrick avant de coder +- Tout nouvel appel RPC → vérifier dans `INTERFACE.md` que la méthode existe +- Ne jamais appeler `Energy.SetChargingMode` pour piloter un EV +- Toujours tester la persistance : killer l'app et vérifier que les données survivent +- Flavors à configurer : `com.etm-powersync.community` / `.auto` / `.predictai`