Compare commits

..

15 Commits

Author SHA1 Message Date
9bfafbff87 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:46:54 +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 196 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" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -1163,16 +1163,17 @@ dependencies = [
[[package]] [[package]]
name = "lua_typed" name = "lua_typed"
version = "0.1.0" 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 = [ dependencies = [
"eui48", "eui48",
"lua_typed_macro", "lua_typed_macro",
"mlua",
] ]
[[package]] [[package]]
name = "lua_typed_macro" name = "lua_typed_macro"
version = "0.1.0" 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 = [ dependencies = [
"convert_case", "convert_case",
"itertools", "itertools",
@@ -1567,7 +1568,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]
@@ -1744,7 +1745,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.52.0", "windows-sys 0.59.0",
] ]
[[package]] [[package]]

View File

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

View File

@@ -29,4 +29,14 @@ return {
require("config.rooms"), require("config.rooms"),
require("config.windows"), 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 utils = require("automation:utils")
local variables = require("automation:variables") local variables = require("automation:variables")
--- @class DebugModule: Module
local module = {} local module = {}
if variables.debug == "true" then if variables.debug == "true" then
@@ -17,6 +16,8 @@ end
--- @type SetupFunction --- @type SetupFunction
function module.setup(mqtt_client) function module.setup(mqtt_client)
print("Lua " .. _VERSION .. " running on " .. utils.get_hostname())
presence.add_callback(function(p) presence.add_callback(function(p)
mqtt_client:send_message(helper.mqtt_automation("debug") .. "/presence", { mqtt_client:send_message(helper.mqtt_automation("debug") .. "/presence", {
state = p, state = p,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,8 +1,10 @@
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Write; use std::io::Write;
use automation::config::generate_definitions; use automation::config::{Config, FulfillmentConfig, Setups};
use automation_lib::Module; use automation_lib::Module;
use automation_lib::mqtt::{MqttConfig, WrappedAsyncClient};
use lua_typed::Typed;
use tracing::{info, warn}; use tracing::{info, warn};
extern crate automation_devices; extern crate automation_devices;
@@ -25,6 +27,24 @@ fn write_definitions(filename: &str, definitions: &str) -> std::io::Result<()> {
Ok(()) 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<()> { fn main() -> std::io::Result<()> {
tracing_subscriber::fmt::init(); 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(()) Ok(())
} }

View File

@@ -1,6 +1,5 @@
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
use std::net::{Ipv4Addr, SocketAddr}; use std::net::{Ipv4Addr, SocketAddr};
use std::ops::Deref;
use automation_lib::action_callback::ActionCallback; use automation_lib::action_callback::ActionCallback;
use automation_lib::device::Device; use automation_lib::device::Device;
@@ -10,8 +9,6 @@ use lua_typed::Typed;
use mlua::FromLua; use mlua::FromLua;
use serde::Deserialize; use serde::Deserialize;
use crate::schedule::Scheduler;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Setup { pub struct Setup {
#[serde(default = "default_entrypoint")] #[serde(default = "default_entrypoint")]
@@ -37,219 +34,90 @@ pub struct FulfillmentConfig {
pub port: u16, 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)] #[derive(Debug, Default)]
struct Schedule(HashMap<String, ActionCallback<()>>); pub struct Setups(mlua::Value);
impl Typed for Schedule { impl Setups {
fn type_name() -> String { pub async fn setup(
"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(
self, self,
lua: &mlua::Lua, lua: &mlua::Lua,
client: &WrappedAsyncClient, client: &WrappedAsyncClient,
) -> mlua::Result<Resolved> { ) -> mlua::Result<Vec<Box<dyn Device>>> {
let mut devices = Vec::new(); 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 { loop {
let Some(module) = modules.pop_front() else { let Some(table) = queue.pop_front() else {
break; break;
}; };
modules.extend(module.modules); for pair in table.pairs() {
let (name, value): (String, _) = pair?;
if let Some(setup) = module.setup { match value {
let result: mlua::Value = setup.call_async(client.clone()).await?; mlua::Value::Table(table) => queue.push_back(table),
mlua::Value::UserData(_)
if result.is_nil() { if let Ok(device) = Box::from_lua(value.clone(), lua) =>
// We ignore nil results
} else if let Ok(d) = <Vec<_> as FromLua>::from_lua(result.clone(), lua)
&& !d.is_empty()
{ {
// This is a shortcut for the common pattern of setup functions that only devices.push(device);
// return devices }
devices.extend(d); mlua::Value::Function(f) if name == "setup" => {
} else if let Ok(module) = FromLua::from_lua(result.clone(), lua) { let value: mlua::Value = f.call_async(client.clone()).await?;
modules.push_back(module); if let Some(table) = value.as_table() {
} else { queue.push_back(table.clone());
return Err(mlua::Error::runtime( }
"Setup function returned data in an unexpected format", }
)); _ => {}
}
} }
} }
devices.extend(module.devices); Ok(devices)
for (cron, f) in module.schedule {
scheduler.add_job(cron, f);
}
}
Ok(Resolved { devices, scheduler })
} }
} }
#[derive(Debug, Default)] impl FromLua for Setups {
pub struct Resolved { fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
pub devices: Vec<Box<dyn Device>>, Ok(Setups(value))
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)] #[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)]
pub modules: Modules, pub modules: Option<Setups>,
#[device_config(from_lua)] #[device_config(from_lua)]
pub mqtt: MqttConfig, pub mqtt: MqttConfig,
#[device_config(from_lua, default)]
#[typed(default)]
pub schedule: HashMap<String, ActionCallback<()>>,
} }
impl From<FulfillmentConfig> for SocketAddr { impl From<FulfillmentConfig> for SocketAddr {
@@ -264,25 +132,3 @@ fn default_fulfillment_ip() -> Ipv4Addr {
fn default_fulfillment_port() -> u16 { fn default_fulfillment_port() -> u16 {
7878 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 std::pin::Pin;
use automation_lib::action_callback::ActionCallback; use automation_lib::action_callback::ActionCallback;
use tokio_cron_scheduler::{Job, JobScheduler, JobSchedulerError}; use tokio_cron_scheduler::{Job, JobScheduler, JobSchedulerError};
#[derive(Debug, Default)] pub async fn start_scheduler(
pub struct Scheduler { schedule: HashMap<String, ActionCallback<()>>,
jobs: Vec<(String, ActionCallback<()>)>, ) -> Result<(), JobSchedulerError> {
}
impl Scheduler {
pub fn add_job(&mut self, cron: String, f: ActionCallback<()>) {
self.jobs.push((cron, f));
}
pub async fn start(self) -> Result<(), JobSchedulerError> {
let scheduler = JobScheduler::new().await?; let scheduler = JobScheduler::new().await?;
for (s, f) in self.jobs { for (s, f) in schedule {
let job = { let job = {
move |_uuid, _lock| -> Pin<Box<dyn Future<Output = ()> + Send>> { move |_uuid, _lock| -> Pin<Box<dyn Future<Output = ()> + Send>> {
let f = f.clone(); let f = f.clone();
@@ -33,5 +26,4 @@ impl Scheduler {
} }
scheduler.start().await scheduler.start().await
}
} }