Set up thermometer
This commit is contained in:
parent
11e4a22307
commit
d69610fc01
17 changed files with 312 additions and 118 deletions
|
@ -18,3 +18,4 @@ chrono = "0.4.31"
|
||||||
actix-web = "4.4.0"
|
actix-web = "4.4.0"
|
||||||
midea = { git = "https://gavania.de/hodasemi/Midea.git" }
|
midea = { git = "https://gavania.de/hodasemi/Midea.git" }
|
||||||
actix-cors = "0.6.4"
|
actix-cors = "0.6.4"
|
||||||
|
dns-lookup = "2.0.4"
|
||||||
|
|
|
@ -4,5 +4,14 @@
|
||||||
["Tasmota-Plug-2", false],
|
["Tasmota-Plug-2", false],
|
||||||
["Tasmota-Plug-3", true],
|
["Tasmota-Plug-3", true],
|
||||||
["Tasmota-Plug-4", true]
|
["Tasmota-Plug-4", true]
|
||||||
|
],
|
||||||
|
"thermostat": [
|
||||||
|
"shellytrv-8CF681A1F886",
|
||||||
|
"shellytrv-8CF681E9BAEE",
|
||||||
|
"shellytrv-B4E3F9D9E2A1"
|
||||||
|
],
|
||||||
|
"thermometer": [
|
||||||
|
"shellyplusht-d4d4da7d85b4",
|
||||||
|
"shellyplusht-80646fc9db9c"
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -30,14 +30,14 @@ class Category {
|
||||||
final Map<String, List<dynamic>> json =
|
final Map<String, List<dynamic>> json =
|
||||||
Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
||||||
|
|
||||||
for (MapEntry<String, List<dynamic>> entry in json.entries) {
|
for (final MapEntry<String, List<dynamic>> entry in json.entries) {
|
||||||
final Category category = Category(entry.key);
|
final Category category = Category(entry.key);
|
||||||
|
|
||||||
for (dynamic device_info_dyn in entry.value) {
|
for (final dynamic device_info_dyn in entry.value) {
|
||||||
final Map<String, dynamic> device_info = device_info_dyn;
|
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||||
|
|
||||||
category.devices
|
category.devices
|
||||||
.add(DeviceIdOnly(device_info['id'], device_info['desc']));
|
.add(DeviceIdOnly(deviceInfo['id'], deviceInfo['desc']));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category.devices.isNotEmpty) {
|
if (category.devices.isNotEmpty) {
|
||||||
|
@ -65,10 +65,10 @@ class Category {
|
||||||
final Category category = Category('plugs');
|
final Category category = Category('plugs');
|
||||||
final List<dynamic> plugs = json['plugs']!;
|
final List<dynamic> plugs = json['plugs']!;
|
||||||
|
|
||||||
for (dynamic device_info_dyn in plugs) {
|
for (final dynamic device_info_dyn in plugs) {
|
||||||
final Map<String, dynamic> device_info = device_info_dyn;
|
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||||
|
|
||||||
category.devices.add(await Plug.create(device_info));
|
category.devices.add(await Plug.create(deviceInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
categories.add(category);
|
categories.add(category);
|
||||||
|
@ -79,10 +79,10 @@ class Category {
|
||||||
final Category category = Category('thermostat');
|
final Category category = Category('thermostat');
|
||||||
final List<dynamic> thermostats = json['thermostat']!;
|
final List<dynamic> thermostats = json['thermostat']!;
|
||||||
|
|
||||||
for (dynamic device_info_dyn in thermostats) {
|
for (final dynamic device_info_dyn in thermostats) {
|
||||||
final Map<String, dynamic> device_info = device_info_dyn;
|
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||||
|
|
||||||
category.devices.add(await Thermostat.create(device_info));
|
category.devices.add(await Thermostat.create(deviceInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
categories.add(category);
|
categories.add(category);
|
||||||
|
@ -91,13 +91,13 @@ class Category {
|
||||||
// create temperature_and_humidity
|
// create temperature_and_humidity
|
||||||
{
|
{
|
||||||
final Category category = Category('temperature_and_humidity');
|
final Category category = Category('temperature_and_humidity');
|
||||||
final List<dynamic> temperature_and_humidities =
|
final List<dynamic> temperatureAndHumidities =
|
||||||
json['temperature_and_humidity']!;
|
json['temperature_and_humidity']!;
|
||||||
|
|
||||||
for (dynamic device_info_dyn in temperature_and_humidities) {
|
for (final dynamic device_info_dyn in temperatureAndHumidities) {
|
||||||
final Map<String, dynamic> device_info = device_info_dyn;
|
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||||
|
|
||||||
category.devices.add(await TemperatureHumidity.create(device_info));
|
category.devices.add(await TemperatureHumidity.create(deviceInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
categories.add(category);
|
categories.add(category);
|
||||||
|
@ -106,12 +106,12 @@ class Category {
|
||||||
// create dish_washer
|
// create dish_washer
|
||||||
{
|
{
|
||||||
final Category category = Category('dish_washer');
|
final Category category = Category('dish_washer');
|
||||||
final List<dynamic> dish_washer = json['dish_washer']!;
|
final List<dynamic> dishWasher = json['dish_washer']!;
|
||||||
|
|
||||||
for (dynamic device_info_dyn in dish_washer) {
|
for (final dynamic device_info_dyn in dishWasher) {
|
||||||
final Map<String, dynamic> device_info = device_info_dyn;
|
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||||
|
|
||||||
category.devices.add(await DishWasher.create(device_info));
|
category.devices.add(await DishWasher.create(deviceInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
categories.add(category);
|
categories.add(category);
|
||||||
|
@ -120,12 +120,12 @@ class Category {
|
||||||
// create washing_machines
|
// create washing_machines
|
||||||
{
|
{
|
||||||
final Category category = Category('washing_machines');
|
final Category category = Category('washing_machines');
|
||||||
final List<dynamic> washing_machines = json['washing_machines']!;
|
final List<dynamic> washingMachines = json['washing_machines']!;
|
||||||
|
|
||||||
for (dynamic device_info_dyn in washing_machines) {
|
for (final dynamic device_info_dyn in washingMachines) {
|
||||||
final Map<String, dynamic> device_info = device_info_dyn;
|
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||||
|
|
||||||
category.devices.add(await WashingMachine.create(device_info));
|
category.devices.add(await WashingMachine.create(deviceInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
categories.add(category);
|
categories.add(category);
|
||||||
|
@ -143,9 +143,9 @@ class Category {
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryWidget extends StatelessWidget {
|
class CategoryWidget extends StatelessWidget {
|
||||||
final Category category;
|
|
||||||
|
|
||||||
CategoryWidget({super.key, required this.category});
|
const CategoryWidget({super.key, required this.category});
|
||||||
|
final Category category;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -169,6 +169,7 @@ class DeviceIdOnly extends Device {
|
||||||
final String device_id;
|
final String device_id;
|
||||||
final String? device_descriptor;
|
final String? device_descriptor;
|
||||||
|
|
||||||
|
@override
|
||||||
Widget create_widget(BuildContext context) {
|
Widget create_widget(BuildContext context) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart';
|
||||||
import 'devices.dart';
|
import 'devices.dart';
|
||||||
|
|
||||||
class DishWasher extends Device {
|
class DishWasher extends Device {
|
||||||
static Future<DishWasher> create(Map<String, dynamic> device_info) async {
|
static Future<DishWasher> create(Map<String, dynamic> deviceInfo) async {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,9 @@ import '../states/plug_settings.dart';
|
||||||
import 'devices.dart';
|
import 'devices.dart';
|
||||||
|
|
||||||
class Plug extends Device {
|
class Plug extends Device {
|
||||||
|
|
||||||
|
Plug(this.device_id, this.device_descriptor, this.led_state, this.power_state,
|
||||||
|
this.power_draw, this.power_control);
|
||||||
final String device_id;
|
final String device_id;
|
||||||
String? device_descriptor;
|
String? device_descriptor;
|
||||||
bool led_state;
|
bool led_state;
|
||||||
|
@ -16,40 +19,37 @@ class Plug extends Device {
|
||||||
double power_draw;
|
double power_draw;
|
||||||
bool power_control;
|
bool power_control;
|
||||||
|
|
||||||
static Future<Plug> create(Map<String, dynamic> device_info) async {
|
static Future<Plug> create(Map<String, dynamic> deviceInfo) async {
|
||||||
final String device_id = device_info['id'];
|
final String deviceId = deviceInfo['id'];
|
||||||
final String? device_descriptor = device_info['desc'];
|
final String? deviceDescriptor = deviceInfo['desc'];
|
||||||
final bool power_control = device_info['toggle'];
|
final bool powerControl = deviceInfo['toggle'];
|
||||||
|
|
||||||
final response = await http
|
final response = await http
|
||||||
.get(Uri.parse("${Constants.BASE_URL}/plug_state/$device_id"));
|
.get(Uri.parse("${Constants.BASE_URL}/plug_state/$deviceId"));
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception("Failed to fetch plug_state for $device_id");
|
throw Exception("Failed to fetch plug_state for $deviceId");
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, dynamic> device_state =
|
final Map<String, dynamic> deviceState =
|
||||||
Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
||||||
|
|
||||||
return Plug(
|
return Plug(
|
||||||
device_id,
|
deviceId,
|
||||||
device_descriptor,
|
deviceDescriptor,
|
||||||
device_state["led"],
|
deviceState["led"],
|
||||||
device_state["power"],
|
deviceState["power"],
|
||||||
device_state["power_draw"],
|
deviceState["power_draw"],
|
||||||
power_control && device_state["power_draw"] < 15,
|
powerControl && deviceState["power_draw"] < 15,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Plug(this.device_id, this.device_descriptor, this.led_state, this.power_state,
|
|
||||||
this.power_draw, this.power_control);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget create_widget(BuildContext context) {
|
Widget create_widget(BuildContext context) {
|
||||||
const double header_height = 40;
|
const double headerHeight = 40;
|
||||||
const double info_height = 30;
|
const double infoHeight = 30;
|
||||||
const double info_width = 60;
|
const double infoWidth = 60;
|
||||||
const double x_offset = 0.9;
|
const double xOffset = 0.9;
|
||||||
|
|
||||||
return Table(
|
return Table(
|
||||||
border: TableBorder(
|
border: TableBorder(
|
||||||
|
@ -67,7 +67,7 @@ class Plug extends Device {
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: header_height,
|
height: headerHeight,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Align(
|
Align(
|
||||||
|
@ -92,7 +92,7 @@ class Plug extends Device {
|
||||||
]),
|
]),
|
||||||
TableRow(children: [
|
TableRow(children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: info_height,
|
height: infoHeight,
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
const Align(
|
const Align(
|
||||||
alignment: Alignment(-0.9, 0.0),
|
alignment: Alignment(-0.9, 0.0),
|
||||||
|
@ -101,16 +101,16 @@ class Plug extends Device {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
"LED")),
|
"LED")),
|
||||||
Align(
|
Align(
|
||||||
alignment: const Alignment(x_offset, 0.0),
|
alignment: const Alignment(xOffset, 0.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: info_width,
|
width: infoWidth,
|
||||||
child: PlugLed(
|
child: PlugLed(
|
||||||
device_id: device_id, led_state: led_state))),
|
device_id: device_id, led_state: led_state))),
|
||||||
]))
|
]))
|
||||||
]),
|
]),
|
||||||
TableRow(children: [
|
TableRow(children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: info_height,
|
height: infoHeight,
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
const Align(
|
const Align(
|
||||||
alignment: Alignment(-0.9, 0.0),
|
alignment: Alignment(-0.9, 0.0),
|
||||||
|
@ -119,9 +119,9 @@ class Plug extends Device {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
"Power")),
|
"Power")),
|
||||||
Align(
|
Align(
|
||||||
alignment: const Alignment(x_offset, 0.0),
|
alignment: const Alignment(xOffset, 0.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: info_width,
|
width: infoWidth,
|
||||||
child: PlugPower(
|
child: PlugPower(
|
||||||
device_id: device_id,
|
device_id: device_id,
|
||||||
power_state: power_state,
|
power_state: power_state,
|
||||||
|
@ -130,7 +130,7 @@ class Plug extends Device {
|
||||||
]),
|
]),
|
||||||
TableRow(children: [
|
TableRow(children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: info_height,
|
height: infoHeight,
|
||||||
child: Stack(children: [
|
child: Stack(children: [
|
||||||
const Align(
|
const Align(
|
||||||
alignment: Alignment(-0.9, 0.0),
|
alignment: Alignment(-0.9, 0.0),
|
||||||
|
@ -139,9 +139,9 @@ class Plug extends Device {
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
"Power Draw")),
|
"Power Draw")),
|
||||||
Align(
|
Align(
|
||||||
alignment: const Alignment(x_offset, 0.0),
|
alignment: const Alignment(xOffset, 0.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: info_width,
|
width: infoWidth,
|
||||||
child: Text(
|
child: Text(
|
||||||
textAlign: TextAlign.center, "$power_draw W")))
|
textAlign: TextAlign.center, "$power_draw W")))
|
||||||
]))
|
]))
|
||||||
|
@ -151,10 +151,10 @@ class Plug extends Device {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlugLed extends StatefulWidget {
|
class PlugLed extends StatefulWidget {
|
||||||
final String device_id;
|
|
||||||
bool led_state;
|
|
||||||
|
|
||||||
PlugLed({super.key, required this.device_id, required this.led_state});
|
PlugLed({super.key, required this.device_id, required this.led_state});
|
||||||
|
final String device_id;
|
||||||
|
bool led_state;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlugLed> createState() => PlugLedState();
|
State<PlugLed> createState() => PlugLedState();
|
||||||
|
@ -164,17 +164,17 @@ class PlugLedState extends State<PlugLed> {
|
||||||
String _led_state_info = "";
|
String _led_state_info = "";
|
||||||
|
|
||||||
void _toggle_led_state() {
|
void _toggle_led_state() {
|
||||||
String target_state;
|
String targetState;
|
||||||
|
|
||||||
if (widget.led_state) {
|
if (widget.led_state) {
|
||||||
target_state = "off";
|
targetState = "off";
|
||||||
} else {
|
} else {
|
||||||
target_state = "on";
|
targetState = "on";
|
||||||
}
|
}
|
||||||
|
|
||||||
change_plug_state(widget.device_id, "led", target_state)
|
change_plug_state(widget.device_id, "led", targetState)
|
||||||
.then((device_state) {
|
.then((deviceState) {
|
||||||
widget.led_state = device_state["led"];
|
widget.led_state = deviceState["led"];
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_led_state_info = widget.led_state ? "On" : "Off";
|
_led_state_info = widget.led_state ? "On" : "Off";
|
||||||
|
@ -193,15 +193,15 @@ class PlugLedState extends State<PlugLed> {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlugPower extends StatefulWidget {
|
class PlugPower extends StatefulWidget {
|
||||||
final String device_id;
|
|
||||||
bool power_state;
|
|
||||||
bool power_control;
|
|
||||||
|
|
||||||
PlugPower(
|
PlugPower(
|
||||||
{super.key,
|
{super.key,
|
||||||
required this.device_id,
|
required this.device_id,
|
||||||
required this.power_state,
|
required this.power_state,
|
||||||
required this.power_control});
|
required this.power_control});
|
||||||
|
final String device_id;
|
||||||
|
bool power_state;
|
||||||
|
bool power_control;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlugPower> createState() => PlugPowerState();
|
State<PlugPower> createState() => PlugPowerState();
|
||||||
|
@ -211,19 +211,19 @@ class PlugPowerState extends State<PlugPower> {
|
||||||
String _power_state_info = "";
|
String _power_state_info = "";
|
||||||
|
|
||||||
void _toggle_power_state() {
|
void _toggle_power_state() {
|
||||||
String target_state;
|
String targetState;
|
||||||
|
|
||||||
if (widget.power_state) {
|
if (widget.power_state) {
|
||||||
target_state = "off";
|
targetState = "off";
|
||||||
} else {
|
} else {
|
||||||
target_state = "on";
|
targetState = "on";
|
||||||
}
|
}
|
||||||
|
|
||||||
change_plug_state(widget.device_id, "power", target_state)
|
change_plug_state(widget.device_id, "power", targetState)
|
||||||
.then((device_state) {
|
.then((deviceState) {
|
||||||
widget.power_state = device_state["power"];
|
widget.power_state = deviceState["power"];
|
||||||
|
|
||||||
widget.power_control = device_state["power_draw"] < 15;
|
widget.power_control = deviceState["power_draw"] < 15;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_power_state_info = widget.power_state ? "On" : "Off";
|
_power_state_info = widget.power_state ? "On" : "Off";
|
||||||
|
@ -246,19 +246,19 @@ class PlugPowerState extends State<PlugPower> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> change_plug_state(
|
Future<Map<String, dynamic>> change_plug_state(
|
||||||
String device_id, String module, String state) async {
|
String deviceId, String module, String state) async {
|
||||||
var response = await http.post(
|
var response = await http.post(
|
||||||
Uri.parse("${Constants.BASE_URL}/plug/$device_id/${module}_$state"));
|
Uri.parse("${Constants.BASE_URL}/plug/$deviceId/${module}_$state"));
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception("Failed to post new state");
|
throw Exception("Failed to post new state");
|
||||||
}
|
}
|
||||||
|
|
||||||
response =
|
response =
|
||||||
await http.get(Uri.parse("${Constants.BASE_URL}/plug_state/$device_id"));
|
await http.get(Uri.parse("${Constants.BASE_URL}/plug_state/$deviceId"));
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception("Failed to fetch plug_state for $device_id");
|
throw Exception("Failed to fetch plug_state for $deviceId");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
return Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'devices.dart';
|
||||||
|
|
||||||
class TemperatureHumidity extends Device {
|
class TemperatureHumidity extends Device {
|
||||||
static Future<TemperatureHumidity> create(
|
static Future<TemperatureHumidity> create(
|
||||||
Map<String, dynamic> device_info) async {
|
Map<String, dynamic> deviceInfo) async {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart';
|
||||||
import 'devices.dart';
|
import 'devices.dart';
|
||||||
|
|
||||||
class Thermostat extends Device {
|
class Thermostat extends Device {
|
||||||
static Future<Thermostat> create(Map<String, dynamic> device_info) async {
|
static Future<Thermostat> create(Map<String, dynamic> deviceInfo) async {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart';
|
||||||
import 'devices.dart';
|
import 'devices.dart';
|
||||||
|
|
||||||
class WashingMachine extends Device {
|
class WashingMachine extends Device {
|
||||||
static Future<WashingMachine> create(Map<String, dynamic> device_info) async {
|
static Future<WashingMachine> create(Map<String, dynamic> deviceInfo) async {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,9 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'constants.dart';
|
import 'constants.dart';
|
||||||
|
import 'states/graphs.dart';
|
||||||
import 'states/home_page.dart';
|
import 'states/home_page.dart';
|
||||||
import 'states/plug_settings.dart';
|
import 'states/plug_settings.dart';
|
||||||
import 'states/graphs.dart';
|
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
|
@ -24,8 +23,8 @@ class MyApp extends StatelessWidget {
|
||||||
),
|
),
|
||||||
home: const MyHomePage(title: 'Home Server'),
|
home: const MyHomePage(title: 'Home Server'),
|
||||||
routes: <String, WidgetBuilder>{
|
routes: <String, WidgetBuilder>{
|
||||||
'/plug_settings': (BuildContext context) => PlugSettings(),
|
'/plug_settings': (BuildContext context) => const PlugSettings(),
|
||||||
'/graphs': (BuildContext context) => Graphs(),
|
'/graphs': (BuildContext context) => const Graphs(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,45 +173,45 @@ class _GraphsState extends State<Graphs> {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (var device in devices) {
|
for (final device in devices) {
|
||||||
if (!device.enabled) {
|
if (!device.enabled) {
|
||||||
device.data = List.empty();
|
device.data = List.empty();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int start_date;
|
int startDate;
|
||||||
int end_date;
|
int endDate;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
start_date = (DateFormat('dd.MM.yyyy')
|
startDate = (DateFormat('dd.MM.yyyy')
|
||||||
.parse(startDateController.text)
|
.parse(startDateController.text)
|
||||||
.millisecondsSinceEpoch /
|
.millisecondsSinceEpoch /
|
||||||
1000) as int;
|
1000) as int;
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
start_date = 0;
|
startDate = 0;
|
||||||
print('no start date set');
|
print('no start date set');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
end_date = (DateFormat('dd.MM.yyyy')
|
endDate = (DateFormat('dd.MM.yyyy')
|
||||||
.parse(endDateController.text)
|
.parse(endDateController.text)
|
||||||
.millisecondsSinceEpoch /
|
.millisecondsSinceEpoch /
|
||||||
1000) as int;
|
1000) as int;
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
end_date = 0;
|
endDate = 0;
|
||||||
print('no end date set');
|
print('no end date set');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start_date == 0 || end_date == 0) {
|
if (startDate == 0 || endDate == 0) {
|
||||||
device.data = List.empty();
|
device.data = List.empty();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const String filter_type = 'hourly';
|
const String filterType = 'hourly';
|
||||||
|
|
||||||
http
|
http
|
||||||
.get(Uri.parse(
|
.get(Uri.parse(
|
||||||
"${Constants.BASE_URL}/plug_data/${device.device.device_id}/$start_date/$end_date/$filter_type"))
|
"${Constants.BASE_URL}/plug_data/${device.device.device_id}/$startDate/$endDate/$filterType"))
|
||||||
.then((response) {
|
.then((response) {
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
throw Exception("Failed to fetch data");
|
throw Exception("Failed to fetch data");
|
||||||
|
@ -220,14 +220,14 @@ class _GraphsState extends State<Graphs> {
|
||||||
final data = jsonDecode(jsonDecode(response.body))
|
final data = jsonDecode(jsonDecode(response.body))
|
||||||
as List<dynamic>;
|
as List<dynamic>;
|
||||||
|
|
||||||
final List<ChartData> chart_data = [];
|
final List<ChartData> chartData = [];
|
||||||
|
|
||||||
for (final entry in data) {
|
for (final entry in data) {
|
||||||
final pair = entry as List<dynamic>;
|
final pair = entry as List<dynamic>;
|
||||||
chart_data.add(ChartData(pair[0], pair[1]));
|
chartData.add(ChartData(pair[0], pair[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
device.data = chart_data;
|
device.data = chartData;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -258,7 +258,6 @@ class _GraphsState extends State<Graphs> {
|
||||||
// titlesData: FlTitlesData(bottomTitles: AxisTitles(sideTitles: ))
|
// titlesData: FlTitlesData(bottomTitles: AxisTitles(sideTitles: ))
|
||||||
),
|
),
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
curve: Curves.linear,
|
|
||||||
)
|
)
|
||||||
])));
|
])));
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,15 +28,15 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = categories.data!;
|
final data = categories.data!;
|
||||||
final category_count = data.length;
|
final categoryCount = data.length;
|
||||||
|
|
||||||
if (category_count > expanded.length) {
|
if (categoryCount > expanded.length) {
|
||||||
final int diff = category_count - expanded.length;
|
final int diff = categoryCount - expanded.length;
|
||||||
|
|
||||||
final List<bool> diff_list = List<bool>.filled(diff, true);
|
final List<bool> diffList = List<bool>.filled(diff, true);
|
||||||
expanded.addAll(diff_list);
|
expanded.addAll(diffList);
|
||||||
} else if (category_count < expanded.length) {
|
} else if (categoryCount < expanded.length) {
|
||||||
final int diff = expanded.length - category_count;
|
final int diff = expanded.length - categoryCount;
|
||||||
|
|
||||||
expanded = List<bool>.filled(diff, false);
|
expanded = List<bool>.filled(diff, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ class PlugSettingsArguments {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlugSettings extends StatefulWidget {
|
class PlugSettings extends StatefulWidget {
|
||||||
PlugSettings({super.key});
|
const PlugSettings({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlugSettings> createState() => _PlugSettingsState();
|
State<PlugSettings> createState() => _PlugSettingsState();
|
||||||
|
|
78
src/db.rs
78
src/db.rs
|
@ -47,7 +47,8 @@ impl DataBase {
|
||||||
"CREATE TABLE IF NOT EXISTS data (
|
"CREATE TABLE IF NOT EXISTS data (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
time BIGINT NOT NULL,
|
time BIGINT NOT NULL,
|
||||||
watts REAL NOT NULL,
|
name VARCHAR(30) NOT NULL,
|
||||||
|
value REAL NOT NULL,
|
||||||
device_id INTEGER NOT NULL,
|
device_id INTEGER NOT NULL,
|
||||||
FOREIGN KEY(device_id) REFERENCES devices(id)
|
FOREIGN KEY(device_id) REFERENCES devices(id)
|
||||||
)",
|
)",
|
||||||
|
@ -116,16 +117,50 @@ impl DataBase {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for device in devices.thermostat.iter() {
|
||||||
|
self.sql.execute(
|
||||||
|
&format!(
|
||||||
|
"INSERT INTO devices (device, type, control)
|
||||||
|
SELECT \"{device}\", \"thermostat\", false
|
||||||
|
WHERE
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT device
|
||||||
|
FROM devices
|
||||||
|
WHERE device=\"{device}\"
|
||||||
|
)
|
||||||
|
"
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for device in devices.thermometer.iter() {
|
||||||
|
self.sql.execute(
|
||||||
|
&format!(
|
||||||
|
"INSERT INTO devices (device, type, control)
|
||||||
|
SELECT \"{device}\", \"thermometer\", false
|
||||||
|
WHERE
|
||||||
|
NOT EXISTS (
|
||||||
|
SELECT device
|
||||||
|
FROM devices
|
||||||
|
WHERE device=\"{device}\"
|
||||||
|
)
|
||||||
|
"
|
||||||
|
),
|
||||||
|
[],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&self, device_name: &str, time: u64, watts: f32) -> Result<()> {
|
pub fn write(&self, device_name: &str, time: u64, name: &str, value: f32) -> Result<()> {
|
||||||
let params: &[&dyn ToSql] = &[&time, &watts];
|
let params: &[&dyn ToSql] = &[&time, &value];
|
||||||
|
|
||||||
self.sql.execute(
|
self.sql.execute(
|
||||||
&format!(
|
&format!(
|
||||||
"INSERT INTO data (time, watts, device_id)
|
"INSERT INTO data (time, name, value, device_id)
|
||||||
VALUES (?1, ?2, (SELECT id FROM devices WHERE device=\"{device_name}\") )"
|
VALUES (?1, \"{name}\", ?2, (SELECT id FROM devices WHERE device=\"{device_name}\") )"
|
||||||
),
|
),
|
||||||
params,
|
params,
|
||||||
)?;
|
)?;
|
||||||
|
@ -156,6 +191,16 @@ impl DataBase {
|
||||||
desc: name,
|
desc: name,
|
||||||
toggle: control != 0,
|
toggle: control != 0,
|
||||||
}),
|
}),
|
||||||
|
"thermostat" => devices.thermostat.push(DeviceWithName {
|
||||||
|
id: device,
|
||||||
|
desc: name,
|
||||||
|
toggle: control != 0,
|
||||||
|
}),
|
||||||
|
"thermometer" => devices.temperature_and_humidity.push(DeviceWithName {
|
||||||
|
id: device,
|
||||||
|
desc: name,
|
||||||
|
toggle: control != 0,
|
||||||
|
}),
|
||||||
|
|
||||||
_ => panic!(),
|
_ => panic!(),
|
||||||
}
|
}
|
||||||
|
@ -164,6 +209,19 @@ impl DataBase {
|
||||||
Ok(devices)
|
Ok(devices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn device_exists(&self, device_id: &str) -> Result<bool> {
|
||||||
|
Ok(self
|
||||||
|
.sql
|
||||||
|
.prepare(&format!(
|
||||||
|
"
|
||||||
|
SELECT *
|
||||||
|
FROM devices
|
||||||
|
WHERE device=\"{device_id}\"
|
||||||
|
"
|
||||||
|
))?
|
||||||
|
.exists([])?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn change_device_name(&self, device: &str, description: &str) -> Result<()> {
|
pub fn change_device_name(&self, device: &str, description: &str) -> Result<()> {
|
||||||
self.sql.execute(
|
self.sql.execute(
|
||||||
&format!(
|
&format!(
|
||||||
|
@ -182,7 +240,7 @@ impl DataBase {
|
||||||
pub fn read(&self, device: &str) -> Result<Vec<(u64, f32)>> {
|
pub fn read(&self, device: &str) -> Result<Vec<(u64, f32)>> {
|
||||||
self._read(&format!(
|
self._read(&format!(
|
||||||
"
|
"
|
||||||
SELECT data.time, data.watts
|
SELECT data.time, data.value
|
||||||
FROM data
|
FROM data
|
||||||
INNER JOIN devices
|
INNER JOIN devices
|
||||||
ON data.device_id=devices.id
|
ON data.device_id=devices.id
|
||||||
|
@ -194,7 +252,7 @@ impl DataBase {
|
||||||
pub fn read_range(&self, device: &str, start: u64, end: u64) -> Result<Vec<(u64, f32)>> {
|
pub fn read_range(&self, device: &str, start: u64, end: u64) -> Result<Vec<(u64, f32)>> {
|
||||||
self._read(&format!(
|
self._read(&format!(
|
||||||
"
|
"
|
||||||
SELECT data.time, data.watts
|
SELECT data.time, data.value
|
||||||
FROM data
|
FROM data
|
||||||
INNER JOIN devices
|
INNER JOIN devices
|
||||||
ON data.device_id=devices.id
|
ON data.device_id=devices.id
|
||||||
|
@ -293,6 +351,8 @@ mod test {
|
||||||
|
|
||||||
db.register_devices(&Devices {
|
db.register_devices(&Devices {
|
||||||
plugs: vec![("test".to_string(), true)],
|
plugs: vec![("test".to_string(), true)],
|
||||||
|
thermostat: Vec::new(),
|
||||||
|
thermometer: Vec::new(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
fs::remove_file("startup_test.db")?;
|
fs::remove_file("startup_test.db")?;
|
||||||
|
@ -308,9 +368,11 @@ mod test {
|
||||||
|
|
||||||
db.register_devices(&Devices {
|
db.register_devices(&Devices {
|
||||||
plugs: vec![(device_name.to_string(), true)],
|
plugs: vec![(device_name.to_string(), true)],
|
||||||
|
thermostat: Vec::new(),
|
||||||
|
thermometer: Vec::new(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
db.write(device_name, 0, 5.5)?;
|
db.write(device_name, 0, "watts", 5.5)?;
|
||||||
|
|
||||||
let device_descriptor = "udo";
|
let device_descriptor = "udo";
|
||||||
db.change_device_name(device_name, device_descriptor)?;
|
db.change_device_name(device_name, device_descriptor)?;
|
||||||
|
|
|
@ -7,6 +7,8 @@ use serde_json::{from_str, to_string, to_string_pretty};
|
||||||
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Debug)]
|
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Debug)]
|
||||||
pub struct Devices {
|
pub struct Devices {
|
||||||
pub plugs: Vec<(String, bool)>,
|
pub plugs: Vec<(String, bool)>,
|
||||||
|
pub thermostat: Vec<String>,
|
||||||
|
pub thermometer: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Devices {
|
impl Devices {
|
||||||
|
@ -55,6 +57,8 @@ mod test {
|
||||||
fn create_conf() -> Result<()> {
|
fn create_conf() -> Result<()> {
|
||||||
let devices = Devices {
|
let devices = Devices {
|
||||||
plugs: vec![("Dev1".to_string(), true), ("Dev2".to_string(), false)],
|
plugs: vec![("Dev1".to_string(), true), ("Dev2".to_string(), false)],
|
||||||
|
thermostat: Vec::new(),
|
||||||
|
thermometer: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
devices.save("test_devices.conf")
|
devices.save("test_devices.conf")
|
||||||
|
|
|
@ -12,6 +12,7 @@ mod db;
|
||||||
mod devices;
|
mod devices;
|
||||||
mod midea_helper;
|
mod midea_helper;
|
||||||
mod tasmota;
|
mod tasmota;
|
||||||
|
mod temperature;
|
||||||
mod tibber_handler;
|
mod tibber_handler;
|
||||||
mod web_server;
|
mod web_server;
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ fn read_power_usage(
|
||||||
if let Ok(usage) = plug.read_power_usage().await {
|
if let Ok(usage) = plug.read_power_usage().await {
|
||||||
db.lock()
|
db.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write(plug.name(), since_epoch()?, usage)?;
|
.write(plug.name(), since_epoch()?, "watts", usage)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok::<(), anyhow::Error>(())
|
||||||
|
@ -81,6 +82,8 @@ async fn run_web_server(
|
||||||
.service(change_device_name)
|
.service(change_device_name)
|
||||||
.service(plug_data)
|
.service(plug_data)
|
||||||
.service(plug_data_range)
|
.service(plug_data_range)
|
||||||
|
.service(push_temperature)
|
||||||
|
.service(push_humidity)
|
||||||
})
|
})
|
||||||
.bind((IP, PORT))
|
.bind((IP, PORT))
|
||||||
.map_err(|err| anyhow::Error::msg(format!("failed binding to address: {err:#?}")))?
|
.map_err(|err| anyhow::Error::msg(format!("failed binding to address: {err:#?}")))?
|
||||||
|
|
76
src/temperature.rs
Normal file
76
src/temperature.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use std::{
|
||||||
|
net::IpAddr,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use actix_web::web::Data;
|
||||||
|
use anyhow::{bail, Result};
|
||||||
|
use dns_lookup::{lookup_addr, lookup_host};
|
||||||
|
use reqwest::Client;
|
||||||
|
|
||||||
|
use crate::{db::DataBase, since_epoch};
|
||||||
|
|
||||||
|
pub struct Thermostat {
|
||||||
|
device: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Thermostat {
|
||||||
|
pub fn new(device: impl ToString) -> Self {
|
||||||
|
Self {
|
||||||
|
device: device.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn set_temperature(&self, temperature: f32) -> Result<()> {
|
||||||
|
let ips = lookup_host(&self.device)?;
|
||||||
|
|
||||||
|
if ips.is_empty() {
|
||||||
|
bail!("could not resolve device name {}", self.device);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = Client::new()
|
||||||
|
.post(format!("http://{}/ext_t?temp={}", ips[0], temperature))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
bail!("response error");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ThermometerChange {
|
||||||
|
Temperature(f32),
|
||||||
|
Humidity(f32),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Thermometer;
|
||||||
|
|
||||||
|
impl Thermometer {
|
||||||
|
pub fn push_change(
|
||||||
|
change: ThermometerChange,
|
||||||
|
ip: IpAddr,
|
||||||
|
db: Data<Arc<Mutex<DataBase>>>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let db_lock = db.lock().unwrap();
|
||||||
|
let device_id = lookup_addr(&ip)?.trim_end_matches(".fritz.box").to_string();
|
||||||
|
|
||||||
|
if db_lock.device_exists(&device_id)? {
|
||||||
|
match change {
|
||||||
|
ThermometerChange::Temperature(temp) => {
|
||||||
|
db_lock.write(&device_id, since_epoch()?, "temperature", temp)?;
|
||||||
|
|
||||||
|
// maybe push to thermostate
|
||||||
|
}
|
||||||
|
ThermometerChange::Humidity(humid) => {
|
||||||
|
db_lock.write(&device_id, since_epoch()?, "humidity", humid)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,13 +1,17 @@
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
get, post,
|
get, post,
|
||||||
web::{Data, Json, Path},
|
web::{Data, Json, Path},
|
||||||
Error, Responder, ResponseError,
|
Error, HttpRequest, Responder, ResponseError,
|
||||||
};
|
};
|
||||||
use chrono::{Datelike, NaiveDateTime, Timelike};
|
use chrono::{Datelike, NaiveDateTime, Timelike};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::to_string;
|
use serde_json::to_string;
|
||||||
|
|
||||||
use crate::{db::DataBase, tasmota::Tasmota};
|
use crate::{
|
||||||
|
db::DataBase,
|
||||||
|
tasmota::Tasmota,
|
||||||
|
temperature::{Thermometer, ThermometerChange},
|
||||||
|
};
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
|
@ -226,6 +230,42 @@ async fn plug_data_range(
|
||||||
)?))
|
)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[get("/push_temp/{temperature}")]
|
||||||
|
async fn push_temperature(
|
||||||
|
param: Path<f32>,
|
||||||
|
req: HttpRequest,
|
||||||
|
db: Data<Arc<Mutex<DataBase>>>,
|
||||||
|
) -> Result<impl Responder, Error> {
|
||||||
|
if let Some(val) = req.peer_addr() {
|
||||||
|
Thermometer::push_change(
|
||||||
|
ThermometerChange::Temperature(param.into_inner()),
|
||||||
|
val.ip(),
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
.map_err(|err| MyError::from(err))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok("Ok")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/push_humid/{humidity}")]
|
||||||
|
async fn push_humidity(
|
||||||
|
param: Path<f32>,
|
||||||
|
req: HttpRequest,
|
||||||
|
db: Data<Arc<Mutex<DataBase>>>,
|
||||||
|
) -> Result<impl Responder, Error> {
|
||||||
|
if let Some(val) = req.peer_addr() {
|
||||||
|
Thermometer::push_change(
|
||||||
|
ThermometerChange::Humidity(param.into_inner()),
|
||||||
|
val.ip(),
|
||||||
|
db,
|
||||||
|
)
|
||||||
|
.map_err(|err| MyError::from(err))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok("Ok")
|
||||||
|
}
|
||||||
|
|
||||||
fn collapse_data<F>(data: Vec<(u64, f32)>, f: F) -> Vec<(u64, f32)>
|
fn collapse_data<F>(data: Vec<(u64, f32)>, f: F) -> Vec<(u64, f32)>
|
||||||
where
|
where
|
||||||
F: Fn(NaiveDateTime) -> NaiveDateTime,
|
F: Fn(NaiveDateTime) -> NaiveDateTime,
|
||||||
|
|
Loading…
Reference in a new issue