From d69610fc0126a53773aa8594fa2a07f11f292151 Mon Sep 17 00:00:00 2001 From: hodasemi Date: Thu, 19 Oct 2023 16:26:09 +0200 Subject: [PATCH] Set up thermometer --- Cargo.toml | 1 + devices.conf | 9 ++ frontend/lib/devices/devices.dart | 49 +++++----- frontend/lib/devices/dish_washer.dart | 2 +- frontend/lib/devices/plug.dart | 104 +++++++++++----------- frontend/lib/devices/temp_humid.dart | 2 +- frontend/lib/devices/thermostat.dart | 2 +- frontend/lib/devices/washing_machine.dart | 2 +- frontend/lib/main.dart | 7 +- frontend/lib/states/graphs.dart | 27 +++--- frontend/lib/states/home_page.dart | 14 +-- frontend/lib/states/plug_settings.dart | 2 +- src/db.rs | 80 +++++++++++++++-- src/devices.rs | 4 + src/main.rs | 5 +- src/temperature.rs | 76 ++++++++++++++++ src/web_server.rs | 44 ++++++++- 17 files changed, 312 insertions(+), 118 deletions(-) create mode 100644 src/temperature.rs diff --git a/Cargo.toml b/Cargo.toml index c07f383..143542b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,3 +18,4 @@ chrono = "0.4.31" actix-web = "4.4.0" midea = { git = "https://gavania.de/hodasemi/Midea.git" } actix-cors = "0.6.4" +dns-lookup = "2.0.4" diff --git a/devices.conf b/devices.conf index 8be4159..f093f26 100644 --- a/devices.conf +++ b/devices.conf @@ -4,5 +4,14 @@ ["Tasmota-Plug-2", false], ["Tasmota-Plug-3", true], ["Tasmota-Plug-4", true] + ], + "thermostat": [ + "shellytrv-8CF681A1F886", + "shellytrv-8CF681E9BAEE", + "shellytrv-B4E3F9D9E2A1" + ], + "thermometer": [ + "shellyplusht-d4d4da7d85b4", + "shellyplusht-80646fc9db9c" ] } \ No newline at end of file diff --git a/frontend/lib/devices/devices.dart b/frontend/lib/devices/devices.dart index 0aa03df..64fa692 100644 --- a/frontend/lib/devices/devices.dart +++ b/frontend/lib/devices/devices.dart @@ -30,14 +30,14 @@ class Category { final Map> json = Map.castFrom(jsonDecode(jsonDecode(response.body))); - for (MapEntry> entry in json.entries) { + for (final MapEntry> entry in json.entries) { final Category category = Category(entry.key); - for (dynamic device_info_dyn in entry.value) { - final Map device_info = device_info_dyn; + for (final dynamic device_info_dyn in entry.value) { + final Map deviceInfo = device_info_dyn; category.devices - .add(DeviceIdOnly(device_info['id'], device_info['desc'])); + .add(DeviceIdOnly(deviceInfo['id'], deviceInfo['desc'])); } if (category.devices.isNotEmpty) { @@ -65,10 +65,10 @@ class Category { final Category category = Category('plugs'); final List plugs = json['plugs']!; - for (dynamic device_info_dyn in plugs) { - final Map device_info = device_info_dyn; + for (final dynamic device_info_dyn in plugs) { + final Map deviceInfo = device_info_dyn; - category.devices.add(await Plug.create(device_info)); + category.devices.add(await Plug.create(deviceInfo)); } categories.add(category); @@ -79,10 +79,10 @@ class Category { final Category category = Category('thermostat'); final List thermostats = json['thermostat']!; - for (dynamic device_info_dyn in thermostats) { - final Map device_info = device_info_dyn; + for (final dynamic device_info_dyn in thermostats) { + final Map deviceInfo = device_info_dyn; - category.devices.add(await Thermostat.create(device_info)); + category.devices.add(await Thermostat.create(deviceInfo)); } categories.add(category); @@ -91,13 +91,13 @@ class Category { // create temperature_and_humidity { final Category category = Category('temperature_and_humidity'); - final List temperature_and_humidities = + final List temperatureAndHumidities = json['temperature_and_humidity']!; - for (dynamic device_info_dyn in temperature_and_humidities) { - final Map device_info = device_info_dyn; + for (final dynamic device_info_dyn in temperatureAndHumidities) { + final Map deviceInfo = device_info_dyn; - category.devices.add(await TemperatureHumidity.create(device_info)); + category.devices.add(await TemperatureHumidity.create(deviceInfo)); } categories.add(category); @@ -106,12 +106,12 @@ class Category { // create dish_washer { final Category category = Category('dish_washer'); - final List dish_washer = json['dish_washer']!; + final List dishWasher = json['dish_washer']!; - for (dynamic device_info_dyn in dish_washer) { - final Map device_info = device_info_dyn; + for (final dynamic device_info_dyn in dishWasher) { + final Map deviceInfo = device_info_dyn; - category.devices.add(await DishWasher.create(device_info)); + category.devices.add(await DishWasher.create(deviceInfo)); } categories.add(category); @@ -120,12 +120,12 @@ class Category { // create washing_machines { final Category category = Category('washing_machines'); - final List washing_machines = json['washing_machines']!; + final List washingMachines = json['washing_machines']!; - for (dynamic device_info_dyn in washing_machines) { - final Map device_info = device_info_dyn; + for (final dynamic device_info_dyn in washingMachines) { + final Map deviceInfo = device_info_dyn; - category.devices.add(await WashingMachine.create(device_info)); + category.devices.add(await WashingMachine.create(deviceInfo)); } categories.add(category); @@ -143,9 +143,9 @@ class Category { } class CategoryWidget extends StatelessWidget { - final Category category; - CategoryWidget({super.key, required this.category}); + const CategoryWidget({super.key, required this.category}); + final Category category; @override Widget build(BuildContext context) { @@ -169,6 +169,7 @@ class DeviceIdOnly extends Device { final String device_id; final String? device_descriptor; + @override Widget create_widget(BuildContext context) { throw UnimplementedError(); } diff --git a/frontend/lib/devices/dish_washer.dart b/frontend/lib/devices/dish_washer.dart index b269772..f2977f2 100644 --- a/frontend/lib/devices/dish_washer.dart +++ b/frontend/lib/devices/dish_washer.dart @@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart'; import 'devices.dart'; class DishWasher extends Device { - static Future create(Map device_info) async { + static Future create(Map deviceInfo) async { throw UnimplementedError(); } diff --git a/frontend/lib/devices/plug.dart b/frontend/lib/devices/plug.dart index 18b2b2a..06e6580 100644 --- a/frontend/lib/devices/plug.dart +++ b/frontend/lib/devices/plug.dart @@ -9,6 +9,9 @@ import '../states/plug_settings.dart'; import 'devices.dart'; 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; String? device_descriptor; bool led_state; @@ -16,40 +19,37 @@ class Plug extends Device { double power_draw; bool power_control; - static Future create(Map device_info) async { - final String device_id = device_info['id']; - final String? device_descriptor = device_info['desc']; - final bool power_control = device_info['toggle']; + static Future create(Map deviceInfo) async { + final String deviceId = deviceInfo['id']; + final String? deviceDescriptor = deviceInfo['desc']; + final bool powerControl = deviceInfo['toggle']; 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) { - throw Exception("Failed to fetch plug_state for $device_id"); + throw Exception("Failed to fetch plug_state for $deviceId"); } - final Map device_state = + final Map deviceState = Map.castFrom(jsonDecode(jsonDecode(response.body))); return Plug( - device_id, - device_descriptor, - device_state["led"], - device_state["power"], - device_state["power_draw"], - power_control && device_state["power_draw"] < 15, + deviceId, + deviceDescriptor, + deviceState["led"], + deviceState["power"], + deviceState["power_draw"], + 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 Widget create_widget(BuildContext context) { - const double header_height = 40; - const double info_height = 30; - const double info_width = 60; - const double x_offset = 0.9; + const double headerHeight = 40; + const double infoHeight = 30; + const double infoWidth = 60; + const double xOffset = 0.9; return Table( border: TableBorder( @@ -67,7 +67,7 @@ class Plug extends Device { ), children: [ SizedBox( - height: header_height, + height: headerHeight, child: Stack( children: [ Align( @@ -92,7 +92,7 @@ class Plug extends Device { ]), TableRow(children: [ SizedBox( - height: info_height, + height: infoHeight, child: Stack(children: [ const Align( alignment: Alignment(-0.9, 0.0), @@ -101,16 +101,16 @@ class Plug extends Device { textAlign: TextAlign.center, "LED")), Align( - alignment: const Alignment(x_offset, 0.0), + alignment: const Alignment(xOffset, 0.0), child: SizedBox( - width: info_width, + width: infoWidth, child: PlugLed( device_id: device_id, led_state: led_state))), ])) ]), TableRow(children: [ SizedBox( - height: info_height, + height: infoHeight, child: Stack(children: [ const Align( alignment: Alignment(-0.9, 0.0), @@ -119,9 +119,9 @@ class Plug extends Device { textAlign: TextAlign.center, "Power")), Align( - alignment: const Alignment(x_offset, 0.0), + alignment: const Alignment(xOffset, 0.0), child: SizedBox( - width: info_width, + width: infoWidth, child: PlugPower( device_id: device_id, power_state: power_state, @@ -130,7 +130,7 @@ class Plug extends Device { ]), TableRow(children: [ SizedBox( - height: info_height, + height: infoHeight, child: Stack(children: [ const Align( alignment: Alignment(-0.9, 0.0), @@ -139,9 +139,9 @@ class Plug extends Device { textAlign: TextAlign.center, "Power Draw")), Align( - alignment: const Alignment(x_offset, 0.0), + alignment: const Alignment(xOffset, 0.0), child: SizedBox( - width: info_width, + width: infoWidth, child: Text( textAlign: TextAlign.center, "$power_draw W"))) ])) @@ -151,10 +151,10 @@ class Plug extends Device { } class PlugLed extends StatefulWidget { - final String device_id; - bool led_state; PlugLed({super.key, required this.device_id, required this.led_state}); + final String device_id; + bool led_state; @override State createState() => PlugLedState(); @@ -164,17 +164,17 @@ class PlugLedState extends State { String _led_state_info = ""; void _toggle_led_state() { - String target_state; + String targetState; if (widget.led_state) { - target_state = "off"; + targetState = "off"; } else { - target_state = "on"; + targetState = "on"; } - change_plug_state(widget.device_id, "led", target_state) - .then((device_state) { - widget.led_state = device_state["led"]; + change_plug_state(widget.device_id, "led", targetState) + .then((deviceState) { + widget.led_state = deviceState["led"]; setState(() { _led_state_info = widget.led_state ? "On" : "Off"; @@ -193,15 +193,15 @@ class PlugLedState extends State { } class PlugPower extends StatefulWidget { - final String device_id; - bool power_state; - bool power_control; PlugPower( {super.key, required this.device_id, required this.power_state, required this.power_control}); + final String device_id; + bool power_state; + bool power_control; @override State createState() => PlugPowerState(); @@ -211,19 +211,19 @@ class PlugPowerState extends State { String _power_state_info = ""; void _toggle_power_state() { - String target_state; + String targetState; if (widget.power_state) { - target_state = "off"; + targetState = "off"; } else { - target_state = "on"; + targetState = "on"; } - change_plug_state(widget.device_id, "power", target_state) - .then((device_state) { - widget.power_state = device_state["power"]; + change_plug_state(widget.device_id, "power", targetState) + .then((deviceState) { + widget.power_state = deviceState["power"]; - widget.power_control = device_state["power_draw"] < 15; + widget.power_control = deviceState["power_draw"] < 15; setState(() { _power_state_info = widget.power_state ? "On" : "Off"; @@ -246,19 +246,19 @@ class PlugPowerState extends State { } Future> change_plug_state( - String device_id, String module, String state) async { + String deviceId, String module, String state) async { 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) { throw Exception("Failed to post new state"); } 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) { - 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))); diff --git a/frontend/lib/devices/temp_humid.dart b/frontend/lib/devices/temp_humid.dart index e2cc195..04853d1 100644 --- a/frontend/lib/devices/temp_humid.dart +++ b/frontend/lib/devices/temp_humid.dart @@ -4,7 +4,7 @@ import 'devices.dart'; class TemperatureHumidity extends Device { static Future create( - Map device_info) async { + Map deviceInfo) async { throw UnimplementedError(); } diff --git a/frontend/lib/devices/thermostat.dart b/frontend/lib/devices/thermostat.dart index 2f8b3e6..46ab4e2 100644 --- a/frontend/lib/devices/thermostat.dart +++ b/frontend/lib/devices/thermostat.dart @@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart'; import 'devices.dart'; class Thermostat extends Device { - static Future create(Map device_info) async { + static Future create(Map deviceInfo) async { throw UnimplementedError(); } diff --git a/frontend/lib/devices/washing_machine.dart b/frontend/lib/devices/washing_machine.dart index 5c7281a..e1476b5 100644 --- a/frontend/lib/devices/washing_machine.dart +++ b/frontend/lib/devices/washing_machine.dart @@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart'; import 'devices.dart'; class WashingMachine extends Device { - static Future create(Map device_info) async { + static Future create(Map deviceInfo) async { throw UnimplementedError(); } diff --git a/frontend/lib/main.dart b/frontend/lib/main.dart index 8c53ba2..c0218e6 100644 --- a/frontend/lib/main.dart +++ b/frontend/lib/main.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'constants.dart'; - +import 'states/graphs.dart'; import 'states/home_page.dart'; import 'states/plug_settings.dart'; -import 'states/graphs.dart'; void main() { runApp(const MyApp()); @@ -24,8 +23,8 @@ class MyApp extends StatelessWidget { ), home: const MyHomePage(title: 'Home Server'), routes: { - '/plug_settings': (BuildContext context) => PlugSettings(), - '/graphs': (BuildContext context) => Graphs(), + '/plug_settings': (BuildContext context) => const PlugSettings(), + '/graphs': (BuildContext context) => const Graphs(), }); } } diff --git a/frontend/lib/states/graphs.dart b/frontend/lib/states/graphs.dart index b32a48f..bc853da 100644 --- a/frontend/lib/states/graphs.dart +++ b/frontend/lib/states/graphs.dart @@ -173,45 +173,45 @@ class _GraphsState extends State { ElevatedButton( onPressed: () { setState(() { - for (var device in devices) { + for (final device in devices) { if (!device.enabled) { device.data = List.empty(); continue; } - int start_date; - int end_date; + int startDate; + int endDate; try { - start_date = (DateFormat('dd.MM.yyyy') + startDate = (DateFormat('dd.MM.yyyy') .parse(startDateController.text) .millisecondsSinceEpoch / 1000) as int; } on Exception catch (_) { - start_date = 0; + startDate = 0; print('no start date set'); } try { - end_date = (DateFormat('dd.MM.yyyy') + endDate = (DateFormat('dd.MM.yyyy') .parse(endDateController.text) .millisecondsSinceEpoch / 1000) as int; } on Exception catch (_) { - end_date = 0; + endDate = 0; print('no end date set'); } - if (start_date == 0 || end_date == 0) { + if (startDate == 0 || endDate == 0) { device.data = List.empty(); continue; } - const String filter_type = 'hourly'; + const String filterType = 'hourly'; http .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) { if (response.statusCode != 200) { throw Exception("Failed to fetch data"); @@ -220,14 +220,14 @@ class _GraphsState extends State { final data = jsonDecode(jsonDecode(response.body)) as List; - final List chart_data = []; + final List chartData = []; for (final entry in data) { final pair = entry as List; - 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 { // titlesData: FlTitlesData(bottomTitles: AxisTitles(sideTitles: )) ), duration: const Duration(milliseconds: 200), - curve: Curves.linear, ) ]))); }); diff --git a/frontend/lib/states/home_page.dart b/frontend/lib/states/home_page.dart index d5565cc..cd6a7eb 100644 --- a/frontend/lib/states/home_page.dart +++ b/frontend/lib/states/home_page.dart @@ -28,15 +28,15 @@ class _MyHomePageState extends State { } final data = categories.data!; - final category_count = data.length; + final categoryCount = data.length; - if (category_count > expanded.length) { - final int diff = category_count - expanded.length; + if (categoryCount > expanded.length) { + final int diff = categoryCount - expanded.length; - final List diff_list = List.filled(diff, true); - expanded.addAll(diff_list); - } else if (category_count < expanded.length) { - final int diff = expanded.length - category_count; + final List diffList = List.filled(diff, true); + expanded.addAll(diffList); + } else if (categoryCount < expanded.length) { + final int diff = expanded.length - categoryCount; expanded = List.filled(diff, false); } diff --git a/frontend/lib/states/plug_settings.dart b/frontend/lib/states/plug_settings.dart index 240033a..7015eb6 100644 --- a/frontend/lib/states/plug_settings.dart +++ b/frontend/lib/states/plug_settings.dart @@ -11,7 +11,7 @@ class PlugSettingsArguments { } class PlugSettings extends StatefulWidget { - PlugSettings({super.key}); + const PlugSettings({super.key}); @override State createState() => _PlugSettingsState(); diff --git a/src/db.rs b/src/db.rs index 72c6d91..eb66a6a 100644 --- a/src/db.rs +++ b/src/db.rs @@ -33,7 +33,7 @@ impl DataBase { )?; self.sql.execute( - "CREATE TABLE IF NOT EXISTS devices( + "CREATE TABLE IF NOT EXISTS devices ( id INTEGER PRIMARY KEY, device VARCHAR(60) NOT NULL, type VARCHAR(30) NOT NULL, @@ -47,7 +47,8 @@ impl DataBase { "CREATE TABLE IF NOT EXISTS data ( id INTEGER PRIMARY KEY, time BIGINT NOT NULL, - watts REAL NOT NULL, + name VARCHAR(30) NOT NULL, + value REAL NOT NULL, device_id INTEGER NOT NULL, 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(()) } - pub fn write(&self, device_name: &str, time: u64, watts: f32) -> Result<()> { - let params: &[&dyn ToSql] = &[&time, &watts]; + pub fn write(&self, device_name: &str, time: u64, name: &str, value: f32) -> Result<()> { + let params: &[&dyn ToSql] = &[&time, &value]; self.sql.execute( &format!( - "INSERT INTO data (time, watts, device_id) - VALUES (?1, ?2, (SELECT id FROM devices WHERE device=\"{device_name}\") )" + "INSERT INTO data (time, name, value, device_id) + VALUES (?1, \"{name}\", ?2, (SELECT id FROM devices WHERE device=\"{device_name}\") )" ), params, )?; @@ -156,6 +191,16 @@ impl DataBase { desc: name, 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!(), } @@ -164,6 +209,19 @@ impl DataBase { Ok(devices) } + pub fn device_exists(&self, device_id: &str) -> Result { + Ok(self + .sql + .prepare(&format!( + " + SELECT * + FROM devices + WHERE device=\"{device_id}\" + " + ))? + .exists([])?) + } + pub fn change_device_name(&self, device: &str, description: &str) -> Result<()> { self.sql.execute( &format!( @@ -182,7 +240,7 @@ impl DataBase { pub fn read(&self, device: &str) -> Result> { self._read(&format!( " - SELECT data.time, data.watts + SELECT data.time, data.value FROM data INNER JOIN devices ON data.device_id=devices.id @@ -194,7 +252,7 @@ impl DataBase { pub fn read_range(&self, device: &str, start: u64, end: u64) -> Result> { self._read(&format!( " - SELECT data.time, data.watts + SELECT data.time, data.value FROM data INNER JOIN devices ON data.device_id=devices.id @@ -293,6 +351,8 @@ mod test { db.register_devices(&Devices { plugs: vec![("test".to_string(), true)], + thermostat: Vec::new(), + thermometer: Vec::new(), })?; fs::remove_file("startup_test.db")?; @@ -308,9 +368,11 @@ mod test { db.register_devices(&Devices { 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"; db.change_device_name(device_name, device_descriptor)?; diff --git a/src/devices.rs b/src/devices.rs index 56af52f..c677fa4 100644 --- a/src/devices.rs +++ b/src/devices.rs @@ -7,6 +7,8 @@ use serde_json::{from_str, to_string, to_string_pretty}; #[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Debug)] pub struct Devices { pub plugs: Vec<(String, bool)>, + pub thermostat: Vec, + pub thermometer: Vec, } impl Devices { @@ -55,6 +57,8 @@ mod test { fn create_conf() -> Result<()> { let devices = Devices { plugs: vec![("Dev1".to_string(), true), ("Dev2".to_string(), false)], + thermostat: Vec::new(), + thermometer: Vec::new(), }; devices.save("test_devices.conf") diff --git a/src/main.rs b/src/main.rs index 31b071b..5cc7274 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod db; mod devices; mod midea_helper; mod tasmota; +mod temperature; mod tibber_handler; mod web_server; @@ -40,7 +41,7 @@ fn read_power_usage( if let Ok(usage) = plug.read_power_usage().await { db.lock() .unwrap() - .write(plug.name(), since_epoch()?, usage)?; + .write(plug.name(), since_epoch()?, "watts", usage)?; } Ok::<(), anyhow::Error>(()) @@ -81,6 +82,8 @@ async fn run_web_server( .service(change_device_name) .service(plug_data) .service(plug_data_range) + .service(push_temperature) + .service(push_humidity) }) .bind((IP, PORT)) .map_err(|err| anyhow::Error::msg(format!("failed binding to address: {err:#?}")))? diff --git a/src/temperature.rs b/src/temperature.rs new file mode 100644 index 0000000..1e8cc3d --- /dev/null +++ b/src/temperature.rs @@ -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>>, + ) -> 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(()) + } +} diff --git a/src/web_server.rs b/src/web_server.rs index e064c85..74ce049 100644 --- a/src/web_server.rs +++ b/src/web_server.rs @@ -1,13 +1,17 @@ use actix_web::{ get, post, web::{Data, Json, Path}, - Error, Responder, ResponseError, + Error, HttpRequest, Responder, ResponseError, }; use chrono::{Datelike, NaiveDateTime, Timelike}; use serde::Serialize; use serde_json::to_string; -use crate::{db::DataBase, tasmota::Tasmota}; +use crate::{ + db::DataBase, + tasmota::Tasmota, + temperature::{Thermometer, ThermometerChange}, +}; use std::{ collections::HashMap, @@ -226,6 +230,42 @@ async fn plug_data_range( )?)) } +#[get("/push_temp/{temperature}")] +async fn push_temperature( + param: Path, + req: HttpRequest, + db: Data>>, +) -> Result { + 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, + req: HttpRequest, + db: Data>>, +) -> Result { + 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(data: Vec<(u64, f32)>, f: F) -> Vec<(u64, f32)> where F: Fn(NaiveDateTime) -> NaiveDateTime,