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

158 lines
4.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:math' as math;
enum WeatherKind { sun, partlyCloudy, cloudy, moon }
class HourlyFlow {
final int hour;
final double solToHouse;
final double solToEcs;
final double solToVe;
final double solToBatt;
final double solToGrid;
final double battToHouse;
final double gridToHouse;
const HourlyFlow({
required this.hour,
this.solToHouse = 0,
this.solToEcs = 0,
this.solToVe = 0,
this.solToBatt = 0,
this.solToGrid = 0,
this.battToHouse = 0,
this.gridToHouse = 0,
});
double get totalPositive =>
solToHouse + solToEcs + solToVe + solToBatt + solToGrid;
double get totalNegative => battToHouse + gridToHouse;
}
class WeatherPoint {
final int hour;
final WeatherKind kind;
final int tempC;
const WeatherPoint({
required this.hour,
required this.kind,
required this.tempC,
});
}
class DayPlan {
final List<HourlyFlow> hours;
final List<WeatherPoint> weather;
final double nowHour;
const DayPlan({
required this.hours,
required this.weather,
required this.nowHour,
});
static DayPlan demo() {
final now = DateTime.now();
final nowHour = now.hour + now.minute / 60.0;
final hours = List.generate(24, (h) {
final solar = h >= 6 && h <= 20
? 3.8 * math.exp(-math.pow(h - 13, 2) / 18.0)
: 0.0;
final baseHouse = 1.2;
final peakHouse = (h >= 7 && h <= 9) || (h >= 18 && h <= 21) ? 0.7 : 0.0;
final house = baseHouse + peakHouse;
final excess = math.max(0.0, solar - house);
final deficit = math.max(0.0, house - solar);
final solToHouse = math.min(solar, house);
final solToEcs = excess > 0.5 ? math.min(excess * 0.35, 0.7) : 0.0;
final solToVe = excess > 1.2 ? math.min(excess * 0.3, 1.1) : 0.0;
final solToBatt = excess > 0.3 ? math.min(excess * 0.15, 0.4) : 0.0;
final solToGrid = math.max(0.0, excess - solToEcs - solToVe - solToBatt);
final battToHouse = h >= 17 ? math.min(deficit * 0.55, 0.8) : 0.0;
final gridToHouse = math.max(0.0, deficit - battToHouse);
return HourlyFlow(
hour: h,
solToHouse: solToHouse,
solToEcs: solToEcs,
solToVe: solToVe,
solToBatt: solToBatt,
solToGrid: solToGrid,
battToHouse: battToHouse,
gridToHouse: gridToHouse,
);
});
final weather = [
const WeatherPoint(hour: 0, kind: WeatherKind.moon, tempC: 17),
const WeatherPoint(hour: 4, kind: WeatherKind.moon, tempC: 15),
const WeatherPoint(hour: 8, kind: WeatherKind.partlyCloudy, tempC: 19),
const WeatherPoint(hour: 12, kind: WeatherKind.sun, tempC: 25),
const WeatherPoint(hour: 16, kind: WeatherKind.sun, tempC: 27),
const WeatherPoint(hour: 20, kind: WeatherKind.partlyCloudy, tempC: 22),
];
return DayPlan(hours: hours, weather: weather, nowHour: nowHour);
}
}
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 18h21h',
powerKw: 0.8,
),
],
updatedAt: DateTime.now(),
);
}