etm-powersync-app/docs/BRIEF_plugin_depuis_maquettes.md
Patrick Schurig 054cbfab3c docs: maquettes de la refonte « Charges pilotables » + CLAUDE.md recalé sur la box
Trois maquettes, neuf écrans, après le passage sur appareil qui a montré que
/energy/setup décrit autre chose que l'installation.

- charges_pilotables_mockup.html : la liste à deux niveaux, avec le bandeau
  d'arbitrage hérité de /energy/loads (qui disparaît) et la télémétrie sur la carte
  de chaque charge.
- configurer_mecanismes_mockup.html : les six « Configurer » — routeur de relais,
  modulable, SG-Ready, borne, zones HVAC, sélecteur d'appareils.
- raccordement_reseau_mockup.html : compteur réseau et protection de surcharge.

Décisions de conception que les images ne portent pas seules : les paliers
n'existent que chez le routeur de relais et y sont DÉDUITS des combinaisons ; le
mode fixed d'etmvariableload (legacy) n'est plus proposé mais reste lisible, parce
qu'ouvrir un écran ne doit pas convertir une config déployée ; SG1/SG2 sont une
convention d'app retrouvée par l'encodage standard, la box ne stocke qu'une liste
de relais par état ; le compteur par charge est dessiné inactif avec sa raison ; la
borne règle un mode par DÉFAUT, pas une commande.

CLAUDE.md corrigé contre JSONRPC.Introspect (plugin 1.15.2+etm15) :

- Energy.SetChargingMode n'existe pas — le « bug critique » décrivait un appel
  impossible. Energy.* n'a que cinq méthodes.
- Le namespace EnergyPlugin.* n'existe pas : c'est NymeaEnergy.*, 20 méthodes et
  14 notifications, toutes listées.
- ChargingInfo : chargingMode / targetPercentage / endDateTime (Uint), et non
  mode / targetSoc / endTime. minCurrent n'existe nulle part.
- phasePowerLimit est en AMPÈRES par phase, et 0 coupe toute la recharge
  intelligente.
- AirConditioning expose 8 méthodes, pas 3.

Toutes les méthodes citées ont été revérifiées une par une : aucune introuvable.
Règle ajoutée : contrôler ce fichier contre Introspect avant chaque lot — trois
fois aujourd'hui il a envoyé un agent dans le vide.

Et docs/BRIEF_plugin_depuis_maquettes.md : les six points que les maquettes
remontent vers le moteur, dont un seul blocage dur (pas de champ de courant
minimum pour le mode Min+PV).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
2026-08-25 21:30:32 +02:00

9.4 KiB

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 :

{"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 :

{"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 :

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.