Compare commits

..

22 Commits

Author SHA1 Message Date
ad158f2c22 feat: Reduced visibility of config structs
All checks were successful
Build and deploy / build (push) Successful in 9m0s
Build and deploy / Deploy container (push) Successful in 49s
2025-10-22 04:13:54 +02:00
f36adf2f19 feat: Implement useful traits to simplify code 2025-10-22 04:09:01 +02:00
5947098bfb chore: Fix config type annotations
All checks were successful
Build and deploy / build (push) Successful in 12m30s
Build and deploy / Deploy container (push) Has been skipped
2025-10-22 03:59:59 +02:00
8a3143a3ea feat: Added type alias for setup and schedule types 2025-10-22 03:59:40 +02:00
9546585440 feat(config)!: Made schedule part of new modules
All checks were successful
Build and deploy / build (push) Successful in 11m57s
Build and deploy / Deploy container (push) Has been skipped
2025-10-22 03:24:34 +02:00
a938f3d71b feat(config)!: Improve config module resolution
All checks were successful
Build and deploy / build (push) Successful in 11m31s
Build and deploy / Deploy container (push) Has been skipped
The new system is slightly less flexible, but the code and lua
definitions is now a lot simpler and easier to understand.
In fact the old lua definition was not actually correct.

It is likely that existing configs require not/minimal tweaks to work
again.
2025-10-22 03:09:15 +02:00
a6c19eb9b4 fix: Fix issues with inner type definitions 2025-10-22 02:59:21 +02:00
7db628709a refactor: Split config
All checks were successful
Build and deploy / build (push) Successful in 10m56s
Build and deploy / Deploy container (push) Has been skipped
2025-10-20 05:02:19 +02:00
bc75f7005c 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 05:02:04 +02:00
2056c6c70d feat(config)!: Changed default config location 2025-10-20 04:48:33 +02:00
2fe9fbadfb feat(config)!: Remove device manager lua code
With the recent changes the device manager no longer needs to be
available in lua.
2025-10-20 04:48:33 +02:00
2db4af7427 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-20 04:48:32 +02:00
7b7279017f refactor: Restructured config to not rely on mqtt client being available
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-20 04:48:29 +02:00
f05856cd0c feat(config)!: In config devices can now also be a (table of) function(s)
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-20 04:48:28 +02:00
02b6cf12a1 feat: Improved device conversion error message 2025-10-20 04:48:28 +02:00
02b87126e1 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-20 04:48:28 +02:00
1ffccd955c 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-20 04:48:28 +02:00
948380ea9b feat: Receive devices through config return 2025-10-20 04:48:28 +02:00
0c80cef5a1 feat: Ensure consistent ordering device definitions 2025-10-20 04:48:28 +02:00
84e8942fc9 feat: Generate definitions for config 2025-10-20 04:48:27 +02:00
b557afe2fc refactor: Move definition writing into separate function 2025-10-20 04:48:27 +02:00
5801421378 refactor: Move main.rs to bin/automation.rs 2025-10-20 04:48:22 +02:00
27 changed files with 369 additions and 205 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.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1163,17 +1163,16 @@ 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#f6a684291432aae2ef7109712882e7e3ed758d08" source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#3d29c9dd143737c8bffe4bacae8e701de3c6ee10"
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#f6a684291432aae2ef7109712882e7e3ed758d08" source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#3d29c9dd143737c8bffe4bacae8e701de3c6ee10"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"itertools", "itertools",
@@ -1568,7 +1567,7 @@ dependencies = [
"once_cell", "once_cell",
"socket2", "socket2",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@@ -1745,7 +1744,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]

View File

@@ -1,5 +1,6 @@
local ntfy = require("config.ntfy") local ntfy = require("config.ntfy")
--- @class BatteryModule: Module
local module = {} local module = {}
--- @type {[string]: number} --- @type {[string]: number}
@@ -17,7 +18,7 @@ function module.callback(device, battery)
end end
end end
function module.notify_low_battery() local function 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")
@@ -38,4 +39,9 @@ function module.notify_low_battery()
}) })
end end
--- @type Schedule
module.schedule = {
["0 0 21 */1 * *"] = notify_low_battery,
}
return module return module

View File

@@ -29,14 +29,4 @@ 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,6 +4,7 @@ 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

View File

@@ -1,6 +1,7 @@
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,5 +1,6 @@
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,6 +3,7 @@ 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,6 +1,7 @@
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
@@ -34,6 +35,7 @@ function module.setup(mqtt_client)
callback = callback, callback = callback,
}) })
--- @type Module
return { return {
module.device, module.device,
} }

View File

@@ -1,6 +1,7 @@
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
@@ -24,6 +25,7 @@ function module.setup()
topic = ntfy_topic, topic = ntfy_topic,
}) })
--- @type Module
return { return {
ntfy, ntfy,
} }

View File

@@ -2,6 +2,7 @@ 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
@@ -61,6 +62,7 @@ function module.setup(mqtt_client)
}) })
end) end)
--- @type Module
return { return {
presence, presence,
} }

View File

@@ -1,4 +1,4 @@
--- @type SetupInner --- @type Module
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,6 +30,7 @@ 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,20 +55,24 @@ 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,6 +25,7 @@ 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,6 +97,7 @@ 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,6 +37,7 @@ 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,6 +4,7 @@ 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,6 +109,7 @@ 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,6 +31,7 @@ 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,6 +57,7 @@ function module.setup(mqtt_client)
battery_callback = battery.callback, battery_callback = battery.callback,
}) })
--- @type Module
return { return {
charger, charger,
outlets, outlets,

View File

@@ -1,6 +1,7 @@
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,20 +3,26 @@
---@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 Setup? ---@field modules (Module)[]
---@field mqtt MqttConfig ---@field mqtt MqttConfig
---@field schedule table<string, fun() | fun()[]>?
local Config local Config
---@alias SetupFunction fun(mqtt_client: AsyncClient): SetupInner? ---@alias SetupFunction fun(mqtt_client: AsyncClient): Module | DeviceInterface[] | nil
---@alias SetupInner (DeviceInterface | { setup: SetupFunction } | SetupInner)[]
---@alias Setup SetupFunction | SetupInner ---@alias Schedule table<string, fun() | fun()[]>
---@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
@@ -24,7 +30,7 @@ local Config
---@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,7 +6,6 @@ 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};
@@ -140,13 +139,12 @@ 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());
if let Some(modules) = config.modules { let resolved = config.modules.resolve(&lua, &mqtt_client).await?;
for device in modules.setup(&lua, &mqtt_client).await? { for device in resolved.devices {
device_manager.add(device).await; device_manager.add(device).await;
} }
}
start_scheduler(config.schedule).await?; resolved.scheduler.start().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,10 +1,8 @@
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Write; use std::io::Write;
use automation::config::{Config, FulfillmentConfig, Setups}; use automation::config::generate_definitions;
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;
@@ -27,24 +25,6 @@ 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();
@@ -59,7 +39,7 @@ fn main() -> std::io::Result<()> {
} }
} }
write_definitions("config.lua", &config_definitions())?; write_definitions("config.lua", &generate_definitions())?;
Ok(()) Ok(())
} }

View File

@@ -1,5 +1,6 @@
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;
@@ -9,6 +10,8 @@ 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")]
@@ -34,90 +37,219 @@ pub struct FulfillmentConfig {
pub port: u16, pub port: u16,
} }
#[derive(Debug, Default)] #[derive(Debug)]
pub struct Setups(mlua::Value); struct SetupFunction(mlua::Function);
impl Setups { impl Typed for SetupFunction {
pub async fn setup(
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 (name, value): (String, _) = pair?;
match value {
mlua::Value::Table(table) => queue.push_back(table),
mlua::Value::UserData(_)
if let Ok(device) = Box::from_lua(value.clone(), lua) =>
{
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());
}
}
_ => {}
}
}
}
Ok(devices)
}
}
impl FromLua for Setups {
fn from_lua(value: mlua::Value, _lua: &mlua::Lua) -> mlua::Result<Self> {
Ok(Setups(value))
}
}
impl Typed for Setups {
fn type_name() -> String { fn type_name() -> String {
"Setup".into() "SetupFunction".into()
} }
fn generate_header() -> Option<String> { fn generate_header() -> Option<String> {
let type_name = Self::type_name();
let client_type = WrappedAsyncClient::type_name();
Some(format!( Some(format!(
r#"---@alias {type_name}Function fun(mqtt_client: {client_type}): {type_name}Inner? "---@alias {} fun(mqtt_client: {}): {} | DeviceInterface[] | nil\n",
---@alias {type_name}Inner (DeviceInterface | {{ setup: {type_name}Function }} | {type_name}Inner)[] Self::type_name(),
---@alias {type_name} {type_name}Function | {type_name}Inner 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<()>>);
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(
self,
lua: &mlua::Lua,
client: &WrappedAsyncClient,
) -> mlua::Result<Resolved> {
let mut devices = Vec::new();
let mut scheduler = Scheduler::default();
let mut modules: VecDeque<_> = self.0.into();
loop {
let Some(module) = modules.pop_front() else {
break;
};
modules.extend(module.modules);
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()
{
// 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.extend(module.devices);
for (cron, f) in module.schedule {
scheduler.add_job(cron, f);
}
}
Ok(Resolved { devices, scheduler })
}
}
#[derive(Debug, Default)]
pub struct Resolved {
pub devices: Vec<Box<dyn Device>>,
pub scheduler: Scheduler,
}
#[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: Option<Setups>, pub modules: Modules,
#[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 {
@@ -132,3 +264,25 @@ 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,15 +1,22 @@
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};
pub async fn start_scheduler( #[derive(Debug, Default)]
schedule: HashMap<String, ActionCallback<()>>, pub struct Scheduler {
) -> Result<(), JobSchedulerError> { 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> {
let scheduler = JobScheduler::new().await?; let scheduler = JobScheduler::new().await?;
for (s, f) in schedule { for (s, f) in self.jobs {
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();
@@ -27,3 +34,4 @@ pub async fn start_scheduler(
scheduler.start().await scheduler.start().await
} }
}