etm-powersync-app/lib/providers/scheduler_provider.dart
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

122 lines
4.0 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import '../services/nymea_service.dart';
enum SchedulerStrategy { rulesBased, manual, aiPro }
extension SchedulerStrategyExt on SchedulerStrategy {
String get label => switch (this) {
SchedulerStrategy.rulesBased => 'Basée sur les règles',
SchedulerStrategy.manual => 'Manuelle',
SchedulerStrategy.aiPro => 'AI (Pro)',
};
bool get isPro => this == SchedulerStrategy.aiPro;
}
class SchedulerConfig {
final double priceThreshold; // €/kWh
final double minSurplus; // W
final int planningHorizon; // heures
final int recalcInterval; // minutes
final double selfSufficiencyGoal; // %
const SchedulerConfig({
this.priceThreshold = 0.08,
this.minSurplus = 200,
this.planningHorizon = 24,
this.recalcInterval = 15,
this.selfSufficiencyGoal = 70,
});
SchedulerConfig copyWith({
double? priceThreshold,
double? minSurplus,
int? planningHorizon,
int? recalcInterval,
double? selfSufficiencyGoal,
}) => SchedulerConfig(
priceThreshold: priceThreshold ?? this.priceThreshold,
minSurplus: minSurplus ?? this.minSurplus,
planningHorizon: planningHorizon ?? this.planningHorizon,
recalcInterval: recalcInterval ?? this.recalcInterval,
selfSufficiencyGoal: selfSufficiencyGoal ?? this.selfSufficiencyGoal,
);
}
enum SchedulerStatus { ok, degraded, error }
class SchedulerState {
final SchedulerStatus status;
final String reason;
final DateTime? lastPlanning;
final DateTime? nextPlanning;
const SchedulerState({
this.status = SchedulerStatus.ok,
this.reason = '',
this.lastPlanning,
this.nextPlanning,
});
}
/// Gère la stratégie, la configuration et l'état du scheduler EMS.
class SchedulerProvider extends ChangeNotifier {
SchedulerStrategy _strategy = SchedulerStrategy.rulesBased;
SchedulerConfig _config = const SchedulerConfig();
SchedulerState _state = const SchedulerState();
bool _loading = false;
SchedulerStrategy get strategy => _strategy;
SchedulerConfig get config => _config;
SchedulerState get state => _state;
bool get loading => _loading;
/// Réinitialise au switch d'installation (reset mémoire aux défauts ; la
/// config de la nouvelle box sera rechargée par `load()`).
void clearForSwitch() {
_strategy = SchedulerStrategy.rulesBased;
_config = const SchedulerConfig();
_state = const SchedulerState();
_loading = false;
notifyListeners();
}
// ── Chargement depuis nymea (stub — à brancher sur RPC réel) ────────────────
Future<void> load(NymeaService service) async {
_loading = true;
notifyListeners();
await Future.delayed(const Duration(milliseconds: 300));
_state = SchedulerState(
status: SchedulerStatus.ok,
reason: 'TariffManager: aWATTar AT actif',
lastPlanning: DateTime.now().subtract(const Duration(minutes: 8)),
nextPlanning: DateTime.now().add(const Duration(minutes: 7)),
);
_loading = false;
notifyListeners();
}
// ── Mutations ────────────────────────────────────────────────────────────────
void setStrategy(SchedulerStrategy s) {
_strategy = s;
notifyListeners();
// TODO: _sendRequest('Energy.SetSchedulerStrategy', ...)
}
void updateConfig(SchedulerConfig c) {
_config = c;
notifyListeners();
// TODO: _sendRequest('Energy.SetSchedulerConfig', ...)
}
Future<void> forceRecalc(NymeaService service) async {
_loading = true;
notifyListeners();
await Future.delayed(const Duration(seconds: 1));
_state = _state; // Mettre à jour depuis la réponse RPC
_loading = false;
notifyListeners();
}
}