Prepare graph rendering
This commit is contained in:
parent
39f52dd9af
commit
31778307bc
4 changed files with 304 additions and 86 deletions
|
@ -18,6 +18,36 @@ class Category {
|
|||
final String name;
|
||||
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 {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<ChartData> data = List.empty();
|
||||
}
|
||||
|
||||
class Graphs extends StatefulWidget {
|
||||
const Graphs({super.key});
|
||||
|
@ -15,84 +34,233 @@ class Graphs extends StatefulWidget {
|
|||
class _GraphsState extends State<Graphs> {
|
||||
String? start_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
|
||||
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<List<Category>>(
|
||||
future: Category.fetch_ids_only(),
|
||||
builder: (context, AsyncSnapshot<List<Category>> 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<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,
|
||||
))
|
||||
]),
|
||||
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<Category>(
|
||||
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<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,
|
||||
)
|
||||
])));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,7 +75,8 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
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],
|
||||
|
|
|
@ -13,19 +13,29 @@ class PlugSettingsArguments {
|
|||
class PlugSettings extends StatefulWidget {
|
||||
PlugSettings({super.key});
|
||||
|
||||
String? device_descriptor;
|
||||
|
||||
@override
|
||||
State<PlugSettings> createState() => _PlugSettingsState();
|
||||
}
|
||||
|
||||
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
|
||||
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<PlugSettings> {
|
|||
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<PlugSettings> {
|
|||
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<PlugSettings> {
|
|||
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");
|
||||
|
|
Loading…
Reference in a new issue