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.
Ajout du 2026-08-27 — la batterie, septième écran. Elle devient une charge du waterfall, donc
son bouton Configurer ouvre un écran de mécanisme comme les autres — et, comme les autres,
il ne porte pas le rang : celui-ci se règle par glisser-déposer dans la liste. Défaut : la
batterie en première position de charge, ce qui reproduit le comportement actuel. Rien de ce qui suit
n'existe côté box : ni BatteryAdapter, ni entrée LoadConfig pour le
stockage, ni prévision PV. Cette colonne est un brief pour le moteur, comme les six autres
l'ont été — et les seuls leviers réels de .75 sont les cinq actions de la classe
SunSpec Storage : enableCharging, enableDischarging,
chargingRate, dischargingRate, gridCharging, plus l'état
controlHealthy. Tout le reste est à créer.
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.
power — taper un UUID au doigt est une erreur
garantie. Il résout nom → ThingId et n'ajoute aucun champ au contrat.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.batteryLevel · capacity ·
currentPower · storageStatus — publiés par le Thing, jamais
recalculés ici.enableDischarging est binaire, et l'onduleur ne sait pas qui tire — il voit
une consommation nette. Coupé, la maison n'est plus servie non plus : le réfrigérateur passe au
réseau comme la voiture.dischargingRate sur la consommation hors borne, recalculé à chaque
cycle — de la modulation continue, pas un interrupteur. Posé comme question au
moteur, pas dessiné ici.chargingRate dans cet écran, et il
a le même motif que le boost, à l'envers : ne pas dépasser ce qu'on a le droit de
soutirer. Charger 6 kWh en pleine nuit à pleine puissance sur un abonnement 6 kVA ferait
sauter le disjoncteur au premier four allumé.currentPower elle-même. Le bloc est dit ici plutôt qu'omis en
silence comme dans les cinq autres écrans — une absence expliquée n'est pas un oubli.GetLoadConfig, et
enableCharging / chargingRate / enableDischarging ont
zéro occurrence dans energyplugin/. Le levier matériel, lui, est déclaré : la
classe SunSpec Storage de .75 publie ces cinq actions.
docs/BRIEF_plugin_depuis_maquettes.md — pas ici : ce sont des détails
techniques, pas de l'interface.batteryLevelConsideration fait aujourd'hui l'INVERSE de ce que le rang
permettra. Sous le seuil, le budget est annulé — la batterie passe donc avant, et
l'ECS ne reçoit rien. Si le rang arrive, il faut dire lequel des deux gagne, sinon deux réglages
se défont l'un l'autre en silence.enableDischarging + dischargingRate — déjà déclarés par la
classe. Le dimensionnement de la marge, la latence de l'onduleur et le rôle de filet de la borne
sont des questions moteur, écrites dans le brief, pas dessinées ici.Trois fonctionnalités demandées à des moments différents ont exactement la même forme. Si la maquette le montre comme un motif unique, le moteur peut l'implémenter une fois au lieu de trois — et l'écran peut le présenter une fois au lieu de trois.
Une cible, une échéance, un point de non-retour — et cela vaut que les paramètres soient saisis ou calculés. C'est ce qui rend le motif implémentable une fois : le moteur porte la forme, Héos ou l'utilisateur la remplit. Le point de non-retour est la partie qui se perd si on ne la nomme pas, et c'est lui qui garantit qu'une prévision fausse ne coûte rien.
enableCharging = false. C'est une différence de nature
avec toutes les autres charges du waterfall, où « 0 W alloué » suffit à ce qu'il ne se passe
rien.enableCharging = false laisse une batterie inerte, sans que rien ne le signale et
sans que personne ne sache pourquoi. Il faut un bail à durée limitée, renouvelé tant que
l'arbitre vit — l'état sûr d'un stockage, c'est « il fait ce qu'il ferait sans nous », pas
« il reste sur le dernier ordre reçu ».controlHealthy == false doit SORTIR la batterie de l'arbitrage, comme
available == false sort une charge en défaut. L'état existe déjà sur la classe. Une
batterie qu'on croit piloter et qui ne répond plus fausse le budget en silence, exactement comme
le faisait le double commandeur des bornes avant 3g-2.