2023-10-15 07:41:35 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
|
2023-10-14 11:38:56 +00:00
|
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
|
|
import 'package:flutter/material.dart';
|
2023-10-15 07:41:35 +00:00
|
|
|
import 'package:flutter_spinkit/flutter_spinkit.dart';
|
2023-10-14 11:38:56 +00:00
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:intl/intl.dart';
|
|
|
|
|
|
|
|
import '../constants.dart';
|
2023-10-15 07:41:35 +00:00
|
|
|
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();
|
|
|
|
}
|
2023-10-14 11:38:56 +00:00
|
|
|
|
|
|
|
class Graphs extends StatefulWidget {
|
|
|
|
const Graphs({super.key});
|
|
|
|
|
|
|
|
@override
|
|
|
|
State<Graphs> createState() => _GraphsState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _GraphsState extends State<Graphs> {
|
|
|
|
String? start_date;
|
|
|
|
String? end_date;
|
2023-10-15 07:41:35 +00:00
|
|
|
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();
|
|
|
|
}
|
2023-10-14 11:38:56 +00:00
|
|
|
|
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
2023-10-15 07:41:35 +00:00
|
|
|
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,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
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();
|
|
|
|
});
|
2023-10-14 11:38:56 +00:00
|
|
|
}
|
|
|
|
},
|
2023-10-15 07:41:35 +00:00
|
|
|
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);
|
|
|
|
|
|
|
|
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,
|
|
|
|
)
|
|
|
|
])));
|
|
|
|
});
|
2023-10-14 11:38:56 +00:00
|
|
|
}
|
|
|
|
}
|