import 'dart:async'; import 'dart:convert'; import 'dart:io'; // Observation d'une BORNE de recharge, état par état — LECTURE SEULE. // // ## Pourquoi cette sonde existe // // `telemetry_watch.dart` regarde l'arbitrage ; il ne montre une borne que si elle est // arbitrée. Or une borne passe l'essentiel de son temps DEHORS : sans véhicule branché, // elle n'entre pas dans `loads[]` (comportement depuis `+etm24`). C'est justement pendant // ces transitions — branchement, démarrage de charge, montée en puissance — qu'on a besoin // de voir ce que le Thing publie. // // Elle sert notamment à trancher une question que le contrat ne documente pas : quelles // bornes publient `sessionEnergy` (l'énergie de la session, remise à zéro au branchement) // et lesquelles ne publient qu'un cumul. La décision LM-1009 en dépend — l'avancement // d'une recharge s'affiche en énergie livrée, et sur une borne qui n'en publie pas, il // faut dire « pas mesurable » plutôt qu'afficher zéro. // // N'ÉCRIT RIEN : ni action, ni configuration. Elle s'abonne et regarde. // // Usage : dart tools/rpc/evcharger_watch.dart [host] [durée_s] [filtre_nom] // dart tools/rpc/evcharger_watch.dart 192.168.1.75 600 V2C Future main(List args) async { final host = args.isNotEmpty ? args[0] : '192.168.1.75'; final seconds = args.length > 1 ? int.parse(args[1]) : 300; final filtre = args.length > 2 ? args[2].toLowerCase() : ''; final ws = await WebSocket.connect('ws://$host:4444') .timeout(const Duration(seconds: 8)); var id = 0; final pending = >>{}; final started = DateTime.now(); String stamp() => '+${DateTime.now().difference(started).inSeconds.toString().padLeft(4)}s'; // thingId → nom ; stateTypeId → nom d'état ; et la dernière valeur vue, pour // n'imprimer que ce qui CHANGE — une borne qui republie la même valeur toutes les // secondes noierait la transition qu'on guette. final noms = {}; final etats = {}; final unites = {}; final dernier = {}; ws.listen((raw) { final m = jsonDecode(raw as String) as Map; final rid = m['id']; if (rid is int && pending.containsKey(rid)) { pending.remove(rid)!.complete(m); return; } if (m['notification'] != 'Integrations.StateChanged') return; final p = m['params'] as Map; final thingId = p['thingId'] as String?; final nom = noms[thingId]; if (nom == null) return; // pas une borne suivie final etat = etats[p['stateTypeId'] as String? ?? ''] ?? '?'; final valeur = '${p['value']}'; final cle = '$thingId/$etat'; if (dernier[cle] == valeur) return; // inchangé : on se tait dernier[cle] = valeur; final u = unites[p['stateTypeId'] as String? ?? ''] ?? ''; stdout.writeln('${stamp()} $nom · $etat = $valeur $u'); }); Future> call(String method, [Map params = const {}]) { final i = ++id; final c = Completer>(); pending[i] = c; ws.add(jsonEncode({'id': i, 'method': method, 'params': params})); return c.future.timeout(const Duration(seconds: 20)); } await call('JSONRPC.Hello', {'locale': 'fr_FR'}); await call('JSONRPC.SetNotificationStatus', { 'namespaces': ['Integrations'] }); final classes = (await call('Integrations.GetThingClasses'))['params'] ['thingClasses'] as List; final bornes = {}; for (final c in classes.cast>()) { final ifs = (c['interfaces'] as List?)?.cast() ?? const []; if (!ifs.contains('evcharger')) continue; bornes.add(c['id'] as String); for (final s in (c['stateTypes'] as List? ?? []).cast>()) { etats[s['id'] as String] = s['name'] as String; final u = s['unit'] as String? ?? ''; unites[s['id'] as String] = (u.isEmpty || u == 'UnitNone') ? '' : u.replaceFirst('Unit', ''); } } final things = (await call('Integrations.GetThings'))['params']['things'] as List; stdout.writeln('── bornes déclarées sur $host ${'─' * 42}'); for (final t in things.cast>()) { if (!bornes.contains(t['thingClassId'])) continue; final nom = t['name'] as String; if (filtre.isNotEmpty && !nom.toLowerCase().contains(filtre)) continue; noms[t['id'] as String] = nom; final vals = {}; for (final s in (t['states'] as List? ?? []).cast>()) { final n = etats[s['stateTypeId'] as String] ?? '?'; vals[n] = '${s['value']}'; dernier['${t['id']}/$n'] = '${s['value']}'; } stdout.writeln('\n■ $nom (${(t['id'] as String).substring(0, 8)})'); // L'énergie d'abord : c'est ce que la décision LM-1009 attend, et sa PRÉSENCE est // l'information — une borne sans `sessionEnergy` rend l'avancement non mesurable. for (final k in ['sessionEnergy', 'chargeEnergy', 'totalEnergyConsumed']) { stdout.writeln(vals.containsKey(k) ? ' $k = ${vals[k]}' : ' $k — ABSENT de cette borne'); } for (final k in ['pluggedIn', 'charging', 'power', 'phaseCount', 'maxChargingCurrent', 'currentPower', 'intensity']) { if (vals.containsKey(k)) stdout.writeln(' $k = ${vals[k]}'); } } if (noms.isEmpty) { stdout.writeln(' aucune borne ne correspond au filtre « $filtre »'); await ws.close(); return; } stdout.writeln('\n── changements pendant ${seconds}s ${'─' * 40}'); stdout.writeln('(seules les valeurs qui CHANGENT sont imprimées)\n'); await Future.delayed(Duration(seconds: seconds)); stdout.writeln('\n── fin ${'─' * 54}'); await ws.close(); }