/// Les valeurs de runtime d'une borne — **constatées, jamais réglées**. /// /// ## Le piège est un mot, pas un calcul /// /// « Minimum » désigne deux choses de natures différentes : /// /// - le **plancher déclaré** d'une charge modulable : une propriété de l'appareil, saisie /// une fois, stable — elle se configure ; /// - le **minimum d'une borne** : `max(borne, voiture)`, qui dépend du véhicule branché et /// change avec lui. Il dépend aussi du nombre de phases visé — de l'ordre de 1,4 kW en /// monophasé contre 4,1 kW en triphasé. /// /// Les ranger au même endroit, ou les rendre modifiables de la même façon, ferait croire /// qu'on peut régler ce qui se constate. Ces tests figent la frontière. /// /// ## Et la valeur s'affiche telle que la box la donne /// /// Pas de conversion côté app entre ampères et watts, pas de recalcul selon les phases : /// c'est exactement le genre de miroir qui finit par diverger, et le lot précédent en a /// déjà retiré un — la combinatoire des paliers, désormais publiée par la box. library; import 'package:etm_powersync_app/models/load_telemetry.dart'; import 'package:flutter_test/flutter_test.dart'; LoadTelemetryEntry _borne(Map mech) => LoadTelemetryEntry.fromJson({ 'loadId': '{88160e45-7df2-48cc-9639-f0d15ae6e382}', 'allocatedW': 0.0, 'available': true, 'decision': {'code': 'SURPLUS_INSUFFICIENT', 'params': {'budgetW': 0}}, 'mechanism': mech, }); void main() { group('le minimum d\'une borne est du RUNTIME', () { test('il est lu tel quel, sans conversion ni recalcul', () { // Monophasé : la box publie ~1 400 W. L'app ne dérive pas cette valeur d'un courant // et d'un nombre de phases — elle la prend. final mono = _borne({ 'kind': 'evcharger', 'phaseCount': 1, 'currentA': 6.0, 'minPowerW': 1400.0, 'pluggedIn': true, }); expect(mono.mechanism!.minPowerW, 1400.0); expect(mono.mechanism!.phaseCount, 1); // Triphasé : ~4 100 W pour le MÊME courant. Si l'app convertissait elle-même, elle // devrait deviner laquelle des deux règles s'applique — et se tromperait un jour. final tri = _borne({ 'kind': 'evcharger', 'phaseCount': 3, 'currentA': 6.0, 'minPowerW': 4100.0, 'pluggedIn': true, }); expect(tri.mechanism!.minPowerW, 4100.0); expect(tri.mechanism!.currentA, mono.mechanism!.currentA, reason: 'même courant, minimum différent : la conversion n\'appartient pas ' 'à l\'app'); }); test('sans véhicule branché, la valeur peut disparaître — et ce n\'est pas 0', () { final vide = _borne({'kind': 'evcharger', 'pluggedIn': false}); expect(vide.mechanism!.minPowerW, isNull, reason: 'le minimum dépend de la voiture : sans voiture, il n\'y a pas de ' 'valeur, et zéro serait un mensonge'); expect(vide.mechanism!.pluggedIn, isFalse); }); test('rien n\'est publié tant que la recharge n\'est pas arbitrée', () { // Aujourd'hui la borne n'est pas dans loads[] : le bloc de runtime reste muet, ce // qui est la bonne réponse et non un trou. final sansMecanisme = LoadTelemetryEntry.fromJson({ 'loadId': 'x', 'allocatedW': 0.0, 'available': true, 'decision': {'code': 'X', 'params': {}}, }); expect(sansMecanisme.mechanism, isNull); }); }); test('le plancher DÉCLARÉ d\'une charge modulable est un autre objet', () { // Même clé de télémétrie, autre nature : ici c'est une propriété de l'appareil, et // elle a un champ de configuration correspondant (à venir au §12). Le minimum d'une // borne n'en aura jamais — il n'y a rien à y régler. final modulable = LoadTelemetryEntry.fromJson({ 'loadId': 'ecs-modbus', 'allocatedW': 1200.0, 'available': true, 'decision': {'code': 'SURPLUS_SETPOINT', 'params': {'budgetW': 1500}}, 'mechanism': {'kind': 'variable', 'maxPowerW': 3000.0, 'minPowerW': 300.0}, }); expect(modulable.mechanism!.minPowerW, 300.0); expect(modulable.mechanism!.phaseCount, isNull, reason: 'une charge modulable n\'a pas de nombre de phases : c\'est bien ' 'l\'absence de ce champ qui distingue les deux natures dans la charge utile'); }); }