From 987150c9b630eb2b1540e4044db2f3b010b18c2b Mon Sep 17 00:00:00 2001 From: sigil-03 Date: Sun, 6 Apr 2025 14:34:15 -0600 Subject: [PATCH 1/2] make SET stub + move elements into system wrapper --- src/control.rs | 5 +++++ src/main.rs | 16 ++++++++++--- src/monitor.rs | 61 +------------------------------------------------- src/system.rs | 51 +++++++++++++++++++++++++++++++++++++++++ src/tasmota.rs | 20 +++++++++++++++-- src/types.rs | 20 +++++++++++++++++ 6 files changed, 108 insertions(+), 65 deletions(-) create mode 100644 src/control.rs create mode 100644 src/system.rs create mode 100644 src/types.rs diff --git a/src/control.rs b/src/control.rs new file mode 100644 index 0000000..2ecde08 --- /dev/null +++ b/src/control.rs @@ -0,0 +1,5 @@ +use crate::types::Error; + +pub trait Control { + async fn set_power(&self) -> Result<(), Error>; +} diff --git a/src/main.rs b/src/main.rs index 129371c..42e68a9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,31 @@ use clap::{Parser, Subcommand}; -use monitor::Monitor; +mod control; mod monitor; +mod system; mod tasmota; +mod types; #[derive(Subcommand)] pub enum Commands { Monitor, + #[command(subcommand)] + Set(types::PowerState), } impl Commands { pub async fn execute(self, config_file: &str) { let handle = match self { Self::Monitor => { - let m = Monitor::new_from_file(config_file).unwrap(); + let s = system::System::new_from_file(config_file).unwrap(); tokio::spawn(async move { - m.get_power().await.unwrap(); + s.get_power().await.unwrap(); + }) + } + Self::Set(state) => { + // let c = Controller::new_from_file(config_file).unwrap(); + tokio::spawn(async move { + println!("SET"); }) } }; diff --git a/src/monitor.rs b/src/monitor.rs index c43974a..6d121d9 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -1,68 +1,9 @@ use crate::tasmota::{PowerStatusData, StatusResponse, TasmotaInterface, TasmotaInterfaceConfig}; +use crate::types::Error; use reqwest::Client; use serde::Deserialize; use std::fs; -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("io error")] - IoError(#[from] std::io::Error), - #[error("toml parsing error")] - ParseError(#[from] toml::de::Error), - #[error("request error")] - RequestError(#[from] reqwest::Error), - #[error("JSON Parse error")] - JsonParseError(#[from] serde_json::Error), -} pub trait Monitoring { async fn get_power(&self) -> Result; } - -#[derive(Deserialize)] -pub struct MonitorConfig { - targets: Vec, -} -impl MonitorConfig { - fn print(&self) { - for t in &self.targets { - t.print(); - } - } -} - -pub struct Monitor { - targets: Vec, - client: Client, -} - -impl Monitor { - pub fn new_from_file(config_file: &str) -> Result { - let config_str = fs::read_to_string(config_file)?; - let config: MonitorConfig = toml::from_str(&config_str)?; - Ok(Self { - targets: Monitor::load_targets(&config.targets), - client: Client::new(), - }) - } - - pub fn load_targets(targets: &Vec) -> Vec { - let mut v = Vec::new(); - for target in targets { - v.push(TasmotaInterface::new(target.clone())); - } - v - } - - pub async fn get_power(&self) -> Result<(), Error> { - for target in &self.targets { - if let Ok(res) = target.get_power().await { - target.print(); - println!("* POWER: {}W", res); - println!("------------------") - } - } - Ok(()) - } -} diff --git a/src/system.rs b/src/system.rs new file mode 100644 index 0000000..61b7791 --- /dev/null +++ b/src/system.rs @@ -0,0 +1,51 @@ +use crate::monitor::Monitoring; +use crate::tasmota::{TasmotaInterface, TasmotaInterfaceConfig}; +use crate::types::Error; +use reqwest::Client; +use serde::Deserialize; +use std::fs; + +#[derive(Deserialize)] +pub struct SystemConfig { + components: Vec, +} +impl SystemConfig { + fn print(&self) { + for t in &self.components { + t.print(); + } + } +} + +pub struct System { + components: Vec, +} + +impl System { + pub fn new_from_file(config_file: &str) -> Result { + let config_str = fs::read_to_string(config_file)?; + let config: SystemConfig = toml::from_str(&config_str)?; + Ok(Self { + components: System::load_targets(&config.components), + }) + } + + pub fn load_targets(targets: &Vec) -> Vec { + let mut v = Vec::new(); + for target in targets { + v.push(TasmotaInterface::new(target.clone())); + } + v + } + + pub async fn get_power(&self) -> Result<(), Error> { + for component in &self.components { + if let Ok(res) = component.get_power().await { + component.print(); + println!("* POWER: {}W", res); + println!("------------------") + } + } + Ok(()) + } +} diff --git a/src/tasmota.rs b/src/tasmota.rs index 1ad6f5b..769eaf2 100644 --- a/src/tasmota.rs +++ b/src/tasmota.rs @@ -1,8 +1,7 @@ use reqwest::Client; use serde::Deserialize; -use crate::monitor::Error; -use crate::monitor::Monitoring; +use crate::{control::Control, monitor::Monitoring}; #[derive(Deserialize)] pub struct EnergyData { @@ -69,3 +68,20 @@ impl Monitoring for TasmotaInterface { Ok(data.status.energy.power) } } + +impl Control for TasmotaInterface { + async fn set_power(&self) -> Result<(), Error> { + let res = self + .client + .get(format!( + "http://{}/cm?cmnd=Power%20TOGGLE", + &self.config.target + )) + .send() + .await? + .text() + .await?; + + Ok(()) + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..889bcb6 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,20 @@ +use clap::Subcommand; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("io error")] + IoError(#[from] std::io::Error), + #[error("toml parsing error")] + ParseError(#[from] toml::de::Error), + #[error("request error")] + RequestError(#[from] reqwest::Error), + #[error("JSON Parse error")] + JsonParseError(#[from] serde_json::Error), +} + +#[derive(Subcommand, Clone)] +pub enum PowerState { + Off, + On, +} From 633a57ff239c5a538e33271a236571f99ab063a1 Mon Sep 17 00:00:00 2001 From: sigil-03 Date: Sun, 6 Apr 2025 15:21:38 -0600 Subject: [PATCH 2/2] add set functionality to control outlets --- src/control.rs | 4 ++-- src/main.rs | 25 +++++++++++++++---------- src/system.rs | 11 +++++++++-- src/tasmota.rs | 19 +++++++++++++------ src/types.rs | 5 +++-- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/src/control.rs b/src/control.rs index 2ecde08..7b9ae29 100644 --- a/src/control.rs +++ b/src/control.rs @@ -1,5 +1,5 @@ -use crate::types::Error; +use crate::types::{Error, PowerState}; pub trait Control { - async fn set_power(&self) -> Result<(), Error>; + async fn set_power(&self, state: PowerState) -> Result<(), Error>; } diff --git a/src/main.rs b/src/main.rs index 42e68a9..a85d5e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,26 +6,31 @@ mod system; mod tasmota; mod types; +#[derive(Parser)] +pub struct PowerCommand { + index: usize, + #[command(subcommand)] + state: types::PowerState, +} + #[derive(Subcommand)] pub enum Commands { Monitor, - #[command(subcommand)] - Set(types::PowerState), + Set(PowerCommand), } impl Commands { pub async fn execute(self, config_file: &str) { + let s = system::System::new_from_file(config_file).unwrap(); + let handle = match self { - Self::Monitor => { - let s = system::System::new_from_file(config_file).unwrap(); - tokio::spawn(async move { - s.get_power().await.unwrap(); - }) - } - Self::Set(state) => { + Self::Monitor => tokio::spawn(async move { + s.try_get_power().await.unwrap(); + }), + Self::Set(command) => { // let c = Controller::new_from_file(config_file).unwrap(); tokio::spawn(async move { - println!("SET"); + s.try_set_power(command.index, command.state).await.unwrap(); }) } }; diff --git a/src/system.rs b/src/system.rs index 61b7791..b5ffa8c 100644 --- a/src/system.rs +++ b/src/system.rs @@ -1,6 +1,7 @@ +use crate::control::Control; use crate::monitor::Monitoring; use crate::tasmota::{TasmotaInterface, TasmotaInterfaceConfig}; -use crate::types::Error; +use crate::types::{self, Error}; use reqwest::Client; use serde::Deserialize; use std::fs; @@ -38,7 +39,7 @@ impl System { v } - pub async fn get_power(&self) -> Result<(), Error> { + pub async fn try_get_power(&self) -> Result<(), Error> { for component in &self.components { if let Ok(res) = component.get_power().await { component.print(); @@ -48,4 +49,10 @@ impl System { } Ok(()) } + + pub async fn try_set_power(&self, index: usize, state: types::PowerState) -> Result<(), Error> { + //TODO: check bounds + self.components[index].set_power(state).await?; + Ok(()) + } } diff --git a/src/tasmota.rs b/src/tasmota.rs index 769eaf2..b8bae8a 100644 --- a/src/tasmota.rs +++ b/src/tasmota.rs @@ -1,7 +1,11 @@ use reqwest::Client; use serde::Deserialize; -use crate::{control::Control, monitor::Monitoring}; +use crate::{ + control::Control, + monitor::Monitoring, + types::{Error, PowerState}, +}; #[derive(Deserialize)] pub struct EnergyData { @@ -70,18 +74,21 @@ impl Monitoring for TasmotaInterface { } impl Control for TasmotaInterface { - async fn set_power(&self) -> Result<(), Error> { - let res = self + async fn set_power(&self, state: PowerState) -> Result<(), Error> { + let cmd = match state { + PowerState::Off => "OFF", + PowerState::On => "ON", + }; + let _res = self .client .get(format!( - "http://{}/cm?cmnd=Power%20TOGGLE", - &self.config.target + "http://{}/cm?cmnd=Power%20{}", + &self.config.target, cmd )) .send() .await? .text() .await?; - Ok(()) } } diff --git a/src/types.rs b/src/types.rs index 889bcb6..d25711e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -1,4 +1,5 @@ -use clap::Subcommand; +use clap::Parser; +use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Error, Debug)] @@ -13,7 +14,7 @@ pub enum Error { JsonParseError(#[from] serde_json::Error), } -#[derive(Subcommand, Clone)] +#[derive(Serialize, Deserialize, Parser, Clone)] pub enum PowerState { Off, On,