feat(config)!: In config devices can now also be a (table of) function(s)
Some checks failed
Build and deploy / Deploy container (push) Blocked by required conditions
Build and deploy / build (push) Has been cancelled

This function receives the mqtt client as an argument. In the future
this will be the only way to create devices that require the mqtt client.
This commit is contained in:
2025-10-19 04:18:26 +02:00
parent 88a7acd55d
commit a26a93550b
5 changed files with 86 additions and 7 deletions

View File

@@ -742,11 +742,15 @@ devs:add(devices.ContactSensor.new({
battery_callback = check_battery, battery_callback = check_battery,
})) }))
-- HACK: If the devices config contains a function it will call it so we have to remove it
devs.add = nil
---@type Config ---@type Config
return { return {
fulfillment = { fulfillment = {
openid_url = "https://login.huizinga.dev/api/oidc", openid_url = "https://login.huizinga.dev/api/oidc",
}, },
mqtt = mqtt_client,
devices = devs, devices = devs,
schedule = { schedule = {
["0 0 19 * * *"] = function() ["0 0 19 * * *"] = function()

View File

@@ -9,6 +9,9 @@ local FulfillmentConfig
---@class Config ---@class Config
---@field fulfillment FulfillmentConfig ---@field fulfillment FulfillmentConfig
---@field devices DeviceInterface[]? ---@field devices Devices?
---@field mqtt AsyncClient
---@field schedule table<string, fun() | fun()[]>? ---@field schedule table<string, fun() | fun()[]>?
local Config local Config
---@alias Devices (DeviceInterface | fun(client: AsyncClient): Devices)[]

View File

@@ -139,8 +139,10 @@ async fn app() -> anyhow::Result<()> {
let entrypoint = Path::new(&setup.entrypoint); let entrypoint = Path::new(&setup.entrypoint);
let config: Config = lua.load(entrypoint).eval_async().await?; let config: Config = lua.load(entrypoint).eval_async().await?;
for device in config.devices { if let Some(devices) = config.devices {
device_manager.add(device).await; for device in devices.get(&lua, &config.mqtt).await? {
device_manager.add(device).await;
}
} }
start_scheduler(config.schedule).await?; start_scheduler(config.schedule).await?;

View File

@@ -1,7 +1,7 @@
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Write; use std::io::Write;
use automation::config::{Config, FulfillmentConfig}; use automation::config::{Config, Devices, FulfillmentConfig};
use automation_lib::Module; use automation_lib::Module;
use lua_typed::Typed; use lua_typed::Typed;
use tracing::{info, warn}; use tracing::{info, warn};
@@ -33,6 +33,8 @@ fn config_definitions() -> String {
&FulfillmentConfig::generate_full().expect("FulfillmentConfig should have a definition"); &FulfillmentConfig::generate_full().expect("FulfillmentConfig should have a definition");
output += "\n"; output += "\n";
output += &Config::generate_full().expect("Config should have a definition"); output += &Config::generate_full().expect("Config should have a definition");
output += "\n";
output += &Devices::generate_full().expect("Devices should have a definition");
output output
} }

View File

@@ -1,10 +1,12 @@
use std::collections::HashMap; use std::collections::{HashMap, VecDeque};
use std::net::{Ipv4Addr, SocketAddr}; use std::net::{Ipv4Addr, SocketAddr};
use automation_lib::action_callback::ActionCallback; use automation_lib::action_callback::ActionCallback;
use automation_lib::device::Device; use automation_lib::device::Device;
use automation_lib::mqtt::WrappedAsyncClient;
use automation_macro::LuaDeviceConfig; use automation_macro::LuaDeviceConfig;
use lua_typed::Typed; use lua_typed::Typed;
use mlua::FromLua;
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -32,12 +34,78 @@ pub struct FulfillmentConfig {
pub port: u16, pub port: u16,
} }
#[derive(Debug, Default)]
pub struct Devices(mlua::Value);
impl Devices {
pub async fn get(
self,
lua: &mlua::Lua,
client: &WrappedAsyncClient,
) -> mlua::Result<Vec<Box<dyn Device>>> {
let mut devices = Vec::new();
let initial_table = match self.0 {
mlua::Value::Table(table) => table,
mlua::Value::Function(f) => f.call_async(client.clone()).await?,
_ => Err(mlua::Error::runtime(format!(
"Expected table or function, instead found: {}",
self.0.type_name()
)))?,
};
let mut queue: VecDeque<mlua::Table> = [initial_table].into();
loop {
let Some(table) = queue.pop_front() else {
break;
};
for pair in table.pairs() {
let (_, value): (mlua::Value, _) = pair?;
match value {
mlua::Value::UserData(_) => devices.push(Box::from_lua(value, lua)?),
mlua::Value::Function(f) => {
queue.push_back(f.call_async(client.clone()).await?);
}
_ => Err(mlua::Error::runtime(format!(
"Expected a device, table, or function, instead found: {}",
value.type_name()
)))?,
}
}
}
Ok(devices)
}
}
impl FromLua for Devices {
fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
Ok(Devices(value))
}
}
impl Typed for Devices {
fn type_name() -> String {
"Devices".into()
}
fn generate_header() -> Option<String> {
Some(format!(
"---@alias {} (DeviceInterface | fun(client: {}): Devices)[]\n",
<Self as Typed>::type_name(),
<WrappedAsyncClient as Typed>::type_name()
))
}
}
#[derive(Debug, LuaDeviceConfig, Typed)] #[derive(Debug, LuaDeviceConfig, Typed)]
pub struct Config { pub struct Config {
pub fulfillment: FulfillmentConfig, pub fulfillment: FulfillmentConfig,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[typed(default)] pub devices: Option<Devices>,
pub devices: Vec<Box<dyn Device>>, #[device_config(from_lua)]
pub mqtt: WrappedAsyncClient,
#[device_config(from_lua, default)] #[device_config(from_lua, default)]
#[typed(default)] #[typed(default)]
pub schedule: HashMap<String, ActionCallback<()>>, pub schedule: HashMap<String, ActionCallback<()>>,