feat!: 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.
This commit is contained in:
2025-09-08 03:30:01 +02:00
parent 352654107a
commit 5383e7265d
10 changed files with 71 additions and 75 deletions

View File

@@ -35,9 +35,9 @@ pub struct Config {
pub sensor_type: SensorType,
#[device_config(from_lua, default)]
pub callback: ActionCallback<ContactSensor, bool>,
pub callback: ActionCallback<(ContactSensor, bool)>,
#[device_config(from_lua, default)]
pub battery_callback: ActionCallback<ContactSensor, f32>,
pub battery_callback: ActionCallback<(ContactSensor, f32)>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
@@ -165,14 +165,17 @@ impl OnMqtt for ContactSensor {
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}");
self.state_mut().await.is_closed = is_closed;
}
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,
#[device_config(from_lua, default)]
pub left_callback: ActionCallback<HueSwitch, ()>,
pub left_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)]
pub right_callback: ActionCallback<HueSwitch, ()>,
pub right_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)]
pub left_hold_callback: ActionCallback<HueSwitch, ()>,
pub left_hold_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)]
pub right_hold_callback: ActionCallback<HueSwitch, ()>,
pub right_hold_callback: ActionCallback<HueSwitch>,
#[device_config(from_lua, default)]
pub battery_callback: ActionCallback<HueSwitch, f32>,
pub battery_callback: ActionCallback<(HueSwitch, f32)>,
}
#[derive(Debug, Copy, Clone, Deserialize)]
@@ -104,19 +104,21 @@ impl OnMqtt for HueSwitch {
);
match action {
Action::LeftPressRelease => self.config.left_callback.call(self, &()).await,
Action::RightPressRelease => self.config.right_callback.call(self, &()).await,
Action::LeftHold => self.config.left_hold_callback.call(self, &()).await,
Action::RightHold => self.config.right_hold_callback.call(self, &()).await,
Action::LeftPressRelease => self.config.left_callback.call(self.clone()).await,
Action::RightPressRelease => {
self.config.right_callback.call(self.clone()).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
Action::RightHoldRelease => {
if !self.config.right_hold_callback.is_set() {
self.config.right_callback.call(self, &()).await
if self.config.right_hold_callback.is_empty() {
self.config.right_callback.call(self.clone()).await
}
}
Action::LeftHoldRelease => {
if !self.config.left_hold_callback.is_set() {
self.config.left_callback.call(self, &()).await
if self.config.left_hold_callback.is_empty() {
self.config.left_callback.call(self.clone()).await
}
}
_ => {}
@@ -124,7 +126,10 @@ impl OnMqtt for HueSwitch {
}
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,
#[device_config(from_lua, default)]
pub callback: ActionCallback<IkeaRemote, bool>,
pub callback: ActionCallback<(IkeaRemote, bool)>,
#[device_config(from_lua, default)]
pub battery_callback: ActionCallback<IkeaRemote, f32>,
pub battery_callback: ActionCallback<(IkeaRemote, f32)>,
}
#[derive(Debug, Clone, LuaDevice)]
@@ -88,12 +88,15 @@ impl OnMqtt for IkeaRemote {
};
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 {
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,
#[device_config(from_lua, default)]
pub callback: ActionCallback<LightSensor, bool>,
pub callback: ActionCallback<(LightSensor, bool)>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
@@ -114,7 +114,7 @@ impl OnMqtt for LightSensor {
self.config
.callback
.call(self, &!self.state().await.is_dark)
.call((self.clone(), !self.state().await.is_dark))
.await;
}
}

View File

@@ -20,7 +20,7 @@ pub struct Config {
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, default)]
pub callback: ActionCallback<Presence, bool>,
pub callback: ActionCallback<(Presence, bool)>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
@@ -118,7 +118,10 @@ impl OnMqtt for Presence {
debug!("Overall presence updated: {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,
#[device_config(from_lua, default)]
pub done_callback: ActionCallback<Washer, ()>,
pub done_callback: ActionCallback<Washer>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
@@ -109,7 +109,7 @@ impl OnMqtt for Washer {
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 {
// Prevent false positives
self.state_mut().await.running = 0;

View File

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

View File

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

View File

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

View File

@@ -29,7 +29,7 @@ impl mlua::UserData for Timeout {
methods.add_async_method(
"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() {
handle.abort();
}
@@ -42,7 +42,7 @@ impl mlua::UserData for Timeout {
async move {
tokio::time::sleep(timeout).await;
callback.call(&mlua::Nil, &false).await;
callback.call(()).await;
}
}));