etm-powersync-app/lib/models/installation.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

73 lines
2.2 KiB
Dart

/// Une installation HEMS enregistrée.
///
/// **Identité stable = [uuid]** (Server UUID renvoyé par `JSONRPC.Hello`), jamais
/// l'IP : en DHCP, [host] change. Le [token] est un **credential** persisté à
/// part (Keychain/Keystore via `flutter_secure_storage`) — il ne figure donc
/// **pas** dans [toJson] (métadonnées SharedPreferences uniquement).
class Installation {
/// Server UUID (clé d'identité, Hello `uuid`).
final String uuid;
/// Nom de la box (Hello `name`).
final String name;
/// Hôte courant (IP/hostname) — peut changer en DHCP, jamais une identité.
final String host;
/// Port JSON-RPC. Défaut **4444** (ws clair, LAN).
final int port;
/// Dernière fois où la box a répondu (connexion ou découverte).
final DateTime? lastSeen;
/// Token d'authentification — **transitoire en mémoire**, persisté séparément
/// par UUID dans le secure storage. `null` tant que la box n'est pas appairée.
final String? token;
const Installation({
required this.uuid,
required this.name,
required this.host,
this.port = 4444,
this.lastSeen,
this.token,
});
bool get hasToken => token != null && token!.isNotEmpty;
Installation copyWith({
String? name,
String? host,
int? port,
DateTime? lastSeen,
String? token,
}) =>
Installation(
uuid: uuid,
name: name ?? this.name,
host: host ?? this.host,
port: port ?? this.port,
lastSeen: lastSeen ?? this.lastSeen,
token: token ?? this.token,
);
/// Métadonnées persistées (SharedPreferences). **Sans token** (secure storage).
Map<String, dynamic> toJson() => {
'uuid': uuid,
'name': name,
'host': host,
'port': port,
if (lastSeen != null) 'lastSeen': lastSeen!.toIso8601String(),
};
factory Installation.fromJson(Map<String, dynamic> j) => Installation(
uuid: j['uuid'] as String,
name: (j['name'] as String?) ?? 'nymea',
host: j['host'] as String,
port: (j['port'] as num?)?.toInt() ?? 4444,
lastSeen: j['lastSeen'] != null
? DateTime.tryParse(j['lastSeen'] as String)
: null,
);
}