fix(dashboard): « Plan de la journée » retirée — cinquième donnée inventée, et la dernière

DayPlan.demo() fabriquait vingt-quatre heures de flux : une gaussienne solaire
et des coefficients écrits en dur — solToEcs = excédent × 0,35, solToVe × 0,3,
solToBatt × 0,15, battToHouse = déficit × 0,55 — sous le titre « Plan de la
journée · aujourd'hui », sans la moindre mention. Y compris une batterie
alimentant la maison à partir de 17 h, sur une installation qui n'a pas de
batterie.

Cinquième après les deux MPPT, les 38 % de PAC, les 82 % de part solaire et le
SOC à 62 %. Même réponse à chaque fois : il n'y a rien derrière.

Un plan suppose de savoir ce qui VA arriver — il demande une prévision PV et une
source tarifaire, dont aucune n'existe. La carte reviendra avec elles.

Le code de fabrication part AVEC la carte : DayPlan, HourlyFlow, WeatherPoint,
WeatherKind et le painter sont supprimés, pas mis de côté. Un générateur de
données plausibles laissé dans le dépôt se rebranche — c'est exactement ce qui
s'est passé quatre fois déjà.

339 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GKM8FDNHZWogAsjjfB8pSw
This commit is contained in:
Patrick Schurig 2026-09-08 13:26:30 +02:00
parent 5a51582729
commit 5944d3a24a
4 changed files with 32 additions and 495 deletions

View File

@ -1,104 +1,16 @@
import 'dart:math' as math;
/// Modèles du tableau de bord.
///
/// 🔴 `HourlyFlow`, `WeatherPoint` et `DayPlan` ont été **supprimés** le 2026-09-08 avec la
/// carte « Plan de la journée » qu'ils alimentaient. `DayPlan.demo()` fabriquait vingt-quatre
/// heures de flux à partir d'une gaussienne et de coefficients écrits en dur, présentées sans
/// mention comme la journée en cours.
///
/// Le code part **avec** la carte, délibérément : un générateur de données plausibles laissé
/// dans le dépôt se rebranche — c'est ce qui s'était passé pour les deux MPPT, les 38 % de
/// PAC, les 82 % de part solaire et le SOC à 62 %. Un plan reviendra quand il y aura une
/// prévision PV et une source tarifaire ; il se réécrira sur des données, pas sur celles-ci.
library;
enum WeatherKind { sun, partlyCloudy, cloudy, moon }
class HourlyFlow {
final int hour;
final double solToHouse;
final double solToEcs;
final double solToVe;
final double solToBatt;
final double solToGrid;
final double battToHouse;
final double gridToHouse;
const HourlyFlow({
required this.hour,
this.solToHouse = 0,
this.solToEcs = 0,
this.solToVe = 0,
this.solToBatt = 0,
this.solToGrid = 0,
this.battToHouse = 0,
this.gridToHouse = 0,
});
double get totalPositive =>
solToHouse + solToEcs + solToVe + solToBatt + solToGrid;
double get totalNegative => battToHouse + gridToHouse;
}
class WeatherPoint {
final int hour;
final WeatherKind kind;
final int tempC;
const WeatherPoint({
required this.hour,
required this.kind,
required this.tempC,
});
}
class DayPlan {
final List<HourlyFlow> hours;
final List<WeatherPoint> weather;
final double nowHour;
const DayPlan({
required this.hours,
required this.weather,
required this.nowHour,
});
static DayPlan demo() {
final now = DateTime.now();
final nowHour = now.hour + now.minute / 60.0;
final hours = List.generate(24, (h) {
final solar = h >= 6 && h <= 20
? 3.8 * math.exp(-math.pow(h - 13, 2) / 18.0)
: 0.0;
final baseHouse = 1.2;
final peakHouse = (h >= 7 && h <= 9) || (h >= 18 && h <= 21) ? 0.7 : 0.0;
final house = baseHouse + peakHouse;
final excess = math.max(0.0, solar - house);
final deficit = math.max(0.0, house - solar);
final solToHouse = math.min(solar, house);
final solToEcs = excess > 0.5 ? math.min(excess * 0.35, 0.7) : 0.0;
final solToVe = excess > 1.2 ? math.min(excess * 0.3, 1.1) : 0.0;
final solToBatt = excess > 0.3 ? math.min(excess * 0.15, 0.4) : 0.0;
final solToGrid = math.max(0.0, excess - solToEcs - solToVe - solToBatt);
final battToHouse = h >= 17 ? math.min(deficit * 0.55, 0.8) : 0.0;
final gridToHouse = math.max(0.0, deficit - battToHouse);
return HourlyFlow(
hour: h,
solToHouse: solToHouse,
solToEcs: solToEcs,
solToVe: solToVe,
solToBatt: solToBatt,
solToGrid: solToGrid,
battToHouse: battToHouse,
gridToHouse: gridToHouse,
);
});
final weather = [
const WeatherPoint(hour: 0, kind: WeatherKind.moon, tempC: 17),
const WeatherPoint(hour: 4, kind: WeatherKind.moon, tempC: 15),
const WeatherPoint(hour: 8, kind: WeatherKind.partlyCloudy, tempC: 19),
const WeatherPoint(hour: 12, kind: WeatherKind.sun, tempC: 25),
const WeatherPoint(hour: 16, kind: WeatherKind.sun, tempC: 27),
const WeatherPoint(hour: 20, kind: WeatherKind.partlyCloudy, tempC: 22),
];
return DayPlan(hours: hours, weather: weather, nowHour: nowHour);
}
}
class HeosAction {
final String iconKey;

View File

@ -1,296 +0,0 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../../../theme/etm_tokens.dart';
import '../models/dashboard_models.dart';
class DayPlanPainter extends CustomPainter {
final DayPlan plan;
final Color surfaceColor;
final Color lineColor;
final Color inkColor;
final Color mutedColor;
final bool isDark;
const DayPlanPainter({
required this.plan,
required this.surfaceColor,
required this.lineColor,
required this.inkColor,
required this.mutedColor,
required this.isDark,
});
static const double _weatherH = 44.0;
static const double _xAxisH = 22.0;
static const double _leftPad = 28.0;
static const double _maxPosKwh = 4.0;
static const double _maxNegKwh = 1.8;
@override
void paint(Canvas canvas, Size size) {
final chartH = size.height - _weatherH - _xAxisH;
final chartW = size.width - _leftPad;
final zeroY = _weatherH + chartH * (_maxPosKwh / (_maxPosKwh + _maxNegKwh));
final posScale = chartH * (_maxPosKwh / (_maxPosKwh + _maxNegKwh)) / _maxPosKwh;
final negScale = chartH * (_maxNegKwh / (_maxPosKwh + _maxNegKwh)) / _maxNegKwh;
final nowX = _leftPad + plan.nowHour / 24 * chartW;
// ── Future background
if (nowX < size.width) {
canvas.drawRect(
Rect.fromLTRB(nowX, _weatherH, size.width, _weatherH + chartH),
Paint()
..color = (isDark
? const Color(0xFF06141D)
: const Color(0xFFEEF2F5))
.withValues(alpha: 0.55),
);
_drawHatch(canvas, Rect.fromLTRB(nowX, _weatherH, size.width, _weatherH + chartH));
}
// ── Bars (24 hours)
final barW = chartW / 24;
for (final h in plan.hours) {
final x = _leftPad + h.hour * barW;
_drawStackedBars(canvas, x, barW, zeroY, posScale, negScale, h);
}
// ── Y axis labels
_drawYLabels(canvas, zeroY, posScale, size);
// ── Zero line
canvas.drawLine(
Offset(_leftPad, zeroY),
Offset(size.width, zeroY),
Paint()
..color = lineColor
..strokeWidth = 1.0,
);
// ── X axis labels (0h 6h 12h 18h 24h)
for (final h in [0, 6, 12, 18, 24]) {
final x = _leftPad + h / 24 * chartW;
_paintText(
canvas,
h == 24 ? '24h' : '${h}h',
Offset(x, _weatherH + chartH + 4),
mutedColor,
9,
center: true,
);
}
// ── "Now" line + label
if (nowX > _leftPad && nowX < size.width) {
canvas.drawLine(
Offset(nowX, _weatherH),
Offset(nowX, _weatherH + chartH),
Paint()
..color = inkColor.withValues(alpha: 0.6)
..strokeWidth = 1.2
..style = PaintingStyle.stroke,
);
final now = DateTime.now();
final label =
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
_drawNowBadge(canvas, nowX, _weatherH, label);
}
// ── Weather band
_drawWeatherBand(canvas, chartW, plan.weather);
}
void _drawStackedBars(Canvas canvas, double x, double barW, double zeroY,
double posScale, double negScale, HourlyFlow h) {
const gap = 1.5;
final bw = barW - gap;
// Positive stack (upward from zeroY)
double topY = zeroY;
void stackPos(double kWh, Color color) {
if (kWh < 0.01) return;
final h2 = kWh * posScale;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(x + gap / 2, topY - h2, bw, h2),
const Radius.circular(1.5),
),
Paint()..color = color,
);
topY -= h2;
}
stackPos(h.solToHouse, EtmTokens.solToHouse);
stackPos(h.solToEcs, EtmTokens.solToEcs);
stackPos(h.solToVe, EtmTokens.solToVe);
stackPos(h.solToBatt, EtmTokens.solToBatt);
stackPos(h.solToGrid, EtmTokens.solToGrid);
// Negative stack (downward from zeroY)
double botY = zeroY;
void stackNeg(double kWh, Color color) {
if (kWh < 0.01) return;
final h2 = kWh * negScale;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(x + gap / 2, botY, bw, h2),
const Radius.circular(1.5),
),
Paint()..color = color.withValues(alpha: 0.7),
);
botY += h2;
}
stackNeg(h.battToHouse, EtmTokens.battToHouse);
stackNeg(h.gridToHouse, EtmTokens.gridToHouse);
}
void _drawHatch(Canvas canvas, Rect rect) {
canvas.save();
canvas.clipRect(rect);
final hatchPaint = Paint()
..color = surfaceColor.withValues(alpha: 0.5)
..strokeWidth = 1.0
..style = PaintingStyle.stroke;
const step = 8.0;
for (double d = -rect.height; d < rect.width + rect.height; d += step) {
canvas.drawLine(
Offset(rect.left + d, rect.top),
Offset(rect.left + d + rect.height, rect.bottom),
hatchPaint,
);
}
canvas.restore();
}
void _drawYLabels(Canvas canvas, double zeroY, double posScale, Size size) {
for (final kWh in [2.0, 4.0]) {
final y = zeroY - kWh * posScale;
_paintText(canvas, '${kWh.toInt()}', Offset(0, y - 5), mutedColor, 8);
canvas.drawLine(
Offset(_leftPad, y),
Offset(size.width, y),
Paint()
..color = lineColor.withValues(alpha: 0.6)
..strokeWidth = 0.5,
);
}
}
void _drawNowBadge(Canvas canvas, double x, double top, String label) {
final tp = TextPainter(
text: TextSpan(
text: label,
style: TextStyle(
fontFamily: 'IBMPlexMono',
fontSize: 9,
fontWeight: FontWeight.w600,
color: surfaceColor,
),
),
textDirection: TextDirection.ltr,
)..layout();
const pad = 4.0;
final rect = Rect.fromCenter(
center: Offset(x, top - tp.height / 2 - pad - 2),
width: tp.width + pad * 2,
height: tp.height + pad,
);
canvas.drawRRect(
RRect.fromRectAndRadius(rect, const Radius.circular(4)),
Paint()..color = inkColor.withValues(alpha: 0.75),
);
tp.paint(canvas, Offset(rect.left + pad, rect.top + pad / 2));
}
void _drawWeatherBand(
Canvas canvas, double chartW, List<WeatherPoint> points) {
for (final p in points) {
final x = _leftPad + p.hour / 24 * chartW;
final iconColor = p.kind == WeatherKind.sun || p.kind == WeatherKind.partlyCloudy
? EtmTokens.solar
: mutedColor;
_drawWeatherIcon(canvas, Offset(x, 12), p.kind, iconColor);
_paintText(
canvas,
'${p.tempC}°',
Offset(x, 26),
mutedColor,
9,
center: true,
);
}
}
void _drawWeatherIcon(
Canvas canvas, Offset center, WeatherKind kind, Color color) {
final paint = Paint()..color = color..style = PaintingStyle.fill;
switch (kind) {
case WeatherKind.sun:
_drawSun(canvas, center, color);
case WeatherKind.partlyCloudy:
_drawSun(canvas, center + const Offset(-3, -3), color, r: 5);
_drawCloud(canvas, center + const Offset(2, 2), color.withValues(alpha: 0.9));
case WeatherKind.cloudy:
_drawCloud(canvas, center, mutedColor);
case WeatherKind.moon:
_drawMoon(canvas, center, color, paint);
}
}
void _drawSun(Canvas canvas, Offset c, Color color, {double r = 6}) {
final p = Paint()..color = color..style = PaintingStyle.fill;
canvas.drawCircle(c, r, p);
final rp = Paint()
..color = color
..strokeWidth = 1.2
..style = PaintingStyle.stroke;
for (int i = 0; i < 8; i++) {
final a = i * math.pi / 4;
canvas.drawLine(
c + Offset(math.cos(a), math.sin(a)) * (r + 2),
c + Offset(math.cos(a), math.sin(a)) * (r + 5),
rp,
);
}
}
void _drawCloud(Canvas canvas, Offset c, Color color) {
final p = Paint()..color = color..style = PaintingStyle.fill;
canvas.drawCircle(c + const Offset(-4, 1), 4, p);
canvas.drawCircle(c + const Offset(1, -1), 5.5, p);
canvas.drawCircle(c + const Offset(6, 2), 3.5, p);
canvas.drawRect(
Rect.fromLTWH(c.dx - 7, c.dy + 1, 16, 4),
p,
);
}
void _drawMoon(Canvas canvas, Offset c, Color color, Paint paint) {
final path = Path()..addOval(Rect.fromCircle(center: c, radius: 7));
final cut = Path()..addOval(Rect.fromCircle(center: c + const Offset(4, -3), radius: 6));
final crescent = Path.combine(PathOperation.difference, path, cut);
canvas.drawPath(crescent, paint);
}
void _paintText(Canvas canvas, String text, Offset pos, Color color,
double size, {bool center = false}) {
final tp = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(fontSize: size, color: color, fontWeight: FontWeight.w500),
),
textDirection: TextDirection.ltr,
)..layout();
tp.paint(
canvas,
center ? Offset(pos.dx - tp.width / 2, pos.dy) : pos,
);
}
@override
bool shouldRepaint(DayPlanPainter old) =>
old.plan != plan || old.isDark != isDark;
}

View File

@ -1,97 +0,0 @@
import 'package:flutter/material.dart';
import '../../../theme/etm_tokens.dart';
import '../models/dashboard_models.dart';
import '../painters/day_plan_painter.dart';
class DayPlanCard extends StatelessWidget {
const DayPlanCard({super.key});
@override
Widget build(BuildContext context) {
final plan = DayPlan.demo();
final isDark = Theme.of(context).brightness == Brightness.dark;
return Container(
decoration: BoxDecoration(
color: EtmTokens.surfaceOf(context),
borderRadius: BorderRadius.circular(EtmTokens.radiusCard),
boxShadow: EtmTokens.cardShadowOf(context),
),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('Plan de la journée',
style: EtmTokens.sans(
size: 15,
weight: FontWeight.w600,
color: EtmTokens.inkOf(context))),
Text("aujourd'hui",
style: EtmTokens.sans(
size: 12, color: EtmTokens.mutedOf(context))),
],
),
const SizedBox(height: 12),
SizedBox(
height: 180,
child: CustomPaint(
painter: DayPlanPainter(
plan: plan,
surfaceColor: EtmTokens.surfaceOf(context),
lineColor: EtmTokens.lineOf(context),
inkColor: EtmTokens.inkOf(context),
mutedColor: EtmTokens.mutedOf(context),
isDark: isDark,
),
size: const Size(double.infinity, 180),
),
),
const SizedBox(height: 12),
_Legend(context: context),
],
),
);
}
}
class _Legend extends StatelessWidget {
final BuildContext context;
const _Legend({required this.context});
@override
Widget build(BuildContext _) {
final items = [
('Maison', EtmTokens.solToHouse),
('ECS', EtmTokens.solToEcs),
('VE', EtmTokens.solToVe),
('Batterie', EtmTokens.solToBatt),
('Réseau', EtmTokens.solToGrid),
('Batt.→maison', EtmTokens.battToHouse),
];
return Wrap(
spacing: 12,
runSpacing: 6,
children: items.map((e) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 10, height: 10,
decoration: BoxDecoration(
color: e.$2,
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 4),
Text(e.$1,
style: EtmTokens.sans(
size: 10, color: EtmTokens.mutedOf(context))),
],
);
}).toList(),
);
}
}

View File

@ -13,7 +13,6 @@ import '../theme/etm_tokens.dart';
import '../features/dashboard/widgets/energy_flow_card.dart';
import '../features/dashboard/widgets/kpi_row.dart';
import '../features/dashboard/widgets/heos_decision_card.dart';
import '../features/dashboard/widgets/day_plan_card.dart';
class DashboardScreen extends StatefulWidget {
const DashboardScreen({super.key});
@ -183,7 +182,26 @@ class _DashboardScreenState extends State<DashboardScreen> {
const SizedBox(height: 14),
const HeosDecisionCard(),
const SizedBox(height: 14),
const DayPlanCard(),
// ── « Plan de la journée » : RETIRÉE le 2026-09-08 ──────────
//
// Elle affichait `DayPlan.demo()` — vingt-quatre heures fabriquées
// par une gaussienne solaire et des coefficients écrits en dur
// (`solToEcs = excédent × 0,35`, `solToVe × 0,3`, `solToBatt × 0,15`,
// `battToHouse = déficit × 0,55`), sous le titre « Plan de la
// journée · aujourd'hui » et sans la moindre mention. Y compris une
// batterie alimentant la maison à partir de 17 h, sur une
// installation qui n'a pas de batterie.
//
// **Cinquième donnée inventée de ce tableau de bord** — après les
// deux MPPT, les 38 % de PAC, les 82 % de part solaire et le SOC à
// 62 %. Même réponse à chaque fois, et elle vaut encore : il n'y a
// rien derrière.
//
// Un plan suppose de savoir ce qui VA arriver : il demande une
// prévision PV et une source tarifaire, dont aucune n'existe. La
// carte reviendra avec elles, et elle dira alors quelque chose. Le
// code de fabrication part avec elle : laissé en place, il se
// rebranche.
const SizedBox(height: 8),
]),
),