Compare commits

...

3 Commits

Author SHA1 Message Date
a2e65c2d1a refactor: Remove unneeded wrapper functions when specifying callbacks
All checks were successful
Build and deploy / build (push) Successful in 10m32s
Build and deploy / Deploy container (push) Successful in 45s
These wrappers can be moved up to where the callback itself is defined
instead of having to wrap the call manually. This also works a lot nicer
now that it is possible to provide multiple callback functions.
2025-09-08 04:13:01 +02:00
e26fd9f132 fix: Front door presence does not get cleared properly 2025-09-08 04:06:02 +02:00
eb0c80c4ce feat(callback)!: ActionCallback can now receive any amount of arguments
ActionCallback now only has one generics argument that has to implement
IntoLuaMulti, this makes ActionCallback much more flexible as it no
longer always requires two arguments.
2025-09-08 04:06:01 +02:00
12 changed files with 139 additions and 136 deletions

View File

@@ -35,9 +35,9 @@ pub struct Config {
pub sensor_type: SensorType, pub sensor_type: SensorType,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub callback: ActionCallback<ContactSensor, bool>, pub callback: ActionCallback<(ContactSensor, bool)>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub battery_callback: ActionCallback<ContactSensor, f32>, pub battery_callback: ActionCallback<(ContactSensor, f32)>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
@@ -165,14 +165,17 @@ impl OnMqtt for ContactSensor {
return; return;
} }
self.config.callback.call(self, &!is_closed).await; self.config.callback.call((self.clone(), !is_closed)).await;
debug!(id = self.get_id(), "Updating state to {is_closed}"); debug!(id = self.get_id(), "Updating state to {is_closed}");
self.state_mut().await.is_closed = is_closed; self.state_mut().await.is_closed = is_closed;
} }
if let Some(battery) = message.battery { if let Some(battery) = message.battery {
self.config.battery_callback.call(self, &battery).await; self.config
.battery_callback
.call((self.clone(), battery))
.await;
} }
} }
} }

View File

@@ -21,19 +21,19 @@ pub struct Config {
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub left_callback: ActionCallback<HueSwitch, ()>, pub left_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub right_callback: ActionCallback<HueSwitch, ()>, pub right_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub left_hold_callback: ActionCallback<HueSwitch, ()>, pub left_hold_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub right_hold_callback: ActionCallback<HueSwitch, ()>, pub right_hold_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub battery_callback: ActionCallback<HueSwitch, f32>, pub battery_callback: ActionCallback<(HueSwitch, f32)>,
} }
#[derive(Debug, Copy, Clone, Deserialize)] #[derive(Debug, Copy, Clone, Deserialize)]
@@ -104,19 +104,21 @@ impl OnMqtt for HueSwitch {
); );
match action { match action {
Action::LeftPressRelease => self.config.left_callback.call(self, &()).await, Action::LeftPressRelease => self.config.left_callback.call(self.clone()).await,
Action::RightPressRelease => self.config.right_callback.call(self, &()).await, Action::RightPressRelease => {
Action::LeftHold => self.config.left_hold_callback.call(self, &()).await, self.config.right_callback.call(self.clone()).await
Action::RightHold => self.config.right_hold_callback.call(self, &()).await, }
Action::LeftHold => self.config.left_hold_callback.call(self.clone()).await,
Action::RightHold => self.config.right_hold_callback.call(self.clone()).await,
// If there is no hold action, the switch will act like a normal release // If there is no hold action, the switch will act like a normal release
Action::RightHoldRelease => { Action::RightHoldRelease => {
if !self.config.right_hold_callback.is_set() { if self.config.right_hold_callback.is_empty() {
self.config.right_callback.call(self, &()).await self.config.right_callback.call(self.clone()).await
} }
} }
Action::LeftHoldRelease => { Action::LeftHoldRelease => {
if !self.config.left_hold_callback.is_set() { if self.config.left_hold_callback.is_empty() {
self.config.left_callback.call(self, &()).await self.config.left_callback.call(self.clone()).await
} }
} }
_ => {} _ => {}
@@ -124,7 +126,10 @@ impl OnMqtt for HueSwitch {
} }
if let Some(battery) = message.battery { if let Some(battery) = message.battery {
self.config.battery_callback.call(self, &battery).await; self.config
.battery_callback
.call((self.clone(), battery))
.await;
} }
} }
} }

View File

@@ -24,9 +24,9 @@ pub struct Config {
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub callback: ActionCallback<IkeaRemote, bool>, pub callback: ActionCallback<(IkeaRemote, bool)>,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub battery_callback: ActionCallback<IkeaRemote, f32>, pub battery_callback: ActionCallback<(IkeaRemote, f32)>,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, LuaDevice)]
@@ -88,12 +88,15 @@ impl OnMqtt for IkeaRemote {
}; };
if let Some(on) = on { if let Some(on) = on {
self.config.callback.call(self, &on).await; self.config.callback.call((self.clone(), on)).await;
} }
} }
if let Some(battery) = message.battery { if let Some(battery) = message.battery {
self.config.battery_callback.call(self, &battery).await; self.config
.battery_callback
.call((self.clone(), battery))
.await;
} }
} }
} }

View File

@@ -21,7 +21,7 @@ pub struct Config {
pub max: isize, pub max: isize,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub callback: ActionCallback<LightSensor, bool>, pub callback: ActionCallback<(LightSensor, bool)>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
@@ -114,7 +114,7 @@ impl OnMqtt for LightSensor {
self.config self.config
.callback .callback
.call(self, &!self.state().await.is_dark) .call((self.clone(), !self.state().await.is_dark))
.await; .await;
} }
} }

View File

@@ -20,7 +20,7 @@ pub struct Config {
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub callback: ActionCallback<Presence, bool>, pub callback: ActionCallback<(Presence, bool)>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
@@ -118,7 +118,10 @@ impl OnMqtt for Presence {
debug!("Overall presence updated: {overall_presence}"); debug!("Overall presence updated: {overall_presence}");
self.state_mut().await.current_overall_presence = overall_presence; self.state_mut().await.current_overall_presence = overall_presence;
self.config.callback.call(self, &overall_presence).await; self.config
.callback
.call((self.clone(), overall_presence))
.await;
} }
} }
} }

View File

@@ -21,7 +21,7 @@ pub struct Config {
pub threshold: f32, pub threshold: f32,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub done_callback: ActionCallback<Washer, ()>, pub done_callback: ActionCallback<Washer>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
@@ -109,7 +109,7 @@ impl OnMqtt for Washer {
self.state_mut().await.running = 0; self.state_mut().await.running = 0;
self.config.done_callback.call(self, &()).await; self.config.done_callback.call(self.clone()).await;
} else if power < self.config.threshold { } else if power < self.config.threshold {
// Prevent false positives // Prevent false positives
self.state_mut().await.running = 0; self.state_mut().await.running = 0;

View File

@@ -34,7 +34,7 @@ pub struct Config<T: LightState> {
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub callback: ActionCallback<Light<T>, T>, pub callback: ActionCallback<(Light<T>, T)>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
@@ -165,7 +165,7 @@ impl OnMqtt for Light<StateOnOff> {
self.config self.config
.callback .callback
.call(self, self.state().await.deref()) .call((self.clone(), self.state().await.clone()))
.await; .await;
} }
} }
@@ -204,7 +204,7 @@ impl OnMqtt for Light<StateBrightness> {
self.config self.config
.callback .callback
.call(self, self.state().await.deref()) .call((self.clone(), self.state().await.clone()))
.await; .await;
} }
} }
@@ -245,7 +245,7 @@ impl OnMqtt for Light<StateColorTemperature> {
self.config self.config
.callback .callback
.call(self, self.state().await.deref()) .call((self.clone(), self.state().await.clone()))
.await; .await;
} }
} }

View File

@@ -51,7 +51,7 @@ pub struct Config<T: OutletState> {
pub outlet_type: OutletType, pub outlet_type: OutletType,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
pub callback: ActionCallback<Outlet<T>, T>, pub callback: ActionCallback<(Outlet<T>, T)>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
@@ -155,7 +155,7 @@ impl OnMqtt for Outlet<StateOnOff> {
self.config self.config
.callback .callback
.call(self, self.state().await.deref()) .call((self.clone(), self.state().await.clone()))
.await; .await;
} }
} }
@@ -192,7 +192,7 @@ impl OnMqtt for Outlet<StatePower> {
self.config self.config
.callback .callback
.call(self, self.state().await.deref()) .call((self.clone(), self.state().await.clone()))
.await; .await;
} }
} }

View File

@@ -1,34 +1,28 @@
use std::marker::PhantomData; use std::marker::PhantomData;
use futures::future::try_join_all; use futures::future::try_join_all;
use mlua::{FromLua, IntoLua, LuaSerdeExt}; use mlua::{FromLua, IntoLuaMulti};
use serde::Serialize;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct Internal { pub struct ActionCallback<P> {
callbacks: Vec<mlua::Function>, callbacks: Vec<mlua::Function>,
lua: mlua::Lua, _parameters: PhantomData<P>,
} }
#[derive(Debug, Clone)] // NOTE: For some reason the derive macro combined with PhantomData leads to issues where it
pub struct ActionCallback<T, S> { // requires all types part of P to implement default, even if they never actually get constructed.
internal: Option<Internal>, // By manually implemented Default it works fine.
_this: PhantomData<T>, impl<P> Default for ActionCallback<P> {
_state: PhantomData<S>,
}
impl<T, S> Default for ActionCallback<T, S> {
fn default() -> Self { fn default() -> Self {
Self { Self {
internal: None, callbacks: Default::default(),
_this: PhantomData::<T>, _parameters: Default::default(),
_state: PhantomData::<S>,
} }
} }
} }
impl<T, S> FromLua for ActionCallback<T, S> { impl<P> FromLua for ActionCallback<P> {
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> { fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
let callbacks = match value { let callbacks = match value {
mlua::Value::Function(f) => vec![f], mlua::Value::Function(f) => vec![f],
mlua::Value::Table(table) => table mlua::Value::Table(table) => table
@@ -49,40 +43,28 @@ impl<T, S> FromLua for ActionCallback<T, S> {
}; };
Ok(ActionCallback { Ok(ActionCallback {
internal: Some(Internal {
callbacks, callbacks,
lua: lua.clone(), _parameters: PhantomData::<P>,
}),
_this: PhantomData::<T>,
_state: PhantomData::<S>,
}) })
} }
} }
// TODO: Return proper error here // TODO: Return proper error here
impl<T, S> ActionCallback<T, S> impl<P> ActionCallback<P>
where where
T: IntoLua + Sync + Send + Clone + 'static, P: IntoLuaMulti + Sync + Clone,
S: Serialize,
{ {
pub async fn call(&self, this: &T, state: &S) { pub async fn call(&self, parameters: P) {
let Some(internal) = self.internal.as_ref() else {
return;
};
let state = internal.lua.to_value(state).unwrap();
try_join_all( try_join_all(
internal self.callbacks
.callbacks
.iter() .iter()
.map(async |f| f.call_async::<()>((this.clone(), state.clone())).await), .map(async |f| f.call_async::<()>(parameters.clone()).await),
) )
.await .await
.unwrap(); .unwrap();
} }
pub fn is_set(&self) -> bool { pub fn is_empty(&self) -> bool {
self.internal.is_some() self.callbacks.is_empty()
} }
} }

View File

@@ -29,7 +29,7 @@ impl mlua::UserData for Timeout {
methods.add_async_method( methods.add_async_method(
"start", "start",
async |_lua, this, (timeout, callback): (f32, ActionCallback<mlua::Value, bool>)| { async |_lua, this, (timeout, callback): (f32, ActionCallback<()>)| {
if let Some(handle) = this.state.write().await.handle.take() { if let Some(handle) = this.state.write().await.handle.take() {
handle.abort(); handle.abort();
} }
@@ -42,7 +42,7 @@ impl mlua::UserData for Timeout {
async move { async move {
tokio::time::sleep(timeout).await; tokio::time::sleep(timeout).await;
callback.call(&mlua::Nil, &false).await; callback.call(()).await;
} }
})); }));

View File

@@ -28,7 +28,13 @@ impl mlua::UserData for WrappedAsyncClient {
methods.add_async_method( methods.add_async_method(
"send_message", "send_message",
async |_lua, this, (topic, message): (String, mlua::Value)| { async |_lua, this, (topic, message): (String, mlua::Value)| {
let message = serde_json::to_string(&message).unwrap(); // serde_json converts nil => "null", but we actually want nil to send an empty
// message
let message = if message.is_nil() {
"".into()
} else {
serde_json::to_string(&message).unwrap()
};
debug!("message = {message}"); debug!("message = {message}");

View File

@@ -425,12 +425,15 @@ device_manager:add(HueSwitch.new({
local hallway_light_automation = { local hallway_light_automation = {
timeout = Timeout.new(), timeout = Timeout.new(),
forced = false, forced = false,
switch_callback = function(self, on) switch_callback = function(self)
return function(_, on)
self.timeout:cancel() self.timeout:cancel()
self.group.set_on(on) self.group.set_on(on)
self.forced = on self.forced = on
end
end, end,
door_callback = function(self, open) door_callback = function(self)
return function(_, open)
if open then if open then
self.timeout:cancel() self.timeout:cancel()
@@ -442,8 +445,10 @@ local hallway_light_automation = {
end end
end) end)
end end
end
end, end,
trash_callback = function(self, open) trash_callback = function(self)
return function(_, open)
if open then if open then
self.group.set_on(true) self.group.set_on(true)
else else
@@ -455,20 +460,23 @@ local hallway_light_automation = {
self.group.set_on(false) self.group.set_on(false)
end end
end end
end
end, end,
light_callback = function(self, on) light_callback = function(self)
return function(_, state)
if if
on state.on
and (self.trash == nil or self.trash:open_percent()) == 0 and (self.trash == nil or self.trash:open_percent()) == 0
and (self.door == nil or self.door:open_percent() == 0) and (self.door == nil or self.door:open_percent() == 0)
then then
-- If the door and trash are not open, that means the light got turned on manually -- If the door and trash are not open, that means the light got turned on manually
self.timeout:cancel() self.timeout:cancel()
self.forced = true self.forced = true
elseif not on then elseif not state.on then
-- The light is never forced when it is off -- The light is never forced when it is off
self.forced = false self.forced = false
end end
end
end, end,
} }
@@ -477,9 +485,7 @@ local hallway_storage = LightBrightness.new({
room = "Hallway", room = "Hallway",
topic = mqtt_z2m("hallway/storage"), topic = mqtt_z2m("hallway/storage"),
client = mqtt_client, client = mqtt_client,
callback = function(_, state) callback = hallway_light_automation:light_callback(),
hallway_light_automation:light_callback(state.state)
end,
}) })
turn_off_when_away(hallway_storage) turn_off_when_away(hallway_storage)
device_manager:add(hallway_storage) device_manager:add(hallway_storage)
@@ -504,13 +510,12 @@ hallway_light_automation.group = {
end, end,
} }
local frontdoor_presence = { local function presence(duration)
timeout = Timeout.new(), local timeout = Timeout.new()
}
setmetatable(frontdoor_presence, { return function(_, open)
__call = function(self, open)
if open then if open then
self.timeout:cancel() timeout:cancel()
if not presence_system:overall_presence() then if not presence_system:overall_presence() then
mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), { mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), {
@@ -519,21 +524,19 @@ setmetatable(frontdoor_presence, {
}) })
end end
else else
self.timeout:start(debug and 10 or 15 * 60, function() timeout:start(duration, function()
mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), {}) mqtt_client:send_message(mqtt_automation("presence/contact/frontdoor"), nil)
end) end)
end end
end, end
}) end
device_manager:add(IkeaRemote.new({ device_manager:add(IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Hallway", room = "Hallway",
client = mqtt_client, client = mqtt_client,
topic = mqtt_z2m("hallway/remote"), topic = mqtt_z2m("hallway/remote"),
callback = function(_, on) callback = hallway_light_automation:switch_callback(),
hallway_light_automation:switch_callback(on)
end,
battery_callback = check_battery, battery_callback = check_battery,
})) }))
local hallway_frontdoor = ContactSensor.new({ local hallway_frontdoor = ContactSensor.new({
@@ -546,10 +549,10 @@ local hallway_frontdoor = ContactSensor.new({
topic = mqtt_automation("presence/contact/frontdoor"), topic = mqtt_automation("presence/contact/frontdoor"),
timeout = debug and 10 or 15 * 60, timeout = debug and 10 or 15 * 60,
}, },
callback = function(_, open) callback = {
hallway_light_automation:door_callback(open) presence(debug and 10 or 15 * 60),
frontdoor_presence(open) hallway_light_automation:door_callback(),
end, },
battery_callback = check_battery, battery_callback = check_battery,
}) })
device_manager:add(hallway_frontdoor) device_manager:add(hallway_frontdoor)
@@ -561,9 +564,7 @@ local hallway_trash = ContactSensor.new({
sensor_type = "Drawer", sensor_type = "Drawer",
topic = mqtt_z2m("hallway/trash"), topic = mqtt_z2m("hallway/trash"),
client = mqtt_client, client = mqtt_client,
callback = function(_, open) callback = hallway_light_automation:trash_callback(),
hallway_light_automation:trash_callback(open)
end,
battery_callback = check_battery, battery_callback = check_battery,
}) })
device_manager:add(hallway_trash) device_manager:add(hallway_trash)