etm-powersync-app/lib/models/nymea_models.dart
pakutz79 8862dc2a72 feat: historique énergie, navigation Things, actions nymea
Énergie :
- Écran Énergie reécrit : line chart (production/conso/autoconso/batterie)
  et bar chart (bilan Wh par période) avec onglets 15 min / 1 h / 1 j / 1 sem
- Datepicker pour sélectionner une période historique (chip dismissible)
- Timelines des deux graphiques alignées (même x=i → data[i].timestamp)
- PowerBalanceEntry + fetchPowerBalanceLogs() + simulation sinusoïdale
- Overflow fixes : energy_flow_widget (Expanded sur titre), production_card

Things :
- Navigation 3 niveaux : ThingsScreen → CategoryOverviewScreen → ThingDetailScreen
- Catégorie Cars ajoutée, carrousel corrigé (clamp RangeError)
- ThingDetailScreen : executeAction, setStateValue, activeThumbColor fix
- NymeaTile widget, state_history_chart widget (générique Logging.GetLogEntries)

Modèles / service :
- HistoryEntry, PowerBalanceEntry ajoutés
- fetchHistory(), fetchPowerBalanceLogs() dans NymeaService
- interfaceToCategoryMap étendu (Cars, etc.)
- AppTheme : nouvelles couleurs (accentTeal, boostRed, pvGreen, minPvBlue…)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 07:15:48 +01:00

293 lines
9.8 KiB
Dart

// ── Thing models ──────────────────────────────────────────────────────────────
class NymeaThing {
final String id;
final String name;
final String thingClassId;
final String setupStatus;
final List<Map<String, dynamic>> paramValues;
final List<NymeaStateValue> 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<String, dynamic> 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<Map<String, dynamic>>.from(j['paramValues'] ?? []),
states: (j['states'] as List? ?? [])
.map((s) => NymeaStateValue.fromJson(s))
.toList(),
);
}
NymeaThing copyWith({String? name, List<NymeaStateValue>? 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<String, dynamic> 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<dynamic>? 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<String, dynamic> 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<Map<String, dynamic>> paramTypes;
const NymeaActionType({
required this.id,
required this.name,
required this.displayName,
this.paramTypes = const [],
});
factory NymeaActionType.fromJson(Map<String, dynamic> j) => NymeaActionType(
id: j['id'] as String? ?? '',
name: j['name'] as String? ?? '',
displayName: j['displayName'] as String? ?? j['name'] as String? ?? '',
paramTypes: List<Map<String, dynamic>>.from(j['paramTypes'] ?? []),
);
}
class NymeaThingClass {
final String id;
final String name;
final String displayName;
final List<String> interfaces; // ← clé pour la catégorisation
final List<NymeaStateType> stateTypes;
final List<NymeaActionType> actionTypes;
final List<Map<String, dynamic>> paramTypes;
final List<NymeaStateType> 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<String, dynamic> j) => NymeaThingClass(
id: j['id'] as String? ?? '',
name: j['name'] as String? ?? '',
displayName: j['displayName'] as String? ?? '',
interfaces: List<String>.from(j['interfaces'] ?? []),
stateTypes: (j['stateTypes'] as List? ?? [])
.map((s) => NymeaStateType.fromJson(s as Map<String, dynamic>))
.toList(),
actionTypes: (j['actionTypes'] as List? ?? [])
.map((a) => NymeaActionType.fromJson(a as Map<String, dynamic>))
.toList(),
paramTypes: List<Map<String, dynamic>>.from(j['paramTypes'] ?? []),
settingsTypes: (j['settingsTypes'] as List? ?? [])
.map((s) => NymeaStateType.fromJson(s as Map<String, dynamic>))
.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<String, dynamic> extra;
const FavoriteWidget({
required this.id,
required this.type,
required this.title,
this.thingId,
this.stateTypeId,
this.extra = const {},
});
}
// ── Historique d'un état ───────────────────────────────────────────────────────
class HistoryEntry {
final DateTime timestamp;
final double value;
const HistoryEntry({required this.timestamp, required this.value});
}