feat(accueil): carte « En ce moment » sur la télémétrie, plus de marque ni de démo
Consigne app §G (décision Patrick du 2026-10-16) et motifs etm70 (§H). G — la carte « Héos pilote en ce moment » affichait des décisions inventées (HeosDecision.demo : voiture 3,6 kW, batterie en charge sur un banc sans batterie). Remplacée par EnCeMomentCard : une ligne par charge de GetLoadTelemetry — libellé de la configuration, decision.code + params rendus par decisionText, puissance MESURÉE seulement (jamais l'allocation à sa place). Titre en clé l10n. Rien sur « optimiseur actif » tant que l'objet de capacités n'existe pas. Marque retirée des identifiants (HeosDecisionCard, HeosDecision, HeosAction, _HeosChip, _HeosBadgeSmall, heosActive → pilotageActif…), des fichiers (heos_decision_card.dart, dashboard_models.dart supprimés), des textes affichés (hub du flux, écran A/C) et des commentaires : grep -ri héos/heos vide sur lib/, test/, assets/ et les .arb. H — contre INTERFACE.md local de la branche moteur, etm70 pas encore sur le banc : - motifs éco publiés à la racine (R7, part majoritaire) rendus en clair ; ventilation des niveaux affichée (« 83 W éco + 0 W confort », vu sur la trame réelle etm69) ; - ECO_FLOOR_MISSED retenu par charge (publié un seul cycle), y compris pour une charge sortie de loads[] ; « manque non chiffrable » quand missingWh est absent, jamais 0 Wh ; - PRICE_SLOT et OPTIMIZER_DECISION en FR / DE / EN (app_en.arb et app_de.arb créés pour ces seules clés ; le reste retombe sur le français, listé dans l10n_untranslated.json). Code inconnu : repli générique avec le code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
044fcbd0f8
commit
fb5b0e7165
@ -1,69 +0,0 @@
|
||||
/// Modèles du tableau de bord.
|
||||
///
|
||||
/// 🔴 `HourlyFlow`, `WeatherPoint` et `DayPlan` ont été **supprimés** le 2026-09-08 avec la
|
||||
/// carte « Plan de la journée » qu'ils alimentaient. `DayPlan.demo()` fabriquait vingt-quatre
|
||||
/// heures de flux à partir d'une gaussienne et de coefficients écrits en dur, présentées sans
|
||||
/// mention comme la journée en cours.
|
||||
///
|
||||
/// Le code part **avec** la carte, délibérément : un générateur de données plausibles laissé
|
||||
/// dans le dépôt se rebranche — c'est ce qui s'était passé pour les deux MPPT, les 38 % de
|
||||
/// PAC, les 82 % de part solaire et le SOC à 62 %. Un plan reviendra quand il y aura une
|
||||
/// prévision PV et une source tarifaire ; il se réécrira sur des données, pas sur celles-ci.
|
||||
library;
|
||||
|
||||
|
||||
class HeosAction {
|
||||
final String iconKey;
|
||||
final String title;
|
||||
final String why;
|
||||
final double powerKw;
|
||||
|
||||
const HeosAction({
|
||||
required this.iconKey,
|
||||
required this.title,
|
||||
required this.why,
|
||||
required this.powerKw,
|
||||
});
|
||||
}
|
||||
|
||||
class HeosDecision {
|
||||
final String summary;
|
||||
final String highlight;
|
||||
final List<HeosAction> actions;
|
||||
final DateTime updatedAt;
|
||||
|
||||
const HeosDecision({
|
||||
required this.summary,
|
||||
required this.highlight,
|
||||
required this.actions,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
static HeosDecision demo() => HeosDecision(
|
||||
summary:
|
||||
'Le surplus solaire part dans l\'eau chaude et la voiture '
|
||||
'plutôt que d\'être revendu à bas prix.',
|
||||
highlight: 'plutôt que d\'être revendu à bas prix',
|
||||
actions: [
|
||||
const HeosAction(
|
||||
iconKey: 'water',
|
||||
title: 'Chauffe-eau',
|
||||
why: 'Surplus PV disponible · coût réseau ce soir 0,22 €/kWh',
|
||||
powerKw: 2.0,
|
||||
),
|
||||
const HeosAction(
|
||||
iconKey: 'ev',
|
||||
title: 'Voiture — mode Éco',
|
||||
why: 'Charge à partir du surplus · objectif 80 % pour 18h',
|
||||
powerKw: 3.6,
|
||||
),
|
||||
const HeosAction(
|
||||
iconKey: 'battery',
|
||||
title: 'Batterie en charge',
|
||||
why: 'Stockage pour couvrir le pic 18h–21h',
|
||||
powerKw: 0.8,
|
||||
),
|
||||
],
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
218
lib/features/dashboard/widgets/en_ce_moment_card.dart
Normal file
218
lib/features/dashboard/widgets/en_ce_moment_card.dart
Normal file
@ -0,0 +1,218 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../l10n/app_localizations.dart';
|
||||
import '../../../l10n/l10n_ext.dart';
|
||||
import '../../../models/load_telemetry.dart';
|
||||
import '../../../providers/load_config_provider.dart';
|
||||
import '../../../providers/load_telemetry_provider.dart';
|
||||
import '../../../services/telemetry_text.dart';
|
||||
import '../../../theme/etm_tokens.dart';
|
||||
|
||||
/// **Carte « En ce moment »** — ce que le moteur décide pour chaque charge, ce cycle-ci.
|
||||
///
|
||||
/// Remplace la carte de démonstration du 2026-09 (décision G de la consigne, 2026-10-16) :
|
||||
/// elle affichait des décisions INVENTÉES (« voiture 3,6 kW », « batterie en charge » sur
|
||||
/// un banc sans batterie) sous un nom de marque. Celle-ci lit `GetLoadTelemetry` —
|
||||
/// `decision.code` et `params` par charge, rendus par `decisionText` — et rien d'autre.
|
||||
///
|
||||
/// Rien n'est dit d'un « optimiseur actif » : l'objet de capacités qui le dirait n'existe
|
||||
/// pas encore côté box.
|
||||
class EnCeMomentCard extends StatelessWidget {
|
||||
const EnCeMomentCard({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tel = context.watch<LoadTelemetryProvider>();
|
||||
final cfg = context.watch<LoadConfigProvider>();
|
||||
String nu(String x) => x.replaceAll(RegExp(r'[{}]'), '').toLowerCase();
|
||||
final config = {for (final c in cfg.entries) nu(c.id): c};
|
||||
return EnCeMomentView(
|
||||
telemetry: tel.telemetry,
|
||||
chargement: tel.loading && tel.telemetry == null,
|
||||
supportee: tel.supported,
|
||||
erreur: tel.error,
|
||||
derniersManques: tel.derniersManques,
|
||||
libelle: (id) => config[nu(id)]?.label ?? id,
|
||||
domaine: (id) => config[nu(id)]?.domain,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Rendu pur — tout ce qu'il affiche lui est donné.
|
||||
class EnCeMomentView extends StatelessWidget {
|
||||
final LoadTelemetry? telemetry;
|
||||
final bool chargement;
|
||||
final bool supportee;
|
||||
final String? erreur;
|
||||
final Map<String, ({LoadDecision decision, DateTime at})> derniersManques;
|
||||
final String Function(String loadId) libelle;
|
||||
final String? Function(String loadId) domaine;
|
||||
|
||||
const EnCeMomentView({
|
||||
super.key,
|
||||
required this.telemetry,
|
||||
this.chargement = false,
|
||||
this.supportee = true,
|
||||
this.erreur,
|
||||
this.derniersManques = const {},
|
||||
required this.libelle,
|
||||
required this.domaine,
|
||||
});
|
||||
|
||||
static String _hm(DateTime t) =>
|
||||
'${t.hour.toString().padLeft(2, '0')}:${t.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = context.l10n;
|
||||
final ink = EtmTokens.inkOf(context);
|
||||
final muted = EtmTokens.mutedOf(context);
|
||||
final tel = telemetry;
|
||||
final cycle = tel?.timestamp?.toLocal();
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: EtmTokens.surfaceOf(context),
|
||||
borderRadius: BorderRadius.circular(EtmTokens.radiusCard),
|
||||
boxShadow: EtmTokens.cardShadowOf(context),
|
||||
),
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: Text(t.enCeMomentTitre,
|
||||
style: EtmTokens.sans(size: 16, weight: FontWeight.w600, color: ink)),
|
||||
),
|
||||
if (cycle != null)
|
||||
Text(t.enCeMomentMaj(_hm(cycle)),
|
||||
style: EtmTokens.mono(size: 10, weight: FontWeight.w400, color: muted)),
|
||||
]),
|
||||
const SizedBox(height: 8),
|
||||
..._corps(context, t),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _corps(BuildContext context, L10n t) {
|
||||
final muted = EtmTokens.mutedOf(context);
|
||||
Text note(String s) => Text(s, style: EtmTokens.sans(size: 12.5, color: muted));
|
||||
final tel = telemetry;
|
||||
if (chargement) {
|
||||
return const [
|
||||
SizedBox(
|
||||
height: 40,
|
||||
child: Center(
|
||||
child: SizedBox(
|
||||
width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2)))),
|
||||
];
|
||||
}
|
||||
if (tel == null) {
|
||||
// Deux « pas de données » distincts : une box sans la méthode n'est pas en panne.
|
||||
return [note(!supportee ? t.enCeMomentNonPublie : t.enCeMomentNonLu)];
|
||||
}
|
||||
|
||||
String nu(String x) => x.replaceAll(RegExp(r'[{}]'), '').toLowerCase();
|
||||
final presentes = {for (final e in tel.loads) nu(e.loadId)};
|
||||
final lignes = <Widget>[
|
||||
if (tel.degradedMode) note(t.enCeMomentDegrade),
|
||||
if (tel.loads.isEmpty && !tel.degradedMode) note(t.enCeMomentAucune),
|
||||
for (final e in tel.loads) _ligne(context, t, e),
|
||||
// Un manque retenu pour une charge qui a quitté `loads[]` (borne débranchée) reste
|
||||
// visible : c'est précisément le cas où on voudrait savoir.
|
||||
for (final m in derniersManques.entries)
|
||||
if (!presentes.contains(m.key))
|
||||
_manque(context, t, m.value, titre: libelle(m.key)),
|
||||
];
|
||||
final out = <Widget>[];
|
||||
for (var i = 0; i < lignes.length; i++) {
|
||||
if (i > 0) out.add(Divider(height: 18, color: EtmTokens.lineOf(context)));
|
||||
out.add(lignes[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
Widget _ligne(BuildContext context, L10n t, LoadTelemetryEntry e) {
|
||||
final ink = EtmTokens.inkOf(context);
|
||||
final muted = EtmTokens.mutedOf(context);
|
||||
final dom = domaine(e.loadId);
|
||||
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(' + ');
|
||||
}
|
||||
final manque = derniersManques[e.loadId.replaceAll(RegExp(r'[{}]'), '').toLowerCase()];
|
||||
|
||||
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Container(
|
||||
width: 34,
|
||||
height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: couleur.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(EtmTokens.radiusCtrl),
|
||||
),
|
||||
child: Icon(_icone(dom, e), color: couleur, size: 18),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Text(libelle(e.loadId),
|
||||
style: EtmTokens.sans(size: 13.5, weight: FontWeight.w600, color: ink)),
|
||||
const SizedBox(height: 2),
|
||||
Text(decisionText(t, e.decision),
|
||||
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),
|
||||
],
|
||||
]),
|
||||
),
|
||||
// La puissance MESURÉE, quand la charge se mesure. Jamais l'allocation à sa place :
|
||||
// décidé ≠ tiré (CLAUDE.md, « trois nombres »).
|
||||
if (m.mesuree) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text('${(m.powerW! / 1000).toStringAsFixed(1).replaceAll('.', ',')} kW',
|
||||
style: EtmTokens.mono(size: 13, color: EtmTokens.ecoOf(context))),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _manque(BuildContext context, L10n t,
|
||||
({LoadDecision decision, DateTime at}) m, {String? titre}) {
|
||||
final texte = t.enCeMomentDernierManque(_hm(m.at), decisionText(t, m.decision));
|
||||
return Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Icon(Icons.warning_amber_rounded, size: 14, color: EtmTokens.orange),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(titre == null ? texte : '$titre — $texte',
|
||||
style: EtmTokens.sans(size: 11.5, color: EtmTokens.inkOf(context))
|
||||
.copyWith(height: 1.35)),
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
static IconData _icone(String? domaine, LoadTelemetryEntry e) {
|
||||
if (e.mechanism?.isEvCharger ?? false) return Icons.ev_station_rounded;
|
||||
return switch (domaine) {
|
||||
'ev' => Icons.ev_station_rounded,
|
||||
'ecs' => Icons.water_drop_rounded,
|
||||
'heating' || 'hvac' => Icons.heat_pump_rounded,
|
||||
_ => Icons.bolt_rounded,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -363,7 +363,7 @@ class _NodeLayerState extends State<_NodeLayer>
|
||||
final d = widget.data;
|
||||
final eco = EtmTokens.ecoOf(context);
|
||||
return Stack(children: [
|
||||
// Hub — Héos with pulsing ring
|
||||
// Hub — pilotage, with pulsing ring
|
||||
_at(Offset(0.5, 0.5), _buildHub(context, eco)),
|
||||
// Solar (top) — tappable → prod
|
||||
_at(Offset(0.5, 0.12),
|
||||
@ -546,7 +546,7 @@ class _NodeLayerState extends State<_NodeLayer>
|
||||
);
|
||||
}
|
||||
|
||||
// Hub with Héos pulsing ring
|
||||
// Hub with pulsing ring
|
||||
Widget _buildHub(BuildContext context, Color eco) {
|
||||
final surf = EtmTokens.surfaceOf(context);
|
||||
final muted = EtmTokens.mutedOf(context);
|
||||
@ -556,7 +556,7 @@ class _NodeLayerState extends State<_NodeLayer>
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Pulsing Héos ring
|
||||
// Pulsing ring
|
||||
if (widget.pulseAnim != null)
|
||||
AnimatedBuilder(
|
||||
animation: widget.pulseAnim!,
|
||||
@ -571,7 +571,7 @@ class _NodeLayerState extends State<_NodeLayer>
|
||||
),
|
||||
),
|
||||
),
|
||||
// Hub circle — Héos
|
||||
// Hub circle — pilotage
|
||||
Container(
|
||||
width: 92, height: 92,
|
||||
decoration: BoxDecoration(
|
||||
@ -584,7 +584,7 @@ class _NodeLayerState extends State<_NodeLayer>
|
||||
children: [
|
||||
HemsIcon(size: 24, color: eco),
|
||||
const SizedBox(height: 3),
|
||||
Text('Héos', style: EtmTokens.sans(size: 9, color: muted)),
|
||||
Text('Pilotage', style: EtmTokens.sans(size: 9, color: muted)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@ -1,280 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../theme/etm_tokens.dart';
|
||||
import '../models/dashboard_models.dart';
|
||||
|
||||
class HeosDecisionCard extends StatefulWidget {
|
||||
const HeosDecisionCard({super.key});
|
||||
|
||||
@override
|
||||
State<HeosDecisionCard> createState() => _HeosDecisionCardState();
|
||||
}
|
||||
|
||||
class _HeosDecisionCardState extends State<HeosDecisionCard>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final AnimationController _pulseCtrl;
|
||||
late final Animation<double> _pulseAnim;
|
||||
final _decision = HeosDecision.demo();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pulseCtrl = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(seconds: 2),
|
||||
)..repeat(reverse: true);
|
||||
_pulseAnim = Tween<double>(begin: 0.25, end: 0.7)
|
||||
.animate(CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pulseCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final eco = EtmTokens.ecoOf(context);
|
||||
final ink = EtmTokens.inkOf(context);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: EtmTokens.surfaceOf(context),
|
||||
borderRadius: BorderRadius.circular(EtmTokens.radiusCard),
|
||||
boxShadow: EtmTokens.cardShadowOf(context),
|
||||
border: Border.all(color: eco.withValues(alpha: 0.25), width: 1),
|
||||
),
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// ── En-tête
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 34, height: 34,
|
||||
decoration: BoxDecoration(
|
||||
color: eco.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(EtmTokens.radiusCtrl),
|
||||
),
|
||||
child: Icon(Icons.auto_awesome_rounded, color: eco, size: 18),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('Héos pilote en ce moment',
|
||||
style: EtmTokens.sans(
|
||||
size: 14,
|
||||
weight: FontWeight.w600,
|
||||
color: ink,
|
||||
)),
|
||||
Text(
|
||||
'optimise sur 24 h · maj ${_fmtTime(_decision.updatedAt)}',
|
||||
style: EtmTokens.mono(
|
||||
size: 10,
|
||||
color: EtmTokens.mutedOf(context),
|
||||
weight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
AnimatedBuilder(
|
||||
animation: _pulseAnim,
|
||||
builder: (context, _) => Container(
|
||||
width: 10, height: 10,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: eco.withValues(alpha: _pulseAnim.value),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: eco.withValues(alpha: _pulseAnim.value * 0.6),
|
||||
blurRadius: 6,
|
||||
spreadRadius: 2,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// ── Résumé avec highlight
|
||||
_HighlightedSummary(
|
||||
text: _decision.summary,
|
||||
highlight: _decision.highlight,
|
||||
highlightColor: eco,
|
||||
textColor: EtmTokens.mutedOf(context),
|
||||
),
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// ── Hairline
|
||||
Divider(height: 1, color: EtmTokens.lineOf(context)),
|
||||
|
||||
// ── Actions
|
||||
...List.generate(_decision.actions.length, (i) {
|
||||
final action = _decision.actions[i];
|
||||
final isLast = i == _decision.actions.length - 1;
|
||||
return Column(
|
||||
children: [
|
||||
_ActionRow(action: action, eco: eco, context: context),
|
||||
if (!isLast)
|
||||
Divider(height: 1, color: EtmTokens.lineOf(context)),
|
||||
],
|
||||
);
|
||||
}),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── Pied
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Voir le plan de la journée',
|
||||
style: EtmTokens.sans(
|
||||
size: 13,
|
||||
weight: FontWeight.w600,
|
||||
color: EtmTokens.brand),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Icon(Icons.arrow_forward_ios_rounded,
|
||||
size: 12, color: EtmTokens.brand),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _fmtTime(DateTime dt) =>
|
||||
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
// ── Highlighted summary ───────────────────────────────────────────────────────
|
||||
|
||||
class _HighlightedSummary extends StatelessWidget {
|
||||
final String text;
|
||||
final String highlight;
|
||||
final Color highlightColor;
|
||||
final Color textColor;
|
||||
|
||||
const _HighlightedSummary({
|
||||
required this.text,
|
||||
required this.highlight,
|
||||
required this.highlightColor,
|
||||
required this.textColor,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final idx = text.indexOf(highlight);
|
||||
if (idx < 0) {
|
||||
return Text(text,
|
||||
style: EtmTokens.sans(size: 13, color: textColor, height: 1.5));
|
||||
}
|
||||
return Text.rich(
|
||||
TextSpan(children: [
|
||||
TextSpan(
|
||||
text: text.substring(0, idx),
|
||||
style: EtmTokens.sans(size: 13, color: textColor, height: 1.5),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 3, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: highlightColor.withValues(alpha: 0.15),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
highlight,
|
||||
style: EtmTokens.sans(
|
||||
size: 13,
|
||||
color: highlightColor,
|
||||
weight: FontWeight.w600,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: text.substring(idx + highlight.length),
|
||||
style: EtmTokens.sans(size: 13, color: textColor, height: 1.5),
|
||||
),
|
||||
]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Action row ────────────────────────────────────────────────────────────────
|
||||
|
||||
class _ActionRow extends StatelessWidget {
|
||||
final HeosAction action;
|
||||
final Color eco;
|
||||
final BuildContext context;
|
||||
|
||||
const _ActionRow({
|
||||
required this.action,
|
||||
required this.eco,
|
||||
required this.context,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext _) {
|
||||
final icon = _iconFor(action.iconKey);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 32, height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: eco.withValues(alpha: 0.10),
|
||||
borderRadius: BorderRadius.circular(EtmTokens.radiusCtrl),
|
||||
),
|
||||
child: Icon(icon, color: eco, size: 17),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(action.title,
|
||||
style: EtmTokens.sans(
|
||||
size: 13,
|
||||
weight: FontWeight.w600,
|
||||
color: EtmTokens.inkOf(context))),
|
||||
const SizedBox(height: 2),
|
||||
Text(action.why,
|
||||
style: EtmTokens.sans(
|
||||
size: 11, color: EtmTokens.mutedOf(context))),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${action.powerKw.toStringAsFixed(1)} kW',
|
||||
style: EtmTokens.mono(size: 13, color: eco),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
IconData _iconFor(String key) => switch (key) {
|
||||
'water' => Icons.water_drop_outlined,
|
||||
'ev' => Icons.ev_station_rounded,
|
||||
'battery' => Icons.battery_charging_full_rounded,
|
||||
'heat' => Icons.heat_pump_outlined,
|
||||
_ => Icons.bolt_rounded,
|
||||
};
|
||||
}
|
||||
7
lib/l10n/app_de.arb
Normal file
7
lib/l10n/app_de.arb
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"@@locale": "de",
|
||||
"enCeMomentTitre": "Gerade jetzt",
|
||||
"decisionPriceSlot": "Günstiger Preis ({prix} ct/kWh).",
|
||||
"decisionPriceSlotSetpoint": "Günstiger Preis ({prix} ct/kWh) — Sollwert {setpoint} W.",
|
||||
"decisionOptimizer": "Entscheidung des Optimierers."
|
||||
}
|
||||
7
lib/l10n/app_en.arb
Normal file
7
lib/l10n/app_en.arb
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"enCeMomentTitre": "Right now",
|
||||
"decisionPriceSlot": "Favourable price ({prix} ct/kWh).",
|
||||
"decisionPriceSlotSetpoint": "Favourable price ({prix} ct/kWh) — setpoint {setpoint} W.",
|
||||
"decisionOptimizer": "Optimizer decision."
|
||||
}
|
||||
@ -479,5 +479,85 @@
|
||||
"requested": { "type": "String" },
|
||||
"effective": { "type": "String" }
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"enCeMomentTitre": "En ce moment",
|
||||
"@enCeMomentTitre": {
|
||||
"description": "Titre de la carte d'accueil des décisions par charge. Décision G du 2026-10-16 : aucun nom de marque."
|
||||
},
|
||||
"enCeMomentMaj": "cycle de {heure}",
|
||||
"@enCeMomentMaj": {
|
||||
"placeholders": {
|
||||
"heure": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"enCeMomentAucune": "Aucune charge pilotée en ce moment.",
|
||||
"enCeMomentNonPublie": "Cette box ne publie pas l'arbitrage des charges.",
|
||||
"enCeMomentNonLu": "Arbitrage non lu : la box n'a pas répondu.",
|
||||
"enCeMomentDegrade": "Mode dégradé : le moteur n'a pas de budget ce cycle.",
|
||||
"enCeMomentDernierManque": "Objectif manqué à {heure} : {texte}",
|
||||
"@enCeMomentDernierManque": {
|
||||
"description": "ECO_FLOOR_MISSED n'est publié qu'un cycle : l'app retient le dernier par charge.",
|
||||
"placeholders": {
|
||||
"heure": {
|
||||
"type": "String"
|
||||
},
|
||||
"texte": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ventilationNiveau": "{w} W {niveau}",
|
||||
"@ventilationNiveau": {
|
||||
"placeholders": {
|
||||
"w": {
|
||||
"type": "int",
|
||||
"format": "decimalPattern"
|
||||
},
|
||||
"niveau": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ecoManqueNonChiffrable": " · manque non chiffrable",
|
||||
"@ecoManqueNonChiffrable": {
|
||||
"description": "missingWh ABSENT (cause unmeasurable) : jamais « 0 Wh »."
|
||||
},
|
||||
"decisionPriceSlot": "Prix favorable ({prix} ct/kWh).",
|
||||
"@decisionPriceSlot": {
|
||||
"description": "PRICE_SLOT (etm70). Prix complet du créneau.",
|
||||
"placeholders": {
|
||||
"prix": {
|
||||
"type": "double",
|
||||
"format": "decimalPatternDigits",
|
||||
"optionalParameters": {
|
||||
"decimalDigits": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"decisionPriceSlotSetpoint": "Prix favorable ({prix} ct/kWh) — consigne {setpoint} W.",
|
||||
"@decisionPriceSlotSetpoint": {
|
||||
"placeholders": {
|
||||
"prix": {
|
||||
"type": "double",
|
||||
"format": "decimalPatternDigits",
|
||||
"optionalParameters": {
|
||||
"decimalDigits": 2
|
||||
}
|
||||
},
|
||||
"setpoint": {
|
||||
"type": "int",
|
||||
"format": "decimalPattern"
|
||||
}
|
||||
}
|
||||
},
|
||||
"decisionOptimizer": "Décision de l'optimiseur.",
|
||||
"@decisionOptimizer": {
|
||||
"description": "OPTIMIZER_DECISION (etm70) : motif hors catalogue, sans paramètre. Aucun nom de marque."
|
||||
},
|
||||
"ventilationEco": "éco",
|
||||
"ventilationConfort": "confort"
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
|
||||
import 'app_localizations_de.dart';
|
||||
import 'app_localizations_en.dart';
|
||||
import 'app_localizations_fr.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
@ -91,7 +93,11 @@ abstract class L10n {
|
||||
];
|
||||
|
||||
/// A list of this localizations delegate's supported locales.
|
||||
static const List<Locale> supportedLocales = <Locale>[Locale('fr')];
|
||||
static const List<Locale> supportedLocales = <Locale>[
|
||||
Locale('de'),
|
||||
Locale('en'),
|
||||
Locale('fr'),
|
||||
];
|
||||
|
||||
/// Titre de l'écran installateur de configuration des charges pilotables
|
||||
///
|
||||
@ -968,6 +974,90 @@ abstract class L10n {
|
||||
/// In fr, this message translates to:
|
||||
/// **'Locale demandée : {requested} · retenue par la box : {effective}'**
|
||||
String localeRequested(String requested, String effective);
|
||||
|
||||
/// Titre de la carte d'accueil des décisions par charge. Décision G du 2026-10-16 : aucun nom de marque.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'En ce moment'**
|
||||
String get enCeMomentTitre;
|
||||
|
||||
/// No description provided for @enCeMomentMaj.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'cycle de {heure}'**
|
||||
String enCeMomentMaj(String heure);
|
||||
|
||||
/// No description provided for @enCeMomentAucune.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Aucune charge pilotée en ce moment.'**
|
||||
String get enCeMomentAucune;
|
||||
|
||||
/// No description provided for @enCeMomentNonPublie.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Cette box ne publie pas l\'arbitrage des charges.'**
|
||||
String get enCeMomentNonPublie;
|
||||
|
||||
/// No description provided for @enCeMomentNonLu.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Arbitrage non lu : la box n\'a pas répondu.'**
|
||||
String get enCeMomentNonLu;
|
||||
|
||||
/// No description provided for @enCeMomentDegrade.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Mode dégradé : le moteur n\'a pas de budget ce cycle.'**
|
||||
String get enCeMomentDegrade;
|
||||
|
||||
/// ECO_FLOOR_MISSED n'est publié qu'un cycle : l'app retient le dernier par charge.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Objectif manqué à {heure} : {texte}'**
|
||||
String enCeMomentDernierManque(String heure, String texte);
|
||||
|
||||
/// No description provided for @ventilationNiveau.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'{w} W {niveau}'**
|
||||
String ventilationNiveau(int w, String niveau);
|
||||
|
||||
/// missingWh ABSENT (cause unmeasurable) : jamais « 0 Wh ».
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **' · manque non chiffrable'**
|
||||
String get ecoManqueNonChiffrable;
|
||||
|
||||
/// PRICE_SLOT (etm70). Prix complet du créneau.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Prix favorable ({prix} ct/kWh).'**
|
||||
String decisionPriceSlot(double prix);
|
||||
|
||||
/// No description provided for @decisionPriceSlotSetpoint.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Prix favorable ({prix} ct/kWh) — consigne {setpoint} W.'**
|
||||
String decisionPriceSlotSetpoint(double prix, int setpoint);
|
||||
|
||||
/// OPTIMIZER_DECISION (etm70) : motif hors catalogue, sans paramètre. Aucun nom de marque.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'Décision de l\'optimiseur.'**
|
||||
String get decisionOptimizer;
|
||||
|
||||
/// No description provided for @ventilationEco.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'éco'**
|
||||
String get ventilationEco;
|
||||
|
||||
/// No description provided for @ventilationConfort.
|
||||
///
|
||||
/// In fr, this message translates to:
|
||||
/// **'confort'**
|
||||
String get ventilationConfort;
|
||||
}
|
||||
|
||||
class _L10nDelegate extends LocalizationsDelegate<L10n> {
|
||||
@ -980,7 +1070,7 @@ class _L10nDelegate extends LocalizationsDelegate<L10n> {
|
||||
|
||||
@override
|
||||
bool isSupported(Locale locale) =>
|
||||
<String>['fr'].contains(locale.languageCode);
|
||||
<String>['de', 'en', 'fr'].contains(locale.languageCode);
|
||||
|
||||
@override
|
||||
bool shouldReload(_L10nDelegate old) => false;
|
||||
@ -989,6 +1079,10 @@ class _L10nDelegate extends LocalizationsDelegate<L10n> {
|
||||
L10n lookupL10n(Locale locale) {
|
||||
// Lookup logic when only language code is specified.
|
||||
switch (locale.languageCode) {
|
||||
case 'de':
|
||||
return L10nDe();
|
||||
case 'en':
|
||||
return L10nEn();
|
||||
case 'fr':
|
||||
return L10nFr();
|
||||
}
|
||||
|
||||
820
lib/l10n/app_localizations_de.dart
Normal file
820
lib/l10n/app_localizations_de.dart
Normal file
@ -0,0 +1,820 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for German (`de`).
|
||||
class L10nDe extends L10n {
|
||||
L10nDe([String locale = 'de']) : super(locale);
|
||||
|
||||
@override
|
||||
String get installerLoadsTitle => 'Rôles & appareils';
|
||||
|
||||
@override
|
||||
String get installerLoadsSubtitle =>
|
||||
'Ordre de service des charges pilotables';
|
||||
|
||||
@override
|
||||
String get loadsEmpty =>
|
||||
'Aucune charge pilotable déclarée sur cette installation.';
|
||||
|
||||
@override
|
||||
String loadsLoadError(String error) {
|
||||
return 'Lecture de la configuration impossible : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sectionUnclassified => 'Non classé';
|
||||
|
||||
@override
|
||||
String get domainEv => 'Véhicule électrique';
|
||||
|
||||
@override
|
||||
String get domainEcs => 'Eau chaude sanitaire';
|
||||
|
||||
@override
|
||||
String get domainHvac => 'Climatisation';
|
||||
|
||||
@override
|
||||
String get domainHeating => 'Chauffage';
|
||||
|
||||
@override
|
||||
String domainUnknown(String code) {
|
||||
return 'Domaine « $code »';
|
||||
}
|
||||
|
||||
@override
|
||||
String get adapterRelayRouter => 'Routeur de relais';
|
||||
|
||||
@override
|
||||
String get adapterEtmVariableLoad => 'Charge modulable';
|
||||
|
||||
@override
|
||||
String get adapterSgReady => 'SG-Ready';
|
||||
|
||||
@override
|
||||
String get adapterEvCharger => 'Borne de recharge';
|
||||
|
||||
@override
|
||||
String adapterUnknown(String code) {
|
||||
return 'Mécanisme « $code »';
|
||||
}
|
||||
|
||||
@override
|
||||
String get loadEnabled => 'Active';
|
||||
|
||||
@override
|
||||
String get loadDisabled => 'Exclue de l\'arbitrage';
|
||||
|
||||
@override
|
||||
String loadPriority(int rank) {
|
||||
return 'Rang $rank';
|
||||
}
|
||||
|
||||
@override
|
||||
String loadMinOn(int seconds) {
|
||||
return 'Maintien ON minimal : $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String loadMinOff(int seconds) {
|
||||
return 'Maintien OFF minimal : $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String loadRelayCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count relais',
|
||||
one: '1 relais',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get loadMeterSlot => 'Compteur dédié : non rattachable';
|
||||
|
||||
@override
|
||||
String get orderNotGroupedTitle => 'Ordre non groupé';
|
||||
|
||||
@override
|
||||
String get orderNotGroupedBody =>
|
||||
'L\'ordre en place sur la box n\'est pas groupé par domaine : les charges d\'un même domaine ne se suivent pas. L\'ordre réel est affiché ci-dessous, tel que la box l\'applique.';
|
||||
|
||||
@override
|
||||
String get orderNotGroupedAction => 'Normaliser l\'ordre';
|
||||
|
||||
@override
|
||||
String get orderNotGroupedWarning =>
|
||||
'Normaliser modifiera l\'ordre d\'arbitrage réel de l\'installation. Aucune écriture ne part tant que vous n\'avez pas confirmé.';
|
||||
|
||||
@override
|
||||
String get orderNormalizeConfirm => 'Normaliser et enregistrer';
|
||||
|
||||
@override
|
||||
String get cancel => 'Annuler';
|
||||
|
||||
@override
|
||||
String get saveOrder => 'Enregistrer l\'ordre';
|
||||
|
||||
@override
|
||||
String get savePending => 'Envoi en cours…';
|
||||
|
||||
@override
|
||||
String get saveWaitingBox => 'En attente de confirmation de la box…';
|
||||
|
||||
@override
|
||||
String get saveConfirmed => 'Configuration confirmée par la box.';
|
||||
|
||||
@override
|
||||
String saveRejected(String reason) {
|
||||
return 'La box a refusé : $reason';
|
||||
}
|
||||
|
||||
@override
|
||||
String get saveDiverged =>
|
||||
'La box a enregistré un état différent de celui demandé. L\'affichage montre ce que la box applique réellement.';
|
||||
|
||||
@override
|
||||
String validationDuplicateId(String id) {
|
||||
return 'L\'identifiant de charge « $id » apparaît deux fois.';
|
||||
}
|
||||
|
||||
@override
|
||||
String validationThingClaimedTwice(
|
||||
String thingId,
|
||||
String first,
|
||||
String second,
|
||||
) {
|
||||
return 'L\'appareil $thingId est commandé par deux charges actives, « $first » et « $second » : deux commandes contradictoires et une puissance comptée deux fois.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryUnavailableTitle => 'Télémétrie indisponible';
|
||||
|
||||
@override
|
||||
String get telemetryUnavailableBody =>
|
||||
'Cette box n\'expose pas GetLoadTelemetry. Puissance allouée, verrous, disponibilité et motifs de décision ne sont pas lisibles depuis l\'app.';
|
||||
|
||||
@override
|
||||
String telemetryStale(int seconds) {
|
||||
return 'Données anciennes — dernier point il y a $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryDegradedMode =>
|
||||
'Mode dégradé L2 — compteur muet, planification suspendue';
|
||||
|
||||
@override
|
||||
String get faultTitle => 'En défaut';
|
||||
|
||||
@override
|
||||
String get faultClearAction => 'Lever le défaut';
|
||||
|
||||
@override
|
||||
String get faultCleared => 'Défaut levé — confirmé par la télémétrie relue.';
|
||||
|
||||
@override
|
||||
String get faultClearNoop => 'Aucun défaut à lever sur cette charge.';
|
||||
|
||||
@override
|
||||
String faultUnknown(String code) {
|
||||
return 'Défaut « $code »';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionUnknown(String code, String params) {
|
||||
return 'Décision « $code »$params';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryTitle => 'Arbitrage en direct';
|
||||
|
||||
@override
|
||||
String get telemetryNeverRan =>
|
||||
'Aucun cycle d\'arbitrage depuis le démarrage du service.';
|
||||
|
||||
@override
|
||||
String telemetryLastCycle(int seconds) {
|
||||
return 'Dernier cycle il y a $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryBudgetTitle => 'Budget de surplus du cycle';
|
||||
|
||||
@override
|
||||
String get telemetryBudgetAbsent =>
|
||||
'Aucun budget publié pour ce cycle : la planification est suspendue.';
|
||||
|
||||
@override
|
||||
String get budgetSurplus => 'Surplus entrant';
|
||||
|
||||
@override
|
||||
String get budgetEvReserved => 'Commandé, pas encore mesuré';
|
||||
|
||||
@override
|
||||
String get budgetRecredited => 'Recrédité';
|
||||
|
||||
@override
|
||||
String get budgetAllocated => 'Alloué';
|
||||
|
||||
@override
|
||||
String get budgetRemaining => 'Reliquat';
|
||||
|
||||
@override
|
||||
String watts(int watts) {
|
||||
final intl.NumberFormat wattsNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String wattsString = wattsNumberFormat.format(watts);
|
||||
|
||||
return '$wattsString W';
|
||||
}
|
||||
|
||||
@override
|
||||
String telemetryAllocated(int watts) {
|
||||
final intl.NumberFormat wattsNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String wattsString = wattsNumberFormat.format(watts);
|
||||
|
||||
return 'Alloué $wattsString W';
|
||||
}
|
||||
|
||||
@override
|
||||
String telemetryEvCharging(double current, int phases) {
|
||||
final intl.NumberFormat currentNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String currentString = currentNumberFormat.format(current);
|
||||
|
||||
return 'Borne : en charge, $currentString A sur $phases phase(s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryEvIdle => 'Borne : ne charge pas';
|
||||
|
||||
@override
|
||||
String get telemetryEvUnplugged => 'Borne : aucun véhicule branché';
|
||||
|
||||
@override
|
||||
String get telemetryEvStateUnknown => 'Borne : état non publié ce cycle';
|
||||
|
||||
@override
|
||||
String get telemetryFundedByGrid =>
|
||||
'Financée au réseau (échéance ou tarif dynamique) — hors budget de surplus';
|
||||
|
||||
@override
|
||||
String get niveauEco => 'ÉCO';
|
||||
|
||||
@override
|
||||
String get niveauConfort => 'CONFORT';
|
||||
|
||||
@override
|
||||
String decisionEcoFloorGrid(int gridW) {
|
||||
return 'Plancher servi — $gridW W achetés au réseau';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionEcoFloorMet(String livre, String cible) {
|
||||
return 'Plancher atteint — $cible dus, $livre livrés';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionEcoFloorMissed(String raison) {
|
||||
return 'Obligation abandonnée à la bascule — $raison';
|
||||
}
|
||||
|
||||
@override
|
||||
String get ecoCauseDrawCap => 'le plafond de soutirage l\'a refusée';
|
||||
|
||||
@override
|
||||
String get ecoCauseLoadCapacity =>
|
||||
'la charge ne pouvait pas absorber davantage';
|
||||
|
||||
@override
|
||||
String get ecoCauseUnmeasurable =>
|
||||
'avancement non vérifiable — ni tenue, ni chiffrable';
|
||||
|
||||
@override
|
||||
String get ecoCauseUnavailable => 'la charge était indisponible';
|
||||
|
||||
@override
|
||||
String ecoManque(String wh) {
|
||||
return ' · il manquait $wh';
|
||||
}
|
||||
|
||||
@override
|
||||
String ecoAvancementMesure(String livre, String cible) {
|
||||
return '$livre sur $cible livrés';
|
||||
}
|
||||
|
||||
@override
|
||||
String ecoAvancementNonVerifiable(String wh) {
|
||||
return 'reste $wh à livrer — avancement non vérifiable';
|
||||
}
|
||||
|
||||
@override
|
||||
String ecoMesureDepuis(String heure) {
|
||||
return 'mesuré depuis $heure';
|
||||
}
|
||||
|
||||
@override
|
||||
String ecoMesureIntermittente(String heure) {
|
||||
return 'mesuré par intermittence depuis $heure';
|
||||
}
|
||||
|
||||
@override
|
||||
String get ecoTauxConstant =>
|
||||
'taux constant (cible ÷ jour) — sans mesure, rien ne le corrige';
|
||||
|
||||
@override
|
||||
String get ecoAuMaximum => 'au maximum de la charge';
|
||||
|
||||
@override
|
||||
String ecoAchete(String wh) {
|
||||
return '$wh achetés pour cette obligation';
|
||||
}
|
||||
|
||||
@override
|
||||
String get obligationTitre => 'Obligation quotidienne';
|
||||
|
||||
@override
|
||||
String get obligationSousTitre =>
|
||||
'Le plancher que la box servira quoi qu\'il arrive, au réseau s\'il le faut.';
|
||||
|
||||
@override
|
||||
String get obligationEnergie => 'Énergie due par jour';
|
||||
|
||||
@override
|
||||
String get obligationEcheance => 'Avant';
|
||||
|
||||
@override
|
||||
String obligationEcheanceHorloge(String heure, String fuseau) {
|
||||
return '$heure heure de la box ($fuseau) — pas celle de cet appareil.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get obligationNonMesurableTitre =>
|
||||
'Cette obligation ne pourra jamais être close';
|
||||
|
||||
@override
|
||||
String obligationNonMesurableCorps(String cout) {
|
||||
return 'La box ne saura pas mesurer ce qu\'elle livre ici : l\'obligation ne pourra être annoncée ni tenue, ni manquée d\'une quantité connue. Elle sera achetée au réseau à taux constant — $cout par jour — sans que rien ne vienne le corriger.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get obligationCauseNoMeter =>
|
||||
'Aucun compteur n\'est rattaché à cette charge. Rattachez-en un ci-dessus : c\'est la cause la plus fréquente, et elle se répare en une manipulation.';
|
||||
|
||||
@override
|
||||
String get obligationCauseNoEnergyMeter =>
|
||||
'Le compteur rattaché publie une puissance, pas une énergie. Il faut un compteur qui totalise les kWh — la puissance seule ne suffit pas à mesurer ce qui a été livré.';
|
||||
|
||||
@override
|
||||
String get obligationCauseNoSessionEnergy =>
|
||||
'Cette borne ne publie pas l\'énergie de session. C\'est le matériel : aucun réglage ne le changera, et il vaut mieux le savoir maintenant qu\'après avoir cherché.';
|
||||
|
||||
@override
|
||||
String obligationCauseInconnue(String cause) {
|
||||
return 'La box ne dit pas pourquoi (cause « $cause » inconnue de cette version).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get ecoIncoherenceTenue =>
|
||||
'Incohérence de la box : plancher annoncé atteint alors que l\'avancement n\'est pas mesurable';
|
||||
|
||||
@override
|
||||
String confortEnAttente(String motif) {
|
||||
return 'En attente — $motif';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryNotArbitrated =>
|
||||
'Non arbitrée — absente du dernier cycle publié';
|
||||
|
||||
@override
|
||||
String lockMinOn(int seconds) {
|
||||
return 'Maintien ON — $seconds s restantes';
|
||||
}
|
||||
|
||||
@override
|
||||
String lockMinOff(int seconds) {
|
||||
return 'Maintien OFF — $seconds s restantes';
|
||||
}
|
||||
|
||||
@override
|
||||
String lockMinStateHold(int seconds) {
|
||||
return 'Maintien d\'état — $seconds s restantes';
|
||||
}
|
||||
|
||||
@override
|
||||
String lockUnknown(String kind, int seconds) {
|
||||
return 'Verrou « $kind » — $seconds s restantes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get lockAsOfCycle => 'au dernier cycle';
|
||||
|
||||
@override
|
||||
String mechanismRelayStage(int stage, int max) {
|
||||
final intl.NumberFormat stageNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String stageString = stageNumberFormat.format(stage);
|
||||
final intl.NumberFormat maxNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String maxString = maxNumberFormat.format(max);
|
||||
|
||||
return 'Palier $stageString W sur $maxString W';
|
||||
}
|
||||
|
||||
@override
|
||||
String mechanismVariableSetpoint(int setpoint, int percent) {
|
||||
final intl.NumberFormat setpointNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String setpointString = setpointNumberFormat.format(setpoint);
|
||||
|
||||
return 'Consigne $setpointString W ($percent %)';
|
||||
}
|
||||
|
||||
@override
|
||||
String mechanismSgReadyState(int state, int estimated) {
|
||||
final intl.NumberFormat estimatedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String estimatedString = estimatedNumberFormat.format(estimated);
|
||||
|
||||
return 'État SG-Ready $state — estimation $estimatedString W';
|
||||
}
|
||||
|
||||
@override
|
||||
String mechanismEvCharger(double current, int phases) {
|
||||
final intl.NumberFormat currentNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String currentString = currentNumberFormat.format(current);
|
||||
|
||||
return '$currentString A sur $phases phase(s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String mechanismUnknown(String kind) {
|
||||
return 'Mécanisme « $kind »';
|
||||
}
|
||||
|
||||
@override
|
||||
String get decisionEvSurplus =>
|
||||
'Véhicule rechargé sur le surplus photovoltaïque.';
|
||||
|
||||
@override
|
||||
String get decisionEvSpotMarket =>
|
||||
'Véhicule rechargé sur un créneau tarifaire favorable.';
|
||||
|
||||
@override
|
||||
String get decisionEvDeadline =>
|
||||
'Véhicule prioritaire : l\'échéance approche.';
|
||||
|
||||
@override
|
||||
String get decisionEvEcoMin =>
|
||||
'Pas de surplus : le courant minimum est maintenu.';
|
||||
|
||||
@override
|
||||
String get decisionEvIdle => 'Pas de surplus : la recharge est suspendue.';
|
||||
|
||||
@override
|
||||
String decisionLoadUnavailable(int frozen) {
|
||||
final intl.NumberFormat frozenNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String frozenString = frozenNumberFormat.format(frozen);
|
||||
|
||||
return 'Charge en défaut : aucune commande n\'est émise, et $frozenString W restent comptés au budget.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionLockMinOn(int applied, int budget) {
|
||||
final intl.NumberFormat appliedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String appliedString = appliedNumberFormat.format(applied);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Le maintien ON a relevé la consigne à $appliedString W ; le budget en payait $budgetString W.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionLockMinOff(int requested, int budget) {
|
||||
final intl.NumberFormat requestedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String requestedString = requestedNumberFormat.format(requested);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Le maintien OFF interdit l\'enclenchement ; le budget payait pourtant $requestedString W sur $budgetString W disponibles.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionLockMinOffCapped(int applied, int requested, int budget) {
|
||||
final intl.NumberFormat appliedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String appliedString = appliedNumberFormat.format(applied);
|
||||
final intl.NumberFormat requestedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String requestedString = requestedNumberFormat.format(requested);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Le maintien OFF plafonne la consigne à $appliedString W ; $requestedString W étaient demandés sur $budgetString W de budget.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSurplusSetpoint(int setpoint, int budget) {
|
||||
final intl.NumberFormat setpointNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String setpointString = setpointNumberFormat.format(setpoint);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Consigne de $setpointString W servie par le surplus ($budgetString W de budget).';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSurplusSetpointStepped(int setpoint, int budget) {
|
||||
final intl.NumberFormat setpointNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String setpointString = setpointNumberFormat.format(setpoint);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Consigne de $setpointString W servie par le surplus, arrondie sur un palier déclaré ($budgetString W de budget).';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSurplusInsufficient(int budget) {
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Aucun surplus à distribuer ($budgetString W) — rien n\'est commandé.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionLockMinStateHold(int state) {
|
||||
return 'État $state maintenu par protection anti court-cycle.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSgForced(int estimated, int budget) {
|
||||
final intl.NumberFormat estimatedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String estimatedString = estimatedNumberFormat.format(estimated);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Pompe à chaleur en état 4 (forcé) — estimation $estimatedString W sur $budgetString W de budget.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSgRecommended(int estimated, int budget) {
|
||||
final intl.NumberFormat estimatedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String estimatedString = estimatedNumberFormat.format(estimated);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Pompe à chaleur en état 3 (recommandation) — estimation $estimatedString W sur $budgetString W de budget.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSgNormal(int budget) {
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Aucun surplus ($budgetString W) — pompe à chaleur en état 2 (normal, sans consigne).';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionBelowMinPower(int budget, int min) {
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
final intl.NumberFormat minNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String minString = minNumberFormat.format(min);
|
||||
|
||||
return 'Budget de $budgetString W sous le premier palier ($minString W) — rien n\'est commandé, le budget passe à la charge suivante.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionBelowMinPowerState(int budget, int state, int min) {
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
final intl.NumberFormat minNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String minString = minNumberFormat.format(min);
|
||||
|
||||
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 decisionEvGridStart(int grid, int budget, int floor) {
|
||||
final intl.NumberFormat gridNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String gridString = gridNumberFormat.format(grid);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
final intl.NumberFormat floorNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String floorString = floorNumberFormat.format(floor);
|
||||
|
||||
return 'Démarrage soutiré : $gridString W achetés au réseau pour compléter les $budgetString W de surplus et atteindre le plancher de $floorString W.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionPhaseLimit(int limit, int required) {
|
||||
final intl.NumberFormat limitNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String limitString = limitNumberFormat.format(limit);
|
||||
final intl.NumberFormat requiredNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String requiredString = requiredNumberFormat.format(required);
|
||||
|
||||
return 'Limite de phase atteinte : $limitString W disponibles pour $requiredString W exigés — ce n\'est pas le surplus qui manque.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionBatteryReserveWithheld(int withheld, int soc, int seuil) {
|
||||
final intl.NumberFormat withheldNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String withheldString = withheldNumberFormat.format(withheld);
|
||||
|
||||
return '$withheldString W de surplus retenus 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 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.';
|
||||
|
||||
@override
|
||||
String get decisionSafeStateRelay =>
|
||||
'État sûr avant retrait : tous les relais sont ouverts.';
|
||||
|
||||
@override
|
||||
String get decisionSafeStateSetpoint =>
|
||||
'État sûr avant retrait : consigne ramenée à 0 W.';
|
||||
|
||||
@override
|
||||
String get decisionSafeStateSgReady =>
|
||||
'État sûr avant retrait : état 2 (normal), jamais le blocage.';
|
||||
|
||||
@override
|
||||
String get faultWriteFailed =>
|
||||
'Échec d\'écriture : l\'échelle de commande est épuisée, plus aucune commande n\'est émise tant que le défaut n\'est pas levé.';
|
||||
|
||||
@override
|
||||
String get faultThingMissing =>
|
||||
'L\'appareil commandé n\'existe plus dans nymea — retiré, ou jamais réinstallé après un remplacement. La charge, elle, est toujours déclarée : c\'est pour ça qu\'elle apparaît ici.';
|
||||
|
||||
@override
|
||||
String get faultThingMissingAction =>
|
||||
'Rien à lever ici : le défaut ne partira pas tant qu\'aucun appareil ne répond. Réinstallez-le dans nymea, ou retirez cette charge de la configuration.';
|
||||
|
||||
@override
|
||||
String get faultUnusableEncoding =>
|
||||
'Encodage SG-Ready inexploitable : l\'état 2 ne peut pas être exprimé, cette pompe à chaleur n\'est pas pilotable.';
|
||||
|
||||
@override
|
||||
String get faultClearPending =>
|
||||
'Levée demandée — en attente de la télémétrie…';
|
||||
|
||||
@override
|
||||
String get faultClearStillFaulty =>
|
||||
'La charge est toujours en défaut après la levée.';
|
||||
|
||||
@override
|
||||
String faultClearRejected(String reason) {
|
||||
return 'La box a refusé la levée : $reason';
|
||||
}
|
||||
|
||||
@override
|
||||
String get faultClearHint =>
|
||||
'La levée est une action délibérée : le défaut ne se lève jamais tout seul.';
|
||||
|
||||
@override
|
||||
String get domainBattery => 'Batterie';
|
||||
|
||||
@override
|
||||
String domainAssignTitle(String label) {
|
||||
return 'Classer « $label »';
|
||||
}
|
||||
|
||||
@override
|
||||
String get domainAssignAction => 'Changer de domaine';
|
||||
|
||||
@override
|
||||
String get domainAssignNone => 'Retirer du classement';
|
||||
|
||||
@override
|
||||
String get domainAssignHint =>
|
||||
'Le domaine est une intention déclarée par l\'installateur. L\'arbitre ne la lit pas : elle ne sert qu\'à grouper l\'ordre de service.';
|
||||
|
||||
@override
|
||||
String localeRequested(String requested, String effective) {
|
||||
return 'Locale demandée : $requested · retenue par la box : $effective';
|
||||
}
|
||||
|
||||
@override
|
||||
String get enCeMomentTitre => 'Gerade jetzt';
|
||||
|
||||
@override
|
||||
String enCeMomentMaj(String heure) {
|
||||
return 'cycle de $heure';
|
||||
}
|
||||
|
||||
@override
|
||||
String get enCeMomentAucune => 'Aucune charge pilotée en ce moment.';
|
||||
|
||||
@override
|
||||
String get enCeMomentNonPublie =>
|
||||
'Cette box ne publie pas l\'arbitrage des charges.';
|
||||
|
||||
@override
|
||||
String get enCeMomentNonLu => 'Arbitrage non lu : la box n\'a pas répondu.';
|
||||
|
||||
@override
|
||||
String get enCeMomentDegrade =>
|
||||
'Mode dégradé : le moteur n\'a pas de budget ce cycle.';
|
||||
|
||||
@override
|
||||
String enCeMomentDernierManque(String heure, String texte) {
|
||||
return 'Objectif manqué à $heure : $texte';
|
||||
}
|
||||
|
||||
@override
|
||||
String ventilationNiveau(int w, String niveau) {
|
||||
final intl.NumberFormat wNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String wString = wNumberFormat.format(w);
|
||||
|
||||
return '$wString W $niveau';
|
||||
}
|
||||
|
||||
@override
|
||||
String get ecoManqueNonChiffrable => ' · manque non chiffrable';
|
||||
|
||||
@override
|
||||
String decisionPriceSlot(double prix) {
|
||||
final intl.NumberFormat prixNumberFormat =
|
||||
intl.NumberFormat.decimalPatternDigits(
|
||||
locale: localeName,
|
||||
decimalDigits: 2,
|
||||
);
|
||||
final String prixString = prixNumberFormat.format(prix);
|
||||
|
||||
return 'Günstiger Preis ($prixString ct/kWh).';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionPriceSlotSetpoint(double prix, int setpoint) {
|
||||
final intl.NumberFormat prixNumberFormat =
|
||||
intl.NumberFormat.decimalPatternDigits(
|
||||
locale: localeName,
|
||||
decimalDigits: 2,
|
||||
);
|
||||
final String prixString = prixNumberFormat.format(prix);
|
||||
final intl.NumberFormat setpointNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String setpointString = setpointNumberFormat.format(setpoint);
|
||||
|
||||
return 'Günstiger Preis ($prixString ct/kWh) — Sollwert $setpointString W.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get decisionOptimizer => 'Entscheidung des Optimierers.';
|
||||
|
||||
@override
|
||||
String get ventilationEco => 'éco';
|
||||
|
||||
@override
|
||||
String get ventilationConfort => 'confort';
|
||||
}
|
||||
820
lib/l10n/app_localizations_en.dart
Normal file
820
lib/l10n/app_localizations_en.dart
Normal file
@ -0,0 +1,820 @@
|
||||
// ignore: unused_import
|
||||
import 'package:intl/intl.dart' as intl;
|
||||
import 'app_localizations.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
|
||||
/// The translations for English (`en`).
|
||||
class L10nEn extends L10n {
|
||||
L10nEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get installerLoadsTitle => 'Rôles & appareils';
|
||||
|
||||
@override
|
||||
String get installerLoadsSubtitle =>
|
||||
'Ordre de service des charges pilotables';
|
||||
|
||||
@override
|
||||
String get loadsEmpty =>
|
||||
'Aucune charge pilotable déclarée sur cette installation.';
|
||||
|
||||
@override
|
||||
String loadsLoadError(String error) {
|
||||
return 'Lecture de la configuration impossible : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get sectionUnclassified => 'Non classé';
|
||||
|
||||
@override
|
||||
String get domainEv => 'Véhicule électrique';
|
||||
|
||||
@override
|
||||
String get domainEcs => 'Eau chaude sanitaire';
|
||||
|
||||
@override
|
||||
String get domainHvac => 'Climatisation';
|
||||
|
||||
@override
|
||||
String get domainHeating => 'Chauffage';
|
||||
|
||||
@override
|
||||
String domainUnknown(String code) {
|
||||
return 'Domaine « $code »';
|
||||
}
|
||||
|
||||
@override
|
||||
String get adapterRelayRouter => 'Routeur de relais';
|
||||
|
||||
@override
|
||||
String get adapterEtmVariableLoad => 'Charge modulable';
|
||||
|
||||
@override
|
||||
String get adapterSgReady => 'SG-Ready';
|
||||
|
||||
@override
|
||||
String get adapterEvCharger => 'Borne de recharge';
|
||||
|
||||
@override
|
||||
String adapterUnknown(String code) {
|
||||
return 'Mécanisme « $code »';
|
||||
}
|
||||
|
||||
@override
|
||||
String get loadEnabled => 'Active';
|
||||
|
||||
@override
|
||||
String get loadDisabled => 'Exclue de l\'arbitrage';
|
||||
|
||||
@override
|
||||
String loadPriority(int rank) {
|
||||
return 'Rang $rank';
|
||||
}
|
||||
|
||||
@override
|
||||
String loadMinOn(int seconds) {
|
||||
return 'Maintien ON minimal : $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String loadMinOff(int seconds) {
|
||||
return 'Maintien OFF minimal : $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String loadRelayCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count relais',
|
||||
one: '1 relais',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get loadMeterSlot => 'Compteur dédié : non rattachable';
|
||||
|
||||
@override
|
||||
String get orderNotGroupedTitle => 'Ordre non groupé';
|
||||
|
||||
@override
|
||||
String get orderNotGroupedBody =>
|
||||
'L\'ordre en place sur la box n\'est pas groupé par domaine : les charges d\'un même domaine ne se suivent pas. L\'ordre réel est affiché ci-dessous, tel que la box l\'applique.';
|
||||
|
||||
@override
|
||||
String get orderNotGroupedAction => 'Normaliser l\'ordre';
|
||||
|
||||
@override
|
||||
String get orderNotGroupedWarning =>
|
||||
'Normaliser modifiera l\'ordre d\'arbitrage réel de l\'installation. Aucune écriture ne part tant que vous n\'avez pas confirmé.';
|
||||
|
||||
@override
|
||||
String get orderNormalizeConfirm => 'Normaliser et enregistrer';
|
||||
|
||||
@override
|
||||
String get cancel => 'Annuler';
|
||||
|
||||
@override
|
||||
String get saveOrder => 'Enregistrer l\'ordre';
|
||||
|
||||
@override
|
||||
String get savePending => 'Envoi en cours…';
|
||||
|
||||
@override
|
||||
String get saveWaitingBox => 'En attente de confirmation de la box…';
|
||||
|
||||
@override
|
||||
String get saveConfirmed => 'Configuration confirmée par la box.';
|
||||
|
||||
@override
|
||||
String saveRejected(String reason) {
|
||||
return 'La box a refusé : $reason';
|
||||
}
|
||||
|
||||
@override
|
||||
String get saveDiverged =>
|
||||
'La box a enregistré un état différent de celui demandé. L\'affichage montre ce que la box applique réellement.';
|
||||
|
||||
@override
|
||||
String validationDuplicateId(String id) {
|
||||
return 'L\'identifiant de charge « $id » apparaît deux fois.';
|
||||
}
|
||||
|
||||
@override
|
||||
String validationThingClaimedTwice(
|
||||
String thingId,
|
||||
String first,
|
||||
String second,
|
||||
) {
|
||||
return 'L\'appareil $thingId est commandé par deux charges actives, « $first » et « $second » : deux commandes contradictoires et une puissance comptée deux fois.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryUnavailableTitle => 'Télémétrie indisponible';
|
||||
|
||||
@override
|
||||
String get telemetryUnavailableBody =>
|
||||
'Cette box n\'expose pas GetLoadTelemetry. Puissance allouée, verrous, disponibilité et motifs de décision ne sont pas lisibles depuis l\'app.';
|
||||
|
||||
@override
|
||||
String telemetryStale(int seconds) {
|
||||
return 'Données anciennes — dernier point il y a $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryDegradedMode =>
|
||||
'Mode dégradé L2 — compteur muet, planification suspendue';
|
||||
|
||||
@override
|
||||
String get faultTitle => 'En défaut';
|
||||
|
||||
@override
|
||||
String get faultClearAction => 'Lever le défaut';
|
||||
|
||||
@override
|
||||
String get faultCleared => 'Défaut levé — confirmé par la télémétrie relue.';
|
||||
|
||||
@override
|
||||
String get faultClearNoop => 'Aucun défaut à lever sur cette charge.';
|
||||
|
||||
@override
|
||||
String faultUnknown(String code) {
|
||||
return 'Défaut « $code »';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionUnknown(String code, String params) {
|
||||
return 'Décision « $code »$params';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryTitle => 'Arbitrage en direct';
|
||||
|
||||
@override
|
||||
String get telemetryNeverRan =>
|
||||
'Aucun cycle d\'arbitrage depuis le démarrage du service.';
|
||||
|
||||
@override
|
||||
String telemetryLastCycle(int seconds) {
|
||||
return 'Dernier cycle il y a $seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryBudgetTitle => 'Budget de surplus du cycle';
|
||||
|
||||
@override
|
||||
String get telemetryBudgetAbsent =>
|
||||
'Aucun budget publié pour ce cycle : la planification est suspendue.';
|
||||
|
||||
@override
|
||||
String get budgetSurplus => 'Surplus entrant';
|
||||
|
||||
@override
|
||||
String get budgetEvReserved => 'Commandé, pas encore mesuré';
|
||||
|
||||
@override
|
||||
String get budgetRecredited => 'Recrédité';
|
||||
|
||||
@override
|
||||
String get budgetAllocated => 'Alloué';
|
||||
|
||||
@override
|
||||
String get budgetRemaining => 'Reliquat';
|
||||
|
||||
@override
|
||||
String watts(int watts) {
|
||||
final intl.NumberFormat wattsNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String wattsString = wattsNumberFormat.format(watts);
|
||||
|
||||
return '$wattsString W';
|
||||
}
|
||||
|
||||
@override
|
||||
String telemetryAllocated(int watts) {
|
||||
final intl.NumberFormat wattsNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String wattsString = wattsNumberFormat.format(watts);
|
||||
|
||||
return 'Alloué $wattsString W';
|
||||
}
|
||||
|
||||
@override
|
||||
String telemetryEvCharging(double current, int phases) {
|
||||
final intl.NumberFormat currentNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String currentString = currentNumberFormat.format(current);
|
||||
|
||||
return 'Borne : en charge, $currentString A sur $phases phase(s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryEvIdle => 'Borne : ne charge pas';
|
||||
|
||||
@override
|
||||
String get telemetryEvUnplugged => 'Borne : aucun véhicule branché';
|
||||
|
||||
@override
|
||||
String get telemetryEvStateUnknown => 'Borne : état non publié ce cycle';
|
||||
|
||||
@override
|
||||
String get telemetryFundedByGrid =>
|
||||
'Financée au réseau (échéance ou tarif dynamique) — hors budget de surplus';
|
||||
|
||||
@override
|
||||
String get niveauEco => 'ÉCO';
|
||||
|
||||
@override
|
||||
String get niveauConfort => 'CONFORT';
|
||||
|
||||
@override
|
||||
String decisionEcoFloorGrid(int gridW) {
|
||||
return 'Plancher servi — $gridW W achetés au réseau';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionEcoFloorMet(String livre, String cible) {
|
||||
return 'Plancher atteint — $cible dus, $livre livrés';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionEcoFloorMissed(String raison) {
|
||||
return 'Obligation abandonnée à la bascule — $raison';
|
||||
}
|
||||
|
||||
@override
|
||||
String get ecoCauseDrawCap => 'le plafond de soutirage l\'a refusée';
|
||||
|
||||
@override
|
||||
String get ecoCauseLoadCapacity =>
|
||||
'la charge ne pouvait pas absorber davantage';
|
||||
|
||||
@override
|
||||
String get ecoCauseUnmeasurable =>
|
||||
'avancement non vérifiable — ni tenue, ni chiffrable';
|
||||
|
||||
@override
|
||||
String get ecoCauseUnavailable => 'la charge était indisponible';
|
||||
|
||||
@override
|
||||
String ecoManque(String wh) {
|
||||
return ' · il manquait $wh';
|
||||
}
|
||||
|
||||
@override
|
||||
String ecoAvancementMesure(String livre, String cible) {
|
||||
return '$livre sur $cible livrés';
|
||||
}
|
||||
|
||||
@override
|
||||
String ecoAvancementNonVerifiable(String wh) {
|
||||
return 'reste $wh à livrer — avancement non vérifiable';
|
||||
}
|
||||
|
||||
@override
|
||||
String ecoMesureDepuis(String heure) {
|
||||
return 'mesuré depuis $heure';
|
||||
}
|
||||
|
||||
@override
|
||||
String ecoMesureIntermittente(String heure) {
|
||||
return 'mesuré par intermittence depuis $heure';
|
||||
}
|
||||
|
||||
@override
|
||||
String get ecoTauxConstant =>
|
||||
'taux constant (cible ÷ jour) — sans mesure, rien ne le corrige';
|
||||
|
||||
@override
|
||||
String get ecoAuMaximum => 'au maximum de la charge';
|
||||
|
||||
@override
|
||||
String ecoAchete(String wh) {
|
||||
return '$wh achetés pour cette obligation';
|
||||
}
|
||||
|
||||
@override
|
||||
String get obligationTitre => 'Obligation quotidienne';
|
||||
|
||||
@override
|
||||
String get obligationSousTitre =>
|
||||
'Le plancher que la box servira quoi qu\'il arrive, au réseau s\'il le faut.';
|
||||
|
||||
@override
|
||||
String get obligationEnergie => 'Énergie due par jour';
|
||||
|
||||
@override
|
||||
String get obligationEcheance => 'Avant';
|
||||
|
||||
@override
|
||||
String obligationEcheanceHorloge(String heure, String fuseau) {
|
||||
return '$heure heure de la box ($fuseau) — pas celle de cet appareil.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get obligationNonMesurableTitre =>
|
||||
'Cette obligation ne pourra jamais être close';
|
||||
|
||||
@override
|
||||
String obligationNonMesurableCorps(String cout) {
|
||||
return 'La box ne saura pas mesurer ce qu\'elle livre ici : l\'obligation ne pourra être annoncée ni tenue, ni manquée d\'une quantité connue. Elle sera achetée au réseau à taux constant — $cout par jour — sans que rien ne vienne le corriger.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get obligationCauseNoMeter =>
|
||||
'Aucun compteur n\'est rattaché à cette charge. Rattachez-en un ci-dessus : c\'est la cause la plus fréquente, et elle se répare en une manipulation.';
|
||||
|
||||
@override
|
||||
String get obligationCauseNoEnergyMeter =>
|
||||
'Le compteur rattaché publie une puissance, pas une énergie. Il faut un compteur qui totalise les kWh — la puissance seule ne suffit pas à mesurer ce qui a été livré.';
|
||||
|
||||
@override
|
||||
String get obligationCauseNoSessionEnergy =>
|
||||
'Cette borne ne publie pas l\'énergie de session. C\'est le matériel : aucun réglage ne le changera, et il vaut mieux le savoir maintenant qu\'après avoir cherché.';
|
||||
|
||||
@override
|
||||
String obligationCauseInconnue(String cause) {
|
||||
return 'La box ne dit pas pourquoi (cause « $cause » inconnue de cette version).';
|
||||
}
|
||||
|
||||
@override
|
||||
String get ecoIncoherenceTenue =>
|
||||
'Incohérence de la box : plancher annoncé atteint alors que l\'avancement n\'est pas mesurable';
|
||||
|
||||
@override
|
||||
String confortEnAttente(String motif) {
|
||||
return 'En attente — $motif';
|
||||
}
|
||||
|
||||
@override
|
||||
String get telemetryNotArbitrated =>
|
||||
'Non arbitrée — absente du dernier cycle publié';
|
||||
|
||||
@override
|
||||
String lockMinOn(int seconds) {
|
||||
return 'Maintien ON — $seconds s restantes';
|
||||
}
|
||||
|
||||
@override
|
||||
String lockMinOff(int seconds) {
|
||||
return 'Maintien OFF — $seconds s restantes';
|
||||
}
|
||||
|
||||
@override
|
||||
String lockMinStateHold(int seconds) {
|
||||
return 'Maintien d\'état — $seconds s restantes';
|
||||
}
|
||||
|
||||
@override
|
||||
String lockUnknown(String kind, int seconds) {
|
||||
return 'Verrou « $kind » — $seconds s restantes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get lockAsOfCycle => 'au dernier cycle';
|
||||
|
||||
@override
|
||||
String mechanismRelayStage(int stage, int max) {
|
||||
final intl.NumberFormat stageNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String stageString = stageNumberFormat.format(stage);
|
||||
final intl.NumberFormat maxNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String maxString = maxNumberFormat.format(max);
|
||||
|
||||
return 'Palier $stageString W sur $maxString W';
|
||||
}
|
||||
|
||||
@override
|
||||
String mechanismVariableSetpoint(int setpoint, int percent) {
|
||||
final intl.NumberFormat setpointNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String setpointString = setpointNumberFormat.format(setpoint);
|
||||
|
||||
return 'Consigne $setpointString W ($percent %)';
|
||||
}
|
||||
|
||||
@override
|
||||
String mechanismSgReadyState(int state, int estimated) {
|
||||
final intl.NumberFormat estimatedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String estimatedString = estimatedNumberFormat.format(estimated);
|
||||
|
||||
return 'État SG-Ready $state — estimation $estimatedString W';
|
||||
}
|
||||
|
||||
@override
|
||||
String mechanismEvCharger(double current, int phases) {
|
||||
final intl.NumberFormat currentNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String currentString = currentNumberFormat.format(current);
|
||||
|
||||
return '$currentString A sur $phases phase(s)';
|
||||
}
|
||||
|
||||
@override
|
||||
String mechanismUnknown(String kind) {
|
||||
return 'Mécanisme « $kind »';
|
||||
}
|
||||
|
||||
@override
|
||||
String get decisionEvSurplus =>
|
||||
'Véhicule rechargé sur le surplus photovoltaïque.';
|
||||
|
||||
@override
|
||||
String get decisionEvSpotMarket =>
|
||||
'Véhicule rechargé sur un créneau tarifaire favorable.';
|
||||
|
||||
@override
|
||||
String get decisionEvDeadline =>
|
||||
'Véhicule prioritaire : l\'échéance approche.';
|
||||
|
||||
@override
|
||||
String get decisionEvEcoMin =>
|
||||
'Pas de surplus : le courant minimum est maintenu.';
|
||||
|
||||
@override
|
||||
String get decisionEvIdle => 'Pas de surplus : la recharge est suspendue.';
|
||||
|
||||
@override
|
||||
String decisionLoadUnavailable(int frozen) {
|
||||
final intl.NumberFormat frozenNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String frozenString = frozenNumberFormat.format(frozen);
|
||||
|
||||
return 'Charge en défaut : aucune commande n\'est émise, et $frozenString W restent comptés au budget.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionLockMinOn(int applied, int budget) {
|
||||
final intl.NumberFormat appliedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String appliedString = appliedNumberFormat.format(applied);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Le maintien ON a relevé la consigne à $appliedString W ; le budget en payait $budgetString W.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionLockMinOff(int requested, int budget) {
|
||||
final intl.NumberFormat requestedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String requestedString = requestedNumberFormat.format(requested);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Le maintien OFF interdit l\'enclenchement ; le budget payait pourtant $requestedString W sur $budgetString W disponibles.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionLockMinOffCapped(int applied, int requested, int budget) {
|
||||
final intl.NumberFormat appliedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String appliedString = appliedNumberFormat.format(applied);
|
||||
final intl.NumberFormat requestedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String requestedString = requestedNumberFormat.format(requested);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Le maintien OFF plafonne la consigne à $appliedString W ; $requestedString W étaient demandés sur $budgetString W de budget.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSurplusSetpoint(int setpoint, int budget) {
|
||||
final intl.NumberFormat setpointNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String setpointString = setpointNumberFormat.format(setpoint);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Consigne de $setpointString W servie par le surplus ($budgetString W de budget).';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSurplusSetpointStepped(int setpoint, int budget) {
|
||||
final intl.NumberFormat setpointNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String setpointString = setpointNumberFormat.format(setpoint);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Consigne de $setpointString W servie par le surplus, arrondie sur un palier déclaré ($budgetString W de budget).';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSurplusInsufficient(int budget) {
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Aucun surplus à distribuer ($budgetString W) — rien n\'est commandé.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionLockMinStateHold(int state) {
|
||||
return 'État $state maintenu par protection anti court-cycle.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSgForced(int estimated, int budget) {
|
||||
final intl.NumberFormat estimatedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String estimatedString = estimatedNumberFormat.format(estimated);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Pompe à chaleur en état 4 (forcé) — estimation $estimatedString W sur $budgetString W de budget.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSgRecommended(int estimated, int budget) {
|
||||
final intl.NumberFormat estimatedNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String estimatedString = estimatedNumberFormat.format(estimated);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Pompe à chaleur en état 3 (recommandation) — estimation $estimatedString W sur $budgetString W de budget.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionSgNormal(int budget) {
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
|
||||
return 'Aucun surplus ($budgetString W) — pompe à chaleur en état 2 (normal, sans consigne).';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionBelowMinPower(int budget, int min) {
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
final intl.NumberFormat minNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String minString = minNumberFormat.format(min);
|
||||
|
||||
return 'Budget de $budgetString W sous le premier palier ($minString W) — rien n\'est commandé, le budget passe à la charge suivante.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionBelowMinPowerState(int budget, int state, int min) {
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
final intl.NumberFormat minNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String minString = minNumberFormat.format(min);
|
||||
|
||||
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 decisionEvGridStart(int grid, int budget, int floor) {
|
||||
final intl.NumberFormat gridNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String gridString = gridNumberFormat.format(grid);
|
||||
final intl.NumberFormat budgetNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String budgetString = budgetNumberFormat.format(budget);
|
||||
final intl.NumberFormat floorNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String floorString = floorNumberFormat.format(floor);
|
||||
|
||||
return 'Démarrage soutiré : $gridString W achetés au réseau pour compléter les $budgetString W de surplus et atteindre le plancher de $floorString W.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionPhaseLimit(int limit, int required) {
|
||||
final intl.NumberFormat limitNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String limitString = limitNumberFormat.format(limit);
|
||||
final intl.NumberFormat requiredNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String requiredString = requiredNumberFormat.format(required);
|
||||
|
||||
return 'Limite de phase atteinte : $limitString W disponibles pour $requiredString W exigés — ce n\'est pas le surplus qui manque.';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionBatteryReserveWithheld(int withheld, int soc, int seuil) {
|
||||
final intl.NumberFormat withheldNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String withheldString = withheldNumberFormat.format(withheld);
|
||||
|
||||
return '$withheldString W de surplus retenus 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 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.';
|
||||
|
||||
@override
|
||||
String get decisionSafeStateRelay =>
|
||||
'État sûr avant retrait : tous les relais sont ouverts.';
|
||||
|
||||
@override
|
||||
String get decisionSafeStateSetpoint =>
|
||||
'État sûr avant retrait : consigne ramenée à 0 W.';
|
||||
|
||||
@override
|
||||
String get decisionSafeStateSgReady =>
|
||||
'État sûr avant retrait : état 2 (normal), jamais le blocage.';
|
||||
|
||||
@override
|
||||
String get faultWriteFailed =>
|
||||
'Échec d\'écriture : l\'échelle de commande est épuisée, plus aucune commande n\'est émise tant que le défaut n\'est pas levé.';
|
||||
|
||||
@override
|
||||
String get faultThingMissing =>
|
||||
'L\'appareil commandé n\'existe plus dans nymea — retiré, ou jamais réinstallé après un remplacement. La charge, elle, est toujours déclarée : c\'est pour ça qu\'elle apparaît ici.';
|
||||
|
||||
@override
|
||||
String get faultThingMissingAction =>
|
||||
'Rien à lever ici : le défaut ne partira pas tant qu\'aucun appareil ne répond. Réinstallez-le dans nymea, ou retirez cette charge de la configuration.';
|
||||
|
||||
@override
|
||||
String get faultUnusableEncoding =>
|
||||
'Encodage SG-Ready inexploitable : l\'état 2 ne peut pas être exprimé, cette pompe à chaleur n\'est pas pilotable.';
|
||||
|
||||
@override
|
||||
String get faultClearPending =>
|
||||
'Levée demandée — en attente de la télémétrie…';
|
||||
|
||||
@override
|
||||
String get faultClearStillFaulty =>
|
||||
'La charge est toujours en défaut après la levée.';
|
||||
|
||||
@override
|
||||
String faultClearRejected(String reason) {
|
||||
return 'La box a refusé la levée : $reason';
|
||||
}
|
||||
|
||||
@override
|
||||
String get faultClearHint =>
|
||||
'La levée est une action délibérée : le défaut ne se lève jamais tout seul.';
|
||||
|
||||
@override
|
||||
String get domainBattery => 'Batterie';
|
||||
|
||||
@override
|
||||
String domainAssignTitle(String label) {
|
||||
return 'Classer « $label »';
|
||||
}
|
||||
|
||||
@override
|
||||
String get domainAssignAction => 'Changer de domaine';
|
||||
|
||||
@override
|
||||
String get domainAssignNone => 'Retirer du classement';
|
||||
|
||||
@override
|
||||
String get domainAssignHint =>
|
||||
'Le domaine est une intention déclarée par l\'installateur. L\'arbitre ne la lit pas : elle ne sert qu\'à grouper l\'ordre de service.';
|
||||
|
||||
@override
|
||||
String localeRequested(String requested, String effective) {
|
||||
return 'Locale demandée : $requested · retenue par la box : $effective';
|
||||
}
|
||||
|
||||
@override
|
||||
String get enCeMomentTitre => 'Right now';
|
||||
|
||||
@override
|
||||
String enCeMomentMaj(String heure) {
|
||||
return 'cycle de $heure';
|
||||
}
|
||||
|
||||
@override
|
||||
String get enCeMomentAucune => 'Aucune charge pilotée en ce moment.';
|
||||
|
||||
@override
|
||||
String get enCeMomentNonPublie =>
|
||||
'Cette box ne publie pas l\'arbitrage des charges.';
|
||||
|
||||
@override
|
||||
String get enCeMomentNonLu => 'Arbitrage non lu : la box n\'a pas répondu.';
|
||||
|
||||
@override
|
||||
String get enCeMomentDegrade =>
|
||||
'Mode dégradé : le moteur n\'a pas de budget ce cycle.';
|
||||
|
||||
@override
|
||||
String enCeMomentDernierManque(String heure, String texte) {
|
||||
return 'Objectif manqué à $heure : $texte';
|
||||
}
|
||||
|
||||
@override
|
||||
String ventilationNiveau(int w, String niveau) {
|
||||
final intl.NumberFormat wNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String wString = wNumberFormat.format(w);
|
||||
|
||||
return '$wString W $niveau';
|
||||
}
|
||||
|
||||
@override
|
||||
String get ecoManqueNonChiffrable => ' · manque non chiffrable';
|
||||
|
||||
@override
|
||||
String decisionPriceSlot(double prix) {
|
||||
final intl.NumberFormat prixNumberFormat =
|
||||
intl.NumberFormat.decimalPatternDigits(
|
||||
locale: localeName,
|
||||
decimalDigits: 2,
|
||||
);
|
||||
final String prixString = prixNumberFormat.format(prix);
|
||||
|
||||
return 'Favourable price ($prixString ct/kWh).';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionPriceSlotSetpoint(double prix, int setpoint) {
|
||||
final intl.NumberFormat prixNumberFormat =
|
||||
intl.NumberFormat.decimalPatternDigits(
|
||||
locale: localeName,
|
||||
decimalDigits: 2,
|
||||
);
|
||||
final String prixString = prixNumberFormat.format(prix);
|
||||
final intl.NumberFormat setpointNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String setpointString = setpointNumberFormat.format(setpoint);
|
||||
|
||||
return 'Favourable price ($prixString ct/kWh) — setpoint $setpointString W.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get decisionOptimizer => 'Optimizer decision.';
|
||||
|
||||
@override
|
||||
String get ventilationEco => 'éco';
|
||||
|
||||
@override
|
||||
String get ventilationConfort => 'confort';
|
||||
}
|
||||
@ -741,4 +741,80 @@ class L10nFr extends L10n {
|
||||
String localeRequested(String requested, String effective) {
|
||||
return 'Locale demandée : $requested · retenue par la box : $effective';
|
||||
}
|
||||
|
||||
@override
|
||||
String get enCeMomentTitre => 'En ce moment';
|
||||
|
||||
@override
|
||||
String enCeMomentMaj(String heure) {
|
||||
return 'cycle de $heure';
|
||||
}
|
||||
|
||||
@override
|
||||
String get enCeMomentAucune => 'Aucune charge pilotée en ce moment.';
|
||||
|
||||
@override
|
||||
String get enCeMomentNonPublie =>
|
||||
'Cette box ne publie pas l\'arbitrage des charges.';
|
||||
|
||||
@override
|
||||
String get enCeMomentNonLu => 'Arbitrage non lu : la box n\'a pas répondu.';
|
||||
|
||||
@override
|
||||
String get enCeMomentDegrade =>
|
||||
'Mode dégradé : le moteur n\'a pas de budget ce cycle.';
|
||||
|
||||
@override
|
||||
String enCeMomentDernierManque(String heure, String texte) {
|
||||
return 'Objectif manqué à $heure : $texte';
|
||||
}
|
||||
|
||||
@override
|
||||
String ventilationNiveau(int w, String niveau) {
|
||||
final intl.NumberFormat wNumberFormat = intl.NumberFormat.decimalPattern(
|
||||
localeName,
|
||||
);
|
||||
final String wString = wNumberFormat.format(w);
|
||||
|
||||
return '$wString W $niveau';
|
||||
}
|
||||
|
||||
@override
|
||||
String get ecoManqueNonChiffrable => ' · manque non chiffrable';
|
||||
|
||||
@override
|
||||
String decisionPriceSlot(double prix) {
|
||||
final intl.NumberFormat prixNumberFormat =
|
||||
intl.NumberFormat.decimalPatternDigits(
|
||||
locale: localeName,
|
||||
decimalDigits: 2,
|
||||
);
|
||||
final String prixString = prixNumberFormat.format(prix);
|
||||
|
||||
return 'Prix favorable ($prixString ct/kWh).';
|
||||
}
|
||||
|
||||
@override
|
||||
String decisionPriceSlotSetpoint(double prix, int setpoint) {
|
||||
final intl.NumberFormat prixNumberFormat =
|
||||
intl.NumberFormat.decimalPatternDigits(
|
||||
locale: localeName,
|
||||
decimalDigits: 2,
|
||||
);
|
||||
final String prixString = prixNumberFormat.format(prix);
|
||||
final intl.NumberFormat setpointNumberFormat =
|
||||
intl.NumberFormat.decimalPattern(localeName);
|
||||
final String setpointString = setpointNumberFormat.format(setpoint);
|
||||
|
||||
return 'Prix favorable ($prixString ct/kWh) — consigne $setpointString W.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get decisionOptimizer => 'Décision de l\'optimiseur.';
|
||||
|
||||
@override
|
||||
String get ventilationEco => 'éco';
|
||||
|
||||
@override
|
||||
String get ventilationConfort => 'confort';
|
||||
}
|
||||
|
||||
@ -78,6 +78,16 @@ class LoadTelemetryProvider extends ChangeNotifier {
|
||||
|
||||
// ── Lecture ────────────────────────────────────────────────────────────────
|
||||
LoadTelemetry? get telemetry => _telemetry;
|
||||
|
||||
/// **Le dernier objectif éco manqué, par charge** (`ECO_FLOOR_MISSED`, etm70).
|
||||
///
|
||||
/// Le moteur ne le publie qu'UN cycle — celui de la bascule — puis la charge reprend son
|
||||
/// motif ordinaire. Lu à l'instant, il serait presque toujours invisible : l'app le
|
||||
/// RETIENT. Clé = identifiant de charge normalisé (sans accolades, minuscules), y compris
|
||||
/// pour une charge qui a quitté `loads[]` depuis (borne débranchée).
|
||||
Map<String, ({LoadDecision decision, DateTime at})> get derniersManques =>
|
||||
Map.unmodifiable(_derniersManques);
|
||||
final _derniersManques = <String, ({LoadDecision decision, DateTime at})>{};
|
||||
bool get loading => _loading;
|
||||
String? get error => _error;
|
||||
|
||||
@ -182,6 +192,16 @@ class LoadTelemetryProvider extends ChangeNotifier {
|
||||
void _adopt(Map<String, dynamic> payload) {
|
||||
final t = LoadTelemetry.fromJson(payload);
|
||||
final now = DateTime.now();
|
||||
for (final e in t.loads) {
|
||||
// En tête de `decision` (R7), ou dans son niveau éco : les deux se lisent.
|
||||
final candidats = [e.decision, for (final l in e.levels) ?l.decision];
|
||||
for (final d in candidats) {
|
||||
if (d.code != 'ECO_FLOOR_MISSED') continue;
|
||||
_derniersManques[e.loadId.replaceAll(RegExp(r'[{}]'), '').toLowerCase()] =
|
||||
(decision: d, at: t.timestamp?.toLocal() ?? now);
|
||||
break;
|
||||
}
|
||||
}
|
||||
_telemetry = t;
|
||||
_lastFrameAt = now;
|
||||
_error = null;
|
||||
@ -279,6 +299,7 @@ class LoadTelemetryProvider extends ChangeNotifier {
|
||||
/// Vide l'état au switch d'installation — cette télémétrie appartient à la box quittée.
|
||||
void clearForSwitch() {
|
||||
_telemetry = null;
|
||||
_derniersManques.clear();
|
||||
_error = null;
|
||||
_lastFrameAt = null;
|
||||
_cycleFirstSeenAt = null;
|
||||
|
||||
@ -14,7 +14,7 @@ import '../main.dart' show DrawerMenuButton;
|
||||
// ① Thermostats par pièce EN HAUT (geste fréquent)
|
||||
// - pièces actives : expandées avec temp + modes
|
||||
// - pièces éteintes : compactes
|
||||
// ② Sources pilotées par Héos EN BAS
|
||||
// ② Sources pilotées EN BAS
|
||||
// - PAC Atlantic (SG-Ready : 4 états)
|
||||
// - Chauffe-eau thermodynamique (Surplus / Eco / Boost)
|
||||
// - Climatiseur (rafraîchissement anticipé)
|
||||
@ -39,7 +39,7 @@ class _Zone {
|
||||
final double targetTemp;
|
||||
final ACMode mode;
|
||||
final bool isOn;
|
||||
final bool heosActive; // Héos pilote en ce moment
|
||||
final bool pilotageActif; // piloté en ce moment
|
||||
|
||||
const _Zone({
|
||||
required this.name,
|
||||
@ -47,7 +47,7 @@ class _Zone {
|
||||
required this.targetTemp,
|
||||
required this.mode,
|
||||
required this.isOn,
|
||||
this.heosActive = false,
|
||||
this.pilotageActif = false,
|
||||
});
|
||||
|
||||
_Zone copyWith({double? targetTemp, ACMode? mode, bool? isOn}) => _Zone(
|
||||
@ -56,7 +56,7 @@ class _Zone {
|
||||
targetTemp: targetTemp ?? this.targetTemp,
|
||||
mode: mode ?? this.mode,
|
||||
isOn: isOn ?? this.isOn,
|
||||
heosActive: heosActive,
|
||||
pilotageActif: pilotageActif,
|
||||
);
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ class ACScreen extends StatefulWidget {
|
||||
|
||||
class _ACScreenState extends State<ACScreen> {
|
||||
final List<_Zone> _zones = [
|
||||
const _Zone(name: 'Salon', currentTemp: 19.5, targetTemp: 21.0, mode: ACMode.heat, isOn: true, heosActive: true),
|
||||
const _Zone(name: 'Salon', currentTemp: 19.5, targetTemp: 21.0, mode: ACMode.heat, isOn: true, pilotageActif: true),
|
||||
const _Zone(name: 'Chambre', currentTemp: 18.0, targetTemp: 19.0, mode: ACMode.cool, isOn: false),
|
||||
const _Zone(name: 'Bureau', currentTemp: 20.0, targetTemp: 22.0, mode: ACMode.heat, isOn: true),
|
||||
const _Zone(name: 'Cuisine', currentTemp: 21.5, targetTemp: 21.0, mode: ACMode.auto, isOn: false),
|
||||
@ -81,7 +81,7 @@ class _ACScreenState extends State<ACScreen> {
|
||||
int? _zonesSurLaBox;
|
||||
|
||||
SGReadyState _sgReady = SGReadyState.forced;
|
||||
bool _sgAuto = true; // Héos pilote SG-Ready
|
||||
bool _sgAuto = true; // SG-Ready piloté
|
||||
|
||||
DHWMode _dhwMode = DHWMode.surplus;
|
||||
double _dhwCurrent = 52;
|
||||
@ -155,8 +155,8 @@ class _ACScreenState extends State<ACScreen> {
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// ── ② Sources pilotées par Héos ─────────────────────────────
|
||||
_SectionLabel('SOURCES PILOTÉES PAR HÉOS'),
|
||||
// ── ② Sources pilotées ─────────────────────────────
|
||||
_SectionLabel('SOURCES PILOTÉES'),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// PAC Atlantic — SG-Ready
|
||||
@ -367,10 +367,10 @@ class _ActiveZoneCard extends StatelessWidget {
|
||||
}).toList(),
|
||||
),
|
||||
|
||||
// Chip Héos si actif
|
||||
if (zone.heosActive) ...[
|
||||
// Puce « piloté » si actif
|
||||
if (zone.pilotageActif) ...[
|
||||
const SizedBox(height: 12),
|
||||
_HeosChip('Chauffe au solaire en ce moment'),
|
||||
_PilotageChip('Chauffe au solaire en ce moment'),
|
||||
],
|
||||
],
|
||||
),
|
||||
@ -463,7 +463,7 @@ class _PACCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
_HeosBadgeSmall(),
|
||||
_PilotageBadge(),
|
||||
],
|
||||
),
|
||||
|
||||
@ -624,7 +624,7 @@ class _DHWCard extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
_HeosBadgeSmall(),
|
||||
_PilotageBadge(),
|
||||
],
|
||||
),
|
||||
|
||||
@ -827,7 +827,7 @@ class _ClimCard extends StatelessWidget {
|
||||
|
||||
const SizedBox(height: 14),
|
||||
|
||||
// Info Héos
|
||||
// Info pilotage
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
@ -914,9 +914,9 @@ class _TempButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _HeosChip extends StatelessWidget {
|
||||
class _PilotageChip extends StatelessWidget {
|
||||
final String label;
|
||||
const _HeosChip(this.label);
|
||||
const _PilotageChip(this.label);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -940,8 +940,8 @@ class _HeosChip extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _HeosBadgeSmall extends StatelessWidget {
|
||||
const _HeosBadgeSmall();
|
||||
class _PilotageBadge extends StatelessWidget {
|
||||
const _PilotageBadge();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -956,7 +956,7 @@ class _HeosBadgeSmall extends StatelessWidget {
|
||||
children: [
|
||||
const Icon(Icons.auto_awesome_rounded, size: 10, color: EtmTokens.greenDark),
|
||||
const SizedBox(width: 4),
|
||||
Text('Piloté par Héos',
|
||||
Text('Piloté',
|
||||
style: EtmTokens.sans(size: 10, weight: FontWeight.w600,
|
||||
color: EtmTokens.greenDark)),
|
||||
],
|
||||
|
||||
@ -12,7 +12,7 @@ import '../services/nymea_service.dart';
|
||||
import '../theme/etm_tokens.dart';
|
||||
import '../features/dashboard/widgets/energy_flow_card.dart';
|
||||
import '../features/dashboard/widgets/kpi_row.dart';
|
||||
import '../features/dashboard/widgets/heos_decision_card.dart';
|
||||
import '../features/dashboard/widgets/en_ce_moment_card.dart';
|
||||
import '../features/dashboard/widgets/journee_card.dart';
|
||||
|
||||
class DashboardScreen extends StatefulWidget {
|
||||
@ -181,7 +181,9 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
const SizedBox(height: 14),
|
||||
KpiRow(data: data, balance: _bilan, ratios: _ratios),
|
||||
const SizedBox(height: 14),
|
||||
const HeosDecisionCard(),
|
||||
// Ce que le moteur décide, charge par charge — lu dans la télémétrie
|
||||
// d'arbitrage (consigne G, 2026-10-16). Plus de démo, plus de marque.
|
||||
const EnCeMomentCard(),
|
||||
const SizedBox(height: 14),
|
||||
// La journée heure par heure — APRÈS les décisions (Patrick,
|
||||
// 2026-10-14). Variante E de la maquette, retenue le 2026-10-15 :
|
||||
|
||||
@ -25,6 +25,31 @@ String decisionText(L10n t, LoadDecision d) {
|
||||
final p = d.params;
|
||||
|
||||
switch (d.code) {
|
||||
// ── Motifs de niveau éco remontés en tête de `decision` (R7, etm70) ─────────
|
||||
//
|
||||
// Depuis etm70, `decision.level` est le niveau dont le motif est publié : la part
|
||||
// majoritaire de la consigne. Un motif éco peut donc arriver ici, à la racine — et
|
||||
// `ECO_FLOOR_MISSED` y arrive en tête le cycle de la bascule. Une seule phrase pour le
|
||||
// même motif, où qu'il soit lu.
|
||||
case 'ECO_FLOOR_GRID' || 'ECO_FLOOR_MET' || 'ECO_FLOOR_MISSED':
|
||||
return ecoDecisionText(t, d);
|
||||
|
||||
// ── Optimiseur (brief 4 T5, etm70) ──────────────────────────────────────────
|
||||
//
|
||||
// `priceCtkWh` est le prix COMPLET du créneau, vérifiable sur `price.importCtkWh`.
|
||||
case 'PRICE_SLOT':
|
||||
final prix = d.numParam('priceCtkWh')?.toDouble();
|
||||
if (prix == null) break;
|
||||
final consigne = _w(d, 'setpointW');
|
||||
return consigne == null
|
||||
? t.decisionPriceSlot(prix)
|
||||
: t.decisionPriceSlotSetpoint(prix, consigne);
|
||||
|
||||
// Motif hors catalogue d'un optimiseur : transmis sans texte ni paramètre. Aucun nom
|
||||
// de marque (décision G).
|
||||
case 'OPTIMIZER_DECISION':
|
||||
return t.decisionOptimizer;
|
||||
|
||||
// `EV_SURPLUS`, `EV_ECO_MIN` et `EV_IDLE` ne sont **plus émis** depuis `+etm22` : une
|
||||
// borne reçoit désormais les motifs communs à tous les mécanismes
|
||||
// (`SURPLUS_SETPOINT`, `BELOW_MIN_POWER`, `SURPLUS_INSUFFICIENT`,
|
||||
@ -317,10 +342,10 @@ String ecoDecisionText(L10n t, LoadDecision d) {
|
||||
};
|
||||
if (raison == null) break;
|
||||
// `missingWh` est ABSENT sous `unmeasurable`, et c'est voulu : on ne sait pas ce qui a
|
||||
// été livré, donc pas ce qui manquait. Ne rien ajouter alors.
|
||||
// été livré, donc pas ce qui manquait. On le DIT (consigne H) — jamais « 0 Wh ».
|
||||
final manque = d.numParam('missingWh');
|
||||
return t.decisionEcoFloorMissed(raison) +
|
||||
(manque == null ? '' : t.ecoManque(energieCourte(manque)));
|
||||
(manque == null ? t.ecoManqueNonChiffrable : t.ecoManque(energieCourte(manque)));
|
||||
}
|
||||
return t.decisionUnknown(d.code.isEmpty ? '—' : d.code, formatParams(d.params));
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Icône du cœur HEMS (« Héos ») : étincelle de marque au centre,
|
||||
/// Icône du cœur HEMS : étincelle au centre,
|
||||
/// 4 nœuds pilotés alignés sur les pointes.
|
||||
class HemsIcon extends StatelessWidget {
|
||||
final double size;
|
||||
|
||||
@ -437,7 +437,7 @@ class _OptimalBanner extends StatelessWidget {
|
||||
children: [
|
||||
Text('Mode optimal',
|
||||
style: EtmTokens.sans(size: 14, weight: FontWeight.w600, color: EtmTokens.greenDark)),
|
||||
Text('Héos optimise votre consommation',
|
||||
Text('Pilotage local actif',
|
||||
style: EtmTokens.sans(size: 12, color: EtmTokens.mutedOf(context))),
|
||||
],
|
||||
),
|
||||
|
||||
180
test/en_ce_moment_test.dart
Normal file
180
test/en_ce_moment_test.dart
Normal file
@ -0,0 +1,180 @@
|
||||
/// Carte « En ce moment » (consigne G) et motifs du moteur etm70 (consigne H).
|
||||
///
|
||||
/// La trame de base est RÉELLE (banc `hems`, `+etm69`, 2026-10-16). Les trames etm70 en sont
|
||||
/// dérivées d'après `INTERFACE.md` de la branche moteur (R7, catalogue) : etm70 n'est pas
|
||||
/// encore sur le banc, rien ici n'a été vu sur machine.
|
||||
library;
|
||||
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:etm_powersync_app/features/dashboard/widgets/en_ce_moment_card.dart';
|
||||
import 'package:etm_powersync_app/l10n/app_localizations.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/services/telemetry_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
Map<String, dynamic> _banc() => (jsonDecode(
|
||||
File('test/fixtures/loadtelemetry_hems_etm69.json').readAsStringSync())
|
||||
as Map<String, dynamic>)['telemetry'] as Map<String, dynamic>;
|
||||
|
||||
Widget _app(Widget w) => MaterialApp(
|
||||
localizationsDelegates: L10n.localizationsDelegates,
|
||||
supportedLocales: L10n.supportedLocales,
|
||||
locale: const Locale('fr'),
|
||||
home: Scaffold(body: ListView(children: [w])),
|
||||
);
|
||||
|
||||
EnCeMomentView _vue(Map<String, dynamic> trame,
|
||||
{Map<String, ({LoadDecision decision, DateTime at})> manques = const {}}) =>
|
||||
EnCeMomentView(
|
||||
telemetry: LoadTelemetry.fromJson(trame),
|
||||
derniersManques: manques,
|
||||
libelle: (id) => switch (id.replaceAll(RegExp(r'[{}]'), '').toLowerCase()) {
|
||||
'chauffe-eau' => 'Chauffe-eau',
|
||||
'pac-terrain' => 'PAC banc',
|
||||
'4d1adafd-cafc-4813-93b1-ca8933ce71ab' => 'V2C Trydan',
|
||||
_ => id,
|
||||
},
|
||||
domaine: (id) => switch (id) {
|
||||
'chauffe-eau' => 'ecs',
|
||||
'pac-terrain' => 'heating',
|
||||
_ => 'ev',
|
||||
},
|
||||
);
|
||||
|
||||
LoadDecision _d(String code, Map<String, dynamic> params) =>
|
||||
LoadDecision.fromJson({'code': code, 'params': params});
|
||||
|
||||
void main() {
|
||||
final fr = lookupL10n(const Locale('fr'));
|
||||
|
||||
testWidgets('trame réelle : un motif par charge, rien d\'inventé, aucune marque',
|
||||
(t) async {
|
||||
await t.pumpWidget(_app(_vue(_banc())));
|
||||
expect(find.text('En ce moment'), findsOneWidget);
|
||||
expect(find.text('Chauffe-eau'), findsOneWidget);
|
||||
expect(find.text('PAC banc'), findsOneWidget);
|
||||
for (final e in LoadTelemetry.fromJson(_banc()).loads) {
|
||||
expect(find.text(decisionText(fr, e.decision)), findsOneWidget);
|
||||
}
|
||||
// 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\u00e9os'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('la ventilation des niveaux se lit (83 W éco + 0 W confort)', (t) async {
|
||||
await t.pumpWidget(_app(_vue(_banc())));
|
||||
expect(find.text('83 W éco + 0 W confort'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('puissance MESURÉE seulement — jamais l\'allocation à sa place', (t) async {
|
||||
await t.pumpWidget(_app(_vue(_banc())));
|
||||
// La PAC mesure 0 W (compteur) ; le chauffe-eau est `source: none`, sans chiffre.
|
||||
expect(find.text('0,0 kW'), findsOneWidget);
|
||||
});
|
||||
|
||||
test('etm70 : un motif éco publié à la racine (part majoritaire) se rend en clair', () {
|
||||
// R7 : chauffe-eau, éco 83 W > confort 0 W → `decision` = motif éco, `level: "eco"`.
|
||||
final d = _d('ECO_FLOOR_GRID', {'gridW': 83});
|
||||
expect(decisionText(fr, d), 'Plancher servi — 83 W achetés au réseau');
|
||||
});
|
||||
|
||||
group('ECO_FLOOR_MISSED — publié un seul cycle, RETENU par l\'app', () {
|
||||
Map<String, dynamic> trameManque({Map<String, dynamic>? params, bool avecBorne = true}) {
|
||||
final b = _banc();
|
||||
final loads = [
|
||||
for (final l in (b['loads'] as List).cast<Map<String, dynamic>>())
|
||||
if (l['loadId'] == 'chauffe-eau')
|
||||
{
|
||||
...l,
|
||||
'decision': {
|
||||
'code': 'ECO_FLOOR_MISSED',
|
||||
'level': 'eco',
|
||||
'params': params ?? {'targetWh': 2000, 'cause': 'unmeasurable'},
|
||||
},
|
||||
}
|
||||
else
|
||||
l,
|
||||
];
|
||||
return {...b, 'loads': loads};
|
||||
}
|
||||
|
||||
test('le provider le garde après le cycle suivant', () {
|
||||
final p = LoadTelemetryProvider()
|
||||
..adoptForTest(trameManque())
|
||||
..adoptForTest(_banc()); // cycle suivant : motif ordinaire
|
||||
expect(p.derniersManques['chauffe-eau']!.decision.code, 'ECO_FLOOR_MISSED');
|
||||
p.dispose();
|
||||
});
|
||||
|
||||
testWidgets('missingWh absent : « manque non chiffrable », jamais 0 Wh', (t) async {
|
||||
final p = LoadTelemetryProvider()
|
||||
..adoptForTest(trameManque())
|
||||
..adoptForTest(_banc());
|
||||
await t.pumpWidget(_app(_vue(_banc(), manques: p.derniersManques)));
|
||||
expect(find.textContaining('manque non chiffrable'), findsOneWidget);
|
||||
expect(find.textContaining('0 Wh'), findsNothing);
|
||||
expect(find.textContaining('Objectif manqué à'), findsOneWidget);
|
||||
p.dispose();
|
||||
});
|
||||
|
||||
test('missingWh présent : chiffré', () {
|
||||
final d = _d('ECO_FLOOR_MISSED',
|
||||
{'targetWh': 4000, 'cause': 'drawCap', 'missingWh': 2500});
|
||||
expect(decisionText(fr, d), contains('il manquait 2,5 kWh'));
|
||||
});
|
||||
|
||||
testWidgets('une charge partie de loads[] garde son manque visible', (t) async {
|
||||
final borne = {
|
||||
'4d1adafd-cafc-4813-93b1-ca8933ce71ab': (
|
||||
decision: _d('ECO_FLOOR_MISSED', {'targetWh': 5000, 'cause': 'unavailable'}),
|
||||
at: DateTime(2026, 10, 16, 18, 0),
|
||||
)
|
||||
};
|
||||
await t.pumpWidget(_app(_vue(_banc(), manques: borne)));
|
||||
expect(find.textContaining('V2C Trydan — Objectif manqué à 18:00'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
group('nouveaux codes, FR / DE / EN, sans marque', () {
|
||||
final prix = _d('PRICE_SLOT', {'priceCtkWh': 16.96, 'setpointW': 2400});
|
||||
|
||||
test('PRICE_SLOT', () {
|
||||
// Le français groupe les milliers avec une espace fine insécable : on la normalise.
|
||||
String n(String x) => x.replaceAll(RegExp('[\u202f\u00a0]'), ' ');
|
||||
expect(n(decisionText(fr, prix)), 'Prix favorable (16,96 ct/kWh) — consigne 2 400 W.');
|
||||
expect(decisionText(lookupL10n(const Locale('en')), prix),
|
||||
'Favourable price (16.96 ct/kWh) — setpoint 2,400 W.');
|
||||
expect(n(decisionText(lookupL10n(const Locale('de')), prix)),
|
||||
'Günstiger Preis (16,96 ct/kWh) — Sollwert 2.400 W.');
|
||||
expect(decisionText(fr, _d('PRICE_SLOT', {'priceCtkWh': 16.96})),
|
||||
'Prix favorable (16,96 ct/kWh).');
|
||||
});
|
||||
|
||||
test('OPTIMIZER_DECISION', () {
|
||||
final o = _d('OPTIMIZER_DECISION', const {});
|
||||
expect(decisionText(fr, o), 'Décision de l\'optimiseur.');
|
||||
expect(decisionText(lookupL10n(const Locale('de')), o), 'Entscheidung des Optimierers.');
|
||||
expect(decisionText(lookupL10n(const Locale('en')), o), 'Optimizer decision.');
|
||||
});
|
||||
|
||||
test('code inconnu : libellé générique avec le code, rien d\'inventé', () {
|
||||
expect(decisionText(fr, _d('NOUVEAU_MOTIF', {'x': 1})), contains('NOUVEAU_MOTIF'));
|
||||
});
|
||||
|
||||
test('PRICE_SLOT sans prix : repli, pas une phrase trouée', () {
|
||||
expect(decisionText(fr, _d('PRICE_SLOT', const {})), contains('PRICE_SLOT'));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('box sans télémétrie : dit « ne publie pas », pas « aucune charge »',
|
||||
(t) async {
|
||||
await t.pumpWidget(_app(EnCeMomentView(
|
||||
telemetry: null, supportee: false, libelle: (i) => i, domaine: (_) => null)));
|
||||
expect(find.text('Cette box ne publie pas l\'arbitrage des charges.'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
141
test/fixtures/loadtelemetry_hems_etm69.json
vendored
Normal file
141
test/fixtures/loadtelemetry_hems_etm69.json
vendored
Normal file
@ -0,0 +1,141 @@
|
||||
{
|
||||
"_source": "dump réel banc hems 10.42.60.21, NymeaEnergy.GetLoadTelemetry, 2026-10-16, moteur 1.15.2+etm69",
|
||||
"telemetry": {
|
||||
"budget": {
|
||||
"allocatedW": 1200,
|
||||
"evReservedW": 0,
|
||||
"recreditedW": 1200,
|
||||
"remainingW": -949.3777465820312,
|
||||
"surplusW": -949.3777465820312
|
||||
},
|
||||
"degradedMode": false,
|
||||
"draw": {
|
||||
"authorisedW": 6261.473164558411,
|
||||
"binding": "connection",
|
||||
"bindingPhase": "C",
|
||||
"committedW": 83.33333333333333,
|
||||
"perPhaseBound": true,
|
||||
"remainingW": 6178.139831225078
|
||||
},
|
||||
"loads": [
|
||||
{
|
||||
"allocatedW": 83.33333333333333,
|
||||
"available": true,
|
||||
"decision": {
|
||||
"code": "SURPLUS_INSUFFICIENT",
|
||||
"level": "comfort",
|
||||
"params": {
|
||||
"budgetW": -949
|
||||
}
|
||||
},
|
||||
"levels": [
|
||||
{
|
||||
"counts": {
|
||||
"draw.committedW": 83.33333333333333
|
||||
},
|
||||
"decision": {
|
||||
"code": "ECO_FLOOR_GRID",
|
||||
"params": {
|
||||
"atMaximum": false,
|
||||
"gridW": 83,
|
||||
"regime": "unmeasurable",
|
||||
"remainingS": 53039,
|
||||
"remainingWh": 2000,
|
||||
"targetWh": 2000
|
||||
}
|
||||
},
|
||||
"funding": "grid",
|
||||
"level": "eco",
|
||||
"progress": {
|
||||
"purchasedWh": 764.9768518518601,
|
||||
"regime": "unmeasurable",
|
||||
"targetWh": 2000
|
||||
},
|
||||
"targetW": 83.33333333333333
|
||||
},
|
||||
{
|
||||
"counts": {
|
||||
"budget.allocatedW": 0
|
||||
},
|
||||
"decision": {
|
||||
"code": "SURPLUS_INSUFFICIENT",
|
||||
"params": {
|
||||
"budgetW": -949
|
||||
}
|
||||
},
|
||||
"funding": "surplus",
|
||||
"level": "comfort",
|
||||
"targetW": 0
|
||||
}
|
||||
],
|
||||
"loadId": "chauffe-eau",
|
||||
"measurement": {
|
||||
"source": "none"
|
||||
},
|
||||
"mechanism": {
|
||||
"kind": "relay",
|
||||
"maxStageW": 3500,
|
||||
"stageW": 0,
|
||||
"stagesW": [
|
||||
0,
|
||||
500,
|
||||
1000,
|
||||
1500,
|
||||
2000,
|
||||
2500,
|
||||
3000,
|
||||
3500
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"allocatedW": 1200,
|
||||
"available": true,
|
||||
"command": {
|
||||
"divergent": true,
|
||||
"expectedW": 1200,
|
||||
"observedW": 0,
|
||||
"since": "2026-10-15T06:12:00Z"
|
||||
},
|
||||
"decision": {
|
||||
"code": "BELOW_MIN_POWER",
|
||||
"level": "comfort",
|
||||
"params": {
|
||||
"budgetW": 251,
|
||||
"minPowerW": 1500,
|
||||
"state": 3
|
||||
}
|
||||
},
|
||||
"levels": [
|
||||
{
|
||||
"counts": {
|
||||
"budget.allocatedW": 1200
|
||||
},
|
||||
"decision": {
|
||||
"code": "BELOW_MIN_POWER",
|
||||
"params": {
|
||||
"budgetW": 251,
|
||||
"minPowerW": 1500,
|
||||
"state": 3
|
||||
}
|
||||
},
|
||||
"funding": "surplus",
|
||||
"level": "comfort",
|
||||
"targetW": 1200
|
||||
}
|
||||
],
|
||||
"loadId": "pac-terrain",
|
||||
"measurement": {
|
||||
"powerW": 0,
|
||||
"source": "meter"
|
||||
},
|
||||
"mechanism": {
|
||||
"estimatedPowerW": 1200,
|
||||
"kind": "sgReady",
|
||||
"state": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-10-16T19:16:00Z"
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user