Initial commit
This commit is contained in:
commit
09ab124103
11 changed files with 336 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
|
||||||
|
*.db
|
7
.vscode/settings.json
vendored
Normal file
7
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"workbench.colorCustomizations": {
|
||||||
|
"activityBar.background": "#4C0C61",
|
||||||
|
"titleBar.activeBackground": "#6B1188",
|
||||||
|
"titleBar.activeForeground": "#FDFAFE"
|
||||||
|
}
|
||||||
|
}
|
13
.vscode/tasks.json
vendored
Normal file
13
.vscode/tasks.json
vendored
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"type": "cargo",
|
||||||
|
"command": "run",
|
||||||
|
"problemMatcher": [
|
||||||
|
"$rustc"
|
||||||
|
],
|
||||||
|
"label": "rust: cargo run"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
15
Cargo.toml
Normal file
15
Cargo.toml
Normal file
|
@ -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"] }
|
6
devices.conf
Normal file
6
devices.conf
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"plugs": [
|
||||||
|
"Dev1",
|
||||||
|
"Dev2"
|
||||||
|
]
|
||||||
|
}
|
94
meross.py
Normal file
94
meross.py
Normal file
|
@ -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()
|
21
plot_meross.py
Normal file
21
plot_meross.py
Normal file
|
@ -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()
|
28
src/db.rs
Normal file
28
src/db.rs
Normal file
|
@ -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<Path>) -> Result<Self> {
|
||||||
|
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!()
|
||||||
|
}
|
||||||
|
}
|
37
src/devices.rs
Normal file
37
src/devices.rs
Normal file
|
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Devices {
|
||||||
|
pub async fn read(file: &str) -> Result<Self> {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
52
src/main.rs
Normal file
52
src/main.rs
Normal file
|
@ -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<u64> {
|
||||||
|
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<Tasmota> = 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));
|
||||||
|
}
|
||||||
|
}
|
59
src/tasmota.rs
Normal file
59
src/tasmota.rs
Normal file
|
@ -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<String> {
|
||||||
|
Ok(reqwest::Client::new()
|
||||||
|
.post(&self.command(command))
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.text()
|
||||||
|
.await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get(&self, command: &str) -> Result<String> {
|
||||||
|
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<f32> {
|
||||||
|
todo!("Status=8")
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue