From 4ae0c30171c18f49f4edf5687771e9c5c1263050 Mon Sep 17 00:00:00 2001 From: hodasemi Date: Wed, 20 Sep 2023 14:58:58 +0200 Subject: [PATCH] Test tasmota interface --- .vscode/launch.json | 16 +++++ devices.conf | 6 +- src/tasmota.rs | 161 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 170 insertions(+), 13 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..10efcb2 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Debug", + "program": "${workspaceFolder}/", + "args": [], + "cwd": "${workspaceFolder}" + } + ] +} \ No newline at end of file diff --git a/devices.conf b/devices.conf index 0301acd..9ed1d37 100644 --- a/devices.conf +++ b/devices.conf @@ -1,6 +1,8 @@ { "plugs": [ - "Dev1", - "Dev2" + "Tasmota-Plug-1", + "Tasmota-Plug-2", + "Tasmota-Plug-3", + "Tasmota-Plug-4" ] } \ No newline at end of file diff --git a/src/tasmota.rs b/src/tasmota.rs index a09094b..d971e36 100644 --- a/src/tasmota.rs +++ b/src/tasmota.rs @@ -1,4 +1,6 @@ use anyhow::Result; +use serde::Deserialize; +use serde_json::from_str; pub struct Tasmota { device: String, @@ -15,11 +17,18 @@ impl Tasmota { &self.device } - fn command(&self, command: &str) -> String { - format!("http://{}/cm?cmnd={}", self.device, command) + fn command<'a>(&self, command: impl IntoIterator) -> String { + let mut str = String::new(); + + for s in command.into_iter() { + str += s; + str += "%20"; + } + + format!("http://{}/cm?cmnd={}", self.device, str) } - async fn post(&self, command: &str) -> Result { + async fn post<'a>(&self, command: impl IntoIterator) -> Result { Ok(reqwest::Client::new() .post(&self.command(command)) .send() @@ -28,7 +37,7 @@ impl Tasmota { .await?) } - async fn get(&self, command: &str) -> Result { + async fn get<'a>(&self, command: impl IntoIterator) -> Result { Ok(reqwest::Client::new() .get(&self.command(command)) .send() @@ -38,32 +47,162 @@ impl Tasmota { } pub async fn turn_on_led(&self) -> Result<()> { - self.post("LedPower=1").await?; + self.post(["LedPower", "1"]).await?; Ok(()) } pub async fn turn_off_led(&self) -> Result<()> { - self.post("LedPower=2").await?; + self.post(["LedPower", "0"]).await?; Ok(()) } pub async fn switch_on(&self) -> Result<()> { - self.post("Power0=1").await?; + self.post(["Power0", "1"]).await?; Ok(()) } pub async fn switch_off(&self) -> Result<()> { - self.post("Power0=0").await?; + self.post(["Power0", "0"]).await?; Ok(()) } - pub async fn read_power_usage(&self) -> Result { - let res = self.get("Status=8").await?; + pub async fn power_state(&self) -> Result { + let res = self.get(["Power0"]).await?; - Ok(res.parse()?) + let state = PowerState::from_str(&res)?; + + Ok(state.is_on()) + } + + pub async fn read_power_usage(&self) -> Result { + let res = self.get(["Status", "8"]).await?; + + let status = Status::from_str(&res)?; + + Ok(status.StatusSNS.ENERGY.Power) + } + + pub async fn led_state(&self) -> Result { + let res = self.get(["LedState"]).await?; + + let state = LedState::from_str(&res)?; + + Ok(state.LedState != 0) + } +} + +#[cfg(test)] +mod test { + use std::{thread, time::Duration}; + + use super::*; + + use anyhow::Result; + + #[tokio::test] + async fn test_connection() -> Result<()> { + let dev = Tasmota::new("Tasmota-Plug-1"); + + let power = dev.read_power_usage().await?; + + println!("{power}"); + + Ok(()) + } + + #[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); + + thread::sleep(Duration::from_secs(5)); + + dev.switch_on().await?; + assert_eq!(dev.power_state().await?, true); + + Ok(()) + } + + #[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); + + thread::sleep(Duration::from_secs(5)); + + dev.turn_on_led().await?; + assert_eq!(dev.led_state().await?, true); + + Ok(()) + } +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct Status { + pub StatusSNS: StatusSNS, +} + +impl Status { + fn from_str(s: &str) -> Result { + Ok(from_str(s)?) + } +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct StatusSNS { + pub Time: String, + pub ENERGY: Energy, +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct Energy { + pub TotalStartTime: String, + pub Total: f32, + pub Yesterday: f32, + pub Today: f32, + pub Power: f32, + pub ApparentPower: u32, + pub ReactivePower: u32, + pub Factor: f32, + pub Voltage: u32, + pub Current: f32, +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct LedState { + LedState: u8, +} + +impl LedState { + fn from_str(s: &str) -> Result { + Ok(from_str(s)?) + } +} + +#[allow(non_snake_case)] +#[derive(Deserialize, Debug)] +pub struct PowerState { + POWER: String, +} + +impl PowerState { + fn from_str(s: &str) -> Result { + Ok(from_str(s)?) + } + + pub fn is_on(&self) -> bool { + self.POWER == "ON" } }