Set up thermometer
This commit is contained in:
parent
11e4a22307
commit
d69610fc01
17 changed files with 312 additions and 118 deletions
|
@ -18,3 +18,4 @@ chrono = "0.4.31"
|
|||
actix-web = "4.4.0"
|
||||
midea = { git = "https://gavania.de/hodasemi/Midea.git" }
|
||||
actix-cors = "0.6.4"
|
||||
dns-lookup = "2.0.4"
|
||||
|
|
|
@ -4,5 +4,14 @@
|
|||
["Tasmota-Plug-2", false],
|
||||
["Tasmota-Plug-3", true],
|
||||
["Tasmota-Plug-4", true]
|
||||
],
|
||||
"thermostat": [
|
||||
"shellytrv-8CF681A1F886",
|
||||
"shellytrv-8CF681E9BAEE",
|
||||
"shellytrv-B4E3F9D9E2A1"
|
||||
],
|
||||
"thermometer": [
|
||||
"shellyplusht-d4d4da7d85b4",
|
||||
"shellyplusht-80646fc9db9c"
|
||||
]
|
||||
}
|
|
@ -30,14 +30,14 @@ class Category {
|
|||
final Map<String, List<dynamic>> json =
|
||||
Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
||||
|
||||
for (MapEntry<String, List<dynamic>> entry in json.entries) {
|
||||
for (final MapEntry<String, List<dynamic>> entry in json.entries) {
|
||||
final Category category = Category(entry.key);
|
||||
|
||||
for (dynamic device_info_dyn in entry.value) {
|
||||
final Map<String, dynamic> device_info = device_info_dyn;
|
||||
for (final dynamic device_info_dyn in entry.value) {
|
||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||
|
||||
category.devices
|
||||
.add(DeviceIdOnly(device_info['id'], device_info['desc']));
|
||||
.add(DeviceIdOnly(deviceInfo['id'], deviceInfo['desc']));
|
||||
}
|
||||
|
||||
if (category.devices.isNotEmpty) {
|
||||
|
@ -65,10 +65,10 @@ class Category {
|
|||
final Category category = Category('plugs');
|
||||
final List<dynamic> plugs = json['plugs']!;
|
||||
|
||||
for (dynamic device_info_dyn in plugs) {
|
||||
final Map<String, dynamic> device_info = device_info_dyn;
|
||||
for (final dynamic device_info_dyn in plugs) {
|
||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||
|
||||
category.devices.add(await Plug.create(device_info));
|
||||
category.devices.add(await Plug.create(deviceInfo));
|
||||
}
|
||||
|
||||
categories.add(category);
|
||||
|
@ -79,10 +79,10 @@ class Category {
|
|||
final Category category = Category('thermostat');
|
||||
final List<dynamic> thermostats = json['thermostat']!;
|
||||
|
||||
for (dynamic device_info_dyn in thermostats) {
|
||||
final Map<String, dynamic> device_info = device_info_dyn;
|
||||
for (final dynamic device_info_dyn in thermostats) {
|
||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||
|
||||
category.devices.add(await Thermostat.create(device_info));
|
||||
category.devices.add(await Thermostat.create(deviceInfo));
|
||||
}
|
||||
|
||||
categories.add(category);
|
||||
|
@ -91,13 +91,13 @@ class Category {
|
|||
// create temperature_and_humidity
|
||||
{
|
||||
final Category category = Category('temperature_and_humidity');
|
||||
final List<dynamic> temperature_and_humidities =
|
||||
final List<dynamic> temperatureAndHumidities =
|
||||
json['temperature_and_humidity']!;
|
||||
|
||||
for (dynamic device_info_dyn in temperature_and_humidities) {
|
||||
final Map<String, dynamic> device_info = device_info_dyn;
|
||||
for (final dynamic device_info_dyn in temperatureAndHumidities) {
|
||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||
|
||||
category.devices.add(await TemperatureHumidity.create(device_info));
|
||||
category.devices.add(await TemperatureHumidity.create(deviceInfo));
|
||||
}
|
||||
|
||||
categories.add(category);
|
||||
|
@ -106,12 +106,12 @@ class Category {
|
|||
// create dish_washer
|
||||
{
|
||||
final Category category = Category('dish_washer');
|
||||
final List<dynamic> dish_washer = json['dish_washer']!;
|
||||
final List<dynamic> dishWasher = json['dish_washer']!;
|
||||
|
||||
for (dynamic device_info_dyn in dish_washer) {
|
||||
final Map<String, dynamic> device_info = device_info_dyn;
|
||||
for (final dynamic device_info_dyn in dishWasher) {
|
||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||
|
||||
category.devices.add(await DishWasher.create(device_info));
|
||||
category.devices.add(await DishWasher.create(deviceInfo));
|
||||
}
|
||||
|
||||
categories.add(category);
|
||||
|
@ -120,12 +120,12 @@ class Category {
|
|||
// create washing_machines
|
||||
{
|
||||
final Category category = Category('washing_machines');
|
||||
final List<dynamic> washing_machines = json['washing_machines']!;
|
||||
final List<dynamic> washingMachines = json['washing_machines']!;
|
||||
|
||||
for (dynamic device_info_dyn in washing_machines) {
|
||||
final Map<String, dynamic> device_info = device_info_dyn;
|
||||
for (final dynamic device_info_dyn in washingMachines) {
|
||||
final Map<String, dynamic> deviceInfo = device_info_dyn;
|
||||
|
||||
category.devices.add(await WashingMachine.create(device_info));
|
||||
category.devices.add(await WashingMachine.create(deviceInfo));
|
||||
}
|
||||
|
||||
categories.add(category);
|
||||
|
@ -143,9 +143,9 @@ class Category {
|
|||
}
|
||||
|
||||
class CategoryWidget extends StatelessWidget {
|
||||
final Category category;
|
||||
|
||||
CategoryWidget({super.key, required this.category});
|
||||
const CategoryWidget({super.key, required this.category});
|
||||
final Category category;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
@ -169,6 +169,7 @@ class DeviceIdOnly extends Device {
|
|||
final String device_id;
|
||||
final String? device_descriptor;
|
||||
|
||||
@override
|
||||
Widget create_widget(BuildContext context) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
|
|
@ -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> device_info) async {
|
||||
static Future<DishWasher> create(Map<String, dynamic> deviceInfo) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,9 @@ import '../states/plug_settings.dart';
|
|||
import 'devices.dart';
|
||||
|
||||
class Plug extends Device {
|
||||
|
||||
Plug(this.device_id, this.device_descriptor, this.led_state, this.power_state,
|
||||
this.power_draw, this.power_control);
|
||||
final String device_id;
|
||||
String? device_descriptor;
|
||||
bool led_state;
|
||||
|
@ -16,40 +19,37 @@ class Plug extends Device {
|
|||
double power_draw;
|
||||
bool power_control;
|
||||
|
||||
static Future<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'];
|
||||
static Future<Plug> create(Map<String, dynamic> deviceInfo) async {
|
||||
final String deviceId = deviceInfo['id'];
|
||||
final String? deviceDescriptor = deviceInfo['desc'];
|
||||
final bool powerControl = deviceInfo['toggle'];
|
||||
|
||||
final response = await http
|
||||
.get(Uri.parse("${Constants.BASE_URL}/plug_state/$device_id"));
|
||||
.get(Uri.parse("${Constants.BASE_URL}/plug_state/$deviceId"));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Failed to fetch plug_state for $device_id");
|
||||
throw Exception("Failed to fetch plug_state for $deviceId");
|
||||
}
|
||||
|
||||
final Map<String, dynamic> device_state =
|
||||
final Map<String, dynamic> deviceState =
|
||||
Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
||||
|
||||
return Plug(
|
||||
device_id,
|
||||
device_descriptor,
|
||||
device_state["led"],
|
||||
device_state["power"],
|
||||
device_state["power_draw"],
|
||||
power_control && device_state["power_draw"] < 15,
|
||||
deviceId,
|
||||
deviceDescriptor,
|
||||
deviceState["led"],
|
||||
deviceState["power"],
|
||||
deviceState["power_draw"],
|
||||
powerControl && deviceState["power_draw"] < 15,
|
||||
);
|
||||
}
|
||||
|
||||
Plug(this.device_id, this.device_descriptor, this.led_state, this.power_state,
|
||||
this.power_draw, this.power_control);
|
||||
|
||||
@override
|
||||
Widget create_widget(BuildContext context) {
|
||||
const double header_height = 40;
|
||||
const double info_height = 30;
|
||||
const double info_width = 60;
|
||||
const double x_offset = 0.9;
|
||||
const double headerHeight = 40;
|
||||
const double infoHeight = 30;
|
||||
const double infoWidth = 60;
|
||||
const double xOffset = 0.9;
|
||||
|
||||
return Table(
|
||||
border: TableBorder(
|
||||
|
@ -67,7 +67,7 @@ class Plug extends Device {
|
|||
),
|
||||
children: [
|
||||
SizedBox(
|
||||
height: header_height,
|
||||
height: headerHeight,
|
||||
child: Stack(
|
||||
children: [
|
||||
Align(
|
||||
|
@ -92,7 +92,7 @@ class Plug extends Device {
|
|||
]),
|
||||
TableRow(children: [
|
||||
SizedBox(
|
||||
height: info_height,
|
||||
height: infoHeight,
|
||||
child: Stack(children: [
|
||||
const Align(
|
||||
alignment: Alignment(-0.9, 0.0),
|
||||
|
@ -101,16 +101,16 @@ class Plug extends Device {
|
|||
textAlign: TextAlign.center,
|
||||
"LED")),
|
||||
Align(
|
||||
alignment: const Alignment(x_offset, 0.0),
|
||||
alignment: const Alignment(xOffset, 0.0),
|
||||
child: SizedBox(
|
||||
width: info_width,
|
||||
width: infoWidth,
|
||||
child: PlugLed(
|
||||
device_id: device_id, led_state: led_state))),
|
||||
]))
|
||||
]),
|
||||
TableRow(children: [
|
||||
SizedBox(
|
||||
height: info_height,
|
||||
height: infoHeight,
|
||||
child: Stack(children: [
|
||||
const Align(
|
||||
alignment: Alignment(-0.9, 0.0),
|
||||
|
@ -119,9 +119,9 @@ class Plug extends Device {
|
|||
textAlign: TextAlign.center,
|
||||
"Power")),
|
||||
Align(
|
||||
alignment: const Alignment(x_offset, 0.0),
|
||||
alignment: const Alignment(xOffset, 0.0),
|
||||
child: SizedBox(
|
||||
width: info_width,
|
||||
width: infoWidth,
|
||||
child: PlugPower(
|
||||
device_id: device_id,
|
||||
power_state: power_state,
|
||||
|
@ -130,7 +130,7 @@ class Plug extends Device {
|
|||
]),
|
||||
TableRow(children: [
|
||||
SizedBox(
|
||||
height: info_height,
|
||||
height: infoHeight,
|
||||
child: Stack(children: [
|
||||
const Align(
|
||||
alignment: Alignment(-0.9, 0.0),
|
||||
|
@ -139,9 +139,9 @@ class Plug extends Device {
|
|||
textAlign: TextAlign.center,
|
||||
"Power Draw")),
|
||||
Align(
|
||||
alignment: const Alignment(x_offset, 0.0),
|
||||
alignment: const Alignment(xOffset, 0.0),
|
||||
child: SizedBox(
|
||||
width: info_width,
|
||||
width: infoWidth,
|
||||
child: Text(
|
||||
textAlign: TextAlign.center, "$power_draw W")))
|
||||
]))
|
||||
|
@ -151,10 +151,10 @@ class Plug extends Device {
|
|||
}
|
||||
|
||||
class PlugLed extends StatefulWidget {
|
||||
final String device_id;
|
||||
bool led_state;
|
||||
|
||||
PlugLed({super.key, required this.device_id, required this.led_state});
|
||||
final String device_id;
|
||||
bool led_state;
|
||||
|
||||
@override
|
||||
State<PlugLed> createState() => PlugLedState();
|
||||
|
@ -164,17 +164,17 @@ class PlugLedState extends State<PlugLed> {
|
|||
String _led_state_info = "";
|
||||
|
||||
void _toggle_led_state() {
|
||||
String target_state;
|
||||
String targetState;
|
||||
|
||||
if (widget.led_state) {
|
||||
target_state = "off";
|
||||
targetState = "off";
|
||||
} else {
|
||||
target_state = "on";
|
||||
targetState = "on";
|
||||
}
|
||||
|
||||
change_plug_state(widget.device_id, "led", target_state)
|
||||
.then((device_state) {
|
||||
widget.led_state = device_state["led"];
|
||||
change_plug_state(widget.device_id, "led", targetState)
|
||||
.then((deviceState) {
|
||||
widget.led_state = deviceState["led"];
|
||||
|
||||
setState(() {
|
||||
_led_state_info = widget.led_state ? "On" : "Off";
|
||||
|
@ -193,15 +193,15 @@ class PlugLedState extends State<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 target_state;
|
||||
String targetState;
|
||||
|
||||
if (widget.power_state) {
|
||||
target_state = "off";
|
||||
targetState = "off";
|
||||
} else {
|
||||
target_state = "on";
|
||||
targetState = "on";
|
||||
}
|
||||
|
||||
change_plug_state(widget.device_id, "power", target_state)
|
||||
.then((device_state) {
|
||||
widget.power_state = device_state["power"];
|
||||
change_plug_state(widget.device_id, "power", targetState)
|
||||
.then((deviceState) {
|
||||
widget.power_state = deviceState["power"];
|
||||
|
||||
widget.power_control = device_state["power_draw"] < 15;
|
||||
widget.power_control = deviceState["power_draw"] < 15;
|
||||
|
||||
setState(() {
|
||||
_power_state_info = widget.power_state ? "On" : "Off";
|
||||
|
@ -246,19 +246,19 @@ class PlugPowerState extends State<PlugPower> {
|
|||
}
|
||||
|
||||
Future<Map<String, dynamic>> change_plug_state(
|
||||
String device_id, String module, String state) async {
|
||||
String deviceId, String module, String state) async {
|
||||
var response = await http.post(
|
||||
Uri.parse("${Constants.BASE_URL}/plug/$device_id/${module}_$state"));
|
||||
Uri.parse("${Constants.BASE_URL}/plug/$deviceId/${module}_$state"));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Failed to post new state");
|
||||
}
|
||||
|
||||
response =
|
||||
await http.get(Uri.parse("${Constants.BASE_URL}/plug_state/$device_id"));
|
||||
await http.get(Uri.parse("${Constants.BASE_URL}/plug_state/$deviceId"));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Failed to fetch plug_state for $device_id");
|
||||
throw Exception("Failed to fetch plug_state for $deviceId");
|
||||
}
|
||||
|
||||
return Map.castFrom(jsonDecode(jsonDecode(response.body)));
|
||||
|
|
|
@ -4,7 +4,7 @@ import 'devices.dart';
|
|||
|
||||
class TemperatureHumidity extends Device {
|
||||
static Future<TemperatureHumidity> create(
|
||||
Map<String, dynamic> device_info) async {
|
||||
Map<String, dynamic> deviceInfo) 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> device_info) async {
|
||||
static Future<Thermostat> create(Map<String, dynamic> deviceInfo) 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> device_info) async {
|
||||
static Future<WashingMachine> create(Map<String, dynamic> deviceInfo) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'constants.dart';
|
||||
|
||||
import 'states/graphs.dart';
|
||||
import 'states/home_page.dart';
|
||||
import 'states/plug_settings.dart';
|
||||
import 'states/graphs.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
|
@ -24,8 +23,8 @@ class MyApp extends StatelessWidget {
|
|||
),
|
||||
home: const MyHomePage(title: 'Home Server'),
|
||||
routes: <String, WidgetBuilder>{
|
||||
'/plug_settings': (BuildContext context) => PlugSettings(),
|
||||
'/graphs': (BuildContext context) => Graphs(),
|
||||
'/plug_settings': (BuildContext context) => const PlugSettings(),
|
||||
'/graphs': (BuildContext context) => const Graphs(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -173,45 +173,45 @@ class _GraphsState extends State<Graphs> {
|
|||
ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
for (var device in devices) {
|
||||
for (final device in devices) {
|
||||
if (!device.enabled) {
|
||||
device.data = List.empty();
|
||||
continue;
|
||||
}
|
||||
|
||||
int start_date;
|
||||
int end_date;
|
||||
int startDate;
|
||||
int endDate;
|
||||
|
||||
try {
|
||||
start_date = (DateFormat('dd.MM.yyyy')
|
||||
startDate = (DateFormat('dd.MM.yyyy')
|
||||
.parse(startDateController.text)
|
||||
.millisecondsSinceEpoch /
|
||||
1000) as int;
|
||||
} on Exception catch (_) {
|
||||
start_date = 0;
|
||||
startDate = 0;
|
||||
print('no start date set');
|
||||
}
|
||||
|
||||
try {
|
||||
end_date = (DateFormat('dd.MM.yyyy')
|
||||
endDate = (DateFormat('dd.MM.yyyy')
|
||||
.parse(endDateController.text)
|
||||
.millisecondsSinceEpoch /
|
||||
1000) as int;
|
||||
} on Exception catch (_) {
|
||||
end_date = 0;
|
||||
endDate = 0;
|
||||
print('no end date set');
|
||||
}
|
||||
|
||||
if (start_date == 0 || end_date == 0) {
|
||||
if (startDate == 0 || endDate == 0) {
|
||||
device.data = List.empty();
|
||||
continue;
|
||||
}
|
||||
|
||||
const String filter_type = 'hourly';
|
||||
const String filterType = 'hourly';
|
||||
|
||||
http
|
||||
.get(Uri.parse(
|
||||
"${Constants.BASE_URL}/plug_data/${device.device.device_id}/$start_date/$end_date/$filter_type"))
|
||||
"${Constants.BASE_URL}/plug_data/${device.device.device_id}/$startDate/$endDate/$filterType"))
|
||||
.then((response) {
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception("Failed to fetch data");
|
||||
|
@ -220,14 +220,14 @@ class _GraphsState extends State<Graphs> {
|
|||
final data = jsonDecode(jsonDecode(response.body))
|
||||
as List<dynamic>;
|
||||
|
||||
final List<ChartData> chart_data = [];
|
||||
final List<ChartData> chartData = [];
|
||||
|
||||
for (final entry in data) {
|
||||
final pair = entry as List<dynamic>;
|
||||
chart_data.add(ChartData(pair[0], pair[1]));
|
||||
chartData.add(ChartData(pair[0], pair[1]));
|
||||
}
|
||||
|
||||
device.data = chart_data;
|
||||
device.data = chartData;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -258,7 +258,6 @@ class _GraphsState extends State<Graphs> {
|
|||
// titlesData: FlTitlesData(bottomTitles: AxisTitles(sideTitles: ))
|
||||
),
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.linear,
|
||||
)
|
||||
])));
|
||||
});
|
||||
|
|
|
@ -28,15 +28,15 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||
}
|
||||
|
||||
final data = categories.data!;
|
||||
final category_count = data.length;
|
||||
final categoryCount = data.length;
|
||||
|
||||
if (category_count > expanded.length) {
|
||||
final int diff = category_count - expanded.length;
|
||||
if (categoryCount > expanded.length) {
|
||||
final int diff = categoryCount - expanded.length;
|
||||
|
||||
final List<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;
|
||||
final List<bool> diffList = List<bool>.filled(diff, true);
|
||||
expanded.addAll(diffList);
|
||||
} else if (categoryCount < expanded.length) {
|
||||
final int diff = expanded.length - categoryCount;
|
||||
|
||||
expanded = List<bool>.filled(diff, false);
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ class PlugSettingsArguments {
|
|||
}
|
||||
|
||||
class PlugSettings extends StatefulWidget {
|
||||
PlugSettings({super.key});
|
||||
const PlugSettings({super.key});
|
||||
|
||||
@override
|
||||
State<PlugSettings> createState() => _PlugSettingsState();
|
||||
|
|
78
src/db.rs
78
src/db.rs
|
@ -47,7 +47,8 @@ impl DataBase {
|
|||
"CREATE TABLE IF NOT EXISTS data (
|
||||
id INTEGER PRIMARY KEY,
|
||||
time BIGINT NOT NULL,
|
||||
watts REAL NOT NULL,
|
||||
name VARCHAR(30) NOT NULL,
|
||||
value REAL NOT NULL,
|
||||
device_id INTEGER NOT NULL,
|
||||
FOREIGN KEY(device_id) REFERENCES devices(id)
|
||||
)",
|
||||
|
@ -116,16 +117,50 @@ impl DataBase {
|
|||
)?;
|
||||
}
|
||||
|
||||
for device in devices.thermostat.iter() {
|
||||
self.sql.execute(
|
||||
&format!(
|
||||
"INSERT INTO devices (device, type, control)
|
||||
SELECT \"{device}\", \"thermostat\", false
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT device
|
||||
FROM devices
|
||||
WHERE device=\"{device}\"
|
||||
)
|
||||
"
|
||||
),
|
||||
[],
|
||||
)?;
|
||||
}
|
||||
|
||||
for device in devices.thermometer.iter() {
|
||||
self.sql.execute(
|
||||
&format!(
|
||||
"INSERT INTO devices (device, type, control)
|
||||
SELECT \"{device}\", \"thermometer\", false
|
||||
WHERE
|
||||
NOT EXISTS (
|
||||
SELECT device
|
||||
FROM devices
|
||||
WHERE device=\"{device}\"
|
||||
)
|
||||
"
|
||||
),
|
||||
[],
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write(&self, device_name: &str, time: u64, watts: f32) -> Result<()> {
|
||||
let params: &[&dyn ToSql] = &[&time, &watts];
|
||||
pub fn write(&self, device_name: &str, time: u64, name: &str, value: f32) -> Result<()> {
|
||||
let params: &[&dyn ToSql] = &[&time, &value];
|
||||
|
||||
self.sql.execute(
|
||||
&format!(
|
||||
"INSERT INTO data (time, watts, device_id)
|
||||
VALUES (?1, ?2, (SELECT id FROM devices WHERE device=\"{device_name}\") )"
|
||||
"INSERT INTO data (time, name, value, device_id)
|
||||
VALUES (?1, \"{name}\", ?2, (SELECT id FROM devices WHERE device=\"{device_name}\") )"
|
||||
),
|
||||
params,
|
||||
)?;
|
||||
|
@ -156,6 +191,16 @@ impl DataBase {
|
|||
desc: name,
|
||||
toggle: control != 0,
|
||||
}),
|
||||
"thermostat" => devices.thermostat.push(DeviceWithName {
|
||||
id: device,
|
||||
desc: name,
|
||||
toggle: control != 0,
|
||||
}),
|
||||
"thermometer" => devices.temperature_and_humidity.push(DeviceWithName {
|
||||
id: device,
|
||||
desc: name,
|
||||
toggle: control != 0,
|
||||
}),
|
||||
|
||||
_ => panic!(),
|
||||
}
|
||||
|
@ -164,6 +209,19 @@ impl DataBase {
|
|||
Ok(devices)
|
||||
}
|
||||
|
||||
pub fn device_exists(&self, device_id: &str) -> Result<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!(
|
||||
|
@ -182,7 +240,7 @@ impl DataBase {
|
|||
pub fn read(&self, device: &str) -> Result<Vec<(u64, f32)>> {
|
||||
self._read(&format!(
|
||||
"
|
||||
SELECT data.time, data.watts
|
||||
SELECT data.time, data.value
|
||||
FROM data
|
||||
INNER JOIN devices
|
||||
ON data.device_id=devices.id
|
||||
|
@ -194,7 +252,7 @@ impl DataBase {
|
|||
pub fn read_range(&self, device: &str, start: u64, end: u64) -> Result<Vec<(u64, f32)>> {
|
||||
self._read(&format!(
|
||||
"
|
||||
SELECT data.time, data.watts
|
||||
SELECT data.time, data.value
|
||||
FROM data
|
||||
INNER JOIN devices
|
||||
ON data.device_id=devices.id
|
||||
|
@ -293,6 +351,8 @@ mod test {
|
|||
|
||||
db.register_devices(&Devices {
|
||||
plugs: vec![("test".to_string(), true)],
|
||||
thermostat: Vec::new(),
|
||||
thermometer: Vec::new(),
|
||||
})?;
|
||||
|
||||
fs::remove_file("startup_test.db")?;
|
||||
|
@ -308,9 +368,11 @@ mod test {
|
|||
|
||||
db.register_devices(&Devices {
|
||||
plugs: vec![(device_name.to_string(), true)],
|
||||
thermostat: Vec::new(),
|
||||
thermometer: Vec::new(),
|
||||
})?;
|
||||
|
||||
db.write(device_name, 0, 5.5)?;
|
||||
db.write(device_name, 0, "watts", 5.5)?;
|
||||
|
||||
let device_descriptor = "udo";
|
||||
db.change_device_name(device_name, device_descriptor)?;
|
||||
|
|
|
@ -7,6 +7,8 @@ use serde_json::{from_str, to_string, to_string_pretty};
|
|||
#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Debug)]
|
||||
pub struct Devices {
|
||||
pub plugs: Vec<(String, bool)>,
|
||||
pub thermostat: Vec<String>,
|
||||
pub thermometer: Vec<String>,
|
||||
}
|
||||
|
||||
impl Devices {
|
||||
|
@ -55,6 +57,8 @@ mod test {
|
|||
fn create_conf() -> Result<()> {
|
||||
let devices = Devices {
|
||||
plugs: vec![("Dev1".to_string(), true), ("Dev2".to_string(), false)],
|
||||
thermostat: Vec::new(),
|
||||
thermometer: Vec::new(),
|
||||
};
|
||||
|
||||
devices.save("test_devices.conf")
|
||||
|
|
|
@ -12,6 +12,7 @@ mod db;
|
|||
mod devices;
|
||||
mod midea_helper;
|
||||
mod tasmota;
|
||||
mod temperature;
|
||||
mod tibber_handler;
|
||||
mod web_server;
|
||||
|
||||
|
@ -40,7 +41,7 @@ fn read_power_usage(
|
|||
if let Ok(usage) = plug.read_power_usage().await {
|
||||
db.lock()
|
||||
.unwrap()
|
||||
.write(plug.name(), since_epoch()?, usage)?;
|
||||
.write(plug.name(), since_epoch()?, "watts", usage)?;
|
||||
}
|
||||
|
||||
Ok::<(), anyhow::Error>(())
|
||||
|
@ -81,6 +82,8 @@ async fn run_web_server(
|
|||
.service(change_device_name)
|
||||
.service(plug_data)
|
||||
.service(plug_data_range)
|
||||
.service(push_temperature)
|
||||
.service(push_humidity)
|
||||
})
|
||||
.bind((IP, PORT))
|
||||
.map_err(|err| anyhow::Error::msg(format!("failed binding to address: {err:#?}")))?
|
||||
|
|
76
src/temperature.rs
Normal file
76
src/temperature.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use std::{
|
||||
net::IpAddr,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use actix_web::web::Data;
|
||||
use anyhow::{bail, Result};
|
||||
use dns_lookup::{lookup_addr, lookup_host};
|
||||
use reqwest::Client;
|
||||
|
||||
use crate::{db::DataBase, since_epoch};
|
||||
|
||||
pub struct Thermostat {
|
||||
device: String,
|
||||
}
|
||||
|
||||
impl Thermostat {
|
||||
pub fn new(device: impl ToString) -> Self {
|
||||
Self {
|
||||
device: device.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_temperature(&self, temperature: f32) -> Result<()> {
|
||||
let ips = lookup_host(&self.device)?;
|
||||
|
||||
if ips.is_empty() {
|
||||
bail!("could not resolve device name {}", self.device);
|
||||
}
|
||||
|
||||
let resp = Client::new()
|
||||
.post(format!("http://{}/ext_t?temp={}", ips[0], temperature))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
bail!("response error");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ThermometerChange {
|
||||
Temperature(f32),
|
||||
Humidity(f32),
|
||||
}
|
||||
|
||||
pub struct Thermometer;
|
||||
|
||||
impl Thermometer {
|
||||
pub fn push_change(
|
||||
change: ThermometerChange,
|
||||
ip: IpAddr,
|
||||
db: Data<Arc<Mutex<DataBase>>>,
|
||||
) -> Result<()> {
|
||||
let db_lock = db.lock().unwrap();
|
||||
let device_id = lookup_addr(&ip)?.trim_end_matches(".fritz.box").to_string();
|
||||
|
||||
if db_lock.device_exists(&device_id)? {
|
||||
match change {
|
||||
ThermometerChange::Temperature(temp) => {
|
||||
db_lock.write(&device_id, since_epoch()?, "temperature", temp)?;
|
||||
|
||||
// maybe push to thermostate
|
||||
}
|
||||
ThermometerChange::Humidity(humid) => {
|
||||
db_lock.write(&device_id, since_epoch()?, "humidity", humid)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,13 +1,17 @@
|
|||
use actix_web::{
|
||||
get, post,
|
||||
web::{Data, Json, Path},
|
||||
Error, Responder, ResponseError,
|
||||
Error, HttpRequest, Responder, ResponseError,
|
||||
};
|
||||
use chrono::{Datelike, NaiveDateTime, Timelike};
|
||||
use serde::Serialize;
|
||||
use serde_json::to_string;
|
||||
|
||||
use crate::{db::DataBase, tasmota::Tasmota};
|
||||
use crate::{
|
||||
db::DataBase,
|
||||
tasmota::Tasmota,
|
||||
temperature::{Thermometer, ThermometerChange},
|
||||
};
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
|
@ -226,6 +230,42 @@ async fn plug_data_range(
|
|||
)?))
|
||||
}
|
||||
|
||||
#[get("/push_temp/{temperature}")]
|
||||
async fn push_temperature(
|
||||
param: Path<f32>,
|
||||
req: HttpRequest,
|
||||
db: Data<Arc<Mutex<DataBase>>>,
|
||||
) -> Result<impl Responder, Error> {
|
||||
if let Some(val) = req.peer_addr() {
|
||||
Thermometer::push_change(
|
||||
ThermometerChange::Temperature(param.into_inner()),
|
||||
val.ip(),
|
||||
db,
|
||||
)
|
||||
.map_err(|err| MyError::from(err))?;
|
||||
}
|
||||
|
||||
Ok("Ok")
|
||||
}
|
||||
|
||||
#[get("/push_humid/{humidity}")]
|
||||
async fn push_humidity(
|
||||
param: Path<f32>,
|
||||
req: HttpRequest,
|
||||
db: Data<Arc<Mutex<DataBase>>>,
|
||||
) -> Result<impl Responder, Error> {
|
||||
if let Some(val) = req.peer_addr() {
|
||||
Thermometer::push_change(
|
||||
ThermometerChange::Humidity(param.into_inner()),
|
||||
val.ip(),
|
||||
db,
|
||||
)
|
||||
.map_err(|err| MyError::from(err))?;
|
||||
}
|
||||
|
||||
Ok("Ok")
|
||||
}
|
||||
|
||||
fn collapse_data<F>(data: Vec<(u64, f32)>, f: F) -> Vec<(u64, f32)>
|
||||
where
|
||||
F: Fn(NaiveDateTime) -> NaiveDateTime,
|
||||
|
|
Loading…
Reference in a new issue