diff --git a/CLAUDE.md b/CLAUDE.md index 2bf1868..6fbe624 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -29,69 +29,130 @@ mais la source de vérité du tier est dans `powersync-optimizer`. --- -## 🔴 Bug critique — à corriger en priorité absolue +## 🔴 Recharge EV — l'appel réel, vérifié contre la box + +> **Ce bloc décrivait un bug qui n'existe plus, avec des champs qui n'ont jamais existé.** +> `Energy.SetChargingMode` **n'est pas une méthode de nymea** — il n'y en a que cinq dans +> `Energy.*`, et celle-là n'en fait pas partie : l'appel échouerait sur « No such method ». +> Et le namespace `EnergyPlugin.*` n'existe pas non plus. Corrigé le 2026-08-25 contre +> `JSONRPC.Introspect` sur `.75` (plugin **1.15.2+etm15**). ```dart -// ❌ FAUX — ne déclenche pas le SmartChargingManager -nymeaService.call('Energy.SetChargingMode', {'mode': 'pv'}); - -// ✅ CORRECT — appelle vraiment le plugin ETM -nymeaService.call('EnergyPlugin.SetChargingInfo', { +nymeaService.call('NymeaEnergy.SetChargingInfo', { 'chargingInfo': { - 'evChargerId': chargerId, - 'mode': 'Eco', // Eco | EcoWithMinCurrent | Normal - // EcoWithTargetTime | EcoMinWithTargetTime - 'minCurrent': 6, // requis pour EcoWithMinCurrent/* - 'targetSoc': 80, // requis pour *WithTargetTime - 'endTime': null, // requis pour *WithTargetTime + 'evChargerId': chargerId, // Uuid — requis + 'chargingMode': 'ChargingModeEco', // requis, énumération ci-dessous + 'targetPercentage': 80, // o: Uint — charge cible + 'endDateTime': 1786250400, // o: Uint — horodatage, PAS une chaîne + 'repeatDays': [1, 2, 3, 4, 5], // o: Int[] + 'assignedCarId': carId, // o: Uuid + 'spotMarketChargingEnabled': false, // o: Bool + 'dailySpotMarketPercentage': 30, // o: Uint } }); ``` -### Modes UI EV — 3 boutons + option deadline +`chargingState` est en **lecture seule** (`r:`) dans la charge utile : +`ChargingStateIdle` · `ChargingStateSurplusCharging` · `ChargingStateSpotMarketCharging` · +`ChargingStateTimeRequirement`. + +### Énumération `ChargingMode` — les vrais noms + +`ChargingModeNormal` · `ChargingModeEco` · `ChargingModeEcoWithTargetTime` · +`ChargingModeEcoWithMinCurrent` · `ChargingModeEcoMinWithTargetTime` + +### Modes UI EV — 3 boutons + option échéance ``` [PV] [Min+PV] [Boost] ↓ - Option deadline ? → afficher champs SOC cible + heure d'arrivée + Option échéance ? → charge cible (targetPercentage) + heure (endDateTime) -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 +PV → ChargingModeEco +Min+PV → ChargingModeEcoWithMinCurrent +Boost → ChargingModeNormal +PV + échéance → ChargingModeEcoWithTargetTime + targetPercentage + endDateTime +Min+PV + échéance → ChargingModeEcoMinWithTargetTime + targetPercentage + endDateTime ``` +> ⚠️ **`minCurrent` n'existe nulle part.** Le mode `ChargingModeEcoWithMinCurrent` est bien +> dans l'énumération, mais `ChargingInfo` ne porte **aucun champ** de courant minimum. Soit +> il est fixé ailleurs (paramètre du Thing borne), soit il manque côté plugin. **Ne pas +> proposer de réglage de courant minimum tant que ce n'est pas tranché** — un champ qui ne +> part nulle part est pire que pas de champ. + --- ## APIs JSON-RPC consommées -### `Energy.*` — nymea-experience-plugin-energy +### `Energy.*` — nymea-experience-plugin-energy · **5 méthodes** | 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 | +| `GetRootMeter` | Lire le compteur racine configuré | Community | | `SetRootMeter(rootMeterThingId)` | Config installateur | Community | -### `EnergyPlugin.*` — powersync-energy-plugin-etm +> `Energy.*` s'arrête là. **`SetChargingMode` n'existe pas** — ni ici, ni ailleurs. + +### `NymeaEnergy.*` — powersync-energy-plugin-etm · **20 méthodes / 14 notifications** + +> ⚠️ Le namespace est **`NymeaEnergy`**, pas `EnergyPlugin`. Ce dernier n'existe pas. +> Source d'autorité : `energyplugin/nymeaenergyjsonhandler.cpp` et `JSONRPC.Introspect`. +> **Ce n'est pas dans `OPTIMIZER_PROTOCOL.md`** — ce document décrit le protocole +> plugin ↔ optimiseur (couche A ↔ couche B), pas la frontière JSON-RPC de l'app. + +**Charges pilotées** +| Méthode | Usage | Tier | +|---|---|---| +| `GetLoadConfig` | Lire les charges déclarées | Community | +| `SetLoadConfig(loadConfigs[])` | Écrire — **remplace TOUT l'ensemble**, refus en bloc | Community | +| `GetLoadTelemetry` | État runtime de l'arbitrage (budget, allocation, verrous, motifs) | Community | +| `ClearLoadFault(loadId)` | Lever le verrou de défaut — **l'ack ne prouve rien** | Community | + +**Recharge EV** | Méthode | Usage | Tier | |---|---|---| | `GetChargingInfos(evChargerId)` | Lire config EV | Community | -| `SetChargingInfo(chargingInfo)` | **⚠️ CORRECTION CRITIQUE** | Community | +| `SetChargingInfo(chargingInfo)` | Écrire config EV — voir le bloc plus haut | 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 | +| `GetLockOnUnplug` / `SetLockOnUnplug(Bool)` | Verrouillage au débranchement | Community | -### `AirConditioning.*` — nymea-experience-plugin-airconditioning +**Réglages globaux d'installation** +| Méthode | Usage | Tier | +|---|---|---| +| `GetPhasePowerLimit` / `SetPhasePowerLimit(Uint)` | **AMPÈRES par phase**, pas des watts. `0` = désactivé, et coupe toute la recharge intelligente | Community | +| `GetAcquisitionTolerance` / `SetAcquisitionTolerance(Double)` | Seuil de surplus au démarrage — **unité non documentée par la box** | Community | +| `GetBatteryLevelConsideration` / `SetBatteryLevelConsideration(Double)` | Part du stockage prise en compte | Community | + +**Tarifs dynamiques** +| Méthode | Usage | Tier | +|---|---|---| +| `GetAvailableSpotMarketProviders` | Liste providers | Predict AI | +| `GetSpotMarketConfiguration` / `SetSpotMarketConfiguration(enabled, providerId)` | Config tarif dynamique | Predict AI | +| `GetSpotMarketScoreEntries(date)` | Cotations aWATTar | Predict AI | +| `GetEnergyRatios` | Autoconsommation / autonomie | Community | + +**Notifications** — `LoadConfigChanged` · `LoadTelemetryChanged` · `ChargingInfoAdded/Changed/Removed` · +`ChargingSchedulesChanged` · `EnergyRatiosChanged` · `PhasePowerLimitChanged` · +`AcquisitionToleranceChanged` · `BatteryLevelConsiderationChanged` · `LockOnUnplugChanged` · +`SpotMarketConfigurationChanged` · `SpotMarketStatusChanged` · `SpotMarketScoreEntriesChanged` + +### `AirConditioning.*` — nymea-experience-plugin-airconditioning · **8 méthodes** | Méthode | Usage | Tier | |---|---|---| | `GetZones` | Afficher zones PAC/thermostat | Auto | -| `SetZoneSetpointOverride` | Pilotage manuel | Auto | +| `AddZone` / `RemoveZone` | CRUD de zones | Auto | +| `SetZoneName` | Renommer | Auto | +| `SetZoneThings(zoneId, thermostats[], valves[], indoorSensors[], outdoorSensors[], windowSensors[], notifications[])` | Rattacher les appareils | Auto | +| `SetZoneSetpointOverride(zoneId, mode, setpointOverride, minutes)` | Dérogation manuelle | Auto | +| `SetZoneStandbySetpoint` | Consigne de réduit | Auto | | `SetZoneWeekSchedule` | Planning 7 jours | Auto | +> `ZoneInfo` est **presque entièrement en lecture** (`r:`) : température, humidité, PM2.5, COV, +> consigne courante, dérogation et sa fin, listes d'appareils. On écrit par les six `Set*`, +> jamais par un objet complet. + ### `Rules.*` — nymea core | Méthode | Usage | Tier | |---|---|---| @@ -175,6 +236,9 @@ batteryGreen boostRed pvGreen minPvBlue accentTeal ## Règles de modification - 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 +- Piloter un EV = `NymeaEnergy.SetChargingInfo` (`Energy.SetChargingMode` n'existe pas) +- **Vérifier ce fichier contre `JSONRPC.Introspect` avant chaque lot.** Trois fois le + 2026-08-25, la doc locale était en retard sur la machine et a envoyé un agent dans le vide. + La sonde tient en une ligne : `dart tools/rpc/probe.dart 192.168.1.75 JSONRPC.Introspect` - Toujours tester la persistance : killer l'app et vérifier que les données survivent - Flavors à configurer : `com.etm-powersync.community` / `.auto` / `.predictai` diff --git a/RECAP.md b/RECAP.md index 00bccde..9736cf6 100644 --- a/RECAP.md +++ b/RECAP.md @@ -260,6 +260,66 @@ autres (`cycleAge`, `sinceLastFrame`) sont court-circuités par l'éjection. la cause racine du défaut 2 ; sans ça, un glisser-déposer n'y écrira jamais un `priority` réel. +## Refonte décidée le 2026-08-25 (soir) — maquettes validées + +Patrick a tranché la question ouverte du RECAP : **l'écran « Ordre de service des charges » +(`/energy/loads`) disparaît.** Le rang se règle dans « Rôles & appareils → Charges +pilotables » au glisser-déposer, le **regroupement à deux niveaux est conservé**, et le bloc +de télémétrie descend sur la carte de chaque charge. + +| Maquette | Contenu | +|---|---| +| `docs/mockups/charges_pilotables_mockup.html` | La liste : bandeau arbitrage, domaines glissables, cartes avec télémétrie, défaut, bouton Configurer | +| `docs/mockups/configurer_mecanismes_mockup.html` | Les six « Configurer » : relais · modulable · SG-Ready · borne · zones HVAC · sélecteur d'appareils | +| `docs/mockups/raccordement_reseau_mockup.html` | Compteur réseau + protection de surcharge | + +**Décisions de conception qui ne se déduisent pas des maquettes :** + +- **Les paliers n'existent que chez le routeur de relais**, et ils y sont *déduits* des + combinaisons de contacts, jamais saisis. Le modulable ne connaît qu'un plafond + (`maxPowerW`). Le mode `fixed` d'`etmvariableload` — que le plugin qualifie lui-même de + *legacy* — **n'est plus proposé**, mais reste **lisible** : ouvrir l'écran ne doit pas + convertir une config déployée. +- **Le critère de choix entre les deux ECS**, formulé pour l'installateur : *où sont les + contacteurs ?* Sur les sorties de la box → routeur de relais. Dans l'appareil, piloté par + une consigne → modulable. +- **SG1 / SG2 n'existent pas côté box.** `sgReady.states[].relays[]` n'est qu'une liste par + état. Les étiquettes sont une convention d'app, retrouvée à la lecture par l'encodage + standard (SG1 = fermé en état 1, SG2 = fermé en état 3). Un encodage non standard perd les + noms, pas la configuration. +- **Le compteur par charge figure dans les cinq écrans, inactif**, avec la raison sur place. + Aucun champ n'existe côté box, et nymea rejette toute clé inconnue avant le handler. +- **La borne règle un mode PAR DÉFAUT**, pas une commande — le pilotage vit sur la carte + véhicule du tableau de bord. + +## Ce qui remonte vers le plugin + +Six points, rédigés en brief exploitable : `docs/BRIEF_plugin_depuis_maquettes.md`. +Le seul **blocage dur** est le premier — `ChargingInfo` n'a aucun champ de courant minimum, +donc le mode Min+PV reste sélectionnable mais non réglable. + +Les cinq autres : motif de refus de `SetLoadConfig` qui ne franchit pas la frontière · +`WRITE_FAILED` qui masque `THING_MISSING` · paliers atteignables non exposés (l'app devra +sinon dupliquer la combinatoire de `relayrouter.cpp`) · état matériel de `enabled:false` à +confirmer pour relais et modulable · `estimatedPowerW` du banc encore fictif. + +## Contrôle de la doc contre la machine — à refaire avant chaque lot + +`CLAUDE.md` a été **corrigé contre `JSONRPC.Introspect`** le 2026-08-25. Trois fois dans la +journée la doc locale était en retard sur la box : + +1. le namespace `LoadConfig` cherché dans `OPTIMIZER_PROTOCOL.md`, où il n'est pas (note de + périmètre ajoutée côté plugin, commit `afaa8c2`) ; +2. le « bug critique » `Energy.SetChargingMode` — **cette méthode n'existe pas**, l'appel + échouerait sur « No such method » ; +3. les champs de `ChargingInfo` (`mode`/`minCurrent`/`targetSoc`/`endTime` au lieu de + `chargingMode`/`targetPercentage`/`endDateTime`), le namespace `EnergyPlugin.*` + inexistant, et `AirConditioning` donné pour 3 méthodes quand il en expose 8. + +Toutes les méthodes citées par `CLAUDE.md` ont été revérifiées une par une : **aucune +introuvable**. La sonde tient en une ligne — +`dart tools/rpc/probe.dart 192.168.1.75 JSONRPC.Introspect`. + ## Vérifié / non vérifié (lot B app) | Critère | Statut | diff --git a/docs/BRIEF_plugin_depuis_maquettes.md b/docs/BRIEF_plugin_depuis_maquettes.md new file mode 100644 index 0000000..4546b8d --- /dev/null +++ b/docs/BRIEF_plugin_depuis_maquettes.md @@ -0,0 +1,196 @@ +# Brief plugin — ce que les maquettes « Configurer » demandent au moteur + +**Origine :** session du 2026-08-25, côté app. Écrit après le passage sur appareil et la +conception des six écrans « Configurer » (`docs/mockups/`). +**Cible :** `etm-powersync-energy-plugin-etm`, plugin déployé au moment du relevé : +**1.15.2+etm15** sur `.75`. +**Méthode :** chaque point est un constat vérifié contre la machine ou la source, pas une +préférence d'UI. Les maquettes sont jointes — elles montrent où chaque manque se voit. + +--- + +## 1. `SetChargingInfo` ne permet pas l'écran borne — seul blocage dur + +**Ce que l'écran veut** (`configurer_mecanismes_mockup.html`, écran ③) : trois modes +`[PV] [Min+PV] [Boost]`, une option d'échéance de départ avec charge cible et heure, et le +basculement automatique vers les variantes `…WithTargetTime`. + +**Ce que la box expose**, introspecté sur `.75` : + +``` +ChargingInfo = chargingMode · evChargerId + · o:assignedCarId · o:dailySpotMarketPercentage · o:endDateTime (Uint) + · o:repeatDays · o:spotMarketChargingEnabled · o:targetPercentage + · r:chargingState +``` + +L'échéance est **partiellement là** : `targetPercentage` et `endDateTime` existent, et les +modes `ChargingModeEcoWithTargetTime` / `ChargingModeEcoMinWithTargetTime` sont dans +l'énumération. Ce qui manque est ailleurs : + +**⛔ `minCurrent` n'existe nulle part.** `ChargingModeEcoWithMinCurrent` est un mode valide, +mais aucun champ de `ChargingInfo` ne porte le courant à maintenir. Deux issues possibles, +et elles ne demandent pas le même travail : + +- le courant est déjà fixé ailleurs (paramètre du Thing borne, réglage global) → **le dire**, + et l'app pointera l'utilisateur au bon endroit ; +- il manque → **ajouter le champ**, et dire s'il est par borne ou global. + +Tant que ce n'est pas tranché, l'app **ne proposera pas** de réglage de courant minimum : un +champ qui ne part nulle part est pire que pas de champ. Le mode Min+PV reste donc à moitié +livrable — sélectionnable, mais non réglable. + +> Corollaire déjà traité côté app : `CLAUDE.md` documentait `mode` / `minCurrent` / +> `targetSoc` / `endTime` sur un namespace `EnergyPlugin.*` inexistant. Corrigé le +> 2026-08-25 contre `Introspect`. + +--- + +## 2. Le motif de refus de `SetLoadConfig` ne franchit pas la frontière + +**Constat, reproduit deux fois.** Une charge invalide fait répondre : + +```json +{"params":{"energyError":"EnergyErrorInvalidParameter"},"status":"success"} +``` + +…et rien d'autre. Le motif reste au journal de la box : + +``` +W | NymeaEnergy: [NymeaEnergy] SetLoadConfig rejetée: + "Config \"SONDE-TEST-A-SUPPRIMER\" invalide : etmvariableload fixed : powerLevels requis (non vide)" +``` + +**Pourquoi ça compte maintenant.** Les six écrans « Configurer » écrivent tous par +`SetLoadConfig`, et le refus est **en bloc** : une seule charge invalide fait échouer +l'enregistrement de toutes. L'installateur apprendra qu'on a refusé, jamais pourquoi ni sur +quelle charge — devant un client, avec un tableau électrique ouvert. + +**Ce que l'app peut faire seule, et sa limite.** Rejouer `validateSet()` côté client couvre +l'unicité des ids et des Things. Elle ne couvre pas les règles de mécanisme +(`powerLevels` commençant par 0, `powerW > 0`, `relays[]` interdit en modulable, `mode` +imposé…) sans dupliquer `LoadConfig::validate()` — deuxième implémentation, deuxième +divergence. + +**Demande.** Le même remède que la télémétrie : un **code** et des **paramètres**, pas une +phrase. Par exemple, à côté de `energyError` : + +```json +{"energyError":"EnergyErrorInvalidParameter", + "rejection":{"loadId":"chauffe-eau","code":"POWERLEVELS_REQUIRED","params":{"mode":"fixed"}}} +``` + +Le libellé français est fabriqué par l'app, comme pour `decision.code`. + +--- + +## 3. `WRITE_FAILED` masque `THING_MISSING` + +**Constat**, provoqué délibérément sur `.75` (`tools/rpc/fault_probe.dart`) : une charge +`etmvariableload` dont le ThingId n'existe pas publie **`WRITE_FAILED`**, pas +`THING_MISSING`. L'écriture est tentée, échoue faute de Thing, l'échelle s'épuise, et +`m_faulted` prend le pas — l'ordre des `if` dans `etmvariableloadadapter.cpp:148-150` : + +```cpp +if (m_faulted) v.faultCode = "WRITE_FAILED"; +else if (!thingManager->findConfiguredThing(...)) v.faultCode = "THING_MISSING"; +``` + +**Pourquoi ça compte.** Les deux codes appellent **deux gestes différents** chez le client. +« Échec d'écriture » envoie chercher un défaut de liaison — bus Modbus, alimentation, +contacteur. « Appareil absent » envoie dans la configuration nymea. L'app rend fidèlement ce +que la box dit, et la box envoie au mauvais endroit. + +**Demande.** Faire primer la cause structurelle : un Thing absent est un `THING_MISSING`, +même si des écritures ont échoué entre-temps. Ou publier les deux. + +--- + +## 4. Les paliers atteignables ne sont pas exposés + +**Constat.** `mechanism` (kind `relay`) publie `stageW` et `maxStageW` — **la liste des +paliers atteignables n'existe dans aucun champ** : + +``` +mechanism = kind · o:stageW · o:maxStageW · o:setpointW · o:percent · o:maxPowerW + · o:state · o:estimatedPowerW · o:chargingEnabled · o:currentA + · o:phaseCount · o:pluggedIn +``` + +**Ce que la maquette affiche** (écran ①, routeur de relais) : + +> 3 relais → paliers atteignables : **0 · 500 · 1 000 · 1 500 · 2 000 · 2 500 · 3 000 · 3 500 W** + +C'est l'aide la plus utile du formulaire : elle dit à l'installateur ce que son câblage +permet, avant qu'il n'enregistre. + +**Le risque.** Faute d'exposition, l'app **dérivera** cette liste — donc réimplémentera la +combinatoire de `relayrouter.cpp` (toutes les combinaisons 2^N, doublons écartés, plafond +16 relais). Deux implémentations d'une même règle divergeront : à la première correction +côté moteur, l'app annoncera des paliers que la box n'atteint plus. + +**Demande.** Exposer la liste — soit dans la télémétrie (`mechanism.stagesW: [...]`), soit en +retour d'un `GetLoadConfig` enrichi. À défaut, **le dire explicitement**, et l'app +documentera sa dérivation comme un miroir à resynchroniser. + +--- + +## 5. `enabled: false` — ce que deviennent les contacts + +**Ce que l'app sait déjà**, et qui est bon : la charge quitte l'arbitrage, ne consomme aucun +budget, et disparaît de `loads[]` **au cycle suivant** — pas dans la seconde. + +**Ce qu'il reste à établir côté app :** l'état matériel. Le relevé du lot B-bis (§4) répond +sur `sg-ready` — contacts K1/K2 mesurés ouverts après désactivation, l'état sûr passe bien +par `applySafeState()` avant retrait de l'adaptateur. **Reste à confirmer pour +`relay-router` et `etmvariableload`**, sur matériel. + +**Pourquoi ça compte.** La maquette écrit aujourd'hui « à la désactivation, le retrait passe +par l'état sûr : tous les relais ouverts ». Si c'est faux pour un mécanisme, l'écran laisse +croire à une coupure qui n'a pas eu lieu — sur un chauffe-eau, c'est une promesse de +sécurité. + +**Demande.** Confirmer, par mécanisme, ce que produit `enabled: false` sur le matériel. Une +ligne de journal par adaptateur suffit ; c'est déjà le cas pour SG-Ready. + +--- + +## 6. `estimatedPowerW` SG-Ready reste fictif sur `.75`, et il alimente le recrédit + +**Constat, déjà relevé côté plugin** (`RELEVE_LOTBbis.md` §7) : les valeurs {état 3 : 1500 W, +état 4 : 3000 W} et `minStateHoldS: 300` sont reconduites du bloc codé en dur supprimé, où +elles décrivaient une PAC **inventée**. Aucune PAC n'est sous tension sur K1/K2. + +**Ce que ça change côté app.** L'écran SG-Ready (écran ②) présente `estimatedPowerW` comme +une **donnée d'installation**, saisie par l'installateur d'après la plaque signalétique. Sur +le banc, c'est une valeur de test — et elle sert de base au **recrédit de budget** et au +choix d'état, donc elle décale l'allocation de tout le waterfall en aval. + +**Demande.** Rien à changer dans le code : c'est un point de **données de banc**. Mais tant +qu'il n'est pas clos, aucune capture de `.75` ne doit servir d'exemple d'installation, et le +§4 de `TEST_TERRAIN.md` reste non transposable. + +--- + +## Ce que l'app livre sans rien attendre + +Pour situer ce qui est bloqué et ce qui ne l'est pas : + +| Écran | État | +|---|---| +| Liste « Charges pilotables » à deux niveaux, télémétrie sur carte | livrable | +| Configurer · routeur de relais | livrable — sauf l'aperçu des paliers (point 4) | +| Configurer · modulable | livrable | +| Configurer · SG-Ready | livrable | +| Configurer · borne de recharge | **bloqué sur le point 1** pour le mode Min+PV | +| Zones HVAC + sélecteur d'appareils | livrable côté RPC — **aucun matériel HVAC sur `.75`** | +| Raccordement réseau / protection de surcharge | livrable, mais voir ci-dessous | + +**Et une question de périmètre, pas un défaut.** La protection de surcharge ne déleste +aujourd'hui **que les bornes de recharge** (`m_evChargers` dans +`SmartChargingManager::verifyOverloadProtection`). Le chauffe-eau, la PAC et les charges +modulables ne sont pas réduits par elle. L'écran « Raccordement » le dit explicitement plutôt +que de promettre une protection générale. **Étendre le délestage au waterfall est une +décision produit** — si elle est prise, l'app aura besoin d'un ordre de délestage, qui n'est +pas l'ordre de service : le premier retire de la puissance en surcharge, le second en +répartit en surplus. Deux listes qui peuvent légitimement différer. diff --git a/docs/mockups/charges_pilotables_mockup.html b/docs/mockups/charges_pilotables_mockup.html new file mode 100644 index 0000000..f3eff78 --- /dev/null +++ b/docs/mockups/charges_pilotables_mockup.html @@ -0,0 +1,317 @@ + + +
+ + +Décision du 2026-08-25 : l'écran « Ordre de service des charges » disparaît. Le rang se règle + ici au glisser-déposer, le regroupement à deux niveaux est conservé (domaines ordonnés entre eux, + charges ordonnées dedans), et le bloc de télémétrie descend sur la carte de chaque charge.
+Les six écrans « Configurer » qu'ouvre le bouton de chaque carte sont dans
+ configurer_mecanismes_mockup.html.
Le changement de fond n'est pas visuel : ces cartes doivent être les charges lues par
+ NymeaEnergy.GetLoadConfig, pas des LoadDescriptor reconstruits depuis des
+ EmsRole. C'est la cause racine du défaut constaté sur appareil — l'écran actuel affichait
+ « 6/6 rôles configurés · tout est assigné » en pointant sur des appareils absents de la box, pendant que
+ celle-ci arbitrait deux charges qui n'apparaissaient nulle part.
priority à plat —
+ la box ne sait rien des domaines, le rang de domaine se déduit de l'ordre d'apparition. Le toggle écrit
+ enabled.
+ GetLoadConfig absente de
+ loads[] se lit désactivée, jamais perdue. L'inclusion ne vaut que dans un sens.available: false, et son résultat n'est
+ jamais annoncé sur le retour du RPC : mesuré sur le banc, ClearLoadFault répond
+ EnergyErrorNoError, lève réellement le verrou… et le cycle suivant le repose si la cause
+ demeure. Le verdict vient de la télémétrie relue, deux cycles plus tard.Le bouton Configurer ne peut pas être un formulaire unique. SG-Ready est une
+ LoadConfig comme le routeur de relais ; la borne de recharge et les zones de climatisation
+ n'en sont pas du tout — elles vivent sur d'autres namespaces, avec d'autres champs et d'autres tiers.
Deux écarts relevés en introspectant .75 (plugin 1.15.2+etm15) :
+ ① le ChargingInfo réellement déployé ne porte ni mode, ni minCurrent,
+ ni targetSoc, ni endTime — les noms écrits dans CLAUDE.md ne sont pas
+ ceux de la box. ② AirConditioning expose 8 méthodes, pas 3.
relays[] vide · un powerW ≤ 0 · un même Thing sur deux lignes ·
+ un mode autre que fixed. Le motif n'apparaît qu'au journal de la box —
+ le formulaire doit donc valider avant d'envoyer.
+ Aucun champ n'existe côté box. SetLoadConfig n'accepte que
+ adapter · enabled · id · label · mode · priority · domain · maxPowerW · minOffS · minOnS ·
+ needs · powerLevels · relays · sgReady. nymea rejette toute clé inconnue avant le
+ handler : inventer meterThingId ferait échouer l'enregistrement de toutes les
+ charges. Ici le besoin est le plus fort des trois
+ mécanismes : la box estime la consommation depuis les paliers déclarés, elle ne la
+ mesure pas.
mode doit valoir fixed — la box refuse toute autre valeur sur
+ ce mécanisme. Le champ n'a donc pas à être proposé : il est déterminé par le choix du mécanisme.sameHardware() : reconstruction de l'adaptateur
+ et réarmement des verrous à froid — contrairement à label, domain,
+ priority et enabled, qui passent en mise à jour sur place..75 : trois relais 500 / 1 000 / 2 000 W, huit paliers,
+ maxStageW: 3 500 — la télémétrie l'a confirmé pendant le passage sur appareil.Aucun champ n'existe côté box. SetLoadConfig n'accepte que
+ adapter · enabled · id · label · mode · priority · domain · maxPowerW · minOffS · minOnS ·
+ needs · powerLevels · relays · sgReady, et nymea rejette toute clé inconnue avant
+ le handler. Ici la question se double : l'interface
+ remonte déjà la puissance réelle de l'appareil — un compteur externe mesurerait-il autre
+ chose ?
L'app ne propose plus ce mode — le plugin lui-même le qualifie de legacy — mais
+ elle ne le réécrit pas en douce : basculer une charge de fixed vers
+ dynamic change l'arbitrage réel, et cela demande un geste explicite.
+ La conversion exige aussi une
+ puissance nominale, que la déclaration en paliers ne porte pas : la box refuse
+ dynamic sans maxPowerW, et refuse powerLevels avec.
+
etmvariableload : un seul appareil, une consigne en watts, aucun palier.
+ relays[] y est interdit — la box refuse en bloc.
+ fixed existe encore côté box — « multipalier natif, legacy »
+ dans les mots du plugin. L'app doit savoir le lire sans le proposer : une config déployée
+ qui l'utilise ne doit pas être convertie par le simple fait d'ouvrir l'écran.sgReady.states[] ne stocke qu'une liste de relais par état, sans nom ni ordre.
+ Les deux étiquettes sont une convention de cet écran, et elles se retrouvent à la lecture :
+ SG1 = le contact fermé en état 1 (blocage), SG2 = celui fermé en état 3
+ (recommandation). C'est l'encodage SG-Ready standard, et c'est celui du banc.
+ UNUSABLE_ENCODING et la charge sort de
+ l'arbitrage définitivement (ECS-110). Le formulaire doit le refuser avant l'envoi.
+ estimatedPowerW sert de base au recrédit de budget et au choix d'état : une
+ valeur fausse ne décale pas seulement la PAC, elle décale l'allocation de tout le waterfall en
+ aval. À relever sur la plaque signalétique, puis à mesurer sous les deux régimes.
+ Aucun champ n'existe côté box. SetLoadConfig n'accepte que adapter · enabled · id · label · mode · priority · domain · maxPowerW · minOffS · minOnS · needs · powerLevels · relays · sgReady. nymea rejette toute clé inconnue avant le handler : inventer meterThingId ferait échouer l'enregistrement de toutes les charges.
LoadConfig, elle est arbitrée par le
+ waterfall, elle apparaît dans GetLoadTelemetry, elle a un rang et un domaine.
+ sameHardware() :
+ reconstruction de l'adaptateur et réarmement des verrous à froid.relays[] à
+ l'enregistrement : la liste des contacts fermés pour cet état.ChargingModeEco · Min+PV → ChargingModeEcoWithMinCurrent ·
+ Boost → ChargingModeNormalChargingModeEcoWithTargetTime /
+ ChargingModeEcoMinWithTargetTime
+ chargingMode — d'où l'importance de ne pas laisser croire
+ que ce bouton agit à l'instant.Le mode existe, le réglage n'existe pas. ChargingModeEcoWithMinCurrent est
+ bien dans l'énumération de la box, mais ChargingInfo n'a aucun champ de courant
+ minimum — ni minCurrent, ni équivalent. Soit la valeur est fixée ailleurs (paramètre
+ du Thing borne), soit le champ manque. À trancher avec l'agent plugin avant de livrer ce
+ mode : proposer un réglage qui ne part nulle part serait pire que de ne pas le proposer.
Aucun champ n'existe côté box. ChargingInfo ne porte que chargingMode · evChargerId · assignedCarId · dailySpotMarketPercentage · endDateTime · repeatDays · spotMarketChargingEnabled · targetPercentage, plus chargingState en lecture. Rien pour un compteur — et la borne mesure déjà, ce qui pose la question de quelle mesure fait foi.
LoadConfig. Le véhicule n'a ni adapter, ni
+ priority, ni domain — et il n'apparaît pas dans
+ GetLoadTelemetry en beta : il est décidé en amont du waterfall, et sa puissance est
+ seulement réservée dans budget.evReservedW. Il rejoindra loads[] à
+ l'étape 3g.
+ CLAUDE.md sont périmés. La box déploie
+ chargingMode (pas mode), targetPercentage (pas
+ targetSoc), endDateTime en Uint (pas endTime), plus
+ assignedCarId, repeatDays et deux champs spot market. À corriger dans
+ CLAUDE.md.NymeaEnergy.*, pas EnergyPlugin.*.Aucun champ n'existe côté box. ZoneInfo rattache des indoorSensors et outdoorSensors — des sondes de température, pas de consommation. Un compteur de zone est un champ à ajouter, et il faudra dire s'il mesure la zone ou l'appareil qui la chauffe.
TemperatureWeekSchedule reste
+ à introspecter, le type n'est pas détaillé dans le schéma de cette box.AirConditioning.GetZones renvoie zones: [] sur .75.
+ L'état vide ci-dessous est donc le premier écran que verra un installateur.
+ Une zone regroupe des thermostats, des vannes et des sondes sous une même consigne. + Sans zone, la climatisation n'est pas pilotée par l'énergie.
+ +LoadConfig, ni ChargingInfo. Une zone n'a pas de
+ rang de service ni de domaine — c'est un regroupement de Things sous une consigne de température.
+ CLAUDE.md : s'ajoutent
+ AddZone, RemoveZone, SetZoneName,
+ SetZoneThings et SetZoneStandbySetpoint. Le CRUD complet est donc
+ possible depuis l'app.ZoneInfo est
+ r: — on écrit par les six méthodes Set*, jamais par un objet complet.ZoneInfo ne relie une zone à une
+ LoadConfig. La PAC SG-Ready et les zones vivent côte à côte sans se connaître — si
+ l'ordre de service doit tenir compte des zones, c'est une question de conception, pas de câblage.SetZoneThings. On
+ n'envoie que celles qu'on a touchées — les autres sont optionnelles et restent en place.SetZoneThings prend une liste d'UUID sans contrainte
+ d'unicité déclarée. Tant que ce n'est pas éprouvé sur la box, l'app signale l'appartenance
+ existante sans bloquer, et ne prétend pas connaître la règle.
+ .75 ne portent aucune interface thermostat, vanne ou
+ sonde de température — il n'y a pas de matériel HVAC dessus. Les appareils ci-dessus sont
+ fictifs : cet écran n'est pas éprouvable en l'état, et la première vérification à faire est
+ d'y brancher un thermostat réel.
+ Le filtre, lui, existe déjà. .75 porte les interfaces
+ energymeter, smartmeter, smartmeterconsumer et
+ smartmeterproducer : le jour où le champ est ajouté, l'app saura proposer les bons
+ appareils sans table maintenue à la main. C'est le champ de destination qui manque, pas la
+ façon de choisir.
ThingClass.interfaces est une
+ liste en lecture sur chaque classe ; s'en servir évite la table de correspondance codée en dur que
+ le brief interdit — et qui vieillirait à chaque plugin ajouté..75 : sur 784 chaînes,
+ le français en traduit zéro. Les displayName sont des identifiants anglais, à
+ afficher tels quels.Le réglage existe déjà côté box : NymeaEnergy.SetPhasePowerLimit. Sa place naturelle est
+ bien le compteur réseau, puisqu'il n'a de sens qu'avec lui — la protection est inopérante sans
+ compteur racine.
Trois pièges relevés en lisant la box et le plugin : ① le champ s'appelle « power » mais son unité + est l'ampère par phase ; ② 0 = désactivé, et cela coupe aussi toute la recharge intelligente ; + ③ la protection ne déleste que le véhicule — aucune charge du waterfall n'est réduite par elle.
+currentPowerPhaseA/B/C sur ce Thing. Sans compteur réseau, la box journalise
+ « overload protection is not available » et ne fait rien.
+ currentPowerPhaseA/B/C du Thing compteur, divisés par la tension. Les afficher ici rend la
+ limite concrète — l'installateur voit tout de suite laquelle de ses phases approche.
+ rootMeter.phasePowerLimit mais la box attend un courant par phase — sa propre description le
+ dit : « maximum allowed Ampere per phase ». Un formulaire en kVA se tromperait de grandeur..75, et c'est pour ça que son journal répète
+ « overload protection is disabled ». Un champ vide ne doit jamais être enregistré comme 0
+ par inadvertance.
+ m_evChargers) et réduit leur courant, jusqu'à couper si le minimum n'est plus
+ tenable. Les charges du waterfall — routeur de relais, modulable, SG-Ready — ne sont
+ pas réduites par cette protection.
+ Étendre le délestage à ces charges est une question
+ côté plugin, pas côté app. Tant qu'elle n'est pas tranchée, l'écran ne doit pas laisser
+ croire que le raccordement est protégé dans son ensemble.
+ À ne pas confondre avec l'ordre de service. L'ordre de « Charges pilotables » répartit + un surplus ; un ordre de délestage retirerait de la puissance en surcharge. Ce + sont deux listes qui peuvent différer — et la seconde n'a aujourd'hui aucun support côté box. + À spécifier avec l'agent plugin si le délestage doit dépasser le véhicule.
+SetPhasePowerLimit, SetAcquisitionTolerance,
+ SetBatteryLevelConsideration, SetLockOnUnplug.
+ .75 le 2026-08-25 : phasePowerLimit: 0 (non
+ réglé), acquisitionTolerance: 0.5, batteryLevelConsideration: 0.9.