diff --git a/src/control.rs b/src/control.rs new file mode 100644 index 0000000..7b9ae29 --- /dev/null +++ b/src/control.rs @@ -0,0 +1,5 @@ +use crate::types::{Error, PowerState}; + +pub trait Control { + async fn set_power(&self, state: PowerState) -> Result<(), Error>; +} diff --git a/src/main.rs b/src/main.rs index 129371c..a85d5e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,36 @@ use clap::{Parser, Subcommand}; -use monitor::Monitor; +mod control; mod monitor; +mod system; mod tasmota; +mod types; + +#[derive(Parser)] +pub struct PowerCommand { + index: usize, + #[command(subcommand)] + state: types::PowerState, +} #[derive(Subcommand)] pub enum Commands { Monitor, + 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 m = Monitor::new_from_file(config_file).unwrap(); + 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 { - m.get_power().await.unwrap(); + s.try_set_power(command.index, command.state).await.unwrap(); }) } }; 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..b5ffa8c --- /dev/null +++ b/src/system.rs @@ -0,0 +1,58 @@ +use crate::control::Control; +use crate::monitor::Monitoring; +use crate::tasmota::{TasmotaInterface, TasmotaInterfaceConfig}; +use crate::types::{self, 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 try_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(()) + } + + 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 1ad6f5b..b8bae8a 100644 --- a/src/tasmota.rs +++ b/src/tasmota.rs @@ -1,8 +1,11 @@ use reqwest::Client; use serde::Deserialize; -use crate::monitor::Error; -use crate::monitor::Monitoring; +use crate::{ + control::Control, + monitor::Monitoring, + types::{Error, PowerState}, +}; #[derive(Deserialize)] pub struct EnergyData { @@ -69,3 +72,23 @@ impl Monitoring for TasmotaInterface { Ok(data.status.energy.power) } } + +impl Control for TasmotaInterface { + 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%20{}", + &self.config.target, cmd + )) + .send() + .await? + .text() + .await?; + Ok(()) + } +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..d25711e --- /dev/null +++ b/src/types.rs @@ -0,0 +1,21 @@ +use clap::Parser; +use serde::{Deserialize, Serialize}; +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(Serialize, Deserialize, Parser, Clone)] +pub enum PowerState { + Off, + On, +}