diff --git a/lib/screens/energy_screen.dart b/lib/screens/energy_screen.dart index 31e0fdf..6cb0216 100644 --- a/lib/screens/energy_screen.dart +++ b/lib/screens/energy_screen.dart @@ -54,6 +54,7 @@ class _EnergyScreenState extends State { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _nymeaService = context.read(); + if (!_nymeaService!.connected) _nymeaService!.startSimulation(); _nymeaService!.addListener(_onServiceChangedForSoc); _fetch(); }); @@ -298,7 +299,8 @@ class _EnergyScreenState extends State { } final xInterval = _xInterval(); - final labelStyle = EtmTokens.sans(size: 9, color: EtmTokens.muted); + final labelStyle = EtmTokens.sans(size: 9, color: EtmTokens.muted); + final yInterval = yMax / 4; // 5 graduations : 0, 25%, 50%, 75%, 100% de yMax return LineChart( LineChartData( @@ -308,52 +310,67 @@ class _EnergyScreenState extends State { gridData: FlGridData( show: true, drawVerticalLine: false, + horizontalInterval: yInterval, getDrawingHorizontalLine: (_) => FlLine(color: EtmTokens.line, strokeWidth: 1), ), borderData: FlBorderData(show: false), titlesData: FlTitlesData( topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)), + + // Axe gauche : kW leftTitles: AxisTitles( - axisNameWidget: Text('kW', style: EtmTokens.sans(size: 9, color: EtmTokens.muted)), - axisNameSize: 14, sideTitles: SideTitles( showTitles: true, - reservedSize: 42, - getTitlesWidget: (v, _) => Text( - v.toStringAsFixed(1), - style: labelStyle, - textAlign: TextAlign.right, - ), + reservedSize: 46, + interval: yInterval, + getTitlesWidget: (v, meta) { + if (v == meta.max) return const SizedBox.shrink(); // évite label dupliqué au sommet + return SideTitleWidget( + meta: meta, + child: Text( + '${v.toStringAsFixed(1)}kW', + style: labelStyle, + ), + ); + }, ), ), + + // Axe droit : SOC % rightTitles: hasSoc ? AxisTitles( - axisNameWidget: Text('%', style: EtmTokens.sans(size: 9, color: EtmTokens.green)), - axisNameSize: 14, sideTitles: SideTitles( showTitles: true, - reservedSize: 34, - interval: yMax / 4, - getTitlesWidget: (v, _) { + reservedSize: 40, + interval: yInterval, + getTitlesWidget: (v, meta) { + if (v == meta.max) return const SizedBox.shrink(); final pct = (v / yMax * 100).round(); if (pct < 0 || pct > 100) return const SizedBox.shrink(); - return Text('$pct%', - style: EtmTokens.sans(size: 9, color: EtmTokens.green)); + return SideTitleWidget( + meta: meta, + child: Text( + '$pct%', + style: EtmTokens.sans(size: 9, color: EtmTokens.green), + ), + ); }, ), ) : const AxisTitles(sideTitles: SideTitles(showTitles: false)), + + // Axe bas : temps bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - reservedSize: 20, + reservedSize: 22, interval: xInterval, - getTitlesWidget: (v, _) { + getTitlesWidget: (v, meta) { final idx = v.round(); if (idx < 0 || idx >= _data.length) return const SizedBox.shrink(); - return Padding( - padding: const EdgeInsets.only(top: 4), + return SideTitleWidget( + meta: meta, child: Text(_fmtTime(_data[idx].timestamp), style: labelStyle), ); }, @@ -457,22 +474,28 @@ class _EnergyScreenState extends State { leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - reservedSize: 46, - getTitlesWidget: (v, _) => Text(_fmtWh(v), - style: labelStyle, textAlign: TextAlign.right), + reservedSize: 52, + interval: maxE * 1.15 / 4, + getTitlesWidget: (v, meta) { + if (v == meta.max) return const SizedBox.shrink(); + return SideTitleWidget( + meta: meta, + child: Text(_fmtWh(v), style: labelStyle), + ); + }, ), ), bottomTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - reservedSize: 20, - getTitlesWidget: (v, _) { + reservedSize: 22, + getTitlesWidget: (v, meta) { final idx = v.toInt(); if (idx < 0 || idx >= _data.length) return const SizedBox.shrink(); final step = _xInterval().toInt().clamp(1, _data.length); if (idx % step != 0) return const SizedBox.shrink(); - return Padding( - padding: const EdgeInsets.only(top: 4), + return SideTitleWidget( + meta: meta, child: Text(_fmtTime(_data[idx].timestamp), style: labelStyle), ); }, diff --git a/lib/services/nymea_service.dart b/lib/services/nymea_service.dart index f073330..5eb4150 100644 --- a/lib/services/nymea_service.dart +++ b/lib/services/nymea_service.dart @@ -125,9 +125,11 @@ class NymeaService extends ChangeNotifier { /// Retourne {thingId, stateName} de la première batterie trouvée, /// ou null si aucune batterie configurée / pas encore chargée. - /// Non disponible en simulation (fetchHistory retourne des W, pas des %). Map? get batterySOCSource { - if (_isSimulation) return null; + // En simulation : retourne directement la batterie simulée. + if (_isSimulation) { + return {'thingId': 'sim-battery', 'stateName': 'soc'}; + } if (_things.isEmpty) { _log('🔋 batterySOCSource: things pas encore chargés', force: true); return null; @@ -1137,14 +1139,24 @@ class NymeaService extends ChangeNotifier { String sampleRate = 'SampleRate15Mins', }) async { if (_isSimulation) { - // Données sinusoïdales simulées + // SOC simulé : charge dans la journée (solaire) puis décharge le soir. + // Valeurs en % (0-100) pour être normalisables côté graphe. final entries = []; const steps = 48; final step = to.difference(from) ~/ steps; for (int i = 0; i <= steps; i++) { final t = from.add(step * i); - final v = 500 + 400 * sin(i * pi / 12); - entries.add(HistoryEntry(timestamp: t, value: v)); + final hour = t.hour + t.minute / 60.0; + // Montée solaire 8h-15h → 20% → 85%, descente soir → 20% + final double soc; + if (hour < 8) { + soc = 20 + 5 * (hour / 8); + } else if (hour < 15) { + soc = 25 + 60 * ((hour - 8) / 7); + } else { + soc = 85 - 65 * ((hour - 15) / 9); + } + entries.add(HistoryEntry(timestamp: t, value: soc.clamp(5.0, 95.0))); } return entries; }