From 31778307bc4f187a486d85b5ce67a7605c982972 Mon Sep 17 00:00:00 2001 From: hodasemi Date: Sun, 15 Oct 2023 09:41:35 +0200 Subject: [PATCH] Prepare graph rendering --- frontend/lib/devices/devices.dart | 55 ++++- frontend/lib/states/graphs.dart | 310 +++++++++++++++++++------ frontend/lib/states/home_page.dart | 3 +- frontend/lib/states/plug_settings.dart | 22 +- 4 files changed, 304 insertions(+), 86 deletions(-) diff --git a/frontend/lib/devices/devices.dart b/frontend/lib/devices/devices.dart index 222460a..0aa03df 100644 --- a/frontend/lib/devices/devices.dart +++ b/frontend/lib/devices/devices.dart @@ -18,6 +18,36 @@ class Category { final String name; List devices; + static Future> fetch_ids_only() async { + final response = await http.get(Uri.parse("${Constants.BASE_URL}/devices")); + + if (response.statusCode != 200) { + throw Exception("Failed to fetch devices"); + } + + final List categories = []; + + final Map> json = + Map.castFrom(jsonDecode(jsonDecode(response.body))); + + for (MapEntry> entry in json.entries) { + final Category category = Category(entry.key); + + for (dynamic device_info_dyn in entry.value) { + final Map device_info = device_info_dyn; + + category.devices + .add(DeviceIdOnly(device_info['id'], device_info['desc'])); + } + + if (category.devices.isNotEmpty) { + categories.add(category); + } + } + + return categories; + } + static Future> fetch() async { final response = await http.get(Uri.parse("${Constants.BASE_URL}/devices")); @@ -103,6 +133,13 @@ class Category { return categories; } + + String Name() { + return name + .split('_') + .map((s) => "${s[0].toUpperCase()}${s.substring(1)}") + .join(' '); + } } class CategoryWidget extends StatelessWidget { @@ -110,13 +147,6 @@ class CategoryWidget extends StatelessWidget { CategoryWidget({super.key, required this.category}); - String Name() { - return category.name - .split('_') - .map((s) => "${s[0].toUpperCase()}${s.substring(1)}") - .join(' '); - } - @override Widget build(BuildContext context) { return Wrap( @@ -132,3 +162,14 @@ class CategoryWidget extends StatelessWidget { abstract class Device { Widget create_widget(BuildContext context); } + +class DeviceIdOnly extends Device { + DeviceIdOnly(this.device_id, this.device_descriptor); + + final String device_id; + final String? device_descriptor; + + Widget create_widget(BuildContext context) { + throw UnimplementedError(); + } +} diff --git a/frontend/lib/states/graphs.dart b/frontend/lib/states/graphs.dart index 122ae0c..b32a48f 100644 --- a/frontend/lib/states/graphs.dart +++ b/frontend/lib/states/graphs.dart @@ -1,9 +1,28 @@ +import 'dart:convert'; + import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:http/http.dart' as http; import 'package:intl/intl.dart'; import '../constants.dart'; +import '../devices/devices.dart'; + +class ChartData { + ChartData(this.time, this.watts); + + int time; + double watts; +} + +class CheckBoxDevice { + CheckBoxDevice(this.device); + + final DeviceIdOnly device; + bool enabled = false; + List data = List.empty(); +} class Graphs extends StatefulWidget { const Graphs({super.key}); @@ -15,84 +34,233 @@ class Graphs extends StatefulWidget { class _GraphsState extends State { String? start_date; String? end_date; + List devices = List.empty(); + + final TextEditingController categoryController = TextEditingController(); + final TextEditingController startDateController = TextEditingController(); + final TextEditingController endDateController = TextEditingController(); + + @override + void dispose() { + // Clean up the controller when the widget is removed from the + // widget tree. + categoryController.dispose(); + startDateController.dispose(); + endDateController.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.inversePrimary, - title: const Text('Graphs'), - actions: [ - IconButton( - icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.of(context).pushReplacementNamed( - '/', - )) - ], - ), - body: Center( - child: Column(children: [ - Row(children: [ - const Text('Start'), - SizedBox( - width: 200, - height: 40, - child: TextField( - decoration: const InputDecoration( - icon: Icon(Icons.calendar_today), - labelText: "Enter Date"), - readOnly: true, - onTap: () async { - final DateTime? pickedDate = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(2023), - lastDate: DateTime(2101)); + return FutureBuilder>( + future: Category.fetch_ids_only(), + builder: (context, AsyncSnapshot> categories) { + if (!categories.hasData) { + return SpinKitWanderingCubes( + color: Colors.deepPurple[100], + size: 80.0, + ); + } - if (pickedDate != null) { - final String formattedDate = - DateFormat('dd.MM.yyyy').format(pickedDate); - - print(formattedDate); + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.inversePrimary, + title: const Text('Graphs'), + actions: [ + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () => + Navigator.of(context).pushReplacementNamed( + '/', + )) + ], + ), + body: Center( + child: Column(children: [ + const SizedBox( + height: 40, + ), + DropdownMenu( + label: const Text('Category'), + controller: categoryController, + width: 200, + onSelected: (Category? category) { + if (category != null) { + setState(() { + devices = category.devices + .map((dev) => CheckBoxDevice(dev as DeviceIdOnly)) + .toList(); + }); } }, - onChanged: (text) => start_date = text, - )) - ]), - Row(children: [ - const Text('End'), - SizedBox( - width: 200, - height: 40, - child: TextField( - decoration: const InputDecoration( - icon: Icon(Icons.calendar_today), - labelText: "Enter Date"), - readOnly: true, - onTap: () async { - final DateTime? pickedDate = await showDatePicker( - context: context, - initialDate: DateTime.now(), - firstDate: DateTime(2023), - lastDate: DateTime(2101)); + dropdownMenuEntries: categories.data! + .map((category) => DropdownMenuEntry( + label: category.Name(), value: category)) + .toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: devices.map((device) { + return SizedBox( + width: 200, + height: 40, + child: CheckboxListTile( + title: Text(device.device.device_descriptor ?? + device.device.device_id), + controlAffinity: ListTileControlAffinity.leading, + splashRadius: 10.0, + value: device.enabled, + onChanged: (bool? value) { + setState(() { + device.enabled = value!; + }); + })); + }).toList()), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + const Text('Start'), + SizedBox( + width: 200, + height: 40, + child: TextField( + controller: startDateController, + decoration: const InputDecoration( + icon: Icon(Icons.calendar_today), + ), + readOnly: true, + onTap: () async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2023), + lastDate: DateTime(2101)); - if (pickedDate != null) { - final String formattedDate = - DateFormat('dd.MM.yyyy').format(pickedDate); + if (pickedDate != null) { + final String formattedDate = + DateFormat('dd.MM.yyyy').format(pickedDate); - print(formattedDate); - } - }, - onChanged: (text) => end_date = text, - )) - ]), - // LineChart( - // LineChartData( - // // - // ), - // duration: const Duration(milliseconds: 200), - // curve: Curves.linear, - // ) - ]))); + startDateController.text = formattedDate; + } + }, + )) + ]), + Row(mainAxisAlignment: MainAxisAlignment.center, children: [ + const Text('End'), + SizedBox( + width: 200, + height: 40, + child: TextField( + controller: endDateController, + decoration: const InputDecoration( + icon: Icon(Icons.calendar_today), + ), + readOnly: true, + onTap: () async { + final DateTime? pickedDate = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(2023), + lastDate: DateTime(2101)); + + if (pickedDate != null) { + final String formattedDate = + DateFormat('dd.MM.yyyy').format(pickedDate); + + endDateController.text = formattedDate; + } + }, + )) + ]), + ElevatedButton( + onPressed: () { + setState(() { + for (var device in devices) { + if (!device.enabled) { + device.data = List.empty(); + continue; + } + + int start_date; + int end_date; + + try { + start_date = (DateFormat('dd.MM.yyyy') + .parse(startDateController.text) + .millisecondsSinceEpoch / + 1000) as int; + } on Exception catch (_) { + start_date = 0; + print('no start date set'); + } + + try { + end_date = (DateFormat('dd.MM.yyyy') + .parse(endDateController.text) + .millisecondsSinceEpoch / + 1000) as int; + } on Exception catch (_) { + end_date = 0; + print('no end date set'); + } + + if (start_date == 0 || end_date == 0) { + device.data = List.empty(); + continue; + } + + const String filter_type = 'hourly'; + + http + .get(Uri.parse( + "${Constants.BASE_URL}/plug_data/${device.device.device_id}/$start_date/$end_date/$filter_type")) + .then((response) { + if (response.statusCode != 200) { + throw Exception("Failed to fetch data"); + } + + final data = jsonDecode(jsonDecode(response.body)) + as List; + + final List chart_data = []; + + for (final entry in data) { + final pair = entry as List; + chart_data.add(ChartData(pair[0], pair[1])); + } + + device.data = chart_data; + }); + } + }); + }, + child: const Text('Update')), + if (devices.any((device) => device.data.isNotEmpty)) + LineChart( + LineChartData( + lineBarsData: devices + .map((device) { + if (device.data.isNotEmpty) { + final data = device.data.map((data) { + return FlSpot(data.time as double, data.watts); + }).toList(); + + print('chart data: $data'); + + return LineChartBarData( + spots: data, + ); + } else { + return null; + } + }) + .where((element) => element != null) + .map((opt) => opt!) + .toList(), + // titlesData: FlTitlesData(bottomTitles: AxisTitles(sideTitles: )) + ), + duration: const Duration(milliseconds: 200), + curve: Curves.linear, + ) + ]))); + }); } } diff --git a/frontend/lib/states/home_page.dart b/frontend/lib/states/home_page.dart index c77c761..d5565cc 100644 --- a/frontend/lib/states/home_page.dart +++ b/frontend/lib/states/home_page.dart @@ -75,7 +75,8 @@ class _MyHomePageState extends State { return ExpansionPanel( headerBuilder: (BuildContext context, bool isExpanded) { - return ListTile(title: Text(widget.Name())); + return ListTile( + title: Text(widget.category.Name())); }, body: widget, isExpanded: expanded[category.key], diff --git a/frontend/lib/states/plug_settings.dart b/frontend/lib/states/plug_settings.dart index 8d2c817..240033a 100644 --- a/frontend/lib/states/plug_settings.dart +++ b/frontend/lib/states/plug_settings.dart @@ -13,19 +13,29 @@ class PlugSettingsArguments { class PlugSettings extends StatefulWidget { PlugSettings({super.key}); - String? device_descriptor; - @override State createState() => _PlugSettingsState(); } class _PlugSettingsState extends State { + final TextEditingController descriptor_controller = TextEditingController(); + + @override + void dispose() { + // Clean up the controller when the widget is removed from the + // widget tree. + descriptor_controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final args = ModalRoute.of(context)!.settings.arguments! as PlugSettingsArguments; - widget.device_descriptor = args.device_descriptor; + if (args.device_descriptor != null) { + descriptor_controller.text = args.device_descriptor!; + } return Scaffold( appBar: AppBar( @@ -58,8 +68,7 @@ class _PlugSettingsState extends State { style: TextStyle(fontWeight: FontWeight.bold), ), TextField( - controller: TextEditingController() - ..text = args.device_descriptor ?? '', + controller: descriptor_controller, decoration: InputDecoration( border: OutlineInputBorder( borderRadius: @@ -68,7 +77,6 @@ class _PlugSettingsState extends State { isDense: true, ), style: const TextStyle(height: 1.0, color: Colors.black), - onChanged: (text) => widget.device_descriptor = text, ), ]) ], @@ -95,7 +103,7 @@ class _PlugSettingsState extends State { onPressed: () { http .post(Uri.parse( - "${Constants.BASE_URL}/device_name/${args.device_id}/${widget.device_descriptor}")) + "${Constants.BASE_URL}/device_name/${args.device_id}/${descriptor_controller.text}")) .then((response) { if (response.statusCode != 200) { throw Exception("Failed to fetch devices");