Émis après le lot C, tout constaté sur .75 (plugin 1.15.2+etm15), rien corrigé côté moteur. L'app s'est arrangée pour ne pas mentir en attendant. 1. Les compteurs cumulés d'Energy.GetPowerBalance sont CORROMPUS : totalReturn 9,1e+33, totalAcquisition 7,5e+33, totalConsumption -1,6e+33 — alors que totalProduction est sain et que les états du Thing compteur sont tous plausibles (1429 / 87,9 kWh). Ce n'est donc pas le compteur qui ment, c'est l'accumulation au-dessus. 2. Renommer une charge ne parvient jamais à l'adaptateur : updateSoftConfig() ne porte que priority et needs, donc le journal de la box garde l'ancien libellé jusqu'à la prochaine reconstruction — trompeur en mise en service. 3. relays[] est comparé PAR INDEX dans sameHardware() : un simple réordonnancement reconstruit la charge (contacts ouverts, verrous réarmés). Question de conception ouverte — le routeur dérive toutes les combinaisons, l'ordre ne change rien au routage. 4. Les paliers dérivés ne sont publiés nulle part (powerLevels vide, télémétrie limitée à maxStageW/stageW). L'app les dérive pour l'affichage : deux implémentations de la même combinatoire, et c'est l'app qui aura tort si elles divergent. 5. minCurrent n'existe dans aucun champ de ChargingInfo alors que ChargingModeEcoWithMinCurrent est publié : le mode se choisit, son paramètre non. 6. Pas de compteur dédié par charge. L'app affiche le bloc grisé, dérivé du schéma — il s'activera seul le jour où le champ existera. 7. L'avertissement LM-200 attend typeAppareil : keyé sur domain il crierait sur des installations correctes. Plus la liste de ce sur quoi l'app s'appuie désormais et qu'il ne faut pas casser. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
8.6 KiB
Brief agent plugin — ce que le lot C (app) a trouvé côté moteur
Émis par : l'agent app, après le lot C · Date : 2026-08-25
Banc : .75, plugin 1.15.2+etm15, nymea 1.15.2+202606191336~trixie1
Contexte : docs/RELEVE_LOTC.md (dépôt app) porte le détail et les traces.
Rien ici n'a été corrigé côté moteur. Tout a été constaté sur la machine, et l'app s'est arrangée pour ne pas mentir en attendant. Les sept points sont classés par ce qu'ils coûtent aujourd'hui, pas par difficulté.
1. ⚠️ Les compteurs cumulés d'Energy.GetPowerBalance sont corrompus
Le plus visible, et le plus gênant : le tableau de bord affichait 9,1 × 10³³ kWh exportés.
{ "totalAcquisition": 7.534470527909902e+33,
"totalConsumption": -1.5673735661234482e+33,
"totalReturn": 9.10184409403335e+33,
"totalProduction": 62007.5439453125 }
Trois des quatre compteurs sont absurdes ; totalProduction est sain. Et les états du
Thing compteur (Fronius Three Phase Meter) sont tous plausibles — 1 429 kWh importés,
87,9 kWh exportés. Ce n'est donc pas le compteur qui ment : c'est l'accumulation faite
au-dessus.
Piste : une intégration puissance × Δt où le Δt part en vrille (saut d'horloge, premier
échantillon, reprise après redémarrage). Le fait que totalProduction survive alors que les
trois autres tombent mérite d'être expliqué — ils ne passent probablement pas par le même
chemin.
Ce que l'app fait en attendant : un plafond de plausibilité (10 GWh, ~3 000 ans de consommation) au-delà duquel la tuile affiche « — · compteur box invalide ». Elle ne corrige rien et n'invente rien. Elle redeviendra juste toute seule dès que la box publiera des valeurs saines.
2. Renommer une charge ne parvient jamais à l'adaptateur
label n'est pas dans sameHardware(), ce qui est correct. Mais la mise à jour sur place
(updateSoftConfig()) ne porte que priority et needs. Un changement de libellé seul donne
donc :
[EnergyArbitrator] 2 charge(s) active(s) — 0 créée(s), 0 mise(s) à jour, 2 inchangée(s)
L'adaptateur est réutilisé tel quel, avec son ancien libellé. GetLoadConfig rend bien le
nouveau — c'est ce que l'app affiche — mais le journal de la box continue d'imprimer
l'ancien jusqu'à la prochaine reconstruction.
Pour qui suit journalctl pendant une mise en service, c'est trompeur : on renomme
« chauffe-eau » en « Ballon 200 L », et le journal parle encore de « chauffe-eau ».
Correctif pressenti : porter label dans updateSoftConfig(). Il n'entre pas dans
sameHardware() pour autant — c'est une mise à jour sur place, pas une reconstruction.
3. relays[] est comparé PAR INDEX — un réordonnancement reconstruit la charge
Vérifié sur .75 : relays[] inversé (2000/1000/500 au lieu de 500/1000/2000), même
ensemble, mêmes Things, mêmes puissances :
[RelayRouter] chauffe-eau — reprise au démarrage : palier 0 W [correspondance exacte] …
[EnergyArbitrator] relay-router construit depuis config: "chauffe-eau" ( 3 relais)
[RelayRouter] "chauffe-eau" → … (force) | "Charge désactivée — mise en état sûr … (ECS-413)"
[EnergyArbitrator] 2 charge(s) active(s) — 1 créée(s), 1 inchangée(s), 1 retirée(s)
Contacts ouverts, verrous réarmés à froid. Question ouverte, et c'est une décision de
conception, pas un bug évident : le routeur dérive ses paliers de toutes les combinaisons
de relais — l'ordre de la liste ne change donc rien au routage. Faut-il comparer relays[]
comme un ensemble plutôt qu'index par index ?
- Pour : un installateur qui remonte une ligne dans l'app ne s'attend pas à couper sa charge. Et annuler son geste coûte une seconde reconstruction.
- Contre : si l'ordre porte un sens quelque part (priorité d'enclenchement, choix de la combinaison à somme égale), le comparer comme un ensemble masquerait un vrai changement.
À trancher côté moteur. L'app annonce l'impact avant d'écrire, donc elle est correcte dans les deux cas — mais elle annonce aujourd'hui une reconstruction pour un geste qui n'en mérite peut-être pas une.
4. Les paliers atteignables ne sont publiés nulle part
GetLoadConfig rend "powerLevels": [] sur une charge relay-router à trois contacteurs, et
GetLoadTelemetry n'expose que maxStageW et le stageW courant. La liste des paliers
dérivés n'existe sur aucune frontière.
L'app les dérive pour l'affichage (2^N combinaisons, doublons écartés, plafond 16 relais), conformément à la maquette, et ne les enregistre jamais. C'est donc une seconde implémentation de la même combinatoire, et si les deux divergent, c'est l'app qui aura tort sans le savoir.
Question : faut-il que la box publie les paliers dérivés — dans GetLoadConfig en lecture
seule, ou dans la charge utile de mécanisme de la télémétrie ? Le champ powerLevels existe
déjà au schéma ; il est simplement vide sur ce mécanisme.
5. minCurrent n'existe dans aucun champ de ChargingInfo
L'énumération publie ChargingModeEcoWithMinCurrent et ChargingModeEcoMinWithTargetTime,
mais ChargingInfo ne porte aucun champ de courant :
chargingMode · evChargerId · assignedCarId · dailySpotMarketPercentage ·
endDateTime · repeatDays · spotMarketChargingEnabled · targetPercentage (+ r:chargingState)
Le mode se choisit, son paramètre non. Soit la valeur vit ailleurs (paramètre du Thing borne), soit le champ manque.
L'app grise le réglage et le dit — proposer un curseur qui ne part nulle part serait pire que de ne pas le proposer. Mais le mode reste sélectionnable, donc la borne applique un courant minimum que personne ne sait régler ni lire.
6. Pas de compteur dédié par charge
SetLoadConfig n'accepte que adapter · enabled · id · label · mode · priority · domain · maxPowerW · minOffS · minOnS · needs · powerLevels · relays · sgReady.
L'app affiche un bloc « Compteur dédié — prévu, pas encore supporté », grisé, avec sa
raison. Le grisage est dérivé du schéma (JSONRPC.Introspect), donc il s'activera seul le
jour où le champ existera, sans nouvelle version de l'app.
Le besoin est le plus fort sur relay-router : la box estime la consommation depuis les
paliers déclarés, elle ne la mesure pas. Sur etmvariableload la question se double — l'interface
remonte déjà la puissance réelle.
Rappel de contrainte, pas d'inquiétude : nymea rejette toute clé inconnue avant le handler, et le refus porte sur l'appel entier. Inventer
meterThingIdcôté app ferait échouer l'enregistrement de toutes les charges. L'app ne le fera pas.
7. L'avertissement LM-200 attend typeAppareil
Décidé côté modèle : une PAC ne prend ni relay-router ni etmvariableload, et le moteur doit
avertir au journal sans refuser. Mais l'avertissement n'a pas de prédicat aujourd'hui —
domain décrit ce qu'on arbitre, pas la machine, et keyé dessus il crierait sur des
installations correctes (heating + relay-router = chauffage résistif ; hvac +
relay-router = VMC).
Le discriminant est typeAppareil (specs/spec_loadmodel.md §3 LM-300, §8-1), valeur
thermodynamique. Spécifié, non implémenté — voir LM-200.
Ce sur quoi l'app s'appuie désormais, et qu'il ne faut pas casser
| Comportement | Où l'app en dépend |
|---|---|
LoadConfigChanged arrive avant l'accusé de SetLoadConfig |
confirmation d'enregistrement (mesuré : 4 écritures sur 4) |
GetLoadTelemetry rend le dernier cycle publié, sans recalcul |
l'app n'attend pas de disparition immédiate après enabled: false |
enabled: false fait passer la charge par applySafeState() |
l'infobulle annonce « contacts ouverts » — vérifié PAC en état 4 → contacts ouverts |
Refus en bloc et sans motif de SetLoadConfig |
l'app valide avant d'envoyer (relays vides, powerW ≤ 0, Thing en double, état 2 absent, maxPowerW ≤ 0) |
JSONRPC.Introspect décrit SetLoadConfig jusqu'au champ |
tout le grisage en dérive |
loads[] ⊆ GetLoadConfig |
« non arbitrée » se lit désactivée, jamais perdue |
Et une chose qui n'est PAS sur le fil : la version du paquet (1.15.2+etm15) n'est exposée
par aucune méthode. L'app annonce la version d'API d'expérience (NymeaEnergy 0.8), qui
décrit le contrat. Si un jour la version de paquet devient utile côté client, il faudra la
publier — mais l'API est sans doute le bon discriminant de toute façon.