etm-powersync-app/tools/rpc/measurement_check.dart
Patrick Schurig 771345f1bc tools(rpc): une sonde qui dit ce que l'app AFFICHERAIT de measurement
Les règles d'affichage de `measurement` sont figées par test sur des trames fabriquées.
Cette sonde les éprouve sur les trames réelles de la box — en important le modèle de l'app
(LoadTelemetryEntry.fromJson), pas en relisant le JSON à sa façon : une lecture parallèle
ne prouverait rien sur ce que l'écran fera.

Elle montre ce qu'un écran regardé de loin ne distingue pas : `none` (bloc masqué, « pas
mesurable ») d'une charge simplement absente de loads[] (rien à conclure), et la portée
d'un écart selon `meter` ou `device`.

Relevé du 2026-08-28 sur .75 en 1.15.2+etm26 :

  measuredW encore publié ? false
  chauffe-eau   source=meter → Mesuré 1500 W · mesure indépendante : concordance = preuve
  pac-terrain   source=meter → Mesuré 800 W  · idem
  V2C Trydan    absente de loads[] — pas `none`

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
2026-08-28 15:32:52 +02:00

63 lines
2.8 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:etm_powersync_app/models/load_telemetry.dart';
// Lit `NymeaEnergy.GetLoadTelemetry` et dit, charge par charge, **ce que l'app afficherait**
// — en passant par le vrai modèle de l'app, pas par une lecture parallèle du JSON.
//
// ## Pourquoi cette sonde plutôt qu'un coup d'œil à l'écran
//
// Les règles d'affichage de `measurement` sont figées par test sur des trames fabriquées.
// Ceci vérifie la même chose sur les trames RÉELLES de la box, et c'est la seule façon de
// voir que le contrat déployé correspond à ce que l'app attend. Un écran regardé de loin ne
// distingue pas « pas mesurable » d'un bloc simplement absent.
//
// LECTURE SEULE. Usage : dart run tools/rpc/measurement_check.dart [host]
Future<void> main(List<String> args) async {
final host = args.isNotEmpty ? args[0] : '192.168.1.75';
final ws = await WebSocket.connect('ws://$host:4444')
.timeout(const Duration(seconds: 8));
final done = Completer<Map<String, dynamic>>();
ws.listen((d) {
final m = jsonDecode(d as String) as Map<String, dynamic>;
if (m['id'] == 2) done.complete(m);
});
ws.add(jsonEncode({'id': 1, 'method': 'JSONRPC.Hello', 'params': {}}));
ws.add(jsonEncode(
{'id': 2, 'method': 'NymeaEnergy.GetLoadTelemetry', 'params': {}}));
final r = await done.future.timeout(const Duration(seconds: 10));
await ws.close();
final p = r['params'] as Map<String, dynamic>;
final loads = (p['loads'] as List? ?? const []);
final brut = jsonEncode(p);
print('measuredW encore publié ? ${brut.contains('"measuredW"')} '
'(doit être false depuis 1.15.2+etm26)');
print('${loads.length} charge(s) dans loads[] — et `measurement` est présent pour '
'chacune, pour elles seules.\n');
for (final l in loads.whereType<Map>()) {
final e = LoadTelemetryEntry.fromJson(Map<String, dynamic>.from(l));
final m = e.measurement;
final ligne = switch (m.source) {
MeasurementSource.none =>
'bloc de mesure MASQUÉ — « pas mesurable » est un état, pas un vide',
MeasurementSource.meter =>
'Mesuré ${m.powerW!.round()} W · mesure indépendante : une concordance est une preuve',
MeasurementSource.device =>
'Mesuré ${m.powerW!.round()} W · publiée par l\'appareil : une concordance ne prouve rien',
MeasurementSource.inconnue =>
'Mesuré ${m.powerW?.round()} W · source inconnue de cette app, aucune portée affichée',
};
print(' ${e.loadId.padRight(38)} alloué ${e.allocatedW.round()} W');
print(' ${' '.padRight(38)} source=${m.source.name} → $ligne');
}
print('\nToute charge de GetLoadConfig ABSENTE de cette liste n\'est pas `none` :');
print('elle est hors de loads[], et l\'app n\'en conclut rien.');
}