# 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. --- ## Commandes ```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) | | 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 `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. ```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 - **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. --- ## 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