Set up thermometer

This commit is contained in:
hodasemi 2023-10-19 16:26:09 +02:00
parent 11e4a22307
commit d69610fc01
17 changed files with 312 additions and 118 deletions

View file

@ -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"

View file

@ -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"
] ]
} }

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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)));

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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();
} }

View file

@ -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(),
}); });
} }
} }

View file

@ -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,
) )
]))); ])));
}); });

View file

@ -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);
} }

View file

@ -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();

View file

@ -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)?;

View file

@ -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")

View file

@ -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
View 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(())
}
}

View file

@ -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,