Compare commits
10 commits
e8fdbbaa45
...
028c5eeb36
| Author | SHA1 | Date | |
|---|---|---|---|
| 028c5eeb36 | |||
| 90810474c5 | |||
| 99350853ca | |||
|
|
aa71936eaa | ||
| 230034c7f0 | |||
|
|
c5866eeccc | ||
| 633a57ff23 | |||
| 987150c9b6 | |||
|
|
fd120be85e | ||
| 503bab511a |
10 changed files with 202 additions and 81 deletions
|
|
@ -11,3 +11,7 @@ serde_json = "1.0.140"
|
||||||
thiserror = "2.0.12"
|
thiserror = "2.0.12"
|
||||||
tokio = { version = "1.44.1", features = ["full"] }
|
tokio = { version = "1.44.1", features = ["full"] }
|
||||||
toml = "0.8.20"
|
toml = "0.8.20"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name="cli"
|
||||||
|
path="src/bin/cli.rs"
|
||||||
58
README.md
58
README.md
|
|
@ -1,7 +1,57 @@
|
||||||
CLI application for interfacing with powered devices.
|
# POWER
|
||||||
|
this repository provides both
|
||||||
|
1. modular library for interfacing with powered devices
|
||||||
|
2. CLI application which uses this library to control powered devices from the command line
|
||||||
|
|
||||||
in this case, tasmota smart outlets being used on the hydroponics project
|
**IMPORTANT:** this is a work in progress, and the API will likely have breaking changes as it grows. ADDITIONALLY, currently there is only trait implementations for Tasmota outlets, however other devices can be supported by implementing these traits for your device-specific API. You can use `tasmota.rs` as an example of how to do this. once implemented, your devices will be compatible with this library.
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
## BUILDING THE CLI
|
||||||
|
with a working rust toolchain on your machine, run the following:
|
||||||
|
|
||||||
TODO:
|
`cargo build --release --bin cli`
|
||||||
* add reqwest support to access the tasmota outlets
|
|
||||||
|
once built, the CLI binary will be available at the following path:
|
||||||
|
`<project root>/target/release/cli`
|
||||||
|
|
||||||
|
## CONFIGURING THE SYSTEM DESCRIPTION FILE
|
||||||
|
a `system` consists of a number of `components` which are defined in a configuration `toml` file.
|
||||||
|
|
||||||
|
an example, `config.toml` has been provided.
|
||||||
|
|
||||||
|
each entry corresponds to a `component` and provides a `name` as well as the `target` IP address of the (in this case tasmota) power device.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
components = [
|
||||||
|
{name="OUTLET 1", target="OUTLET_1_IP"}, # OUTLET 1
|
||||||
|
{name="OUTLET 2", target="OUTLET_2_IP"}, # OUTLET 2
|
||||||
|
{name="OUTLET 3", target="OUTLET_3_IP"}, # OUTLET 3 ...
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
## RUNNING THE CLI
|
||||||
|
the CLI takes as an argument the `config-file` (described above) as well as a subcommand.
|
||||||
|
|
||||||
|
more usage information about the CLI can be obtained by running
|
||||||
|
|
||||||
|
`./cli help`
|
||||||
|
|
||||||
|
## COMMANDS
|
||||||
|
### MONITOR
|
||||||
|
the `monitor` command will output the power consumption of the components listed in the config file.
|
||||||
|
|
||||||
|
example:
|
||||||
|
```log
|
||||||
|
OUTLET_1
|
||||||
|
* POWER: 31W
|
||||||
|
------------------
|
||||||
|
OUTLET_2
|
||||||
|
* POWER: 0W
|
||||||
|
------------------
|
||||||
|
OUTLET_3
|
||||||
|
* POWER: 0W
|
||||||
|
------------------
|
||||||
|
```
|
||||||
|
|
||||||
|
### SET
|
||||||
|
the `set` command allows a user to set the power state of a `component` at a given `index`. `component`s are indexed starting at 0 in the order they are defined in your configuration file.
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
target = [
|
components = [
|
||||||
"YOUR IP HERE"
|
{name="OUTLET 1", target="OUTLET_1_IP"}, # OUTLET 1
|
||||||
]
|
{name="OUTLET 2", target="OUTLET_2_IP"}, # OUTLET 2
|
||||||
|
{name="OUTLET 3", target="OUTLET_3_IP"}, # OUTLET 3 ...
|
||||||
|
]
|
||||||
|
|
@ -1,23 +1,31 @@
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use monitor::Monitor;
|
|
||||||
|
|
||||||
mod monitor;
|
#[derive(Parser)]
|
||||||
mod tasmota;
|
pub struct PowerCommand {
|
||||||
|
index: usize,
|
||||||
|
#[command(subcommand)]
|
||||||
|
state: power::types::PowerState,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
Monitor,
|
Monitor,
|
||||||
|
Set(PowerCommand),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Commands {
|
impl Commands {
|
||||||
pub async fn execute(self, config_file: &str) {
|
pub async fn execute(self, config_file: &str) {
|
||||||
|
let s = power::system::System::new_from_file(config_file).unwrap();
|
||||||
|
|
||||||
let handle = match self {
|
let handle = match self {
|
||||||
Self::Monitor => {
|
Self::Monitor => tokio::spawn(async move {
|
||||||
let m = Monitor::new_from_file(config_file).unwrap();
|
s.try_get_power().await.unwrap();
|
||||||
|
}),
|
||||||
|
Self::Set(command) => {
|
||||||
|
// let c = Controller::new_from_file(config_file).unwrap();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
m.get_power().await.unwrap();
|
s.try_set_power(command.index, command.state).await.unwrap();
|
||||||
})
|
})
|
||||||
// println!("[TODO] Power: ----W")
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
handle.await.unwrap();
|
handle.await.unwrap();
|
||||||
8
src/control.rs
Normal file
8
src/control.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
use crate::types::{Error, PowerState};
|
||||||
|
|
||||||
|
pub trait Control {
|
||||||
|
fn set_power(
|
||||||
|
&self,
|
||||||
|
state: PowerState,
|
||||||
|
) -> impl std::future::Future<Output = Result<(), Error>> + Send;
|
||||||
|
}
|
||||||
5
src/lib.rs
Normal file
5
src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
pub mod control;
|
||||||
|
pub mod monitor;
|
||||||
|
pub mod system;
|
||||||
|
pub mod tasmota;
|
||||||
|
pub mod types;
|
||||||
|
|
@ -1,68 +1,5 @@
|
||||||
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 {
|
pub trait Monitoring {
|
||||||
async fn get_power(&self) -> Result<isize, Error>;
|
fn get_power(&self) -> impl std::future::Future<Output = Result<isize, Error>> + Send;
|
||||||
}
|
|
||||||
|
|
||||||
#[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)?;
|
|
||||||
config.print();
|
|
||||||
|
|
||||||
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 {
|
|
||||||
println!("POWER: {}W", res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
58
src/system.rs
Normal file
58
src/system.rs
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
use crate::control::Control;
|
||||||
|
use crate::monitor::Monitoring;
|
||||||
|
use crate::tasmota::{TasmotaInterface, TasmotaInterfaceConfig};
|
||||||
|
use crate::types::{self, Error};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SystemConfig {
|
||||||
|
components: Vec<TasmotaInterfaceConfig>,
|
||||||
|
}
|
||||||
|
impl SystemConfig {
|
||||||
|
#[allow(unused)]
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::monitor::Error;
|
use crate::{
|
||||||
use crate::monitor::Monitoring;
|
control::Control,
|
||||||
|
monitor::Monitoring,
|
||||||
|
types::{Error, PowerState},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct EnergyData {
|
pub struct EnergyData {
|
||||||
|
|
@ -26,11 +29,13 @@ pub struct StatusResponse {
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub struct TasmotaInterfaceConfig {
|
pub struct TasmotaInterfaceConfig {
|
||||||
|
name: String,
|
||||||
target: String,
|
target: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TasmotaInterfaceConfig {
|
impl TasmotaInterfaceConfig {
|
||||||
pub fn print(&self) {
|
pub fn print(&self) {
|
||||||
|
println!("{}", self.name);
|
||||||
println!("* {}", self.target);
|
println!("* {}", self.target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,6 +52,9 @@ impl TasmotaInterface {
|
||||||
client: Client::new(),
|
client: Client::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pub fn print(&self) {
|
||||||
|
println!("{}", self.config.name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Monitoring
|
// Monitoring
|
||||||
|
|
@ -64,3 +72,23 @@ impl Monitoring for TasmotaInterface {
|
||||||
Ok(data.status.energy.power)
|
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
21
src/types.rs
Normal 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,
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue