# 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. ```json { "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 `meterThingId` cô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.