From ce870cfcb595deacde67c700ff3a564343b17f07 Mon Sep 17 00:00:00 2001 From: Dreaded_X Date: Fri, 19 Jul 2024 22:50:58 +0200 Subject: [PATCH] WIP: Lua action callback --- Cargo.lock | 4 +-- Cargo.toml | 2 +- config.lua | 48 +++++++++++++++++++++++++++++++---- src/device_manager.rs | 14 ++++++++-- src/devices/contact_sensor.rs | 37 +++++++++++++++++++++++++++ src/lib.rs | 1 + src/main.rs | 16 ++++++++++++ 7 files changed, 112 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2a2c450..52246b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2221,9 +2221,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.8.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" dependencies = [ "getrandom", ] diff --git a/Cargo.toml b/Cargo.toml index fa901b1..cf0f1e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,7 +54,7 @@ mlua = { version = "0.9.7", features = [ once_cell = "1.19.0" hostname = "0.4.0" tokio-util = { version = "0.7.11", features = ["full"] } -uuid = "1.8.0" +uuid = { version = "1.8.0", features = ["v4"] } [patch.crates-io] wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" } diff --git a/config.lua b/config.lua index 3750fb7..d632e1a 100644 --- a/config.lua +++ b/config.lua @@ -36,11 +36,12 @@ automation.device_manager:add(Ntfy.new({ event_channel = automation.device_manager:event_channel(), })) -automation.device_manager:add(Presence.new({ +local presence = Presence.new({ topic = mqtt_automation("presence/+/#"), client = mqtt_client, event_channel = automation.device_manager:event_channel(), -})) +}) +automation.device_manager:add(presence) automation.device_manager:add(DebugBridge.new({ identifier = "debug_bridge", @@ -164,15 +165,48 @@ automation.device_manager:add(ContactSensor.new({ devices = { hallway_lights }, timeout = debug and 10 or 2 * 60, }, + action = function(closed) + -- if state then + -- hallway_lights:set_on(true) + -- presence:set_presence("contact/frontdoor", true) + -- else + -- -- TODO: New timeout implementation -> Device stores timestamp of last state change. + -- -- Generic timeout implementation then works by storing the timestamp at the start and comparing once the timer expires. + -- -- If the timestamp has changed -> Cancel the state change + -- -- Maybe add some sort identifier, such that if the function gets called repeatedly it will overwrite the existing timeout? + -- -- Tag? Autogenerated from position in lua code? Not sure if that is possible + -- automation.timeout(2 * 60, function() + -- hallway_lights:set_on(false) + -- end) + -- automation.timeout(15 * 60, function() + -- presence:set_presence("contact/frontdoor", false) + -- end) + -- end + end, })) +local function trash_action(device) + local previous = device:is_on() + local function f(closed) + if closed then + if not previous then + device:set_on(false) + end + else + previous = device:is_on() + device:set_on(true) + end + end + + return f +end + automation.device_manager:add(ContactSensor.new({ identifier = "hallway_trash", topic = mqtt_z2m("hallway/trash"), client = mqtt_client, - trigger = { - devices = { hallway_lights }, - }, + trigger = { devices = {} }, + action = trash_action(hallway_lights), })) local bedroom_air_filter = AirFilter.new({ @@ -189,3 +223,7 @@ end) automation.device_manager:schedule("0 0 20 * * *", function() bedroom_air_filter:set_on(false) end) + +automation.timeout(10, function() + print("Cool stuff") +end) diff --git a/src/device_manager.rs b/src/device_manager.rs index 5d3143e..e162b63 100644 --- a/src/device_manager.rs +++ b/src/device_manager.rs @@ -54,6 +54,17 @@ impl mlua::UserData for WrappedDevice { Ok(()) }); + + methods.add_async_method("is_on", |_lua, this, _: ()| async move { + let device = this.0.read().await; + let device = device.as_ref(); + + if let Some(device) = device.cast() as Option<&dyn OnOff> { + return Ok(device.on().await.unwrap()); + }; + + Ok(false) + }); } } @@ -229,8 +240,7 @@ impl mlua::UserData for DeviceManager { let uuid = this.scheduler.add(job).await.unwrap(); // Store the function in the registry - lua.set_named_registry_value(uuid.to_string().as_str(), f) - .unwrap(); + lua.set_named_registry_value(&uuid.to_string(), f).unwrap(); Ok(()) }, diff --git a/src/devices/contact_sensor.rs b/src/devices/contact_sensor.rs index c3231b3..11481a9 100644 --- a/src/devices/contact_sensor.rs +++ b/src/devices/contact_sensor.rs @@ -5,6 +5,7 @@ use automation_macro::{LuaDevice, LuaDeviceConfig}; use google_home::traits::OnOff; use mlua::FromLua; use tokio::task::JoinHandle; +use tokio_util::task::LocalPoolHandle; use tracing::{debug, error, trace, warn}; use super::{Device, LuaDeviceCreate}; @@ -16,6 +17,7 @@ use crate::event::{OnMqtt, OnPresence}; use crate::messages::{ContactMessage, PresenceMessage}; use crate::mqtt::WrappedAsyncClient; use crate::traits::Timeout; +use crate::LUA; // NOTE: If we add more presence devices we might need to move this out of here #[derive(Debug, Clone, LuaDeviceConfig)] @@ -49,6 +51,37 @@ pub struct TriggerConfig { pub timeout: Option, } +#[derive(Debug, Clone, Copy)] +pub struct ActionCallback(uuid::Uuid); + +impl<'lua> FromLua<'lua> for ActionCallback { + fn from_lua(value: mlua::Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result { + let uuid = uuid::Uuid::new_v4(); + lua.set_named_registry_value(&uuid.to_string(), value)?; + + Ok(ActionCallback(uuid)) + } +} + +impl ActionCallback { + async fn call(&self, closed: bool) { + let pool = LocalPoolHandle::new(1); + let uuid = self.0; + + pool.spawn_pinned(move || async move { + let lua = LUA.lock().await; + let action: mlua::Value = lua.named_registry_value(&uuid.to_string())?; + match action { + mlua::Value::Function(f) => f.call_async::<_, ()>(closed).await, + _ => todo!("Only functions are currently supported"), + } + }) + .await + .unwrap() + .unwrap() + } +} + #[derive(Debug, Clone, LuaDeviceConfig)] pub struct ContactSensorConfig { pub identifier: String, @@ -60,6 +93,8 @@ pub struct ContactSensorConfig { pub trigger: Option, #[device_config(from_lua)] pub client: WrappedAsyncClient, + #[device_config(from_lua)] + pub action: ActionCallback, } #[derive(Debug, LuaDevice)] @@ -150,6 +185,8 @@ impl OnMqtt for ContactSensor { debug!(id = self.config.identifier, "Updating state to {is_closed}"); self.is_closed = is_closed; + self.config.action.call(self.is_closed).await; + if let Some(trigger) = &mut self.config.trigger { if !self.is_closed { for (light, previous) in &mut trigger.devices { diff --git a/src/lib.rs b/src/lib.rs index b04db7f..9b66238 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ #![allow(incomplete_features)] #![feature(specialization)] #![feature(let_chains)] +#![feature(unboxed_closures)] use once_cell::sync::Lazy; use tokio::sync::Mutex; diff --git a/src/main.rs b/src/main.rs index b88c35c..181e9e4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,6 +18,7 @@ use dotenvy::dotenv; use google_home::{GoogleHome, Request}; use mlua::LuaSerdeExt; use rumqttc::AsyncClient; +use tokio_util::task::LocalPoolHandle; use tracing::{debug, error, info, warn}; #[derive(Clone)] @@ -79,6 +80,21 @@ async fn app() -> anyhow::Result<()> { automation.set("new_mqtt_client", new_mqtt_client)?; automation.set("device_manager", device_manager.clone())?; + let timeout = lua.create_function(|lua, (t, f): (u64, mlua::Function)| { + let pool = LocalPoolHandle::new(1); + let key = lua.create_registry_value(f).unwrap(); + pool.spawn_pinned(move || async move { + tokio::time::sleep(std::time::Duration::from_secs(t)).await; + let lua = LUA.lock().await; + let f: mlua::Function = lua.registry_value(&key).unwrap(); + f.call_async::<_, ()>(()).await.unwrap(); + lua.remove_registry_value(key).unwrap(); + }); + + Ok(()) + })?; + automation.set("timeout", timeout)?; + let util = lua.create_table()?; let get_env = lua.create_function(|_lua, name: String| { std::env::var(name).map_err(mlua::ExternalError::into_lua_err)