diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/config.rs | 64 | ||||
-rw-r--r-- | src/device.rs | 27 | ||||
-rw-r--r-- | src/device/on_action.rs | 54 | ||||
-rw-r--r-- | src/device/set_state.rs | 64 | ||||
-rw-r--r-- | src/main.rs | 27 |
5 files changed, 236 insertions, 0 deletions
diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..e8a4287 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,64 @@ +use std::{fs::File, io::Read, process, time::Duration}; + +use rumqttc::{Client, Connection, MqttOptions}; +use serde::Deserialize; + +use crate::PROGRAM; + +#[derive(Deserialize)] +pub struct Credentials { + pub username: String, + pub password: String, +} + +fn default_host() -> String { + "localhost".to_string() +} + +fn default_port() -> u16 { + 1883 +} + +fn default_id_prefix() -> String { + PROGRAM.to_string() +} + +#[derive(Deserialize)] +pub struct Config { + #[serde(default = "default_host")] + pub host: String, + #[serde(default = "default_port")] + pub port: u16, + #[serde(default = "default_id_prefix")] + pub id_prefix: String, + pub credentials: Option<Credentials>, +} + +impl Config { + pub fn mqtt_client(&self) -> (Client, Connection) { + let client_id = format!("{}_{}", self.id_prefix, process::id()); + let mut options = MqttOptions::new(client_id, &self.host, self.port); + if let Some(credentials) = &self.credentials { + options.set_credentials(&credentials.username, &credentials.password); + } + options.set_keep_alive(Duration::from_secs(5)); + Client::new(options, 10) + } +} + +pub fn get() -> Config { + let config = if let Ok(dirs) = xdg::BaseDirectories::with_prefix(PROGRAM) { + if let Some(config_file) = dirs.find_config_file("config.toml") { + let mut config_file = File::open(config_file).unwrap(); + let mut config = String::new(); + config_file.read_to_string(&mut config).unwrap(); + Some(config) + } else { + None + } + } else { + None + }; + let config = config.unwrap_or_default(); + toml::from_str(&config).unwrap() +} diff --git a/src/device.rs b/src/device.rs new file mode 100644 index 0000000..3911b25 --- /dev/null +++ b/src/device.rs @@ -0,0 +1,27 @@ +use std::{env::ArgsOs, process}; + +use crate::config::Config; + +mod on_action; +mod set_state; + +pub fn main(argv0: &str, config: Config, mut args: ArgsOs) { + let (Some(device_name), Some(command)) = (args.next(), args.next()) else { + eprintln!("Usage: {argv0} device <device-name> <command> [args...]"); + process::exit(1); + }; + + let Some(device_name) = device_name.to_str() else { + eprintln!("{argv0}: error: Invalid device name"); + process::exit(1); + }; + + match command.to_str() { + Some("on-action") => on_action::main(argv0, config, device_name, args), + Some("set-state") => set_state::main(argv0, config, device_name, args), + _ => { + eprintln!("{argv0}: error: Unknown device command: {command:?}"); + process::exit(1); + } + } +} diff --git a/src/device/on_action.rs b/src/device/on_action.rs new file mode 100644 index 0000000..5425086 --- /dev/null +++ b/src/device/on_action.rs @@ -0,0 +1,54 @@ +use std::{ + env::ArgsOs, ffi::OsString, process::{self, Command}, str +}; + +use rumqttc::{Event::Incoming, Packet::Publish, QoS}; +use serde_json::Value; + +use crate::config::Config; + +pub fn main(argv0: &str, config: Config, device_name: &str, mut args: ArgsOs) { + let (Some(action_filter), Some(command)) = (args.next(), args.next()) else { + eprintln!("Usage: {argv0} device <device-name> on-action <action-filter> <command...>"); + process::exit(1); + }; + + let args: Box<[OsString]> = args.collect(); + + let Ok(action_filter) = action_filter.into_string() else { + eprintln!("{argv0}: error: Invalid action-filter"); + process::exit(1); + }; + + let (client, mut connection) = config.mqtt_client(); + + let device_topic = format!("zigbee2mqtt/{device_name}"); + client.subscribe(&device_topic, QoS::AtLeastOnce).unwrap(); + + for notification in connection.iter() { + match notification.unwrap() { + Incoming(Publish(p)) => { + if p.topic == device_topic { + let parsed: Value = + serde_json::from_str(str::from_utf8(&p.payload).unwrap()).unwrap(); + if parsed["action"] == action_filter { + let mut c = Command::new(&command); + c.args(&args); + let status = c.status().expect("Unable to execute command"); + match status.code() { + Some(code) => { + if code != 0 { + eprintln!("command exited with: {code}"); + } + } + None => { + eprintln!("command was killed by a signal: {status:?}"); + } + } + } + } + } + _ => (), + } + } +} diff --git a/src/device/set_state.rs b/src/device/set_state.rs new file mode 100644 index 0000000..24ed99e --- /dev/null +++ b/src/device/set_state.rs @@ -0,0 +1,64 @@ +use std::{env::ArgsOs, process, str}; + +use rumqttc::{Event::Incoming, Packet::Publish, QoS}; +use serde_json::{json, Value}; + +use crate::config::Config; + +pub fn main(argv0: &str, config: Config, device_name: &str, mut args: ArgsOs) { + let (Some(target_state), None) = (args.next(), args.next()) else { + eprintln!("Usage: {argv0} device <device-name> set-state <target-state>"); + process::exit(1); + }; + + let target_state = if target_state.eq_ignore_ascii_case("on") { + "ON" + } else if target_state.eq_ignore_ascii_case("off") { + "OFF" + } else if target_state.eq_ignore_ascii_case("toggle") { + "TOGGLE" + } else { + eprintln!("{argv0}: error: target-state must be on/off/toggle"); + process::exit(1); + }; + + let (client, mut connection) = config.mqtt_client(); + + let device_topic = format!("zigbee2mqtt/{device_name}"); + client.subscribe(&device_topic, QoS::AtMostOnce).unwrap(); + + client + .publish( + &format!("{device_topic}/get"), + QoS::AtLeastOnce, + false, + json!({"state": "" }).to_string(), + ) + .unwrap(); + client + .publish( + &format!("{device_topic}/set"), + QoS::AtLeastOnce, + false, + json!({"state": target_state}).to_string(), + ) + .unwrap(); + + for notification in connection.iter() { + match notification.unwrap() { + Incoming(Publish(p)) => { + if p.topic == device_topic { + let parsed: Value = + serde_json::from_str(str::from_utf8(&p.payload).unwrap()).unwrap(); + if target_state == "TOGGLE" { + break; + } + if parsed["state"] == target_state { + break; + } + } + } + _ => (), + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a4f8963 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,27 @@ +mod device; +mod config; + +use std::{env, process, str}; + +const PROGRAM: &str = "z2m-utils"; + +fn main() { + let config = config::get(); + + let mut args = env::args_os(); + // Can't fail (it's a bug for sure) + let argv0 = args.next().unwrap(); + let argv0 = argv0.to_str().unwrap_or(PROGRAM); + let Some(command) = args.next() else { + eprintln!("Usage: {argv0} <command> [args...]"); + process::exit(1); + }; + + match command.to_str() { + Some("device") => device::main(argv0, config, args), + _ => { + eprintln!("{argv0}: error: Unknown command: {command:?}"); + process::exit(1); + } + } +} |