É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
174 lines
8.6 KiB
Markdown
174 lines
8.6 KiB
Markdown
# 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.
|