From 9d4b52b51150d673d0702a82085c9a98901c0439 Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Wed, 4 Dec 2024 03:03:53 +0100 Subject: [PATCH] Implemented new timeout mechanism for ikea_outlet --- config.lua | 23 +++++++++++--- src/devices/ikea_outlet.rs | 57 ++++++---------------------------- src/helpers/mod.rs | 10 ++++++ src/helpers/timeout.rs | 63 ++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 3 +- 6 files changed, 105 insertions(+), 52 deletions(-) create mode 100644 src/helpers/mod.rs create mode 100644 src/helpers/timeout.rs diff --git a/config.lua b/config.lua index 9b31183..e0a78c5 100644 --- a/config.lua +++ b/config.lua @@ -109,16 +109,31 @@ automation.device_manager:add(IkeaRemote.new({ end, })) +local function off_timeout(duration) + local timeout = Timeout.new() + + return function(this, on) + if on then + timeout:start(duration, function() + this:set_on(false) + end) + else + timeout:cancel() + end + end +end + local kettle = IkeaOutlet.new({ outlet_type = "Kettle", name = "Kettle", room = "Kitchen", topic = mqtt_z2m("kitchen/kettle"), client = mqtt_client, - timeout = debug and 5 or 300, + callback = off_timeout(debug and 5 or 300), }) automation.device_manager:add(kettle) -function set_kettle(on) + +local function set_kettle(on) kettle:set_on(on) end @@ -146,7 +161,7 @@ automation.device_manager:add(IkeaOutlet.new({ room = "Bathroom", topic = mqtt_z2m("bathroom/light"), client = mqtt_client, - timeout = debug and 60 or 45 * 60, + callback = off_timeout(debug and 60 or 45 * 60), })) automation.device_manager:add(Washer.new({ @@ -163,7 +178,7 @@ automation.device_manager:add(IkeaOutlet.new({ room = "Workbench", topic = mqtt_z2m("workbench/charger"), client = mqtt_client, - timeout = debug and 5 or 20 * 3600, + callback = off_timeout(debug and 5 or 20 * 3600), })) automation.device_manager:add(IkeaOutlet.new({ diff --git a/src/devices/ikea_outlet.rs b/src/devices/ikea_outlet.rs index 5ae3c3b..4f70579 100644 --- a/src/devices/ikea_outlet.rs +++ b/src/devices/ikea_outlet.rs @@ -1,5 +1,4 @@ use std::sync::Arc; -use std::time::Duration; use anyhow::Result; use async_trait::async_trait; @@ -11,16 +10,15 @@ use google_home::types::Type; use rumqttc::{matches, Publish}; use serde::Deserialize; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; -use tokio::task::JoinHandle; use tracing::{debug, error, trace, warn}; use super::LuaDeviceCreate; +use crate::action_callback::ActionCallback; use crate::config::{InfoConfig, MqttDeviceConfig}; use crate::devices::Device; use crate::event::{OnMqtt, OnPresence}; use crate::messages::OnOffMessage; use crate::mqtt::WrappedAsyncClient; -use crate::traits::Timeout; #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] pub enum OutletType { @@ -38,17 +36,17 @@ pub struct Config { pub mqtt: MqttDeviceConfig, #[device_config(default(OutletType::Outlet))] pub outlet_type: OutletType, - #[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))] - pub timeout: Option, + + #[device_config(from_lua, default)] + pub callback: ActionCallback<(IkeaOutlet, bool)>, #[device_config(from_lua)] pub client: WrappedAsyncClient, } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct State { last_known_state: bool, - handle: Option>, } #[derive(Debug, Clone)] @@ -81,13 +79,10 @@ impl LuaDeviceCreate for IkeaOutlet { .subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce) .await?; - let state = State { - last_known_state: false, - handle: None, - }; - let state = Arc::new(RwLock::new(state)); - - Ok(Self { config, state }) + Ok(Self { + config, + state: Default::default(), + }) } } @@ -116,16 +111,10 @@ impl OnMqtt for IkeaOutlet { return; } - // Abort any timer that is currently running - self.stop_timeout().await.unwrap(); + self.config.callback.call((self.clone(), state)).await; debug!(id = Device::get_id(self), "Updating state to {state}"); self.state_mut().await.last_known_state = state; - - // If this is a kettle start a timeout for turning it of again - if state && let Some(timeout) = self.config.timeout { - self.start_timeout(timeout).await.unwrap(); - } } } } @@ -199,29 +188,3 @@ impl traits::OnOff for IkeaOutlet { Ok(()) } } - -#[async_trait] -impl crate::traits::Timeout for IkeaOutlet { - async fn start_timeout(&self, timeout: Duration) -> Result<()> { - // Abort any timer that is currently running - self.stop_timeout().await?; - - let device = self.clone(); - self.state_mut().await.handle = Some(tokio::spawn(async move { - debug!(id = device.get_id(), "Starting timeout ({timeout:?})..."); - tokio::time::sleep(timeout).await; - debug!(id = device.get_id(), "Turning outlet off!"); - device.set_on(false).await.unwrap(); - })); - - Ok(()) - } - - async fn stop_timeout(&self) -> Result<()> { - if let Some(handle) = self.state_mut().await.handle.take() { - handle.abort(); - } - - Ok(()) - } -} diff --git a/src/helpers/mod.rs b/src/helpers/mod.rs new file mode 100644 index 0000000..d4e0f95 --- /dev/null +++ b/src/helpers/mod.rs @@ -0,0 +1,10 @@ +mod timeout; + +pub use timeout::Timeout; + +pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> { + lua.globals() + .set("Timeout", lua.create_proxy::()?)?; + + Ok(()) +} diff --git a/src/helpers/timeout.rs b/src/helpers/timeout.rs new file mode 100644 index 0000000..81c14be --- /dev/null +++ b/src/helpers/timeout.rs @@ -0,0 +1,63 @@ +use std::sync::Arc; +use std::time::Duration; + +use tokio::sync::RwLock; +use tokio::task::JoinHandle; +use tracing::debug; + +use crate::action_callback::ActionCallback; + +#[derive(Debug, Default)] +pub struct State { + handle: Option>, +} + +#[derive(Debug, Clone)] +pub struct Timeout { + state: Arc>, +} + +impl mlua::UserData for Timeout { + fn add_methods>(methods: &mut M) { + methods.add_function("new", |_lua, ()| { + let device = Self { + state: Default::default(), + }; + + Ok(device) + }); + + methods.add_async_method( + "start", + |_lua, this, (timeout, callback): (u64, ActionCallback)| async move { + if let Some(handle) = this.state.write().await.handle.take() { + handle.abort(); + } + + debug!("Running timeout callback after {timeout}s"); + + let timeout = Duration::from_secs(timeout); + + this.state.write().await.handle = Some(tokio::spawn({ + async move { + tokio::time::sleep(timeout).await; + + callback.call(false).await; + } + })); + + Ok(()) + }, + ); + + methods.add_async_method("cancel", |_lua, this, ()| async move { + debug!("Canceling timeout callback"); + + if let Some(handle) = this.state.write().await.handle.take() { + handle.abort(); + } + + Ok(()) + }); + } +} diff --git a/src/lib.rs b/src/lib.rs index 2de680a..cca1452 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -9,6 +9,7 @@ pub mod device_manager; pub mod devices; pub mod error; pub mod event; +pub mod helpers; pub mod messages; pub mod mqtt; pub mod schedule; diff --git a/src/main.rs b/src/main.rs index 24ba0be..a865045 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,9 +5,9 @@ use anyhow::anyhow; use automation::auth::User; use automation::config::{FulfillmentConfig, MqttConfig}; use automation::device_manager::DeviceManager; -use automation::devices; use automation::error::ApiError; use automation::mqtt::{self, WrappedAsyncClient}; +use automation::{devices, helpers}; use axum::extract::{FromRef, State}; use axum::http::StatusCode; use axum::routing::post; @@ -112,6 +112,7 @@ async fn app() -> anyhow::Result<()> { lua.globals().set("automation", automation)?; devices::register_with_lua(&lua)?; + helpers::register_with_lua(&lua)?; // TODO: Make this not hardcoded let config_filename = std::env::var("AUTOMATION_CONFIG").unwrap_or("./config.lua".into());