etm-powersync-app/lib/widgets/energy_flow_widget.dart
pakutz79 c19c9d1a98 feat: navigation drawer, EMS setup, scheduler, tarifs, paramètres app
- Drawer custom (overlay Stack) avec mode installateur PIN SHA-256
- GoRouter + ShellRoute : navigation préservée entre onglets
- 6 providers : NavigationProvider, InstallerModeProvider, AppSettingsProvider,
  EnergySetupProvider, SchedulerProvider, TariffProvider
- Écrans Energy Manager : RoleConfigFlow (3 étapes), Scheduler, Tarifs, Timeline
- Écrans Paramètres : Apparence, Écrans actifs, AppSettingsScreen
- DrawerMenuButton présent dans les 5 AppBars principaux
- Simulation : _thingClasses générées avec interfaces EMS pour filtrage des rôles
- Compteur solaire : ajout smartmeter aux interfaces compatibles
- Thème ETM (etm_theme.dart), ProLockBadge, widgets PowerBar/RoleCard/TimelineSlotCard
- Dépendances : go_router, shared_preferences, crypto, url_launcher

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 14:52:32 +01:00

289 lines
8.5 KiB
Dart

import 'package:flutter/material.dart';
import '../models/energy_data.dart';
import '../theme/app_theme.dart';
class EnergyFlowWidget extends StatelessWidget {
final EnergyData data;
const EnergyFlowWidget({super.key, required this.data});
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Expanded(
child: Text(
'Données en temps réel',
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: AppTheme.textDark,
),
overflow: TextOverflow.ellipsis,
),
),
Row(
children: [
Text(
'${data.temperature.toStringAsFixed(0)}°C',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: AppTheme.textDark,
),
),
const SizedBox(width: 4),
const Icon(Icons.cloud, color: Colors.blueGrey, size: 22),
],
),
],
),
const SizedBox(height: 20),
// Energy nodes
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_EnergyNode(
icon: Icons.wb_sunny_rounded,
color: AppTheme.solarYellow,
power: data.pvPower,
label: 'Solaire',
),
_EnergyNode(
icon: Icons.home_rounded,
color: AppTheme.homeBlue,
power: data.homePower,
label: 'Maison',
),
_EnergyNode(
icon: Icons.battery_charging_full_rounded,
color: AppTheme.batteryGreen,
power: data.batteryPower.abs(),
label: 'Batterie',
badge: '${data.batterySOC.toStringAsFixed(0)}%',
),
_EnergyNode(
icon: Icons.electrical_services_rounded,
color: AppTheme.gridGray,
power: data.gridPower.abs(),
label: data.gridPower >= 0 ? 'Réseau ↓' : 'Réseau ↑',
),
],
),
const SizedBox(height: 16),
// Flow diagram
SizedBox(
height: 80,
child: CustomPaint(
painter: _FlowPainter(data: data),
size: Size.infinite,
),
),
],
),
),
);
}
}
class _EnergyNode extends StatelessWidget {
final IconData icon;
final Color color;
final double power;
final String label;
final String? badge;
const _EnergyNode({
required this.icon,
required this.color,
required this.power,
required this.label,
this.badge,
});
@override
Widget build(BuildContext context) {
return Column(
children: [
Stack(
clipBehavior: Clip.none,
children: [
Container(
width: 52,
height: 52,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
child: Icon(icon, color: Colors.white, size: 24),
),
if (badge != null)
Positioned(
bottom: -4,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 1),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color, width: 1.5),
),
child: Text(
badge!,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: color,
),
),
),
),
),
],
),
const SizedBox(height: 10),
Text(
_formatPower(power),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textDark,
),
),
Text(
_powerUnit(power),
style: const TextStyle(
fontSize: 12,
color: AppTheme.textLight,
),
),
Text(
label,
style: const TextStyle(
fontSize: 11,
color: AppTheme.textLight,
),
),
],
);
}
String _formatPower(double w) {
if (w >= 1000) return (w / 1000).toStringAsFixed(1);
return w.toStringAsFixed(0);
}
String _powerUnit(double w) => w >= 1000 ? 'kW' : 'W';
}
class _FlowPainter extends CustomPainter {
final EnergyData data;
_FlowPainter({required this.data});
@override
void paint(Canvas canvas, Size size) {
// Draw animated flow lines between nodes
final centerY = size.height * 0.4;
final nodePositions = [
size.width * 0.125, // PV
size.width * 0.375, // Home
size.width * 0.625, // Battery
size.width * 0.875, // Grid
];
final linePaint = Paint()
..strokeWidth = 2
..style = PaintingStyle.stroke;
// PV → Home (si PV produit)
if (data.pvPower > 0) {
_drawArrowLine(canvas, Offset(nodePositions[0], centerY),
Offset(nodePositions[1], centerY), AppTheme.solarYellow, linePaint);
}
// Batterie en charge : source = PV si disponible, sinon Réseau → Batterie
if (data.batteryPower > 0) {
if (data.pvPower > 0) {
// PV → Battery (solaire charge la batterie)
_drawArrowLine(canvas, Offset(nodePositions[0], centerY),
Offset(nodePositions[2], centerY), AppTheme.batteryGreen, linePaint);
} else {
// Grid → Battery (réseau charge la batterie)
_drawArrowLine(canvas, Offset(nodePositions[3], centerY),
Offset(nodePositions[2], centerY), AppTheme.batteryGreen, linePaint);
}
}
// Batterie en décharge → Home
if (data.batteryPower < 0) {
_drawArrowLine(canvas, Offset(nodePositions[2], centerY),
Offset(nodePositions[1], centerY), AppTheme.batteryGreen, linePaint);
}
// Réseau → Home (si import)
if (data.gridPower > 0) {
_drawArrowLine(canvas, Offset(nodePositions[3], centerY),
Offset(nodePositions[1], centerY), Colors.orange, linePaint);
} else if (data.gridPower < 0) {
// PV → Réseau (export / injection réseau)
_drawArrowLine(canvas, Offset(nodePositions[0], centerY),
Offset(nodePositions[3], centerY), AppTheme.solarYellow, linePaint);
}
// Draw node dots
final dotPaint = Paint()..style = PaintingStyle.fill;
for (int i = 0; i < nodePositions.length; i++) {
final colors = [
AppTheme.solarYellow,
AppTheme.homeBlue,
AppTheme.batteryGreen,
AppTheme.gridGray,
];
dotPaint.color = colors[i];
canvas.drawCircle(Offset(nodePositions[i], centerY), 6, dotPaint);
}
}
void _drawArrowLine(Canvas canvas, Offset start, Offset end, Color color,
Paint paint) {
paint.color = color.withValues(alpha:0.7);
final path = Path()
..moveTo(start.dx, start.dy)
..lineTo(end.dx, end.dy);
canvas.drawPath(path, paint);
// Arrow head
final arrowPaint = Paint()
..color = color
..style = PaintingStyle.fill;
final dx = end.dx - start.dx;
final arrowSign = dx > 0 ? 1 : -1;
final arrowTip = Offset(end.dx - arrowSign * 8, end.dy);
final arrowPath = Path()
..moveTo(arrowTip.dx + arrowSign * 8, arrowTip.dy)
..lineTo(arrowTip.dx, arrowTip.dy - 5)
..lineTo(arrowTip.dx, arrowTip.dy + 5)
..close();
canvas.drawPath(arrowPath, arrowPaint);
}
@override
bool shouldRepaint(_FlowPainter oldDelegate) => true;
}