diff --git a/config.lua b/config.lua index e0a78c5..5b3131d 100644 --- a/config.lua +++ b/config.lua @@ -112,10 +112,10 @@ automation.device_manager:add(IkeaRemote.new({ local function off_timeout(duration) local timeout = Timeout.new() - return function(this, on) + return function(self, on) if on then timeout:start(duration, function() - this:set_on(false) + self:set_on(false) end) else timeout:cancel() @@ -188,26 +188,6 @@ automation.device_manager:add(IkeaOutlet.new({ client = mqtt_client, })) -local hallway_bottom_lights = HueGroup.new({ - identifier = "hallway_bottom_lights", - ip = hue_ip, - login = hue_token, - group_id = 81, - scene_id = "3qWKxGVadXFFG4o", - timer_id = 1, - client = mqtt_client, -}) -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_bottom_lights:set_on(on) - end, -})) - local hallway_top_light = HueGroup.new({ identifier = "hallway_top_light", ip = hue_ip, @@ -235,6 +215,64 @@ automation.device_manager:add(HueSwitch.new({ end, })) +local hallway_bottom_lights = HueGroup.new({ + identifier = "hallway_bottom_lights", + ip = hue_ip, + login = hue_token, + group_id = 81, + scene_id = "3qWKxGVadXFFG4o", + client = mqtt_client, +}) +automation.device_manager:add(hallway_bottom_lights) + +local hallway_light_automation = { + group = hallway_bottom_lights, + timeout = Timeout.new(), + state = { + door_open = false, + trash_open = false, + forced = false, + }, + switch_callback = function(self, on) + self.timeout:cancel() + self.group:set_on(on) + self.state.forced = on + end, + door_callback = function(self, open) + self.state.door_open = open + if open then + self.timeout:cancel() + + self.group:set_on(true) + elseif not self.state.forced then + self.timeout:start(debug and 10 or 60, function() + if not self.state.trash_open then + self.group:set_on(false) + end + end) + end + end, + trash_callback = function(self, open) + self.state.trash_open = open + if open then + self.group:set_on(true) + else + if not self.timeout:is_waiting() and not self.state.door_open and not self.state.forced then + self.group:set_on(false) + end + end + end, +} + +automation.device_manager:add(IkeaRemote.new({ + name = "Remote", + room = "Hallway", + client = mqtt_client, + topic = mqtt_z2m("hallway/remote"), + callback = function(on) + hallway_light_automation:switch_callback(on) + end, +})) automation.device_manager:add(ContactSensor.new({ identifier = "hallway_frontdoor", topic = mqtt_z2m("hallway/frontdoor"), @@ -243,19 +281,17 @@ automation.device_manager:add(ContactSensor.new({ topic = mqtt_automation("presence/contact/frontdoor"), timeout = debug and 10 or 15 * 60, }, - trigger = { - devices = { hallway_bottom_lights }, - timeout = debug and 10 or 2 * 60, - }, + callback = function(open) + hallway_light_automation:door_callback(open) + end, })) - automation.device_manager:add(ContactSensor.new({ identifier = "hallway_trash", topic = mqtt_z2m("hallway/trash"), client = mqtt_client, - trigger = { - devices = { hallway_bottom_lights }, - }, + callback = function(open) + hallway_light_automation:trash_callback(open) + end, })) automation.device_manager:add(IkeaOutlet.new({ diff --git a/src/devices/contact_sensor.rs b/src/devices/contact_sensor.rs index 03c8e05..3de9c68 100644 --- a/src/devices/contact_sensor.rs +++ b/src/devices/contact_sensor.rs @@ -3,19 +3,18 @@ use std::time::Duration; use async_trait::async_trait; use automation_macro::LuaDeviceConfig; -use google_home::traits::OnOff; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::task::JoinHandle; use tracing::{debug, error, trace, warn}; use super::{Device, LuaDeviceCreate}; +use crate::action_callback::ActionCallback; use crate::config::MqttDeviceConfig; use crate::devices::DEFAULT_PRESENCE; use crate::error::DeviceConfigError; use crate::event::{OnMqtt, OnPresence}; use crate::messages::{ContactMessage, PresenceMessage}; use crate::mqtt::WrappedAsyncClient; -use crate::traits::Timeout; // NOTE: If we add more presence devices we might need to move this out of here #[derive(Debug, Clone, LuaDeviceConfig)] @@ -26,14 +25,6 @@ pub struct PresenceDeviceConfig { pub timeout: Duration, } -#[derive(Debug, Clone, LuaDeviceConfig)] -pub struct TriggerConfig { - #[device_config(from_lua)] - pub devices: Vec>, - #[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))] - pub timeout: Option, -} - #[derive(Debug, Clone, LuaDeviceConfig)] pub struct Config { pub identifier: String, @@ -41,8 +32,8 @@ pub struct Config { pub mqtt: MqttDeviceConfig, #[device_config(from_lua, default)] pub presence: Option, - #[device_config(from_lua)] - pub trigger: Option, + #[device_config(from_lua, default)] + pub callback: ActionCallback, #[device_config(from_lua)] pub client: WrappedAsyncClient, } @@ -51,7 +42,6 @@ pub struct Config { struct State { overall_presence: bool, is_closed: bool, - previous: Vec, handle: Option>, } @@ -79,26 +69,6 @@ impl LuaDeviceCreate for ContactSensor { async fn create(config: Self::Config) -> Result { trace!(id = config.identifier, "Setting up ContactSensor"); - let mut previous = Vec::new(); - // Make sure the devices implement the required traits - if let Some(trigger) = &config.trigger { - for device in &trigger.devices { - { - let id = device.get_id().to_owned(); - if (device.cast() as Option<&dyn OnOff>).is_none() { - return Err(DeviceConfigError::MissingTrait(id, "OnOff".into())); - } - - if trigger.timeout.is_none() - && (device.cast() as Option<&dyn Timeout>).is_none() - { - return Err(DeviceConfigError::MissingTrait(id, "Timeout".into())); - } - } - } - previous.resize(trigger.devices.len(), false); - } - config .client .subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce) @@ -107,7 +77,6 @@ impl LuaDeviceCreate for ContactSensor { let state = State { overall_presence: DEFAULT_PRESENCE, is_closed: true, - previous, handle: None, }; let state = Arc::new(RwLock::new(state)); @@ -148,44 +117,11 @@ impl OnMqtt for ContactSensor { return; } + self.config.callback.call(!is_closed).await; + debug!(id = self.get_id(), "Updating state to {is_closed}"); self.state_mut().await.is_closed = is_closed; - if let Some(trigger) = &self.config.trigger { - if !is_closed { - for (light, previous) in trigger - .devices - .iter() - .zip(self.state_mut().await.previous.iter_mut()) - { - if let Some(light) = light.cast() as Option<&dyn OnOff> { - *previous = light.on().await.unwrap(); - light.set_on(true).await.ok(); - } - } - } else { - for (light, previous) in trigger - .devices - .iter() - .zip(self.state_mut().await.previous.iter()) - { - if !previous { - // If the timeout is zero just turn the light off directly - if trigger.timeout.is_none() - && let Some(light) = light.cast() as Option<&dyn OnOff> - { - light.set_on(false).await.ok(); - } else if let Some(timeout) = trigger.timeout - && let Some(light) = light.cast() as Option<&dyn Timeout> - { - light.start_timeout(timeout).await.unwrap(); - } - // TODO: Put a warning/error on creation if either of this has to option to fail - } - } - } - } - // Check if this contact sensor works as a presence device // If not we are done here let presence = match &self.config.presence { diff --git a/src/devices/hue_group.rs b/src/devices/hue_group.rs index d753d68..210b2e8 100644 --- a/src/devices/hue_group.rs +++ b/src/devices/hue_group.rs @@ -1,7 +1,6 @@ use std::net::SocketAddr; -use std::time::Duration; -use anyhow::{anyhow, Context, Result}; +use anyhow::Result; use async_trait::async_trait; use automation_macro::LuaDeviceConfig; use google_home::errors::ErrorCode; @@ -10,7 +9,6 @@ use tracing::{error, trace, warn}; use super::{Device, LuaDeviceCreate}; use crate::mqtt::WrappedAsyncClient; -use crate::traits::Timeout; #[derive(Debug, Clone, LuaDeviceConfig)] pub struct Config { @@ -19,8 +17,6 @@ pub struct Config { pub addr: SocketAddr, pub login: String, pub group_id: isize, - #[device_config(default)] - pub timer_id: Option, pub scene_id: String, #[device_config(from_lua)] pub client: WrappedAsyncClient, @@ -49,11 +45,6 @@ impl HueGroup { format!("http://{}/api/{}", self.config.addr, self.config.login) } - 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 { format!("{}/groups/{}/action", self.url_base(), self.config.group_id) } @@ -72,9 +63,6 @@ impl Device for HueGroup { #[async_trait] impl OnOff for HueGroup { async fn set_on(&self, on: bool) -> Result<(), ErrorCode> { - // Abort any timer that is currently running - self.stop_timeout().await.unwrap(); - let message = if on { message::Action::scene(self.config.scene_id.clone()) } else { @@ -131,57 +119,6 @@ impl OnOff for HueGroup { } } -#[async_trait] -impl Timeout for HueGroup { - async fn start_timeout(&self, timeout: Duration) -> Result<()> { - // Abort any timer that is currently running - self.stop_timeout().await?; - - // 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(url) - .json(&message) - .send() - .await - .context("Failed to start timeout")?; - - let status = res.status(); - if !status.is_success() { - return Err(anyhow!( - "Hue bridge returned unsuccessful status '{status}'" - )); - } - - Ok(()) - } - - 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(url) - .json(&message) - .send() - .await - .context("Failed to stop timeout")?; - - let status = res.status(); - if !status.is_success() { - return Err(anyhow!( - "Hue bridge returned unsuccessful status '{status}'" - )); - } - - Ok(()) - } -} - mod message { use std::time::Duration; diff --git a/src/devices/mod.rs b/src/devices/mod.rs index 00fc011..71b422f 100644 --- a/src/devices/mod.rs +++ b/src/devices/mod.rs @@ -37,7 +37,6 @@ pub use self::presence::{Presence, DEFAULT_PRESENCE}; pub use self::wake_on_lan::WakeOnLAN; pub use self::washer::Washer; use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence}; -use crate::traits::Timeout; #[async_trait] pub trait LuaDeviceCreate { @@ -145,7 +144,6 @@ pub trait Device: + Cast + Cast + Cast - + Cast { fn get_id(&self) -> String; } diff --git a/src/helpers/timeout.rs b/src/helpers/timeout.rs index 81c14be..3432fed 100644 --- a/src/helpers/timeout.rs +++ b/src/helpers/timeout.rs @@ -59,5 +59,18 @@ impl mlua::UserData for Timeout { Ok(()) }); + + methods.add_async_method("is_waiting", |_lua, this, ()| async move { + debug!("Canceling timeout callback"); + + if let Some(handle) = this.state.read().await.handle.as_ref() { + debug!("Join handle: {}", handle.is_finished()); + return Ok(!handle.is_finished()); + } + + debug!("Join handle: None"); + + Ok(false) + }); } } diff --git a/src/lib.rs b/src/lib.rs index cca1452..31495d5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,4 +13,3 @@ pub mod helpers; pub mod messages; pub mod mqtt; pub mod schedule; -pub mod traits; diff --git a/src/traits.rs b/src/traits.rs deleted file mode 100644 index 9e67051..0000000 --- a/src/traits.rs +++ /dev/null @@ -1,10 +0,0 @@ -use std::time::Duration; - -use anyhow::Result; -use async_trait::async_trait; - -#[async_trait] -pub trait Timeout: Sync + Send { - async fn start_timeout(&self, _timeout: Duration) -> Result<()>; - async fn stop_timeout(&self) -> Result<()>; -}