Compare commits

..

15 Commits

Author SHA1 Message Date
bb554b2c31 refactor: Split config
Some checks failed
Build and deploy / Deploy container (push) Blocked by required conditions
Build and deploy / build (push) Has been cancelled
2025-10-20 04:47:11 +02:00
956f818a3b feat(config)!: Device creation function is now named entry
It now has to be called 'setup', this makes it possible to just
include the table as a whole in devices and it will automatically call
the correct function.
2025-10-20 04:41:27 +02:00
a0c5189ada feat(config)!: Changed default config location
All checks were successful
Build and deploy / build (push) Successful in 10m26s
Build and deploy / Deploy container (push) Has been skipped
2025-10-19 05:43:43 +02:00
3676aafa23 feat(config)!: Remove device manager lua code
With the recent changes the device manager no longer needs to be
available in lua.
2025-10-19 05:43:43 +02:00
95ec3f28ff feat(config)!: Config now returns the mqtt config instead of the client
Instead the client is now created on the rust side based on the config.
Devices that require the mqtt client will now instead need to be
constructor using a function. This function receives the mqtt client.
2025-10-19 05:43:43 +02:00
303541929c chore: Restructured config to not rely on mqtt client being available
All checks were successful
Build and deploy / build (push) Successful in 10m27s
Build and deploy / Deploy container (push) Has been skipped
In preparation of changes to the mqtt client the config is rewritten to
use a device creation function for devices that need the mqtt client.

This also fixes a but where hallway_top_light was not actually added to
the device manager.
2025-10-19 04:31:25 +02:00
a26a93550b 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.
2025-10-19 04:18:26 +02:00
88a7acd55d feat: Improved device conversion error message
All checks were successful
Build and deploy / build (push) Successful in 10m37s
Build and deploy / Deploy container (push) Has been skipped
2025-10-19 03:40:16 +02:00
3b7579878b feat: Use ActionCallback for schedule
This has two advantages:
- Each schedule entry can take either a single function or table of
  functions.
- We get a better type definition.
2025-10-19 03:40:16 +02:00
46c32c3605 refactor(config)!: Move scheduler out of device_manager
Due to changes made in mlua the new scheduler is much simpler. It also
had no real business being part of the device manager, so it has now been
moved to be part of the returned config.
2025-10-19 03:40:16 +02:00
d1e7988117 feat: Receive devices through config return 2025-10-19 03:40:15 +02:00
93b0a526b1 feat: Ensure consistent ordering device definitions 2025-10-19 03:40:15 +02:00
7bb5e65c1c feat: Generate definitions for config 2025-10-19 03:40:15 +02:00
a0ed373971 refactor: Move definition writing into separate function
Some checks failed
Build and deploy / Deploy container (push) Blocked by required conditions
Build and deploy / build (push) Has been cancelled
2025-10-17 03:12:40 +02:00
5e13dff2b5 chore: Move main.rs to bin/automation.rs 2025-10-17 03:08:37 +02:00
27 changed files with 194 additions and 358 deletions

11
Cargo.lock generated
View File

@@ -555,7 +555,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1163,16 +1163,17 @@ dependencies = [
[[package]]
name = "lua_typed"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#3d29c9dd143737c8bffe4bacae8e701de3c6ee10"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#f6a684291432aae2ef7109712882e7e3ed758d08"
dependencies = [
"eui48",
"lua_typed_macro",
"mlua",
]
[[package]]
name = "lua_typed_macro"
version = "0.1.0"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#3d29c9dd143737c8bffe4bacae8e701de3c6ee10"
source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#f6a684291432aae2ef7109712882e7e3ed758d08"
dependencies = [
"convert_case",
"itertools",
@@ -1567,7 +1568,7 @@ dependencies = [
"once_cell",
"socket2",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -1744,7 +1745,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]

View File

@@ -1,6 +1,5 @@
local ntfy = require("config.ntfy")
--- @class BatteryModule: Module
local module = {}
--- @type {[string]: number}
@@ -18,7 +17,7 @@ function module.callback(device, battery)
end
end
local function notify_low_battery()
function module.notify_low_battery()
-- Don't send notifications if there are now devices with low battery
if next(low_battery) == nil then
print("No devices with low battery")
@@ -39,9 +38,4 @@ local function notify_low_battery()
})
end
--- @type Schedule
module.schedule = {
["0 0 21 */1 * *"] = notify_low_battery,
}
return module

View File

@@ -29,4 +29,14 @@ return {
require("config.rooms"),
require("config.windows"),
},
-- TODO: Make this also part of the modules
schedule = {
["0 0 19 * * *"] = function()
require("config.rooms.bedroom").set_airfilter_on(true)
end,
["0 0 20 * * *"] = function()
require("config.rooms.bedroom").set_airfilter_on(false)
end,
["0 0 21 */1 * *"] = require("config.battery").notify_low_battery,
},
}

View File

@@ -4,7 +4,6 @@ local presence = require("config.presence")
local utils = require("automation:utils")
local variables = require("automation:variables")
--- @class DebugModule: Module
local module = {}
if variables.debug == "true" then

View File

@@ -1,7 +1,6 @@
local debug = require("config.debug")
local utils = require("automation:utils")
--- @class HallwayAutomationModule: Module
local module = {}
local timeout = utils.Timeout.new()

View File

@@ -1,6 +1,5 @@
local utils = require("automation:utils")
--- @class HelperModule: Module
local module = {}
--- @param topic string

View File

@@ -3,7 +3,6 @@ local light = require("config.light")
local presence = require("config.presence")
local secrets = require("automation:secrets")
--- @class HueBridgeModule: Module
local module = {}
module.ip = "10.0.0.102"

View File

@@ -1,7 +1,6 @@
local devices = require("automation:devices")
local helper = require("config.helper")
--- @class LightModule: Module
local module = {}
--- @class OnPresence
@@ -35,7 +34,6 @@ function module.setup(mqtt_client)
callback = callback,
})
--- @type Module
return {
module.device,
}

View File

@@ -1,7 +1,6 @@
local devices = require("automation:devices")
local secrets = require("automation:secrets")
--- @class NtfyModule: Module
local module = {}
local ntfy_topic = secrets.ntfy_topic
@@ -25,7 +24,6 @@ function module.setup()
topic = ntfy_topic,
})
--- @type Module
return {
ntfy,
}

View File

@@ -2,7 +2,6 @@ local devices = require("automation:devices")
local helper = require("config.helper")
local ntfy = require("config.ntfy")
--- @class PresenceModule: Module
local module = {}
--- @class OnPresence
@@ -62,7 +61,6 @@ function module.setup(mqtt_client)
})
end)
--- @type Module
return {
presence,
}

View File

@@ -1,4 +1,4 @@
--- @type Module
--- @type SetupInner
return {
require("config.rooms.bathroom"),
require("config.rooms.bedroom"),

View File

@@ -3,9 +3,9 @@ local devices = require("automation:devices")
local helper = require("config.helper")
local ntfy = require("config.ntfy")
--- @type Module
local module = {}
--- @type SetupFunction
function module.setup(mqtt_client)
local light = devices.LightOnOff.new({
name = "Light",
@@ -30,7 +30,6 @@ function module.setup(mqtt_client)
end,
})
--- @type Module
return {
light,
washer,

View File

@@ -4,12 +4,12 @@ local helper = require("config.helper")
local hue_bridge = require("config.hue_bridge")
local windows = require("config.windows")
--- @type Module
local module = {}
--- @type AirFilter?
local air_filter = nil
--- @type SetupFunction
function module.setup(mqtt_client)
local lights = devices.HueGroup.new({
identifier = "bedroom_lights",
@@ -55,24 +55,20 @@ function module.setup(mqtt_client)
})
windows.add(window)
--- @type Module
return {
devices = {
lights,
lights_relax,
air_filter,
switch,
window,
},
schedule = {
["0 0 19 * * *"] = function()
air_filter:set_on(true)
end,
["0 0 20 * * *"] = function()
air_filter:set_on(false)
end,
},
}
end
--- @param on boolean
function module.set_airfilter_on(on)
if air_filter then
air_filter:set_on(on)
end
end
return module

View File

@@ -4,9 +4,9 @@ local helper = require("config.helper")
local presence = require("config.presence")
local windows = require("config.windows")
--- @type Module
local module = {}
--- @type SetupFunction
function module.setup(mqtt_client)
local light = devices.LightOnOff.new({
name = "Light",
@@ -25,7 +25,6 @@ function module.setup(mqtt_client)
})
windows.add(window)
--- @type Module
return {
light,
window,

View File

@@ -8,9 +8,9 @@ local presence = require("config.presence")
local utils = require("automation:utils")
local windows = require("config.windows")
--- @type Module
local module = {}
--- @type SetupFunction
function module.setup(mqtt_client)
local main_light = devices.HueGroup.new({
identifier = "hallway_main_light",
@@ -97,7 +97,6 @@ function module.setup(mqtt_client)
windows.add(frontdoor)
hallway_automation.set_door(frontdoor)
--- @type Module
return {
main_light,
storage_light,

View File

@@ -3,9 +3,9 @@ local devices = require("automation:devices")
local helper = require("config.helper")
local hue_bridge = require("config.hue_bridge")
--- @type Module
local module = {}
--- @type SetupFunction
function module.setup(mqtt_client)
local light = devices.HueGroup.new({
identifier = "hallway_top_light",
@@ -37,7 +37,6 @@ function module.setup(mqtt_client)
battery_callback = battery.callback,
})
--- @type Module
return {
light,
top_switch,

View File

@@ -4,7 +4,6 @@ local helper = require("config.helper")
local hue_bridge = require("config.hue_bridge")
local presence = require("config.presence")
--- @class KitchenModule: Module
local module = {}
--- @type HueGroup?

View File

@@ -5,9 +5,9 @@ local hue_bridge = require("config.hue_bridge")
local presence = require("config.presence")
local windows = require("config.windows")
--- @type Module
local module = {}
--- @type SetupFunction
function module.setup(mqtt_client)
local lights = devices.HueGroup.new({
identifier = "living_lights",
@@ -109,7 +109,6 @@ function module.setup(mqtt_client)
})
windows.add(window)
--- @type Module
return {
lights,
lights_relax,

View File

@@ -3,9 +3,9 @@ local devices = require("automation:devices")
local helper = require("config.helper")
local presence = require("config.presence")
--- @type Module
local module = {}
--- @type SetupFunction
function module.setup(mqtt_client)
local light = devices.LightBrightness.new({
name = "Light",
@@ -31,7 +31,6 @@ function module.setup(mqtt_client)
battery_callback = battery.callback,
})
--- @type Module
return {
light,
door,

View File

@@ -5,9 +5,9 @@ local helper = require("config.helper")
local presence = require("config.presence")
local utils = require("automation:utils")
--- @type Module
local module = {}
--- @type SetupFunction
function module.setup(mqtt_client)
local charger = devices.OutletOnOff.new({
name = "Charger",
@@ -57,7 +57,6 @@ function module.setup(mqtt_client)
battery_callback = battery.callback,
})
--- @type Module
return {
charger,
outlets,

View File

@@ -1,7 +1,6 @@
local ntfy = require("config.ntfy")
local presence = require("config.presence")
--- @class WindowsModule: Module
local module = {}
--- @class OnPresence

View File

@@ -6,9 +6,9 @@ local devices
---@class Action
---@field action
---| "broadcast"
---@field extras (table<string, string>)?
---@field extras table<string, string>?
---@field label string
---@field clear (boolean)?
---@field clear boolean?
local Action
---@class AirFilter: DeviceInterface, OnOffInterface
@@ -20,49 +20,49 @@ function devices.AirFilter.new(config) end
---@class AirFilterConfig
---@field name string
---@field room (string)?
---@field room string?
---@field url string
local AirFilterConfig
---@class ConfigLightLightStateBrightness
---@field name string
---@field room (string)?
---@field room string?
---@field topic string
---@field callback (fun(_: LightBrightness, _: LightStateBrightness) | fun(_: LightBrightness, _: LightStateBrightness)[])?
---@field client (AsyncClient)?
---@field callback fun(_: LightBrightness, _: LightStateBrightness) | fun(_: LightBrightness, _: LightStateBrightness)[]?
---@field client AsyncClient?
local ConfigLightLightStateBrightness
---@class ConfigLightLightStateColorTemperature
---@field name string
---@field room (string)?
---@field room string?
---@field topic string
---@field callback (fun(_: LightColorTemperature, _: LightStateColorTemperature) | fun(_: LightColorTemperature, _: LightStateColorTemperature)[])?
---@field client (AsyncClient)?
---@field callback fun(_: LightColorTemperature, _: LightStateColorTemperature) | fun(_: LightColorTemperature, _: LightStateColorTemperature)[]?
---@field client AsyncClient?
local ConfigLightLightStateColorTemperature
---@class ConfigLightLightStateOnOff
---@field name string
---@field room (string)?
---@field room string?
---@field topic string
---@field callback (fun(_: LightOnOff, _: LightStateOnOff) | fun(_: LightOnOff, _: LightStateOnOff)[])?
---@field client (AsyncClient)?
---@field callback fun(_: LightOnOff, _: LightStateOnOff) | fun(_: LightOnOff, _: LightStateOnOff)[]?
---@field client AsyncClient?
local ConfigLightLightStateOnOff
---@class ConfigOutletOutletStateOnOff
---@field name string
---@field room (string)?
---@field room string?
---@field topic string
---@field outlet_type (OutletType)?
---@field callback (fun(_: OutletOnOff, _: OutletStateOnOff) | fun(_: OutletOnOff, _: OutletStateOnOff)[])?
---@field outlet_type OutletType?
---@field callback fun(_: OutletOnOff, _: OutletStateOnOff) | fun(_: OutletOnOff, _: OutletStateOnOff)[]?
---@field client AsyncClient
local ConfigOutletOutletStateOnOff
---@class ConfigOutletOutletStatePower
---@field name string
---@field room (string)?
---@field room string?
---@field topic string
---@field outlet_type (OutletType)?
---@field callback (fun(_: OutletPower, _: OutletStatePower) | fun(_: OutletPower, _: OutletStatePower)[])?
---@field outlet_type OutletType?
---@field callback fun(_: OutletPower, _: OutletStatePower) | fun(_: OutletPower, _: OutletStatePower)[]?
---@field client AsyncClient
local ConfigOutletOutletStatePower
@@ -75,12 +75,12 @@ function devices.ContactSensor.new(config) end
---@class ContactSensorConfig
---@field name string
---@field room (string)?
---@field room string?
---@field topic string
---@field sensor_type (SensorType)?
---@field callback (fun(_: ContactSensor, _: boolean) | fun(_: ContactSensor, _: boolean)[])?
---@field battery_callback (fun(_: ContactSensor, _: number) | fun(_: ContactSensor, _: number)[])?
---@field client (AsyncClient)?
---@field sensor_type SensorType?
---@field callback fun(_: ContactSensor, _: boolean) | fun(_: ContactSensor, _: boolean)[]?
---@field battery_callback fun(_: ContactSensor, _: number) | fun(_: ContactSensor, _: number)[]?
---@field client AsyncClient?
local ContactSensorConfig
---@alias Flag
@@ -134,14 +134,14 @@ function devices.HueSwitch.new(config) end
---@class HueSwitchConfig
---@field name string
---@field room (string)?
---@field room string?
---@field topic string
---@field client AsyncClient
---@field left_callback (fun(_: HueSwitch) | fun(_: HueSwitch)[])?
---@field right_callback (fun(_: HueSwitch) | fun(_: HueSwitch)[])?
---@field left_hold_callback (fun(_: HueSwitch) | fun(_: HueSwitch)[])?
---@field right_hold_callback (fun(_: HueSwitch) | fun(_: HueSwitch)[])?
---@field battery_callback (fun(_: HueSwitch, _: number) | fun(_: HueSwitch, _: number)[])?
---@field left_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]?
---@field right_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]?
---@field left_hold_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]?
---@field right_hold_callback fun(_: HueSwitch) | fun(_: HueSwitch)[]?
---@field battery_callback fun(_: HueSwitch, _: number) | fun(_: HueSwitch, _: number)[]?
local HueSwitchConfig
---@class IkeaRemote: DeviceInterface
@@ -153,12 +153,12 @@ function devices.IkeaRemote.new(config) end
---@class IkeaRemoteConfig
---@field name string
---@field room (string)?
---@field single_button (boolean)?
---@field room string?
---@field single_button boolean?
---@field topic string
---@field client AsyncClient
---@field callback (fun(_: IkeaRemote, _: boolean) | fun(_: IkeaRemote, _: boolean)[])?
---@field battery_callback (fun(_: IkeaRemote, _: number) | fun(_: IkeaRemote, _: number)[])?
---@field callback fun(_: IkeaRemote, _: boolean) | fun(_: IkeaRemote, _: boolean)[]?
---@field battery_callback fun(_: IkeaRemote, _: number) | fun(_: IkeaRemote, _: number)[]?
local IkeaRemoteConfig
---@class KasaOutlet: DeviceInterface, OnOffInterface
@@ -206,7 +206,7 @@ function devices.LightSensor.new(config) end
---@field topic string
---@field min integer
---@field max integer
---@field callback (fun(_: LightSensor, _: boolean) | fun(_: LightSensor, _: boolean)[])?
---@field callback fun(_: LightSensor, _: boolean) | fun(_: LightSensor, _: boolean)[]?
---@field client AsyncClient
local LightSensorConfig
@@ -227,10 +227,10 @@ local LightStateOnOff
---@class Notification
---@field title string
---@field message (string)?
---@field tags ((string)[])?
---@field priority (Priority)?
---@field actions ((Action)[])?
---@field message string?
---@field tags string[]?
---@field priority Priority?
---@field actions Action[]?
local Notification
---@class Ntfy: DeviceInterface
@@ -244,7 +244,7 @@ function devices.Ntfy.new(config) end
function Ntfy:send_notification(notification) end
---@class NtfyConfig
---@field url (string)?
---@field url string?
---@field topic string
local NtfyConfig
@@ -287,7 +287,7 @@ function Presence:overall_presence() end
---@class PresenceConfig
---@field topic string
---@field callback (fun(_: Presence, _: boolean) | fun(_: Presence, _: boolean)[])?
---@field callback fun(_: Presence, _: boolean) | fun(_: Presence, _: boolean)[]?
---@field client AsyncClient
local PresenceConfig
@@ -321,16 +321,16 @@ function devices.Washer.new(config) end
---@field identifier string
---@field topic string
---@field threshold number
---@field done_callback (fun(_: Washer) | fun(_: Washer)[])?
---@field done_callback fun(_: Washer) | fun(_: Washer)[]?
---@field client AsyncClient
local WasherConfig
---@class WolConfig
---@field name string
---@field room (string)?
---@field room string?
---@field topic string
---@field mac_address string
---@field broadcast_ip (string)?
---@field broadcast_ip string?
---@field client AsyncClient
local WolConfig

View File

@@ -3,26 +3,20 @@
---@class FulfillmentConfig
---@field openid_url string
---@field ip (string)?
---@field port (integer)?
---@field ip string?
---@field port integer?
local FulfillmentConfig
---@class Config
---@field fulfillment FulfillmentConfig
---@field modules (Module)[]
---@field modules Setup?
---@field mqtt MqttConfig
---@field schedule table<string, fun() | fun()[]>?
local Config
---@alias SetupFunction fun(mqtt_client: AsyncClient): Module | DeviceInterface[] | nil
---@alias Schedule table<string, fun() | fun()[]>
---@class Module
---@field setup (SetupFunction)?
---@field devices (DeviceInterface)[]?
---@field schedule Schedule?
---@field [number] (Module)[]?
local Module
---@alias SetupFunction fun(mqtt_client: AsyncClient): SetupInner?
---@alias SetupInner (DeviceInterface | { setup: SetupFunction } | SetupInner)[]
---@alias Setup SetupFunction | SetupInner
---@class MqttConfig
---@field host string
@@ -30,7 +24,7 @@ local Module
---@field client_name string
---@field username string
---@field password string
---@field tls (boolean)?
---@field tls boolean?
local MqttConfig
---@class AsyncClient

View File

@@ -6,6 +6,7 @@ use std::process;
use ::config::{Environment, File};
use automation::config::{Config, Setup};
use automation::schedule::start_scheduler;
use automation::secret::EnvironmentSecretFile;
use automation::version::VERSION;
use automation::web::{ApiError, User};
@@ -139,12 +140,13 @@ async fn app() -> anyhow::Result<()> {
let mqtt_client = mqtt::start(config.mqtt, &device_manager.event_channel());
let resolved = config.modules.resolve(&lua, &mqtt_client).await?;
for device in resolved.devices {
if let Some(modules) = config.modules {
for device in modules.setup(&lua, &mqtt_client).await? {
device_manager.add(device).await;
}
}
resolved.scheduler.start().await?;
start_scheduler(config.schedule).await?;
// Create google home fulfillment route
let fulfillment = Router::new().route("/google_home", post(fulfillment));

View File

@@ -1,8 +1,10 @@
use std::fs::{self, File};
use std::io::Write;
use automation::config::generate_definitions;
use automation::config::{Config, FulfillmentConfig, Setups};
use automation_lib::Module;
use automation_lib::mqtt::{MqttConfig, WrappedAsyncClient};
use lua_typed::Typed;
use tracing::{info, warn};
extern crate automation_devices;
@@ -25,6 +27,24 @@ fn write_definitions(filename: &str, definitions: &str) -> std::io::Result<()> {
Ok(())
}
fn config_definitions() -> String {
let mut output = "---@meta\n\n".to_string();
output +=
&FulfillmentConfig::generate_full().expect("FulfillmentConfig should have a definition");
output += "\n";
output += &Config::generate_full().expect("Config should have a definition");
output += "\n";
output += &Setups::generate_full().expect("Setups should have a definition");
output += "\n";
output += &MqttConfig::generate_full().expect("MqttConfig should have a definition");
output += "\n";
output +=
&WrappedAsyncClient::generate_full().expect("WrappedAsyncClient should have a definition");
output
}
fn main() -> std::io::Result<()> {
tracing_subscriber::fmt::init();
@@ -39,7 +59,7 @@ fn main() -> std::io::Result<()> {
}
}
write_definitions("config.lua", &generate_definitions())?;
write_definitions("config.lua", &config_definitions())?;
Ok(())
}

View File

@@ -1,6 +1,5 @@
use std::collections::{HashMap, VecDeque};
use std::net::{Ipv4Addr, SocketAddr};
use std::ops::Deref;
use automation_lib::action_callback::ActionCallback;
use automation_lib::device::Device;
@@ -10,8 +9,6 @@ use lua_typed::Typed;
use mlua::FromLua;
use serde::Deserialize;
use crate::schedule::Scheduler;
#[derive(Debug, Deserialize)]
pub struct Setup {
#[serde(default = "default_entrypoint")]
@@ -37,219 +34,90 @@ pub struct FulfillmentConfig {
pub port: u16,
}
#[derive(Debug)]
struct SetupFunction(mlua::Function);
impl Typed for SetupFunction {
fn type_name() -> String {
"SetupFunction".into()
}
fn generate_header() -> Option<String> {
Some(format!(
"---@alias {} fun(mqtt_client: {}): {} | DeviceInterface[] | nil\n",
Self::type_name(),
WrappedAsyncClient::type_name(),
Module::type_name()
))
}
}
impl FromLua for SetupFunction {
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
Ok(Self(FromLua::from_lua(value, lua)?))
}
}
impl Deref for SetupFunction {
type Target = mlua::Function;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Default)]
struct Schedule(HashMap<String, ActionCallback<()>>);
pub struct Setups(mlua::Value);
impl Typed for Schedule {
fn type_name() -> String {
"Schedule".into()
}
fn generate_header() -> Option<String> {
Some(format!(
"---@alias {} {}\n",
Self::type_name(),
HashMap::<String, ActionCallback<()>>::type_name(),
))
}
}
impl FromLua for Schedule {
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
Ok(Self(FromLua::from_lua(value, lua)?))
}
}
impl IntoIterator for Schedule {
type Item = <HashMap<String, ActionCallback<()>> as IntoIterator>::Item;
type IntoIter = <HashMap<String, ActionCallback<()>> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[derive(Debug, Default)]
struct Module {
setup: Option<SetupFunction>,
devices: Vec<Box<dyn Device>>,
schedule: Schedule,
modules: Vec<Module>,
}
// TODO: Add option to typed to rename field
impl Typed for Module {
fn type_name() -> String {
"Module".into()
}
fn generate_header() -> Option<String> {
Some(format!("---@class {}\n", Self::type_name()))
}
fn generate_members() -> Option<String> {
Some(format!(
r#"---@field setup {}
---@field devices {}?
---@field schedule {}?
---@field [number] {}?
"#,
Option::<SetupFunction>::type_name(),
Vec::<Box<dyn Device>>::type_name(),
Schedule::type_name(),
Vec::<Module>::type_name(),
))
}
fn generate_footer() -> Option<String> {
let type_name = <Self as Typed>::type_name();
Some(format!("local {type_name}\n"))
}
}
impl FromLua for Module {
fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
// When calling require it might return a result from the searcher indicating how the
// module was found, we want to ignore these entries.
// TODO: Find a better solution for this
if value.is_string() {
return Ok(Default::default());
}
let mlua::Value::Table(table) = value else {
return Err(mlua::Error::runtime(format!(
"Expected module table, instead found: {}",
value.type_name()
)));
};
let setup = table.get("setup")?;
let devices = table.get("devices").unwrap_or_default();
let schedule = table.get("schedule").unwrap_or_default();
let mut modules = Vec::new();
for module in table.sequence_values::<Module>() {
modules.push(module?);
}
Ok(Module {
setup,
devices,
schedule,
modules,
})
}
}
#[derive(Debug, Default)]
pub struct Modules(Vec<Module>);
impl Typed for Modules {
fn type_name() -> String {
Vec::<Module>::type_name()
}
}
impl FromLua for Modules {
fn from_lua(value: mlua::Value, lua: &mlua::Lua) -> mlua::Result<Self> {
Ok(Self(FromLua::from_lua(value, lua)?))
}
}
impl Modules {
pub async fn resolve(
impl Setups {
pub async fn setup(
self,
lua: &mlua::Lua,
client: &WrappedAsyncClient,
) -> mlua::Result<Resolved> {
) -> mlua::Result<Vec<Box<dyn Device>>> {
let mut devices = Vec::new();
let mut scheduler = Scheduler::default();
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 modules: VecDeque<_> = self.0.into();
let mut queue: VecDeque<mlua::Table> = [initial_table].into();
loop {
let Some(module) = modules.pop_front() else {
let Some(table) = queue.pop_front() else {
break;
};
modules.extend(module.modules);
for pair in table.pairs() {
let (name, value): (String, _) = pair?;
if let Some(setup) = module.setup {
let result: mlua::Value = setup.call_async(client.clone()).await?;
if result.is_nil() {
// We ignore nil results
} else if let Ok(d) = <Vec<_> as FromLua>::from_lua(result.clone(), lua)
&& !d.is_empty()
match value {
mlua::Value::Table(table) => queue.push_back(table),
mlua::Value::UserData(_)
if let Ok(device) = Box::from_lua(value.clone(), lua) =>
{
// This is a shortcut for the common pattern of setup functions that only
// return devices
devices.extend(d);
} else if let Ok(module) = FromLua::from_lua(result.clone(), lua) {
modules.push_back(module);
} else {
return Err(mlua::Error::runtime(
"Setup function returned data in an unexpected format",
));
devices.push(device);
}
mlua::Value::Function(f) if name == "setup" => {
let value: mlua::Value = f.call_async(client.clone()).await?;
if let Some(table) = value.as_table() {
queue.push_back(table.clone());
}
}
_ => {}
}
}
}
devices.extend(module.devices);
for (cron, f) in module.schedule {
scheduler.add_job(cron, f);
Ok(devices)
}
}
Ok(Resolved { devices, scheduler })
impl FromLua for Setups {
fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
Ok(Setups(value))
}
}
#[derive(Debug, Default)]
pub struct Resolved {
pub devices: Vec<Box<dyn Device>>,
pub scheduler: Scheduler,
impl Typed for Setups {
fn type_name() -> String {
"Setup".into()
}
fn generate_header() -> Option<String> {
let type_name = Self::type_name();
let client_type = WrappedAsyncClient::type_name();
Some(format!(
r#"---@alias {type_name}Function fun(mqtt_client: {client_type}): {type_name}Inner?
---@alias {type_name}Inner (DeviceInterface | {{ setup: {type_name}Function }} | {type_name}Inner)[]
---@alias {type_name} {type_name}Function | {type_name}Inner
"#,
))
}
}
#[derive(Debug, LuaDeviceConfig, Typed)]
pub struct Config {
pub fulfillment: FulfillmentConfig,
#[device_config(from_lua, default)]
pub modules: Modules,
pub modules: Option<Setups>,
#[device_config(from_lua)]
pub mqtt: MqttConfig,
#[device_config(from_lua, default)]
#[typed(default)]
pub schedule: HashMap<String, ActionCallback<()>>,
}
impl From<FulfillmentConfig> for SocketAddr {
@@ -264,25 +132,3 @@ fn default_fulfillment_ip() -> Ipv4Addr {
fn default_fulfillment_port() -> u16 {
7878
}
pub fn generate_definitions() -> String {
let mut output = "---@meta\n\n".to_string();
output +=
&FulfillmentConfig::generate_full().expect("FulfillmentConfig should have a definition");
output += "\n";
output += &Config::generate_full().expect("Config should have a definition");
output += "\n";
output += &SetupFunction::generate_full().expect("SetupFunction should have a definition");
output += "\n";
output += &Schedule::generate_full().expect("Schedule should have a definition");
output += "\n";
output += &Module::generate_full().expect("Module should have a definition");
output += "\n";
output += &MqttConfig::generate_full().expect("MqttConfig should have a definition");
output += "\n";
output +=
&WrappedAsyncClient::generate_full().expect("WrappedAsyncClient should have a definition");
output
}

View File

@@ -1,22 +1,15 @@
use std::collections::HashMap;
use std::pin::Pin;
use automation_lib::action_callback::ActionCallback;
use tokio_cron_scheduler::{Job, JobScheduler, JobSchedulerError};
#[derive(Debug, Default)]
pub struct Scheduler {
jobs: Vec<(String, ActionCallback<()>)>,
}
impl Scheduler {
pub fn add_job(&mut self, cron: String, f: ActionCallback<()>) {
self.jobs.push((cron, f));
}
pub async fn start(self) -> Result<(), JobSchedulerError> {
pub async fn start_scheduler(
schedule: HashMap<String, ActionCallback<()>>,
) -> Result<(), JobSchedulerError> {
let scheduler = JobScheduler::new().await?;
for (s, f) in self.jobs {
for (s, f) in schedule {
let job = {
move |_uuid, _lock| -> Pin<Box<dyn Future<Output = ()> + Send>> {
let f = f.clone();
@@ -34,4 +27,3 @@ impl Scheduler {
scheduler.start().await
}
}