Compare commits

...

9 Commits

Author SHA1 Message Date
60bf0f236c refactor(config)!: Move device proxies into module
All checks were successful
Build and deploy / build (push) Successful in 10m0s
Build and deploy / Deploy container (push) Successful in 43s
Instead of registering the device proxies in the global namespace they
are now registered in a module called `devices`.
2025-09-10 01:46:17 +02:00
07e2d18a28 feat!: Improve lua module registration
Instead of having to call all the module registration functions in one
place it is possible for each module to register itself in a global registry.
During startup all the all the modules will be registered
automatically.

This does currently have one weakness, to need to ensure that the crate
is linked.
2025-09-10 01:46:17 +02:00
d008bb0786 feat!: Removed AddAdditionalMethods
It has been replaced with the add_methods device attribute.
2025-09-10 01:46:17 +02:00
43cb1b31fd 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-10 01:46:16 +02:00
ff78ac0c76 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-10 01:46:16 +02:00
aad089aa10 chore: Removed old leftover contact sensor presence config 2025-09-10 01:46:16 +02:00
18e40726fe refactor: Remove unneeded wrapper functions when specifying callbacks
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-10 01:46:16 +02:00
1925bac73c fix: Front door presence does not get cleared properly 2025-09-10 01:46:16 +02:00
5383e7265d 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.
2025-09-10 01:46:12 +02:00
30 changed files with 612 additions and 393 deletions

13
Cargo.lock generated
View File

@@ -100,6 +100,7 @@ dependencies = [
"git-version", "git-version",
"google_home", "google_home",
"hostname", "hostname",
"inventory",
"mlua", "mlua",
"reqwest", "reqwest",
"rumqttc", "rumqttc",
@@ -128,6 +129,7 @@ dependencies = [
"dyn-clone", "dyn-clone",
"eui48", "eui48",
"google_home", "google_home",
"inventory",
"mlua", "mlua",
"reqwest", "reqwest",
"rumqttc", "rumqttc",
@@ -151,6 +153,7 @@ dependencies = [
"futures", "futures",
"google_home", "google_home",
"indexmap", "indexmap",
"inventory",
"mlua", "mlua",
"rumqttc", "rumqttc",
"serde", "serde",
@@ -167,6 +170,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",
@@ -966,6 +970,15 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "inventory"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc61209c082fbeb19919bee74b176221b27223e27b65d781eb91af24eb1fb46e"
dependencies = [
"rustversion",
]
[[package]] [[package]]
name = "io-uring" name = "io-uring"
version = "0.7.10" version = "0.7.10"

View File

@@ -34,6 +34,7 @@ google_home = { path = "./google_home/google_home" }
google_home_macro = { path = "./google_home/google_home_macro" } google_home_macro = { path = "./google_home/google_home_macro" }
hostname = "0.4.1" hostname = "0.4.1"
indexmap = { version = "2.11.0", features = ["serde"] } indexmap = { version = "2.11.0", features = ["serde"] }
inventory = "0.3.21"
itertools = "0.14.0" itertools = "0.14.0"
json_value_merge = "2.0.1" json_value_merge = "2.0.1"
mlua = { version = "0.11.3", features = [ mlua = { version = "0.11.3", features = [
@@ -54,7 +55,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"
@@ -77,6 +78,7 @@ dotenvy = { workspace = true }
git-version = "0.3.9" git-version = "0.3.9"
google_home = { workspace = true } google_home = { workspace = true }
hostname = { workspace = true } hostname = { workspace = true }
inventory = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
reqwest = { workspace = true } reqwest = { workspace = true }
rumqttc = { workspace = true } rumqttc = { workspace = true }

View File

@@ -13,6 +13,7 @@ bytes = { workspace = true }
dyn-clone = { workspace = true } dyn-clone = { workspace = true }
eui48 = { workspace = true } eui48 = { workspace = true }
google_home = { workspace = true } google_home = { workspace = true }
inventory = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
reqwest = { workspace = true } reqwest = { workspace = true }
rumqttc = { workspace = true } rumqttc = { workspace = true }

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

@@ -12,6 +12,7 @@ mod wake_on_lan;
mod washer; mod washer;
mod zigbee; mod zigbee;
use automation_lib::Module;
use automation_lib::device::{Device, LuaDeviceCreate}; use automation_lib::device::{Device, LuaDeviceCreate};
use zigbee::light::{LightBrightness, LightColorTemperature, LightOnOff}; use zigbee::light::{LightBrightness, LightColorTemperature, LightOnOff};
use zigbee::outlet::{OutletOnOff, OutletPower}; use zigbee::outlet::{OutletOnOff, OutletPower};
@@ -30,30 +31,33 @@ pub use self::wake_on_lan::WakeOnLAN;
pub use self::washer::Washer; pub use self::washer::Washer;
macro_rules! register_device { macro_rules! register_device {
($lua:expr, $device:ty) => { ($lua:expr, $table:expr, $device:ty) => {
$lua.globals() $table.set(stringify!($device), $lua.create_proxy::<$device>()?)?;
.set(stringify!($device), $lua.create_proxy::<$device>()?)?;
}; };
} }
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> { pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
register_device!(lua, AirFilter); let devices = lua.create_table()?;
register_device!(lua, ContactSensor);
register_device!(lua, HueBridge);
register_device!(lua, HueGroup);
register_device!(lua, HueSwitch);
register_device!(lua, IkeaRemote);
register_device!(lua, KasaOutlet);
register_device!(lua, LightBrightness);
register_device!(lua, LightColorTemperature);
register_device!(lua, LightOnOff);
register_device!(lua, LightSensor);
register_device!(lua, Ntfy);
register_device!(lua, OutletOnOff);
register_device!(lua, OutletPower);
register_device!(lua, Presence);
register_device!(lua, WakeOnLAN);
register_device!(lua, Washer);
Ok(()) register_device!(lua, devices, AirFilter);
register_device!(lua, devices, ContactSensor);
register_device!(lua, devices, HueBridge);
register_device!(lua, devices, HueGroup);
register_device!(lua, devices, HueSwitch);
register_device!(lua, devices, IkeaRemote);
register_device!(lua, devices, KasaOutlet);
register_device!(lua, devices, LightBrightness);
register_device!(lua, devices, LightColorTemperature);
register_device!(lua, devices, LightOnOff);
register_device!(lua, devices, LightSensor);
register_device!(lua, devices, Ntfy);
register_device!(lua, devices, OutletOnOff);
register_device!(lua, devices, OutletPower);
register_device!(lua, devices, Presence);
register_device!(lua, devices, WakeOnLAN);
register_device!(lua, devices, Washer);
Ok(devices)
} }
inventory::submit! {Module::new("devices", register_with_lua)}

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

@@ -11,6 +11,7 @@ dyn-clone = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
google_home = { workspace = true } google_home = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
inventory = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
rumqttc = { workspace = true } rumqttc = { workspace = true }
serde = { workspace = true } serde = { workspace = true }

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

@@ -12,3 +12,26 @@ pub mod lua;
pub mod messages; pub mod messages;
pub mod mqtt; pub mod mqtt;
pub mod schedule; pub mod schedule;
type RegisterFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::Table>;
pub struct Module {
name: &'static str,
register_fn: RegisterFn,
}
impl Module {
pub const fn new(name: &'static str, register_fn: RegisterFn) -> Self {
Self { name, register_fn }
}
pub const fn get_name(&self) -> &'static str {
self.name
}
pub fn register(&self, lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
(self.register_fn)(lua)
}
}
inventory::collect!(Module);

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

@@ -1,3 +1,4 @@
local devices = require("devices")
local device_manager = require("device_manager") local device_manager = require("device_manager")
local utils = require("utils") local utils = require("utils")
local secrets = require("secrets") local secrets = require("secrets")
@@ -29,7 +30,7 @@ local mqtt_client = require("mqtt").new({
tls = host == "zeus" or host == "hephaestus", tls = host == "zeus" or host == "hephaestus",
}) })
local ntfy = Ntfy.new({ local ntfy = devices.Ntfy.new({
topic = secrets.ntfy_topic, topic = secrets.ntfy_topic,
}) })
device_manager:add(ntfy) device_manager:add(ntfy)
@@ -71,7 +72,7 @@ local on_presence = {
end, end,
} }
local presence_system = Presence.new({ local presence_system = devices.Presence.new({
topic = mqtt_automation("presence/+/#"), topic = mqtt_automation("presence/+/#"),
client = mqtt_client, client = mqtt_client,
callback = function(_, presence) callback = function(_, presence)
@@ -122,7 +123,7 @@ local on_light = {
self[#self + 1] = f self[#self + 1] = f
end, end,
} }
device_manager:add(LightSensor.new({ device_manager:add(devices.LightSensor.new({
identifier = "living_light_sensor", identifier = "living_light_sensor",
topic = mqtt_z2m("living/light"), topic = mqtt_z2m("living/light"),
client = mqtt_client, client = mqtt_client,
@@ -146,7 +147,7 @@ end)
local hue_ip = "10.0.0.102" local hue_ip = "10.0.0.102"
local hue_token = secrets.hue_token local hue_token = secrets.hue_token
local hue_bridge = HueBridge.new({ local hue_bridge = devices.HueBridge.new({
identifier = "hue_bridge", identifier = "hue_bridge",
ip = hue_ip, ip = hue_ip,
login = hue_token, login = hue_token,
@@ -163,7 +164,7 @@ on_presence:add(function(presence)
hue_bridge:set_flag("presence", presence) hue_bridge:set_flag("presence", presence)
end) end)
local kitchen_lights = HueGroup.new({ local kitchen_lights = devices.HueGroup.new({
identifier = "kitchen_lights", identifier = "kitchen_lights",
ip = hue_ip, ip = hue_ip,
login = hue_token, login = hue_token,
@@ -171,7 +172,7 @@ local kitchen_lights = HueGroup.new({
scene_id = "7MJLG27RzeRAEVJ", scene_id = "7MJLG27RzeRAEVJ",
}) })
device_manager:add(kitchen_lights) device_manager:add(kitchen_lights)
local living_lights = HueGroup.new({ local living_lights = devices.HueGroup.new({
identifier = "living_lights", identifier = "living_lights",
ip = hue_ip, ip = hue_ip,
login = hue_token, login = hue_token,
@@ -179,7 +180,7 @@ local living_lights = HueGroup.new({
scene_id = "SNZw7jUhQ3cXSjkj", scene_id = "SNZw7jUhQ3cXSjkj",
}) })
device_manager:add(living_lights) device_manager:add(living_lights)
local living_lights_relax = HueGroup.new({ local living_lights_relax = devices.HueGroup.new({
identifier = "living_lights", identifier = "living_lights",
ip = hue_ip, ip = hue_ip,
login = hue_token, login = hue_token,
@@ -188,7 +189,7 @@ local living_lights_relax = HueGroup.new({
}) })
device_manager:add(living_lights_relax) device_manager:add(living_lights_relax)
device_manager:add(HueSwitch.new({ device_manager:add(devices.HueSwitch.new({
name = "Switch", name = "Switch",
room = "Living", room = "Living",
client = mqtt_client, client = mqtt_client,
@@ -205,7 +206,7 @@ device_manager:add(HueSwitch.new({
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:add(WakeOnLAN.new({ device_manager:add(devices.WakeOnLAN.new({
name = "Zeus", name = "Zeus",
room = "Living Room", room = "Living Room",
topic = mqtt_automation("appliance/living_room/zeus"), topic = mqtt_automation("appliance/living_room/zeus"),
@@ -214,7 +215,7 @@ device_manager:add(WakeOnLAN.new({
broadcast_ip = "10.0.3.255", broadcast_ip = "10.0.3.255",
})) }))
local living_mixer = OutletOnOff.new({ local living_mixer = devices.OutletOnOff.new({
name = "Mixer", name = "Mixer",
room = "Living Room", room = "Living Room",
topic = mqtt_z2m("living/mixer"), topic = mqtt_z2m("living/mixer"),
@@ -222,7 +223,7 @@ local living_mixer = OutletOnOff.new({
}) })
turn_off_when_away(living_mixer) turn_off_when_away(living_mixer)
device_manager:add(living_mixer) device_manager:add(living_mixer)
local living_speakers = OutletOnOff.new({ local living_speakers = devices.OutletOnOff.new({
name = "Speakers", name = "Speakers",
room = "Living Room", room = "Living Room",
topic = mqtt_z2m("living/speakers"), topic = mqtt_z2m("living/speakers"),
@@ -231,7 +232,7 @@ local living_speakers = OutletOnOff.new({
turn_off_when_away(living_speakers) turn_off_when_away(living_speakers)
device_manager:add(living_speakers) device_manager:add(living_speakers)
device_manager:add(IkeaRemote.new({ device_manager:add(devices.IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Living Room", room = "Living Room",
client = mqtt_client, client = mqtt_client,
@@ -271,7 +272,7 @@ local function kettle_timeout()
end end
end end
local kettle = OutletPower.new({ local kettle = devices.OutletPower.new({
outlet_type = "Kettle", outlet_type = "Kettle",
name = "Kettle", name = "Kettle",
room = "Kitchen", room = "Kitchen",
@@ -286,7 +287,7 @@ local function set_kettle(_, on)
kettle:set_on(on) kettle:set_on(on)
end end
device_manager:add(IkeaRemote.new({ device_manager:add(devices.IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Bedroom", room = "Bedroom",
client = mqtt_client, client = mqtt_client,
@@ -296,7 +297,7 @@ device_manager:add(IkeaRemote.new({
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:add(IkeaRemote.new({ device_manager:add(devices.IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Kitchen", room = "Kitchen",
client = mqtt_client, client = mqtt_client,
@@ -320,7 +321,7 @@ local function off_timeout(duration)
end end
end end
local bathroom_light = LightOnOff.new({ local bathroom_light = devices.LightOnOff.new({
name = "Light", name = "Light",
room = "Bathroom", room = "Bathroom",
topic = mqtt_z2m("bathroom/light"), topic = mqtt_z2m("bathroom/light"),
@@ -329,7 +330,7 @@ local bathroom_light = LightOnOff.new({
}) })
device_manager:add(bathroom_light) device_manager:add(bathroom_light)
device_manager:add(Washer.new({ device_manager:add(devices.Washer.new({
identifier = "bathroom_washer", identifier = "bathroom_washer",
topic = mqtt_z2m("bathroom/washer"), topic = mqtt_z2m("bathroom/washer"),
client = mqtt_client, client = mqtt_client,
@@ -344,7 +345,7 @@ device_manager:add(Washer.new({
end, end,
})) }))
device_manager:add(OutletOnOff.new({ device_manager:add(devices.OutletOnOff.new({
name = "Charger", name = "Charger",
room = "Workbench", room = "Workbench",
topic = mqtt_z2m("workbench/charger"), topic = mqtt_z2m("workbench/charger"),
@@ -352,7 +353,7 @@ device_manager:add(OutletOnOff.new({
callback = off_timeout(debug and 5 or 20 * 3600), callback = off_timeout(debug and 5 or 20 * 3600),
})) }))
local workbench_outlet = OutletOnOff.new({ local workbench_outlet = devices.OutletOnOff.new({
name = "Outlet", name = "Outlet",
room = "Workbench", room = "Workbench",
topic = mqtt_z2m("workbench/outlet"), topic = mqtt_z2m("workbench/outlet"),
@@ -361,7 +362,7 @@ local workbench_outlet = OutletOnOff.new({
turn_off_when_away(workbench_outlet) turn_off_when_away(workbench_outlet)
device_manager:add(workbench_outlet) device_manager:add(workbench_outlet)
local workbench_light = LightColorTemperature.new({ local workbench_light = devices.LightColorTemperature.new({
name = "Light", name = "Light",
room = "Workbench", room = "Workbench",
topic = mqtt_z2m("workbench/light"), topic = mqtt_z2m("workbench/light"),
@@ -371,7 +372,7 @@ turn_off_when_away(workbench_light)
device_manager:add(workbench_light) device_manager:add(workbench_light)
local delay_color_temp = Timeout.new() local delay_color_temp = Timeout.new()
device_manager:add(IkeaRemote.new({ device_manager:add(devices.IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Workbench", room = "Workbench",
client = mqtt_client, client = mqtt_client,
@@ -394,14 +395,14 @@ device_manager:add(IkeaRemote.new({
battery_callback = check_battery, battery_callback = check_battery,
})) }))
local hallway_top_light = HueGroup.new({ local hallway_top_light = devices.HueGroup.new({
identifier = "hallway_top_light", identifier = "hallway_top_light",
ip = hue_ip, ip = hue_ip,
login = hue_token, login = hue_token,
group_id = 83, group_id = 83,
scene_id = "QeufkFDICEHWeKJ7", scene_id = "QeufkFDICEHWeKJ7",
}) })
device_manager:add(HueSwitch.new({ device_manager:add(devices.HueSwitch.new({
name = "SwitchBottom", name = "SwitchBottom",
room = "Hallway", room = "Hallway",
client = mqtt_client, client = mqtt_client,
@@ -411,7 +412,7 @@ device_manager:add(HueSwitch.new({
end, end,
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:add(HueSwitch.new({ device_manager:add(devices.HueSwitch.new({
name = "SwitchTop", name = "SwitchTop",
room = "Hallway", room = "Hallway",
client = mqtt_client, client = mqtt_client,
@@ -425,12 +426,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 +446,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,36 +461,37 @@ 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,
} }
local hallway_storage = LightBrightness.new({ local hallway_storage = devices.LightBrightness.new({
name = "Storage", name = "Storage",
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)
local hallway_bottom_lights = HueGroup.new({ local hallway_bottom_lights = devices.HueGroup.new({
identifier = "hallway_bottom_lights", identifier = "hallway_bottom_lights",
ip = hue_ip, ip = hue_ip,
login = hue_token, login = hue_token,
@@ -504,13 +511,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,57 +525,49 @@ 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(devices.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 = devices.ContactSensor.new({
name = "Frontdoor", name = "Frontdoor",
room = "Hallway", room = "Hallway",
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)
hallway_light_automation.door = hallway_frontdoor hallway_light_automation.door = hallway_frontdoor
local hallway_trash = ContactSensor.new({ local hallway_trash = devices.ContactSensor.new({
name = "Trash", name = "Trash",
room = "Hallway", room = "Hallway",
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)
hallway_light_automation.trash = hallway_trash hallway_light_automation.trash = hallway_trash
local guest_light = LightOnOff.new({ local guest_light = devices.LightOnOff.new({
name = "Light", name = "Light",
room = "Guest Room", room = "Guest Room",
topic = mqtt_z2m("guest/light"), topic = mqtt_z2m("guest/light"),
@@ -578,14 +576,14 @@ local guest_light = LightOnOff.new({
turn_off_when_away(guest_light) turn_off_when_away(guest_light)
device_manager:add(guest_light) device_manager:add(guest_light)
local bedroom_air_filter = AirFilter.new({ local bedroom_air_filter = devices.AirFilter.new({
name = "Air Filter", name = "Air Filter",
room = "Bedroom", room = "Bedroom",
url = "http://10.0.0.103", url = "http://10.0.0.103",
}) })
device_manager:add(bedroom_air_filter) device_manager:add(bedroom_air_filter)
local bedroom_lights = HueGroup.new({ local bedroom_lights = devices.HueGroup.new({
identifier = "bedroom_lights", identifier = "bedroom_lights",
ip = hue_ip, ip = hue_ip,
login = hue_token, login = hue_token,
@@ -593,7 +591,7 @@ local bedroom_lights = HueGroup.new({
scene_id = "PvRs-lGD4VRytL9", scene_id = "PvRs-lGD4VRytL9",
}) })
device_manager:add(bedroom_lights) device_manager:add(bedroom_lights)
local bedroom_lights_relax = HueGroup.new({ local bedroom_lights_relax = devices.HueGroup.new({
identifier = "bedroom_lights", identifier = "bedroom_lights",
ip = hue_ip, ip = hue_ip,
login = hue_token, login = hue_token,
@@ -602,7 +600,7 @@ local bedroom_lights_relax = HueGroup.new({
}) })
device_manager:add(bedroom_lights_relax) device_manager:add(bedroom_lights_relax)
device_manager:add(HueSwitch.new({ device_manager:add(devices.HueSwitch.new({
name = "Switch", name = "Switch",
room = "Bedroom", room = "Bedroom",
client = mqtt_client, client = mqtt_client,
@@ -616,7 +614,7 @@ device_manager:add(HueSwitch.new({
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:add(ContactSensor.new({ device_manager:add(devices.ContactSensor.new({
name = "Balcony", name = "Balcony",
room = "Living Room", room = "Living Room",
sensor_type = "Door", sensor_type = "Door",
@@ -624,21 +622,21 @@ device_manager:add(ContactSensor.new({
client = mqtt_client, client = mqtt_client,
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:add(ContactSensor.new({ device_manager:add(devices.ContactSensor.new({
name = "Window", name = "Window",
room = "Living Room", room = "Living Room",
topic = mqtt_z2m("living/window"), topic = mqtt_z2m("living/window"),
client = mqtt_client, client = mqtt_client,
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:add(ContactSensor.new({ device_manager:add(devices.ContactSensor.new({
name = "Window", name = "Window",
room = "Bedroom", room = "Bedroom",
topic = mqtt_z2m("bedroom/window"), topic = mqtt_z2m("bedroom/window"),
client = mqtt_client, client = mqtt_client,
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:add(ContactSensor.new({ device_manager:add(devices.ContactSensor.new({
name = "Window", name = "Window",
room = "Guest Room", room = "Guest Room",
topic = mqtt_z2m("guest/window"), topic = mqtt_z2m("guest/window"),
@@ -646,7 +644,7 @@ device_manager:add(ContactSensor.new({
battery_callback = check_battery, battery_callback = check_battery,
})) }))
local storage_light = LightBrightness.new({ local storage_light = devices.LightBrightness.new({
name = "Light", name = "Light",
room = "Storage", room = "Storage",
topic = mqtt_z2m("storage/light"), topic = mqtt_z2m("storage/light"),
@@ -655,7 +653,7 @@ local storage_light = LightBrightness.new({
turn_off_when_away(storage_light) turn_off_when_away(storage_light)
device_manager:add(storage_light) device_manager:add(storage_light)
device_manager:add(ContactSensor.new({ device_manager:add(devices.ContactSensor.new({
name = "Door", name = "Door",
room = "Storage", room = "Storage",
sensor_type = "Door", sensor_type = "Door",

View File

@@ -7,13 +7,13 @@ 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};
use automation_lib::device_manager::DeviceManager; use automation_lib::device_manager::DeviceManager;
use automation_lib::helpers;
use automation_lib::mqtt::{self, WrappedAsyncClient}; use automation_lib::mqtt::{self, WrappedAsyncClient};
use automation_lib::{Module, helpers};
use axum::extract::{FromRef, State}; use axum::extract::{FromRef, State};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::routing::post; use axum::routing::post;
@@ -30,6 +30,9 @@ use web::{ApiError, User};
use crate::secret::EnvironmentSecretFile; use crate::secret::EnvironmentSecretFile;
use crate::version::VERSION; use crate::version::VERSION;
// Force automation_devices to link so that it gets registered as a module
extern crate automation_devices;
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
pub openid_url: String, pub openid_url: String,
@@ -138,6 +141,13 @@ async fn app() -> anyhow::Result<()> {
})?; })?;
lua.globals().set("print", print)?; lua.globals().set("print", print)?;
debug!("Loading modules...");
for module in inventory::iter::<Module> {
debug!(name = module.get_name(), "Registering");
let table = module.register(&lua)?;
lua.register_module(module.get_name(), table)?;
}
let mqtt = lua.create_table()?; let mqtt = lua.create_table()?;
let event_channel = device_manager.event_channel(); let event_channel = device_manager.event_channel();
let mqtt_new = lua.create_function(move |lua, config: mlua::Value| { let mqtt_new = lua.create_function(move |lua, config: mlua::Value| {
@@ -172,9 +182,13 @@ 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)?;
helpers::register_with_lua(&lua)?; helpers::register_with_lua(&lua)?;
let entrypoint = Path::new(&config.entrypoint); let entrypoint = Path::new(&config.entrypoint);