summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/config.rs64
-rw-r--r--src/device.rs27
-rw-r--r--src/device/on_action.rs54
-rw-r--r--src/device/set_state.rs64
-rw-r--r--src/main.rs27
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);
+ }
+ }
+}