Compare commits

..

5 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
11 changed files with 52 additions and 56 deletions

3
Cargo.lock generated
View File

@@ -99,6 +99,8 @@ dependencies = [
"dotenvy", "dotenvy",
"git-version", "git-version",
"google_home", "google_home",
"hostname",
"inventory",
"mlua", "mlua",
"reqwest", "reqwest",
"rumqttc", "rumqttc",
@@ -150,7 +152,6 @@ dependencies = [
"dyn-clone", "dyn-clone",
"futures", "futures",
"google_home", "google_home",
"hostname",
"indexmap", "indexmap",
"inventory", "inventory",
"mlua", "mlua",

View File

@@ -77,6 +77,8 @@ config = { version = "0.15.15", default-features = false, features = [
dotenvy = { workspace = true } dotenvy = { workspace = true }
git-version = "0.3.9" git-version = "0.3.9"
google_home = { workspace = true } google_home = { 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

@@ -36,7 +36,7 @@ macro_rules! register_device {
}; };
} }
pub fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> { pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
let devices = lua.create_table()?; let devices = lua.create_table()?;
register_device!(lua, devices, AirFilter); register_device!(lua, devices, AirFilter);
@@ -60,4 +60,4 @@ pub fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
Ok(devices) Ok(devices)
} }
inventory::submit! {Module::new("devices", create_module)} inventory::submit! {Module::new("devices", register_with_lua)}

View File

@@ -10,7 +10,6 @@ bytes = { workspace = true }
dyn-clone = { workspace = true } dyn-clone = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
google_home = { workspace = true } google_home = { workspace = true }
hostname = { workspace = true }
indexmap = { workspace = true } indexmap = { workspace = true }
inventory = { workspace = true } inventory = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }

View File

@@ -1 +1,11 @@
pub mod serialization; pub mod serialization;
mod timeout;
pub use timeout::Timeout;
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
lua.globals()
.set("Timeout", lua.create_proxy::<Timeout>()?)?;
Ok(())
}

View File

@@ -1,8 +1,6 @@
#![allow(incomplete_features)] #![allow(incomplete_features)]
#![feature(iterator_try_collect)] #![feature(iterator_try_collect)]
use tracing::debug;
pub mod action_callback; pub mod action_callback;
pub mod config; pub mod config;
pub mod device; pub mod device;
@@ -36,15 +34,4 @@ impl Module {
} }
} }
pub fn load_modules(lua: &mlua::Lua) -> mlua::Result<()> {
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)?;
}
Ok(())
}
inventory::collect!(Module); inventory::collect!(Module);

View File

@@ -1,3 +1 @@
pub mod traits; pub mod traits;
mod utils;

View File

@@ -1,31 +0,0 @@
mod timeout;
use std::time::{SystemTime, UNIX_EPOCH};
pub use timeout::Timeout;
use crate::Module;
fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
let utils = lua.create_table()?;
utils.set("Timeout", lua.create_proxy::<Timeout>()?)?;
let get_hostname = lua.create_function(|_lua, ()| {
hostname::get()
.map(|name| name.to_str().unwrap_or("unknown").to_owned())
.map_err(mlua::ExternalError::into_lua_err)
})?;
utils.set("get_hostname", get_hostname)?;
let get_epoch = lua.create_function(|_lua, ()| {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time is after UNIX EPOCH")
.as_millis())
})?;
utils.set("get_epoch", get_epoch)?;
Ok(utils)
}
inventory::submit! {Module::new("utils", create_module)}

View File

@@ -259,7 +259,7 @@ device_manager:add(devices.IkeaRemote.new({
})) }))
local function kettle_timeout() local function kettle_timeout()
local timeout = utils.Timeout.new() local timeout = Timeout.new()
return function(self, state) return function(self, state)
if state.state and state.power < 100 then if state.state and state.power < 100 then
@@ -308,7 +308,7 @@ device_manager:add(devices.IkeaRemote.new({
})) }))
local function off_timeout(duration) local function off_timeout(duration)
local timeout = utils.Timeout.new() local timeout = Timeout.new()
return function(self, state) return function(self, state)
if state.state then if state.state then
@@ -371,7 +371,7 @@ local workbench_light = devices.LightColorTemperature.new({
turn_off_when_away(workbench_light) turn_off_when_away(workbench_light)
device_manager:add(workbench_light) device_manager:add(workbench_light)
local delay_color_temp = utils.Timeout.new() local delay_color_temp = Timeout.new()
device_manager:add(devices.IkeaRemote.new({ device_manager:add(devices.IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Workbench", room = "Workbench",
@@ -424,7 +424,7 @@ device_manager:add(devices.HueSwitch.new({
})) }))
local hallway_light_automation = { local hallway_light_automation = {
timeout = utils.Timeout.new(), timeout = Timeout.new(),
forced = false, forced = false,
switch_callback = function(self) switch_callback = function(self)
return function(_, on) return function(_, on)
@@ -512,7 +512,7 @@ hallway_light_automation.group = {
} }
local function presence(duration) local function presence(duration)
local timeout = utils.Timeout.new() local timeout = Timeout.new()
return function(_, open) return function(_, open)
if open then if open then

View File

@@ -7,11 +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::{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::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;
@@ -139,7 +141,12 @@ async fn app() -> anyhow::Result<()> {
})?; })?;
lua.globals().set("print", print)?; lua.globals().set("print", print)?;
automation_lib::load_modules(&lua)?; 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();
@@ -161,6 +168,29 @@ async fn app() -> anyhow::Result<()> {
lua.register_module("variables", lua.to_value(&config.variables)?)?; lua.register_module("variables", lua.to_value(&config.variables)?)?;
lua.register_module("secrets", lua.to_value(&config.secrets)?)?; lua.register_module("secrets", lua.to_value(&config.secrets)?)?;
let utils = lua.create_table()?;
let get_hostname = lua.create_function(|_lua, ()| {
hostname::get()
.map(|name| name.to_str().unwrap_or("unknown").to_owned())
.map_err(mlua::ExternalError::into_lua_err)
})?;
utils.set("get_hostname", get_hostname)?;
let get_epoch = lua.create_function(|_lua, ()| {
Ok(SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time is after UNIX EPOCH")
.as_millis())
})?;
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)?;
helpers::register_with_lua(&lua)?;
let entrypoint = Path::new(&config.entrypoint); let entrypoint = Path::new(&config.entrypoint);
let fulfillment_config: mlua::Value = lua.load(entrypoint).eval_async().await?; let fulfillment_config: mlua::Value = lua.load(entrypoint).eval_async().await?;
let fulfillment_config: FulfillmentConfig = lua.from_value(fulfillment_config)?; let fulfillment_config: FulfillmentConfig = lua.from_value(fulfillment_config)?;