diff --git a/Cargo.lock b/Cargo.lock index 80b3127..a707f23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,6 +127,7 @@ dependencies = [ "tracing-subscriber", "uuid", "wakey", + "zigbee2mqtt-types", ] [[package]] @@ -2618,3 +2619,13 @@ dependencies = [ "quote", "syn 2.0.60", ] + +[[package]] +name = "zigbee2mqtt-types" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0ebe51c23f1de3efd64cca1a2176a060e8c90e72070e9ce9f0c023e514e08ac" +dependencies = [ + "serde", + "serde_json", +] diff --git a/Cargo.toml b/Cargo.toml index 5c3ab19..4032878 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,6 +61,7 @@ tokio-util = { version = "0.7.11", features = ["full"] } uuid = "1.8.0" dyn-clone = "1.0.17" impls = "1.0.3" +zigbee2mqtt-types = { version = "0.2.0", features = ["debug", "philips"] } [patch.crates-io] wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" } diff --git a/config.lua b/config.lua index 6ad472c..9b31183 100644 --- a/config.lua +++ b/config.lua @@ -173,8 +173,8 @@ automation.device_manager:add(IkeaOutlet.new({ client = mqtt_client, })) -local hallway_lights = HueGroup.new({ - identifier = "hallway_lights", +local hallway_bottom_lights = HueGroup.new({ + identifier = "hallway_bottom_lights", ip = hue_ip, login = hue_token, group_id = 81, @@ -182,14 +182,41 @@ local hallway_lights = HueGroup.new({ timer_id = 1, client = mqtt_client, }) -automation.device_manager:add(hallway_lights) +automation.device_manager:add(hallway_bottom_lights) automation.device_manager:add(IkeaRemote.new({ name = "Remote", room = "Hallway", client = mqtt_client, topic = mqtt_z2m("hallway/remote"), callback = function(on) - hallway_lights:set_on(on) + hallway_bottom_lights:set_on(on) + end, +})) + +local hallway_top_light = HueGroup.new({ + identifier = "hallway_top_light", + ip = hue_ip, + login = hue_token, + group_id = 83, + scene_id = "QeufkFDICEHWeKJ7", + client = mqtt_client, +}) +automation.device_manager:add(HueSwitch.new({ + name = "SwitchBottom", + room = "Hallway", + client = mqtt_client, + topic = mqtt_z2m("hallway/switchbottom"), + left_callback = function() + hallway_top_light:set_on(not hallway_top_light:is_on()) + end, +})) +automation.device_manager:add(HueSwitch.new({ + name = "SwitchTop", + room = "Hallway", + client = mqtt_client, + topic = mqtt_z2m("hallway/switchtop"), + left_callback = function() + hallway_top_light:set_on(not hallway_top_light:is_on()) end, })) @@ -202,7 +229,7 @@ automation.device_manager:add(ContactSensor.new({ timeout = debug and 10 or 15 * 60, }, trigger = { - devices = { hallway_lights }, + devices = { hallway_bottom_lights }, timeout = debug and 10 or 2 * 60, }, })) @@ -212,7 +239,7 @@ automation.device_manager:add(ContactSensor.new({ topic = mqtt_z2m("hallway/trash"), client = mqtt_client, trigger = { - devices = { hallway_lights }, + devices = { hallway_bottom_lights }, }, })) diff --git a/src/action_callback.rs b/src/action_callback.rs index 750cab8..d6c9a78 100644 --- a/src/action_callback.rs +++ b/src/action_callback.rs @@ -3,9 +3,14 @@ use std::marker::PhantomData; use mlua::{FromLua, IntoLua}; #[derive(Debug, Clone)] -pub struct ActionCallback { +struct Internal { uuid: uuid::Uuid, lua: mlua::Lua, +} + +#[derive(Debug, Clone, Default)] +pub struct ActionCallback { + internal: Option, phantom: PhantomData, } @@ -15,8 +20,10 @@ impl FromLua for ActionCallback { lua.set_named_registry_value(&uuid.to_string(), value)?; Ok(ActionCallback { - uuid, - lua: lua.clone(), + internal: Some(Internal { + uuid, + lua: lua.clone(), + }), phantom: PhantomData::, }) } @@ -28,9 +35,14 @@ where T: IntoLua + Sync + Send + Clone + Copy + 'static, { pub async fn call(&self, state: T) { - let uuid = self.uuid; + let Some(internal) = self.internal.as_ref() else { + return; + }; - let callback: mlua::Value = self.lua.named_registry_value(&uuid.to_string()).unwrap(); + let callback: mlua::Value = internal + .lua + .named_registry_value(&internal.uuid.to_string()) + .unwrap(); match callback { mlua::Value::Function(f) => f.call_async::<()>(state).await.unwrap(), _ => todo!("Only functions are currently supported"), diff --git a/src/devices/hue_group.rs b/src/devices/hue_group.rs index fc5c4dd..d753d68 100644 --- a/src/devices/hue_group.rs +++ b/src/devices/hue_group.rs @@ -19,7 +19,8 @@ pub struct Config { pub addr: SocketAddr, pub login: String, pub group_id: isize, - pub timer_id: isize, + #[device_config(default)] + pub timer_id: Option, pub scene_id: String, #[device_config(from_lua)] pub client: WrappedAsyncClient, @@ -48,8 +49,9 @@ impl HueGroup { format!("http://{}/api/{}", self.config.addr, self.config.login) } - fn url_set_schedule(&self) -> String { - format!("{}/schedules/{}", self.url_base(), self.config.timer_id) + fn url_set_schedule(&self) -> Option { + let timer_id = self.config.timer_id?; + Some(format!("{}/schedules/{}", self.url_base(), timer_id)) } fn url_set_action(&self) -> String { @@ -137,8 +139,11 @@ impl Timeout for HueGroup { // NOTE: This uses an existing timer, as we are unable to cancel it on the hub otherwise let message = message::Timeout::new(Some(timeout)); + let Some(url) = self.url_set_schedule() else { + return Ok(()); + }; let res = reqwest::Client::new() - .put(self.url_set_schedule()) + .put(url) .json(&message) .send() .await @@ -156,8 +161,11 @@ impl Timeout for HueGroup { async fn stop_timeout(&self) -> Result<()> { let message = message::Timeout::new(None); + let Some(url) = self.url_set_schedule() else { + return Ok(()); + }; let res = reqwest::Client::new() - .put(self.url_set_schedule()) + .put(url) .json(&message) .send() .await diff --git a/src/devices/hue_switch.rs b/src/devices/hue_switch.rs new file mode 100644 index 0000000..97fbd43 --- /dev/null +++ b/src/devices/hue_switch.rs @@ -0,0 +1,87 @@ +use automation_macro::LuaDeviceConfig; +use axum::async_trait; +use rumqttc::{matches, Publish}; +use tracing::{debug, trace, warn}; +use zigbee2mqtt_types::vendors::philips::Zigbee929003017102; + +use super::LuaDeviceCreate; +use crate::action_callback::ActionCallback; +use crate::config::{InfoConfig, MqttDeviceConfig}; +use crate::devices::Device; +use crate::event::OnMqtt; +use crate::mqtt::WrappedAsyncClient; + +#[derive(Debug, Clone, LuaDeviceConfig)] +pub struct Config { + #[device_config(flatten)] + pub info: InfoConfig, + + #[device_config(flatten)] + pub mqtt: MqttDeviceConfig, + + #[device_config(from_lua)] + pub client: WrappedAsyncClient, + + // TODO: IntoLua is not implemented for unit type () + #[device_config(from_lua, default)] + pub left_callback: ActionCallback, + + #[device_config(from_lua, default)] + pub right_callback: ActionCallback, +} + +#[derive(Debug, Clone)] +pub struct HueSwitch { + config: Config, +} + +impl Device for HueSwitch { + fn get_id(&self) -> String { + self.config.info.identifier() + } +} + +#[async_trait] +impl LuaDeviceCreate for HueSwitch { + type Config = Config; + type Error = rumqttc::ClientError; + + async fn create(config: Self::Config) -> Result { + trace!(id = config.info.identifier(), "Setting up HueSwitch"); + + config + .client + .subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce) + .await?; + + Ok(Self { config }) + } +} + +#[async_trait] +impl OnMqtt for HueSwitch { + async fn on_mqtt(&self, message: Publish) { + // Check if the message is from the deviec itself or from a remote + debug!(id = Device::get_id(self), "Mqtt message received"); + if matches(&message.topic, &self.config.mqtt.topic) { + let action = match serde_json::from_slice::(&message.payload) { + Ok(message) => message.action, + Err(err) => { + warn!(id = Device::get_id(self), "Failed to parse message: {err}"); + return; + } + }; + debug!(id = Device::get_id(self), "Remote action = {:?}", action); + + match action { + zigbee2mqtt_types::vendors::philips::Zigbee929003017102Action::Leftpress => { + self.config.left_callback.call(true).await + } + zigbee2mqtt_types::vendors::philips::Zigbee929003017102Action::Rightpress => { + self.config.right_callback.call(true).await + } + _ => {} + } + } + } +} diff --git a/src/devices/mod.rs b/src/devices/mod.rs index cb39ac1..00fc011 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -3,6 +3,7 @@ mod contact_sensor; mod debug_bridge; mod hue_bridge; mod hue_group; +mod hue_switch; mod ikea_outlet; mod ikea_remote; mod kasa_outlet; @@ -26,6 +27,7 @@ pub use self::contact_sensor::ContactSensor; pub use self::debug_bridge::DebugBridge; pub use self::hue_bridge::HueBridge; pub use self::hue_group::HueGroup; +pub use self::hue_switch::HueSwitch; pub use self::ikea_outlet::IkeaOutlet; pub use self::ikea_remote::IkeaRemote; pub use self::kasa_outlet::KasaOutlet; @@ -102,6 +104,7 @@ impl_device!(ContactSensor); impl_device!(DebugBridge); impl_device!(HueBridge); impl_device!(HueGroup); +impl_device!(HueSwitch); impl_device!(IkeaOutlet); impl_device!(IkeaRemote); impl_device!(KasaOutlet); @@ -117,6 +120,7 @@ pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> { register_device!(lua, DebugBridge); register_device!(lua, HueBridge); register_device!(lua, HueGroup); + register_device!(lua, HueSwitch); register_device!(lua, IkeaOutlet); register_device!(lua, IkeaRemote); register_device!(lua, KasaOutlet);