use anyhow::Result; use serde::Deserialize; use serde_json::from_str; #[derive(Debug, Clone)] 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<'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<'a>(&self, command: impl IntoIterator) -> Result { Ok(reqwest::Client::new() .post(&self.command(command)) .send() .await? .text() .await?) } async fn get<'a>(&self, command: impl IntoIterator) -> Result { Ok(reqwest::Client::new() .get(&self.command(command)) .send() .await? .text() .await?) } pub async fn turn_on_led(&self) -> Result<()> { self.post(["LedPower", "1"]).await?; Ok(()) } pub async fn turn_off_led(&self) -> Result<()> { self.post(["LedPower", "0"]).await?; Ok(()) } pub async fn switch_on(&self) -> Result<()> { self.post(["Power0", "1"]).await?; Ok(()) } pub async fn switch_off(&self) -> Result<()> { self.post(["Power0", "0"]).await?; Ok(()) } pub async fn power_state(&self) -> Result { let res = self.get(["Power0"]).await?; 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" } }