Compare commits

...

5 Commits

Author SHA1 Message Date
2f181e35f3 refactor!: ActionCallback can now receive any amount of arguments
All checks were successful
Build and deploy / build (push) Successful in 12m37s
Build and deploy / Deploy container (push) Successful in 33s
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 03:30:01 +02:00
613fd63895 feat: Added derive macro to implement IntoLua on structs that implement Serialize
This can be very useful if you want to convert a data struct to a lua
table without having to write the boilerplane (however small it may
be).

It also adds the macro on several state structs so they can be
converted to lua in the upcoming ActionCallback refactor.
2025-09-08 03:18:12 +02:00
cfa482aa03 fix: Added missing default for IkeaRemote callback 2025-09-08 02:14:09 +02:00
379d840158 feat: Allow for multiple callbacks inside of an ActionCallback
This also results in the conversion being performed when the
ActionCallback is instantiated instead of when it is called, this should
make it easier to catch errors.
2025-09-08 02:11:05 +02:00
0c428d1d9b refactor: Store callback function directly instead of in the registry 2025-09-08 02:09:11 +02:00
12 changed files with 122 additions and 91 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

@@ -23,10 +23,10 @@ pub struct Config {
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
#[device_config(from_lua)]
pub callback: ActionCallback<IkeaRemote, bool>,
#[device_config(from_lua, default)]
pub battery_callback: ActionCallback<IkeaRemote, f32>,
pub callback: ActionCallback<(IkeaRemote, bool)>,
#[device_config(from_lua, default)]
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

@@ -10,7 +10,7 @@ use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt;
use automation_lib::helpers::serialization::state_deserializer;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaSerialize};
use google_home::device;
use google_home::errors::ErrorCode;
use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff};
@@ -34,13 +34,13 @@ 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,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
pub struct StateOnOff {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
@@ -48,7 +48,7 @@ pub struct StateOnOff {
impl LightState for StateOnOff {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
pub struct StateBrightness {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
@@ -63,7 +63,7 @@ impl From<StateBrightness> for StateOnOff {
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
pub struct StateColorTemperature {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
@@ -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

@@ -10,7 +10,7 @@ use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt;
use automation_lib::helpers::serialization::state_deserializer;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaSerialize};
use google_home::device;
use google_home::errors::ErrorCode;
use google_home::traits::OnOff;
@@ -51,13 +51,13 @@ 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,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
pub struct StateOnOff {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
@@ -65,7 +65,7 @@ pub struct StateOnOff {
impl OutletState for StateOnOff {}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[derive(Debug, Clone, Default, Serialize, Deserialize, LuaSerialize)]
pub struct StatePower {
#[serde(deserialize_with = "state_deserializer")]
state: bool,
@@ -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,71 +1,70 @@
use std::marker::PhantomData;
use mlua::{FromLua, IntoLua, LuaSerdeExt};
use serde::Serialize;
use futures::future::try_join_all;
use mlua::{FromLua, IntoLuaMulti};
#[derive(Debug, Clone)]
struct Internal {
uuid: uuid::Uuid,
lua: mlua::Lua,
pub struct ActionCallback<P> {
callbacks: Vec<mlua::Function>,
_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> {
let uuid = uuid::Uuid::new_v4();
lua.set_named_registry_value(&uuid.to_string(), value)?;
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
.pairs::<mlua::Value, mlua::Function>()
.map(|pair| {
let (_, f) = pair?;
Ok::<_, mlua::Error>(f)
})
.try_collect()?,
_ => {
return Err(mlua::Error::FromLuaConversionError {
from: value.type_name(),
to: "ActionCallback".into(),
message: Some("expected function or table of functions".into()),
});
}
};
Ok(ActionCallback {
internal: Some(Internal {
uuid,
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();
let callback: mlua::Value = internal
.lua
.named_registry_value(&internal.uuid.to_string())
pub async fn call(&self, parameters: P) {
try_join_all(
self.callbacks
.iter()
.map(async |f| f.call_async::<()>(parameters.clone()).await),
)
.await
.unwrap();
match callback {
mlua::Value::Function(f) => f.call_async::<()>((this.clone(), state)).await.unwrap(),
_ => todo!("Only functions are currently supported"),
}
}
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;
}
}));

View File

@@ -1,4 +1,5 @@
#![allow(incomplete_features)]
#![feature(iterator_try_collect)]
pub mod action_callback;
pub mod config;

View File

@@ -3,6 +3,7 @@ mod impl_device;
mod lua_device_config;
use lua_device_config::impl_lua_device_config_macro;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
use crate::impl_device::impl_device_macro;
@@ -20,3 +21,19 @@ pub fn impl_device(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
impl_device_macro(&ast).into()
}
#[proc_macro_derive(LuaSerialize, attributes(traits))]
pub fn lua_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let name = &ast.ident;
quote! {
impl ::mlua::IntoLua for #name {
fn into_lua(self, lua: &::mlua::Lua) -> ::mlua::Result<::mlua::Value> {
::mlua::LuaSerdeExt::to_value(lua, &self)
}
}
}
.into()
}