É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>
273 lines
10 KiB
Dart
273 lines
10 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:provider/provider.dart';
|
||
import '../models/nymea_models.dart';
|
||
import '../models/thing_category.dart';
|
||
import '../services/nymea_service.dart';
|
||
import '../theme/app_theme.dart';
|
||
import 'thing_detail_screen.dart';
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// CategoryOverviewScreen — NIVEAU 2
|
||
//
|
||
// Liste des things d'une catégorie.
|
||
// Reproduit smart-meters_overview.png (nymea-app) :
|
||
//
|
||
// AppBar : ‹ [NOM CATÉGORIE] (centré)
|
||
//
|
||
// Pour chaque thing :
|
||
// ┌─────────────────────────────────────┐
|
||
// │ Nom du thing (bold) ● │ ← fond coloré léger
|
||
// ├─────────────────────────────────────┤
|
||
// │ [icône] Classe appareil valeur ›│ ← fond blanc
|
||
// └─────────────────────────────────────┘
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
class CategoryOverviewScreen extends StatelessWidget {
|
||
final ThingCategoryInfo info;
|
||
final List<NymeaThing> things;
|
||
final List<NymeaThingClass> thingClasses;
|
||
|
||
const CategoryOverviewScreen({
|
||
super.key,
|
||
required this.info,
|
||
required this.things,
|
||
required this.thingClasses,
|
||
});
|
||
|
||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||
|
||
NymeaThingClass? _cls(NymeaThing t) {
|
||
try {
|
||
return thingClasses.firstWhere((c) => c.id == t.thingClassId);
|
||
} catch (_) {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
bool _isOnline(NymeaThing t, NymeaThingClass? cls) {
|
||
final ct =
|
||
cls?.stateTypes.where((s) => s.name.toLowerCase() == 'connected');
|
||
if (ct != null && ct.isNotEmpty) {
|
||
final v = t.stateValue(ct.first.id);
|
||
return v == true || v == 'true';
|
||
}
|
||
return t.isSetupComplete;
|
||
}
|
||
|
||
String? _primaryValue(NymeaThing t, NymeaThingClass? cls) {
|
||
final p = cls?.primaryStateType;
|
||
if (p == null) return null;
|
||
return p.formatValue(t.stateValue(p.id));
|
||
}
|
||
|
||
// ── Build ──────────────────────────────────────────────────────────────────
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
// Écoute les mises à jour live via le service
|
||
final service = context.watch<NymeaService>();
|
||
final liveThings = things.map((t) {
|
||
try {
|
||
return service.things.firstWhere((lt) => lt.id == t.id);
|
||
} catch (_) {
|
||
return t;
|
||
}
|
||
}).toList();
|
||
|
||
return Scaffold(
|
||
backgroundColor: AppTheme.backgroundGray,
|
||
appBar: AppBar(
|
||
backgroundColor: AppTheme.backgroundGray,
|
||
elevation: 0,
|
||
centerTitle: true,
|
||
leading: IconButton(
|
||
icon: const Icon(Icons.chevron_left,
|
||
color: AppTheme.textDark, size: 30),
|
||
onPressed: () => Navigator.pop(context),
|
||
),
|
||
title: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(info.icon, color: info.color, size: 20),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
info.label,
|
||
style: const TextStyle(
|
||
color: AppTheme.textDark,
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 18),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
body: ListView.separated(
|
||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 60),
|
||
itemCount: liveThings.length,
|
||
separatorBuilder: (context, i) => const SizedBox(height: 10),
|
||
itemBuilder: (context, i) {
|
||
final t = liveThings[i];
|
||
final cls = _cls(t);
|
||
return _ThingListCard(
|
||
thing: t,
|
||
thingClass: cls,
|
||
info: info,
|
||
isOnline: _isOnline(t, cls),
|
||
primaryValue: _primaryValue(t, cls),
|
||
onTap: () => Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => ThingDetailScreen(thing: t)),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
// _ThingListCard — Card deux lignes style nymea
|
||
// ─────────────────────────────────────────────────────────────────────────────
|
||
|
||
class _ThingListCard extends StatelessWidget {
|
||
final NymeaThing thing;
|
||
final NymeaThingClass? thingClass;
|
||
final ThingCategoryInfo info;
|
||
final bool isOnline;
|
||
final String? primaryValue;
|
||
final VoidCallback onTap;
|
||
|
||
const _ThingListCard({
|
||
required this.thing,
|
||
required this.thingClass,
|
||
required this.info,
|
||
required this.isOnline,
|
||
required this.primaryValue,
|
||
required this.onTap,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return ClipRRect(
|
||
borderRadius: BorderRadius.circular(AppTheme.cornerRadius),
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: InkWell(
|
||
onTap: onTap,
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(AppTheme.cornerRadius),
|
||
border: Border.all(
|
||
color: info.color.withValues(alpha: 0.28), width: 1),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// ── Ligne 1 : nom du thing ─────────────────────────────────
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: info.color.withValues(alpha: 0.10),
|
||
borderRadius: const BorderRadius.vertical(
|
||
top: Radius.circular(9)),
|
||
),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 16, vertical: 11),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
thing.name,
|
||
style: const TextStyle(
|
||
fontSize: 15,
|
||
fontWeight: FontWeight.bold,
|
||
color: AppTheme.textDark),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
// Pastille statut
|
||
Container(
|
||
width: 9,
|
||
height: 9,
|
||
decoration: BoxDecoration(
|
||
color:
|
||
isOnline ? info.color : Colors.grey[400],
|
||
shape: BoxShape.circle,
|
||
boxShadow: isOnline
|
||
? [
|
||
BoxShadow(
|
||
color: info.color
|
||
.withValues(alpha: 0.45),
|
||
blurRadius: 4,
|
||
)
|
||
]
|
||
: null,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// ── Ligne 2 : icône + classe + valeur primaire ─────────────
|
||
Container(
|
||
color: AppTheme.cardWhite,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 16, vertical: 12),
|
||
child: Row(
|
||
children: [
|
||
// Icône
|
||
Container(
|
||
width: 32,
|
||
height: 32,
|
||
decoration: BoxDecoration(
|
||
color: info.color
|
||
.withValues(alpha: isOnline ? 0.12 : 0.06),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Icon(
|
||
info.icon,
|
||
size: 18,
|
||
color: isOnline
|
||
? info.color
|
||
: info.color.withValues(alpha: 0.4),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
|
||
// Classe appareil
|
||
Expanded(
|
||
child: Text(
|
||
thingClass?.displayName ?? 'Appareil',
|
||
style: const TextStyle(
|
||
fontSize: 13, color: AppTheme.textLight),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
|
||
// Valeur primaire
|
||
Text(
|
||
primaryValue ?? '—',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
color: isOnline
|
||
? info.color
|
||
: AppTheme.textLight,
|
||
),
|
||
),
|
||
const SizedBox(width: 4),
|
||
|
||
// Flèche
|
||
const Icon(Icons.chevron_right,
|
||
size: 18, color: AppTheme.textLight),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|