Compare commits

..

No commits in common. "master" and "flutter" have entirely different histories.

28 changed files with 191 additions and 1165 deletions

View file

@ -6,16 +6,14 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
rusqlite = "0.31.0"
anyhow = { version = "1.0.86", features = ["backtrace"] }
reqwest = "0.11.27"
rusqlite = "0.29.0"
anyhow = { version = "1.0.75", features = ["backtrace"] }
reqwest = "0.11.22"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
futures = "0.3.30"
tokio = { version = "1.38.0", features=["macros", "rt-multi-thread"] }
tibber = "0.5.0"
chrono = "0.4.38"
actix-web = "4.8.0"
futures = "0.3.28"
tokio = { version = "1.33.0", features=["macros", "rt-multi-thread"] }
chrono = "0.4.31"
actix-web = "4.4.0"
midea = { git = "https://gavania.de/hodasemi/Midea.git" }
actix-cors = "0.7.0"
dns-lookup = "2.0.4"
actix-cors = "0.6.4"

View file

@ -9,5 +9,4 @@ cargo build --release
mkdir -p server
cp devices.conf server/
cp tibber_token.txt server/
cp target/release/home_server server/

View file

@ -4,14 +4,5 @@
["Tasmota-Plug-2", false],
["Tasmota-Plug-3", true],
["Tasmota-Plug-4", true]
],
"thermostat": [
"shellytrv-8CF681A1F886",
"shellytrv-8CF681E9BAEE",
"shellytrv-B4E3F9D9E2A1"
],
"thermometer": [
"shellyplusht-d4d4da7d85b4",
"shellyplusht-80646fc9db9c"
]
}

View file

@ -1,12 +1,12 @@
buildscript {
ext.kotlin_version = '2.0.0'
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.5.0'
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View file

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip

View file

@ -30,14 +30,14 @@ class Category {
final Map<String, List<dynamic>> json =
Map.castFrom(jsonDecode(jsonDecode(response.body)));
for (final MapEntry<String, List<dynamic>> entry in json.entries) {
for (MapEntry<String, List<dynamic>> entry in json.entries) {
final Category category = Category(entry.key);
for (final dynamic device_info_dyn in entry.value) {
final Map<String, dynamic> deviceInfo = device_info_dyn;
for (dynamic device_info_dyn in entry.value) {
final Map<String, dynamic> device_info = device_info_dyn;
category.devices
.add(DeviceIdOnly(deviceInfo['id'], deviceInfo['desc']));
.add(DeviceIdOnly(device_info['id'], device_info['desc']));
}
if (category.devices.isNotEmpty) {
@ -65,10 +65,10 @@ class Category {
final Category category = Category('plugs');
final List<dynamic> plugs = json['plugs']!;
for (final dynamic device_info_dyn in plugs) {
final Map<String, dynamic> deviceInfo = device_info_dyn;
for (dynamic device_info_dyn in plugs) {
final Map<String, dynamic> device_info = device_info_dyn;
category.devices.add(await Plug.create(deviceInfo));
category.devices.add(await Plug.create(device_info));
}
categories.add(category);
@ -79,10 +79,10 @@ class Category {
final Category category = Category('thermostat');
final List<dynamic> thermostats = json['thermostat']!;
for (final dynamic device_info_dyn in thermostats) {
final Map<String, dynamic> deviceInfo = device_info_dyn;
for (dynamic device_info_dyn in thermostats) {
final Map<String, dynamic> device_info = device_info_dyn;
category.devices.add(await Thermostat.create(deviceInfo));
category.devices.add(await Thermostat.create(device_info));
}
categories.add(category);
@ -91,13 +91,13 @@ class Category {
// create temperature_and_humidity
{
final Category category = Category('temperature_and_humidity');
final List<dynamic> temperatureAndHumidities =
final List<dynamic> temperature_and_humidities =
json['temperature_and_humidity']!;
for (final dynamic device_info_dyn in temperatureAndHumidities) {
final Map<String, dynamic> deviceInfo = device_info_dyn;
for (dynamic device_info_dyn in temperature_and_humidities) {
final Map<String, dynamic> device_info = device_info_dyn;
category.devices.add(await TemperatureHumidity.create(deviceInfo));
category.devices.add(await TemperatureHumidity.create(device_info));
}
categories.add(category);
@ -106,12 +106,12 @@ class Category {
// create dish_washer
{
final Category category = Category('dish_washer');
final List<dynamic> dishWasher = json['dish_washer']!;
final List<dynamic> dish_washer = json['dish_washer']!;
for (final dynamic device_info_dyn in dishWasher) {
final Map<String, dynamic> deviceInfo = device_info_dyn;
for (dynamic device_info_dyn in dish_washer) {
final Map<String, dynamic> device_info = device_info_dyn;
category.devices.add(await DishWasher.create(deviceInfo));
category.devices.add(await DishWasher.create(device_info));
}
categories.add(category);
@ -120,12 +120,12 @@ class Category {
// create washing_machines
{
final Category category = Category('washing_machines');
final List<dynamic> washingMachines = json['washing_machines']!;
final List<dynamic> washing_machines = json['washing_machines']!;
for (final dynamic device_info_dyn in washingMachines) {
final Map<String, dynamic> deviceInfo = device_info_dyn;
for (dynamic device_info_dyn in washing_machines) {
final Map<String, dynamic> device_info = device_info_dyn;
category.devices.add(await WashingMachine.create(deviceInfo));
category.devices.add(await WashingMachine.create(device_info));
}
categories.add(category);
@ -143,10 +143,10 @@ class Category {
}
class CategoryWidget extends StatelessWidget {
const CategoryWidget({super.key, required this.category});
final Category category;
CategoryWidget({super.key, required this.category});
@override
Widget build(BuildContext context) {
return Wrap(
@ -169,7 +169,6 @@ class DeviceIdOnly extends Device {
final String device_id;
final String? device_descriptor;
@override
Widget create_widget(BuildContext context) {
throw UnimplementedError();
}

View file

@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart';
import 'devices.dart';
class DishWasher extends Device {
static Future<DishWasher> create(Map<String, dynamic> deviceInfo) async {
static Future<DishWasher> create(Map<String, dynamic> device_info) async {
throw UnimplementedError();
}

View file

@ -9,9 +9,6 @@ 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;
@ -19,37 +16,40 @@ class Plug extends Device {
double power_draw;
bool power_control;
static Future<Plug> create(Map<String, dynamic> deviceInfo) async {
final String deviceId = deviceInfo['id'];
final String? deviceDescriptor = deviceInfo['desc'];
final bool powerControl = deviceInfo['toggle'];
static Future<Plug> create(Map<String, dynamic> device_info) async {
final String device_id = device_info['id'];
final String? device_descriptor = device_info['desc'];
final bool power_control = device_info['toggle'];
final response = await http
.get(Uri.parse("${Constants.BASE_URL}/plug_state/$deviceId"));
.get(Uri.parse("${Constants.BASE_URL}/plug_state/$device_id"));
if (response.statusCode != 200) {
throw Exception("Failed to fetch plug_state for $deviceId");
throw Exception("Failed to fetch plug_state for $device_id");
}
final Map<String, dynamic> deviceState =
final Map<String, dynamic> device_state =
Map.castFrom(jsonDecode(jsonDecode(response.body)));
return Plug(
deviceId,
deviceDescriptor,
deviceState["led"],
deviceState["power"],
deviceState["power_draw"],
powerControl && deviceState["power_draw"] < 15,
device_id,
device_descriptor,
device_state["led"],
device_state["power"],
device_state["power_draw"],
power_control && device_state["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 headerHeight = 40;
const double infoHeight = 30;
const double infoWidth = 60;
const double xOffset = 0.9;
const double header_height = 40;
const double info_height = 30;
const double info_width = 60;
const double x_offset = 0.9;
return Table(
border: TableBorder(
@ -67,7 +67,7 @@ class Plug extends Device {
),
children: [
SizedBox(
height: headerHeight,
height: header_height,
child: Stack(
children: [
Align(
@ -92,7 +92,7 @@ class Plug extends Device {
]),
TableRow(children: [
SizedBox(
height: infoHeight,
height: info_height,
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(xOffset, 0.0),
alignment: const Alignment(x_offset, 0.0),
child: SizedBox(
width: infoWidth,
width: info_width,
child: PlugLed(
device_id: device_id, led_state: led_state))),
]))
]),
TableRow(children: [
SizedBox(
height: infoHeight,
height: info_height,
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(xOffset, 0.0),
alignment: const Alignment(x_offset, 0.0),
child: SizedBox(
width: infoWidth,
width: info_width,
child: PlugPower(
device_id: device_id,
power_state: power_state,
@ -130,7 +130,7 @@ class Plug extends Device {
]),
TableRow(children: [
SizedBox(
height: infoHeight,
height: info_height,
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(xOffset, 0.0),
alignment: const Alignment(x_offset, 0.0),
child: SizedBox(
width: infoWidth,
width: info_width,
child: Text(
textAlign: TextAlign.center, "$power_draw W")))
]))
@ -151,11 +151,11 @@ class Plug extends Device {
}
class PlugLed extends StatefulWidget {
PlugLed({super.key, required this.device_id, required this.led_state});
final String device_id;
bool led_state;
PlugLed({super.key, required this.device_id, required this.led_state});
@override
State<PlugLed> createState() => PlugLedState();
}
@ -164,17 +164,17 @@ class PlugLedState extends State<PlugLed> {
String _led_state_info = "";
void _toggle_led_state() {
String targetState;
String target_state;
if (widget.led_state) {
targetState = "off";
target_state = "off";
} else {
targetState = "on";
target_state = "on";
}
change_plug_state(widget.device_id, "led", targetState)
.then((deviceState) {
widget.led_state = deviceState["led"];
change_plug_state(widget.device_id, "led", target_state)
.then((device_state) {
widget.led_state = device_state["led"];
setState(() {
_led_state_info = widget.led_state ? "On" : "Off";
@ -193,15 +193,15 @@ class PlugLedState extends State<PlugLed> {
}
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<PlugPower> createState() => PlugPowerState();
@ -211,19 +211,19 @@ class PlugPowerState extends State<PlugPower> {
String _power_state_info = "";
void _toggle_power_state() {
String targetState;
String target_state;
if (widget.power_state) {
targetState = "off";
target_state = "off";
} else {
targetState = "on";
target_state = "on";
}
change_plug_state(widget.device_id, "power", targetState)
.then((deviceState) {
widget.power_state = deviceState["power"];
change_plug_state(widget.device_id, "power", target_state)
.then((device_state) {
widget.power_state = device_state["power"];
widget.power_control = deviceState["power_draw"] < 15;
widget.power_control = device_state["power_draw"] < 15;
setState(() {
_power_state_info = widget.power_state ? "On" : "Off";
@ -246,19 +246,19 @@ class PlugPowerState extends State<PlugPower> {
}
Future<Map<String, dynamic>> change_plug_state(
String deviceId, String module, String state) async {
String device_id, String module, String state) async {
var response = await http.post(
Uri.parse("${Constants.BASE_URL}/plug/$deviceId/${module}_$state"));
Uri.parse("${Constants.BASE_URL}/plug/$device_id/${module}_$state"));
if (response.statusCode != 200) {
throw Exception("Failed to post new state");
}
response =
await http.get(Uri.parse("${Constants.BASE_URL}/plug_state/$deviceId"));
await http.get(Uri.parse("${Constants.BASE_URL}/plug_state/$device_id"));
if (response.statusCode != 200) {
throw Exception("Failed to fetch plug_state for $deviceId");
throw Exception("Failed to fetch plug_state for $device_id");
}
return Map.castFrom(jsonDecode(jsonDecode(response.body)));

View file

@ -4,7 +4,7 @@ import 'devices.dart';
class TemperatureHumidity extends Device {
static Future<TemperatureHumidity> create(
Map<String, dynamic> deviceInfo) async {
Map<String, dynamic> device_info) async {
throw UnimplementedError();
}

View file

@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart';
import 'devices.dart';
class Thermostat extends Device {
static Future<Thermostat> create(Map<String, dynamic> deviceInfo) async {
static Future<Thermostat> create(Map<String, dynamic> device_info) async {
throw UnimplementedError();
}

View file

@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart';
import 'devices.dart';
class WashingMachine extends Device {
static Future<WashingMachine> create(Map<String, dynamic> deviceInfo) async {
static Future<WashingMachine> create(Map<String, dynamic> device_info) async {
throw UnimplementedError();
}

View file

@ -1,9 +1,10 @@
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());
@ -23,8 +24,8 @@ class MyApp extends StatelessWidget {
),
home: const MyHomePage(title: 'Home Server'),
routes: <String, WidgetBuilder>{
'/plug_settings': (BuildContext context) => const PlugSettings(),
'/graphs': (BuildContext context) => const Graphs(),
'/plug_settings': (BuildContext context) => PlugSettings(),
'/graphs': (BuildContext context) => Graphs(),
});
}
}

View file

@ -173,45 +173,45 @@ class _GraphsState extends State<Graphs> {
ElevatedButton(
onPressed: () {
setState(() {
for (final device in devices) {
for (var device in devices) {
if (!device.enabled) {
device.data = List.empty();
continue;
}
int startDate;
int endDate;
int start_date;
int end_date;
try {
startDate = (DateFormat('dd.MM.yyyy')
start_date = (DateFormat('dd.MM.yyyy')
.parse(startDateController.text)
.millisecondsSinceEpoch /
1000) as int;
} on Exception catch (_) {
startDate = 0;
start_date = 0;
print('no start date set');
}
try {
endDate = (DateFormat('dd.MM.yyyy')
end_date = (DateFormat('dd.MM.yyyy')
.parse(endDateController.text)
.millisecondsSinceEpoch /
1000) as int;
} on Exception catch (_) {
endDate = 0;
end_date = 0;
print('no end date set');
}
if (startDate == 0 || endDate == 0) {
if (start_date == 0 || end_date == 0) {
device.data = List.empty();
continue;
}
const String filterType = 'hourly';
const String filter_type = 'hourly';
http
.get(Uri.parse(
"${Constants.BASE_URL}/plug_data/${device.device.device_id}/$startDate/$endDate/$filterType"))
"${Constants.BASE_URL}/plug_data/${device.device.device_id}/$start_date/$end_date/$filter_type"))
.then((response) {
if (response.statusCode != 200) {
throw Exception("Failed to fetch data");
@ -220,14 +220,14 @@ class _GraphsState extends State<Graphs> {
final data = jsonDecode(jsonDecode(response.body))
as List<dynamic>;
final List<ChartData> chartData = [];
final List<ChartData> chart_data = [];
for (final entry in data) {
final pair = entry as List<dynamic>;
chartData.add(ChartData(pair[0], pair[1]));
chart_data.add(ChartData(pair[0], pair[1]));
}
device.data = chartData;
device.data = chart_data;
});
}
});
@ -258,6 +258,7 @@ class _GraphsState extends State<Graphs> {
// titlesData: FlTitlesData(bottomTitles: AxisTitles(sideTitles: ))
),
duration: const Duration(milliseconds: 200),
curve: Curves.linear,
)
])));
});

View file

@ -28,15 +28,15 @@ class _MyHomePageState extends State<MyHomePage> {
}
final data = categories.data!;
final categoryCount = data.length;
final category_count = data.length;
if (categoryCount > expanded.length) {
final int diff = categoryCount - expanded.length;
if (category_count > expanded.length) {
final int diff = category_count - expanded.length;
final List<bool> diffList = List<bool>.filled(diff, true);
expanded.addAll(diffList);
} else if (categoryCount < expanded.length) {
final int diff = expanded.length - categoryCount;
final List<bool> diff_list = List<bool>.filled(diff, true);
expanded.addAll(diff_list);
} else if (category_count < expanded.length) {
final int diff = expanded.length - category_count;
expanded = List<bool>.filled(diff, false);
}

View file

@ -11,7 +11,7 @@ class PlugSettingsArguments {
}
class PlugSettings extends StatefulWidget {
const PlugSettings({super.key});
PlugSettings({super.key});
@override
State<PlugSettings> createState() => _PlugSettingsState();

View file

@ -38,8 +38,8 @@ dependencies:
http: ^1.1.0
flutter_spinkit: ^5.2.0
fl_chart: ^0.68.0
intl: ^0.19.0
fl_chart: ^0.64.0
intl: ^0.18.1
dev_dependencies:
flutter_test:
@ -50,7 +50,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^4.0.0
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

View file

@ -2,12 +2,5 @@
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:base"
],
"prHourlyLimit": 0,
"packageRules": [
{
"matchUpdateTypes": ["minor", "patch"],
"automerge": true
}
]
}

View file

@ -1,166 +0,0 @@
use core::slice::Iter;
use std::{fmt::Display, str::FromStr};
use anyhow::{bail, Result};
use serde::Serialize;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
pub enum ActionType {
GreaterThan,
LessThan,
Push,
Receive,
Update,
}
impl Display for ActionType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::GreaterThan => write!(f, "GreaterThan"),
Self::LessThan => write!(f, "LessThan"),
Self::Push => write!(f, "Push"),
Self::Receive => write!(f, "Receive"),
Self::Update => write!(f, "Update"),
}
}
}
impl FromStr for ActionType {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"GreaterThan" => Ok(Self::GreaterThan),
"LessThan" => Ok(Self::LessThan),
"Push" => Ok(Self::Push),
"Receive" => Ok(Self::Receive),
"Update" => Ok(Self::Update),
_ => bail!("could not parse ActionType from {s}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct ActionID(pub(crate) i64);
#[derive(Debug, Clone, Eq, PartialOrd, Ord, Serialize)]
pub struct Action {
#[serde(skip)]
pub(crate) id: Option<ActionID>,
pub device_id: String,
pub action_type: ActionType,
pub parameter: String,
}
impl Action {
pub fn new(
device_id: impl ToString,
action_type: ActionType,
parameter: impl ToString,
) -> Self {
Self {
id: None,
device_id: device_id.to_string(),
action_type,
parameter: parameter.to_string(),
}
}
}
impl PartialEq for Action {
fn eq(&self, other: &Self) -> bool {
let id_comp = match (self.id, other.id) {
(Some(self_id), Some(other_id)) => self_id == other_id,
_ => true,
};
id_comp
&& self.device_id == other.device_id
&& self.action_type == other.action_type
&& self.parameter == other.parameter
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize)]
pub struct ActionSet {
actions: Vec<Action>,
}
impl ActionSet {
pub fn push_device(&self) -> Option<String> {
self.iter()
.find(|action| action.action_type == ActionType::Push)
.map(|action| action.device_id.clone())
}
pub fn receive_device(&self) -> Option<String> {
self.iter()
.find(|action| action.action_type == ActionType::Receive)
.map(|action| action.device_id.clone())
}
pub fn begins_with_device(&self, device_name: &str) -> bool {
match self.actions.get(0) {
Some(action) => action.device_id == device_name,
None => false,
}
}
pub(crate) fn first_id(&self) -> Option<ActionID> {
self.actions.get(0).map(|action| action.id).flatten()
}
pub fn parameter(&self, parameter: &str) -> bool {
match self.actions.get(0) {
Some(action) => action.parameter == parameter,
None => false,
}
}
pub fn chain(&mut self, action: Action) {
self.actions.push(action);
}
pub fn iter(&self) -> Iter<'_, Action> {
self.actions.iter()
}
}
impl<I> From<I> for ActionSet
where
I: IntoIterator<Item = Action>,
{
fn from(value: I) -> Self {
Self {
actions: value.into_iter().collect(),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use anyhow::Result;
#[test]
fn example_chain() -> Result<()> {
let mut action_set = ActionSet::default();
action_set.chain(Action::new(
"shelly_plus_ht",
ActionType::Push,
"temperature",
));
action_set.chain(Action::new(
"shelly_trv",
ActionType::Receive,
"temperature",
));
Ok(())
}
}

291
src/db.rs
View file

@ -1,12 +1,9 @@
use std::{path::Path, str::FromStr};
use std::path::Path;
use anyhow::Result;
use rusqlite::{Connection, OptionalExtension, ToSql};
use crate::{
action::{Action, ActionID, ActionSet, ActionType},
devices::{DeviceWithName, Devices, DevicesWithName},
};
use crate::devices::{DeviceWithName, Devices, DevicesWithName};
pub struct DataBase {
sql: Connection,
@ -36,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,
@ -50,8 +47,7 @@ impl DataBase {
"CREATE TABLE IF NOT EXISTS data (
id INTEGER PRIMARY KEY,
time BIGINT NOT NULL,
name VARCHAR(30) NOT NULL,
value REAL NOT NULL,
watts REAL NOT NULL,
device_id INTEGER NOT NULL,
FOREIGN KEY(device_id) REFERENCES devices(id)
)",
@ -68,176 +64,9 @@ impl DataBase {
[],
)?;
self.sql.execute(
"
CREATE TABLE IF NOT EXISTS actions (
id INTEGER PRIMARY KEY,
device_id INTEGER NOT NULL,
action VARCHAR(30) NOT NULL,
parameter VARCHAR(60) NOT NULL,
action_id INTEGER,
FOREIGN KEY(action_id) REFERENCES actions(id),
FOREIGN KEY(device_id) REFERENCES devices(id)
)
",
[],
)?;
Ok(())
}
fn device_id(&self, device_name: &str) -> Result<i64> {
Ok(self
.sql
.prepare(&format!(
"
SELECT id
FROM devices
WHERE device=\"{}\"
",
device_name
))?
.query_row([], |row| Ok(row.get(0)?))?)
}
pub fn insert_action_set(&self, action_set: ActionSet) -> Result<ActionID> {
let mut action_ids = Vec::new();
for (i, action) in action_set.iter().enumerate() {
// get device id from device name
let device_id = self.device_id(&action.device_id)?;
// insert action to DB
self.sql.execute(
&format!(
"INSERT INTO actions (device_id, action, parameter)
VALUES (?1, \"{}\", \"{}\")",
action.action_type, action.parameter
),
&[&device_id],
)?;
action_ids.push(self.sql.last_insert_rowid());
if i > 0 {
// chain actions
self.sql.execute(
&format!(
"
UPDATE actions
SET action_id=?2
WHERE id=?1
"
),
[&action_ids[i - 1], &action_ids[i]],
)?;
}
}
Ok(ActionID(action_ids[0]))
}
pub fn remove_action_set(&self, action_set: &ActionSet) -> Result<()> {
if let Some(action_id) = action_set.first_id() {
self.sql.execute(
"
DELETE FROM actions
WHERE id=?1
",
&[&action_id.0],
)?;
}
Ok(())
}
pub fn action_set(&self, mut action_id: ActionID) -> Result<ActionSet> {
let mut action_set = ActionSet::default();
loop {
let (device_id, action, parameter, next_action): (i64, String, String, Option<i64>) =
self.sql.query_row(
"
SELECT device_id, action, parameter, action_id
FROM actions
WHERE id=?1
",
&[&action_id.0],
|row| Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?)),
)?;
let device_name: String = self.sql.query_row(
"
SELECT device
FROM devices
WHERE id=?1
",
&[&device_id],
|row| row.get(0),
)?;
let mut action = Action::new(device_name, ActionType::from_str(&action)?, parameter);
action.id = Some(action_id);
action_set.chain(action);
match next_action {
Some(id) => action_id.0 = id,
None => break,
}
}
Ok(action_set)
}
pub fn action_sets(&self, device_name: &str) -> Result<Vec<ActionSet>> {
let mut action_sets = Vec::new();
let device_id = self.device_id(device_name)?;
let base_actions: Vec<i64> = self
.sql
.prepare(
"
SELECT id
FROM actions
WHERE device_id=?1
",
)?
.query_map(&[&device_id], |row| row.get(0))?
.map(|row| {
let r: i64 = row?;
Ok(r)
})
.collect::<Result<Vec<i64>>>()?;
for mut action_id in base_actions {
loop {
match self
.sql
.query_row(
"
SELECT id
FROM actions
WHERE action_id=?1
",
&[&action_id],
|row| row.get(0),
)
.optional()?
{
Some(id) => action_id = id,
None => {
action_sets.push(self.action_set(ActionID(action_id))?);
break;
}
}
}
}
Ok(action_sets)
}
pub fn version(&self) -> Result<String> {
Ok(self
.sql
@ -287,50 +116,16 @@ 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, name: &str, value: f32) -> Result<()> {
let params: &[&dyn ToSql] = &[&time, &value];
pub fn write(&self, device_name: &str, time: u64, watts: f32) -> Result<()> {
let params: &[&dyn ToSql] = &[&time, &watts];
self.sql.execute(
&format!(
"INSERT INTO data (time, name, value, device_id)
VALUES (?1, \"{name}\", ?2, (SELECT id FROM devices WHERE device=\"{device_name}\") )"
"INSERT INTO data (time, watts, device_id)
VALUES (?1, ?2, (SELECT id FROM devices WHERE device=\"{device_name}\") )"
),
params,
)?;
@ -361,16 +156,6 @@ 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!(),
}
@ -379,19 +164,6 @@ impl DataBase {
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<()> {
self.sql.execute(
&format!(
@ -410,7 +182,7 @@ impl DataBase {
pub fn read(&self, device: &str) -> Result<Vec<(u64, f32)>> {
self._read(&format!(
"
SELECT data.time, data.value
SELECT data.time, data.watts
FROM data
INNER JOIN devices
ON data.device_id=devices.id
@ -422,7 +194,7 @@ impl DataBase {
pub fn read_range(&self, device: &str, start: u64, end: u64) -> Result<Vec<(u64, f32)>> {
self._read(&format!(
"
SELECT data.time, data.value
SELECT data.time, data.watts
FROM data
INNER JOIN devices
ON data.device_id=devices.id
@ -501,10 +273,7 @@ mod test {
use anyhow::Result;
use crate::{
action::{Action, ActionSet, ActionType},
devices::Devices,
};
use crate::devices::Devices;
use super::DataBase;
@ -524,8 +293,6 @@ mod test {
db.register_devices(&Devices {
plugs: vec![("test".to_string(), true)],
thermostat: Vec::new(),
thermometer: Vec::new(),
})?;
fs::remove_file("startup_test.db")?;
@ -541,11 +308,9 @@ mod test {
db.register_devices(&Devices {
plugs: vec![(device_name.to_string(), true)],
thermostat: Vec::new(),
thermometer: Vec::new(),
})?;
db.write(device_name, 0, "watts", 5.5)?;
db.write(device_name, 0, 5.5)?;
let device_descriptor = "udo";
db.change_device_name(device_name, device_descriptor)?;
@ -559,36 +324,4 @@ mod test {
Ok(())
}
#[tokio::test]
async fn action_set_test() -> Result<()> {
let db = DataBase::new("action_set_test.db").await?;
let thermometer = "shelly_plus_ht";
let thermostat = "shelly_trv";
db.register_devices(&Devices {
plugs: Vec::new(),
thermostat: vec![thermostat.to_string()],
thermometer: vec![thermometer.to_string()],
})?;
let mut action_set = ActionSet::default();
action_set.chain(Action::new(thermometer, ActionType::Push, "temperature"));
action_set.chain(Action::new(thermostat, ActionType::Receive, "temperature"));
let action_id = db.insert_action_set(action_set.clone())?;
let cmp_action_set = db.action_set(action_id)?;
assert_eq!(action_set, cmp_action_set);
let action_sets_thermometer = db.action_sets(thermometer)?;
let action_sets_thermostat = db.action_sets(thermostat)?;
assert_eq!(action_sets_thermometer, action_sets_thermostat);
fs::remove_file("action_set_test.db")?;
Ok(())
}
}

View file

@ -7,8 +7,6 @@ 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<String>,
pub thermometer: Vec<String>,
}
impl Devices {
@ -57,8 +55,6 @@ 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")

View file

@ -1,68 +1,50 @@
use std::{
fs,
sync::{Arc, Mutex},
thread,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use crate::{db::DataBase, midea_helper::MideaDiscovery, web_server::plug_data_range};
mod action;
mod data;
mod db;
mod devices;
mod midea_helper;
mod task_scheduler;
mod tasmota;
mod temperature;
mod tibber_handler;
mod web_server;
use actix_cors::Cors;
use actix_web::{web::Data, App, HttpServer};
use anyhow::Result;
use devices::Devices;
use futures::{try_join, Future};
use futures::{future::try_join_all, try_join, Future};
use midea_helper::MideaDishwasher;
use task_scheduler::{Scheduler, Task};
use tasmota::Tasmota;
use tibber::TimeResolution::Daily;
use tibber_handler::TibberHandler;
use web_server::*;
fn since_epoch() -> Result<u64> {
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
}
fn handle_error(
f: impl Future<Output = Result<()>> + Send + 'static,
) -> impl Future<Output = ()> + Unpin + Send + 'static {
Box::pin(async move {
if let Err(err) = f.await {
println!("{err}:?");
}
})
}
fn setup_tasmota_tasks(
scheduler: &Scheduler,
fn read_power_usage(
tasmota_plugs: Vec<Tasmota>,
db: Arc<Mutex<DataBase>>,
) {
for plug in tasmota_plugs.into_iter() {
let db_clone = db.clone();
) -> impl Future<Output = Result<()>> {
async move {
loop {
try_join_all(tasmota_plugs.iter().map(|plug| async {
if let Ok(usage) = plug.read_power_usage().await {
db.lock()
.unwrap()
.write(plug.name(), since_epoch()?, usage)?;
}
let fut = async move {
if let Ok(usage) = plug.read_power_usage().await {
db_clone
.lock()
.unwrap()
.write(plug.name(), since_epoch()?, "watts", usage)?;
}
Ok::<(), anyhow::Error>(())
}))
.await?;
Ok(())
};
scheduler.add_task(Task::looping(Duration::from_secs(3), handle_error(fut)));
thread::sleep(Duration::from_secs(3));
}
}
}
@ -71,7 +53,6 @@ async fn run_web_server(
plugs: Vec<Tasmota>,
db: Arc<Mutex<DataBase>>,
dishwasher: Vec<Arc<MideaDishwasher>>,
scheduler: Scheduler,
) -> Result<()> {
const IP: &str = "0.0.0.0";
const PORT: u16 = 8062;
@ -90,17 +71,12 @@ async fn run_web_server(
.app_data(Data::new(db.clone()))
.app_data(Data::new(plugs.clone()))
.app_data(Data::new(dishwasher.clone()))
.app_data(Data::new(scheduler.clone()))
.service(device_query)
.service(plug_state)
.service(change_plug_state)
.service(change_device_name)
.service(plug_data)
.service(plug_data_range)
.service(push_temperature)
.service(push_humidity)
.service(update_push_action)
.service(actions)
})
.bind((IP, PORT))
.map_err(|err| anyhow::Error::msg(format!("failed binding to address: {err:#?}")))?
@ -114,18 +90,8 @@ async fn run_web_server(
async fn main() -> Result<()> {
let db_future = DataBase::new("home_server.db");
let devices_future = Devices::read("devices.conf");
let tibber_future = TibberHandler::new(fs::read_to_string("tibber_token.txt")?);
let (db, devices, tibber, midea) = try_join!(
db_future,
devices_future,
tibber_future,
MideaDiscovery::discover()
)?;
let prices_today = tibber.prices_today().await?;
let prices_tomorrow = tibber.prices_tomorrow().await?;
let consumption = tibber.consumption(Daily, 1).await?;
let (db, devices, midea) = try_join!(db_future, devices_future, MideaDiscovery::discover())?;
db.register_devices(&devices)?;
let shared_db = Arc::new(Mutex::new(db));
@ -142,19 +108,9 @@ async fn main() -> Result<()> {
.map(|d| Arc::new(d))
.collect();
let scheduler = Scheduler::default();
setup_tasmota_tasks(&scheduler, tasmota_plugs.clone(), shared_db.clone());
let scheduler_clone = scheduler.clone();
try_join!(
scheduler.run(),
run_web_server(
devices,
tasmota_plugs,
shared_db,
dishwasher,
scheduler_clone
)
read_power_usage(tasmota_plugs.clone(), shared_db.clone()),
run_web_server(devices, tasmota_plugs, shared_db, dishwasher)
)?;
Ok(())

View file

@ -11,10 +11,10 @@ enum LoginInfo {
}
impl LoginInfo {
const MIDEA_KEY_EMAIL: &'static str = "midea_cloud_mail";
const MIDEA_KEY_PW: &'static str = "midea_cloud_pw";
const MIDEA_KEY_TOKEN: &'static str = "midea_token";
const MIDEA_KEY_KEY: &'static str = "midea_key";
const MIDEA_KEY_EMAIL: &str = "midea_cloud_mail";
const MIDEA_KEY_PW: &str = "midea_cloud_pw";
const MIDEA_KEY_TOKEN: &str = "midea_token";
const MIDEA_KEY_KEY: &str = "midea_key";
fn new(db: &Arc<Mutex<DataBase>>, device_id: u64) -> Result<LoginInfo> {
let db_lock = db.lock().unwrap();

View file

@ -1,127 +0,0 @@
use anyhow::Result;
use futures::future::Shared;
use futures::FutureExt;
use std::collections::VecDeque;
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::thread;
use std::{
sync::Mutex,
time::{Duration, SystemTime},
};
enum Callback {
Looping(Shared<Pin<Box<dyn Future<Output = ()> + Unpin + Send + 'static>>>),
Once(Pin<Box<dyn Future<Output = ()> + Unpin + Send + 'static>>),
}
pub struct Task {
creation_time: SystemTime,
time: Duration,
callback: Callback,
}
impl Task {
pub fn looping<F>(interval: Duration, f: F) -> Self
where
F: Future<Output = ()> + Unpin + Send + 'static,
{
let c: Pin<Box<dyn Future<Output = ()> + Unpin + Send + 'static>> = Box::pin(f);
Self {
creation_time: SystemTime::now(),
time: interval,
callback: Callback::Looping(c.shared()),
}
}
fn recreate(
start: SystemTime,
interval: Duration,
f: Shared<Pin<Box<dyn Future<Output = ()> + Unpin + Send + 'static>>>,
) -> Self {
Self {
creation_time: start,
time: interval,
callback: Callback::Looping(f),
}
}
pub fn one_shot<F>(time: Duration, f: F) -> Self
where
F: Future<Output = ()> + Unpin + Send + 'static,
{
Self {
creation_time: SystemTime::now(),
time,
callback: Callback::Once(Box::pin(f)),
}
}
fn execution_time(&self) -> SystemTime {
self.creation_time + self.time
}
}
#[derive(Default, Clone)]
pub struct Scheduler {
tasks: Arc<Mutex<VecDeque<Task>>>,
}
impl Scheduler {
pub fn add_task(&self, new_task: Task) {
let mut task_lock = self.tasks.lock().unwrap();
let pos = task_lock
.binary_search_by_key(&new_task.execution_time(), |task| task.execution_time())
.unwrap_or_else(|e| e);
task_lock.insert(pos, new_task);
}
pub fn run(self) -> impl Future<Output = Result<()>> {
async move {
loop {
// exec first if time is up
while let Some(first) = self.check_first() {
let execution_time = first.execution_time();
match first.callback {
Callback::Looping(callback) => {
let callback_clone = callback.clone();
tokio::spawn(callback_clone);
self.add_task(Task::recreate(execution_time, first.time, callback));
}
Callback::Once(callback) => {
tokio::spawn(callback);
}
}
}
thread::sleep(Duration::from_millis(500));
}
}
}
pub fn check_first(&self) -> Option<Task> {
let mut task_lock = self.tasks.lock().unwrap();
// get first element
if let Some(first) = task_lock.front() {
// check if execution time is reached
if first.execution_time() < SystemTime::now() {
return task_lock.pop_front();
}
}
return None;
}
}

View file

@ -96,55 +96,55 @@ impl Tasmota {
}
}
// #[cfg(test)]
// mod test {
// use std::{thread, time::Duration};
#[cfg(test)]
mod test {
use std::{thread, time::Duration};
// use super::*;
use super::*;
// use anyhow::Result;
use anyhow::Result;
// #[tokio::test]
// async fn test_connection() -> Result<()> {
// let dev = Tasmota::new("Tasmota-Plug-1");
#[tokio::test]
async fn test_connection() -> Result<()> {
let dev = Tasmota::new("Tasmota-Plug-1");
// let power = dev.read_power_usage().await?;
let power = dev.read_power_usage().await?;
// println!("{power}");
println!("{power}");
// Ok(())
// }
Ok(())
}
// #[tokio::test]
// async fn test_toggle() -> Result<()> {
// let dev = Tasmota::new("Tasmota-Plug-4");
#[tokio::test]
async fn test_toggle() -> Result<()> {
let dev = Tasmota::new("Tasmota-Plug-4");
// dev.switch_off().await?;
// assert_eq!(dev.power_state().await?, false);
dev.switch_off().await?;
assert_eq!(dev.power_state().await?, false);
// thread::sleep(Duration::from_secs(5));
thread::sleep(Duration::from_secs(5));
// dev.switch_on().await?;
// assert_eq!(dev.power_state().await?, true);
dev.switch_on().await?;
assert_eq!(dev.power_state().await?, true);
// Ok(())
// }
Ok(())
}
// #[tokio::test]
// async fn test_led() -> Result<()> {
// let dev = Tasmota::new("Tasmota-Plug-4");
#[tokio::test]
async fn test_led() -> Result<()> {
let dev = Tasmota::new("Tasmota-Plug-4");
// dev.turn_off_led().await?;
// assert_eq!(dev.led_state().await?, false);
dev.turn_off_led().await?;
assert_eq!(dev.led_state().await?, false);
// thread::sleep(Duration::from_secs(5));
thread::sleep(Duration::from_secs(5));
// dev.turn_on_led().await?;
// assert_eq!(dev.led_state().await?, true);
dev.turn_on_led().await?;
assert_eq!(dev.led_state().await?, true);
// Ok(())
// }
// }
Ok(())
}
}
#[allow(non_snake_case)]
#[derive(Deserialize, Debug)]

View file

@ -1,96 +0,0 @@
use std::{
net::IpAddr,
sync::{Arc, Mutex},
time::Duration,
};
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,
task_scheduler::{Scheduler, Task},
};
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>>>,
scheduler: Data<Scheduler>,
) -> 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)?;
for action_set in db_lock.action_sets(&device_id)? {
if let Some(push_device) = action_set.push_device() {
if action_set.parameter("temperature") && push_device == device_id {
if let Some(receive_device) = action_set.receive_device() {
scheduler.add_task(Task::one_shot(
Duration::from_secs(0),
Box::pin(async move {
let _ = Thermostat::new(receive_device)
.set_temperature(temp);
}),
));
}
}
}
}
}
ThermometerChange::Humidity(humid) => {
db_lock.write(&device_id, since_epoch()?, "humidity", humid)?;
}
}
}
Ok(())
}
}

View file

@ -1,149 +0,0 @@
use std::sync::Arc;
use anyhow::{Error, Result};
use tibber::{Consumption, HomeId, House, PriceInfo, TibberSession, TimeResolution, User};
pub struct TibberHandler {
session: Arc<TibberSession>,
pub user: User,
pub homes: Vec<(HomeId, House)>,
}
impl TibberHandler {
pub async fn new(token: impl ToString + Send + 'static) -> Result<Self> {
tokio::task::spawn_blocking(move || {
let session = Arc::new(TibberSession::new(token.to_string()));
let user = session.get_user().map_err(|err| {
Error::msg(format!(
"TibberHandler: failed getting user information: {err:?}"
))
})?;
let mut homes = Vec::new();
for home_id in user.homes.clone().into_iter() {
let house = session.get_home(&home_id).map_err(|err| {
Error::msg(format!(
"TibberHandler: failed getting house information: {err:?}"
))
})?;
homes.push((home_id, house));
}
Ok(Self {
homes,
user,
session,
})
})
.await?
}
async fn get_data<F, T>(&self, f: F) -> Result<Vec<(House, T)>>
where
F: Fn(&TibberSession, &HomeId) -> Result<T> + Send + Sync + Copy + 'static,
T: Send + Sync + 'static,
{
let mut v = Vec::new();
for (home_id, house) in self.homes.iter() {
v.push((
house.clone(),
tokio::task::spawn_blocking({
let session = self.session.clone();
let home_id = home_id.clone();
move || f(&session, &home_id)
})
.await??,
));
}
Ok(v)
}
pub async fn current_prices(&self) -> Result<Vec<(House, PriceInfo)>> {
self.get_data(|session, home_id| {
session.get_current_price(home_id).map_err(|err| {
Error::msg(format!(
"TibberHandler: failed getting current price: {err:?}"
))
})
})
.await
}
pub async fn prices_today(&self) -> Result<Vec<(House, Vec<PriceInfo>)>> {
self.get_data(|session, home_id| {
session.get_prices_today(home_id).map_err(|err| {
Error::msg(format!(
"TibberHandler: failed getting prices of today: {err:?}"
))
})
})
.await
}
pub async fn prices_tomorrow(&self) -> Result<Vec<(House, Vec<PriceInfo>)>> {
self.get_data(|session, home_id| {
session.get_prices_tomorrow(home_id).map_err(|err| {
Error::msg(format!(
"TibberHandler: failed getting prices for tomorrow: {err:?}"
))
})
})
.await
}
pub async fn consumption(
&self,
resolution: TimeResolution,
last: u32,
) -> Result<Vec<(House, Vec<Consumption>)>> {
let mut v = Vec::new();
for (home_id, house) in self.homes.iter() {
v.push((
house.clone(),
tokio::task::spawn_blocking({
let session = self.session.clone();
let home_id = home_id.clone();
let resolution = resolution.clone();
move || {
session
.get_consuption(&home_id, resolution, last)
.map_err(|err| {
Error::msg(format!(
"TibberHandler: failed getting consumption: {err:?}"
))
})
}
})
.await??,
));
}
Ok(v)
}
}
// #[cfg(test)]
// mod test {
// use super::TibberHandler;
// use anyhow::Result;
// use std::fs;
// #[tokio::test]
// async fn test_connection() -> Result<()> {
// let tibber = TibberHandler::new(fs::read_to_string("tibber_token.txt")?).await?;
// let current_prices = tibber.current_prices().await?;
// println!("{current_prices:?}");
// Ok(())
// }
// }

View file

@ -1,19 +1,13 @@
use actix_web::{
get, post,
web::{Data, Json, Path},
Error, HttpRequest, Responder, ResponseError,
Error, Responder, ResponseError,
};
use chrono::{Datelike, NaiveDateTime, Timelike};
use serde::Serialize;
use serde_json::to_string;
use crate::{
action::{Action, ActionSet, ActionType},
db::DataBase,
task_scheduler::Scheduler,
tasmota::Tasmota,
temperature::{Thermometer, ThermometerChange},
};
use crate::{db::DataBase, tasmota::Tasmota};
use std::{
collections::HashMap,
@ -232,102 +226,6 @@ async fn plug_data_range(
)?))
}
#[get("/push_temp/{temperature}")]
async fn push_temperature(
param: Path<f32>,
req: HttpRequest,
db: Data<Arc<Mutex<DataBase>>>,
scheduler: Data<Scheduler>,
) -> Result<impl Responder, Error> {
if let Some(val) = req.peer_addr() {
Thermometer::push_change(
ThermometerChange::Temperature(param.into_inner()),
val.ip(),
db,
scheduler,
)
.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>>>,
scheduler: Data<Scheduler>,
) -> Result<impl Responder, Error> {
if let Some(val) = req.peer_addr() {
Thermometer::push_change(
ThermometerChange::Humidity(param.into_inner()),
val.ip(),
db,
scheduler,
)
.map_err(|err| MyError::from(err))?;
}
Ok("Ok")
}
#[post("/update_push_action/{source_device}/{parameter}/{destination_device}")]
async fn update_push_action(
param: Path<(String, String, String)>,
db: Data<Arc<Mutex<DataBase>>>,
) -> Result<impl Responder, Error> {
let (source_device, parameter, destination_device) = param.into_inner();
let db_lock = db.lock().unwrap();
let action_sets = db_lock
.action_sets(&source_device)
.map_err(|err| MyError::from(err))?;
// check if action set is already present
if let Some(old_action_set) = action_sets.iter().find(|action_set| {
action_set.push_device() == Some(source_device.clone())
&& action_set.receive_device() == Some(destination_device.clone())
&& action_set.parameter(&parameter)
}) {
// remove old action set
db_lock
.remove_action_set(old_action_set)
.map_err(|err| MyError::from(err))?;
}
let new_action_set = ActionSet::from(vec![
Action::new(source_device, ActionType::Push, parameter.clone()),
Action::new(destination_device, ActionType::Receive, parameter),
]);
db_lock
.insert_action_set(new_action_set)
.map_err(|err| MyError::from(err))?;
Ok("Ok")
}
#[get("/actions/{device}")]
async fn actions(
param: Path<String>,
db: Data<Arc<Mutex<DataBase>>>,
) -> Result<impl Responder, Error> {
let device_name = param.into_inner();
let db_lock = db.lock().unwrap();
let action_sets: Vec<ActionSet> = db_lock
.action_sets(&device_name)
.map_err(|err| MyError::from(err))?
.into_iter()
.filter(|action_set| action_set.begins_with_device(&device_name))
.collect();
Ok(Json(
to_string(&action_sets).map_err(|err| MyError::from(anyhow::Error::from(err)))?,
))
}
fn collapse_data<F>(data: Vec<(u64, f32)>, f: F) -> Vec<(u64, f32)>
where
F: Fn(NaiveDateTime) -> NaiveDateTime,

View file

@ -1 +0,0 @@
dkvF6ax77CP4v9-sxYyjWUD-9NunpzVPRYHfLJ8a9ps