Prepare graph rendering

This commit is contained in:
hodasemi 2023-10-15 09:41:35 +02:00
parent 39f52dd9af
commit 31778307bc
4 changed files with 304 additions and 86 deletions

View file

@ -18,6 +18,36 @@ class Category {
final String name; final String name;
List<Device> devices; List<Device> devices;
static Future<List<Category>> 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<Category> categories = [];
final Map<String, List<dynamic>> json =
Map.castFrom(jsonDecode(jsonDecode(response.body)));
for (MapEntry<String, List<dynamic>> entry in json.entries) {
final Category category = Category(entry.key);
for (dynamic device_info_dyn in entry.value) {
final Map<String, dynamic> 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<List<Category>> fetch() async { static Future<List<Category>> fetch() async {
final response = await http.get(Uri.parse("${Constants.BASE_URL}/devices")); final response = await http.get(Uri.parse("${Constants.BASE_URL}/devices"));
@ -103,6 +133,13 @@ class Category {
return categories; return categories;
} }
String Name() {
return name
.split('_')
.map((s) => "${s[0].toUpperCase()}${s.substring(1)}")
.join(' ');
}
} }
class CategoryWidget extends StatelessWidget { class CategoryWidget extends StatelessWidget {
@ -110,13 +147,6 @@ class CategoryWidget extends StatelessWidget {
CategoryWidget({super.key, required this.category}); CategoryWidget({super.key, required this.category});
String Name() {
return category.name
.split('_')
.map((s) => "${s[0].toUpperCase()}${s.substring(1)}")
.join(' ');
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Wrap( return Wrap(
@ -132,3 +162,14 @@ class CategoryWidget extends StatelessWidget {
abstract class Device { abstract class Device {
Widget create_widget(BuildContext context); 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();
}
}

View file

@ -1,9 +1,28 @@
import 'dart:convert';
import 'package:fl_chart/fl_chart.dart'; import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_spinkit/flutter_spinkit.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import '../constants.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<ChartData> data = List.empty();
}
class Graphs extends StatefulWidget { class Graphs extends StatefulWidget {
const Graphs({super.key}); const Graphs({super.key});
@ -15,84 +34,233 @@ class Graphs extends StatefulWidget {
class _GraphsState extends State<Graphs> { class _GraphsState extends State<Graphs> {
String? start_date; String? start_date;
String? end_date; String? end_date;
List<CheckBoxDevice> 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return FutureBuilder<List<Category>>(
appBar: AppBar( future: Category.fetch_ids_only(),
backgroundColor: Theme.of(context).colorScheme.inversePrimary, builder: (context, AsyncSnapshot<List<Category>> categories) {
title: const Text('Graphs'), if (!categories.hasData) {
actions: [ return SpinKitWanderingCubes(
IconButton( color: Colors.deepPurple[100],
icon: const Icon(Icons.arrow_back), size: 80.0,
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));
if (pickedDate != null) { return Scaffold(
final String formattedDate = appBar: AppBar(
DateFormat('dd.MM.yyyy').format(pickedDate); backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Graphs'),
print(formattedDate); actions: [
IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () =>
Navigator.of(context).pushReplacementNamed(
'/',
))
],
),
body: Center(
child: Column(children: [
const SizedBox(
height: 40,
),
DropdownMenu<Category>(
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, dropdownMenuEntries: categories.data!
)) .map((category) => DropdownMenuEntry<Category>(
]), label: category.Name(), value: category))
Row(children: [ .toList(),
const Text('End'), ),
SizedBox( Row(
width: 200, mainAxisAlignment: MainAxisAlignment.center,
height: 40, children: devices.map((device) {
child: TextField( return SizedBox(
decoration: const InputDecoration( width: 200,
icon: Icon(Icons.calendar_today), height: 40,
labelText: "Enter Date"), child: CheckboxListTile(
readOnly: true, title: Text(device.device.device_descriptor ??
onTap: () async { device.device.device_id),
final DateTime? pickedDate = await showDatePicker( controlAffinity: ListTileControlAffinity.leading,
context: context, splashRadius: 10.0,
initialDate: DateTime.now(), value: device.enabled,
firstDate: DateTime(2023), onChanged: (bool? value) {
lastDate: DateTime(2101)); 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) { if (pickedDate != null) {
final String formattedDate = final String formattedDate =
DateFormat('dd.MM.yyyy').format(pickedDate); DateFormat('dd.MM.yyyy').format(pickedDate);
print(formattedDate); startDateController.text = formattedDate;
} }
}, },
onChanged: (text) => end_date = text, ))
)) ]),
]), Row(mainAxisAlignment: MainAxisAlignment.center, children: [
// LineChart( const Text('End'),
// LineChartData( SizedBox(
// // width: 200,
// ), height: 40,
// duration: const Duration(milliseconds: 200), child: TextField(
// curve: Curves.linear, 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<dynamic>;
final List<ChartData> chart_data = [];
for (final entry in data) {
final pair = entry as List<dynamic>;
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,
)
])));
});
} }
} }

View file

@ -75,7 +75,8 @@ class _MyHomePageState extends State<MyHomePage> {
return ExpansionPanel( return ExpansionPanel(
headerBuilder: headerBuilder:
(BuildContext context, bool isExpanded) { (BuildContext context, bool isExpanded) {
return ListTile(title: Text(widget.Name())); return ListTile(
title: Text(widget.category.Name()));
}, },
body: widget, body: widget,
isExpanded: expanded[category.key], isExpanded: expanded[category.key],

View file

@ -13,19 +13,29 @@ class PlugSettingsArguments {
class PlugSettings extends StatefulWidget { class PlugSettings extends StatefulWidget {
PlugSettings({super.key}); PlugSettings({super.key});
String? device_descriptor;
@override @override
State<PlugSettings> createState() => _PlugSettingsState(); State<PlugSettings> createState() => _PlugSettingsState();
} }
class _PlugSettingsState extends State<PlugSettings> { class _PlugSettingsState extends State<PlugSettings> {
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final args = final args =
ModalRoute.of(context)!.settings.arguments! as PlugSettingsArguments; 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( return Scaffold(
appBar: AppBar( appBar: AppBar(
@ -58,8 +68,7 @@ class _PlugSettingsState extends State<PlugSettings> {
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(fontWeight: FontWeight.bold),
), ),
TextField( TextField(
controller: TextEditingController() controller: descriptor_controller,
..text = args.device_descriptor ?? '',
decoration: InputDecoration( decoration: InputDecoration(
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: borderRadius:
@ -68,7 +77,6 @@ class _PlugSettingsState extends State<PlugSettings> {
isDense: true, isDense: true,
), ),
style: const TextStyle(height: 1.0, color: Colors.black), style: const TextStyle(height: 1.0, color: Colors.black),
onChanged: (text) => widget.device_descriptor = text,
), ),
]) ])
], ],
@ -95,7 +103,7 @@ class _PlugSettingsState extends State<PlugSettings> {
onPressed: () { onPressed: () {
http http
.post(Uri.parse( .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) { .then((response) {
if (response.statusCode != 200) { if (response.statusCode != 200) {
throw Exception("Failed to fetch devices"); throw Exception("Failed to fetch devices");