Compare commits

...

6 Commits

Author SHA1 Message Date
92a0bff8c4 refactor(config)!: Move scheduler out of device_manager
All checks were successful
Build and deploy / build (push) Successful in 10m5s
Build and deploy / Deploy container (push) Has been skipped
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-17 04:31:27 +02:00
187220a49b feat: Receive devices through config return 2025-10-17 04:00:34 +02:00
4e2da2ecca feat: Ensure consistent ordering device definitions 2025-10-17 03:57:33 +02:00
65c7ed6349 feat: Generate definitions for config
All checks were successful
Build and deploy / build (push) Successful in 9m15s
Build and deploy / Deploy container (push) Has been skipped
2025-10-17 03:15:27 +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
17 changed files with 649 additions and 472 deletions

235
Cargo.lock generated
View File

@@ -36,12 +36,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]] [[package]]
name = "android_system_properties" name = "android_system_properties"
version = "0.1.5" version = "0.1.5"
@@ -94,11 +88,13 @@ dependencies = [
"async-trait", "async-trait",
"automation_devices", "automation_devices",
"automation_lib", "automation_lib",
"automation_macro",
"axum", "axum",
"config", "config",
"git-version", "git-version",
"google_home", "google_home",
"inventory", "inventory",
"lua_typed",
"mlua", "mlua",
"reqwest", "reqwest",
"rumqttc", "rumqttc",
@@ -106,6 +102,7 @@ dependencies = [
"serde_json", "serde_json",
"thiserror 2.0.16", "thiserror 2.0.16",
"tokio", "tokio",
"tokio-cron-scheduler",
"tracing", "tracing",
"tracing-subscriber", "tracing-subscriber",
] ]
@@ -152,7 +149,6 @@ dependencies = [
"futures", "futures",
"google_home", "google_home",
"hostname", "hostname",
"indexmap",
"inventory", "inventory",
"lua_typed", "lua_typed",
"mlua", "mlua",
@@ -161,9 +157,7 @@ dependencies = [
"serde_json", "serde_json",
"thiserror 2.0.16", "thiserror 2.0.16",
"tokio", "tokio",
"tokio-cron-scheduler",
"tracing", "tracing",
"uuid",
] ]
[[package]] [[package]]
@@ -322,16 +316,25 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.41" version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [ dependencies = [
"android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys", "js-sys",
"num-traits", "num-traits",
"wasm-bindgen", "wasm-bindgen",
"windows-link", "windows-link 0.2.1",
]
[[package]]
name = "chrono-tz"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
dependencies = [
"chrono",
"phf",
] ]
[[package]] [[package]]
@@ -374,11 +377,48 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "croner" name = "croner"
version = "2.2.0" version = "3.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c344b0690c1ad1c7176fe18eb173e0c927008fdaaa256e40dfd43ddd149c0843" checksum = "4c007081651a19b42931f86f7d4f74ee1c2a7d0cd2c6636a81695b5ffd4e9990"
dependencies = [ dependencies = [
"chrono", "chrono",
"derive_builder",
"strum",
]
[[package]]
name = "darling"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn 2.0.106",
]
[[package]]
name = "darling_macro"
version = "0.20.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [
"darling_core",
"quote",
"syn 2.0.106",
] ]
[[package]] [[package]]
@@ -422,6 +462,37 @@ dependencies = [
"thiserror 2.0.16", "thiserror 2.0.16",
] ]
[[package]]
name = "derive_builder"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "derive_builder_macro"
version = "0.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
dependencies = [
"derive_builder_core",
"syn 2.0.106",
]
[[package]] [[package]]
name = "displaydoc" name = "displaydoc"
version = "0.2.5" version = "0.2.5"
@@ -466,12 +537,6 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]] [[package]]
name = "erased-serde" name = "erased-serde"
version = "0.4.6" version = "0.4.6"
@@ -701,10 +766,10 @@ dependencies = [
] ]
[[package]] [[package]]
name = "hashbrown" name = "heck"
version = "0.15.5" version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hex" name = "hex"
@@ -720,7 +785,7 @@ checksum = "a56f203cd1c76362b69e3863fd987520ac36cf70a8c92627449b2f64a8cf7d65"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"libc", "libc",
"windows-link", "windows-link 0.1.3",
] ]
[[package]] [[package]]
@@ -834,9 +899,9 @@ dependencies = [
[[package]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.63" version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [ dependencies = [
"android_system_properties", "android_system_properties",
"core-foundation-sys", "core-foundation-sys",
@@ -942,6 +1007,12 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "1.1.0" version = "1.1.0"
@@ -963,17 +1034,6 @@ dependencies = [
"icu_properties", "icu_properties",
] ]
[[package]]
name = "indexmap"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9"
dependencies = [
"equivalent",
"hashbrown",
"serde",
]
[[package]] [[package]]
name = "inventory" name = "inventory"
version = "0.3.21" version = "0.3.21"
@@ -1102,16 +1162,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#08f5c4533a93131e8eda6702c062fb841d14d4e1" 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#08f5c4533a93131e8eda6702c062fb841d14d4e1" source = "git+https://git.huizinga.dev/Dreaded_X/lua_typed#f6a684291432aae2ef7109712882e7e3ed758d08"
dependencies = [ dependencies = [
"convert_case", "convert_case",
"itertools", "itertools",
@@ -1207,9 +1268,9 @@ dependencies = [
[[package]] [[package]]
name = "mlua" name = "mlua"
version = "0.11.3" version = "0.11.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b3dd94c3c4dea0049b22296397040840a8f6b5b5229f438434ba82df402b42d" checksum = "9be1c2bfc684b8a228fbaebf954af7a47a98ec27721986654a4cc2c40a20cc7e"
dependencies = [ dependencies = [
"bstr", "bstr",
"either", "either",
@@ -1347,6 +1408,24 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "phf"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
dependencies = [
"phf_shared",
]
[[package]]
name = "phf_shared"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
dependencies = [
"siphasher",
]
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@@ -1899,6 +1978,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "siphasher"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.11" version = "0.4.11"
@@ -1936,6 +2021,33 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@@ -2078,11 +2190,12 @@ dependencies = [
[[package]] [[package]]
name = "tokio-cron-scheduler" name = "tokio-cron-scheduler"
version = "0.14.0" version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c71ce8f810abc9fabebccc30302a952f9e89c6cf246fafaf170fef164063141" checksum = "bb73c4033ddcbbf81fd828293fd41a0145cde2cbc30dd782227c5081a523214d"
dependencies = [ dependencies = [
"chrono", "chrono",
"chrono-tz",
"croner", "croner",
"num-derive", "num-derive",
"num-traits", "num-traits",
@@ -2477,22 +2590,22 @@ dependencies = [
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [ dependencies = [
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
"windows-link", "windows-link 0.2.1",
"windows-result", "windows-result",
"windows-strings", "windows-strings",
] ]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.60.0" version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2501,9 +2614,9 @@ dependencies = [
[[package]] [[package]]
name = "windows-interface" name = "windows-interface"
version = "0.59.1" version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -2517,21 +2630,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
[[package]] [[package]]
name = "windows-result" name = "windows-link"
version = "0.3.4" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.2.1",
] ]
[[package]] [[package]]
name = "windows-strings" name = "windows-strings"
version = "0.4.2" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [ dependencies = [
"windows-link", "windows-link 0.2.1",
] ]
[[package]] [[package]]

View File

@@ -33,7 +33,6 @@ futures = "0.3.31"
google_home = { path = "./google_home/google_home" } google_home = { path = "./google_home/google_home" }
google_home_macro = { path = "./google_home/google_home_macro" } google_home_macro = { path = "./google_home/google_home_macro" }
hostname = "0.4.1" hostname = "0.4.1"
indexmap = { version = "2.11.0", features = ["serde"] }
inventory = "0.3.21" inventory = "0.3.21"
itertools = "0.14.0" itertools = "0.14.0"
json_value_merge = "2.0.1" json_value_merge = "2.0.1"
@@ -59,10 +58,9 @@ serde_repr = "0.1.20"
syn = { version = "2.0.106" } syn = { version = "2.0.106" }
thiserror = "2.0.16" thiserror = "2.0.16"
tokio = { version = "1", features = ["rt-multi-thread"] } tokio = { version = "1", features = ["rt-multi-thread"] }
tokio-cron-scheduler = "0.14.0" tokio-cron-scheduler = "0.15.0"
tracing = "0.1.41" tracing = "0.1.41"
tracing-subscriber = "0.3.20" tracing-subscriber = "0.3.20"
uuid = "1.18.1"
wakey = "0.3.0" wakey = "0.3.0"
[dependencies] [dependencies]
@@ -70,6 +68,7 @@ anyhow = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
automation_devices = { workspace = true } automation_devices = { workspace = true }
automation_lib = { workspace = true } automation_lib = { workspace = true }
automation_macro = { path = "./automation_macro" }
axum = { workspace = true } axum = { workspace = true }
config = { version = "0.15.15", default-features = false, features = [ config = { version = "0.15.15", default-features = false, features = [
"async", "async",
@@ -77,6 +76,7 @@ config = { version = "0.15.15", default-features = false, features = [
] } ] }
git-version = "0.3.9" git-version = "0.3.9"
google_home = { workspace = true } google_home = { workspace = true }
lua_typed = { workspace = true }
inventory = { workspace = true } inventory = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
reqwest = { workspace = true } reqwest = { workspace = true }
@@ -85,6 +85,7 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-cron-scheduler = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }

View File

@@ -70,13 +70,35 @@ pub fn create_module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
Ok(devices) Ok(devices)
} }
type RegisterTypeFn = fn() -> Option<String>; type TypeNameFn = fn() -> String;
type TypeDefinitionFn = fn() -> Option<String>;
pub struct RegisteredType(RegisterTypeFn); pub struct RegisteredType {
name_fn: TypeNameFn,
definition_fn: TypeDefinitionFn,
}
impl RegisteredType {
pub const fn new(name_fn: TypeNameFn, definition_fn: TypeDefinitionFn) -> Self {
Self {
name_fn,
definition_fn,
}
}
pub fn get_name(&self) -> String {
(self.name_fn)()
}
pub fn register(&self) -> Option<String> {
(self.definition_fn)()
}
}
macro_rules! register_type { macro_rules! register_type {
($ty:ty) => { ($ty:ty) => {
::inventory::submit!(crate::RegisteredType( ::inventory::submit!(crate::RegisteredType::new(
<$ty as ::lua_typed::Typed>::type_name,
<$ty as ::lua_typed::Typed>::generate_full <$ty as ::lua_typed::Typed>::generate_full
)); ));
}; };
@@ -88,9 +110,12 @@ inventory::collect!(RegisteredType);
fn generate_definitions() -> String { fn generate_definitions() -> String {
let mut output = String::new(); let mut output = String::new();
let mut types: Vec<_> = inventory::iter::<RegisteredType>.into_iter().collect();
types.sort_by_key(|ty| ty.get_name());
output += "---@meta\n\nlocal devices\n\n"; output += "---@meta\n\nlocal devices\n\n";
for ty in inventory::iter::<RegisteredType> { for ty in types {
if let Some(def) = ty.0() { if let Some(def) = (ty.definition_fn)() {
output += &(def + "\n"); output += &(def + "\n");
} else { } else {
// NOTE: Due to how this works the typed is erased, so we don't know the cause // NOTE: Due to how this works the typed is erased, so we don't know the cause

View File

@@ -11,7 +11,6 @@ dyn-clone = { workspace = true }
futures = { workspace = true } futures = { workspace = true }
google_home = { workspace = true } google_home = { workspace = true }
hostname = { workspace = true } hostname = { workspace = true }
indexmap = { workspace = true }
inventory = { workspace = true } inventory = { workspace = true }
lua_typed = { workspace = true } lua_typed = { workspace = true }
mlua = { workspace = true } mlua = { workspace = true }
@@ -20,6 +19,4 @@ serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
tokio-cron-scheduler = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
uuid = { workspace = true }

View File

@@ -2,6 +2,7 @@ use std::fmt::Debug;
use automation_cast::Cast; use automation_cast::Cast;
use dyn_clone::DynClone; use dyn_clone::DynClone;
use lua_typed::Typed;
use mlua::ObjectLike; use mlua::ObjectLike;
use crate::event::OnMqtt; use crate::event::OnMqtt;
@@ -41,4 +42,10 @@ impl mlua::FromLua for Box<dyn Device> {
} }
impl mlua::UserData for Box<dyn Device> {} impl mlua::UserData for Box<dyn Device> {}
impl Typed for Box<dyn Device> {
fn type_name() -> String {
"DeviceInterface".into()
}
}
dyn_clone::clone_trait_object!(Device); dyn_clone::clone_trait_object!(Device);

View File

@@ -1,13 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use futures::Future;
use futures::future::join_all; use futures::future::join_all;
use lua_typed::Typed; use lua_typed::Typed;
use mlua::FromLua; use mlua::FromLua;
use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::sync::{RwLock, RwLockReadGuard};
use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{debug, instrument, trace}; use tracing::{debug, instrument, trace};
use crate::device::Device; use crate::device::Device;
@@ -19,7 +16,6 @@ pub type DeviceMap = HashMap<String, Box<dyn Device>>;
pub struct DeviceManager { pub struct DeviceManager {
devices: Arc<RwLock<DeviceMap>>, devices: Arc<RwLock<DeviceMap>>,
event_channel: EventChannel, event_channel: EventChannel,
scheduler: JobScheduler,
} }
impl DeviceManager { impl DeviceManager {
@@ -29,7 +25,6 @@ impl DeviceManager {
let device_manager = Self { let device_manager = Self {
devices: Arc::new(RwLock::new(HashMap::new())), devices: Arc::new(RwLock::new(HashMap::new())),
event_channel, event_channel,
scheduler: JobScheduler::new().await.unwrap(),
}; };
tokio::spawn({ tokio::spawn({
@@ -45,8 +40,6 @@ impl DeviceManager {
} }
}); });
device_manager.scheduler.start().await.unwrap();
device_manager device_manager
} }
@@ -105,42 +98,6 @@ impl mlua::UserData for DeviceManager {
Ok(()) Ok(())
}); });
methods.add_async_method(
"schedule",
async |lua, this, (schedule, f): (String, mlua::Function)| {
debug!("schedule = {schedule}");
// This creates a function, that returns the actual job we want to run
let create_job = {
let lua = lua.clone();
move |uuid: uuid::Uuid,
_: tokio_cron_scheduler::JobScheduler|
-> Pin<Box<dyn Future<Output = ()> + Send>> {
let lua = lua.clone();
// Create the actual function we want to run on a schedule
let future = async move {
let f: mlua::Function =
lua.named_registry_value(uuid.to_string().as_str()).unwrap();
f.call_async::<()>(()).await.unwrap();
};
Box::pin(future)
}
};
let job = Job::new_async(schedule.as_str(), create_job).unwrap();
let uuid = this.scheduler.add(job).await.unwrap();
// Store the function in the registry
lua.set_named_registry_value(uuid.to_string().as_str(), f)
.unwrap();
Ok(())
},
);
methods.add_method("event_channel", |_lua, this, ()| Ok(this.event_channel())) methods.add_method("event_channel", |_lua, this, ()| Ok(this.event_channel()))
} }
} }

View File

@@ -13,7 +13,6 @@ pub mod helpers;
pub mod lua; pub mod lua;
pub mod messages; pub mod messages;
pub mod mqtt; pub mod mqtt;
pub mod schedule;
type RegisterFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::Table>; type RegisterFn = fn(lua: &mlua::Lua) -> mlua::Result<mlua::Table>;
type DefinitionsFn = fn() -> String; type DefinitionsFn = fn() -> String;

View File

@@ -1,17 +0,0 @@
use indexmap::IndexMap;
use serde::Deserialize;
#[derive(Debug, Deserialize, Hash, PartialEq, Eq, Clone, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Action {
On,
Off,
}
pub type Schedule = IndexMap<String, IndexMap<Action, Vec<String>>>;
// #[derive(Debug, Deserialize)]
// pub struct Schedule {
// pub when: String,
// pub actions: IndexMap<Action, Vec<String>>,
// }

View File

@@ -30,6 +30,11 @@ local mqtt_client = require("automation:mqtt").new(device_manager, {
tls = host == "zeus" or host == "hephaestus", tls = host == "zeus" or host == "hephaestus",
}) })
local devs = {}
function devs:add(device)
table.insert(self, device)
end
local ntfy_topic = secrets.ntfy_topic local ntfy_topic = secrets.ntfy_topic
if ntfy_topic == nil then if ntfy_topic == nil then
error("Ntfy topic is not specified") error("Ntfy topic is not specified")
@@ -37,7 +42,7 @@ end
local ntfy = devices.Ntfy.new({ local ntfy = devices.Ntfy.new({
topic = ntfy_topic, topic = ntfy_topic,
}) })
device_manager:add(ntfy) devs:add(ntfy)
--- @type {[string]: number} --- @type {[string]: number}
local low_battery = {} local low_battery = {}
@@ -52,7 +57,7 @@ local function check_battery(device, battery)
low_battery[id] = nil low_battery[id] = nil
end end
end end
device_manager:schedule("0 0 21 */1 * *", function() 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")
@@ -71,7 +76,7 @@ device_manager:schedule("0 0 21 */1 * *", function()
tags = { "battery" }, tags = { "battery" },
priority = "default", priority = "default",
}) })
end) end
--- @class OnPresence --- @class OnPresence
--- @field [integer] fun(presence: boolean) --- @field [integer] fun(presence: boolean)
@@ -92,7 +97,7 @@ local presence_system = devices.Presence.new({
end end
end, end,
}) })
device_manager:add(presence_system) devs:add(presence_system)
on_presence:add(function(presence) on_presence:add(function(presence)
ntfy:send_notification({ ntfy:send_notification({
title = "Presence", title = "Presence",
@@ -166,7 +171,7 @@ local on_light = {}
function on_light:add(f) function on_light:add(f)
self[#self + 1] = f self[#self + 1] = f
end end
device_manager:add(devices.LightSensor.new({ devs:add(devices.LightSensor.new({
identifier = "living_light_sensor", identifier = "living_light_sensor",
topic = mqtt_z2m("living/light"), topic = mqtt_z2m("living/light"),
client = mqtt_client, client = mqtt_client,
@@ -202,7 +207,7 @@ local hue_bridge = devices.HueBridge.new({
darkness = 43, darkness = 43,
}, },
}) })
device_manager:add(hue_bridge) devs:add(hue_bridge)
on_light:add(function(light) on_light:add(function(light)
hue_bridge:set_flag("darkness", not light) hue_bridge:set_flag("darkness", not light)
end) end)
@@ -217,7 +222,7 @@ local kitchen_lights = devices.HueGroup.new({
group_id = 7, group_id = 7,
scene_id = "7MJLG27RzeRAEVJ", scene_id = "7MJLG27RzeRAEVJ",
}) })
device_manager:add(kitchen_lights) devs:add(kitchen_lights)
local living_lights = devices.HueGroup.new({ local living_lights = devices.HueGroup.new({
identifier = "living_lights", identifier = "living_lights",
ip = hue_ip, ip = hue_ip,
@@ -225,7 +230,7 @@ local living_lights = devices.HueGroup.new({
group_id = 1, group_id = 1,
scene_id = "SNZw7jUhQ3cXSjkj", scene_id = "SNZw7jUhQ3cXSjkj",
}) })
device_manager:add(living_lights) devs:add(living_lights)
local living_lights_relax = devices.HueGroup.new({ local living_lights_relax = devices.HueGroup.new({
identifier = "living_lights", identifier = "living_lights",
ip = hue_ip, ip = hue_ip,
@@ -233,9 +238,9 @@ local living_lights_relax = devices.HueGroup.new({
group_id = 1, group_id = 1,
scene_id = "eRJ3fvGHCcb6yNw", scene_id = "eRJ3fvGHCcb6yNw",
}) })
device_manager:add(living_lights_relax) devs:add(living_lights_relax)
device_manager:add(devices.HueSwitch.new({ devs:add(devices.HueSwitch.new({
name = "Switch", name = "Switch",
room = "Living", room = "Living",
client = mqtt_client, client = mqtt_client,
@@ -252,7 +257,7 @@ device_manager:add(devices.HueSwitch.new({
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:add(devices.WakeOnLAN.new({ devs:add(devices.WakeOnLAN.new({
name = "Zeus", name = "Zeus",
room = "Living Room", room = "Living Room",
topic = mqtt_automation("appliance/living_room/zeus"), topic = mqtt_automation("appliance/living_room/zeus"),
@@ -268,7 +273,7 @@ local living_mixer = devices.OutletOnOff.new({
client = mqtt_client, client = mqtt_client,
}) })
turn_off_when_away(living_mixer) turn_off_when_away(living_mixer)
device_manager:add(living_mixer) devs:add(living_mixer)
local living_speakers = devices.OutletOnOff.new({ local living_speakers = devices.OutletOnOff.new({
name = "Speakers", name = "Speakers",
room = "Living Room", room = "Living Room",
@@ -276,9 +281,9 @@ local living_speakers = devices.OutletOnOff.new({
client = mqtt_client, client = mqtt_client,
}) })
turn_off_when_away(living_speakers) turn_off_when_away(living_speakers)
device_manager:add(living_speakers) devs:add(living_speakers)
device_manager:add(devices.IkeaRemote.new({ devs:add(devices.IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Living Room", room = "Living Room",
client = mqtt_client, client = mqtt_client,
@@ -329,14 +334,14 @@ local kettle = devices.OutletPower.new({
callback = kettle_timeout(), callback = kettle_timeout(),
}) })
turn_off_when_away(kettle) turn_off_when_away(kettle)
device_manager:add(kettle) devs:add(kettle)
--- @param on boolean --- @param on boolean
local function set_kettle(_, on) local function set_kettle(_, on)
kettle:set_on(on) kettle:set_on(on)
end end
device_manager:add(devices.IkeaRemote.new({ devs:add(devices.IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Bedroom", room = "Bedroom",
client = mqtt_client, client = mqtt_client,
@@ -346,7 +351,7 @@ device_manager:add(devices.IkeaRemote.new({
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:add(devices.IkeaRemote.new({ devs:add(devices.IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Kitchen", room = "Kitchen",
client = mqtt_client, client = mqtt_client,
@@ -379,9 +384,9 @@ local bathroom_light = devices.LightOnOff.new({
client = mqtt_client, client = mqtt_client,
callback = off_timeout(debug and 60 or 45 * 60), callback = off_timeout(debug and 60 or 45 * 60),
}) })
device_manager:add(bathroom_light) devs:add(bathroom_light)
device_manager:add(devices.Washer.new({ devs:add(devices.Washer.new({
identifier = "bathroom_washer", identifier = "bathroom_washer",
topic = mqtt_z2m("bathroom/washer"), topic = mqtt_z2m("bathroom/washer"),
client = mqtt_client, client = mqtt_client,
@@ -396,7 +401,7 @@ device_manager:add(devices.Washer.new({
end, end,
})) }))
device_manager:add(devices.OutletOnOff.new({ devs:add(devices.OutletOnOff.new({
name = "Charger", name = "Charger",
room = "Workbench", room = "Workbench",
topic = mqtt_z2m("workbench/charger"), topic = mqtt_z2m("workbench/charger"),
@@ -411,7 +416,7 @@ local workbench_outlet = devices.OutletOnOff.new({
client = mqtt_client, client = mqtt_client,
}) })
turn_off_when_away(workbench_outlet) turn_off_when_away(workbench_outlet)
device_manager:add(workbench_outlet) devs:add(workbench_outlet)
local workbench_light = devices.LightColorTemperature.new({ local workbench_light = devices.LightColorTemperature.new({
name = "Light", name = "Light",
@@ -420,10 +425,10 @@ local workbench_light = devices.LightColorTemperature.new({
client = mqtt_client, client = mqtt_client,
}) })
turn_off_when_away(workbench_light) turn_off_when_away(workbench_light)
device_manager:add(workbench_light) devs:add(workbench_light)
local delay_color_temp = utils.Timeout.new() local delay_color_temp = utils.Timeout.new()
device_manager:add(devices.IkeaRemote.new({ devs:add(devices.IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Workbench", room = "Workbench",
client = mqtt_client, client = mqtt_client,
@@ -453,7 +458,7 @@ local hallway_top_light = devices.HueGroup.new({
group_id = 83, group_id = 83,
scene_id = "QeufkFDICEHWeKJ7", scene_id = "QeufkFDICEHWeKJ7",
}) })
device_manager:add(devices.HueSwitch.new({ devs:add(devices.HueSwitch.new({
name = "SwitchBottom", name = "SwitchBottom",
room = "Hallway", room = "Hallway",
client = mqtt_client, client = mqtt_client,
@@ -463,7 +468,7 @@ device_manager:add(devices.HueSwitch.new({
end, end,
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:add(devices.HueSwitch.new({ devs:add(devices.HueSwitch.new({
name = "SwitchTop", name = "SwitchTop",
room = "Hallway", room = "Hallway",
client = mqtt_client, client = mqtt_client,
@@ -546,7 +551,7 @@ local hallway_storage = devices.LightBrightness.new({
callback = hallway_light_automation:light_callback(), callback = hallway_light_automation:light_callback(),
}) })
turn_off_when_away(hallway_storage) turn_off_when_away(hallway_storage)
device_manager:add(hallway_storage) devs:add(hallway_storage)
local hallway_bottom_lights = devices.HueGroup.new({ local hallway_bottom_lights = devices.HueGroup.new({
identifier = "hallway_bottom_lights", identifier = "hallway_bottom_lights",
@@ -555,7 +560,7 @@ local hallway_bottom_lights = devices.HueGroup.new({
group_id = 81, group_id = 81,
scene_id = "3qWKxGVadXFFG4o", scene_id = "3qWKxGVadXFFG4o",
}) })
device_manager:add(hallway_bottom_lights) devs:add(hallway_bottom_lights)
hallway_light_automation.group = { hallway_light_automation.group = {
set_on = function(on) set_on = function(on)
@@ -591,7 +596,7 @@ local function presence(duration)
end end
end end
device_manager:add(devices.IkeaRemote.new({ devs:add(devices.IkeaRemote.new({
name = "Remote", name = "Remote",
room = "Hallway", room = "Hallway",
client = mqtt_client, client = mqtt_client,
@@ -611,7 +616,7 @@ local hallway_frontdoor = devices.ContactSensor.new({
}, },
battery_callback = check_battery, battery_callback = check_battery,
}) })
device_manager:add(hallway_frontdoor) devs:add(hallway_frontdoor)
window_sensors:add(hallway_frontdoor) window_sensors:add(hallway_frontdoor)
hallway_light_automation.door = hallway_frontdoor hallway_light_automation.door = hallway_frontdoor
@@ -624,7 +629,7 @@ local hallway_trash = devices.ContactSensor.new({
callback = hallway_light_automation:trash_callback(), callback = hallway_light_automation:trash_callback(),
battery_callback = check_battery, battery_callback = check_battery,
}) })
device_manager:add(hallway_trash) devs:add(hallway_trash)
hallway_light_automation.trash = hallway_trash hallway_light_automation.trash = hallway_trash
local guest_light = devices.LightOnOff.new({ local guest_light = devices.LightOnOff.new({
@@ -634,14 +639,14 @@ local guest_light = devices.LightOnOff.new({
client = mqtt_client, client = mqtt_client,
}) })
turn_off_when_away(guest_light) turn_off_when_away(guest_light)
device_manager:add(guest_light) devs:add(guest_light)
local bedroom_air_filter = devices.AirFilter.new({ local bedroom_air_filter = devices.AirFilter.new({
name = "Air Filter", name = "Air Filter",
room = "Bedroom", room = "Bedroom",
url = "http://10.0.0.103", url = "http://10.0.0.103",
}) })
device_manager:add(bedroom_air_filter) devs:add(bedroom_air_filter)
local bedroom_lights = devices.HueGroup.new({ local bedroom_lights = devices.HueGroup.new({
identifier = "bedroom_lights", identifier = "bedroom_lights",
@@ -650,7 +655,7 @@ local bedroom_lights = devices.HueGroup.new({
group_id = 3, group_id = 3,
scene_id = "PvRs-lGD4VRytL9", scene_id = "PvRs-lGD4VRytL9",
}) })
device_manager:add(bedroom_lights) devs:add(bedroom_lights)
local bedroom_lights_relax = devices.HueGroup.new({ local bedroom_lights_relax = devices.HueGroup.new({
identifier = "bedroom_lights", identifier = "bedroom_lights",
ip = hue_ip, ip = hue_ip,
@@ -658,9 +663,9 @@ local bedroom_lights_relax = devices.HueGroup.new({
group_id = 3, group_id = 3,
scene_id = "60tfTyR168v2csz", scene_id = "60tfTyR168v2csz",
}) })
device_manager:add(bedroom_lights_relax) devs:add(bedroom_lights_relax)
device_manager:add(devices.HueSwitch.new({ devs:add(devices.HueSwitch.new({
name = "Switch", name = "Switch",
room = "Bedroom", room = "Bedroom",
client = mqtt_client, client = mqtt_client,
@@ -682,7 +687,7 @@ local balcony = devices.ContactSensor.new({
client = mqtt_client, client = mqtt_client,
battery_callback = check_battery, battery_callback = check_battery,
}) })
device_manager:add(balcony) devs:add(balcony)
window_sensors:add(balcony) window_sensors:add(balcony)
local living_window = devices.ContactSensor.new({ local living_window = devices.ContactSensor.new({
name = "Window", name = "Window",
@@ -691,7 +696,7 @@ local living_window = devices.ContactSensor.new({
client = mqtt_client, client = mqtt_client,
battery_callback = check_battery, battery_callback = check_battery,
}) })
device_manager:add(living_window) devs:add(living_window)
window_sensors:add(living_window) window_sensors:add(living_window)
local bedroom_window = devices.ContactSensor.new({ local bedroom_window = devices.ContactSensor.new({
name = "Window", name = "Window",
@@ -700,7 +705,7 @@ local bedroom_window = devices.ContactSensor.new({
client = mqtt_client, client = mqtt_client,
battery_callback = check_battery, battery_callback = check_battery,
}) })
device_manager:add(bedroom_window) devs:add(bedroom_window)
window_sensors:add(bedroom_window) window_sensors:add(bedroom_window)
local guest_window = devices.ContactSensor.new({ local guest_window = devices.ContactSensor.new({
name = "Window", name = "Window",
@@ -709,7 +714,7 @@ local guest_window = devices.ContactSensor.new({
client = mqtt_client, client = mqtt_client,
battery_callback = check_battery, battery_callback = check_battery,
}) })
device_manager:add(guest_window) devs:add(guest_window)
window_sensors:add(guest_window) window_sensors:add(guest_window)
local storage_light = devices.LightBrightness.new({ local storage_light = devices.LightBrightness.new({
@@ -719,9 +724,9 @@ local storage_light = devices.LightBrightness.new({
client = mqtt_client, client = mqtt_client,
}) })
turn_off_when_away(storage_light) turn_off_when_away(storage_light)
device_manager:add(storage_light) devs:add(storage_light)
device_manager:add(devices.ContactSensor.new({ devs:add(devices.ContactSensor.new({
name = "Door", name = "Door",
room = "Storage", room = "Storage",
sensor_type = "Door", sensor_type = "Door",
@@ -737,15 +742,19 @@ device_manager:add(devices.ContactSensor.new({
battery_callback = check_battery, battery_callback = check_battery,
})) }))
device_manager:schedule("0 0 19 * * *", function() ---@type Config
bedroom_air_filter:set_on(true)
end)
device_manager:schedule("0 0 20 * * *", function()
bedroom_air_filter:set_on(false)
end)
return { return {
fulfillment = { fulfillment = {
openid_url = "https://login.huizinga.dev/api/oidc", openid_url = "https://login.huizinga.dev/api/oidc",
}, },
devices = devs,
schedule = {
["0 0 19 * * *"] = function()
bedroom_air_filter:set_on(true)
end,
["0 0 20 * * *"] = function()
bedroom_air_filter:set_on(false)
end,
["0 0 21 */1 * *"] = notify_low_battery,
},
} }

View File

@@ -5,8 +5,4 @@ local DeviceManager
---@param device DeviceInterface ---@param device DeviceInterface
function DeviceManager:add(device) end function DeviceManager:add(device) end
---@param cron string
---@param callback fun()
function DeviceManager:schedule(cron, callback) end
return DeviceManager return DeviceManager

View File

@@ -3,17 +3,13 @@
local devices local devices
---@class KasaOutletConfig ---@class Action
---@field identifier string ---@field action
---@field ip string ---| "broadcast"
local KasaOutletConfig ---@field extras table<string, string>?
---@field label string
---@class KasaOutlet: DeviceInterface, OnOffInterface ---@field clear boolean?
local KasaOutlet local Action
devices.KasaOutlet = {}
---@param config KasaOutletConfig
---@return KasaOutlet
function devices.KasaOutlet.new(config) end
---@class AirFilter: DeviceInterface, OnOffInterface ---@class AirFilter: DeviceInterface, OnOffInterface
local AirFilter local AirFilter
@@ -28,21 +24,73 @@ function devices.AirFilter.new(config) end
---@field url string ---@field url string
local AirFilterConfig local AirFilterConfig
---@class Presence: DeviceInterface ---@class ConfigLightLightStateBrightness
local Presence ---@field name string
devices.Presence = {} ---@field room string?
---@param config PresenceConfig
---@return Presence
function devices.Presence.new(config) end
---@async
---@return boolean
function Presence:overall_presence() end
---@class PresenceConfig
---@field topic string ---@field topic string
---@field callback fun(_: Presence, _: boolean) | fun(_: Presence, _: boolean)[]? ---@field callback fun(_: LightBrightness, _: LightStateBrightness) | fun(_: LightBrightness, _: LightStateBrightness)[]?
---@field client AsyncClient?
local ConfigLightLightStateBrightness
---@class ConfigLightLightStateColorTemperature
---@field name string
---@field room string?
---@field topic string
---@field callback fun(_: LightColorTemperature, _: LightStateColorTemperature) | fun(_: LightColorTemperature, _: LightStateColorTemperature)[]?
---@field client AsyncClient?
local ConfigLightLightStateColorTemperature
---@class ConfigLightLightStateOnOff
---@field name string
---@field room string?
---@field topic string
---@field callback fun(_: LightOnOff, _: LightStateOnOff) | fun(_: LightOnOff, _: LightStateOnOff)[]?
---@field client AsyncClient?
local ConfigLightLightStateOnOff
---@class ConfigOutletOutletStateOnOff
---@field name string
---@field room string?
---@field topic string
---@field outlet_type OutletType?
---@field callback fun(_: OutletOnOff, _: OutletStateOnOff) | fun(_: OutletOnOff, _: OutletStateOnOff)[]?
---@field client AsyncClient ---@field client AsyncClient
local PresenceConfig local ConfigOutletOutletStateOnOff
---@class ConfigOutletOutletStatePower
---@field name string
---@field room string?
---@field topic string
---@field outlet_type OutletType?
---@field callback fun(_: OutletPower, _: OutletStatePower) | fun(_: OutletPower, _: OutletStatePower)[]?
---@field client AsyncClient
local ConfigOutletOutletStatePower
---@class ContactSensor: DeviceInterface, OpenCloseInterface
local ContactSensor
devices.ContactSensor = {}
---@param config ContactSensorConfig
---@return ContactSensor
function devices.ContactSensor.new(config) end
---@class ContactSensorConfig
---@field name 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?
local ContactSensorConfig
---@alias Flag
---| "presence"
---| "darkness"
---@class FlagIDs
---@field presence integer
---@field darkness integer
local FlagIDs
---@class HueBridge: DeviceInterface ---@class HueBridge: DeviceInterface
local HueBridge local HueBridge
@@ -55,11 +103,6 @@ function devices.HueBridge.new(config) end
---@param value boolean ---@param value boolean
function HueBridge:set_flag(flag, value) end function HueBridge:set_flag(flag, value) end
---@class FlagIDs
---@field presence integer
---@field darkness integer
local FlagIDs
---@class HueBridgeConfig ---@class HueBridgeConfig
---@field identifier string ---@field identifier string
---@field ip string ---@field ip string
@@ -67,49 +110,6 @@ local FlagIDs
---@field flags FlagIDs ---@field flags FlagIDs
local HueBridgeConfig local HueBridgeConfig
---@alias Flag
---| "presence"
---| "darkness"
---@class WasherConfig
---@field identifier string
---@field topic string
---@field threshold number
---@field done_callback fun(_: Washer) | fun(_: Washer)[]?
---@field client AsyncClient
local WasherConfig
---@class Washer: DeviceInterface
local Washer
devices.Washer = {}
---@param config WasherConfig
---@return Washer
function devices.Washer.new(config) end
---@class LightSensor: DeviceInterface
local LightSensor
devices.LightSensor = {}
---@param config LightSensorConfig
---@return LightSensor
function devices.LightSensor.new(config) end
---@class LightSensorConfig
---@field identifier string
---@field topic string
---@field min integer
---@field max integer
---@field callback fun(_: LightSensor, _: boolean) | fun(_: LightSensor, _: boolean)[]?
---@field client AsyncClient
local LightSensorConfig
---@class HueGroupConfig
---@field identifier string
---@field ip string
---@field login string
---@field group_id integer
---@field scene_id string
local HueGroupConfig
---@class HueGroup: DeviceInterface, OnOffInterface ---@class HueGroup: DeviceInterface, OnOffInterface
local HueGroup local HueGroup
devices.HueGroup = {} devices.HueGroup = {}
@@ -117,203 +117,13 @@ devices.HueGroup = {}
---@return HueGroup ---@return HueGroup
function devices.HueGroup.new(config) end function devices.HueGroup.new(config) end
---@class IkeaRemote: DeviceInterface ---@class HueGroupConfig
local IkeaRemote ---@field identifier string
devices.IkeaRemote = {} ---@field ip string
---@param config IkeaRemoteConfig ---@field login string
---@return IkeaRemote ---@field group_id integer
function devices.IkeaRemote.new(config) end ---@field scene_id string
local HueGroupConfig
---@class IkeaRemoteConfig
---@field name string
---@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)[]?
local IkeaRemoteConfig
---@class WolConfig
---@field name string
---@field room string?
---@field topic string
---@field mac_address string
---@field broadcast_ip string?
---@field client AsyncClient
local WolConfig
---@class WakeOnLAN: DeviceInterface
local WakeOnLAN
devices.WakeOnLAN = {}
---@param config WolConfig
---@return WakeOnLAN
function devices.WakeOnLAN.new(config) end
---@class ConfigOutletOutletStateOnOff
---@field name string
---@field room string?
---@field topic string
---@field outlet_type OutletType?
---@field callback fun(_: OutletOnOff, _: OutletStateOnOff) | fun(_: OutletOnOff, _: OutletStateOnOff)[]?
---@field client AsyncClient
local ConfigOutletOutletStateOnOff
---@class OutletStatePower
---@field state boolean
---@field power number
local OutletStatePower
---@class ConfigOutletOutletStatePower
---@field name string
---@field room string?
---@field topic string
---@field outlet_type OutletType?
---@field callback fun(_: OutletPower, _: OutletStatePower) | fun(_: OutletPower, _: OutletStatePower)[]?
---@field client AsyncClient
local ConfigOutletOutletStatePower
---@alias OutletType
---| "Outlet"
---| "Kettle"
---@class OutletStateOnOff
---@field state boolean
local OutletStateOnOff
---@class OutletOnOff: DeviceInterface, OnOffInterface
local OutletOnOff
devices.OutletOnOff = {}
---@param config ConfigOutletOutletStateOnOff
---@return OutletOnOff
function devices.OutletOnOff.new(config) end
---@class OutletPower: DeviceInterface, OnOffInterface
local OutletPower
devices.OutletPower = {}
---@param config ConfigOutletOutletStatePower
---@return OutletPower
function devices.OutletPower.new(config) end
---@class ContactSensorConfig
---@field name 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?
local ContactSensorConfig
---@alias SensorType
---| "Door"
---| "Drawer"
---| "Window"
---@class ContactSensor: DeviceInterface, OpenCloseInterface
local ContactSensor
devices.ContactSensor = {}
---@param config ContactSensorConfig
---@return ContactSensor
function devices.ContactSensor.new(config) end
---@class LightStateOnOff
---@field state boolean
local LightStateOnOff
---@class ConfigLightLightStateColorTemperature
---@field name string
---@field room string?
---@field topic string
---@field callback fun(_: LightColorTemperature, _: LightStateColorTemperature) | fun(_: LightColorTemperature, _: LightStateColorTemperature)[]?
---@field client AsyncClient?
local ConfigLightLightStateColorTemperature
---@class LightStateBrightness
---@field state boolean
---@field brightness number
local LightStateBrightness
---@class LightStateColorTemperature
---@field state boolean
---@field brightness number
---@field color_temp integer
local LightStateColorTemperature
---@class LightBrightness: DeviceInterface, OnOffInterface, BrightnessInterface
local LightBrightness
devices.LightBrightness = {}
---@param config ConfigLightLightStateBrightness
---@return LightBrightness
function devices.LightBrightness.new(config) end
---@class LightColorTemperature: DeviceInterface, OnOffInterface, BrightnessInterface, ColorSettingInterface
local LightColorTemperature
devices.LightColorTemperature = {}
---@param config ConfigLightLightStateColorTemperature
---@return LightColorTemperature
function devices.LightColorTemperature.new(config) end
---@class ConfigLightLightStateOnOff
---@field name string
---@field room string?
---@field topic string
---@field callback fun(_: LightOnOff, _: LightStateOnOff) | fun(_: LightOnOff, _: LightStateOnOff)[]?
---@field client AsyncClient?
local ConfigLightLightStateOnOff
---@class ConfigLightLightStateBrightness
---@field name string
---@field room string?
---@field topic string
---@field callback fun(_: LightBrightness, _: LightStateBrightness) | fun(_: LightBrightness, _: LightStateBrightness)[]?
---@field client AsyncClient?
local ConfigLightLightStateBrightness
---@class LightOnOff: DeviceInterface, OnOffInterface
local LightOnOff
devices.LightOnOff = {}
---@param config ConfigLightLightStateOnOff
---@return LightOnOff
function devices.LightOnOff.new(config) end
---@class Action
---@field action
---| "broadcast"
---@field extras table<string, string>?
---@field label string
---@field clear boolean?
local Action
---@class NtfyConfig
---@field url string?
---@field topic string
local NtfyConfig
---@class Notification
---@field title string
---@field message string?
---@field tags string[]?
---@field priority Priority?
---@field actions Action[]?
local Notification
---@alias Priority
---| "min"
---| "low"
---| "default"
---| "high"
---| "max"
---@class Ntfy: DeviceInterface
local Ntfy
devices.Ntfy = {}
---@param config NtfyConfig
---@return Ntfy
function devices.Ntfy.new(config) end
---@async
---@param notification Notification
function Ntfy:send_notification(notification) end
---@class HueSwitch: DeviceInterface ---@class HueSwitch: DeviceInterface
local HueSwitch local HueSwitch
@@ -334,4 +144,194 @@ function devices.HueSwitch.new(config) end
---@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
local IkeaRemote
devices.IkeaRemote = {}
---@param config IkeaRemoteConfig
---@return IkeaRemote
function devices.IkeaRemote.new(config) end
---@class IkeaRemoteConfig
---@field name string
---@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)[]?
local IkeaRemoteConfig
---@class KasaOutlet: DeviceInterface, OnOffInterface
local KasaOutlet
devices.KasaOutlet = {}
---@param config KasaOutletConfig
---@return KasaOutlet
function devices.KasaOutlet.new(config) end
---@class KasaOutletConfig
---@field identifier string
---@field ip string
local KasaOutletConfig
---@class LightBrightness: DeviceInterface, OnOffInterface, BrightnessInterface
local LightBrightness
devices.LightBrightness = {}
---@param config ConfigLightLightStateBrightness
---@return LightBrightness
function devices.LightBrightness.new(config) end
---@class LightColorTemperature: DeviceInterface, OnOffInterface, BrightnessInterface, ColorSettingInterface
local LightColorTemperature
devices.LightColorTemperature = {}
---@param config ConfigLightLightStateColorTemperature
---@return LightColorTemperature
function devices.LightColorTemperature.new(config) end
---@class LightOnOff: DeviceInterface, OnOffInterface
local LightOnOff
devices.LightOnOff = {}
---@param config ConfigLightLightStateOnOff
---@return LightOnOff
function devices.LightOnOff.new(config) end
---@class LightSensor: DeviceInterface
local LightSensor
devices.LightSensor = {}
---@param config LightSensorConfig
---@return LightSensor
function devices.LightSensor.new(config) end
---@class LightSensorConfig
---@field identifier string
---@field topic string
---@field min integer
---@field max integer
---@field callback fun(_: LightSensor, _: boolean) | fun(_: LightSensor, _: boolean)[]?
---@field client AsyncClient
local LightSensorConfig
---@class LightStateBrightness
---@field state boolean
---@field brightness number
local LightStateBrightness
---@class LightStateColorTemperature
---@field state boolean
---@field brightness number
---@field color_temp integer
local LightStateColorTemperature
---@class LightStateOnOff
---@field state boolean
local LightStateOnOff
---@class Notification
---@field title string
---@field message string?
---@field tags string[]?
---@field priority Priority?
---@field actions Action[]?
local Notification
---@class Ntfy: DeviceInterface
local Ntfy
devices.Ntfy = {}
---@param config NtfyConfig
---@return Ntfy
function devices.Ntfy.new(config) end
---@async
---@param notification Notification
function Ntfy:send_notification(notification) end
---@class NtfyConfig
---@field url string?
---@field topic string
local NtfyConfig
---@class OutletOnOff: DeviceInterface, OnOffInterface
local OutletOnOff
devices.OutletOnOff = {}
---@param config ConfigOutletOutletStateOnOff
---@return OutletOnOff
function devices.OutletOnOff.new(config) end
---@class OutletPower: DeviceInterface, OnOffInterface
local OutletPower
devices.OutletPower = {}
---@param config ConfigOutletOutletStatePower
---@return OutletPower
function devices.OutletPower.new(config) end
---@class OutletStateOnOff
---@field state boolean
local OutletStateOnOff
---@class OutletStatePower
---@field state boolean
---@field power number
local OutletStatePower
---@alias OutletType
---| "Outlet"
---| "Kettle"
---@class Presence: DeviceInterface
local Presence
devices.Presence = {}
---@param config PresenceConfig
---@return Presence
function devices.Presence.new(config) end
---@async
---@return boolean
function Presence:overall_presence() end
---@class PresenceConfig
---@field topic string
---@field callback fun(_: Presence, _: boolean) | fun(_: Presence, _: boolean)[]?
---@field client AsyncClient
local PresenceConfig
---@alias Priority
---| "min"
---| "low"
---| "default"
---| "high"
---| "max"
---@alias SensorType
---| "Door"
---| "Drawer"
---| "Window"
---@class WakeOnLAN: DeviceInterface
local WakeOnLAN
devices.WakeOnLAN = {}
---@param config WolConfig
---@return WakeOnLAN
function devices.WakeOnLAN.new(config) end
---@class Washer: DeviceInterface
local Washer
devices.Washer = {}
---@param config WasherConfig
---@return Washer
function devices.Washer.new(config) end
---@class WasherConfig
---@field identifier string
---@field topic string
---@field threshold number
---@field done_callback fun(_: Washer) | fun(_: Washer)[]?
---@field client AsyncClient
local WasherConfig
---@class WolConfig
---@field name string
---@field room string?
---@field topic string
---@field mac_address string
---@field broadcast_ip string?
---@field client AsyncClient
local WolConfig
return devices return devices

14
definitions/config.lua Normal file
View File

@@ -0,0 +1,14 @@
-- DO NOT MODIFY, FILE IS AUTOMATICALLY GENERATED
---@meta
---@class FulfillmentConfig
---@field openid_url string
---@field ip string?
---@field port integer?
local FulfillmentConfig
---@class Config
---@field fulfillment FulfillmentConfig
---@field devices DeviceInterface[]?
---@field schedule table<string, function>?
local Config

View File

@@ -1,14 +1,15 @@
#![feature(iter_intersperse)] #![feature(iter_intersperse)]
mod config;
mod secret;
mod version;
mod web;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::Path; use std::path::Path;
use std::process; use std::process;
use ::config::{Environment, File}; 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};
use automation_lib::device_manager::DeviceManager; use automation_lib::device_manager::DeviceManager;
use axum::extract::{FromRef, State}; use axum::extract::{FromRef, State};
use axum::http::StatusCode; use axum::http::StatusCode;
@@ -18,11 +19,6 @@ use google_home::{GoogleHome, Request, Response};
use mlua::LuaSerdeExt; use mlua::LuaSerdeExt;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tracing::{debug, error, info, warn}; use tracing::{debug, error, info, warn};
use web::{ApiError, User};
use crate::config::{Config, Setup};
use crate::secret::EnvironmentSecretFile;
use crate::version::VERSION;
// Force automation_devices to link so that it gets registered as a module // Force automation_devices to link so that it gets registered as a module
extern crate automation_devices; extern crate automation_devices;
@@ -141,8 +137,13 @@ async fn app() -> anyhow::Result<()> {
lua.register_module("automation:secrets", lua.to_value(&setup.secrets)?)?; lua.register_module("automation:secrets", lua.to_value(&setup.secrets)?)?;
let entrypoint = Path::new(&setup.entrypoint); let entrypoint = Path::new(&setup.entrypoint);
let config: mlua::Value = lua.load(entrypoint).eval_async().await?; let config: Config = lua.load(entrypoint).eval_async().await?;
let config: Config = lua.from_value(config)?;
for device in config.devices {
device_manager.add(device).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,32 +1,57 @@
use std::fs::{self, File}; use std::fs::{self, File};
use std::io::Write; use std::io::Write;
use automation::config::{Config, FulfillmentConfig};
use automation_lib::Module; use automation_lib::Module;
use lua_typed::Typed;
use tracing::{info, warn}; use tracing::{info, warn};
extern crate automation_devices; extern crate automation_devices;
fn main() -> std::io::Result<()> { fn write_definitions(filename: &str, definitions: &str) -> std::io::Result<()> {
tracing_subscriber::fmt::init();
let definitions_directory = let definitions_directory =
std::path::Path::new(std::env!("CARGO_MANIFEST_DIR")).join("definitions"); std::path::Path::new(std::env!("CARGO_MANIFEST_DIR")).join("definitions");
fs::create_dir_all(&definitions_directory)?; fs::create_dir_all(&definitions_directory)?;
let mut file = File::create(definitions_directory.join(filename))?;
file.write_all(b"-- DO NOT MODIFY, FILE IS AUTOMATICALLY GENERATED\n")?;
file.write_all(definitions.as_bytes())?;
// Make sure we have a trailing new line
if !definitions.ends_with("\n") {
file.write_all(b"\n")?;
}
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("FulfillmentConfig should have a definition");
output
}
fn main() -> std::io::Result<()> {
tracing_subscriber::fmt::init();
for module in inventory::iter::<Module> { for module in inventory::iter::<Module> {
if let Some(definitions) = module.definitions() { if let Some(definitions) = module.definitions() {
info!(name = module.get_name(), "Generating definitions"); info!(name = module.get_name(), "Generating definitions");
let filename = format!("{}.lua", module.get_name()); let filename = format!("{}.lua", module.get_name());
let mut file = File::create(definitions_directory.join(filename))?; write_definitions(&filename, &definitions)?;
file.write_all(b"-- DO NOT MODIFY, FILE IS AUTOMATICALLY GENERATED\n")?;
file.write_all(definitions.as_bytes())?;
file.write_all(b"\n")?;
} else { } else {
warn!(name = module.get_name(), "No definitions"); warn!(name = module.get_name(), "No definitions");
} }
} }
write_definitions("config.lua", &config_definitions())?;
Ok(()) Ok(())
} }

View File

@@ -1,6 +1,9 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::net::{Ipv4Addr, SocketAddr}; use std::net::{Ipv4Addr, SocketAddr};
use automation_lib::device::Device;
use automation_macro::LuaDeviceConfig;
use lua_typed::Typed;
use serde::Deserialize; use serde::Deserialize;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -17,18 +20,26 @@ fn default_entrypoint() -> String {
"./config.lua".into() "./config.lua".into()
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize, Typed)]
pub struct FulfillmentConfig { pub struct FulfillmentConfig {
pub openid_url: String, pub openid_url: String,
#[serde(default = "default_fulfillment_ip")] #[serde(default = "default_fulfillment_ip")]
#[typed(default)]
pub ip: Ipv4Addr, pub ip: Ipv4Addr,
#[serde(default = "default_fulfillment_port")] #[serde(default = "default_fulfillment_port")]
#[typed(default)]
pub port: u16, pub port: u16,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, LuaDeviceConfig, Typed)]
pub struct Config { pub struct Config {
pub fulfillment: FulfillmentConfig, pub fulfillment: FulfillmentConfig,
#[device_config(from_lua, default)]
#[typed(default)]
pub devices: Vec<Box<dyn Device>>,
#[device_config(from_lua, default)]
#[typed(default)]
pub schedule: HashMap<String, mlua::Function>,
} }
impl From<FulfillmentConfig> for SocketAddr { impl From<FulfillmentConfig> for SocketAddr {

5
src/lib.rs Normal file
View File

@@ -0,0 +1,5 @@
pub mod config;
pub mod schedule;
pub mod secret;
pub mod version;
pub mod web;

28
src/schedule.rs Normal file
View File

@@ -0,0 +1,28 @@
use std::collections::HashMap;
use std::pin::Pin;
use tokio_cron_scheduler::{Job, JobScheduler, JobSchedulerError};
pub async fn start_scheduler(
schedule: HashMap<String, mlua::Function>,
) -> Result<(), JobSchedulerError> {
let scheduler = JobScheduler::new().await?;
for (s, f) in schedule {
let job = {
move |_uuid, _lock| -> Pin<Box<dyn Future<Output = ()> + Send>> {
let f = f.clone();
Box::pin(async move {
f.call_async::<()>(()).await.unwrap();
})
}
};
let job = Job::new_async(s, job)?;
scheduler.add(job).await?;
}
scheduler.start().await
}