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>
158 lines
4.3 KiB
Dart
158 lines
4.3 KiB
Dart
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 18h–21h',
|
||
powerKw: 0.8,
|
||
),
|
||
],
|
||
updatedAt: DateTime.now(),
|
||
);
|
||
}
|