Deux suites directes du geste réel du 2026-08-28 sur le banc.
── 1. Une vérification qui arrive trop tard ne prouve rien ───────────────────
Sur le banc, la suppression a été vérifiée à la main, APRÈS un autre geste : entre-temps un
« Enregistrer » avait regroupé et renuméroté (chauffe-eau 1 → 3). Il est alors devenu
impossible de dire laquelle des deux écritures avait déplacé les rangs. Le journal de la box
montrait deux persistances à deux minutes d'écart, rien de plus.
`removeLoad` relit donc la configuration IMMÉDIATEMENT après l'écriture — sans attendre
LoadConfigChanged, qui laisserait une fenêtre où un autre geste s'intercale — et compare
clé par clé ce qu'elle a envoyé à ce que la box porte (diffPayloads, fonction pure). Les
survivantes doivent être identiques, rangs compris : c'est le contrôle de l'aller-retour
neutre appliqué à une écriture qui RETIRE.
Le message le dit : « Relu sur la box : les autres sont identiques clé par clé, rangs
compris », ou l'écart en rouge, nommé champ par champ. Sur le seul geste de l'app qui
détruit de la configuration, « la box a dit oui » ne suffisait pas.
La comparaison est structurelle : relays, sgReady et powerLevels sont des listes et des
objets, que `==` compare par référence — l'alerte aurait crié à chaque suppression, donc
plus personne ne l'aurait lue.
── 2. Le sélecteur de compteur était plus restrictif que la box ─────────────
Il filtrait sur l'interface smartmeter. Le moteur n'en regarde AUCUNE : findConfiguredThing
puis hasState("currentPower") (attachMeasurements, LM-1101). Sur .75, la classe wallbox
déclare smartmeter, la classe trydan non — la V2C était donc impossible à désigner comme
son propre compteur, alors que la box l'aurait accepté et que c'est le seul moyen de voir
« mesuré » à côté de « alloué » sur sa carte.
Le sélecteur accepte maintenant, en plus des smartmeter, les Things d'une interface
élargie qui publient l'état exigé par le moteur — ici evcharger + currentPower. Il s'élargit
là où la box est plus large, pas partout : une borne qui ne publie aucune puissance reste
dehors, sans quoi on écrirait une configuration muette sans erreur.
⚠️ Ce n'est pas la fin de l'écart : la classe trydan devrait déclarer smartmeter, ce qui
suppose un CUMUL d'énergie qu'elle ne publie pas — même manque que celui qui a bloqué
LM-1009. Lot plugin noté au TODO avec cette raison.
── Banc ──────────────────────────────────────────────────────────────────────
Chauffe-eau remis au rang 1 (lire → patcher → réécrire, seul `priority` touché, relecture
vérifiée : la box porte exactement ce qui a été envoyé). Ordre courant : chauffe-eau,
V2C, wallbox simulée, PAC.
188 tests (+7), flutter analyze inchangé à 27 remarques.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
147 lines
5.0 KiB
Dart
147 lines
5.0 KiB
Dart
/// Le sélecteur de compteur était **plus restrictif que la box**.
|
|
///
|
|
/// Il filtrait sur l'interface `smartmeter`. Le moteur, lui, n'en regarde aucune : il fait
|
|
/// `findConfiguredThing(meterThingId)` puis `hasState("currentPower")`
|
|
/// (`attachMeasurements`, LM-1101).
|
|
///
|
|
/// Constaté sur `.75` le 2026-08-28 : la classe `wallbox` (borne simulée) déclare
|
|
/// `smartmeter`, la classe `trydan` (V2C) non — `connectable, evcharger, power` seulement.
|
|
/// La V2C était donc impossible à désigner comme son propre compteur, alors que c'est le
|
|
/// seul moyen de faire apparaître « mesuré » à côté de « alloué » sur sa carte, et que la
|
|
/// box l'aurait accepté.
|
|
library;
|
|
|
|
import 'package:etm_powersync_app/models/nymea_models.dart';
|
|
import 'package:etm_powersync_app/services/nymea_service.dart';
|
|
import 'package:etm_powersync_app/widgets/thing_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
NymeaStateType _st(String id, String nom, {String type = 'Double'}) =>
|
|
NymeaStateType(id: id, name: nom, displayName: nom, type: type, unit: '');
|
|
|
|
/// Le parc de `.75`, réduit aux trois cas qui comptent.
|
|
NymeaService _service() => NymeaService()
|
|
..adoptThingsForTest(
|
|
[
|
|
const NymeaThing(
|
|
id: 'v2c',
|
|
name: 'V2C Trydan',
|
|
thingClassId: 'cls-trydan',
|
|
setupStatus: 'ok',
|
|
paramValues: []),
|
|
const NymeaThing(
|
|
id: 'sim',
|
|
name: 'Simulated wallbox',
|
|
thingClassId: 'cls-wallbox',
|
|
setupStatus: 'ok',
|
|
paramValues: []),
|
|
const NymeaThing(
|
|
id: 'ecs',
|
|
name: 'ECS-Meter',
|
|
thingClassId: 'cls-meter',
|
|
setupStatus: 'ok',
|
|
paramValues: []),
|
|
// Une borne SANS puissance publiée : élargir le filtre ne doit pas la faire entrer.
|
|
const NymeaThing(
|
|
id: 'muette',
|
|
name: 'Borne muette',
|
|
thingClassId: 'cls-borne-muette',
|
|
setupStatus: 'ok',
|
|
paramValues: []),
|
|
],
|
|
[
|
|
NymeaThingClass(
|
|
id: 'cls-trydan',
|
|
name: 'trydan',
|
|
displayName: 'Trydan',
|
|
interfaces: const ['connectable', 'evcharger', 'power'],
|
|
stateTypes: [_st('p', 'currentPower')],
|
|
),
|
|
NymeaThingClass(
|
|
id: 'cls-wallbox',
|
|
name: 'wallbox',
|
|
displayName: 'Simulated wallbox',
|
|
interfaces: const ['evcharger', 'power', 'smartmeter', 'smartmeterconsumer'],
|
|
stateTypes: [_st('p', 'currentPower')],
|
|
),
|
|
NymeaThingClass(
|
|
id: 'cls-meter',
|
|
name: 'smartMeterConsumer',
|
|
displayName: 'Compteur',
|
|
interfaces: const ['smartmeter', 'smartmeterconsumer'],
|
|
stateTypes: [_st('p', 'currentPower')],
|
|
),
|
|
NymeaThingClass(
|
|
id: 'cls-borne-muette',
|
|
name: 'borneMuette',
|
|
displayName: 'Borne muette',
|
|
interfaces: const ['evcharger'],
|
|
stateTypes: [_st('c', 'charging', type: 'Bool')],
|
|
),
|
|
],
|
|
);
|
|
|
|
Widget _harness({
|
|
List<String> aussi = const [],
|
|
String? etatRequis,
|
|
}) =>
|
|
MaterialApp(
|
|
home: Scaffold(
|
|
body: ThingPicker(
|
|
service: _service(),
|
|
interfaceRequise: 'smartmeter',
|
|
interfacesAussi: aussi,
|
|
etatRequis: etatRequis,
|
|
valeur: null,
|
|
onChoisi: (_) {},
|
|
placeholder: 'Choisir un appareil de mesure…',
|
|
),
|
|
),
|
|
);
|
|
|
|
Future<List<String>> _liste(WidgetTester tester) async {
|
|
await tester.tap(find.text('Choisir un appareil de mesure…'));
|
|
await tester.pumpAndSettle();
|
|
return tester
|
|
.widgetList<Text>(find.byType(Text))
|
|
.map((t) => t.data ?? '')
|
|
.toList();
|
|
}
|
|
|
|
void main() {
|
|
testWidgets('sans élargissement, la V2C n\'est PAS proposée', (tester) async {
|
|
// L'état d'avant : le comportement qu'on corrige, figé pour qu'on sache ce qu'il était.
|
|
await tester.pumpWidget(_harness());
|
|
final noms = await _liste(tester);
|
|
|
|
expect(noms, contains('ECS-Meter'));
|
|
expect(noms, contains('Simulated wallbox'));
|
|
expect(noms.contains('V2C Trydan'), isFalse);
|
|
});
|
|
|
|
testWidgets('élargi aux bornes qui publient currentPower, elle l\'est',
|
|
(tester) async {
|
|
await tester.pumpWidget(
|
|
_harness(aussi: const ['evcharger'], etatRequis: 'currentPower'));
|
|
final noms = await _liste(tester);
|
|
|
|
expect(noms, contains('V2C Trydan'));
|
|
// Les compteurs restent : le filtre s'élargit, il ne se remplace pas.
|
|
expect(noms, contains('ECS-Meter'));
|
|
expect(noms, contains('Simulated wallbox'));
|
|
});
|
|
|
|
testWidgets('une borne qui ne publie AUCUNE puissance reste dehors',
|
|
(tester) async {
|
|
// L'élargissement suit la règle du moteur — `hasState("currentPower")` — et pas
|
|
// « toute borne fera l'affaire ». Proposer un appareil qui ne mesure rien ferait
|
|
// écrire une configuration muette, sans erreur.
|
|
await tester.pumpWidget(
|
|
_harness(aussi: const ['evcharger'], etatRequis: 'currentPower'));
|
|
final noms = await _liste(tester);
|
|
|
|
expect(noms.contains('Borne muette'), isFalse);
|
|
});
|
|
}
|