etm-powersync-app/lib/providers/tariff_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

149 lines
4.3 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show TimeOfDay;
enum TariffProviderType {
manual, // Tarif manuel saisi par l'utilisateur
customJson,
linkyTIC,
tibberPro,
octopusPro,
entsoe,
jsonRpc,
}
extension TariffProviderTypeExt on TariffProviderType {
String get label => switch (this) {
TariffProviderType.manual => 'Tarif manuel',
TariffProviderType.customJson => 'Fichier JSON custom',
TariffProviderType.linkyTIC => 'Linky TIC',
TariffProviderType.tibberPro => 'Tibber',
TariffProviderType.octopusPro => 'Octopus',
TariffProviderType.entsoe => 'ENTSO-E',
TariffProviderType.jsonRpc => 'JSON-RPC externe',
};
bool get isPro => this == TariffProviderType.tibberPro ||
this == TariffProviderType.octopusPro ||
this == TariffProviderType.entsoe;
}
enum TariffMode {
spotMarket,
hchp, // Heures Creuses / Heures Pleines
tou, // TOU contractuel
fixed,
}
extension TariffModeExt on TariffMode {
String get label => switch (this) {
TariffMode.spotMarket => 'Spot Market',
TariffMode.hchp => 'Heures Creuses / Heures Pleines',
TariffMode.tou => 'TOU contractuel',
TariffMode.fixed => 'Tarif fixe',
};
}
class HcHpConfig {
final double offPeakPrice; // €/kWh HC
final double peakPrice; // €/kWh HP
final TimeOfDay offPeakStart;
final TimeOfDay offPeakEnd;
const HcHpConfig({
this.offPeakPrice = 0.096,
this.peakPrice = 0.146,
this.offPeakStart = const TimeOfDay(hour: 22, minute: 0),
this.offPeakEnd = const TimeOfDay(hour: 6, minute: 0),
});
HcHpConfig copyWith({
double? offPeakPrice,
double? peakPrice,
TimeOfDay? offPeakStart,
TimeOfDay? offPeakEnd,
}) => HcHpConfig(
offPeakPrice: offPeakPrice ?? this.offPeakPrice,
peakPrice: peakPrice ?? this.peakPrice,
offPeakStart: offPeakStart ?? this.offPeakStart,
offPeakEnd: offPeakEnd ?? this.offPeakEnd,
);
}
class CurrentPrices {
final double now;
final double inOneHour;
final double min24h;
final double max24h;
final DateTime? minTime;
final DateTime? maxTime;
const CurrentPrices({
this.now = 0,
this.inOneHour = 0,
this.min24h = 0,
this.max24h = 0,
this.minTime,
this.maxTime,
});
}
/// Gère le provider tarifaire actif, le mode et les prix courants.
class TariffProvider extends ChangeNotifier {
TariffProviderType _activeProvider = TariffProviderType.manual;
TariffMode _mode = TariffMode.fixed;
double _manualPrice = 0.25; // €/kWh tarif manuel
HcHpConfig _hchp = const HcHpConfig();
CurrentPrices _prices = const CurrentPrices(
now: 0.092, inOneHour: 0.085,
min24h: 0.048, max24h: 0.182,
);
bool _loading = false;
/// Réinitialise au switch d'installation (reset mémoire aux défauts ; la
/// config tarifaire de la nouvelle box sera rechargée par `load()`).
void clearForSwitch() {
_activeProvider = TariffProviderType.manual;
_mode = TariffMode.fixed;
_manualPrice = 0.25;
_hchp = const HcHpConfig();
_prices = const CurrentPrices(
now: 0.092, inOneHour: 0.085,
min24h: 0.048, max24h: 0.182,
);
_loading = false;
notifyListeners();
}
TariffProviderType get activeProvider => _activeProvider;
TariffMode get mode => _mode;
HcHpConfig get hchp => _hchp;
CurrentPrices get prices => _prices;
bool get loading => _loading;
double get manualPrice => _manualPrice;
Future<void> load() async {
_loading = true;
notifyListeners();
await Future.delayed(const Duration(milliseconds: 200));
_loading = false;
notifyListeners();
// TODO: charger depuis nymea RPC
}
void setProvider(TariffProviderType p) {
_activeProvider = p; notifyListeners();
}
void setMode(TariffMode m) {
_mode = m; notifyListeners();
}
void updateHcHp(HcHpConfig c) {
_hchp = c; notifyListeners();
}
void setManualPrice(double price) {
_manualPrice = price.clamp(0.001, 9.999);
notifyListeners();
}
}