docs: recalage sur la rév. 3 du contrat charge pilotée

Le miroir INTERFACE_etmvariableload.md est passé en rév. 3 pendant la session (corps
identique au canonique du dépôt plugin, vérifié). Son §10 s'adresse nommément à l'agent
app — et il RATIFIE les maquettes dessinées aujourd'hui : relays[] = {thingId, powerW}
pour l'ECS relais, maxPowerW seul pour le continu, powerLevels absents de la config et
dérivés par le routeur.

Trois recalages :

- Le picker de relais filtre sur l'interface nymea « power » et présente les things par
  NOM — nommé dans la maquette, il ne l'était pas.
- Le point 4 du brief plugin est tranché par la rév. 3 : la duplication de la
  combinatoire est voulue (l'app dérive pour montrer, le routeur pour exécuter). La
  demande devient donc un moyen de détecter la divergence — la télémétrie publie déjà
  maxStageW, l'app peut y confronter le maximum de sa propre liste.
- En-tête du pont VM Service, que le commit précédent avait laissé dans sa forme brute.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XPUo3RMr8SzK6qbFtfBm8H
This commit is contained in:
Patrick Schurig 2026-08-25 21:45:53 +02:00
parent a4487324dc
commit eac1dde4ee
3 changed files with 43 additions and 9 deletions

View File

@ -129,9 +129,22 @@ combinatoire de `relayrouter.cpp` (toutes les combinaisons 2^N, doublons écart
16 relais). Deux implémentations d'une même règle divergeront : à la première correction
côté moteur, l'app annoncera des paliers que la box n'atteint plus.
**Demande.** Exposer la liste — soit dans la télémétrie (`mechanism.stagesW: [...]`), soit en
retour d'un `GetLoadConfig` enrichi. À défaut, **le dire explicitement**, et l'app
documentera sa dérivation comme un miroir à resynchroniser.
**⚠️ Tranché par la rév. 3 du contrat, arrivée pendant la rédaction de ce brief.**
`INTERFACE_etmvariableload.md` §10 dit désormais : l'assistant calcule et **affiche** les
paliers atteignables côté app, et ces `powerLevels` ne sont **pas** envoyés — ils sont
« dérivés côté plugin ». La duplication est donc **voulue et contractuelle** : l'app dérive
pour montrer, le routeur dérive pour exécuter.
**Ce qui reste, et qui devient la vraie demande :** un moyen de vérifier que les deux
dérivations n'ont pas divergé. Il existe déjà à moitié — la télémétrie publie
`mechanism.maxStageW`. L'app peut donc comparer **le maximum de sa propre liste** à celui que
la box annonce, et se taire si ça correspond, alerter sinon. Ça ne couvre pas les paliers
intermédiaires, mais ça attrape le cas qui compte : une règle de combinatoire changée d'un
côté seulement.
**Demande minimale** : confirmer que `maxStageW` reste bien le maximum atteignable et non un
plafond de sécurité — sinon la comparaison ne vaut rien. **Demande confortable** : publier la
liste complète (`mechanism.stagesW: [...]`), et l'app cessera de dériver.
---

View File

@ -288,6 +288,9 @@
créent pas de palier supplémentaire — les doublons sont écartés.</em>
</div>
<div class="mode-hint"><b>Le picker liste les things par NOM</b>, filtrés sur
l'interface nymea <code>power</code> — taper un UUID au doigt est une erreur
garantie. Il résout nom → ThingId et n'ajoute aucun champ au contrat.</div>
<div class="mode-hint"><b>Un ECS simple, c'est un seul relais</b> — deux paliers, 0 et P. Une
résistance multipalier en déclare 2, 3 ou 4. La puissance saisie est celle de la
<b>résistance</b>, pas le calibre du contacteur : rien dans nymea ne dit ce qu'un relais

View File

@ -2,14 +2,32 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
// Capte les traces `dart:developer log()` de l'app en cours d'exécution.
// Pont VM Service — rend visibles les traces `dart:developer log()` de l'app.
//
// `flutter run` ne les imprime PAS sur stdout (elles partent au VM Service, pas à
// stdout ni à logcat). Sans ce pont, tout ce que `NymeaService._log()` journalise est
// invisible depuis le poste — donc les traces de locale, de télémétrie et d'écriture
// aussi.
// ## Pourquoi ce fichier existe
//
// Usage : dart vm_logs.dart ws://127.0.0.1:PORT/TOKEN=/ws
// `flutter run` **n'imprime pas** ces traces sur stdout : elles partent au VM Service, ni sur
// la console, ni dans `logcat`. Tout ce que `NymeaService._log()` journalise — locale demandée
// et retenue, aller-retours RPC, séquence d'écriture, télémétrie poussée — est donc invisible
// depuis le poste tant qu'on n'est pas branché ici.
//
// Éprouvé le 2026-08-25, premier passage de l'app sur appareil (Redmi Note 9S) : sans ce pont,
// aucun constat du journal de vérification n'était atteignable.
//
// ## Usage
//
// flutter run -d <device> --use-application-binary=build/app/outputs/flutter-apk/app-debug.apk
// # relever « A Dart VM Service on … is available at: http://127.0.0.1:PORT/TOKEN=/ »
// dart tools/devlog/vm_logs.dart 'ws://127.0.0.1:PORT/TOKEN=/ws'
//
// Le `/ws` final est obligatoire, et les guillemets aussi — le jeton contient un `=`.
//
// ## Le piège d'horodatage, déjà payé une fois
//
// Le VM Service livre **l'historique bufférisé d'un coup** au moment du `streamListen`.
// Horodater à la réception écrase donc toute la chronologie sous une seule seconde — c'est ce
// qui est arrivé au premier essai. On lit `event.timestamp`, jamais `DateTime.now()`.
Future<void> main(List<String> args) async {
final uri = args.first;
final ws = await WebSocket.connect(uri);