From 1278da2a040f66f4567295609b1bc7eac95d7f6a Mon Sep 17 00:00:00 2001 From: Patrick Schurig ETM-Schurig Date: Sat, 4 Apr 2026 17:35:57 +0200 Subject: [PATCH] chore: add CLAUDE.md agent context --- CLAUDE.md | 196 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 181 insertions(+), 15 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9804a7a..33bb194 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,10 +6,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Dossier parent de tous les projets : `/home/etm/Projects/` -- **`etm_powersync_app/`** (ce projet) — application Flutter HEMS, client mobile +- **`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. --- @@ -23,6 +26,20 @@ 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) | +| 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) | + +--- + ## Architecture ### State management — un seul Provider @@ -50,7 +67,7 @@ flutter test # lancer les tests 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()`. +**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 @@ -65,7 +82,160 @@ Les requêtes utilisent des IDs auto-incrémentés stockés dans `_pendingReques ### Thème -`AppTheme` (`lib/theme/app_theme.dart`) expose des constantes `Color` nommées : `primaryGreen`, `solarYellow`, `gridGray`, `homeBlue`, `batteryGreen`, `boostRed`, `pvGreen`, `minPvBlue`, etc. Toujours utiliser ces constantes plutôt que des valeurs hex brutes. +`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 +// ❌ FAUX — ne déclenche pas le SmartChargingManager +nymeaService.call('Energy.SetChargingMode', {'mode': 'pv'}); + +// ✅ CORRECT — appelle vraiment le plugin ETM +nymeaService.call('EnergyPlugin.SetChargingInfo', { + 'chargingInfo': { + 'evChargerId': chargerId, + 'mode': 'Eco', // Normal | Eco | EcoWithTargetTime + 'targetSoc': 80, + 'endTime': null, + } +}); +``` + +--- + +## APIs JSON-RPC consommées + +### `Energy.*` (nymea-experience-plugin-energy) + +| 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 | + +**Notifications push :** `PowerBalanceChanged`, `PowerBalanceLogEntryAdded`, `ThingPowerLogEntryAdded`, `RootMeterChanged` + +### `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) + +--- + +## État des écrans + +| É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** | +| 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 | + +--- + +## Persistance + +| Donnée | Actuellement | À faire | +|---|---|---| +| 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 | + +--- + +## 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 +if (tierProvider.tier >= Tier.auto) { + // afficher feature Auto +} +``` + +| Feature | Community | Auto | Predict AI | +|---|---|---|---| +| Dashboard temps réel | ✅ | ✅ | ✅ | +| Historique journée en cours | ✅ | ✅ | ✅ | +| Config EV (`EnergyPlugin.SetChargingInfo`) | ✅ | ✅ | ✅ | +| Tarif HP/HC statique | ✅ | ✅ | ✅ | +| UI automatisations (`Rules.*`) | ✅ | ✅ | ✅ | +| Historique 90 jours | 🔒 | ✅ | ✅ | +| Wizard onboarding guidé | 🔒 | ✅ | ✅ | +| 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 | 🔒 | 🔒 | ✅ | +| Accès fonctionnalités beta | 🔒 | 🔒 | ✅ | + +Flavors Android/iOS à configurer : `com.etm-powersync.community` / `.auto` / `.predictai` + +--- ## Conventions @@ -74,20 +244,16 @@ Les requêtes utilisent des IDs auto-incrémentés stockés dans `_pendingReques - 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. -## Méthodes nymea principales +--- -| Méthode | Rôle | -|---------|------| -| `JSONRPC.Hello` | Handshake (WebSocket uniquement — envoyé par le client) | -| `Integrations.GetThings` | Charger les devices configurés | -| `Integrations.GetThingClasses` | Charger les définitions de classes (filtre `thingClassIds` supporté) | -| `Integrations.ExecuteAction` | Exécuter une action sur un thing | -| `Integrations.GetStateValue` | Lire une valeur d'état | -| `Energy.GetPowerBalance` | Snapshot du flux énergétique actuel | -| `Energy.GetEnergyLogs` | Historique énergie (échantillons horaires) | -| `Energy.SetChargingMode` | Mode borne EV : `pv`, `minpv`, `boost` | +## Règles de modification -Notifications push gérées : `Energy.PowerBalanceChanged`, `Energy.RootMeterChanged`, `Integrations.StateChanged`, `Integrations.ThingAdded`, `Integrations.ThingRemoved`. +- 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