etm-powersync-app/Héos_UI/hems_icon.dart
Patrick Schurig 7ca42973f2 feat(installateur): ordre de service des charges + locale + ossature i18n
Lot B côté app, partie NON bloquée par le lot plugin (voir §Limites).

§1 Locale (indépendant) — JSONRPC.Hello portait {} : l'app était en en_US de
fait alors que nymea gère la locale PAR CONNEXION (o:locale). Envoi de la
locale à chaque Hello, donc aussi après reconnexion subie (m_clientLocales
repart au défaut de la box à chaque socket). Locale demandée ET retenue
journalisées : un .qm introuvable rend exactement comme une absence de
traduction, sans cette trace un catalogue mal packagé passe inaperçu.

§2 i18n — flutter_localizations + intl + ARB (français seul). Aucune chaîne
en dur dans le nouvel écran. Les codes reçus de la box (domaine, mécanisme)
sont des clés ARB avec repli explicite sur code brut si l'app ne les connaît
pas : une box en avance sur l'app est un cas normal en parc déployé.

§3 Modèle — LoadConfigEntry est une VUE en lecture au-dessus de la map brute ;
l'écriture repart de la map d'origine (patched()). Pas de couche de filtrage
préventive : l'aller-retour neutre passe contre .75, donc l'écho verbatim est
sûr par construction (§7-1).

§4 Priorité à deux niveaux — groupByDomain / flattenToPayload. Le rang de
domaine se déduit de l'ordre d'apparition à plat ; la box ne sait rien des
groupes. Config non contiguë : signalée, ordre réel affiché, normalisation
seulement sur confirmation explicite. Miroir client de validateSet().

§5 Écritures asynchrones — LoadConfigProvider ne conclut jamais sur
l'acquittement du RPC : pending → LoadConfigChanged → confirmed|diverged.

NymeaEnergy.GetLoadConfig / SetLoadConfig deviennent RÉELS (ils étaient des
stubs journalisés). Conséquence traitée : EnergySetupProvider.persist()
n'émet plus setLoadConfig(buildLoadDescriptors()) — SetLoadConfig remplace
TOUT l'ensemble, et _commit() l'appelait à chaque changement d'UI : une
reconstruction depuis le modèle typé aurait écrasé la config réelle de la
box sans geste explicite.

Écran /energy/loads, gaté installateur (drawer + garde de routeur).

Limites — le lot plugin n'est PAS déployé sur .75 : GetLoadTelemetry,
LoadTelemetryChanged et o:domain sont absents (19 méthodes / 13 notifications
inchangées). Toute la moitié télémétrie du brief est donc hors d'atteinte, et
o:domain n'étant pas au schéma, la box rejetterait la clé (jsonvalidator.cpp:143).
Fixtures multi-domaines synthétiques : .75 n'a qu'une charge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-25 11:34:53 +02:00

72 lines
2.0 KiB
Dart

import 'package:flutter/material.dart';
/// Icône du cœur HEMS (« Héos ») : étincelle de marque au centre,
/// 4 nœuds pilotés alignés sur les pointes. Dessinée en natif (pas
/// de dépendance SVG), hérite de la couleur d'`IconTheme` par défaut.
///
/// Exemple :
/// const HemsIcon(size: 24) // couleur du thème
/// HemsIcon(size: 28, color: EtmColors.eco) // couleur explicite
class HemsIcon extends StatelessWidget {
final double size;
final Color? color;
const HemsIcon({super.key, this.size = 24, this.color});
@override
Widget build(BuildContext context) {
final c = color ??
IconTheme.of(context).color ??
const Color(0xFF28A06A); // eco par défaut
return SizedBox(
width: size,
height: size,
child: CustomPaint(painter: _HemsPainter(c)),
);
}
}
class _HemsPainter extends CustomPainter {
final Color color;
_HemsPainter(this.color);
// nœuds cardinaux (repère viewBox 24x24)
static const _nodes = <Offset>[
Offset(12, 3.4), // haut
Offset(20.6, 12), // droite
Offset(12, 20.6), // bas
Offset(3.4, 12), // gauche
];
static const _nodeR = 1.7;
@override
void paint(Canvas canvas, Size size) {
final s = size.width / 24.0; // échelle depuis le viewBox
final paint = Paint()
..color = color
..style = PaintingStyle.fill
..isAntiAlias = true;
// étincelle Héos
final spark = Path()
..moveTo(12 * s, 7.4 * s)
..lineTo(13.25 * s, 10.75 * s)
..lineTo(16.6 * s, 12 * s)
..lineTo(13.25 * s, 13.25 * s)
..lineTo(12 * s, 16.6 * s)
..lineTo(10.75 * s, 13.25 * s)
..lineTo(7.4 * s, 12 * s)
..lineTo(10.75 * s, 10.75 * s)
..close();
canvas.drawPath(spark, paint);
// nœuds pilotés
for (final n in _nodes) {
canvas.drawCircle(Offset(n.dx * s, n.dy * s), _nodeR * s, paint);
}
}
@override
bool shouldRepaint(_HemsPainter old) => old.color != color;
}