Compare commits

...

7 Commits

Author SHA1 Message Date
5abdc88a35 feat!: Removed AddAdditionalMethods
All checks were successful
Build and deploy / build (push) Successful in 11m1s
Build and deploy / Deploy container (push) Successful in 40s
It has been replaced with the add_methods device attribute.
2025-09-09 04:24:20 +02:00
19cdb37dfb feat: Added attribute to easily register additional lua methods
Previously this could only be done by implementing a trait, like
`AddAdditionalMethods`, that that has an add_methods function where you
can put your custom methods. With this new attribute you can pass in a
register function directly!
2025-09-09 04:23:58 +02:00
3a33e3fa55 refactor!: Rewrote device implementation macro once again
This time with a bit more though put into the design of the code, as a
result the macro should be a lot more robust.

This did result in the macro getting renamed from LuaDevice to Device as
this should be _the_ Device macro.
The attribute also got renamed from traits() to device(traits()) and the
syntax got overhauled to allow for a bit more expression.
2025-09-09 04:05:56 +02:00
e642562ddb chore: Removed old leftover contact sensor presence config
All checks were successful
Build and deploy / build (push) Successful in 11m3s
Build and deploy / Deploy container (push) Successful in 4m8s
2025-09-08 04:29:30 +02:00
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
26 changed files with 495 additions and 329 deletions

1
Cargo.lock generated
View File

@@ -167,6 +167,7 @@ name = "automation_macro"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"itertools", "itertools",
"mlua",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.106", "syn 2.0.106",

View File

@@ -54,7 +54,7 @@ rumqttc = "0.24.0"
serde = { version = "1.0.219", features = ["derive"] } serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.143" serde_json = "1.0.143"
serde_repr = "0.1.20" serde_repr = "0.1.20"
syn = { version = "2.0.106", features = ["extra-traits", "full"] } syn = { version = "2.0.106" }
thiserror = "2.0.16" thiserror = "2.0.16"
tokio = { version = "1", features = ["rt-multi-thread"] } tokio = { version = "1", features = ["rt-multi-thread"] }
tokio-cron-scheduler = "0.14.0" tokio-cron-scheduler = "0.14.0"

View File

@@ -1,7 +1,7 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::config::InfoConfig; use automation_lib::config::InfoConfig;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_macro::{LuaDevice, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use google_home::device::Name; use google_home::device::Name;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{ use google_home::traits::{
@@ -19,8 +19,8 @@ pub struct Config {
pub url: String, pub url: String,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
#[traits(OnOff)] #[device(traits(OnOff))]
pub struct AirFilter { pub struct AirFilter {
config: Config, config: Config,
} }

View File

@@ -8,7 +8,7 @@ use automation_lib::error::DeviceConfigError;
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::messages::ContactMessage; use automation_lib::messages::ContactMessage;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use google_home::device; use google_home::device;
use google_home::errors::{DeviceError, ErrorCode}; use google_home::errors::{DeviceError, ErrorCode};
use google_home::traits::OpenClose; use google_home::traits::OpenClose;
@@ -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,
@@ -48,8 +48,8 @@ struct State {
is_closed: bool, is_closed: bool,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
#[traits(OpenClose)] #[device(traits(OpenClose))]
pub struct ContactSensor { pub struct ContactSensor {
config: Config, config: Config,
state: Arc<RwLock<State>>, state: Arc<RwLock<State>>,
@@ -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

@@ -3,8 +3,7 @@ use std::net::SocketAddr;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::lua::traits::AddAdditionalMethods; use automation_macro::{Device, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig};
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
@@ -31,8 +30,8 @@ pub struct Config {
pub flags: FlagIDs, pub flags: FlagIDs,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
#[traits(AddAdditionalMethods)] #[device(add_methods(Self::add_methods))]
pub struct HueBridge { pub struct HueBridge {
config: Config, config: Config,
} }
@@ -84,19 +83,8 @@ impl HueBridge {
} }
} }
} }
}
impl Device for HueBridge { fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}
impl AddAdditionalMethods for HueBridge {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + 'static,
{
methods.add_async_method( methods.add_async_method(
"set_flag", "set_flag",
async |lua, this, (flag, value): (mlua::Value, bool)| { async |lua, this, (flag, value): (mlua::Value, bool)| {
@@ -109,3 +97,9 @@ impl AddAdditionalMethods for HueBridge {
); );
} }
} }
impl Device for HueBridge {
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}

View File

@@ -2,7 +2,7 @@ use std::net::SocketAddr;
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::OnOff; use google_home::traits::OnOff;
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
@@ -19,8 +19,8 @@ pub struct Config {
pub scene_id: String, pub scene_id: String,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
#[traits(OnOff)] #[device(traits(OnOff))]
pub struct HueGroup { pub struct HueGroup {
config: Config, config: Config,
} }

View File

@@ -4,7 +4,7 @@ use automation_lib::config::{InfoConfig, MqttDeviceConfig};
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use rumqttc::{Publish, matches}; use rumqttc::{Publish, matches};
use serde::Deserialize; use serde::Deserialize;
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
@@ -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)]
@@ -55,7 +55,7 @@ struct State {
battery: Option<f32>, battery: Option<f32>,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
pub struct HueSwitch { pub struct HueSwitch {
config: Config, config: Config,
} }
@@ -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

@@ -5,7 +5,7 @@ use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::messages::{RemoteAction, RemoteMessage}; use automation_lib::messages::{RemoteAction, RemoteMessage};
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use rumqttc::{Publish, matches}; use rumqttc::{Publish, matches};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
@@ -24,12 +24,12 @@ 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, Device)]
pub struct IkeaRemote { pub struct IkeaRemote {
config: Config, config: Config,
} }
@@ -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

@@ -4,7 +4,7 @@ use std::str::Utf8Error;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_macro::{LuaDevice, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use bytes::{Buf, BufMut}; use bytes::{Buf, BufMut};
use google_home::errors::{self, DeviceError}; use google_home::errors::{self, DeviceError};
use google_home::traits::OnOff; use google_home::traits::OnOff;
@@ -21,8 +21,8 @@ pub struct Config {
pub addr: SocketAddr, pub addr: SocketAddr,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
#[traits(OnOff)] #[device(traits(OnOff))]
pub struct KasaOutlet { pub struct KasaOutlet {
config: Config, config: Config,
} }

View File

@@ -7,7 +7,7 @@ use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::messages::BrightnessMessage; use automation_lib::messages::BrightnessMessage;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
@@ -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,
@@ -34,7 +34,7 @@ pub struct State {
is_dark: bool, is_dark: bool,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
pub struct LightSensor { pub struct LightSensor {
config: Config, config: Config,
state: Arc<RwLock<State>>, state: Arc<RwLock<State>>,
@@ -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

@@ -3,8 +3,7 @@ use std::convert::Infallible;
use async_trait::async_trait; use async_trait::async_trait;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::lua::traits::AddAdditionalMethods; use automation_macro::{Device, LuaDeviceConfig};
use automation_macro::{LuaDevice, LuaDeviceConfig};
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_repr::*; use serde_repr::*;
@@ -118,12 +117,27 @@ pub struct Config {
pub topic: String, pub topic: String,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
#[traits(AddAdditionalMethods)] #[device(add_methods(Self::add_methods))]
pub struct Ntfy { pub struct Ntfy {
config: Config, config: Config,
} }
impl Ntfy {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method(
"send_notification",
async |lua, this, notification: mlua::Value| {
let notification: Notification = lua.from_value(notification)?;
this.send(notification).await;
Ok(())
},
);
}
}
#[async_trait] #[async_trait]
impl LuaDeviceCreate for Ntfy { impl LuaDeviceCreate for Ntfy {
type Config = Config; type Config = Config;
@@ -162,21 +176,3 @@ impl Ntfy {
} }
} }
} }
impl AddAdditionalMethods for Ntfy {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + 'static,
{
methods.add_async_method(
"send_notification",
async |lua, this, notification: mlua::Value| {
let notification: Notification = lua.from_value(notification)?;
this.send(notification).await;
Ok(())
},
);
}
}

View File

@@ -6,10 +6,9 @@ use automation_lib::action_callback::ActionCallback;
use automation_lib::config::MqttDeviceConfig; use automation_lib::config::MqttDeviceConfig;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::lua::traits::AddAdditionalMethods;
use automation_lib::messages::PresenceMessage; use automation_lib::messages::PresenceMessage;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
@@ -20,7 +19,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,
@@ -34,8 +33,8 @@ pub struct State {
current_overall_presence: bool, current_overall_presence: bool,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
#[traits(AddAdditionalMethods)] #[device(add_methods(Self::add_methods))]
pub struct Presence { pub struct Presence {
config: Config, config: Config,
state: Arc<RwLock<State>>, state: Arc<RwLock<State>>,
@@ -49,6 +48,12 @@ impl Presence {
async fn state_mut(&self) -> RwLockWriteGuard<'_, State> { async fn state_mut(&self) -> RwLockWriteGuard<'_, State> {
self.state.write().await self.state.write().await
} }
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_method("overall_presence", async |_lua, this, ()| {
Ok(this.state().await.current_overall_presence)
});
}
} }
#[async_trait] #[async_trait]
@@ -118,18 +123,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;
} }
} }
} }
impl AddAdditionalMethods for Presence {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + 'static,
{
methods.add_async_method("overall_presence", async |_lua, this, ()| {
Ok(this.state().await.current_overall_presence)
});
}
}

View File

@@ -6,7 +6,7 @@ use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::messages::ActivateMessage; use automation_lib::messages::ActivateMessage;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use eui48::MacAddress; use eui48::MacAddress;
use google_home::device; use google_home::device;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
@@ -28,7 +28,7 @@ pub struct Config {
pub client: WrappedAsyncClient, pub client: WrappedAsyncClient,
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
pub struct WakeOnLAN { pub struct WakeOnLAN {
config: Config, config: Config,
} }

View File

@@ -7,7 +7,7 @@ use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::messages::PowerMessage; use automation_lib::messages::PowerMessage;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig}; use automation_macro::{Device, LuaDeviceConfig};
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
@@ -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,
@@ -33,7 +33,7 @@ pub struct State {
} }
// TODO: Add google home integration // TODO: Add google home integration
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
pub struct Washer { pub struct Washer {
config: Config, config: Config,
state: Arc<RwLock<State>>, state: Arc<RwLock<State>>,
@@ -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

@@ -10,7 +10,7 @@ use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::helpers::serialization::state_deserializer; use automation_lib::helpers::serialization::state_deserializer;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaSerialize}; use automation_macro::{Device, LuaDeviceConfig, LuaSerialize};
use google_home::device; use google_home::device;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff}; use google_home::traits::{Brightness, Color, ColorSetting, ColorTemperatureRange, OnOff};
@@ -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,
@@ -88,10 +88,10 @@ impl From<StateColorTemperature> for StateBrightness {
} }
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
#[traits(<StateOnOff>: OnOff)] #[device(traits(OnOff for <StateOnOff>, <StateBrightness>, <StateColorTemperature>))]
#[traits(<StateBrightness>: OnOff, Brightness)] #[device(traits(Brightness for <StateBrightness>, <StateColorTemperature>))]
#[traits(<StateColorTemperature>: OnOff, Brightness, ColorSetting)] #[device(traits(ColorSetting for <StateColorTemperature>))]
pub struct Light<T: LightState> { pub struct Light<T: LightState> {
config: Config<T>, config: Config<T>,
@@ -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

@@ -10,7 +10,7 @@ use automation_lib::device::{Device, LuaDeviceCreate};
use automation_lib::event::OnMqtt; use automation_lib::event::OnMqtt;
use automation_lib::helpers::serialization::state_deserializer; use automation_lib::helpers::serialization::state_deserializer;
use automation_lib::mqtt::WrappedAsyncClient; use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaSerialize}; use automation_macro::{Device, LuaDeviceConfig, LuaSerialize};
use google_home::device; use google_home::device;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::OnOff; use google_home::traits::OnOff;
@@ -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,
@@ -80,9 +80,8 @@ impl From<StatePower> for StateOnOff {
} }
} }
#[derive(Debug, Clone, LuaDevice)] #[derive(Debug, Clone, Device)]
#[traits(<StateOnOff>: OnOff)] #[device(traits(OnOff for <StateOnOff>, <StatePower>))]
#[traits(<StatePower>: OnOff)]
pub struct Outlet<T: OutletState> { pub struct Outlet<T: OutletState> {
config: Config<T>, config: Config<T>,
@@ -155,7 +154,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 +191,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, _parameters: PhantomData::<P>,
lua: lua.clone(),
}),
_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

@@ -78,9 +78,3 @@ pub trait OpenClose {
} }
} }
impl<T> OpenClose for T where T: google_home::traits::OpenClose {} impl<T> OpenClose for T where T: google_home::traits::OpenClose {}
pub trait AddAdditionalMethods {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M)
where
Self: Sized + 'static;
}

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

@@ -11,3 +11,6 @@ itertools = { workspace = true }
proc-macro2 = { workspace = true } proc-macro2 = { workspace = true }
quote = { workspace = true } quote = { workspace = true }
syn = { workspace = true } syn = { workspace = true }
[dev-dependencies]
mlua = { workspace = true }

View File

@@ -0,0 +1,235 @@
use std::collections::HashMap;
use proc_macro2::TokenStream as TokenStream2;
use quote::{ToTokens, quote};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::{Attribute, DeriveInput, Token, parenthesized};
enum Attr {
Trait(TraitAttr),
AddMethods(AddMethodsAttr),
}
impl Parse for Attr {
fn parse(input: ParseStream) -> syn::Result<Self> {
let ident: syn::Ident = input.parse()?;
let attr;
_ = parenthesized!(attr in input);
let attr = match ident.to_string().as_str() {
"traits" => Attr::Trait(attr.parse()?),
"add_methods" => Attr::AddMethods(attr.parse()?),
_ => {
return Err(syn::Error::new(
ident.span(),
"Expected 'traits' or 'add_methods'",
));
}
};
Ok(attr)
}
}
struct TraitAttr {
traits: Traits,
generics: Generics,
}
impl Parse for TraitAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
traits: input.parse()?,
generics: input.parse()?,
})
}
}
#[derive(Default)]
struct Traits(Vec<syn::Ident>);
impl Traits {
fn extend(&mut self, other: &Traits) {
self.0.extend_from_slice(&other.0);
}
}
impl Parse for Traits {
fn parse(input: ParseStream) -> syn::Result<Self> {
input
.call(Punctuated::<_, Token![,]>::parse_separated_nonempty)
.map(|traits| traits.into_iter().collect())
.map(Self)
}
}
impl ToTokens for Traits {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self(traits) = &self;
tokens.extend(quote! {
#(
::automation_lib::lua::traits::#traits::add_methods(methods);
)*
});
}
}
#[derive(Default)]
struct Generics(Vec<syn::AngleBracketedGenericArguments>);
impl Generics {
fn has_generics(&self) -> bool {
!self.0.is_empty()
}
}
impl Parse for Generics {
fn parse(input: ParseStream) -> syn::Result<Self> {
if !input.peek(Token![for]) {
if input.is_empty() {
return Ok(Default::default());
} else {
return Err(input.error("Expected ')' or 'for'"));
}
}
_ = input.parse::<syn::Token![for]>()?;
input
.call(Punctuated::<_, Token![,]>::parse_separated_nonempty)
.map(|generics| generics.into_iter().collect())
.map(Self)
}
}
#[derive(Clone)]
struct AddMethodsAttr(syn::Path);
impl Parse for AddMethodsAttr {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self(input.parse()?))
}
}
impl ToTokens for AddMethodsAttr {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self(path) = self;
tokens.extend(quote! {
#path
});
}
}
struct Implementation {
generics: Option<syn::AngleBracketedGenericArguments>,
traits: Traits,
add_methods: Vec<AddMethodsAttr>,
}
impl quote::ToTokens for Implementation {
fn to_tokens(&self, tokens: &mut TokenStream2) {
let Self {
generics,
traits,
add_methods,
} = &self;
tokens.extend(quote! {
#generics {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_function("new", async |_lua, config| {
let device: Self = LuaDeviceCreate::create(config)
.await
.map_err(mlua::ExternalError::into_lua_err)?;
Ok(device)
});
methods.add_method("__box", |_lua, this, _: ()| {
let b: Box<dyn Device> = Box::new(this.clone());
Ok(b)
});
methods.add_async_method("get_id", async |_lua, this, _: ()| { Ok(this.get_id()) });
#traits
#(
#add_methods(methods);
)*
}
}
});
}
}
struct Implementations(Vec<Implementation>);
impl From<Vec<Attr>> for Implementations {
fn from(attributes: Vec<Attr>) -> Self {
let mut add_methods = Vec::new();
let mut all = Traits::default();
let mut implementations: HashMap<_, Traits> = HashMap::new();
for attribute in attributes {
match attribute {
Attr::Trait(attribute) => {
if attribute.generics.has_generics() {
for generic in &attribute.generics.0 {
implementations
.entry(Some(generic.clone()))
.or_default()
.extend(&attribute.traits);
}
} else {
all.extend(&attribute.traits);
}
}
Attr::AddMethods(attribute) => add_methods.push(attribute),
}
}
if implementations.is_empty() {
implementations.entry(None).or_default().extend(&all);
} else {
for traits in implementations.values_mut() {
traits.extend(&all);
}
}
Self(
implementations
.into_iter()
.map(|(generics, traits)| Implementation {
generics,
traits,
add_methods: add_methods.clone(),
})
.collect(),
)
}
}
pub fn device(input: &DeriveInput) -> TokenStream2 {
let name = &input.ident;
let Implementations(imp) = match input
.attrs
.iter()
.filter(|attr| attr.path().is_ident("device"))
.map(Attribute::parse_args)
.try_collect::<Vec<_>>()
{
Ok(result) => result.into(),
Err(err) => return err.into_compile_error(),
};
quote! {
#(
impl mlua::UserData for #name #imp
)*
}
}

View File

@@ -1,88 +0,0 @@
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::parse::Parse;
use syn::punctuated::Punctuated;
use syn::{AngleBracketedGenericArguments, Attribute, DeriveInput, Ident, Path, Token};
#[derive(Debug, Default)]
struct Impl {
generics: Option<AngleBracketedGenericArguments>,
traits: Vec<Path>,
}
impl Parse for Impl {
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
let generics = if input.peek(Token![<]) {
let generics = input.parse()?;
input.parse::<Token![:]>()?;
Some(generics)
} else {
None
};
let traits: Punctuated<_, _> = input.parse_terminated(Path::parse, Token![,])?;
let traits = traits.into_iter().collect();
Ok(Impl { generics, traits })
}
}
impl Impl {
fn generate(&self, name: &Ident) -> TokenStream {
let generics = &self.generics;
// If an identifier is specified, assume it is placed in ::automation_lib::lua::traits,
// otherwise use the provided path
let traits = self.traits.iter().map(|t| {
if let Some(ident) = t.get_ident() {
quote! {::automation_lib::lua::traits::#ident }
} else {
t.to_token_stream()
}
});
quote! {
impl mlua::UserData for #name #generics {
fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
methods.add_async_function("new", async |_lua, config| {
let device: Self = LuaDeviceCreate::create(config)
.await
.map_err(mlua::ExternalError::into_lua_err)?;
Ok(device)
});
methods.add_method("__box", |_lua, this, _: ()| {
let b: Box<dyn Device> = Box::new(this.clone());
Ok(b)
});
methods.add_async_method("get_id", async |_lua, this, _: ()| { Ok(this.get_id()) });
#(
#traits::add_methods(methods);
)*
}
}
}
}
}
pub fn impl_device_macro(ast: &DeriveInput) -> TokenStream {
let name = &ast.ident;
let impls: TokenStream = ast
.attrs
.iter()
.filter(|attr| attr.path().is_ident("traits"))
.flat_map(Attribute::parse_args::<Impl>)
.map(|im| im.generate(name))
.collect();
if impls.is_empty() {
Impl::default().generate(name)
} else {
impls
}
}

View File

@@ -1,13 +1,12 @@
#![feature(iter_intersperse)] #![feature(iter_intersperse)]
mod impl_device; #![feature(iterator_try_collect)]
mod device;
mod lua_device_config; mod lua_device_config;
use lua_device_config::impl_lua_device_config_macro; use lua_device_config::impl_lua_device_config_macro;
use quote::quote; use quote::quote;
use syn::{DeriveInput, parse_macro_input}; use syn::{DeriveInput, parse_macro_input};
use crate::impl_device::impl_device_macro;
#[proc_macro_derive(LuaDeviceConfig, attributes(device_config))] #[proc_macro_derive(LuaDeviceConfig, attributes(device_config))]
pub fn lua_device_config_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn lua_device_config_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput); let ast = parse_macro_input!(input as DeriveInput);
@@ -15,13 +14,6 @@ pub fn lua_device_config_derive(input: proc_macro::TokenStream) -> proc_macro::T
impl_lua_device_config_macro(&ast).into() impl_lua_device_config_macro(&ast).into()
} }
#[proc_macro_derive(LuaDevice, attributes(traits))]
pub fn impl_device(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_device_macro(&ast).into()
}
#[proc_macro_derive(LuaSerialize, attributes(traits))] #[proc_macro_derive(LuaSerialize, attributes(traits))]
pub fn lua_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream { pub fn lua_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput); let ast = parse_macro_input!(input as DeriveInput);
@@ -37,3 +29,45 @@ pub fn lua_serialize(input: proc_macro::TokenStream) -> proc_macro::TokenStream
} }
.into() .into()
} }
/// Derive macro generating an impl for the trait `::mlua::UserData`
///
/// # Device traits
/// The `device(traits)` attribute can be used to tell the macro what traits are implemented so that
/// the appropriate methods can automatically be registered.
/// If the struct does not have any type parameters the syntax is very simple:
/// ```rust
/// #[device(traits(TraitA, TraitB))]
/// ```
///
/// If the type does have type parameters you will have to manually specify all variations that
/// have the trait available:
/// ```rust
/// #[device(traits(TraitA, TraitB for <StateA>, <StateB>))]
/// ```
/// If multiple of these attributes are specified they will all combined appropriately.
///
///
/// ## NOTE
/// If your type _has_ type parameters any instance of the traits attribute that does not specify
/// any type parameters will have the traits applied to _all_ other type parameter variations
/// listed in the other trait attributes. This behavior only applies if there is at least one
/// instance with type parameters specified.
///
/// # Additional methods
/// Additional methods can be added by using the `device(add_methods)` attribute. This attribute
/// takes the path to a function with the following signature that can register the additional methods:
///
/// ```rust
/// # struct D;
/// fn top_secret<M: mlua::UserDataMethods<D>>(methods: &mut M) {}
/// ```
/// It can then be registered with:
/// ```rust
/// #[device(add_methods(top_secret))]
/// ```
#[proc_macro_derive(Device, attributes(device))]
pub fn device(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
device::device(&ast).into()
}

View File

@@ -425,49 +425,57 @@ 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)
self.timeout:cancel() return function(_, on)
self.group.set_on(on)
self.forced = on
end,
door_callback = function(self, open)
if open then
self.timeout:cancel() self.timeout:cancel()
self.group.set_on(on)
self.group.set_on(true) self.forced = on
elseif not self.forced then
self.timeout:start(debug and 10 or 2 * 60, function()
if self.trash == nil or self.trash:open_percent() == 0 then
self.group.set_on(false)
end
end)
end end
end, end,
trash_callback = function(self, open) door_callback = function(self)
if open then return function(_, open)
self.group.set_on(true) if open then
else self.timeout:cancel()
if
not self.timeout:is_waiting() self.group.set_on(true)
and (self.door == nil or self.door:open_percent() == 0) elseif not self.forced then
and not self.forced self.timeout:start(debug and 10 or 2 * 60, function()
then if self.trash == nil or self.trash:open_percent() == 0 then
self.group.set_on(false) self.group.set_on(false)
end
end)
end end
end end
end, end,
light_callback = function(self, on) trash_callback = function(self)
if return function(_, open)
on if open then
and (self.trash == nil or self.trash:open_percent()) == 0 self.group.set_on(true)
and (self.door == nil or self.door:open_percent() == 0) else
then if
-- If the door and trash are not open, that means the light got turned on manually not self.timeout:is_waiting()
self.timeout:cancel() and (self.door == nil or self.door:open_percent() == 0)
self.forced = true and not self.forced
elseif not on then then
-- The light is never forced when it is off self.group.set_on(false)
self.forced = false end
end
end
end,
light_callback = function(self)
return function(_, state)
if
state.on
and (self.trash == nil or self.trash:open_percent()) == 0
and (self.door == nil or self.door:open_percent() == 0)
then
-- If the door and trash are not open, that means the light got turned on manually
self.timeout:cancel()
self.forced = true
elseif not state.on then
-- The light is never forced when it is off
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({
@@ -542,14 +545,10 @@ local hallway_frontdoor = ContactSensor.new({
sensor_type = "Door", sensor_type = "Door",
topic = mqtt_z2m("hallway/frontdoor"), topic = mqtt_z2m("hallway/frontdoor"),
client = mqtt_client, client = mqtt_client,
presence = { callback = {
topic = mqtt_automation("presence/contact/frontdoor"), presence(debug and 10 or 15 * 60),
timeout = debug and 10 or 15 * 60, hallway_light_automation:door_callback(),
}, },
callback = function(_, open)
hallway_light_automation:door_callback(open)
frontdoor_presence(open)
end,
battery_callback = check_battery, battery_callback = check_battery,
}) })
device_manager:add(hallway_frontdoor) device_manager:add(hallway_frontdoor)
@@ -561,9 +560,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)

View File

@@ -7,7 +7,7 @@ mod web;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
use std::process; use std::process;
use std::time::{SystemTime, UNIX_EPOCH}; use std::time::{Duration, SystemTime, UNIX_EPOCH};
use ::config::{Environment, File}; use ::config::{Environment, File};
use automation_lib::config::{FulfillmentConfig, MqttConfig}; use automation_lib::config::{FulfillmentConfig, MqttConfig};
@@ -172,6 +172,11 @@ async fn app() -> anyhow::Result<()> {
.as_millis()) .as_millis())
})?; })?;
utils.set("get_epoch", get_epoch)?; utils.set("get_epoch", get_epoch)?;
let sleep = lua.create_async_function(async |_lua, duration: u64| {
tokio::time::sleep(Duration::from_millis(duration)).await;
Ok(())
})?;
utils.set("sleep", sleep)?;
lua.register_module("utils", utils)?; lua.register_module("utils", utils)?;
automation_devices::register_with_lua(&lua)?; automation_devices::register_with_lua(&lua)?;