Compare commits
No commits in common. "master" and "flutter" have entirely different histories.
28 changed files with 191 additions and 1165 deletions
18
Cargo.toml
18
Cargo.toml
|
@ -6,16 +6,14 @@ edition = "2021"
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rusqlite = "0.31.0"
|
rusqlite = "0.29.0"
|
||||||
anyhow = { version = "1.0.86", features = ["backtrace"] }
|
anyhow = { version = "1.0.75", features = ["backtrace"] }
|
||||||
reqwest = "0.11.27"
|
reqwest = "0.11.22"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
futures = "0.3.30"
|
futures = "0.3.28"
|
||||||
tokio = { version = "1.38.0", features=["macros", "rt-multi-thread"] }
|
tokio = { version = "1.33.0", features=["macros", "rt-multi-thread"] }
|
||||||
tibber = "0.5.0"
|
chrono = "0.4.31"
|
||||||
chrono = "0.4.38"
|
actix-web = "4.4.0"
|
||||||
actix-web = "4.8.0"
|
|
||||||
midea = { git = "https://gavania.de/hodasemi/Midea.git" }
|
midea = { git = "https://gavania.de/hodasemi/Midea.git" }
|
||||||
actix-cors = "0.7.0"
|
actix-cors = "0.6.4"
|
||||||
dns-lookup = "2.0.4"
|
|
1
build.sh
1
build.sh
|
@ -9,5 +9,4 @@ cargo build --release
|
||||||
mkdir -p server
|
mkdir -p server
|
||||||
|
|
||||||
cp devices.conf server/
|
cp devices.conf server/
|
||||||
cp tibber_token.txt server/
|
|
||||||
cp target/release/home_server server/
|
cp target/release/home_server server/
|
|
@ -4,14 +4,5 @@
|
||||||
["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"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
buildscript {
|
buildscript {
|
||||||
ext.kotlin_version = '2.0.0'
|
ext.kotlin_version = '1.7.10'
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
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"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
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
|
||||||
|
|
|
@ -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 (final MapEntry<String, List<dynamic>> entry in json.entries) {
|
for (MapEntry<String, List<dynamic>> entry in json.entries) {
|
||||||
final Category category = Category(entry.key);
|
final Category category = Category(entry.key);
|
||||||
|
|
||||||
for (final dynamic device_info_dyn in entry.value) {
|
for (dynamic device_info_dyn in entry.value) {
|
||||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
final Map<String, dynamic> device_info = device_info_dyn;
|
||||||
|
|
||||||
category.devices
|
category.devices
|
||||||
.add(DeviceIdOnly(deviceInfo['id'], deviceInfo['desc']));
|
.add(DeviceIdOnly(device_info['id'], device_info['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 (final dynamic device_info_dyn in plugs) {
|
for (dynamic device_info_dyn in plugs) {
|
||||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
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);
|
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 (final dynamic device_info_dyn in thermostats) {
|
for (dynamic device_info_dyn in thermostats) {
|
||||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
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);
|
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> temperatureAndHumidities =
|
final List<dynamic> temperature_and_humidities =
|
||||||
json['temperature_and_humidity']!;
|
json['temperature_and_humidity']!;
|
||||||
|
|
||||||
for (final dynamic device_info_dyn in temperatureAndHumidities) {
|
for (dynamic device_info_dyn in temperature_and_humidities) {
|
||||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
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);
|
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> dishWasher = json['dish_washer']!;
|
final List<dynamic> dish_washer = json['dish_washer']!;
|
||||||
|
|
||||||
for (final dynamic device_info_dyn in dishWasher) {
|
for (dynamic device_info_dyn in dish_washer) {
|
||||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
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);
|
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> washingMachines = json['washing_machines']!;
|
final List<dynamic> washing_machines = json['washing_machines']!;
|
||||||
|
|
||||||
for (final dynamic device_info_dyn in washingMachines) {
|
for (dynamic device_info_dyn in washing_machines) {
|
||||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
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);
|
categories.add(category);
|
||||||
|
@ -143,10 +143,10 @@ class Category {
|
||||||
}
|
}
|
||||||
|
|
||||||
class CategoryWidget extends StatelessWidget {
|
class CategoryWidget extends StatelessWidget {
|
||||||
|
|
||||||
const CategoryWidget({super.key, required this.category});
|
|
||||||
final Category category;
|
final Category category;
|
||||||
|
|
||||||
|
CategoryWidget({super.key, required this.category});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Wrap(
|
return Wrap(
|
||||||
|
@ -169,7 +169,6 @@ class DeviceIdOnly extends Device {
|
||||||
final String device_id;
|
final String device_id;
|
||||||
final String? device_descriptor;
|
final String? device_descriptor;
|
||||||
|
|
||||||
@override
|
|
||||||
Widget create_widget(BuildContext context) {
|
Widget create_widget(BuildContext context) {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart';
|
||||||
import 'devices.dart';
|
import 'devices.dart';
|
||||||
|
|
||||||
class DishWasher extends Device {
|
class DishWasher extends Device {
|
||||||
static Future<DishWasher> create(Map<String, dynamic> deviceInfo) async {
|
static Future<DishWasher> create(Map<String, dynamic> device_info) async {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,9 +9,6 @@ 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;
|
||||||
|
@ -19,37 +16,40 @@ class Plug extends Device {
|
||||||
double power_draw;
|
double power_draw;
|
||||||
bool power_control;
|
bool power_control;
|
||||||
|
|
||||||
static Future<Plug> create(Map<String, dynamic> deviceInfo) async {
|
static Future<Plug> create(Map<String, dynamic> device_info) async {
|
||||||
final String deviceId = deviceInfo['id'];
|
final String device_id = device_info['id'];
|
||||||
final String? deviceDescriptor = deviceInfo['desc'];
|
final String? device_descriptor = device_info['desc'];
|
||||||
final bool powerControl = deviceInfo['toggle'];
|
final bool power_control = device_info['toggle'];
|
||||||
|
|
||||||
final response = await http
|
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) {
|
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)));
|
Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
||||||
|
|
||||||
return Plug(
|
return Plug(
|
||||||
deviceId,
|
device_id,
|
||||||
deviceDescriptor,
|
device_descriptor,
|
||||||
deviceState["led"],
|
device_state["led"],
|
||||||
deviceState["power"],
|
device_state["power"],
|
||||||
deviceState["power_draw"],
|
device_state["power_draw"],
|
||||||
powerControl && deviceState["power_draw"] < 15,
|
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
|
@override
|
||||||
Widget create_widget(BuildContext context) {
|
Widget create_widget(BuildContext context) {
|
||||||
const double headerHeight = 40;
|
const double header_height = 40;
|
||||||
const double infoHeight = 30;
|
const double info_height = 30;
|
||||||
const double infoWidth = 60;
|
const double info_width = 60;
|
||||||
const double xOffset = 0.9;
|
const double x_offset = 0.9;
|
||||||
|
|
||||||
return Table(
|
return Table(
|
||||||
border: TableBorder(
|
border: TableBorder(
|
||||||
|
@ -67,7 +67,7 @@ class Plug extends Device {
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: headerHeight,
|
height: header_height,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Align(
|
Align(
|
||||||
|
@ -92,7 +92,7 @@ class Plug extends Device {
|
||||||
]),
|
]),
|
||||||
TableRow(children: [
|
TableRow(children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: infoHeight,
|
height: info_height,
|
||||||
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(xOffset, 0.0),
|
alignment: const Alignment(x_offset, 0.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: infoWidth,
|
width: info_width,
|
||||||
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: infoHeight,
|
height: info_height,
|
||||||
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(xOffset, 0.0),
|
alignment: const Alignment(x_offset, 0.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: infoWidth,
|
width: info_width,
|
||||||
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: infoHeight,
|
height: info_height,
|
||||||
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(xOffset, 0.0),
|
alignment: const Alignment(x_offset, 0.0),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: infoWidth,
|
width: info_width,
|
||||||
child: Text(
|
child: Text(
|
||||||
textAlign: TextAlign.center, "$power_draw W")))
|
textAlign: TextAlign.center, "$power_draw W")))
|
||||||
]))
|
]))
|
||||||
|
@ -151,11 +151,11 @@ class Plug extends Device {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlugLed extends StatefulWidget {
|
class PlugLed extends StatefulWidget {
|
||||||
|
|
||||||
PlugLed({super.key, required this.device_id, required this.led_state});
|
|
||||||
final String device_id;
|
final String device_id;
|
||||||
bool led_state;
|
bool led_state;
|
||||||
|
|
||||||
|
PlugLed({super.key, required this.device_id, required this.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 targetState;
|
String target_state;
|
||||||
|
|
||||||
if (widget.led_state) {
|
if (widget.led_state) {
|
||||||
targetState = "off";
|
target_state = "off";
|
||||||
} else {
|
} else {
|
||||||
targetState = "on";
|
target_state = "on";
|
||||||
}
|
}
|
||||||
|
|
||||||
change_plug_state(widget.device_id, "led", targetState)
|
change_plug_state(widget.device_id, "led", target_state)
|
||||||
.then((deviceState) {
|
.then((device_state) {
|
||||||
widget.led_state = deviceState["led"];
|
widget.led_state = device_state["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 targetState;
|
String target_state;
|
||||||
|
|
||||||
if (widget.power_state) {
|
if (widget.power_state) {
|
||||||
targetState = "off";
|
target_state = "off";
|
||||||
} else {
|
} else {
|
||||||
targetState = "on";
|
target_state = "on";
|
||||||
}
|
}
|
||||||
|
|
||||||
change_plug_state(widget.device_id, "power", targetState)
|
change_plug_state(widget.device_id, "power", target_state)
|
||||||
.then((deviceState) {
|
.then((device_state) {
|
||||||
widget.power_state = deviceState["power"];
|
widget.power_state = device_state["power"];
|
||||||
|
|
||||||
widget.power_control = deviceState["power_draw"] < 15;
|
widget.power_control = device_state["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 deviceId, String module, String state) async {
|
String device_id, String module, String state) async {
|
||||||
var response = await http.post(
|
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) {
|
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/$deviceId"));
|
await http.get(Uri.parse("${Constants.BASE_URL}/plug_state/$device_id"));
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
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)));
|
return Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
||||||
|
|
|
@ -4,7 +4,7 @@ import 'devices.dart';
|
||||||
|
|
||||||
class TemperatureHumidity extends Device {
|
class TemperatureHumidity extends Device {
|
||||||
static Future<TemperatureHumidity> create(
|
static Future<TemperatureHumidity> create(
|
||||||
Map<String, dynamic> deviceInfo) async {
|
Map<String, dynamic> device_info) async {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart';
|
||||||
import 'devices.dart';
|
import 'devices.dart';
|
||||||
|
|
||||||
class Thermostat extends Device {
|
class Thermostat extends Device {
|
||||||
static Future<Thermostat> create(Map<String, dynamic> deviceInfo) async {
|
static Future<Thermostat> create(Map<String, dynamic> device_info) async {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import 'package:flutter/src/widgets/framework.dart';
|
||||||
import 'devices.dart';
|
import 'devices.dart';
|
||||||
|
|
||||||
class WashingMachine extends Device {
|
class WashingMachine extends Device {
|
||||||
static Future<WashingMachine> create(Map<String, dynamic> deviceInfo) async {
|
static Future<WashingMachine> create(Map<String, dynamic> device_info) async {
|
||||||
throw UnimplementedError();
|
throw UnimplementedError();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
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());
|
||||||
|
@ -23,8 +24,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) => const PlugSettings(),
|
'/plug_settings': (BuildContext context) => PlugSettings(),
|
||||||
'/graphs': (BuildContext context) => const Graphs(),
|
'/graphs': (BuildContext context) => Graphs(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -173,45 +173,45 @@ class _GraphsState extends State<Graphs> {
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (final device in devices) {
|
for (var device in devices) {
|
||||||
if (!device.enabled) {
|
if (!device.enabled) {
|
||||||
device.data = List.empty();
|
device.data = List.empty();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
int startDate;
|
int start_date;
|
||||||
int endDate;
|
int end_date;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
startDate = (DateFormat('dd.MM.yyyy')
|
start_date = (DateFormat('dd.MM.yyyy')
|
||||||
.parse(startDateController.text)
|
.parse(startDateController.text)
|
||||||
.millisecondsSinceEpoch /
|
.millisecondsSinceEpoch /
|
||||||
1000) as int;
|
1000) as int;
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
startDate = 0;
|
start_date = 0;
|
||||||
print('no start date set');
|
print('no start date set');
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
endDate = (DateFormat('dd.MM.yyyy')
|
end_date = (DateFormat('dd.MM.yyyy')
|
||||||
.parse(endDateController.text)
|
.parse(endDateController.text)
|
||||||
.millisecondsSinceEpoch /
|
.millisecondsSinceEpoch /
|
||||||
1000) as int;
|
1000) as int;
|
||||||
} on Exception catch (_) {
|
} on Exception catch (_) {
|
||||||
endDate = 0;
|
end_date = 0;
|
||||||
print('no end date set');
|
print('no end date set');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (startDate == 0 || endDate == 0) {
|
if (start_date == 0 || end_date == 0) {
|
||||||
device.data = List.empty();
|
device.data = List.empty();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const String filterType = 'hourly';
|
const String filter_type = 'hourly';
|
||||||
|
|
||||||
http
|
http
|
||||||
.get(Uri.parse(
|
.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) {
|
.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> chartData = [];
|
final List<ChartData> chart_data = [];
|
||||||
|
|
||||||
for (final entry in data) {
|
for (final entry in data) {
|
||||||
final pair = entry as List<dynamic>;
|
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: ))
|
// titlesData: FlTitlesData(bottomTitles: AxisTitles(sideTitles: ))
|
||||||
),
|
),
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.linear,
|
||||||
)
|
)
|
||||||
])));
|
])));
|
||||||
});
|
});
|
||||||
|
|
|
@ -28,15 +28,15 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final data = categories.data!;
|
final data = categories.data!;
|
||||||
final categoryCount = data.length;
|
final category_count = data.length;
|
||||||
|
|
||||||
if (categoryCount > expanded.length) {
|
if (category_count > expanded.length) {
|
||||||
final int diff = categoryCount - expanded.length;
|
final int diff = category_count - expanded.length;
|
||||||
|
|
||||||
final List<bool> diffList = List<bool>.filled(diff, true);
|
final List<bool> diff_list = List<bool>.filled(diff, true);
|
||||||
expanded.addAll(diffList);
|
expanded.addAll(diff_list);
|
||||||
} else if (categoryCount < expanded.length) {
|
} else if (category_count < expanded.length) {
|
||||||
final int diff = expanded.length - categoryCount;
|
final int diff = expanded.length - category_count;
|
||||||
|
|
||||||
expanded = List<bool>.filled(diff, false);
|
expanded = List<bool>.filled(diff, false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ class PlugSettingsArguments {
|
||||||
}
|
}
|
||||||
|
|
||||||
class PlugSettings extends StatefulWidget {
|
class PlugSettings extends StatefulWidget {
|
||||||
const PlugSettings({super.key});
|
PlugSettings({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<PlugSettings> createState() => _PlugSettingsState();
|
State<PlugSettings> createState() => _PlugSettingsState();
|
||||||
|
|
|
@ -38,8 +38,8 @@ dependencies:
|
||||||
|
|
||||||
http: ^1.1.0
|
http: ^1.1.0
|
||||||
flutter_spinkit: ^5.2.0
|
flutter_spinkit: ^5.2.0
|
||||||
fl_chart: ^0.68.0
|
fl_chart: ^0.64.0
|
||||||
intl: ^0.19.0
|
intl: ^0.18.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@ -50,7 +50,7 @@ dev_dependencies:
|
||||||
# activated in the `analysis_options.yaml` file located at the root of your
|
# activated in the `analysis_options.yaml` file located at the root of your
|
||||||
# package. See that file for information about deactivating specific lint
|
# package. See that file for information about deactivating specific lint
|
||||||
# rules and activating additional ones.
|
# 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
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
|
|
|
@ -2,12 +2,5 @@
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": [
|
"extends": [
|
||||||
"config:base"
|
"config:base"
|
||||||
],
|
|
||||||
"prHourlyLimit": 0,
|
|
||||||
"packageRules": [
|
|
||||||
{
|
|
||||||
"matchUpdateTypes": ["minor", "patch"],
|
|
||||||
"automerge": true
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
166
src/action.rs
166
src/action.rs
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
289
src/db.rs
289
src/db.rs
|
@ -1,12 +1,9 @@
|
||||||
use std::{path::Path, str::FromStr};
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use rusqlite::{Connection, OptionalExtension, ToSql};
|
use rusqlite::{Connection, OptionalExtension, ToSql};
|
||||||
|
|
||||||
use crate::{
|
use crate::devices::{DeviceWithName, Devices, DevicesWithName};
|
||||||
action::{Action, ActionID, ActionSet, ActionType},
|
|
||||||
devices::{DeviceWithName, Devices, DevicesWithName},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct DataBase {
|
pub struct DataBase {
|
||||||
sql: Connection,
|
sql: Connection,
|
||||||
|
@ -50,8 +47,7 @@ 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,
|
||||||
name VARCHAR(30) NOT NULL,
|
watts REAL 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)
|
||||||
)",
|
)",
|
||||||
|
@ -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(())
|
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> {
|
pub fn version(&self) -> Result<String> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.sql
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&self, device_name: &str, time: u64, name: &str, value: f32) -> Result<()> {
|
pub fn write(&self, device_name: &str, time: u64, watts: f32) -> Result<()> {
|
||||||
let params: &[&dyn ToSql] = &[&time, &value];
|
let params: &[&dyn ToSql] = &[&time, &watts];
|
||||||
|
|
||||||
self.sql.execute(
|
self.sql.execute(
|
||||||
&format!(
|
&format!(
|
||||||
"INSERT INTO data (time, name, value, device_id)
|
"INSERT INTO data (time, watts, device_id)
|
||||||
VALUES (?1, \"{name}\", ?2, (SELECT id FROM devices WHERE device=\"{device_name}\") )"
|
VALUES (?1, ?2, (SELECT id FROM devices WHERE device=\"{device_name}\") )"
|
||||||
),
|
),
|
||||||
params,
|
params,
|
||||||
)?;
|
)?;
|
||||||
|
@ -361,16 +156,6 @@ 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!(),
|
||||||
}
|
}
|
||||||
|
@ -379,19 +164,6 @@ 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!(
|
||||||
|
@ -410,7 +182,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.value
|
SELECT data.time, data.watts
|
||||||
FROM data
|
FROM data
|
||||||
INNER JOIN devices
|
INNER JOIN devices
|
||||||
ON data.device_id=devices.id
|
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)>> {
|
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.value
|
SELECT data.time, data.watts
|
||||||
FROM data
|
FROM data
|
||||||
INNER JOIN devices
|
INNER JOIN devices
|
||||||
ON data.device_id=devices.id
|
ON data.device_id=devices.id
|
||||||
|
@ -501,10 +273,7 @@ mod test {
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use crate::{
|
use crate::devices::Devices;
|
||||||
action::{Action, ActionSet, ActionType},
|
|
||||||
devices::Devices,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::DataBase;
|
use super::DataBase;
|
||||||
|
|
||||||
|
@ -524,8 +293,6 @@ 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")?;
|
||||||
|
@ -541,11 +308,9 @@ 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, "watts", 5.5)?;
|
db.write(device_name, 0, 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)?;
|
||||||
|
@ -559,36 +324,4 @@ mod test {
|
||||||
|
|
||||||
Ok(())
|
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(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,6 @@ 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 {
|
||||||
|
@ -57,8 +55,6 @@ 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")
|
||||||
|
|
78
src/main.rs
78
src/main.rs
|
@ -1,68 +1,50 @@
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
|
thread,
|
||||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{db::DataBase, midea_helper::MideaDiscovery, web_server::plug_data_range};
|
use crate::{db::DataBase, midea_helper::MideaDiscovery, web_server::plug_data_range};
|
||||||
|
|
||||||
mod action;
|
|
||||||
mod data;
|
mod data;
|
||||||
mod db;
|
mod db;
|
||||||
mod devices;
|
mod devices;
|
||||||
mod midea_helper;
|
mod midea_helper;
|
||||||
mod task_scheduler;
|
|
||||||
mod tasmota;
|
mod tasmota;
|
||||||
mod temperature;
|
|
||||||
mod tibber_handler;
|
|
||||||
mod web_server;
|
mod web_server;
|
||||||
|
|
||||||
use actix_cors::Cors;
|
use actix_cors::Cors;
|
||||||
use actix_web::{web::Data, App, HttpServer};
|
use actix_web::{web::Data, App, HttpServer};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use devices::Devices;
|
use devices::Devices;
|
||||||
use futures::{try_join, Future};
|
use futures::{future::try_join_all, try_join, Future};
|
||||||
use midea_helper::MideaDishwasher;
|
use midea_helper::MideaDishwasher;
|
||||||
use task_scheduler::{Scheduler, Task};
|
|
||||||
use tasmota::Tasmota;
|
use tasmota::Tasmota;
|
||||||
use tibber::TimeResolution::Daily;
|
|
||||||
use tibber_handler::TibberHandler;
|
|
||||||
use web_server::*;
|
use web_server::*;
|
||||||
|
|
||||||
fn since_epoch() -> Result<u64> {
|
fn since_epoch() -> Result<u64> {
|
||||||
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
|
Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_error(
|
fn read_power_usage(
|
||||||
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,
|
|
||||||
tasmota_plugs: Vec<Tasmota>,
|
tasmota_plugs: Vec<Tasmota>,
|
||||||
db: Arc<Mutex<DataBase>>,
|
db: Arc<Mutex<DataBase>>,
|
||||||
) {
|
) -> impl Future<Output = Result<()>> {
|
||||||
for plug in tasmota_plugs.into_iter() {
|
async move {
|
||||||
let db_clone = db.clone();
|
loop {
|
||||||
|
try_join_all(tasmota_plugs.iter().map(|plug| async {
|
||||||
let fut = async move {
|
|
||||||
if let Ok(usage) = plug.read_power_usage().await {
|
if let Ok(usage) = plug.read_power_usage().await {
|
||||||
db_clone
|
db.lock()
|
||||||
.lock()
|
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write(plug.name(), since_epoch()?, "watts", usage)?;
|
.write(plug.name(), since_epoch()?, usage)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok::<(), anyhow::Error>(())
|
||||||
};
|
}))
|
||||||
|
.await?;
|
||||||
|
|
||||||
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>,
|
plugs: Vec<Tasmota>,
|
||||||
db: Arc<Mutex<DataBase>>,
|
db: Arc<Mutex<DataBase>>,
|
||||||
dishwasher: Vec<Arc<MideaDishwasher>>,
|
dishwasher: Vec<Arc<MideaDishwasher>>,
|
||||||
scheduler: Scheduler,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
const IP: &str = "0.0.0.0";
|
const IP: &str = "0.0.0.0";
|
||||||
const PORT: u16 = 8062;
|
const PORT: u16 = 8062;
|
||||||
|
@ -90,17 +71,12 @@ async fn run_web_server(
|
||||||
.app_data(Data::new(db.clone()))
|
.app_data(Data::new(db.clone()))
|
||||||
.app_data(Data::new(plugs.clone()))
|
.app_data(Data::new(plugs.clone()))
|
||||||
.app_data(Data::new(dishwasher.clone()))
|
.app_data(Data::new(dishwasher.clone()))
|
||||||
.app_data(Data::new(scheduler.clone()))
|
|
||||||
.service(device_query)
|
.service(device_query)
|
||||||
.service(plug_state)
|
.service(plug_state)
|
||||||
.service(change_plug_state)
|
.service(change_plug_state)
|
||||||
.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)
|
|
||||||
.service(update_push_action)
|
|
||||||
.service(actions)
|
|
||||||
})
|
})
|
||||||
.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:#?}")))?
|
||||||
|
@ -114,18 +90,8 @@ async fn run_web_server(
|
||||||
async fn main() -> Result<()> {
|
async fn main() -> Result<()> {
|
||||||
let db_future = DataBase::new("home_server.db");
|
let db_future = DataBase::new("home_server.db");
|
||||||
let devices_future = Devices::read("devices.conf");
|
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!(
|
let (db, devices, midea) = try_join!(db_future, devices_future, MideaDiscovery::discover())?;
|
||||||
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?;
|
|
||||||
|
|
||||||
db.register_devices(&devices)?;
|
db.register_devices(&devices)?;
|
||||||
let shared_db = Arc::new(Mutex::new(db));
|
let shared_db = Arc::new(Mutex::new(db));
|
||||||
|
@ -142,19 +108,9 @@ async fn main() -> Result<()> {
|
||||||
.map(|d| Arc::new(d))
|
.map(|d| Arc::new(d))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let scheduler = Scheduler::default();
|
|
||||||
setup_tasmota_tasks(&scheduler, tasmota_plugs.clone(), shared_db.clone());
|
|
||||||
let scheduler_clone = scheduler.clone();
|
|
||||||
|
|
||||||
try_join!(
|
try_join!(
|
||||||
scheduler.run(),
|
read_power_usage(tasmota_plugs.clone(), shared_db.clone()),
|
||||||
run_web_server(
|
run_web_server(devices, tasmota_plugs, shared_db, dishwasher)
|
||||||
devices,
|
|
||||||
tasmota_plugs,
|
|
||||||
shared_db,
|
|
||||||
dishwasher,
|
|
||||||
scheduler_clone
|
|
||||||
)
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -11,10 +11,10 @@ enum LoginInfo {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LoginInfo {
|
impl LoginInfo {
|
||||||
const MIDEA_KEY_EMAIL: &'static str = "midea_cloud_mail";
|
const MIDEA_KEY_EMAIL: &str = "midea_cloud_mail";
|
||||||
const MIDEA_KEY_PW: &'static str = "midea_cloud_pw";
|
const MIDEA_KEY_PW: &str = "midea_cloud_pw";
|
||||||
const MIDEA_KEY_TOKEN: &'static str = "midea_token";
|
const MIDEA_KEY_TOKEN: &str = "midea_token";
|
||||||
const MIDEA_KEY_KEY: &'static str = "midea_key";
|
const MIDEA_KEY_KEY: &str = "midea_key";
|
||||||
|
|
||||||
fn new(db: &Arc<Mutex<DataBase>>, device_id: u64) -> Result<LoginInfo> {
|
fn new(db: &Arc<Mutex<DataBase>>, device_id: u64) -> Result<LoginInfo> {
|
||||||
let db_lock = db.lock().unwrap();
|
let db_lock = db.lock().unwrap();
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -96,55 +96,55 @@ impl Tasmota {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[cfg(test)]
|
#[cfg(test)]
|
||||||
// mod test {
|
mod test {
|
||||||
// use std::{thread, time::Duration};
|
use std::{thread, time::Duration};
|
||||||
|
|
||||||
// use super::*;
|
use super::*;
|
||||||
|
|
||||||
// use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
// #[tokio::test]
|
#[tokio::test]
|
||||||
// async fn test_connection() -> Result<()> {
|
async fn test_connection() -> Result<()> {
|
||||||
// let dev = Tasmota::new("Tasmota-Plug-1");
|
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]
|
#[tokio::test]
|
||||||
// async fn test_toggle() -> Result<()> {
|
async fn test_toggle() -> Result<()> {
|
||||||
// let dev = Tasmota::new("Tasmota-Plug-4");
|
let dev = Tasmota::new("Tasmota-Plug-4");
|
||||||
|
|
||||||
// dev.switch_off().await?;
|
dev.switch_off().await?;
|
||||||
// assert_eq!(dev.power_state().await?, false);
|
assert_eq!(dev.power_state().await?, false);
|
||||||
|
|
||||||
// thread::sleep(Duration::from_secs(5));
|
thread::sleep(Duration::from_secs(5));
|
||||||
|
|
||||||
// dev.switch_on().await?;
|
dev.switch_on().await?;
|
||||||
// assert_eq!(dev.power_state().await?, true);
|
assert_eq!(dev.power_state().await?, true);
|
||||||
|
|
||||||
// Ok(())
|
Ok(())
|
||||||
// }
|
}
|
||||||
|
|
||||||
// #[tokio::test]
|
#[tokio::test]
|
||||||
// async fn test_led() -> Result<()> {
|
async fn test_led() -> Result<()> {
|
||||||
// let dev = Tasmota::new("Tasmota-Plug-4");
|
let dev = Tasmota::new("Tasmota-Plug-4");
|
||||||
|
|
||||||
// dev.turn_off_led().await?;
|
dev.turn_off_led().await?;
|
||||||
// assert_eq!(dev.led_state().await?, false);
|
assert_eq!(dev.led_state().await?, false);
|
||||||
|
|
||||||
// thread::sleep(Duration::from_secs(5));
|
thread::sleep(Duration::from_secs(5));
|
||||||
|
|
||||||
// dev.turn_on_led().await?;
|
dev.turn_on_led().await?;
|
||||||
// assert_eq!(dev.led_state().await?, true);
|
assert_eq!(dev.led_state().await?, true);
|
||||||
|
|
||||||
// Ok(())
|
Ok(())
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
|
|
@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(())
|
|
||||||
// }
|
|
||||||
// }
|
|
|
@ -1,19 +1,13 @@
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
get, post,
|
get, post,
|
||||||
web::{Data, Json, Path},
|
web::{Data, Json, Path},
|
||||||
Error, HttpRequest, Responder, ResponseError,
|
Error, 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::{
|
use crate::{db::DataBase, tasmota::Tasmota};
|
||||||
action::{Action, ActionSet, ActionType},
|
|
||||||
db::DataBase,
|
|
||||||
task_scheduler::Scheduler,
|
|
||||||
tasmota::Tasmota,
|
|
||||||
temperature::{Thermometer, ThermometerChange},
|
|
||||||
};
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
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(¶meter)
|
|
||||||
}) {
|
|
||||||
// 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)>
|
fn collapse_data<F>(data: Vec<(u64, f32)>, f: F) -> Vec<(u64, f32)>
|
||||||
where
|
where
|
||||||
F: Fn(NaiveDateTime) -> NaiveDateTime,
|
F: Fn(NaiveDateTime) -> NaiveDateTime,
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
dkvF6ax77CP4v9-sxYyjWUD-9NunpzVPRYHfLJ8a9ps
|
|
Loading…
Reference in a new issue