chore(telemetry): l'alias measuredW est retiré — +etm26 est sur .75
Vérifié par sonde avant de couper : `measuredW` a disparu de la charge utile, `measurement` est en ligne. L'alias n'aura vécu que le temps de l'intervalle qu'il devait couvrir, et `MeasurementSource.legacy` part avec lui. Ce qui reste, `MeasurementSource.inconnue`, ne le remplace pas : il couvre une box PLUS RÉCENTE que l'app — la même règle que partout ailleurs dans ce fichier, un code inconnu tombe sur un repli lisible plutôt que sur une phrase inventée. On affiche la valeur, on ne prétend rien sur ce qu'un écart prouverait. ── La garantie qui manquait à ce que j'avais écrit ────────────────────────── `measurement` est présent pour toute charge de `loads[]`, et pour elle seule. C'est elle qui rend la lecture sûre, et elle est maintenant au contrat : son absence ne peut PAS vouloir dire « pas mesurable » — ça, c'est `none`, un état publié — elle ne peut vouloir dire que « charge hors de loads[] », cas où l'on ne conclut rien. J'avais fait la distinction par prudence sur l'écran ; elle est désormais fondée sur une garantie, pas sur une intuition. ── Les sources du banc ne sont pas celles annoncées, et c'est instructif ──── chauffe-eau meter, 1 500 W porte ECS-Meter pac-terrain meter, 800 W porte PAC-Meter — pas `none` V2C Trydan absente de loads[] (pluggedIn: false) `pac-terrain` en `meter` ne contredit pas LM-1105-b : cette règle décrit ce qu'une PAC publie SANS compteur dédié. On lui en a posé un — ce que le brief recommandait — et le compteur explicite l'emporte toujours. Et la V2C illustre la distinction sur machine : absente, pas `none`. Elle porte `currentPower` et publiera sa mesure au cycle où une voiture y sera branchée. ⚠️ Le test sur appareil exigeait `measuredW == null` après détachement du compteur. C'était vrai avant `+etm26`, c'est faux depuis : le moteur retombe sur l'appareil quand il publie lui-même sa puissance. Ce qui doit cesser, c'est que la source vaille `meter` sans compteur désigné — pas que la mesure disparaisse. CLAUDE.md porte le contrat complet et les deux pièges, avec l'illustration du banc. 205 tests, flutter analyze inchangé à 27 remarques. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQbZKrWqsMFP1Lh2jjjd9f
This commit is contained in:
parent
1a357d25c0
commit
e362fb7430
36
CLAUDE.md
36
CLAUDE.md
@ -262,9 +262,43 @@ depuis `+etm23`, `adjustEvChargers()` ne commande plus, l'arbitre est seul (mesu
|
||||
commandes à la borne, 9 par l'arbitre, 0 par le proxy). Mais ce qui devient vrai, c'est
|
||||
que **personne d'autre ne commande** — pas que la charge obéit. Un plafond matériel, un
|
||||
véhicule qui refuse, un câble débranché font toujours diverger l'ordre et la réalité.
|
||||
L'écart se lit dans `measuredW` et dans la charge utile `mechanism` (`chargingEnabled`,
|
||||
L'écart se lit dans `measurement` et dans la charge utile `mechanism` (`chargingEnabled`,
|
||||
`currentA`, `phaseCount`, `pluggedIn`), **jamais dans `allocatedW`**.
|
||||
|
||||
**2-bis. `measuredW` n'existe plus — `measurement` porte le RÉGIME, pas seulement la
|
||||
valeur** (`+etm26`, déployé sur `.75` le 2026-08-28) :
|
||||
|
||||
```jsonc
|
||||
"measurement": { "source": "meter" | "device" | "none", "powerW": 3980.0 }
|
||||
```
|
||||
|
||||
`meter` = un compteur a été désigné exprès · `device` = l'appareil publie lui-même ·
|
||||
`none` = **pas mesurable**, et `powerW` est alors absent.
|
||||
|
||||
Trois règles d'affichage, chacune payée une fois :
|
||||
|
||||
- **`none` est un état positif**, pas une absence : il autorise à **masquer** le bloc de
|
||||
mesure, et sépare « pas mesurable » de « compteur en panne ». Ne jamais combler avec une
|
||||
puissance nominale de `mechanism.*` — l'écart serait identiquement nul à tous les cycles,
|
||||
soit un « tout concorde » qui ne signale plus rien.
|
||||
- **`{source: "device", powerW: 0}` est une MESURE.** Une borne branchée qui ne charge pas
|
||||
mesure zéro watt. La source ne bascule pas vers `none` quand la charge s'éteint.
|
||||
- **Ce qu'un écart prouve dépend de la source** : sous `device`, il montre la désobéissance
|
||||
mais une *concordance* ne prouve rien — plusieurs wallbox renvoient leur consigne ; sous
|
||||
`meter`, la mesure est indépendante et la concordance devient une preuve.
|
||||
|
||||
Et **deux pièges de lecture**, tous deux au contrat :
|
||||
|
||||
1. **Décider sur `measurement.source`, jamais sur `adapter`.** Un SG-Ready est `none` par
|
||||
construction (LM-1105-b : son contact commande un signal, la PAC est alimentée ailleurs)
|
||||
— mais un routeur de relais à topologie **mixte** l'est aussi, une somme partielle
|
||||
n'étant pas une mesure. « Masqué dès que la charge mesure, sauf SG-Ready » manque ce
|
||||
second cas.
|
||||
2. **`measurement` est présent pour toute charge de `loads[]`, et pour elle seule.** Donc
|
||||
une charge **absente** de `loads[]` n'est pas `none` : elle ne dit rien. Sur `.75` le
|
||||
2026-08-28, la V2C était absente (`pluggedIn: false`) alors qu'elle publie
|
||||
`currentPower` — la déclarer « pas mesurable » aurait été faux.
|
||||
|
||||
**3. `EV_GRID_START` partage une allocation entre les DEUX compteurs du budget.** C'est le
|
||||
seul motif qui le fasse : `budgetW` vient du surplus et entre dans `budget.allocatedW`,
|
||||
`gridW` est acheté au réseau et entre dans `budget.evReservedW`, avec
|
||||
|
||||
14
TODO.md
14
TODO.md
@ -2,14 +2,12 @@
|
||||
|
||||
## Ouvert au 2026-08-27
|
||||
|
||||
- [ ] ⏳ **Retirer l'alias `measuredW` dès que `1.15.2+etm26` est sur `.75`.**
|
||||
L'app lit `measurement: {source, powerW}` et retombe sur `measuredW` en son absence —
|
||||
uniquement pour couvrir l'intervalle entre ce livrable et le déploiement du moteur, sans
|
||||
quoi la mesure disparaîtrait des écrans quelques jours. **Le moteur, lui, ne double pas
|
||||
la clé** : deux noms pour une chose divergent. La branche à supprimer est nommée dans
|
||||
`LoadMeasurement.fromJson` (`models/load_telemetry.dart`), avec la sonde qui tranche :
|
||||
`dart tools/rpc/probe.dart 192.168.1.75 NymeaEnergy.GetLoadTelemetry` — dès que
|
||||
`measurement` y figure, l'alias part, et `MeasurementSource.legacy` avec lui.
|
||||
- [x] ~~⏳ Retirer l'alias `measuredW`~~ — **fait le 2026-08-28**, `1.15.2+etm26` déployé
|
||||
sur `.75` et vérifié par sonde : `measuredW` a disparu de la charge utile, `measurement`
|
||||
est en ligne. `MeasurementSource.legacy` est parti avec lui ; ce qui reste,
|
||||
`MeasurementSource.inconnue`, couvre une box **plus récente** que l'app, pas plus
|
||||
ancienne. Sources relevées sur le banc : `meter` 1 500 W (chauffe-eau, ECS-Meter),
|
||||
`meter` 800 W (pac-terrain, PAC-Meter), et la V2C **absente de `loads[]`** — pas `none`.
|
||||
|
||||
|
||||
- [x] ~~🔴 **Bilan JOURNALIER**~~ — **fait le 2026-08-28.** Les quatre tuiles montrent la
|
||||
|
||||
@ -27,6 +27,7 @@ import 'package:etm_powersync_app/main.dart' as app;
|
||||
import 'package:etm_powersync_app/providers/energy_setup_provider.dart';
|
||||
import 'package:etm_powersync_app/providers/installer_mode_provider.dart';
|
||||
import 'package:etm_powersync_app/providers/load_config_provider.dart';
|
||||
import 'package:etm_powersync_app/models/load_telemetry.dart';
|
||||
import 'package:etm_powersync_app/providers/load_telemetry_provider.dart';
|
||||
import 'package:etm_powersync_app/providers/schema_provider.dart';
|
||||
import 'package:etm_powersync_app/screens/energy/roles_devices_screen.dart';
|
||||
@ -480,16 +481,17 @@ void main() {
|
||||
final tele = Provider.of<LoadTelemetryProvider>(ctx, listen: false);
|
||||
await _attendre(
|
||||
tester,
|
||||
() => config.entries
|
||||
.every((e) => tele.entryFor(e.id)?.measuredW != null),
|
||||
() => config.entries.every(
|
||||
(e) => tele.entryFor(e.id)?.measurement.source == MeasurementSource.meter),
|
||||
limite: const Duration(seconds: 150),
|
||||
quoi: 'la publication de measuredW pour chaque charge',
|
||||
quoi: 'la publication de measurement.source = meter pour chaque charge',
|
||||
);
|
||||
|
||||
final mesures = {
|
||||
for (final e in config.entries) e.id: tele.entryFor(e.id)?.measuredW
|
||||
for (final e in config.entries)
|
||||
e.id: tele.entryFor(e.id)?.measurement.powerW
|
||||
};
|
||||
debugPrint('[appareil] measuredW par charge : $mesures');
|
||||
debugPrint('[appareil] mesure par charge : $mesures');
|
||||
expect(mesures.values.every((v) => v != null), isTrue);
|
||||
|
||||
// Ce qui prouve le rattachement PAR CHARGE, c'est que chacune porte un compteur
|
||||
@ -501,9 +503,14 @@ void main() {
|
||||
expect(compteurs.values.toSet().length, compteurs.length,
|
||||
reason: 'chaque charge doit porter SON propre compteur');
|
||||
|
||||
// Et une charge SANS compteur ne publie rien — `null` n'est pas 0. On le vérifie en
|
||||
// détachant, puis on rattache de nouveau : afficher un zéro à la place ferait passer
|
||||
// une absence de mesure pour une absence de consommation.
|
||||
// Et détacher le compteur doit faire CHANGER LE RÉGIME, pas forcément disparaître la
|
||||
// mesure.
|
||||
//
|
||||
// ⚠️ Ce test exigeait `measuredW == null` après détachement. C'était vrai avant
|
||||
// `+etm26` et c'est faux depuis : le moteur retombe sur l'appareil quand il publie
|
||||
// lui-même sa puissance (`source: "device"`), et ne publie `none` que s'il n'y a
|
||||
// vraiment rien à mesurer. Les deux sont corrects — ce qui doit cesser, c'est que la
|
||||
// source vaille `meter` alors qu'aucun compteur n'est désigné.
|
||||
final cible = config.entries.first;
|
||||
final garde = cible.meterThingId;
|
||||
config.replaceEntry(cible.id, cible.patched(meterThingId: ''));
|
||||
@ -517,12 +524,18 @@ void main() {
|
||||
);
|
||||
await _attendre(
|
||||
tester,
|
||||
() => tele.entryFor(cible.id)?.measuredW == null,
|
||||
() =>
|
||||
tele.entryFor(cible.id)?.measurement.source != MeasurementSource.meter,
|
||||
limite: const Duration(seconds: 150),
|
||||
quoi: 'l\'arrêt de publication de measuredW après détachement',
|
||||
quoi: 'le changement de régime après détachement (meter → device ou none)',
|
||||
);
|
||||
debugPrint('[appareil] ${cible.id} sans compteur → measuredW = '
|
||||
'${tele.entryFor(cible.id)?.measuredW}');
|
||||
final apres = tele.entryFor(cible.id)?.measurement;
|
||||
debugPrint('[appareil] ${cible.id} sans compteur → source='
|
||||
'${apres?.source.name} powerW=${apres?.powerW}');
|
||||
// Sous `none`, il n'y a pas de puissance — et `0` n'en est pas l'équivalent.
|
||||
if (apres?.source == MeasurementSource.none) {
|
||||
expect(apres?.powerW, isNull);
|
||||
}
|
||||
|
||||
// Rendre le rattachement : il est demandé, il reste.
|
||||
final remise = config.entries.firstWhere((e) => e.id == cible.id);
|
||||
|
||||
@ -246,7 +246,7 @@ class _EnergyFlowCardState extends State<EnergyFlowCard>
|
||||
//
|
||||
// Elles sont donc marquées `dashed` / `estimated`, comme « Autres » l'était déjà
|
||||
// seul, et la légende sous le schéma le dit en toutes lettres. Le marquage n'est
|
||||
// pas le correctif : la source existe (`GetLoadTelemetry` publie `measuredW` par
|
||||
// pas le correctif : la source existe (`GetLoadTelemetry` publie `measurement` par
|
||||
// charge), et l'y brancher est un lot à part — cf. `TODO.md`. En attendant, un
|
||||
// écran qui annonce son estimation ne trompe personne ; un écran qui affiche
|
||||
// « PAC 1,3 kW » en dur, si.
|
||||
|
||||
@ -98,9 +98,13 @@ enum MeasurementSource {
|
||||
/// Pas mesurable — état positif, pas une absence.
|
||||
none,
|
||||
|
||||
/// **Box antérieure à `+etm26`** : `measuredW` était là, sans régime. On affiche la
|
||||
/// valeur, on ne prétend rien sur ce qu'un écart prouverait.
|
||||
legacy,
|
||||
/// Une source qu'**aucune version de cette app ne connaît** — box plus récente.
|
||||
///
|
||||
/// Ce n'est pas l'ancien repli `measuredW`, retiré le 2026-08-28 avec le déploiement de
|
||||
/// `+etm26` : c'est la règle qui vaut partout ailleurs dans ce fichier — un code inconnu
|
||||
/// tombe sur un repli lisible, jamais sur une phrase inventée. On affiche la valeur, on
|
||||
/// ne prétend rien sur ce qu'un écart prouverait.
|
||||
inconnue,
|
||||
}
|
||||
|
||||
class LoadMeasurement {
|
||||
@ -123,38 +127,33 @@ class LoadMeasurement {
|
||||
/// peut n'être qu'un écho. L'écart, lui, reste informatif dans les deux cas.
|
||||
bool get concordanceProbante => source == MeasurementSource.meter;
|
||||
|
||||
/// Lecture tolérante — `measurement` s'il est là, `measuredW` sinon.
|
||||
/// Lit `measurement` — **et rien d'autre**.
|
||||
///
|
||||
/// > ⏳ **ALIAS TEMPORAIRE, à retirer quand `1.15.2+etm26` sera déployé sur `.75`.**
|
||||
/// > Il n'existe que pour couvrir l'intervalle entre cette version de l'app et le
|
||||
/// > déploiement du moteur : sans lui, la mesure disparaîtrait des écrans pendant
|
||||
/// > quelques jours. Le moteur, lui, **ne double pas la clé** — deux noms pour une chose
|
||||
/// > divergent — et l'app ne doit pas les doubler plus longtemps que nécessaire.
|
||||
/// > Vérification : `dart tools/rpc/probe.dart 192.168.1.75 NymeaEnergy.GetLoadTelemetry`
|
||||
/// > ; dès que `measurement` y figure, supprimer la branche `measuredW` et ce bloc.
|
||||
/// L'alias temporaire vers `measuredW` a été retiré le 2026-08-28 : `1.15.2+etm26` est
|
||||
/// déployé sur `.75`, et `measuredW` a bien disparu de la charge utile (vérifié). Il n'a
|
||||
/// existé que pour couvrir l'intervalle entre le livrable de l'app et celui du moteur.
|
||||
/// Le moteur ne double jamais une clé — deux noms pour une chose divergent — et l'app ne
|
||||
/// les double pas plus longtemps que nécessaire.
|
||||
///
|
||||
/// **La garantie du contrat, celle qui rend cette lecture sûre** : `measurement` est
|
||||
/// présent pour **toute** charge de `loads[]`, et pour elle seule. Son absence ici ne
|
||||
/// peut donc pas vouloir dire « pas mesurable » — ça, c'est [MeasurementSource.none],
|
||||
/// qui est un état **publié**. Elle ne peut vouloir dire que « cette charge n'est pas
|
||||
/// dans `loads[]` », et là on ne conclut rien, on n'affiche rien qui affirme.
|
||||
factory LoadMeasurement.fromJson(Map<String, dynamic> m) {
|
||||
final brut = m['measurement'];
|
||||
if (brut is Map) {
|
||||
final src = brut['source'] as String?;
|
||||
final source = switch (src) {
|
||||
'meter' => MeasurementSource.meter,
|
||||
'device' => MeasurementSource.device,
|
||||
'none' => MeasurementSource.none,
|
||||
// Une source qu'aucune version d'app ne connaît : on affiche la valeur sans
|
||||
// prétendre savoir ce qu'un écart prouverait. Deviner serait pire.
|
||||
_ => MeasurementSource.legacy,
|
||||
};
|
||||
final w = _dn(brut['powerW']);
|
||||
// `none` avec une puissance ne devrait pas arriver ; si ça arrive, `none` gagne —
|
||||
// c'est le régime qui est publié, pas la valeur.
|
||||
return LoadMeasurement(
|
||||
source, source == MeasurementSource.none ? null : w);
|
||||
}
|
||||
if (brut is! Map) return LoadMeasurement.aucune;
|
||||
|
||||
final ancien = _dn(m['measuredW']);
|
||||
return ancien == null
|
||||
? LoadMeasurement.aucune
|
||||
: LoadMeasurement(MeasurementSource.legacy, ancien);
|
||||
final source = switch (brut['source'] as String?) {
|
||||
'meter' => MeasurementSource.meter,
|
||||
'device' => MeasurementSource.device,
|
||||
'none' => MeasurementSource.none,
|
||||
_ => MeasurementSource.inconnue,
|
||||
};
|
||||
final w = _dn(brut['powerW']);
|
||||
// `none` avec une puissance ne devrait pas arriver ; si ça arrive, `none` gagne —
|
||||
// c'est le régime qui est publié, pas la valeur.
|
||||
return LoadMeasurement(source, source == MeasurementSource.none ? null : w);
|
||||
}
|
||||
}
|
||||
|
||||
@ -399,18 +398,6 @@ class LoadTelemetryEntry {
|
||||
/// plus — d'où le repli explicite de [isSurplusFunded].
|
||||
final String? funding;
|
||||
|
||||
/// Puissance **MESURÉE** par le compteur rattaché à cette charge. `null` quand aucun
|
||||
/// compteur ne l'est.
|
||||
///
|
||||
/// Elle ne remplace pas [allocatedW] et ne s'y substitue jamais : l'une est ce que le
|
||||
/// waterfall a **décidé**, l'autre ce qui **passe réellement**. Les afficher côte à côte
|
||||
/// est tout l'intérêt du rattachement — commandé 3 000 W, mesuré 0 W, la charge ne fait
|
||||
/// pas ce qu'on lui demande, et c'est la seule chose que la mesure autorise à conclure.
|
||||
///
|
||||
/// **`null` n'est pas 0.** Une charge sans compteur ne publie rien, et afficher un zéro
|
||||
/// à la place ferait passer une absence de mesure pour une absence de consommation.
|
||||
final double? measuredW;
|
||||
|
||||
/// D'où vient la mesure, et ce qu'elle autorise à conclure. Voir [LoadMeasurement].
|
||||
final LoadMeasurement measurement;
|
||||
|
||||
@ -424,7 +411,6 @@ class LoadTelemetryEntry {
|
||||
required this.allocatedW,
|
||||
required this.available,
|
||||
required this.faultCode,
|
||||
this.measuredW,
|
||||
this.measurement = LoadMeasurement.aucune,
|
||||
this.funding,
|
||||
required this.lock,
|
||||
@ -444,7 +430,6 @@ class LoadTelemetryEntry {
|
||||
// « disponible » est le repli qui n'invente pas un défaut.
|
||||
available: m['available'] as bool? ?? true,
|
||||
faultCode: (fault is String && fault.isNotEmpty) ? fault : null,
|
||||
measuredW: _dn(m['measuredW']),
|
||||
measurement: LoadMeasurement.fromJson(m),
|
||||
funding: (m['funding'] is String && (m['funding'] as String).isNotEmpty)
|
||||
? m['funding'] as String
|
||||
@ -508,8 +493,9 @@ class LoadTelemetryEntry {
|
||||
/// pour un chauffe-eau dont la résistance a lâché.
|
||||
///
|
||||
/// L'écart commande/réalité ne se lit donc **jamais** dans [allocatedW] : il se lit dans
|
||||
/// [measuredW] quand un compteur est rattaché, et dans la charge utile [mechanism] —
|
||||
/// pour une borne, `chargingEnabled`, `currentA`, `phaseCount`, `pluggedIn`.
|
||||
/// [LoadTelemetryEntry.measurement] — dont la `source` dit ce que l'écart prouve — et
|
||||
/// dans la charge utile [mechanism] : pour une borne, `chargingEnabled`, `currentA`,
|
||||
/// `phaseCount`, `pluggedIn`.
|
||||
///
|
||||
/// Ce getter n'existe plus : il portait la réserve « pour une borne, ce n'est même pas
|
||||
/// une commande », qui est levée. Sa disparition est délibérée — le garder à `true`
|
||||
|
||||
@ -1636,7 +1636,7 @@ class _CompteurEtSonde extends StatelessWidget {
|
||||
// borne de recharge ne rend aucun service thermique : le champ ne mènerait nulle
|
||||
// part. Proposé sur cet écran, il faisait chercher à quoi il sert.
|
||||
//
|
||||
// Le compteur, lui, RESTE : c'est le seul chemin par lequel `measuredW` apparaît
|
||||
// Le compteur, lui, RESTE : c'est le seul chemin par lequel une mesure apparaît
|
||||
// pour cette charge dans la télémétrie — et sur une borne qui publie
|
||||
// `currentPower`, on peut la désigner elle-même.
|
||||
if (!entry.isEvCharger) ...[
|
||||
@ -1689,10 +1689,10 @@ String _aideSelonSource(MeasurementSource? source) => switch (source) {
|
||||
'un signal et ne voit pas le courant de la machine, et une topologie de relais '
|
||||
'où un seul mesure ne mesure rien d\'exploitable. Un compteur dédié est donc '
|
||||
'ici la seule voie vers une mesure.',
|
||||
MeasurementSource.legacy =>
|
||||
'Cette box ne publie pas encore l\'origine de la mesure (avant 1.15.2+etm26) : '
|
||||
'la valeur affichée sur la carte est juste, mais ce qu\'un écart prouverait '
|
||||
'reste indéterminé.',
|
||||
MeasurementSource.inconnue =>
|
||||
'La box publie une origine de mesure que cette version de l\'app ne connaît pas : '
|
||||
'la valeur affichée sur la carte est celle de la box, mais ce qu\'un écart '
|
||||
'prouverait reste indéterminé ici.',
|
||||
};
|
||||
|
||||
/// Inclusion dans l'arbitrage — et ce que la désactivation fait vraiment.
|
||||
|
||||
@ -700,8 +700,8 @@ String _etatBorne(L10n t, LoadMechanism? mech) {
|
||||
/// de l'organe commandé — la concordance devient une preuve, et un écart ouvre trois
|
||||
/// hypothèses (charge, compteur, câblage) qu'il ne tranche pas.
|
||||
///
|
||||
/// `null` sous `legacy` : une box antérieure à `+etm26` ne publie pas de régime, et
|
||||
/// inventer une portée serait pire que se taire.
|
||||
/// `null` sous `inconnue` : une box plus récente que l'app publie une source qu'on ne sait
|
||||
/// pas interpréter, et inventer une portée serait pire que se taire.
|
||||
String? _porteeMesure(LoadMeasurement m) => switch (m.source) {
|
||||
MeasurementSource.meter =>
|
||||
'Compteur dédié : mesure indépendante de la commande — une concordance est une '
|
||||
@ -709,7 +709,7 @@ String? _porteeMesure(LoadMeasurement m) => switch (m.source) {
|
||||
MeasurementSource.device =>
|
||||
'Mesure publiée par l\'appareil : un écart montre qu\'il n\'obéit pas, mais une '
|
||||
'concordance ne prouve rien — certaines bornes renvoient leur consigne.',
|
||||
MeasurementSource.none || MeasurementSource.legacy => null,
|
||||
MeasurementSource.none || MeasurementSource.inconnue => null,
|
||||
};
|
||||
|
||||
String _ecartMesure(LoadTelemetryEntry live) {
|
||||
|
||||
@ -171,7 +171,7 @@ void main() {
|
||||
await _poser(tester, _harness(kV2c));
|
||||
|
||||
expect(find.text('Sonde de température'), findsNothing);
|
||||
// Le compteur, lui, garde un sens : c'est le seul chemin par lequel `measuredW`
|
||||
// Le compteur, lui, garde un sens : c'est le seul chemin par lequel une mesure
|
||||
// apparaît, et une borne peut se désigner elle-même (elle publie `currentPower`).
|
||||
expect(find.text('Compteur dédié'), findsOneWidget);
|
||||
// Hors télémétrie, la box ne dit pas d'où viendrait la mesure : l'écran ne le
|
||||
|
||||
@ -5,8 +5,9 @@
|
||||
/// SG-Ready une puissance toujours déclarée. Le régime se publie désormais :
|
||||
/// `measurement: {source: "meter" | "device" | "none", powerW}`.
|
||||
///
|
||||
/// Ces tests figent les trois pièges d'affichage nommés dans le §7 du brief, et la
|
||||
/// tolérance temporaire vers `measuredW`.
|
||||
/// Ces tests figent les trois pièges d'affichage nommés dans le §7 du brief, le retrait de
|
||||
/// l'alias `measuredW` (2026-08-28, `+etm26` déployé), et la garantie qui rend la lecture
|
||||
/// sûre : `measurement` est présent pour toute charge de `loads[]`, et pour elle seule.
|
||||
library;
|
||||
|
||||
import 'package:etm_powersync_app/l10n/app_localizations.dart';
|
||||
@ -72,33 +73,37 @@ void main() {
|
||||
final m = _entree({
|
||||
'measurement': {'source': 'futur', 'powerW': 100.0}
|
||||
}).measurement;
|
||||
expect(m.source, MeasurementSource.legacy);
|
||||
expect(m.source, MeasurementSource.inconnue);
|
||||
expect(m.mesuree, isTrue);
|
||||
expect(m.concordanceProbante, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('tolérance temporaire vers measuredW — à retirer avec +etm26 sur .75', () {
|
||||
test('sans `measurement`, l\'ancienne clé est encore lue', () {
|
||||
// Sans cet alias, la mesure disparaîtrait des écrans entre ce livrable et le
|
||||
// déploiement du moteur.
|
||||
final m = _entree({'measuredW': 1500.0}).measurement;
|
||||
expect(m.source, MeasurementSource.legacy);
|
||||
expect(m.powerW, 1500.0);
|
||||
group('l\'alias measuredW est RETIRÉ — +etm26 déployé le 2026-08-28', () {
|
||||
test('l\'ancienne clé n\'est plus lue du tout', () {
|
||||
// Vérifié sur `.75` avant de retirer : `measuredW` a disparu de la charge utile.
|
||||
// Le garder « au cas où » ferait vivre deux noms pour une chose — et ils divergent.
|
||||
expect(_entree({'measuredW': 1500.0}).measurement.mesuree, isFalse);
|
||||
});
|
||||
|
||||
test('`measurement` GAGNE quand les deux sont là', () {
|
||||
// Deux noms pour une chose divergent : le nouveau fait foi, sans exception.
|
||||
test('`measurement` absent ne veut PAS dire « pas mesurable »', () {
|
||||
// Garantie du contrat : `measurement` est présent pour toute charge de `loads[]`, et
|
||||
// pour elle seule. Son absence ne peut donc signifier que « charge hors de loads[] »
|
||||
// — cas où l'on ne conclut rien. `none`, lui, est un état publié.
|
||||
final m = _entree(const {}).measurement;
|
||||
expect(m.mesuree, isFalse);
|
||||
expect(m.source, MeasurementSource.none,
|
||||
reason: 'valeur de repli inerte — l\'écran distingue le cas par l\'absence '
|
||||
'de la charge dans loads[], pas par cette valeur');
|
||||
});
|
||||
|
||||
test('une source future n\'est pas devinée, et n\'efface pas la valeur', () {
|
||||
final m = _entree({
|
||||
'measuredW': 1500.0,
|
||||
'measurement': {'source': 'meter', 'powerW': 3980.0}
|
||||
'measurement': {'source': 'estimated', 'powerW': 100.0}
|
||||
}).measurement;
|
||||
expect(m.source, MeasurementSource.meter);
|
||||
expect(m.powerW, 3980.0);
|
||||
});
|
||||
|
||||
test('ni l\'un ni l\'autre : pas mesurable', () {
|
||||
expect(_entree(const {}).measurement.mesuree, isFalse);
|
||||
expect(m.source, MeasurementSource.inconnue);
|
||||
expect(m.powerW, 100.0);
|
||||
expect(m.concordanceProbante, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
@ -182,12 +187,29 @@ void main() {
|
||||
expect(find.textContaining('Mesuré 0 W'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('sur une box ancienne, la valeur s\'affiche SANS portée',
|
||||
(tester) async {
|
||||
await tester.pumpWidget(carte({'measuredW': 1500.0}));
|
||||
testWidgets('une source inconnue affiche la valeur SANS portée', (tester) async {
|
||||
// On montre ce que la box dit ; on ne prétend pas savoir ce qu'un écart prouverait.
|
||||
await tester.pumpWidget(carte({
|
||||
'measurement': {'source': 'estimated', 'powerW': 1500.0}
|
||||
}));
|
||||
expect(find.textContaining('Mesuré 1500 W'), findsOneWidget);
|
||||
expect(find.textContaining('concordance'), findsNothing);
|
||||
expect(find.textContaining('indépendante'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('les trois cas du banc, tels que .75 les publie le 2026-08-28',
|
||||
(tester) async {
|
||||
// chauffe-eau et pac-terrain portent chacun un compteur dédié (ECS-Meter,
|
||||
// PAC-Meter) : source `meter`, 1 500 W et 800 W. `pac-terrain` en `meter` ne
|
||||
// contredit pas LM-1105-b — cette règle décrit ce qu'une PAC publie SANS compteur
|
||||
// dédié, et le compteur explicite l'emporte toujours.
|
||||
for (final (w, _) in [(1500.0, 'chauffe-eau'), (800.0, 'pac-terrain')]) {
|
||||
await tester.pumpWidget(carte({
|
||||
'measurement': {'source': 'meter', 'powerW': w}
|
||||
}));
|
||||
expect(find.textContaining('Mesuré ${w.round()} W'), findsOneWidget);
|
||||
expect(find.textContaining('mesure indépendante'), findsOneWidget);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user