Décision de Patrick : trois modes suffisent — Manuel, PV, Min+PV. Le boost n'est pas un quatrième mode, c'est la dérogation (+etm61). LE FAIT. Le bouton écrivait `ChargingModeNormal`, qui sort la borne de l'arbitrage et lui restitue ses valeurs manuelles stockées — manualChargingEnabled, manualMaxChargingCurrent, manualDesiredPhaseCount. Sur .75 elles valaient false / 6 A / 3 phases : mesuré le 2026-09-08 à 11:09:51, l'appui a ARRÊTÉ la charge et armé un verrou de 300 s. Pas une régression — c'est ce que Normal a toujours fait. C'est le nom qui mentait. Renommé jusque dans le code (`ChargingMode.boost` -> `ChargingMode.manuel`), pas seulement sur l'étiquette : un identifiant faux reste un piège pour le prochain lecteur, et c'est précisément ce qui a duré des mois ici. L'ordre met Manuel en tête, parce que ChargingModeNormal est aussi le mode d'office d'une borne détectée. ET LE RENOMMAGE NE SUFFISAIT PAS. « Manuel » dit QUI commande, pas ce que la borne va faire : tant que l'app n'offre pas de régler ces valeurs stockées, le mode veut dire « ce qui traînait la dernière fois ». Passer en manuel demande donc confirmation, et la confirmation dit la conséquence — la charge peut s'arrêter — AVANT, pas après. ⚠️ La confirmation ne se déclenche pas sur un ré-appui du mode déjà en place : une confirmation qu'on apprend à cliquer sans lire ne protège plus de rien. Un test le fige. Vérifié sur .75 (1.15.2+etm61) : SetLoadDerogation et ClearLoadDerogation sont dans Introspect, avec derogationEnded / durationS / restoredMode. Le contrat de la dérogation est écrit dans CLAUDE.md — durée obligatoire bornée [60, 21600], état lu dans decision.code == "DEROGATION_ACTIVE" et jamais dans le mode, fin au niveau de la trame avec six causes. Aucun bouton de boost n'est écrit : c'est le lot suivant. Et une sonde pour le moteur, faite au passage : la V2C est bien restée en ChargingModeNormal et elle est ABSENTE de loads[] — hors arbitrage. Leur hypothèse est confirmée sans redémarrage. 339 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
201 lines
8.8 KiB
Dart
201 lines
8.8 KiB
Dart
/// La tuile « Borne EV » **rendue muette** — éprouvée sur ce qu'elle affiche.
|
|
///
|
|
/// Le défaut qu'elle corrigeait ne se voyait pas en test de fonction : les trois grandeurs
|
|
/// affichées (« 3600 W », « 82 % solaire », « Mode PV ») étaient des valeurs par défaut de
|
|
/// `EnergyData`, justes au sens du type et fausses à l'écran. Ces tests montent donc le
|
|
/// widget et lisent le texte, comme ceux des notes de configuration.
|
|
library;
|
|
|
|
import 'package:etm_powersync_app/models/ev_charger_live.dart';
|
|
import 'package:etm_powersync_app/widgets/ev_favorite_tile.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
Widget _harness({required EvChargerLive? live}) =>
|
|
MaterialApp(home: Scaffold(body: EvFavoriteTile(live: live)));
|
|
|
|
/// La V2C du banc, telle que `.75` la publiait le 2026-08-28 : états `pluggedIn`,
|
|
/// `charging`, `currentPower` sur le Thing, mode par `GetChargingInfos`.
|
|
EvChargerLive _v2c({
|
|
bool? pluggedIn = false,
|
|
bool? charging = false,
|
|
double? powerW = 0,
|
|
String? apiMode = 'ChargingModeNormal',
|
|
}) =>
|
|
EvChargerLive(
|
|
thingId: '4d1adafd-cafc-4813-93b1-ca8933ce71ab',
|
|
name: 'V2C Trydan',
|
|
pluggedIn: pluggedIn,
|
|
charging: charging,
|
|
currentPowerW: powerW,
|
|
apiMode: apiMode,
|
|
);
|
|
|
|
void main() {
|
|
group('borne injoignable — ni le zéro forgé, ni la valeur périmée', () {
|
|
// Mesuré sur `.75` le 2026-08-30 : quand le lien Modbus de la V2C tombe, la box
|
|
// publie `connected: false`, `pluggedIn: false`, `charging: false`,
|
|
// `currentPower: 0` et un `statusMessage`. La voiture, elle, était branchée et en
|
|
// charge à 4 356 W. `NymeaService.evChargerFor` jette ces zéros ; ces tests
|
|
// vérifient ce que la tuile en fait.
|
|
EvChargerLive horsLigne({String? message}) => EvChargerLive(
|
|
thingId: '4d1adafd-cafc-4813-93b1-ca8933ce71ab',
|
|
name: 'V2C Trydan',
|
|
apiMode: 'ChargingModeNormal',
|
|
joignable: false,
|
|
messageEtat: message,
|
|
);
|
|
|
|
testWidgets('elle dit la coupure, avec la phrase du plugin', (tester) async {
|
|
await tester.pumpWidget(_harness(
|
|
live: horsLigne(
|
|
message: 'Hors ligne depuis 30/08 12:39 — timeout Modbus — '
|
|
'vérifier signal WiFi de la borne')));
|
|
|
|
expect(find.text('Hors ligne'), findsOneWidget);
|
|
expect(find.textContaining('timeout Modbus'), findsOneWidget);
|
|
// …et surtout PAS ce que la box publiait au même instant.
|
|
expect(find.textContaining('Aucun véhicule branché'), findsNothing);
|
|
expect(find.textContaining('0 W'), findsNothing);
|
|
});
|
|
|
|
testWidgets('sans phrase du plugin, elle nomme quand même l\'ignorance',
|
|
(tester) async {
|
|
await tester.pumpWidget(_harness(live: horsLigne()));
|
|
expect(find.textContaining('ne sait plus si un véhicule est branché'),
|
|
findsOneWidget);
|
|
});
|
|
|
|
testWidgets('le MODE reste lu — il vient de la box, pas de la borne',
|
|
(tester) async {
|
|
// Un lien coupé n'invalide pas un réglage stocké dans la box : le cacher
|
|
// priverait le client de la seule chose encore vraie à l'écran.
|
|
await tester.pumpWidget(_harness(live: horsLigne()));
|
|
expect(find.textContaining('Mode Manuel'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('`joignable == null` n\'est PAS hors ligne', (tester) async {
|
|
// Une borne qui ne publie pas `connected` ne dit rien de son lien. Supposer la
|
|
// panne serait le symétrique exact du zéro forgé.
|
|
await tester.pumpWidget(_harness(
|
|
live: _v2c(pluggedIn: true, charging: true, powerW: 4356)));
|
|
expect(find.text('Hors ligne'), findsNothing);
|
|
expect(find.text('4356 W'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
group('la tuile ne montre que ce que la box publie', () {
|
|
testWidgets('sans borne, elle le dit — elle ne disparaît pas',
|
|
(tester) async {
|
|
// Retirer la tuile laisserait un trou dans une liste que le client a composée.
|
|
await tester.pumpWidget(_harness(live: null));
|
|
expect(find.textContaining('Aucune borne déclarée'), findsOneWidget);
|
|
expect(find.textContaining('W'), findsNothing);
|
|
});
|
|
|
|
testWidgets('une borne qui ne publie rien n\'affiche AUCUN zéro',
|
|
(tester) async {
|
|
await tester.pumpWidget(_harness(
|
|
live: _v2c(
|
|
pluggedIn: null, charging: null, powerW: null, apiMode: null)));
|
|
|
|
// Les trois absences se disent, chacune à sa place.
|
|
expect(find.text('Puissance non publiée'), findsOneWidget);
|
|
expect(find.textContaining('Branchement non publié'), findsOneWidget);
|
|
expect(find.text('Mode non lu'), findsOneWidget);
|
|
// Et surtout : pas de « 0 W », qui se lirait « la borne ne consomme rien ».
|
|
expect(find.textContaining('0 W'), findsNothing);
|
|
});
|
|
|
|
testWidgets('elle affiche la puissance MESURÉE et le mode DE LA BOX',
|
|
(tester) async {
|
|
await tester.pumpWidget(_harness(
|
|
live: _v2c(
|
|
pluggedIn: true,
|
|
charging: true,
|
|
powerW: 3907,
|
|
apiMode: 'ChargingModeEco')));
|
|
|
|
expect(find.text('3907 W'), findsOneWidget);
|
|
expect(find.text('En charge'), findsOneWidget);
|
|
expect(find.text('Mode PV'), findsOneWidget);
|
|
expect(find.text('V2C Trydan'), findsOneWidget);
|
|
// Aucune part solaire : la box ne dit pas d'où vient le courant d'une borne.
|
|
expect(find.textContaining('solaire'), findsNothing);
|
|
});
|
|
|
|
testWidgets('un mode à échéance se lit comme tel, un mode inconnu aussi',
|
|
(tester) async {
|
|
await tester.pumpWidget(
|
|
_harness(live: _v2c(apiMode: 'ChargingModeEcoMinWithTargetTime')));
|
|
expect(find.text('Mode Min+PV + échéance'), findsOneWidget);
|
|
|
|
// Une box peut porter un mode qu'aucune version d'app ne connaît. Deviner serait
|
|
// pire que le nommer.
|
|
await tester.pumpWidget(_harness(live: _v2c(apiMode: 'ChargingModeFutur')));
|
|
await tester.pump();
|
|
expect(find.textContaining('Mode non reconnu'), findsOneWidget);
|
|
});
|
|
|
|
testWidgets('branché mais à l\'arrêt n\'est pas « en charge »',
|
|
(tester) async {
|
|
await tester.pumpWidget(
|
|
_harness(live: _v2c(pluggedIn: true, charging: false, powerW: 0)));
|
|
expect(find.textContaining('branché, à l\'arrêt'), findsOneWidget);
|
|
// Ici le zéro est une MESURE, pas une absence : la borne publie `currentPower`.
|
|
expect(find.text('0 W'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
group('pleine puissance = hors arbitrage, et l\'écran le dit', () {
|
|
testWidgets('ChargingModeNormal porte la note — c\'est le mode d\'office',
|
|
(tester) async {
|
|
// Une borne détectée arrive en ChargingModeNormal : pleine puissance, hors
|
|
// arbitrage. Sans cette ligne, l'installateur pose une borne, elle charge à fond,
|
|
// et il cherche pourquoi le HEMS l'ignore.
|
|
await tester.pumpWidget(_harness(live: _v2c(apiMode: 'ChargingModeNormal')));
|
|
|
|
expect(find.textContaining('hors arbitrage'), findsOneWidget);
|
|
expect(find.textContaining('mode d\'office'), findsOneWidget);
|
|
// Elle n'accuse personne : la box publie le même code pour un boost délibéré.
|
|
final note = tester
|
|
.widget<Text>(find.textContaining('hors arbitrage'))
|
|
.data!;
|
|
expect(note, isNot(contains('personne')));
|
|
expect(note, isNot(contains('oubli')));
|
|
});
|
|
|
|
testWidgets('un mode éco la fait disparaître', (tester) async {
|
|
await tester.pumpWidget(_harness(live: _v2c(apiMode: 'ChargingModeEco')));
|
|
expect(find.textContaining('hors arbitrage'), findsNothing);
|
|
});
|
|
|
|
testWidgets('mode non lu : aucune note — on ne conclut pas d\'une absence',
|
|
(tester) async {
|
|
await tester.pumpWidget(_harness(live: _v2c(apiMode: null)));
|
|
expect(find.textContaining('hors arbitrage'), findsNothing);
|
|
expect(find.text('Mode non lu'), findsOneWidget);
|
|
});
|
|
});
|
|
|
|
group('la tuile est en LECTURE SEULE depuis le 2026-08-28', () {
|
|
testWidgets('aucun bouton de mode — l\'écriture a un seul lieu, « Configurer »',
|
|
(tester) async {
|
|
// Deux écrans qui écrivent SetChargingInfo sont deux comportements qui divergent.
|
|
await tester.pumpWidget(_harness(live: _v2c()));
|
|
for (final label in ['Manuel', 'PV', 'Min+PV']) {
|
|
expect(find.widgetWithText(OutlinedButton, label), findsNothing,
|
|
reason: '$label ne doit plus être écrivable depuis les favoris');
|
|
}
|
|
expect(find.byType(OutlinedButton), findsNothing);
|
|
});
|
|
|
|
testWidgets('mais l\'avertissement « hors arbitrage » reste, lui', (tester) async {
|
|
// C'est le client qui ouvre ses favoris : il doit voir que sa borne n'est pas
|
|
// pilotée, même s'il doit aller ailleurs pour le corriger.
|
|
await tester.pumpWidget(_harness(live: _v2c(apiMode: 'ChargingModeNormal')));
|
|
expect(find.textContaining('hors arbitrage'), findsOneWidget);
|
|
});
|
|
});
|
|
}
|