Patrick Schurig c638ec6c52 feat: connexions multi-HEMS + rôles & appareils (beta add/config)
Lot Connexions multi-HEMS :
- modèle Installation + InstallationStore (persistance par UUID ;
  métadonnées SharedPreferences, token via flutter_secure_storage)
- ConnectionManager au-dessus de nymea_service : mono-connexion, switchTo,
  connectNew, enterDemo (démo = active factice), contrôle d'identité DHCP
- nymea_service : connect() réveillé (ws://4444, token par UUID), capture
  Hello (uuid/name/initialSetupRequired), authenticate/createUser, resetState
- écran Installations (3 états) + déverrouillage installateur depuis la racine
- écran Connexion : auth 2 branches (JSONRPC.Authenticate / CreateUser),
  détection auto via initialSetupRequired, segmenteur de repli
- gate de routage (redirect + refreshListenable merge[cm,svc]) ;
  hasActiveConnection = isSimulation || (connected && authenticated)
- en-tête drawer cliquable → Installations
- invalidation des caches au switch (service.resetState + clearForSwitch
  rôles/scheduler/tariff), sans persist()

Lot Rôles & appareils :
- EnergySetupProvider refondu en tri-état (present/absent/non-configuré)
- écran roles_devices_screen (3 zones, drag-priorité, inférence solaire/batterie)
- LoadDescriptor (etmvariableload, rév.2 : PAC exclue → SG-Ready) ;
  Energy.SetRootMeter réel, Get/SetLoadConfig en stub loggé

Correctifs :
- bug signe énergie : production +, consommation NÉGATIVE sur nymea 1.15.2
  → consumptionW/home en .abs() (autoconso n'est plus clouée à 0)
- UUID Hello brace-wrapped normalisé

Tests : flutter analyze 0 erreur ; gate + zéro-fuite au switch (5/5 verts).
Validé en vrai : login auth .120, .75 ouvert, switch, déverrouillage installateur.
Note : embarque aussi le WIP dashboard/thème déjà présent dans l'arbre.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-28 08:39:08 +02:00

85 lines
2.1 KiB
Dart

import 'package:flutter/material.dart';
class FlowSegment {
final Offset from;
final Offset to;
final Color color;
final double powerW;
final bool dashed;
const FlowSegment({
required this.from,
required this.to,
required this.color,
required this.powerW,
this.dashed = false,
});
}
class FlowPainter extends CustomPainter {
final List<FlowSegment> segments;
final double animValue;
final Color lineColor;
const FlowPainter({
required this.segments,
required this.animValue,
required this.lineColor,
});
@override
void paint(Canvas canvas, Size size) {
final linePaint = Paint()
..color = lineColor
..strokeWidth = 1.5
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
for (final seg in segments) {
final src = Offset(seg.from.dx * size.width, seg.from.dy * size.height);
final dst = Offset(seg.to.dx * size.width, seg.to.dy * size.height);
if (seg.dashed) {
_drawDashed(canvas, src, dst, linePaint);
} else {
canvas.drawLine(src, dst, linePaint);
}
if (seg.powerW > 30) {
final n = (seg.powerW / 600).clamp(1, 5).round();
final r = seg.powerW > 1500 ? 3.5 : 2.5;
final particlePaint = Paint()
..color = seg.color
..style = PaintingStyle.fill;
for (int i = 0; i < n; i++) {
final t = (animValue + i / n) % 1.0;
final pos = Offset.lerp(src, dst, t)!;
canvas.drawCircle(pos, r, particlePaint);
}
}
}
}
void _drawDashed(Canvas canvas, Offset src, Offset dst, Paint paint) {
final dir = dst - src;
final length = dir.distance;
if (length == 0) return;
final unit = dir / length;
const dashLen = 5.0;
const gapLen = 4.0;
double pos = 0;
while (pos < length) {
final end = (pos + dashLen).clamp(0.0, length);
canvas.drawLine(src + unit * pos, src + unit * end, paint);
pos += dashLen + gapLen;
}
}
@override
bool shouldRepaint(FlowPainter old) =>
old.animValue != animValue ||
old.lineColor != lineColor ||
old.segments.length != segments.length;
}