tools(rpc): sondes JSON-RPC + fixtures du lot télémétrie déployé sur .75
Range dans tools/rpc/ les sondes qui vivaient dans un scratchpad volatil : probe (appel RPC quelconque), notifications (abonnement + comptage), locale et locale_diff (comportement o:locale et son effet réel), loadconfig_roundtrip (aller-retour neutre §7-1). README avec usages. loadconfig_roundtrip est le SEUL qui écrit sur la box : garde --yes ajouté pour qu'un SetLoadConfig ne parte jamais par réflexe de flèche haut. analysis_options : tools/** et les classes l10n générées sortent de l'analyse — flutter analyze revient à 25 issues (ligne de base). Fixtures : le lot plugin télémétrie EST déployé sur .75 depuis ce matin (20 méthodes / 14 notifications). Capturé avant que ça ne se reperde : - loadtelemetry_hems75.json : dump réel de GetLoadTelemetry - loadconfig_hems75_v2.json : GetLoadConfig avec la clé 'domain' - schema_hems75_v2.json : schémas SET + télémétrie introspectés Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bb77039c82
commit
4ade3e4151
@ -9,6 +9,16 @@
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
# Sondes JSON-RPC autonomes (dart:io), exécutées à la main contre une box.
|
||||
# Ce sont des outils de diagnostic, pas du code d'application : leur sortie
|
||||
# console EST leur raison d'être, donc `avoid_print` n'y a pas de sens.
|
||||
# Les exclure garde `flutter analyze` lisible pour ce qui part en production.
|
||||
- tools/**
|
||||
# Classes de localisation générées par `flutter gen-l10n` — jamais éditées à la main.
|
||||
- lib/l10n/app_localizations*.dart
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
|
||||
38
test/fixtures/loadconfig_hems75_v2.json
vendored
Normal file
38
test/fixtures/loadconfig_hems75_v2.json
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"_source": "dump réel HEMS 192.168.1.75, NymeaEnergy.GetLoadConfig, 2026-08-25 (APRÈS déploiement du lot télémétrie).",
|
||||
"_note": "Diffère de loadconfig_hems75.json : la clé 'domain' existe désormais (vide = non classé) et une 2e charge sg-ready 'pac-terrain' est déclarée. Conserve les charges utiles VIDES des mécanismes inutilisés — sérialisation par méta-objet. Ne PAS nettoyer.",
|
||||
"loadConfigs": [
|
||||
{
|
||||
"adapter": "relay-router",
|
||||
"domain": "",
|
||||
"enabled": true,
|
||||
"id": "chauffe-eau",
|
||||
"label": "chauffe-eau",
|
||||
"maxPowerW": 0,
|
||||
"minOffS": 60,
|
||||
"minOnS": 60,
|
||||
"mode": "fixed",
|
||||
"needs": {
|
||||
"dailyDeadline": "",
|
||||
"minEnergyWhPerDay": 0
|
||||
},
|
||||
"powerLevels": [],
|
||||
"priority": 1,
|
||||
"relays": [
|
||||
{
|
||||
"powerW": 500,
|
||||
"thingId": "{8538782f-2c8c-4a30-bfce-c8140f791c9b}"
|
||||
},
|
||||
{
|
||||
"powerW": 1000,
|
||||
"thingId": "{2ebe6bef-829b-4695-9d11-ddb4c16c5448}"
|
||||
},
|
||||
{
|
||||
"powerW": 2000,
|
||||
"thingId": "{b033b212-1adb-4df0-ba2b-8fa477de52a2}"
|
||||
}
|
||||
],
|
||||
"sgReady": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
57
test/fixtures/loadtelemetry_hems75.json
vendored
Normal file
57
test/fixtures/loadtelemetry_hems75.json
vendored
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"_source": "dump réel HEMS 192.168.1.75, NymeaEnergy.GetLoadTelemetry, 2026-08-25 — lot plugin télémétrie déployé (20 méthodes / 14 notifications).",
|
||||
"_note": "Charge utile IDENTIQUE à la notification LoadTelemetryChanged. decision.code et faultCode sont des CODES avec params numériques, jamais des phrases : le rendu en français est à la charge de l'app (clés ARB). 'timestamp' = fin du dernier cycle d'arbitrage, omis tant qu'aucun cycle n'a tourné — c'est lui qui rend un arbitre figé détectable.",
|
||||
"telemetry": {
|
||||
"budget": {
|
||||
"allocatedW": 0,
|
||||
"evReservedW": 0,
|
||||
"recreditedW": 500,
|
||||
"remainingW": 229.29998779296875,
|
||||
"surplusW": -270.70001220703125
|
||||
},
|
||||
"degradedMode": false,
|
||||
"loads": [
|
||||
{
|
||||
"allocatedW": 0,
|
||||
"available": true,
|
||||
"decision": {
|
||||
"code": "SURPLUS_INSUFFICIENT",
|
||||
"params": {
|
||||
"budgetW": 229
|
||||
}
|
||||
},
|
||||
"loadId": "chauffe-eau",
|
||||
"lock": {
|
||||
"kind": "minOff",
|
||||
"remainingS": 60
|
||||
},
|
||||
"mechanism": {
|
||||
"kind": "relay",
|
||||
"maxStageW": 3500,
|
||||
"stageW": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"allocatedW": 0,
|
||||
"available": true,
|
||||
"decision": {
|
||||
"code": "SG_NORMAL",
|
||||
"params": {
|
||||
"budgetW": 229
|
||||
}
|
||||
},
|
||||
"loadId": "pac-terrain",
|
||||
"lock": {
|
||||
"kind": "minStateHold",
|
||||
"remainingS": 240
|
||||
},
|
||||
"mechanism": {
|
||||
"estimatedPowerW": 0,
|
||||
"kind": "sgReady",
|
||||
"state": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-08-25T12:37:00Z"
|
||||
}
|
||||
}
|
||||
127
test/fixtures/schema_hems75_v2.json
vendored
Normal file
127
test/fixtures/schema_hems75_v2.json
vendored
Normal file
@ -0,0 +1,127 @@
|
||||
{
|
||||
"_source": "JSONRPC.Introspect sur 192.168.1.75, 2026-08-25, APRÈS déploiement du lot télémétrie.",
|
||||
"_note": "nymea rejette toute clé inconnue (jsonvalidator.cpp:143). Liste exhaustive de ce que la box accepte et renvoie.",
|
||||
"SetLoadConfig_params": {
|
||||
"loadConfigs": [
|
||||
{
|
||||
"adapter": "String",
|
||||
"enabled": "Bool",
|
||||
"id": "String",
|
||||
"label": "String",
|
||||
"mode": "String",
|
||||
"o:domain": "String",
|
||||
"o:maxPowerW": "Uint",
|
||||
"o:minOffS": "Uint",
|
||||
"o:minOnS": "Uint",
|
||||
"o:needs": {
|
||||
"o:dailyDeadline": "String",
|
||||
"o:minEnergyWhPerDay": "Uint"
|
||||
},
|
||||
"o:powerLevels": [
|
||||
"Uint"
|
||||
],
|
||||
"o:relays": [
|
||||
{
|
||||
"powerW": "Uint",
|
||||
"thingId": "String"
|
||||
}
|
||||
],
|
||||
"o:sgReady": {
|
||||
"o:minStateHoldS": "Uint",
|
||||
"o:states": [
|
||||
{
|
||||
"estimatedPowerW": "Double",
|
||||
"relays": [
|
||||
"String"
|
||||
],
|
||||
"state": "Uint"
|
||||
}
|
||||
]
|
||||
},
|
||||
"priority": "Uint"
|
||||
}
|
||||
]
|
||||
},
|
||||
"GetLoadTelemetry_returns": {
|
||||
"degradedMode": "Bool",
|
||||
"loads": [
|
||||
{
|
||||
"allocatedW": "Double",
|
||||
"available": "Bool",
|
||||
"decision": {
|
||||
"code": "String",
|
||||
"params": "Variant"
|
||||
},
|
||||
"loadId": "String",
|
||||
"o:faultCode": "String",
|
||||
"o:lock": {
|
||||
"kind": "String",
|
||||
"remainingS": "Int"
|
||||
},
|
||||
"o:mechanism": {
|
||||
"kind": "String",
|
||||
"o:chargingEnabled": "Bool",
|
||||
"o:currentA": "Double",
|
||||
"o:estimatedPowerW": "Double",
|
||||
"o:maxPowerW": "Double",
|
||||
"o:maxStageW": "Double",
|
||||
"o:percent": "Double",
|
||||
"o:phaseCount": "Uint",
|
||||
"o:pluggedIn": "Bool",
|
||||
"o:setpointW": "Double",
|
||||
"o:stageW": "Double",
|
||||
"o:state": "Int"
|
||||
}
|
||||
}
|
||||
],
|
||||
"o:budget": {
|
||||
"allocatedW": "Double",
|
||||
"evReservedW": "Double",
|
||||
"recreditedW": "Double",
|
||||
"remainingW": "Double",
|
||||
"surplusW": "Double"
|
||||
},
|
||||
"o:timestamp": "String"
|
||||
},
|
||||
"LoadTelemetryChanged_params": {
|
||||
"degradedMode": "Bool",
|
||||
"loads": [
|
||||
{
|
||||
"allocatedW": "Double",
|
||||
"available": "Bool",
|
||||
"decision": {
|
||||
"code": "String",
|
||||
"params": "Variant"
|
||||
},
|
||||
"loadId": "String",
|
||||
"o:faultCode": "String",
|
||||
"o:lock": {
|
||||
"kind": "String",
|
||||
"remainingS": "Int"
|
||||
},
|
||||
"o:mechanism": {
|
||||
"kind": "String",
|
||||
"o:chargingEnabled": "Bool",
|
||||
"o:currentA": "Double",
|
||||
"o:estimatedPowerW": "Double",
|
||||
"o:maxPowerW": "Double",
|
||||
"o:maxStageW": "Double",
|
||||
"o:percent": "Double",
|
||||
"o:phaseCount": "Uint",
|
||||
"o:pluggedIn": "Bool",
|
||||
"o:setpointW": "Double",
|
||||
"o:stageW": "Double",
|
||||
"o:state": "Int"
|
||||
}
|
||||
}
|
||||
],
|
||||
"o:budget": {
|
||||
"allocatedW": "Double",
|
||||
"evReservedW": "Double",
|
||||
"recreditedW": "Double",
|
||||
"remainingW": "Double",
|
||||
"surplusW": "Double"
|
||||
},
|
||||
"o:timestamp": "String"
|
||||
}
|
||||
}
|
||||
78
tools/rpc/README.md
Normal file
78
tools/rpc/README.md
Normal file
@ -0,0 +1,78 @@
|
||||
# Sondes JSON-RPC — `tools/rpc/`
|
||||
|
||||
Scripts `dart:io` autonomes pour interroger une box nymea **sans lancer l'app**.
|
||||
|
||||
Pourquoi ils existent : sur ce poste, Flutter Linux natif est bloqué (snap sans linker
|
||||
`ld.lld`) et le web ne peut pas joindre le LAN en `ws://` (CORS). Ces sondes sont donc le
|
||||
moyen le plus court de vérifier un comportement réel de la box — et elles font plus autorité
|
||||
qu'une lecture de code ou qu'un `Introspect` recopié de mémoire.
|
||||
|
||||
Aucune dépendance hors SDK : `dart:io` + `dart:convert`. Rien à installer.
|
||||
|
||||
```bash
|
||||
dart tools/rpc/<script>.dart [host] [args…] # host par défaut : 192.168.1.75
|
||||
```
|
||||
|
||||
## Les scripts
|
||||
|
||||
| Script | Ce qu'il fait | Écrit ? |
|
||||
|---|---|---|
|
||||
| `probe.dart` | Un appel RPC quelconque, réponse en JSON indenté. Le couteau suisse. | non |
|
||||
| `notifications.dart` | S'abonne à **tous** les namespaces, compte et échantillonne ce qui arrive. | non |
|
||||
| `locale.dart` | Comportement de `o:locale` : connexions simultanées, re-`Hello`, locale absurde. | non |
|
||||
| `locale_diff.dart` | Aplatit **toutes** les chaînes affichables et les diffe entre locales. | non |
|
||||
| `loadconfig_roundtrip.dart` | Aller-retour neutre `Get` → `Set` verbatim → `Get`. | ⚠️ **OUI** |
|
||||
|
||||
### `probe.dart`
|
||||
|
||||
```bash
|
||||
dart tools/rpc/probe.dart 192.168.1.75 NymeaEnergy.GetLoadConfig
|
||||
dart tools/rpc/probe.dart 192.168.1.75 JSONRPC.Introspect > introspect.json
|
||||
dart tools/rpc/probe.dart 192.168.1.75 Integrations.GetThings
|
||||
dart tools/rpc/probe.dart 192.168.1.75 NymeaEnergy.GetChargingInfos '{"evChargerId":"…"}'
|
||||
```
|
||||
|
||||
Le `Hello` part toujours en premier et son contenu va sur **stderr**, la réponse sur
|
||||
**stdout** — d'où le `> fichier.json` propre ci-dessus.
|
||||
|
||||
### `notifications.dart`
|
||||
|
||||
```bash
|
||||
dart tools/rpc/notifications.dart 192.168.1.75 90 # écoute 90 s
|
||||
```
|
||||
|
||||
Utile pour répondre à « est-ce que cette information franchit la frontière RPC ? ». Un
|
||||
namespace qui ne produit rien pendant que le système travaille est une réponse, pas un
|
||||
silence à interpréter.
|
||||
|
||||
### `locale.dart` / `locale_diff.dart`
|
||||
|
||||
`locale.dart` montre que nymea gère la locale **par connexion** (deux sockets, deux
|
||||
langues). `locale_diff.dart` répond à la question qui compte vraiment : **est-ce que ça
|
||||
change quelque chose ?** Relevé du 2026-08-25 sur `.75` — sur 784 chaînes affichables,
|
||||
`de_DE` en change **1**, `fr_FR`/`it_IT`/`es_ES`/`nl_NL` **zéro**.
|
||||
|
||||
### `loadconfig_roundtrip.dart` — ⚠️ écrit
|
||||
|
||||
```bash
|
||||
dart tools/rpc/loadconfig_roundtrip.dart 192.168.1.75 --yes
|
||||
```
|
||||
|
||||
`SetLoadConfig` **remplace l'ensemble** des charges. Le contenu renvoyé est identique à ce
|
||||
qui vient d'être lu, donc l'opération est neutre — mais elle reste une écriture sur une
|
||||
installation vivante, et elle déclenche une réévaluation des adaptateurs. Le drapeau
|
||||
`--yes` est obligatoire pour qu'elle ne parte jamais par réflexe de flèche haut.
|
||||
|
||||
Pour confirmer qu'aucune reconstruction d'adaptateurs n'a lieu (ECS-412, `m_builtFrom` +
|
||||
`sameHardware()`), lire le journal de la box en parallèle :
|
||||
|
||||
```bash
|
||||
ssh etm@192.168.1.75 'journalctl -u nymead -f'
|
||||
```
|
||||
|
||||
## Bancs
|
||||
|
||||
| Cible | Particularité |
|
||||
|---|---|
|
||||
| `192.168.1.75` | **ouverte** (`authenticationRequired: false`) — aucune auth à gérer dans les sondes. Une seule charge (`chauffe-eau`, relay-router). |
|
||||
| `192.168.1.120` | **auth requise** — ces sondes ne gèrent PAS `Authenticate`. À étendre avant usage. |
|
||||
77
tools/rpc/loadconfig_roundtrip.dart
Normal file
77
tools/rpc/loadconfig_roundtrip.dart
Normal file
@ -0,0 +1,77 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
// §7-1 — aller-retour neutre : GetLoadConfig -> SetLoadConfig verbatim -> GetLoadConfig.
|
||||
//
|
||||
// ⚠️ SEUL SCRIPT DE tools/rpc/ QUI ÉCRIT SUR LA BOX.
|
||||
// `SetLoadConfig` remplace l'ENSEMBLE des charges et déclenche une réévaluation
|
||||
// des adaptateurs. Le contenu renvoyé est identique à ce qui vient d'être lu, donc
|
||||
// l'opération est neutre — mais elle reste une écriture sur une installation vivante.
|
||||
// Le drapeau --yes est obligatoire, précisément pour qu'elle ne parte jamais par
|
||||
// réflexe de flèche haut dans un terminal.
|
||||
//
|
||||
// Usage : dart tools/rpc/loadconfig_roundtrip.dart <host> --yes
|
||||
Future<void> main(List<String> args) async {
|
||||
if (!args.contains('--yes')) {
|
||||
stderr.writeln('Ce script ÉCRIT sur la box (NymeaEnergy.SetLoadConfig).');
|
||||
stderr.writeln('Relancer avec --yes pour confirmer :');
|
||||
stderr.writeln(' dart tools/rpc/loadconfig_roundtrip.dart '
|
||||
'${args.isNotEmpty ? args[0] : "192.168.1.75"} --yes');
|
||||
exitCode = 2;
|
||||
return;
|
||||
}
|
||||
final host = args.isNotEmpty && !args.first.startsWith('--')
|
||||
? args.first
|
||||
: '192.168.1.75';
|
||||
final ws = await WebSocket.connect('ws://$host:4444');
|
||||
var id = 0;
|
||||
final pending = <int, Completer<Map<String, dynamic>>>{};
|
||||
final notifs = <String>[];
|
||||
ws.listen((raw) {
|
||||
final m = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||
final rid = m['id'];
|
||||
if (rid is int && pending.containsKey(rid)) {
|
||||
pending.remove(rid)!.complete(m);
|
||||
} else if (m['notification'] != null) {
|
||||
notifs.add(m['notification'] as String);
|
||||
}
|
||||
});
|
||||
Future<Map<String, dynamic>> call(String me, [Map<String, dynamic> p = const {}]) {
|
||||
final i = ++id;
|
||||
final c = Completer<Map<String, dynamic>>();
|
||||
pending[i] = c;
|
||||
ws.add(jsonEncode({'id': i, 'method': me, 'params': p}));
|
||||
return c.future.timeout(const Duration(seconds: 20));
|
||||
}
|
||||
|
||||
await call('JSONRPC.Hello', {'locale': 'fr_FR'});
|
||||
await call('JSONRPC.SetNotificationStatus', {'enabled': true});
|
||||
|
||||
final before = await call('NymeaEnergy.GetLoadConfig');
|
||||
final cfgBefore = before['params']['loadConfigs'] as List;
|
||||
final jsonBefore = const JsonEncoder.withIndent(' ').convert(cfgBefore);
|
||||
print('--- AVANT ---\n$jsonBefore\n');
|
||||
|
||||
print('--- SetLoadConfig VERBATIM (aucune modification) ---');
|
||||
final set = await call('NymeaEnergy.SetLoadConfig', {'loadConfigs': cfgBefore});
|
||||
print('réponse: ${jsonEncode(set)}\n');
|
||||
|
||||
await Future<void>.delayed(const Duration(seconds: 3));
|
||||
|
||||
final after = await call('NymeaEnergy.GetLoadConfig');
|
||||
final cfgAfter = after['params']['loadConfigs'] as List;
|
||||
final jsonAfter = const JsonEncoder.withIndent(' ').convert(cfgAfter);
|
||||
|
||||
print('--- notifications reçues pendant l\'opération ---');
|
||||
print(notifs.isEmpty ? '(aucune)' : notifs.join('\n'));
|
||||
|
||||
print('\n--- VERDICT ---');
|
||||
if (jsonBefore == jsonAfter) {
|
||||
print('IDENTIQUE — aller-retour neutre OK');
|
||||
} else {
|
||||
print('DIVERGENCE :');
|
||||
print('APRES:\n$jsonAfter');
|
||||
}
|
||||
await ws.close();
|
||||
}
|
||||
93
tools/rpc/locale.dart
Normal file
93
tools/rpc/locale.dart
Normal file
@ -0,0 +1,93 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
class Rpc {
|
||||
final WebSocket ws;
|
||||
int _id = 0;
|
||||
final _pending = <int, Completer<Map<String, dynamic>>>{};
|
||||
Rpc(this.ws) {
|
||||
ws.listen((raw) {
|
||||
final m = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||
final rid = m['id'];
|
||||
if (rid is int && _pending.containsKey(rid)) _pending.remove(rid)!.complete(m);
|
||||
});
|
||||
}
|
||||
Future<Map<String, dynamic>> call(String m, [Map<String, dynamic> p = const {}]) {
|
||||
final id = ++_id;
|
||||
final c = Completer<Map<String, dynamic>>();
|
||||
_pending[id] = c;
|
||||
ws.add(jsonEncode({'id': id, 'method': m, 'params': p}));
|
||||
return c.future.timeout(const Duration(seconds: 20));
|
||||
}
|
||||
}
|
||||
|
||||
Future<Rpc> open(String host, String? locale) async {
|
||||
final ws = await WebSocket.connect('ws://$host:4444');
|
||||
final rpc = Rpc(ws);
|
||||
final hello = await rpc.call('JSONRPC.Hello',
|
||||
locale == null ? {} : {'locale': locale});
|
||||
final p = hello['params'] as Map<String, dynamic>;
|
||||
print('Hello(locale=${locale ?? "<omis>"}) -> locale renvoyée: "${p['locale']}" language: "${p['language']}"');
|
||||
return rpc;
|
||||
}
|
||||
|
||||
/// Renvoie quelques displayName de ThingClass + leurs states/actions.
|
||||
Future<void> sample(Rpc rpc, String tag) async {
|
||||
final r = await rpc.call('Integrations.GetThingClasses', {});
|
||||
final classes = (r['params']['thingClasses'] as List).cast<Map<String, dynamic>>();
|
||||
print(' [$tag] ${classes.length} thingClasses');
|
||||
for (final name in ['Smart Meter', 'Wallbox', 'Relay', 'Light']) {
|
||||
final hit = classes.firstWhere(
|
||||
(c) => (c['displayName'] as String?)?.contains(name) ?? false,
|
||||
orElse: () => {});
|
||||
if (hit.isNotEmpty) {
|
||||
print(' [$tag] ThingClass "${hit['displayName']}" (name=${hit['name']})');
|
||||
final states = (hit['stateTypes'] as List?) ?? [];
|
||||
if (states.isNotEmpty) {
|
||||
print(' [$tag] state[0].displayName = "${states.first['displayName']}"');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Premiers displayName bruts, pour comparaison inter-locale
|
||||
final first = classes.take(5).map((c) => c['displayName']).toList();
|
||||
print(' [$tag] 5 premiers: $first');
|
||||
|
||||
final v = await rpc.call('Integrations.GetVendors', {});
|
||||
final vendors = (v['params']['vendors'] as List).cast<Map<String, dynamic>>();
|
||||
print(' [$tag] vendors: ${vendors.take(5).map((e) => e['displayName']).toList()}');
|
||||
}
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
final host = args.isNotEmpty ? args[0] : '192.168.1.75';
|
||||
|
||||
print('=== 1. Deux connexions SIMULTANÉES, locales différentes ===');
|
||||
final de = await open(host, 'de_DE');
|
||||
final fr = await open(host, 'fr_FR');
|
||||
await sample(de, 'de_DE');
|
||||
await sample(fr, 'fr_FR');
|
||||
|
||||
print('\n=== 2. Locale omise (défaut du processus) ===');
|
||||
final def = await open(host, null);
|
||||
await sample(def, 'défaut');
|
||||
|
||||
print('\n=== 3. Locale absurde ===');
|
||||
final zz = await open(host, 'zz_ZZ');
|
||||
await sample(zz, 'zz_ZZ');
|
||||
|
||||
print('\n=== 4. Re-Hello sur une connexion existante (de_DE -> fr_FR) ===');
|
||||
final h = await de.call('JSONRPC.Hello', {'locale': 'fr_FR'});
|
||||
print(' locale renvoyée après re-Hello: "${h['params']['locale']}"');
|
||||
await sample(de, 'de->fr');
|
||||
|
||||
print('\n=== 5. Nos chaînes NymeaEnergy ===');
|
||||
final lc = await fr.call('NymeaEnergy.GetLoadConfig', {});
|
||||
print(' [fr_FR] label de charge: '
|
||||
'${(lc['params']['loadConfigs'] as List).map((e) => e['label']).toList()}');
|
||||
|
||||
await de.ws.close();
|
||||
await fr.ws.close();
|
||||
await def.ws.close();
|
||||
await zz.ws.close();
|
||||
}
|
||||
70
tools/rpc/locale_diff.dart
Normal file
70
tools/rpc/locale_diff.dart
Normal file
@ -0,0 +1,70 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
Future<Map<String, dynamic>> dump(String host, String? locale) async {
|
||||
final ws = await WebSocket.connect('ws://$host:4444');
|
||||
var id = 0;
|
||||
final pending = <int, Completer<Map<String, dynamic>>>{};
|
||||
ws.listen((raw) {
|
||||
final m = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||
final rid = m['id'];
|
||||
if (rid is int && pending.containsKey(rid)) pending.remove(rid)!.complete(m);
|
||||
});
|
||||
Future<Map<String, dynamic>> call(String m, [Map<String, dynamic> p = const {}]) {
|
||||
final i = ++id;
|
||||
final c = Completer<Map<String, dynamic>>();
|
||||
pending[i] = c;
|
||||
ws.add(jsonEncode({'id': i, 'method': m, 'params': p}));
|
||||
return c.future.timeout(const Duration(seconds: 20));
|
||||
}
|
||||
await call('JSONRPC.Hello', locale == null ? {} : {'locale': locale});
|
||||
final tc = await call('Integrations.GetThingClasses', {});
|
||||
final vd = await call('Integrations.GetVendors', {});
|
||||
final pl = await call('Integrations.GetPlugins', {});
|
||||
await ws.close();
|
||||
return {'tc': tc['params'], 'vd': vd['params'], 'pl': pl['params']};
|
||||
}
|
||||
|
||||
/// Aplatit TOUTES les chaînes affichables en chemin -> valeur.
|
||||
void flatten(dynamic node, String path, Map<String, String> out) {
|
||||
if (node is Map) {
|
||||
for (final e in node.entries) {
|
||||
final k = e.key.toString();
|
||||
if (k == 'displayName' || k == 'displayNames' || k == 'displayMessage') {
|
||||
if (e.value is String) out['$path.$k'] = e.value as String;
|
||||
}
|
||||
flatten(e.value, '$path.$k', out);
|
||||
}
|
||||
} else if (node is List) {
|
||||
for (var i = 0; i < node.length; i++) {
|
||||
flatten(node[i], '$path[$i]', out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
final host = args.isNotEmpty ? args[0] : '192.168.1.75';
|
||||
final locales = ['en_US', 'de_DE', 'fr_FR', 'it_IT', 'es_ES', 'nl_NL'];
|
||||
final maps = <String, Map<String, String>>{};
|
||||
|
||||
for (final l in locales) {
|
||||
final d = await dump(host, l);
|
||||
final f = <String, String>{};
|
||||
flatten(d, 'root', f);
|
||||
maps[l] = f;
|
||||
print('$l : ${f.length} chaînes affichables');
|
||||
}
|
||||
|
||||
final ref = maps['en_US']!;
|
||||
print('\n--- différences par rapport à en_US ---');
|
||||
for (final l in locales.where((e) => e != 'en_US')) {
|
||||
final m = maps[l]!;
|
||||
final diffs = <String>[];
|
||||
for (final k in ref.keys) {
|
||||
if (m.containsKey(k) && m[k] != ref[k]) diffs.add('$k : "${ref[k]}" -> "${m[k]}"');
|
||||
}
|
||||
print('$l : ${diffs.length} chaîne(s) différente(s)');
|
||||
for (final d in diffs.take(12)) print(' $d');
|
||||
}
|
||||
}
|
||||
55
tools/rpc/notifications.dart
Normal file
55
tools/rpc/notifications.dart
Normal file
@ -0,0 +1,55 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
Future<void> main(List<String> args) async {
|
||||
final host = args.isNotEmpty ? args[0] : '192.168.1.75';
|
||||
final secs = args.length > 1 ? int.parse(args[1]) : 90;
|
||||
final ws = await WebSocket.connect('ws://$host:4444');
|
||||
var id = 0;
|
||||
final pending = <int, Completer<Map<String, dynamic>>>{};
|
||||
final counts = <String, int>{};
|
||||
final samples = <String, String>{};
|
||||
|
||||
ws.listen((raw) {
|
||||
final m = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||
final rid = m['id'];
|
||||
if (rid is int && pending.containsKey(rid)) {
|
||||
pending.remove(rid)!.complete(m);
|
||||
return;
|
||||
}
|
||||
final n = m['notification'] as String?;
|
||||
if (n != null) {
|
||||
counts[n] = (counts[n] ?? 0) + 1;
|
||||
samples.putIfAbsent(n, () => jsonEncode(m['params']));
|
||||
}
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> call(String meth, [Map<String, dynamic> p = const {}]) {
|
||||
final i = ++id;
|
||||
final c = Completer<Map<String, dynamic>>();
|
||||
pending[i] = c;
|
||||
ws.add(jsonEncode({'id': i, 'method': meth, 'params': p}));
|
||||
return c.future.timeout(const Duration(seconds: 15));
|
||||
}
|
||||
|
||||
await call('JSONRPC.Hello');
|
||||
final r = await call('JSONRPC.SetNotificationStatus', {'enabled': true});
|
||||
print('Namespaces abonnés: ${r['params']['namespaces']}');
|
||||
print('Écoute ${secs}s…\n');
|
||||
|
||||
await Future<void>.delayed(Duration(seconds: secs));
|
||||
|
||||
print('--- notifications reçues en ${secs}s ---');
|
||||
final keys = counts.keys.toList()..sort();
|
||||
for (final k in keys) {
|
||||
print('${counts[k]!.toString().padLeft(4)} $k');
|
||||
}
|
||||
print('\n--- premier échantillon par type ---');
|
||||
for (final k in keys) {
|
||||
var s = samples[k]!;
|
||||
if (s.length > 260) s = '${s.substring(0, 260)}…';
|
||||
print('$k\n $s');
|
||||
}
|
||||
await ws.close();
|
||||
}
|
||||
45
tools/rpc/probe.dart
Normal file
45
tools/rpc/probe.dart
Normal file
@ -0,0 +1,45 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
// Sonde JSON-RPC read-only contre le HEMS .75.
|
||||
// Usage: dart probe.dart <host> <method> [jsonParams]
|
||||
Future<void> main(List<String> args) async {
|
||||
final host = args.isNotEmpty ? args[0] : '192.168.1.75';
|
||||
final method = args.length > 1 ? args[1] : 'NymeaEnergy.GetLoadConfig';
|
||||
final params = args.length > 2
|
||||
? jsonDecode(args[2]) as Map<String, dynamic>
|
||||
: <String, dynamic>{};
|
||||
|
||||
final ws = await WebSocket.connect('ws://$host:4444')
|
||||
.timeout(const Duration(seconds: 8));
|
||||
var id = 0;
|
||||
final pending = <int, Completer<Map<String, dynamic>>>{};
|
||||
|
||||
ws.listen((raw) {
|
||||
final m = jsonDecode(raw as String) as Map<String, dynamic>;
|
||||
final rid = m['id'];
|
||||
if (rid is int && pending.containsKey(rid)) {
|
||||
pending.remove(rid)!.complete(m);
|
||||
} else if (m['notification'] != null) {
|
||||
stderr.writeln('[notif] ${m['notification']}');
|
||||
}
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> call(String m, [Map<String, dynamic> p = const {}]) {
|
||||
final myId = ++id;
|
||||
final c = Completer<Map<String, dynamic>>();
|
||||
pending[myId] = c;
|
||||
ws.add(jsonEncode({'id': myId, 'method': m, 'params': p}));
|
||||
return c.future.timeout(const Duration(seconds: 15));
|
||||
}
|
||||
|
||||
final hello = await call('JSONRPC.Hello');
|
||||
stderr.writeln('--- JSONRPC.Hello ---');
|
||||
stderr.writeln(const JsonEncoder.withIndent(' ').convert(hello['params']));
|
||||
|
||||
final res = await call(method, params);
|
||||
stdout.writeln(const JsonEncoder.withIndent(' ').convert(res));
|
||||
|
||||
await ws.close();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user