commit 09ab124103643e54899c4fd0bd0b46284f30e97d Author: hodasemi Date: Tue Sep 19 10:52:12 2023 +0200 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5da5de5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/target +Cargo.lock + +*.db \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..d7e496a --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "workbench.colorCustomizations": { + "activityBar.background": "#4C0C61", + "titleBar.activeBackground": "#6B1188", + "titleBar.activeForeground": "#FDFAFE" + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..ffef723 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,13 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "type": "cargo", + "command": "run", + "problemMatcher": [ + "$rustc" + ], + "label": "rust: cargo run" + } + ] +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..05d3953 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "home_server" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rusqlite = "0.29.0" +anyhow = { version = "1.0.71", features = ["backtrace"] } +reqwest = "0.11.20" +serde = { version="1.0", features = ["derive"] } +serde_json = "1.0" +futures = "0.3.28" +tokio = { version="1.32.0", features=["macros", "rt-multi-thread"] } \ No newline at end of file diff --git a/devices.conf b/devices.conf new file mode 100644 index 0000000..0301acd --- /dev/null +++ b/devices.conf @@ -0,0 +1,6 @@ +{ + "plugs": [ + "Dev1", + "Dev2" + ] +} \ No newline at end of file diff --git a/meross.py b/meross.py new file mode 100644 index 0000000..2b99c51 --- /dev/null +++ b/meross.py @@ -0,0 +1,94 @@ +import asyncio +import os +import datetime +import time +import sqlite3 + +from meross_iot.controller.mixins.electricity import ElectricityMixin +from meross_iot.http_api import MerossHttpClient +from meross_iot.manager import MerossManager + +EMAIL = os.environ.get('MEROSS_EMAIL') or "superschneider@t-online.de" +PASSWORD = os.environ.get('MEROSS_PASSWORD') or "hodasemi1" + + +async def main(): + # Setup the HTTP client API from user-password + http_api_client = await MerossHttpClient.async_from_user_password( + api_base_url='https://iotx-eu.meross.com', + email=EMAIL, + password=PASSWORD + ) + + # Setup and start the device manager + manager = MerossManager(http_client=http_api_client) + await manager.async_init() + + # Retrieve all the devices that implement the electricity mixin + await manager.async_device_discovery() + devs = manager.find_devices(device_class=ElectricityMixin) + + if len(devs) < 1: + print("No electricity-capable device found...") + else: + dev = devs[0] + + # Update device status: this is needed only the very first time we play with this device (or if the + # connection goes down) + await dev.async_update() + + con = connect_to_db() + + while True: + # Read the electricity power/voltage/current + instant_consumption = await dev.async_get_instant_metrics() + + insert_into_db(con, instant_consumption.power) + + time.sleep(3.0) + + # Close the manager and logout from http_api + manager.close() + await http_api_client.async_logout() + + +def connect_to_db(): + con = sqlite3.connect("data.db") + + cur = con.cursor() + + cur.execute(""" + CREATE TABLE IF NOT EXISTS data ( + id INTEGER PRIMARY KEY, + time INTEGER NOT NULL, + watts REAL NOT NULL + )""") + + con.commit() + + return con + + +def insert_into_db(con, watts): + now = datetime.datetime.now() + unix_time = time.mktime(now.timetuple()) * 1000 + + date = (int(unix_time), watts) + + cur = con.cursor() + + cur.execute(""" + INSERT INTO data (time, watts) + VALUES (?, ?) + """, + date) + + con.commit() + + +if __name__ == '__main__': + if os.name == 'nt': + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + loop.stop() diff --git a/plot_meross.py b/plot_meross.py new file mode 100644 index 0000000..73eebf6 --- /dev/null +++ b/plot_meross.py @@ -0,0 +1,21 @@ +import sqlite3 +import matplotlib.pyplot as plt +import numpy as np + +con = sqlite3.connect("data.db") +cur = con.cursor() + +res = cur.execute("SELECT time, watts FROM data") + +x_values = [] +y_values = [] + +for time, watts in res: + x_values.append(time) + y_values.append(watts) + +plt.plot(x_values, y_values) + +plt.ylim([0, 200]) + +plt.show() \ No newline at end of file diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..dae71c1 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,28 @@ +use std::path::Path; + +use anyhow::Result; +use rusqlite::Connection; + +pub struct DataBase { + sql: Connection, +} + +impl DataBase { + pub async fn new(path: impl AsRef) -> Result { + let me = Self { + sql: Connection::open(path)?, + }; + + me.generate_tables()?; + + Ok(me) + } + + fn generate_tables(&self) -> Result<()> { + todo!() + } + + pub async fn write(&self, device_name: &str, time: u64, watts: f32) -> Result<()> { + todo!() + } +} diff --git a/src/devices.rs b/src/devices.rs new file mode 100644 index 0000000..84a868a --- /dev/null +++ b/src/devices.rs @@ -0,0 +1,37 @@ +use std::fs; + +use anyhow::Result; +use serde::{Deserialize, Serialize}; +use serde_json::{from_str, to_string_pretty}; + +#[derive(Clone, PartialEq, Eq, Deserialize, Serialize, Debug)] +pub struct Devices { + pub plugs: Vec, +} + +impl Devices { + pub async fn read(file: &str) -> Result { + Ok(from_str(&fs::read_to_string(file)?)?) + } + + pub fn save(&self, file: &str) -> Result<()> { + fs::write("devices.conf", to_string_pretty(self)?)?; + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::Devices; + use anyhow::Result; + + #[test] + fn create_conf() -> Result<()> { + let devices = Devices { + plugs: vec!["Dev1".to_string(), "Dev2".to_string()], + }; + + devices.save("devices.conf") + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..6ff87c8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,52 @@ +use std::{ + sync::{Arc, Mutex}, + thread, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; + +use crate::db::DataBase; + +mod db; +mod devices; +mod tasmota; + +use anyhow::Result; +use devices::Devices; +use futures::{future::try_join_all, try_join}; +use tasmota::Tasmota; + +fn since_epoch() -> Result { + Ok(SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs()) +} + +#[tokio::main] +async fn main() -> Result<()> { + let db_future = DataBase::new("home_server.db"); + let devices_future = Devices::read("devices.conf"); + + let (db, devices) = try_join!(db_future, devices_future)?; + + let shared_db = Arc::new(Mutex::new(db)); + let tasmota_plugs: Vec = devices + .plugs + .iter() + .map(|plug| Tasmota::new(plug)) + .collect(); + + loop { + try_join_all(tasmota_plugs.iter().map(|plug| async { + let usage = plug.read_power_usage().await?; + + shared_db + .lock() + .unwrap() + .write(plug.name(), since_epoch()?, usage) + .await?; + + Ok::<(), anyhow::Error>(()) + })) + .await?; + + thread::sleep(Duration::from_secs(3)); + } +} diff --git a/src/tasmota.rs b/src/tasmota.rs new file mode 100644 index 0000000..f0d9460 --- /dev/null +++ b/src/tasmota.rs @@ -0,0 +1,59 @@ +use anyhow::Result; + +pub struct Tasmota { + device: String, +} + +impl Tasmota { + pub fn new(device: impl ToString) -> Self { + Self { + device: device.to_string(), + } + } + + pub fn name(&self) -> &str { + &self.device + } + + fn command(&self, command: &str) -> String { + format!("http://{}/cm?cmnd={}", self.device, command) + } + + async fn post(&self, command: &str) -> Result { + Ok(reqwest::Client::new() + .post(&self.command(command)) + .send() + .await? + .text() + .await?) + } + + async fn get(&self, command: &str) -> Result { + Ok(reqwest::Client::new() + .get(&self.command(command)) + .send() + .await? + .text() + .await?) + } + + pub async fn turn_on_led(&self) -> Result<()> { + todo!("LedPower=1") + } + + pub async fn turn_off_led(&self) -> Result<()> { + todo!("LedPower=2") + } + + pub async fn switch_on(&self) -> Result<()> { + todo!("Power0=1") + } + + pub async fn switch_off(&self) -> Result<()> { + todo!("Power0=0") + } + + pub async fn read_power_usage(&self) -> Result { + todo!("Status=8") + } +}