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
|
||||
|
||||
[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"
|
1
build.sh
1
build.sh
|
@ -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/
|
|
@ -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"
|
||||
]
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
])));
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ class PlugSettingsArguments {
|
|||
}
|
||||
|
||||
class PlugSettings extends StatefulWidget {
|
||||
const PlugSettings({super.key});
|
||||
PlugSettings({super.key});
|
||||
|
||||
@override
|
||||
State<PlugSettings> createState() => _PlugSettingsState();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -2,12 +2,5 @@
|
|||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"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(())
|
||||
}
|
||||
}
|
291
src/db.rs
291
src/db.rs
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
84
src/main.rs
84
src/main.rs
|
@ -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(())
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)]
|
||||
// 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)]
|
||||
|
|
|
@ -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::{
|
||||
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(¶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)>
|
||||
where
|
||||
F: Fn(NaiveDateTime) -> NaiveDateTime,
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
dkvF6ax77CP4v9-sxYyjWUD-9NunpzVPRYHfLJ8a9ps
|
Loading…
Reference in a new issue