// ── Thing models ────────────────────────────────────────────────────────────── class NymeaThing { final String id; final String name; final String thingClassId; final String setupStatus; final List> paramValues; final List states; const NymeaThing({ required this.id, required this.name, required this.thingClassId, required this.setupStatus, required this.paramValues, this.states = const [], }); factory NymeaThing.fromJson(Map j) { return NymeaThing( id: j['id'] as String? ?? '', name: j['name'] as String? ?? 'Unknown', thingClassId: j['thingClassId'] as String? ?? '', setupStatus: j['setupStatus'] as String? ?? '', paramValues: List>.from(j['paramValues'] ?? []), states: (j['states'] as List? ?? []) .map((s) => NymeaStateValue.fromJson(s)) .toList(), ); } NymeaThing copyWith({String? name, List? states}) => NymeaThing( id: id, name: name ?? this.name, thingClassId: thingClassId, setupStatus: setupStatus, paramValues: paramValues, states: states ?? this.states, ); dynamic settingValue(String settingTypeId) { try { return paramValues .firstWhere((p) => p['paramTypeId'] == settingTypeId)['value']; } catch (_) { return null; } } dynamic stateValue(String stateTypeId) { try { return states.firstWhere((s) => s.stateTypeId == stateTypeId).value; } catch (_) { return null; } } bool get isSetupComplete => setupStatus == 'ThingSetupStatusComplete'; } class NymeaStateValue { final String stateTypeId; final dynamic value; const NymeaStateValue({required this.stateTypeId, required this.value}); factory NymeaStateValue.fromJson(Map j) => NymeaStateValue( stateTypeId: j['stateTypeId'] as String? ?? '', value: j['value'], ); } // ── Résultat d'une action nymea ─────────────────────────────────────────────── class NymeaActionResult { final bool success; final String thingError; // "ThingErrorNoError" si OK final String? displayMessage; // message humain optionnel du serveur const NymeaActionResult({ required this.success, this.thingError = 'ThingErrorNoError', this.displayMessage, }); /// Message d'erreur lisible. Vide si succès. String get errorText { if (success) return ''; if (displayMessage != null && displayMessage!.isNotEmpty) return displayMessage!; const map = { 'ThingErrorNoError': '', 'ThingErrorNotFound': 'Appareil introuvable', 'ThingErrorInvalidParameter': 'Paramètre invalide', 'ThingErrorSetupFailed': 'Échec de configuration', 'ThingErrorHardwareNotAvailable': 'Matériel indisponible', 'ThingErrorActionTypeNotFound': 'Action introuvable', 'ThingErrorStateTypeNotFound': 'État introuvable', 'ThingErrorHardwareFailure': 'Panne matérielle', 'ThingErrorTimeout': 'Délai dépassé', }; return map[thingError] ?? thingError.replaceAll('ThingError', ''); } } // ── State type definition (from GetThingClasses) ────────────────────────────── class NymeaStateType { final String id; final String name; final String displayName; final String type; // "Double", "Bool", "Int", "String" final String unit; // "W", "V", "A", "Percentage", "" final dynamic defaultValue; final bool writable; final List? allowedValues; // valeurs possibles pour les états enum final double? minValue; // borne min pour Int/Double final double? maxValue; // borne max pour Int/Double const NymeaStateType({ required this.id, required this.name, required this.displayName, required this.type, required this.unit, this.defaultValue, this.writable = false, this.allowedValues, this.minValue, this.maxValue, }); factory NymeaStateType.fromJson(Map j) => NymeaStateType( id: j['id'] as String? ?? '', name: j['name'] as String? ?? '', displayName: j['displayName'] as String? ?? j['name'] as String? ?? '', type: j['type'] as String? ?? 'String', unit: j['unit'] as String? ?? '', defaultValue: j['defaultValue'], writable: j['writable'] as bool? ?? false, allowedValues: (j['allowedValues'] as List?)?.toList() ?? (j['possibleValues'] as List?)?.toList(), minValue: (j['minValue'] as num?)?.toDouble(), maxValue: (j['maxValue'] as num?)?.toDouble(), ); String formatValue(dynamic value) { if (value == null) return '—'; switch (unit) { case 'W': return '${_fmt(value)} W'; case 'kW': return '${_fmt(value)} kW'; case 'Wh': return '${_fmt(value)} Wh'; case 'kWh': return '${_fmt(value)} kWh'; case 'V': return '${_fmt(value)} V'; case 'A': return '${_fmt(value)} A'; case 'Percentage': return '${_fmt(value)} %'; case 'Celsius': return '${_fmt(value)} °C'; case 'Hz': return '${_fmt(value)} Hz'; case 'VoltAmpereReactive': return '${_fmt(value)} VAr'; case 'KiloVoltAmpereHour': return '${_fmt(value)} kVAh'; case 'Rpm': return '${_fmt(value)} rpm'; default: if (type == 'Bool') return (value == true || value == 'true') ? 'Oui' : 'Non'; return value.toString(); } } String _fmt(dynamic value) { if (value is double) return value.toStringAsFixed(2); return value.toString(); } } // ── Action type definition ───────────────────────────────────────────────────── class NymeaActionType { final String id; final String name; final String displayName; final List> paramTypes; const NymeaActionType({ required this.id, required this.name, required this.displayName, this.paramTypes = const [], }); factory NymeaActionType.fromJson(Map j) => NymeaActionType( id: j['id'] as String? ?? '', name: j['name'] as String? ?? '', displayName: j['displayName'] as String? ?? j['name'] as String? ?? '', paramTypes: List>.from(j['paramTypes'] ?? []), ); } class NymeaThingClass { final String id; final String name; final String displayName; final List interfaces; // ← clé pour la catégorisation final List stateTypes; final List actionTypes; final List> paramTypes; final List settingsTypes; const NymeaThingClass({ required this.id, required this.name, required this.displayName, this.interfaces = const [], this.stateTypes = const [], this.actionTypes = const [], this.paramTypes = const [], this.settingsTypes = const [], }); factory NymeaThingClass.fromJson(Map j) => NymeaThingClass( id: j['id'] as String? ?? '', name: j['name'] as String? ?? '', displayName: j['displayName'] as String? ?? '', interfaces: List.from(j['interfaces'] ?? []), stateTypes: (j['stateTypes'] as List? ?? []) .map((s) => NymeaStateType.fromJson(s as Map)) .toList(), actionTypes: (j['actionTypes'] as List? ?? []) .map((a) => NymeaActionType.fromJson(a as Map)) .toList(), paramTypes: List>.from(j['paramTypes'] ?? []), settingsTypes: (j['settingsTypes'] as List? ?? []) .map((s) => NymeaStateType.fromJson(s as Map)) .toList(), ); /// Retourne la stateType par son id NymeaStateType? stateTypeById(String id) { try { return stateTypes.firstWhere((s) => s.id == id); } catch (_) { return null; } } /// Retourne la stateType "principale" à afficher sur la card NymeaStateType? get primaryStateType { // Priorité : power → energy → temperature → connected → on/off → premier const priority = [ 'currentPower', 'power', 'totalEnergyConsumed', 'totalEnergyProduced', 'temperature', 'batterylevel', 'soc', 'connected', 'on', ]; for (final p in priority) { final found = stateTypes.where( (s) => s.name.toLowerCase() == p.toLowerCase() ); if (found.isNotEmpty) return found.first; } return stateTypes.isNotEmpty ? stateTypes.first : null; } } // ── Favorite Widgets ────────────────────────────────────────────────────────── enum FavoriteType { pvPower, homePower, batterySOC, gridPower, rates, evCharger, thingState, chart, } class FavoriteWidget { final String id; final FavoriteType type; final String title; final String? thingId; final String? stateTypeId; final Map extra; const FavoriteWidget({ required this.id, required this.type, required this.title, this.thingId, this.stateTypeId, this.extra = const {}, }); factory FavoriteWidget.fromJson(Map j) => FavoriteWidget( id: j['id'] as String, type: FavoriteType.values.firstWhere( (t) => t.name == j['type'], orElse: () => FavoriteType.pvPower), title: j['title'] as String, thingId: j['thingId'] as String?, stateTypeId: j['stateTypeId'] as String?, extra: Map.from(j['extra'] as Map? ?? {}), ); Map toJson() => { 'id': id, 'type': type.name, 'title': title, if (thingId != null) 'thingId': thingId, if (stateTypeId != null) 'stateTypeId': stateTypeId, if (extra.isNotEmpty) 'extra': extra, }; } // ── Historique d'un état ─────────────────────────────────────────────────────── class HistoryEntry { final DateTime timestamp; final double value; const HistoryEntry({required this.timestamp, required this.value}); }