fix(accueil): « En ce moment » n'affiche que ce qui consomme ou travaille

Patrick, 2026-10-16 : « un chauffe-eau qui ne consomme pas, pas la peine
de l'afficher — uniquement des choses actives ».
- Une charge agit si elle consomme (mesure > 50 W) ou si son mécanisme
  est réellement commandé (palier de relais > 0, consigne > 0, borne
  autorisée avec du courant, SG-Ready en état 3 ou 4) — plus sur la
  puissance seulement DÉCIDÉE. Sur le banc, le chauffe-eau avait 83 W
  décidés en éco mais son relais restait au palier 0 : il ne chauffait
  pas. Alertes (défaut, dérogation, objectif manqué) toujours montrées.
- Ventilation « 83 W éco + 0 W confort » retirée : éco / confort se
  lisent comme des réglages de température. Question posée au moteur
  (consignes/QUESTION-moteur-vocabulaire-niveaux.md).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Patrick Schurig 2026-10-16 22:26:52 +02:00
parent 250c0fcb6b
commit 727f309d75
2 changed files with 53 additions and 36 deletions

View File

@ -155,16 +155,28 @@ class EnCeMomentView extends StatelessWidget {
'EV_IDLE',
};
/// Une charge **agit** quand un de ses niveaux porte de la puissance sous un motif qui
/// n'est pas une attente, qu'elle tire réellement de la puissance, ou qu'il y a quelque
/// chose à dire : dérogation, défaut, objectif manqué retenu.
/// Une charge **agit** quand elle consomme ou travaille VRAIMENT — pas quand le moteur
/// lui a seulement décidé de la puissance (Patrick, 2026-10-16 : « uniquement des choses
/// actives »). Sur le banc, le chauffe-eau avait 83 W décidés en éco, mais son relais
/// restait au palier 0 : un relais de 3 500 W ne délivre pas 83 W, rien ne chauffait.
///
/// La puissance seule ne suffit pas : sur le banc, la PAC SG-Ready porte 1 200 W en état
/// « normal » maintenu sous `BELOW_MIN_POWER` (« le budget passe à la charge suivante ») —
/// rien n'est piloté, elle attend (exemple de Patrick, 2026-10-16).
/// Dans l'ordre : une alerte (défaut, dérogation, objectif manqué) se montre toujours ;
/// une MESURE au-delà de 50 W tranche ; sinon le mécanisme APPLIQUÉ ; et seulement sans
/// mécanisme publié, la puissance décidée sous un motif qui n'est pas une attente.
static bool agit(LoadTelemetryEntry e, {bool manque = false}) {
if (manque || !e.available || e.enDerogation) return true;
if (e.measurement.mesuree && e.measurement.powerW! > 50) return true;
final m = e.measurement;
if (m.mesuree && m.powerW! > 50) return true;
final meca = e.mechanism;
if (meca != null) {
if (meca.isRelay) return (meca.stageW ?? 0) > 0;
if (meca.isVariable) return (meca.setpointW ?? 0) > 0 || (meca.percent ?? 0) > 0;
if (meca.isEvCharger) {
return meca.chargingEnabled == true && (meca.currentA ?? 0) > 0;
}
// SG-Ready : 3 = recommandé, 4 = forcé. 2 est l'état normal — rien de piloté.
if (meca.isSgReady) return meca.state == 3 || meca.state == 4;
}
if (e.levels.isEmpty) {
return e.allocatedW > 0 && !motifsDAttente.contains(e.decision.code);
}
@ -197,20 +209,11 @@ class EnCeMomentView extends StatelessWidget {
final couleur = EtmTokens.domaineOf(context, dom) ?? muted;
final m = e.measurement;
// Ventilation éco / confort quand plusieurs passes se cumulent (R7, etm70) : le motif
// publié est celui de la part majoritaire, les deux parts se lisent ici.
String? ventilation;
if (e.levels.length > 1) {
ventilation = e.levels
.map((l) => t.ventilationNiveau(
l.targetW.round(),
switch (l.level) {
'eco' => t.ventilationEco,
'comfort' => t.ventilationConfort,
final autre => autre,
}))
.join(' + ');
}
// La ventilation « 83 W éco + 0 W confort » est RETIRÉE (Patrick, 2026-10-16) : pour un
// client, éco / confort sont des réglages de température, pas des besoins. Elle
// reviendra avec le vocabulaire tranché avec le moteur
// (`consignes/QUESTION-moteur-vocabulaire-niveaux.md`) — sans doute par source,
// « réseau / surplus », depuis `counts`.
final manque = derniersManques[e.loadId.replaceAll(RegExp(r'[{}]'), '').toLowerCase()];
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
@ -231,9 +234,6 @@ class EnCeMomentView extends StatelessWidget {
const SizedBox(height: 2),
Text(decisionText(t, motifDeLAction(e)),
style: EtmTokens.sans(size: 11.5, color: muted).copyWith(height: 1.35)),
if (ventilation != null)
Text(ventilation,
style: EtmTokens.mono(size: 10.5, weight: FontWeight.w400, color: muted)),
if (manque != null) ...[
const SizedBox(height: 4),
_manque(context, t, manque),

View File

@ -51,28 +51,45 @@ LoadDecision _d(String code, Map<String, dynamic> params) =>
void main() {
final fr = lookupL10n(const Locale('fr'));
testWidgets('trame réelle : une ligne par charge qui AGIT, les autres « en attente »',
testWidgets('trame réelle : rien ne consomme ni ne travaille → tout « en attente »',
(t) async {
await t.pumpWidget(_app(_vue(_banc())));
expect(find.text('En ce moment'), findsOneWidget);
// Le chauffe-eau achète 83 W en éco : il agit, et son motif dit l'ACHAT — pas
// « rien n'est commandé », que la racine etm69 publiait (dernier niveau parcouru).
expect(find.text('Chauffe-eau'), findsOneWidget);
expect(find.text('Plancher servi — 83 W achetés au réseau'), findsOneWidget);
expect(find.textContaining('rien n\'est commandé'), findsNothing);
// La PAC SG-Ready porte 1 200 W en état normal maintenu sous BELOW_MIN_POWER : rien de
// piloté — une ligne discrète, pas une action (Patrick, 2026-10-16).
expect(find.text('En attente : PAC banc'), findsOneWidget);
expect(find.text('PAC banc'), findsNothing);
// Le chauffe-eau a 83 W DÉCIDÉS en éco, mais son relais est au palier 0 : il ne chauffe
// pas. La PAC SG-Ready est en état 2 (normal), mesurée à 0 W. Aucune n'agit (Patrick,
// 2026-10-16 : « uniquement des choses actives »).
expect(find.text('En attente : Chauffe-eau, PAC banc'), findsOneWidget);
expect(find.text('Chauffe-eau'), findsNothing);
expect(find.textContaining('Plancher servi'), findsNothing);
// Les décisions inventées de l'ancienne démo ne peuvent plus sortir.
expect(find.textContaining('Batterie en charge'), findsNothing);
// Le nom de marque, écrit en échappement pour que le grep de la consigne reste vide.
expect(find.textContaining('Héos'), findsNothing);
});
testWidgets('la ventilation des niveaux se lit (83 W éco + 0 W confort)', (t) async {
testWidgets('relais commandé : la ligne apparaît, avec le motif de l\'action', (t) async {
final b = _banc();
final loads = [
for (final l in (b['loads'] as List).cast<Map<String, dynamic>>())
if (l['loadId'] == 'chauffe-eau')
{
...l,
'mechanism': {...(l['mechanism'] as Map), 'stageW': 500},
}
else
l,
];
await t.pumpWidget(_app(_vue({...b, 'loads': loads})));
expect(find.text('Chauffe-eau'), findsOneWidget);
expect(find.text('Plancher servi — 83 W achetés au réseau'), findsOneWidget);
expect(find.text('En attente : PAC banc'), findsOneWidget);
});
testWidgets('pas de ventilation « éco / confort » : des mots de thermostat (Patrick)',
(t) async {
await t.pumpWidget(_app(_vue(_banc())));
expect(find.text('83 W éco + 0 W confort'), findsOneWidget);
expect(find.textContaining('confort'), findsNothing);
expect(find.textContaining(' éco'), findsNothing);
});
testWidgets('puissance MESURÉE seulement — jamais l\'allocation à sa place', (t) async {