1
0
Fork 0
forked from sigil-03/power

Merge pull request #2 from sigil-03/set

Add SET functionality
This commit is contained in:
sigil-03 2025-04-06 15:31:52 -06:00 committed by GitHub
commit c5866eeccc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 129 additions and 66 deletions

5
src/control.rs Normal file
View file

@ -0,0 +1,5 @@
use crate::types::{Error, PowerState};
pub trait Control {
async fn set_power(&self, state: PowerState) -> Result<(), Error>;
}

View file

@ -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();
})
}
};

View file

@ -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<isize, Error>;
}
#[derive(Deserialize)]
pub struct MonitorConfig {
targets: Vec<TasmotaInterfaceConfig>,
}
impl MonitorConfig {
fn print(&self) {
for t in &self.targets {
t.print();
}
}
}
pub struct Monitor {
targets: Vec<TasmotaInterface>,
client: Client,
}
impl Monitor {
pub fn new_from_file(config_file: &str) -> Result<Self, Error> {
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<TasmotaInterfaceConfig>) -> Vec<TasmotaInterface> {
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(())
}
}

58
src/system.rs Normal file
View file

@ -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<TasmotaInterfaceConfig>,
}
impl SystemConfig {
fn print(&self) {
for t in &self.components {
t.print();
}
}
}
pub struct System {
components: Vec<TasmotaInterface>,
}
impl System {
pub fn new_from_file(config_file: &str) -> Result<Self, Error> {
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<TasmotaInterfaceConfig>) -> Vec<TasmotaInterface> {
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(())
}
}

View file

@ -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(())
}
}

21
src/types.rs Normal file
View file

@ -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,
}