etm-powersync-app/test/ev_runtime_test.dart
Patrick Schurig 449c6ac50a feat(3g): les valeurs de borne sont du RUNTIME — deux natures derrière un même mot
MINIMUM. « Plancher » désigne désormais deux choses qu'il ne faut surtout pas ranger
ensemble :

- le plancher DÉCLARÉ d'une charge modulable — propriété de l'appareil, saisie une fois,
  stable, avec un champ de configuration à venir ;
- le minimum d'une BORNE — max(borne, voiture), qui dépend du véhicule branché et change
  avec lui. Il n'aura jamais de champ de configuration : il n'y a rien à y régler.

Les rendre modifiables de la même façon ferait croire qu'on peut régler ce qui se constate.
Le bloc « plancher » ne vit que dans la section etmvariableload, ce qui écarte les bornes
structurellement — elles ne sont pas des LoadConfig et n'ouvrent pas cet écran — et son
libellé le dit désormais explicitement.

PHASES. Le plancher effectif d'une borne dépend du nombre de phases visé : ~1,4 kW en
monophasé, ~4,1 kW en triphasé, pour le MÊME courant. La valeur est affichée telle que la
box la donne, sans conversion ni recalcul côté app. Si l'app convertissait, elle devrait
deviner laquelle des deux règles s'applique — et se tromperait un jour. C'est exactement le
miroir qui vient d'être retiré pour la combinatoire des paliers ; on ne va pas en
réintroduire un.

PARTAGE. Le budget ne se divise pas entre bornes : aujourd'hui chacune reçoit l'enveloppe
entière et ce sont le compteur puis la protection de surcharge qui rattrapent ; après 3g
une seule démarrera. Avec deux bornes maintenant déclarées sur le banc, le laisser
implicite invitait à déduire un partage qui n'existe ni avant ni après. Une note l'énonce
dès qu'il y en a plus d'une.

Le bloc de runtime d'une borne reste MUET tant que la recharge n'est pas arbitrée — la box
ne publie rien pour elle, et c'est la bonne réponse, pas un trou. Il affichera phases,
courant, minimum et présence du véhicule le jour où ils arriveront, en lecture seule.

Tests 105/105, analyze 0 erreur.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
2026-08-26 11:37:54 +02:00

103 lines
4.4 KiB
Dart

/// 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<String, dynamic> 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': <String, dynamic>{}},
});
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');
});
}