chore: mise à jour CLAUDE.md — brief refonte UI agent

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 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig ETM-Schurig 2026-05-29 22:10:50 +02:00
parent 80500e21e6
commit 7d71bea527

284
CLAUDE.md
View File

@ -1,106 +1,30 @@
# CLAUDE.md # Agent App — `etm_powersync_app`
> Lire aussi le `CLAUDE.md` du dossier parent avant de commencer.
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.
--- ---
## 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 ## Stack technique
| Élément | Valeur | | Élément | Valeur |
|---|---| |---|---|
| Flutter SDK | ^3.11.0 | | 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 | | Navigation | `go_router ^14.6.3` — ShellRoute + 25+ routes |
| Transport | WebSocket port 4444 / TCP brut port 2222 | | Transport | WebSocket port 4444 / TCP brut port 2222 |
| Graphiques | `fl_chart ^0.70.2` | | Graphiques | `fl_chart ^0.70.2` |
| Persistance locale | `shared_preferences ^2.3.2` | | Persistance | `shared_preferences ^2.3.2` |
| Sécurité installateur | `crypto ^3.0.6` (PIN SHA-256, lock 30s, auto-lock 10 min) | | Sécurité installateur | `crypto ^3.0.6` (PIN SHA-256) |
--- ---
## Architecture ## 🔴 Bug critique — à corriger en priorité absolue
### 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<NymeaService>()` ou `context.read<NymeaService>()`. 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<int, Completer>`. 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.
```dart ```dart
// ❌ FAUX — ne déclenche pas le SmartChargingManager // ❌ FAUX — ne déclenche pas le SmartChargingManager
@ -110,64 +34,65 @@ nymeaService.call('Energy.SetChargingMode', {'mode': 'pv'});
nymeaService.call('EnergyPlugin.SetChargingInfo', { nymeaService.call('EnergyPlugin.SetChargingInfo', {
'chargingInfo': { 'chargingInfo': {
'evChargerId': chargerId, 'evChargerId': chargerId,
'mode': 'Eco', // Normal | Eco | EcoWithTargetTime 'mode': 'Eco', // Eco | EcoWithMinCurrent | Normal
'targetSoc': 80, // EcoWithTargetTime | EcoMinWithTargetTime
'endTime': null, '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 ## 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 | ### `EnergyPlugin.*` — powersync-energy-plugin-etm
|---|---| | Méthode | Usage | Tier |
| `GetPowerBalance` | Dashboard temps réel | |---|---|---|
| `GetPowerBalanceLogs(sampleRate, from, to)` | Historique — envoyer les bons `from/to` pour les 90j | | `GetChargingInfos(evChargerId)` | Lire config EV | Community |
| `GetThingPowerLogs(sampleRate, thingIds[], from, to)` | Historique par device | | `SetChargingInfo(chargingInfo)` | **⚠️ CORRECTION CRITIQUE** | Community |
| `SetRootMeter(rootMeterThingId)` | Config installateur | | `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) ### `Rules.*` — nymea core
| Méthode | Usage | Tier |
| Méthode | Usage | |---|---|---|
|---|---| | `GetRules` | Lecture automatisations | Community |
| `GetChargingInfos(o:evChargerId)` | Lire config recharge EV | | `AddRule` | Créer règle HP/HC, surplus → relais | Community |
| `SetChargingInfo(chargingInfo)` | **Piloter le smart charging EV** (Eco / EcoWithTargetTime / Normal) | | `RemoveRule / EditRule` | Gérer règles | Community |
| `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)
--- ---
@ -175,87 +100,76 @@ nymeaService.call('EnergyPlugin.SetChargingInfo', {
| Écran | État | Action requise | | Écran | État | Action requise |
|---|---|---| |---|---|---|
| Dashboard (Sankey + gains + EV card) | ✅ | Icône notifications à brancher | | Dashboard (Sankey + EV card) | ✅ | Corriger API EV + brancher notifications |
| EnergyScreen (4 onglets + charts) | ✅ | Ajouter sélecteur plage 90j | | EnergyScreen (4 onglets) | ✅ | Ajouter sélecteur plage 90j |
| ThingsScreen + ThingDetailScreen | ✅ | — | | ThingsScreen + ThingDetail | ✅ | — |
| FavoritesScreen | ✅ | **Persister dans SharedPreferences** | | FavoritesScreen | ✅ | Persister dans SharedPreferences |
| InstallerMode (PIN SHA-256) | ✅ | — | | InstallerMode (PIN SHA-256) | ✅ | — |
| RoleConfigFlow wizard | ⚠️ Stub | Brancher Step 3 sur les vrais RPC | | RoleConfigFlow wizard | ⚠️ Stub | Brancher sur vrais RPC |
| TariffScreen | ⚠️ Stub | Brancher sur `EnergyPlugin.SetSpotMarketConfiguration` | | TariffScreen | ⚠️ Stub | Brancher `SetSpotMarketConfiguration` |
| SchedulerScreen | ⚠️ Stub | `setStrategy()` / `forceRecalc()` = fonctions vides | | SchedulerScreen | ⚠️ Stub | Brancher `GetChargingSchedules` |
| TimelineScreen | ⚠️ Stub | Données simulées → brancher scheduler réel | | TimelineScreen | ⚠️ Stub | Brancher scheduler réel |
| AirConditioning zones | ❌ Absent | À créer (tier Auto) | | AirConditioning zones | ❌ Absent | Créer (Auto) |
| DeveloperScreen | ❌ Vide | À créer | | Rules UI (automatisations) | ❌ Absent | Créer (Community) |
| AboutScreen | ❌ Vide | À créer | | 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 | — | | Adresse serveur, PIN, préférences UI | ✅ SharedPreferences | — |
| `RoleAssignments` (configuration EMS) | ❌ En mémoire | Persister SharedPreferences | | `RoleAssignments` | ❌ Mémoire | Persister SharedPreferences |
| `FavoriteWidgets` | ❌ En mémoire | Persister SharedPreferences | | `FavoriteWidgets` | ❌ Mémoire | Persister SharedPreferences |
| `TariffConfig` / `HcHpConfig` | ❌ En mémoire | Persister SharedPreferences | | `TariffConfig` / `HcHpConfig` | ❌ Mémoire | Persister SharedPreferences |
| `SchedulerConfig` | ❌ En mémoire | Persister SharedPreferences | | `SchedulerConfig` | ❌ Mémoire | Persister SharedPreferences |
--- ---
## Feature gating par tier ## 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 ```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) { if (tierProvider.tier >= Tier.auto) {
// afficher feature Auto // afficher feature Auto
} }
// Utiliser pro_lock_badge.dart (déjà présent) pour verrouiller visuellement
``` ```
### Features par tier
| Feature | Community | Auto | Predict AI | | Feature | Community | Auto | Predict AI |
|---|---|---|---| |---|---|---|---|
| Dashboard temps réel | ✅ | ✅ | ✅ | | Dashboard temps réel | ✅ | ✅ | ✅ |
| Historique journée en cours | ✅ | ✅ | ✅ | | Config EV (SetChargingInfo) | ✅ | ✅ | ✅ |
| Config EV (`EnergyPlugin.SetChargingInfo`) | ✅ | ✅ | ✅ |
| Tarif HP/HC statique | ✅ | ✅ | ✅ | | Tarif HP/HC statique | ✅ | ✅ | ✅ |
| UI automatisations (`Rules.*`) | ✅ | ✅ | ✅ | | UI automatisations (Rules.*) | ✅ | ✅ | ✅ |
| Historique 90 jours | 🔒 | ✅ | ✅ | | Historique 90 jours | 🔒 | ✅ | ✅ |
| Wizard onboarding guidé | 🔒 | ✅ | ✅ | | Wizard onboarding | 🔒 | ✅ | ✅ |
| Zones PAC/ECS (`AirConditioning.*`) | 🔒 | ✅ | ✅ | | Zones PAC/ECS (AirConditioning.*) | 🔒 | ✅ | ✅ |
| Prévision solaire Open-Meteo | 🔒 | ✅ | ✅ | | Prévision solaire Open-Meteo | 🔒 | ✅ | ✅ |
| Notifications d'anomalies | 🔒 | ✅ | ✅ | | Notifications d'anomalies | 🔒 | ✅ | ✅ |
| Accès distant sécurisé | 🔒 | ✅ | ✅ | | Accès distant sécurisé | 🔒 | ✅ | ✅ |
| Tarifs dynamiques aWATTar / spot market | 🔒 | 🔒 | ✅ | | Tarifs dynamiques aWATTar | 🔒 | 🔒 | ✅ |
| ENTSO-E / Tibber | 🔒 | 🔒 | ✅ |
| Accès fonctionnalités beta | 🔒 | 🔒 | ✅ | | Accès fonctionnalités beta | 🔒 | 🔒 | ✅ |
Flavors Android/iOS à configurer : `com.etm-powersync.community` / `.auto` / `.predictai`
--- ---
## Conventions ## Thème — utiliser `app_theme.dart` systématiquement
```dart
- **Code** : anglais — **Commentaires** : français primaryGreen solarYellow gridGray homeBlue
- **Nommage** : camelCase variables, PascalCase classes batteryGreen boostRed pvGreen minPvBlue accentTeal
- Les écrans utilisent `Consumer<NymeaService>` 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.
--- ---
## Règles de modification ## Règles de modification
- Tout nouvel écran → valider maquette avec Patrick avant de coder
- Tout nouvel écran → valider la maquette avec Patrick avant de coder - Tout nouvel appel RPC → vérifier dans `INTERFACE.md` que la méthode existe
- 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
- Ne **jamais** appeler `Energy.SetChargingMode` pour piloter un EV - Toujours tester la persistance : killer l'app et vérifier que les données survivent
- Toujours tester la persistance : killer l'app et vérifier que les données survivent au redémarrage - Flavors à configurer : `com.etm-powersync.community` / `.auto` / `.predictai`
---
## Documentation API
- `docs/nymea_api.md` — introspection complète de l'API nymea
- `docs/nymea_integrations.md` — namespace Integrations en détail