L'arrivée de la V2C sur .75 a révélé deux défauts que le banc ne pouvait pas montrer avant,
plus deux pièges de contrat à documenter.
1. ⚠️ isNotGrouped ÉTAIT CALCULÉ ET AFFICHÉ NULLE PART. GroupedOrder détecte depuis le
premier jour qu'un ordre n'est pas groupé par domaine, le provider l'expose, et aucune
vue ne le lisait — le cas ne s'était jamais produit, chaque domaine du banc ayant ses
charges d'un seul tenant.
Il se produit maintenant : la V2C, créée d'office au rang 1 que le chauffe-eau occupait
déjà, donne l'ordre à plat « ecs · ev · heating · ev · ev ». Le domaine ev est coupé en
deux par le chauffage.
Ce n'était pas un détail d'affichage. L'écran groupe pour afficher, donc ce qu'on voit
n'est plus l'ordre réel de la box — et le premier enregistrement réaplatit EN GROUPANT,
ce qui change l'arbitrage sans que personne ne l'ait demandé. Chiffré par test sur la
configuration réelle : la PAC passerait du rang 2 au rang 5. C'est exactement ce que le
commentaire de GroupedOrder interdit depuis le début ; le garde-fou existait, il n'était
pas branché.
2. « L'APPAREIL N'EXISTE PLUS » N'EST PAS UN DÉFAUT À LEVER. La Terra AC a été supprimée de
nymea ; son entrée LoadConfig survit, occupe le rang 4, et s'affiche en THING_MISSING
avec un bouton « Lever le défaut ». ClearLoadFault y répondrait EnergyErrorNoError sans
rien changer, et le défaut reviendrait au cycle suivant — un bouton qui ne peut pas
tenir sa promesse fait douter de la box au lieu de désigner la cause.
Le bouton disparaît sur ce code, remplacé par ce qu'il faut faire : réinstaller
l'appareil, ou retirer la charge. Et le libellé cesse de se lire « votre configuration
est cassée » : le cas normal est un appareil remplacé, le plugin crée d'office une
entrée par borne détectée mais ne la retire pas quand le Thing s'en va.
3. Deux pièges du Thing V2C, documentés là où quelqu'un serait tenté de les câbler :
chargeEnergy est EXACT pendant la charge — sa croissance recoupe currentPower à mieux
d'un pour cent — et REMIS À ZÉRO à l'arrêt de la charge, pas au débranchement. Mesuré :
0,3456 kWh → 0 dès que charging passe à faux, 119 secondes avant que le câble ne bouge.
Sur du pilotage par surplus il repartirait de zéro à chaque nuage, et après coup une
nuit entière vaudrait 0 — soit « rien livré » affiché là où la vérité est « pas
mesurable ». Le plugin publiera un sessionEnergy construit ; rien n'est bâti en
attendant, et surtout aucune accumulation côté app.
powerL1/L2/L3 sont des ampères mal nommés, renommage currentL* en cours côté plugin.
Ni affichage, ni conversion : une conversion écrite ici survivrait au correctif et le
contredirait.
Fixture ajoutée : la configuration réelle à cinq entrées, avec ses deux enseignements de
terrain — deux rangs à 1, et une entrée orpheline qui consomme un rang.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
Trois changements demandés après le passage sur téléphone.
1. L'ARBITRAGE EN DIRECT PASSE TOUT EN BAS. Il était en tête avec cet argument : « Alloué
3 000 W ne veut rien dire tant qu'on ne sait pas s'il y avait 3 100 ou 12 000 W à
distribuer ». L'argument reste juste pour LIRE une allocation — mais cet écran sert à
CONFIGURER, et faire descendre l'installateur sous un pavé de télémétrie à chaque
ouverture met la lecture devant le geste.
La note de rangs ex æquo, elle, RESTE EN TÊTE : elle prévient que l'ordre affiché juste
en dessous n'en est pas un, et la lire après les cartes arriverait trop tard.
2. LES DEUX FLÈCHES DE DOMAINE DEVIENNENT UN CHEVRON DE REPLI. Elles faisaient doublon
avec la poignée de glissement, désormais vérifiée au doigt sur appareil (48 px,
réordonnancement mesuré, 0 px de défilement parasite). Deux chemins pour le même geste,
dont l'un sans repli, valaient moins qu'un chemin sûr plus une commande qui manquait.
Replié, le bloc dit COMBIEN de charges il contient : sinon replier reviendrait à faire
disparaître des charges, et une charge qu'on ne voit plus se lit comme une charge
perdue. Le repli est clé par CODE de domaine, pas par index — un domaine déplacé emporte
son repli au lieu de replier son voisin.
3. LA CARTE RÉSERVE BATTERIE SE REPLIE AUSSI, avec une réserve qui n'est pas cosmétique :
repliée, elle garde le pourcentage ET l'alerte de seuil inatteignable. Replier abrège,
ça ne fait pas taire — c'est le réglage qui peut geler toute l'installation sans erreur
ni panne, et le cacher ramènerait le paramètre d'usine invisible que cette carte existe
pour supprimer. Jamais repliée d'office.
Aucun repli n'est persisté, délibérément : c'est une commodité de lecture, pas une
configuration. La rendre durable ferait retrouver un écran à moitié caché des semaines plus
tard, sans savoir pourquoi.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
Le test d'appareil passe. Il a trouvé deux défauts réels que 129 tests unitaires ne
pouvaient pas voir, parce qu'aucun ne rend ces widgets à une taille d'écran réelle.
1. LA POIGNÉE DE GLISSEMENT MESURAIT 26 × 27 px, pour un minimum Material de 48. Le
Container transparent posé lors de l'enquête précédente avait supprimé la bande morte
entre l'icône et le reste, mais il n'avait jamais AGRANDI la cible : le geste accrochait
quand on visait juste et ratait sinon. C'est le seul geste de cet écran sans repli —
sans lui, l'ordre des domaines ne se change pas du tout.
2. LES FLÈCHES DE RANG MESURAIENT 18 × 18 px, la taille de l'icône, sans aucune marge de
touche. Elles n'ont pas de repli non plus : réordonner DANS un domaine ne se fait que
par elles.
Portées à 48 et 44. Et la capture d'appareil a montré le coût de la correction naïve :
empilées, deux cibles de 44 font 88 px de haut pour deux icônes de 18, et la colonne de
rang devient une colonne vide. Côte à côte, la hauteur retombe à 44 et la largeur ne coûte
rien au libellé. La cible tactile est la contrainte ; la disposition ne l'est pas.
Le harnais a lui-même demandé trois corrections avant de dire la vérité, et chacune est un
piège qui se reproduira :
- il regardait le HAUT de l'écran. Dans un ListView, ce qui n'est pas visible n'est pas
construit : les cibles n'étaient pas mesurables faute d'être montées.
- `find` trouve un widget POSÉ MAIS HORS VIEWPORT, et getCenter en rend une coordonnée qui
n'existe pas — mesuré y=1129 sur un écran de 825 px. Le geste partait dans le vide, avec
un symptôme identique à un glissement ignoré.
- la course était devinée. Une liste réordonnable ne permute qu'une fois passé le milieu du
suivant, et un bloc de domaine porte ses cartes : 384 px mesurés, contre les ~150 devinés.
Elle se calcule maintenant depuis l'écart réel entre deux poignées.
Le test mesure aussi le défilement de la page pendant le glissement : si le ListView
extérieur gagnait l'arène, le symptôme serait le même que celui d'une poignée qui
n'accroche pas, pour une cause opposée. Mesuré à 0 px — c'est bien la poignée qui prend.
Aucun débordement de mise en page sur cet écran. Deux captures d'appareil jointes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
+etm23 rend à chaque borne détectée une entrée GetLoadConfig, et l'invariant
loads[] ⊆ GetLoadConfig est rétabli. Sur .75 : 2 entrées → 4. L'app s'appuyait déjà sur cet
invariant pour résoudre libellé, domaine et rang de toute charge publiée.
Vérifié sur machine avant d'écrire quoi que ce soit :
- l'aller-retour verbatim reste NEUTRE avec les quatre entrées (loadconfig_roundtrip.dart,
verdict IDENTIQUE) ;
- une entrée evcharger porte id/label/domain/priority/enabled, mode dynamic, et AUCUNE
charge utile de mécanisme.
Trois défauts que ce contrat révèle côté app :
1. isVariableLoad était défini par défaut (« ni relais ni sg-ready »), donc une borne y
tombait. L'écran de mécanisme lui proposait une puissance nominale, des paliers et des
temporisations — des réglages qui n'existent pas pour elle et dont l'écriture partirait
quand même. Pire, le sélecteur offrait de la basculer en routeur de relais : une bascule
que basculerMecanisme() sait construire et qu'aucun écran ne sait défaire. Une borne a
maintenant sa section, en lecture seule, qui dit pourquoi il n'y a rien à régler.
2. BorneSection existait pour rattraper des bornes qui n'apparaissaient nulle part. Elles
apparaissent maintenant dans leur domaine : la section les afficherait DEUX fois, en
affirmant qu'elles n'ont « ni rang ni domaine » et qu'elles ne passent pas par
SetLoadConfig. Trois affirmations devenues fausses. Retirée ; son contenu de runtime
vit désormais dans l'écran de mécanisme, où il a un sens.
3. Le rang par défaut d'une borne est un ARTEFACT, et l'écran le disait comme un réglage.
À la création le plugin donne « le plus petit rang existant moins un, borné à 1 » ;
le chauffe-eau occupant déjà le rang 1, l'intention « en tête » dégénère en égalité —
trois charges à 1 sur le banc. Le tri reste total (l'id départage), donc l'ordre est
reproductible, et c'est précisément ce qui le rend crédible : l'installateur lit un
classement plausible et n'y touche pas, validant un ordre que personne n'a posé. Une
note le nomme, et disparaît dès le premier vrai réglage.
Le glissement, lui, n'a demandé aucun cas particulier — c'était tout l'intérêt d'avoir une
seule liste. Vérifié par test, y compris l'inversion borne / charge pilotée.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vrTVifar2GN5rtUh89Wkq
MINIMUM. « Plancher » désigne désormais deux choses qu'il ne faut surtout pas ranger
ensemble :
- le plancher DÉCLARÉ d'une charge modulable — propriété de l'appareil, saisie une fois,
stable, avec un champ de configuration à venir ;
- le minimum d'une BORNE — max(borne, voiture), qui dépend du véhicule branché et change
avec lui. Il n'aura jamais de champ de configuration : il n'y a rien à y régler.
Les rendre modifiables de la même façon ferait croire qu'on peut régler ce qui se constate.
Le bloc « plancher » ne vit que dans la section etmvariableload, ce qui écarte les bornes
structurellement — elles ne sont pas des LoadConfig et n'ouvrent pas cet écran — et son
libellé le dit désormais explicitement.
PHASES. Le plancher effectif d'une borne dépend du nombre de phases visé : ~1,4 kW en
monophasé, ~4,1 kW en triphasé, pour le MÊME courant. La valeur est affichée telle que la
box la donne, sans conversion ni recalcul côté app. Si l'app convertissait, elle devrait
deviner laquelle des deux règles s'applique — et se tromperait un jour. C'est exactement le
miroir qui vient d'être retiré pour la combinatoire des paliers ; on ne va pas en
réintroduire un.
PARTAGE. Le budget ne se divise pas entre bornes : aujourd'hui chacune reçoit l'enveloppe
entière et ce sont le compteur puis la protection de surcharge qui rattrapent ; après 3g
une seule démarrera. Avec deux bornes maintenant déclarées sur le banc, le laisser
implicite invitait à déduire un partage qui n'existe ni avant ni après. Une note l'énonce
dès qu'il y en a plus d'une.
Le bloc de runtime d'une borne reste MUET tant que la recharge n'est pas arbitrée — la box
ne publie rien pour elle, et c'est la bonne réponse, pas un trou. Il affichera phases,
courant, minimum et présence du véhicule le jour où ils arriveront, en lecture seule.
Tests 105/105, analyze 0 erreur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
RELAIS : mon avertissement de reconstruction sur un réordonnancement est devenu FAUX, et
il est corrigé. `+etm16` compare la TABLE que les relais produisent (deriveStages), plus la
liste par index. Le miroir Dart suit, y compris le détail qui compte : les ThingIds d'une
combinaison se comparent comme un ENSEMBLE — collectés dans l'ordre de déclaration, fermer
{K1,K2} ou {K2,K1} est le même geste électrique. Mais l'appariement palier par palier
reste positionnel, ce qui préserve le seul cas où l'ordre signifie encore quelque chose :
deux relais de MÊME puissance, où il décide quel contact sert ce palier.
Conséquence pour l'écran : réordonner des contacteurs de puissances distinctes n'avertit
plus de rien. Avertir sur un geste devenu gratuit serait aussi trompeur que de se taire sur
un geste coûteux.
PALIERS : l'app ne dérive plus la combinatoire pour l'affichage — la box les publie
(mechanism.stagesW). Vérifié sur appareil : [0, 500, 1000, 1500, 2000, 2500, 3000, 3500].
deriveStages reste côté app pour la seule annonce d'impact, qui se fait avant l'écriture.
Tant que le brouillon n'a pas touché aux contacteurs, c'est la table PUBLIÉE qui s'affiche ;
dès qu'il y touche elle devient périmée, et l'écran montre une prévision annoncée comme
telle plutôt qu'une table qui décrit l'état d'avant.
COMPTEUR ET SONDE : ils ne « vont pas arriver », ils SONT au schéma de .75
(o:meterThingId, o:sensorThingId). Le grisage dérivé les a donc activés seul, sans nouvelle
version de l'app — exactement ce qui était promis. Mais un bloc actif sur un placeholder
mort est pire qu'un bloc grisé : les deux sont désormais câblés sur un vrai sélecteur,
filtré par INTERFACE (smartmeter, temperaturesensor) et jamais par nom de plugin.
Ni l'un ni l'autre n'entre dans sameHardware() : les rattacher ne reconstruit rien. Ni dans
le contrôle d'unicité : deux charges peuvent partager une sonde, un compteur divisionnaire
en couvrir plusieurs — le sélecteur ne grise donc rien ici, contrairement aux contacteurs.
Et la mesure sert à VÉRIFIER, jamais à décider : elle n'entre pas dans le budget, la charge
étant déjà comptée dans le racine dont elle n'est qu'une décomposition.
COMPTEURS CUMULÉS : la base purgée, les valeurs sont saines (55 730 / 50 / 6 327 / 62 007
kWh) et la tuile « compteur box invalide » a disparu d'elle-même. Le plafond de
plausibilité reste : un zéro réel s'affiche « 0,0 kWh », jamais « — ».
hasNeverRun était déjà traité comme un état distinct — « l'absence de cycle est dite pour
elle-même, jamais rendue par un âge de zéro ».
Vérifié sur appareil contre .75 : compteur racine relu, « à configurer » vide, deux bornes
listées (Simulated wallbox + Terra AC Charger), stagesW de la box, les deux champs
writable, 5/5 sections de l'écran SG-Ready. Tests 98/98, analyze 0 erreur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
MIUI refuse l'injection d'événements adb : impossible de faire défiler ou de taper depuis
l'hôte pour contrôler visuellement. Les assertions descendent donc dans integration_test,
qui a la main sur l'IU : le chip porte bien un libellé, le sélecteur de mécanisme propose
les trois valeurs, et les sections exigées par la maquette (Relais commandés,
Temporisations, Compteur dédié, Arbitrage) sont présentes.
Une clé est posée sur le chip pour le viser sans compter des icônes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
1) GLISSEMENT — diagnostic d'abord, et il a donné DEUX réponses.
Le geste n'est PAS avalé par le ListView parent : les deux montages du test
(section seule / section imbriquée) glissent identiquement. La cause du symptôme était
bien que .75 n'a qu'une charge par domaine — rien à réordonner.
Mais l'enquête a trouvé un vrai défaut au passage : le centre de la poignée tombait dans
le vide de 3 px séparant l'icône de la pastille de rang, et ReorderableDragStartListener
défère le test de collision à ses enfants. Une bande morte invisible au milieu d'une
poignée qui paraît d'un seul tenant. Corrigé par un Container transparent.
Tentative faite et ANNULÉE : rendre les domaines glissables aussi. Deux
ReorderableListView imbriqués se disputent le geste et l'extérieur gagne — le
glissement des cartes cassait. Les domaines gardent leurs flèches, et une charge seule
dans son domaine n'offre plus de prise qui mènerait nulle part (avec l'infobulle qui
renvoie au bon geste).
Vérifié SUR L'APPAREIL, contre .75, avec les deux charges mises dans le même domaine :
[chauffe-eau, pac-terrain] → [pac-terrain, chauffe-eau], brouillon abandonné sans rien
écrire. Classement ecs/hvac rétabli ensuite.
2) SÉLECTEUR DE MÉCANISME — actif, et c'était le morceau délicat.
LoadConfig est une union discriminée : écrire `adapter` sans nettoyer le reste produit
une configuration que la box refuse EN BLOC, et le refus emporte toutes les charges de
l'appel. D'où basculerMecanisme(), qui retire les clés du mécanisme quitté et pose
celles du nouveau, en récupérant ce qui peut l'être — les deux premiers contacts
deviennent SG1/SG2 dans l'encodage standard, les relais gardent leur puissance.
Ce qui n'est PAS transposé : l'estimation d'un état SG-Ready n'est pas la puissance
d'une résistance. La charge produite est alors incomplète, et c'est voulu — le
formulaire réclame plutôt que d'inventer une valeur de plaque.
La bascule franchit sameHardware() : confirmation préalable qui annonce la
reconstruction et la durée de réarmement (900 s par défaut vers SG-Ready — la valeur
d'une PAC réelle, pas celle du banc).
Le mode fixed d'etmvariableload reste LISIBLE sans être proposé : bloc « Configuration
héritée » en lecture seule, et conversion seulement sur geste explicite, avec le
plafond que la déclaration en paliers ne porte pas. Ouvrir l'écran ne convertit personne.
3) CHIP DE DOMAINE — il ne rendait qu'une icône muette : le domaine d'une charge était
illisible sur sa carte, alors qu'il décide de son groupe et de son rang. Il porte
maintenant son libellé, et sa couleur suit le thème.
4) THÈME — les 11 derniers sites (pas 9) basculés. Trois cas : contexte disponible
(main, ac_screen) ; peintre sans contexte, à qui la couleur est passée résolue
(energy_flow_widget) ; tables statiques, où la teinte devient nullable et se résout au
rendu (categoryInfoMap, favoris). Plus AUCUN jeton hors thème dans lib/.
5) ZONES — non entreprises, comme convenu.
Tests 93/93, analyze 0 erreur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
Je n'avais pas ouvert docs/mockups/ : les écrans ont été construits depuis le texte du brief
seul, et l'écart est franc. Les maquettes du 2026-08-25 font autorité, elles sont désormais
suivies.
ÉCRANS DE MÉCANISME, refaits :
- sélecteur de Things PAR NOM, filtré sur l'interface nymea (power / etmvariableload).
Taper un UUID au doigt est une erreur garantie ET muette : la box accepte un UUID valide
qui ne désigne rien, et la charge ne commande jamais rien. Le sélecteur grise les Things
déjà pris — la box refuse en bloc un même Thing sur deux lignes — et signale en rouge un
ThingId configuré qui n'existe plus sur la box ;
- routeur de relais : ajout/retrait de contacteurs, puissance par ligne, et les paliers
atteignables AFFICHÉS. Ils sont déduits (2^N combinaisons, doublons écartés), jamais
enregistrés — la maquette est explicite là-dessus, ma note précédente disant l'inverse
était une sur-interprétation du brief ;
- SG-Ready : SG1/SG2 se choisissent EN HAUT, une fois, et la table des quatre états devient
éditable — ce qui s'y coche est fermé/ouvert, et c'est ça qui devient relays[]. Les
étiquettes SG1/SG2 sont une convention de l'écran (SG1 = fermé en état 1, SG2 = en état 3)
et elles TOMBENT sur un encodage non standard, où les contacts s'affichent tels quels ;
- modulable : plafond requis > 0, et l'avertissement que changer d'appareil crée une autre
charge puisque l'identifiant EST le ThingId ;
- temporisations minOnS/minOffS (et minStateHoldS pour la PAC) en pas de 30/60 s ;
- « Compteur dédié » présent et GRISÉ, avec sa raison : aucun champ n'existe côté box, et
inventer meterThingId ferait rejeter l'enregistrement de TOUTES les charges ;
- « Inclure dans l'arbitrage » avec rang et domaine, et ce que la désactivation fait
vraiment selon le mécanisme ;
- validation AVANT envoi : relays[] vide, powerW ≤ 0, Thing en double, état 2 absent,
maxPowerW ≤ 0. SetLoadConfig refuse en bloc et sans motif — laisser partir une config
invalide condamne l'installateur à un refus muet qui perd tout l'appel.
LISTE :
- l'en-tête d'arbitrage et le tableau de budget, PERDUS à la fusion, sont rétablis. Sans
eux « Alloué 3 000 W » ne se rattache à rien ;
- le rang passe en pastille près de la poignée, et le sous-titre devient lisible :
« Routeur de relais · 3 relais · min ON 60 s » au lieu de « Routeur de relais · rang 1 » ;
- section « Recharge véhicule » : la borne n'est pas une LoadConfig, elle n'apparaissait donc
NULLE PART — ce qu'un installateur qui vient de la régler lit comme une perte.
MENU : l'entrée « Ordre de service des charges » est retirée ; elle pointait sur une
redirection, donc sur le même écran sous deux noms.
Tests 80/80, analyze 0 erreur.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
FUSION. L'écran « Ordre de service » est supprimé ; sa route redirige. C'est son CHEMIN DE
DONNÉES (LoadConfigProvider) qui a migré dans la présentation de « Rôles & appareils »,
jamais l'inverse. EmsRole fige six rôles dont dhw ET heatPump séparés — or LM-201 dit qu'une
PAC sans ballon séparé est UNE charge : le modèle des rôles aurait exigé deux charges sur un
canal unique, ce que validateSet() refuse à raison. Les charges viennent donc de
GetLoadConfig. Les compteurs restent sur EnergySetupProvider (autre contrat, SetRootMeter).
La note « le pilote peut bousculer l'ordre les jours Tempo rouge ou à l'approche d'une
échéance » est retirée : aucun de ces comportements n'existe dans le moteur.
ANNONCE AVANT ÉCRITURE. Miroir exact de EnergyArbitrator::sameHardware(). Le piège, vérifié
sur .75 : la comparaison des relais est INDEXÉE, donc réordonner relays[] sans rien changer
d'autre reconstruit l'adaptateur — contacts ouverts (applySafeState, force) et verrous
réarmés à froid. Un glisser-déposer dans la liste des contacteurs est un geste matériel.
L'écran le nomme, avec la durée de réarmement.
GRISAGE PAR INTROSPECTION. RpcSchema lit JSONRPC.Introspect à la connexion ; trois états et
non deux — écrivable, r: (mesure, pas fonction manquante), absent — plus « inconnu » quand le
schéma n'a pas pu être lu. Chaque champ grisé porte sa raison et le nom du champ manquant.
La version affichée est celle de l'API d'expérience (NymeaEnergy 0.8) : le paquet +etm15
n'est exposé par aucune méthode.
CORRIGÉ AU PASSAGE — les modes de recharge étaient INOPÉRANTS. setChargingInfo() appelait
EnergyPlugin.SetChargingInfo (namespace inexistant) avec mode/targetSoc/endTime/minCurrent
au lieu de chargingMode/targetPercentage/endDateTime, et minCurrent n'existe dans aucun
champ. Vérifié par appel réel : « Missing required key: chargingMode ». Le refus était de
plus avalé et l'état local mis à jour quand même. Désormais l'état ne suit que si la box
accepte, et le refus remonte sans en inventer la cause.
ZONES — constaté, pas corrigé. L'hypothèse « champs grisés » est fausse : AirConditioning
publie 8 méthodes toutes en écriture. ac_screen.dart n'en appelle aucune, ses 4 pièces sont
des constantes de maquette, et .75 déclare zéro zone. getZones() ajouté + bandeau qui lit le
vrai compte et annonce la maquette. Le câblage est un lot en soi, non exerçable sans zone.
Tests 80/80, analyze 0 erreur. Relevé : docs/RELEVE_LOTC.md — dont §6, ce qui n'a PAS pu
être vérifié : l'IU n'a pas pu être pilotée sur l'appareil, MIUI refusant INJECT_EVENTS.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff