feat(3g): la réserve batterie sort de l'usine — un seuil qui peut tout geler doit se voir
`batteryLevelConsideration` ne gouverne aujourd'hui que la recharge du véhicule. Une fois la règle montée dans le budget, il gouvernera TOUTE l'installation : sous le seuil le budget de surplus est annulé — pas réduit — donc ni ECS, ni PAC, ni bornes. Le défaut d'usine est 0,9. Le banc `.75` est exactement dans ce cas : seuil 0,9, batterie à 50 %. Le symptôme chez un client serait le pire qui soit — un HEMS qui ne pilote plus rien par grand soleil, sans erreur, sans panne, rien à lire dans un journal. Un paramètre qui peut produire ça n'est pas un paramètre d'usine. - `BatteryReserveCard` lit et écrit le réglage, et ALERTE quand le seuil dépasse le SOC réellement atteint — le cas silencieux. Elle n'invente aucune valeur par défaut : si la box ne l'expose pas, la carte se tait plutôt que d'afficher un curseur mort. - La portée future est dite sur la carte : régler ce seuil « pour la voiture » couperait le reste demain. - Clé ARB `decisionBatteryReserve` posée, et `kCodesReserveBatterie` volontairement VIDE côté `telemetry_text` : le crochet attend le code que publiera le moteur, on ne devine pas son nom. Vérifié sur appareil contre `.75` : réglage lu (0.9 / SOC 50 %), carte affichée, campagne verte. L'assertion des cartes de charge fait désormais DÉFILER avant de compter — dans un `ListView` ce qui n'est pas à l'écran n'est pas construit, et chercher sans défiler ne prouvait que la hauteur de l'écran. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HajXLUczEyZd22JeewRfff
This commit is contained in:
parent
449c6ac50a
commit
801ba15094
@ -144,11 +144,18 @@ void main() {
|
||||
expect(ids, contains('pac-terrain'));
|
||||
|
||||
// Les deux charges s'affichent, avec un bouton de détail chacune.
|
||||
await _attendre(
|
||||
tester,
|
||||
() => find.text('Configurer').evaluate().length >= 2,
|
||||
quoi: 'les cartes de charge à l\'écran',
|
||||
);
|
||||
//
|
||||
// Il faut DESCENDRE pour les atteindre : dans un `ListView`, ce qui n'est pas à
|
||||
// l'écran n'est pas construit, et l'en-tête d'arbitrage puis la carte de réserve
|
||||
// batterie occupent le haut. Chercher sans faire défiler ne prouverait que la hauteur
|
||||
// de l'écran.
|
||||
for (var pas = 0; pas < 20; pas++) {
|
||||
if (find.text('Configurer').evaluate().length >= 2) break;
|
||||
await tester.drag(find.byType(Scrollable).first, const Offset(0, -200));
|
||||
await tester.pump(const Duration(milliseconds: 200));
|
||||
}
|
||||
expect(find.text('Configurer'), findsWidgets,
|
||||
reason: 'les cartes de charge doivent être atteignables');
|
||||
expect(find.text('chauffe-eau'), findsWidgets);
|
||||
|
||||
// La PAC vient du lot B-bis : avant lui, elle était arbitrée sans être
|
||||
@ -197,6 +204,21 @@ void main() {
|
||||
);
|
||||
debugPrint('[appareil] libellé restauré : ${avant.label}');
|
||||
|
||||
// ── 5-zero. La réserve batterie est LUE et affichée ─────────────────────
|
||||
//
|
||||
// Un seuil qui peut geler toute l'installation ne doit pas rester un paramètre
|
||||
// d'usine invisible : le symptôme, chez un client, est un HEMS muet par grand soleil,
|
||||
// sans erreur ni panne.
|
||||
final reserve = await svc.getBatteryLevelConsideration();
|
||||
debugPrint('[appareil] réserve batterie : $reserve '
|
||||
'(SOC ${svc.energyData.batterySOC.round()} %)');
|
||||
expect(reserve, isNotNull, reason: '.75 expose le réglage');
|
||||
await _attendre(
|
||||
tester,
|
||||
() => find.text('Réserve batterie').evaluate().isNotEmpty,
|
||||
quoi: 'la carte de réserve batterie à l\'écran',
|
||||
);
|
||||
|
||||
// ── 5-bis. LE GLISSEMENT, sur l'appareil, contre la vraie box ───────────
|
||||
//
|
||||
// Le test POSE lui-même sa condition au lieu de la subir. Le glissement n'a de sens
|
||||
|
||||
@ -289,6 +289,18 @@
|
||||
"min": { "type": "int", "format": "decimalPattern" }
|
||||
}
|
||||
},
|
||||
"decisionBatteryReserve": "Budget annulé par la réserve batterie ({soc} % sous le seuil de {seuil} %) — il y a du surplus, mais une règle interdit de le distribuer.",
|
||||
"@decisionBatteryReserve": {
|
||||
"description": "À NE PAS confondre avec « pas de surplus » : le surplus existe, une règle l'annule. Le client voit une installation qui ne pilote rien par grand soleil, sans erreur ni panne — la phrase doit donc nommer la cause, pas décrire l'effet.",
|
||||
"placeholders": {
|
||||
"soc": { "type": "int" },
|
||||
"seuil": { "type": "int" }
|
||||
}
|
||||
},
|
||||
"decisionBatteryReserveShort": "Budget annulé par la réserve batterie — il y a du surplus, mais une règle interdit de le distribuer.",
|
||||
"@decisionBatteryReserveShort": {
|
||||
"description": "Variante sans paramètres, si le motif n'en porte pas."
|
||||
},
|
||||
"decisionDegradedL2": "Consigne de repli : le compteur est muet et le watchdog a suspendu la planification.",
|
||||
"decisionSafeStateRelay": "État sûr avant retrait : tous les relais sont ouverts.",
|
||||
"decisionSafeStateSetpoint": "État sûr avant retrait : consigne ramenée à 0 W.",
|
||||
|
||||
@ -619,6 +619,18 @@ abstract class L10n {
|
||||
/// **'Budget de {budget} W sous l\'état {state} ({min} W estimés) — maintien en état 2, le budget passe à la charge suivante.'**
|
||||
String decisionBelowMinPowerState(int budget, int state, int min);
|
||||
|
||||
/// À NE PAS confondre avec « pas de surplus » : le surplus existe, une règle l'annule. Le client voit une installation qui ne pilote rien par grand soleil, sans erreur ni panne — la phrase doit donc nommer la cause, pas décrire l'effet.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Budget annulé par la réserve batterie ({soc} % sous le seuil de {seuil} %) — il y a du surplus, mais une règle interdit de le distribuer.'**
|
||||
String decisionBatteryReserve(int soc, int seuil);
|
||||
|
||||
/// Variante sans paramètres, si le motif n'en porte pas.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Budget annulé par la réserve batterie — il y a du surplus, mais une règle interdit de le distribuer.'**
|
||||
String get decisionBatteryReserveShort;
|
||||
|
||||
/// No description provided for @decisionDegradedL2.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
|
||||
@ -477,6 +477,15 @@ class L10nFr extends L10n {
|
||||
return 'Budget de $budgetString W sous l\'état $state ($minString W estimés) — maintien en état 2, le budget passe à la charge suivante.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionBatteryReserve(int soc, int seuil) {
|
||||
return 'Budget annulé par la réserve batterie ($soc % sous le seuil de $seuil %) — il y a du surplus, mais une règle interdit de le distribuer.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get decisionBatteryReserveShort =>
|
||||
'Budget annulé par la réserve batterie — il y a du surplus, mais une règle interdit de le distribuer.';
|
||||
|
||||
@override
|
||||
String get decisionDegradedL2 =>
|
||||
'Consigne de repli : le compteur est muet et le watchdog a suspendu la planification.';
|
||||
|
||||
@ -9,6 +9,7 @@ import '../../providers/load_telemetry_provider.dart';
|
||||
import '../../providers/schema_provider.dart';
|
||||
import '../../services/nymea_service.dart';
|
||||
import '../../theme/etm_tokens.dart';
|
||||
import '../../widgets/battery_reserve_card.dart';
|
||||
import '../../widgets/controllable_loads_section.dart';
|
||||
import '../../widgets/schema_gated.dart';
|
||||
import 'load_mechanism_screen.dart';
|
||||
@ -136,6 +137,9 @@ class _RolesDevicesScreenState extends State<RolesDevicesScreen> {
|
||||
schemaLoaded: context.watch<SchemaProvider>().schema.loaded,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// AVANT les charges : un seuil qui peut toutes les geler doit se lire avant
|
||||
// elles, pas après.
|
||||
const BatteryReserveCard(),
|
||||
ControllableLoadsSection(onConfigure: _openMechanism),
|
||||
const _Legend(),
|
||||
|
||||
|
||||
@ -1130,6 +1130,55 @@ class NymeaService extends ChangeNotifier {
|
||||
// CONFIG ÉNERGIE — rôles & charges pilotées (écran Rôles & appareils)
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
/// `NymeaEnergy.GetBatteryLevelConsideration` — la **réserve batterie**.
|
||||
///
|
||||
/// ## Un seuil dont la portée s'élargit
|
||||
///
|
||||
/// Fraction de 0 à 1 : sous ce niveau de charge, la batterie est considérée comme
|
||||
/// réservée. Aujourd'hui la règle ne gouverne que la recharge du véhicule. Une fois
|
||||
/// montée dans le budget, elle gouvernera **toute l'installation** : sous le seuil, le
|
||||
/// budget de surplus est **annulé** — pas réduit — donc ni ECS, ni PAC, ni bornes.
|
||||
///
|
||||
/// ## Pourquoi il ne peut pas rester invisible
|
||||
///
|
||||
/// Le défaut d'usine est à **0,9**. Sur une installation dont la batterie plafonne à
|
||||
/// 50 %, cela gèle tout — et le symptôme, chez un client, est un HEMS qui ne pilote plus
|
||||
/// rien par grand soleil, **sans erreur ni panne**. Un seuil qui peut produire ça n'est
|
||||
/// pas un paramètre d'usine : c'est un réglage d'installation, et il doit se voir.
|
||||
///
|
||||
/// \return la fraction lue, ou `null` si la box ne l'expose pas.
|
||||
Future<double?> getBatteryLevelConsideration() async {
|
||||
if (_isSimulation || !_connected) return null;
|
||||
try {
|
||||
final r = await _sendRequest('NymeaEnergy.GetBatteryLevelConsideration', {});
|
||||
final v = (r['params']?['batteryLevelConsideration'] as num?)?.toDouble();
|
||||
_log('[Batterie] réserve → ${v ?? 'non exposée'}', force: true);
|
||||
return v;
|
||||
} catch (e) {
|
||||
_log('[Batterie] GetBatteryLevelConsideration: $e', force: true);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// `NymeaEnergy.SetBatteryLevelConsideration`.
|
||||
///
|
||||
/// \param fraction niveau de réserve, de 0 à 1.
|
||||
/// \return `null` si la box a accepté, sinon le code d'erreur.
|
||||
Future<String?> setBatteryLevelConsideration(double fraction) async {
|
||||
if (_isSimulation || !_connected) return 'Disconnected';
|
||||
try {
|
||||
final r = await _sendRequest('NymeaEnergy.SetBatteryLevelConsideration',
|
||||
{'batteryLevelConsideration': fraction});
|
||||
final err = r['params']?['energyError'] as String?;
|
||||
_log('[Batterie] réserve ← $fraction : ${err ?? r['status']}', force: true);
|
||||
if (err == null || err == 'EnergyErrorNoError') return null;
|
||||
return err;
|
||||
} catch (e) {
|
||||
_log('[Batterie] SetBatteryLevelConsideration: $e', force: true);
|
||||
return e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// `Energy.GetRootMeter` — le compteur principal **tel que la box le connaît**.
|
||||
///
|
||||
/// ## Ce qu'il répare
|
||||
|
||||
@ -116,6 +116,27 @@ String decisionText(L10n t, LoadDecision d) {
|
||||
// Deux variantes, selon que le mécanisme se commande en watts ou en états. La présence
|
||||
// de `state` les départage — et si elle manque sur un mécanisme à états, le repli
|
||||
// rendra le code et ses paramètres plutôt qu'une phrase fausse.
|
||||
// ── Réserve batterie : préparé, pas encore branché ──────────────────────
|
||||
//
|
||||
// Le motif viendra quand la règle montera dans le budget. Son CODE n'est pas tranché
|
||||
// côté plugin, donc le deviner reviendrait à écrire une branche morte qui ne
|
||||
// s'activerait jamais — ou pire, à en activer une sur un code homonyme.
|
||||
//
|
||||
// Ce qui est prêt, c'est la PHRASE, et c'est la partie difficile : elle doit nommer la
|
||||
// cause (« une règle interdit de distribuer ») et non décrire l'effet (« rien n'est
|
||||
// piloté »), sans quoi elle se confond avec « pas de surplus ». Le jour où le code est
|
||||
// publié, il n'y a qu'à l'ajouter à [kCodesReserveBatterie].
|
||||
//
|
||||
// En attendant, le repli fait exactement son travail : un code inconnu sort en clair
|
||||
// avec ses paramètres, ce qui ne peut PAS se lire comme « aucun surplus ». C'est
|
||||
// vérifié par un test plutôt qu'espéré.
|
||||
case _ when kCodesReserveBatterie.contains(d.code):
|
||||
final soc = _i(d, 'batteryLevel') ?? _i(d, 'soc');
|
||||
final seuil = _i(d, 'threshold') ?? _i(d, 'batteryLevelConsideration');
|
||||
return (soc == null || seuil == null)
|
||||
? t.decisionBatteryReserveShort
|
||||
: t.decisionBatteryReserve(soc, seuil);
|
||||
|
||||
case 'BELOW_MIN_POWER':
|
||||
final budget = _w(d, 'budgetW');
|
||||
final min = _w(d, 'minPowerW');
|
||||
@ -130,6 +151,14 @@ String decisionText(L10n t, LoadDecision d) {
|
||||
return t.decisionUnknown(d.code.isEmpty ? '—' : d.code, formatParams(p));
|
||||
}
|
||||
|
||||
/// Codes que le plugin publiera pour « budget annulé par la réserve batterie ».
|
||||
///
|
||||
/// **Volontairement vide.** Le nom n'est pas arrêté côté plugin ; y mettre une supposition
|
||||
/// créerait soit une branche morte, soit — pire — une branche qui s'activerait sur un
|
||||
/// homonyme. Le jour où le code est publié, une seule ligne suffit ici, et la phrase est
|
||||
/// déjà écrite et relue.
|
||||
const Set<String> kCodesReserveBatterie = <String>{};
|
||||
|
||||
/// Rendu déterministe des paramètres bruts, pour le repli.
|
||||
///
|
||||
/// Trié par clé : deux affichages du même motif doivent être identiques, sinon l'écran
|
||||
|
||||
183
lib/widgets/battery_reserve_card.dart
Normal file
183
lib/widgets/battery_reserve_card.dart
Normal file
@ -0,0 +1,183 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../services/nymea_service.dart';
|
||||
import '../theme/etm_tokens.dart';
|
||||
|
||||
/// **Réserve batterie** — le seuil sous lequel plus rien n'est piloté.
|
||||
///
|
||||
/// ## Pourquoi ce réglage sort de l'usine
|
||||
///
|
||||
/// `batteryLevelConsideration` ne gouverne aujourd'hui que la recharge du véhicule. Une
|
||||
/// fois la règle montée dans le budget, elle gouvernera **toute l'installation** : sous le
|
||||
/// seuil, le budget de surplus est **annulé** — pas réduit — donc ni ECS, ni PAC, ni
|
||||
/// bornes.
|
||||
///
|
||||
/// Le défaut d'usine est à **0,9**. Sur une installation dont la batterie plafonne à 50 %,
|
||||
/// cela gèle tout. Et le symptôme, chez un client, est le pire qui soit : un HEMS qui ne
|
||||
/// pilote plus rien **par grand soleil, sans erreur ni panne**. Rien à lire dans un
|
||||
/// journal, rien à diagnostiquer — juste une installation muette.
|
||||
///
|
||||
/// Un seuil qui peut produire ça n'est pas un paramètre d'usine. C'est un réglage
|
||||
/// d'installation, et il doit se voir avant de mordre, pas après.
|
||||
///
|
||||
/// ## Ce que la carte ne fait pas
|
||||
///
|
||||
/// Elle n'invente pas de valeur par défaut et n'en propose pas : la box a la sienne, la
|
||||
/// carte la lit. Si la box ne l'expose pas, la carte se tait — un réglage absent affiché
|
||||
/// comme réglable serait pire que rien.
|
||||
class BatteryReserveCard extends StatefulWidget {
|
||||
const BatteryReserveCard({super.key});
|
||||
|
||||
@override
|
||||
State<BatteryReserveCard> createState() => _BatteryReserveCardState();
|
||||
}
|
||||
|
||||
class _BatteryReserveCardState extends State<BatteryReserveCard> {
|
||||
double? _valeur;
|
||||
double? _brouillon;
|
||||
bool _charge = true;
|
||||
String? _erreur;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _lire());
|
||||
}
|
||||
|
||||
Future<void> _lire() async {
|
||||
final v = await context.read<NymeaService>().getBatteryLevelConsideration();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_valeur = v;
|
||||
_brouillon = v;
|
||||
_charge = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _ecrire() async {
|
||||
final v = _brouillon;
|
||||
if (v == null) return;
|
||||
final err = await context.read<NymeaService>().setBatteryLevelConsideration(v);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_erreur = err;
|
||||
if (err == null) _valeur = v;
|
||||
});
|
||||
}
|
||||
|
||||
/// Vrai si le seuil est **au-dessus** de ce que la batterie atteint réellement — le cas
|
||||
/// qui gèle tout sans rien dire.
|
||||
bool _seuilInatteignable(double soc) =>
|
||||
_valeur != null && soc > 0 && _valeur! * 100 > soc;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final svc = context.watch<NymeaService>();
|
||||
if (_charge) return const SizedBox.shrink();
|
||||
// Box qui n'expose pas le réglage : se taire plutôt que de montrer un curseur mort.
|
||||
if (_valeur == null) return const SizedBox.shrink();
|
||||
|
||||
final muted = EtmTokens.mutedOf(context);
|
||||
final soc = svc.energyData.batterySOC;
|
||||
final alerte = _seuilInatteignable(soc);
|
||||
final modifie = _brouillon != null && _brouillon != _valeur;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: EtmTokens.surfaceOf(context),
|
||||
borderRadius: BorderRadius.circular(EtmTokens.radiusCard),
|
||||
border: Border.all(
|
||||
color: alerte
|
||||
? EtmTokens.orange.withValues(alpha: 0.6)
|
||||
: EtmTokens.lineOf(context)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [
|
||||
Icon(Icons.battery_saver_rounded,
|
||||
size: 18, color: alerte ? EtmTokens.orange : muted),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text('Réserve batterie',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: EtmTokens.inkOf(context))),
|
||||
),
|
||||
Text('${((_brouillon ?? _valeur)! * 100).round()} %',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
fontFamily: 'monospace',
|
||||
color: EtmTokens.inkOf(context))),
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'Sous ce niveau de charge, le budget de surplus est ANNULÉ — pas réduit. '
|
||||
'Aucune charge n\'est pilotée : ni eau chaude, ni pompe à chaleur, ni borne.',
|
||||
style: TextStyle(fontSize: 11.5, height: 1.35, color: muted),
|
||||
),
|
||||
Slider(
|
||||
value: (_brouillon ?? _valeur)!.clamp(0.0, 1.0),
|
||||
divisions: 20,
|
||||
label: '${((_brouillon ?? _valeur)! * 100).round()} %',
|
||||
onChanged: (v) => setState(() => _brouillon = v),
|
||||
),
|
||||
if (alerte)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(Icons.warning_amber_rounded,
|
||||
size: 16, color: EtmTokens.orange),
|
||||
const SizedBox(width: 7),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Le seuil est au-dessus de la charge actuelle '
|
||||
'(${soc.round()} %) : rien ne sera piloté tant que la batterie ne '
|
||||
'l\'aura pas dépassé — par grand soleil comme la nuit, et sans '
|
||||
'qu\'aucune erreur ne le signale.',
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
height: 1.35,
|
||||
color: EtmTokens.inkOf(context)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_erreur != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 6),
|
||||
child: Text('La box a refusé le réglage ($_erreur).',
|
||||
style: const TextStyle(fontSize: 11.5, color: EtmTokens.danger)),
|
||||
),
|
||||
if (modifie)
|
||||
Row(children: [
|
||||
TextButton(
|
||||
onPressed: () => setState(() => _brouillon = _valeur),
|
||||
child: const Text('Annuler'),
|
||||
),
|
||||
const Spacer(),
|
||||
FilledButton(onPressed: _ecrire, child: const Text('Enregistrer')),
|
||||
]),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
// La portée VA changer : le dire évite qu'un installateur règle aujourd'hui un
|
||||
// seuil « pour la voiture » qui coupera tout demain.
|
||||
'Aujourd\'hui ce seuil ne gouverne que la recharge du véhicule. Il gouvernera '
|
||||
'toute l\'installation quand l\'arbitrage prendra la recharge en charge — le '
|
||||
'régler « pour la voiture » couperait alors le reste.',
|
||||
style: TextStyle(fontSize: 10.5, height: 1.35, color: muted),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -247,6 +247,39 @@ void main() {
|
||||
expect(phrase, contains('budgetW = 320'));
|
||||
});
|
||||
|
||||
test('réserve batterie — le repli ne peut PAS se lire « aucun surplus »', () {
|
||||
// Le motif viendra ; son code n'est pas tranché, donc rien ne l'attrape encore. Ce
|
||||
// qui compte d'ici là est que le repli ne fabrique aucune confusion : le cas est
|
||||
// « il y a du surplus, une règle l'interdit », et le lire comme « il n'y en a pas »
|
||||
// enverrait chercher un défaut de production.
|
||||
final repli = decisionText(
|
||||
t,
|
||||
const LoadDecision(
|
||||
code: 'BATTERY_RESERVE_HOLD',
|
||||
params: {'batteryLevel': 50, 'threshold': 90}),
|
||||
);
|
||||
expect(repli, contains('BATTERY_RESERVE_HOLD'));
|
||||
expect(repli.toLowerCase(), isNot(contains('aucun surplus')));
|
||||
expect(repli, contains('batteryLevel = 50'));
|
||||
});
|
||||
|
||||
test('la phrase de la réserve nomme la CAUSE, pas l\'effet', () {
|
||||
// Elle est écrite et relue d'avance : c'est la partie difficile, et la brancher le
|
||||
// jour venu ne doit pas être l'occasion de la rédiger dans l'urgence.
|
||||
final avec = t.decisionBatteryReserve(50, 90);
|
||||
expect(avec, contains('réserve batterie'));
|
||||
expect(avec, contains('surplus'));
|
||||
expect(avec.toLowerCase(), isNot(startsWith('rien')),
|
||||
reason: 'décrire l\'effet ferait chercher une panne');
|
||||
expect(t.decisionBatteryReserveShort, contains('réserve batterie'));
|
||||
});
|
||||
|
||||
test('le crochet du motif est VIDE tant que le code n\'est pas publié', () {
|
||||
// Y mettre une supposition créerait une branche morte, ou pire une branche qui
|
||||
// s'activerait sur un homonyme.
|
||||
expect(kCodesReserveBatterie, isEmpty);
|
||||
});
|
||||
|
||||
test('un code absent de l\'ARB s\'affiche en repli lisible, sans plantage', () {
|
||||
final phrase = decisionText(
|
||||
t,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user