etm-powersync-app/CLAUDE.md

262 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<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
// ❌ 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<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
- 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