Compare commits

...

19 Commits

Author SHA1 Message Date
631fac5061
Fixed typo in README.md and added mosquitto as word
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 5m9s
Build and deploy automation_rs / Build Docker image (push) Successful in 36s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-05-03 01:08:44 +02:00
bf3ce9efd4
Started work on reimplementing schedules 2024-05-03 01:08:44 +02:00
ff428e3d20
Fixed spelling mistakes 2024-05-03 01:08:44 +02:00
8d8878218c
Moved last config items to lua + small cleanup 2024-05-03 01:08:44 +02:00
8ad11e7965
Fixed visibility of device configs 2024-05-03 01:08:43 +02:00
42eb4b3fa5
LuaDevice macro now uses LuaDeviceCreate trait to create devices from configs 2024-05-03 01:08:43 +02:00
c91de03333
mqtt client is now created in lua 2024-05-03 01:08:43 +02:00
3fed29e0ef
DeviceManager no longer handles subscribing and filtering topics, each device has to do this themselves now 2024-05-03 01:08:43 +02:00
b928964b41
Improved how devices are created, ntfy and presence are now treated like any other device 2024-05-03 01:08:43 +02:00
e5447ba4b0
Moved schedule config from yml to lua 2024-05-03 01:08:43 +02:00
0f3992dacc
Set lua warning function 2024-05-03 01:08:43 +02:00
c2efd1e110
Slight macro cleanup 2024-05-03 01:08:43 +02:00
ffe06fde6e
Improved the internals of the LuaDeviceConfig macro and improve the
usability of the macro
2024-05-03 01:08:43 +02:00
4781610fcd
Use helper types to process config input into the right type 2024-05-03 01:08:42 +02:00
7cc9a2a090
Added helper type to convert from ip addr to socketaddr with the correct port 2024-05-03 01:08:42 +02:00
45c9e9e1d4
Added rename option to macro 2024-05-03 01:08:42 +02:00
7f0b2b3021
Everything needed to construct a new device is passed in through lua 2024-05-03 01:08:42 +02:00
463b5347b4
Device config is now done through lua 2024-05-03 01:08:42 +02:00
cde9654a78
Fix: Memory leak
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 4m19s
Build and deploy automation_rs / Build Docker image (push) Successful in 1m3s
Build and deploy automation_rs / Deploy Docker container (push) Successful in 31s
It turns out that console-subscriber has a memory leak, this is fixed in
main, but there has not been a new release yet. So for now we go back
to tracing subscriber.
2024-05-03 01:07:24 +02:00
38 changed files with 1842 additions and 1396 deletions

2
.typos.toml Normal file
View File

@ -0,0 +1,2 @@
[default.extend-words]
mosquitto = "mosquitto"

396
Cargo.lock generated
View File

@ -61,7 +61,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -76,6 +76,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"automation_macro",
"axum", "axum",
"bytes", "bytes",
"console-subscriber", "console-subscriber",
@ -86,6 +87,8 @@ dependencies = [
"google-home", "google-home",
"impl_cast", "impl_cast",
"indexmap 2.0.0", "indexmap 2.0.0",
"mlua",
"once_cell",
"paste", "paste",
"pollster", "pollster",
"regex", "regex",
@ -104,6 +107,16 @@ dependencies = [
"wakey", "wakey",
] ]
[[package]]
name = "automation_macro"
version = "0.1.0"
dependencies = [
"itertools 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.6.20" version = "0.6.20"
@ -112,7 +125,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
"bitflags", "bitflags 1.3.2",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
@ -186,6 +199,22 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "bstr"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.13.0" version = "3.13.0"
@ -344,7 +373,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -355,7 +384,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -397,7 +426,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -406,6 +435,25 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b73807008a3c7f171cc40312f37d95ef0396e048b5848d775f54b1a4dd4a0d3"
dependencies = [
"serde",
]
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "eui48" name = "eui48"
version = "1.1.0" version = "1.1.0"
@ -510,7 +558,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -632,6 +680,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.9" version = "0.2.9"
@ -766,7 +823,7 @@ name = "impl_cast"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -806,6 +863,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.9" version = "1.0.9"
@ -829,9 +895,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.147" version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@ -849,6 +921,25 @@ version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
[[package]]
name = "lua-src"
version = "546.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da0daa7eee611a4c30c8f5ee31af55266e26e573971ba9336d2993e2da129b2"
dependencies = [
"cc",
]
[[package]]
name = "luajit-src"
version = "210.5.7+d06beb0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d251fdacdabbf87704cf48ac1f8b1eb23d6e10855c3ee08e5beb25b4be2e9e4"
dependencies = [
"cc",
"which",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@ -899,7 +990,53 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi",
"windows-sys", "windows-sys 0.48.0",
]
[[package]]
name = "mlua"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d9bed6bce296397a9d6a86f995dd10a547a4e6949825d45225906bdcbfe7367"
dependencies = [
"bstr",
"erased-serde",
"futures-util",
"mlua-sys",
"mlua_derive",
"num-traits",
"once_cell",
"rustc-hash",
"serde",
"serde-value",
]
[[package]]
name = "mlua-sys"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16a9ba1dd2c6ac971b204262d434c24d65067038598f0638b64e5dca28d52b8"
dependencies = [
"cc",
"cfg-if",
"lua-src",
"luajit-src",
"pkg-config",
]
[[package]]
name = "mlua_derive"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaade5f94e5829db58791664ba98f35fea6a3ffebc783becb51dc97c7a21abee"
dependencies = [
"itertools 0.12.1",
"once_cell",
"proc-macro-error",
"proc-macro2",
"quote",
"regex",
"syn 2.0.60",
] ]
[[package]] [[package]]
@ -972,9 +1109,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.18.0" version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]] [[package]]
name = "openssl-probe" name = "openssl-probe"
@ -982,6 +1119,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@ -1017,7 +1163,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -1032,6 +1178,12 @@ 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 = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]] [[package]]
name = "pollster" name = "pollster"
version = "0.2.5" version = "0.2.5"
@ -1045,10 +1197,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro-error"
version = "1.0.66" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -1070,7 +1246,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools", "itertools 0.10.5",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn 1.0.109",
@ -1087,9 +1263,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.32" version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -1246,6 +1422,25 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.5.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.20.8" version = "0.20.8"
@ -1328,7 +1523,7 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [ dependencies = [
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -1353,7 +1548,7 @@ version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@ -1372,22 +1567,32 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.190" version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde-value"
version = "1.0.190" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -1419,7 +1624,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -1460,7 +1665,7 @@ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -1517,7 +1722,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -1554,9 +1759,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.28" version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1586,7 +1791,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -1657,7 +1862,7 @@ dependencies = [
"socket2 0.5.3", "socket2 0.5.3",
"tokio-macros", "tokio-macros",
"tracing", "tracing",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@ -1693,7 +1898,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -1823,7 +2028,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@ -1930,6 +2135,12 @@ 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 = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]] [[package]]
name = "wakey" name = "wakey"
version = "0.3.0" version = "0.3.0"
@ -1976,7 +2187,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2010,7 +2221,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -2050,6 +2261,18 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "which"
version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7"
dependencies = [
"either",
"home",
"rustix",
"winsafe",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@ -2078,7 +2301,7 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [ dependencies = [
"windows-targets", "windows-targets 0.48.1",
] ]
[[package]] [[package]]
@ -2087,7 +2310,16 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [ dependencies = [
"windows-targets", "windows-targets 0.48.1",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.5",
] ]
[[package]] [[package]]
@ -2096,13 +2328,29 @@ version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm", "windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_msvc", "windows_aarch64_msvc 0.48.0",
"windows_i686_gnu", "windows_i686_gnu 0.48.0",
"windows_i686_msvc", "windows_i686_msvc 0.48.0",
"windows_x86_64_gnu", "windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm", "windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_msvc", "windows_x86_64_msvc 0.48.0",
]
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm 0.52.5",
"windows_aarch64_msvc 0.52.5",
"windows_i686_gnu 0.52.5",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.5",
"windows_x86_64_gnu 0.52.5",
"windows_x86_64_gnullvm 0.52.5",
"windows_x86_64_msvc 0.52.5",
] ]
[[package]] [[package]]
@ -2111,42 +2359,90 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.10.1" version = "0.10.1"
@ -2155,3 +2451,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "winsafe"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"

View File

@ -4,9 +4,10 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[workspace] [workspace]
members = ["impl_cast", "google-home"] members = ["impl_cast", "google-home", "automation_macro"]
[dependencies] [dependencies]
automation_macro = { path = "./automation_macro" }
rumqttc = "0.18" rumqttc = "0.18"
serde = { version = "1.0.149", features = ["derive"] } serde = { version = "1.0.149", features = ["derive"] }
serde_json = "1.0.89" serde_json = "1.0.89"
@ -41,6 +42,8 @@ enum_dispatch = "0.3.12"
indexmap = { version = "2.0.0", features = ["serde"] } indexmap = { version = "2.0.0", features = ["serde"] }
serde_yaml = "0.9.27" serde_yaml = "0.9.27"
tokio-cron-scheduler = "0.9.4" tokio-cron-scheduler = "0.9.4"
mlua = { version = "0.9.7", features = ["lua54", "vendored", "macros", "serialize", "async", "send"] }
once_cell = "1.19.0"
[patch.crates-io] [patch.crates-io]
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" } wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }

View File

@ -1,7 +1,7 @@
FROM gcr.io/distroless/cc-debian12:nonroot FROM gcr.io/distroless/cc-debian12:nonroot
ENV AUTOMATION_CONFIG=/app/config.yml ENV AUTOMATION_CONFIG=/app/config.lua
COPY ./config/config.yml /app/config.yml COPY ./config.lua /app/config.lua
COPY ./build/automation /app/automation COPY ./build/automation /app/automation

View File

@ -1,8 +1,11 @@
# automation_rs # automation_rs
Custom home automation solution with google-home intergration
Custom home automation solution with google-home integration
## Development ## Development
Make sure to setup git hooks by running Make sure to setup git hooks by running
```sh ```sh
git config --local core.hooksPath .git-hooks/ git config --local core.hooksPath .git-hooks/
``` ```

View File

@ -0,0 +1,13 @@
[package]
name = "automation_macro"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]
itertools = "0.12.1"
proc-macro2 = "1.0.81"
quote = "1.0.36"
syn = { version = "2.0.60", features = ["extra-traits", "full"] }

View File

@ -0,0 +1,20 @@
mod lua_device;
mod lua_device_config;
use lua_device::impl_lua_device_macro;
use lua_device_config::impl_lua_device_config_macro;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(LuaDevice, attributes(config))]
pub fn lua_device_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_lua_device_macro(&ast).into()
}
#[proc_macro_derive(LuaDeviceConfig, attributes(device_config))]
pub fn lua_device_config_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
impl_lua_device_config_macro(&ast).into()
}

View File

@ -0,0 +1,28 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::DeriveInput;
pub fn impl_lua_device_macro(ast: &DeriveInput) -> TokenStream {
let name = &ast.ident;
let gen = quote! {
impl #name {
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
lua.globals().set(stringify!(#name), lua.create_proxy::<#name>()?)
}
}
impl mlua::UserData for #name {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_function("new", |lua, config: mlua::Value| async {
let config = mlua::FromLua::from_lua(config, lua)?;
// TODO: Using crate:: could cause issues
let device: #name = crate::devices::LuaDeviceCreate::create(config).await.map_err(mlua::ExternalError::into_lua_err)?;
Ok(crate::device_manager::WrappedDevice::new(Box::new(device)))
});
}
}
};
gen
}

View File

@ -0,0 +1,280 @@
use itertools::Itertools;
use proc_macro2::TokenStream;
use quote::{quote, quote_spanned};
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::spanned::Spanned;
use syn::token::Paren;
use syn::{
parenthesized, Data, DataStruct, DeriveInput, Expr, Field, Fields, FieldsNamed, LitStr, Result,
Token, Type,
};
mod kw {
use syn::custom_keyword;
custom_keyword!(device_config);
custom_keyword!(flatten);
custom_keyword!(from_lua);
custom_keyword!(rename);
custom_keyword!(with);
custom_keyword!(from);
custom_keyword!(default);
}
#[derive(Debug)]
enum Argument {
Flatten {
_keyword: kw::flatten,
},
FromLua {
_keyword: kw::from_lua,
},
Rename {
_keyword: kw::rename,
_paren: Paren,
ident: LitStr,
},
With {
_keyword: kw::with,
_paren: Paren,
// TODO: Ideally we capture this better
expr: Expr,
},
From {
_keyword: kw::from,
_paren: Paren,
ty: Type,
},
Default {
_keyword: kw::default,
},
DefaultExpr {
_keyword: kw::default,
_paren: Paren,
expr: Expr,
},
}
impl Parse for Argument {
fn parse(input: ParseStream) -> Result<Self> {
let lookahead = input.lookahead1();
if lookahead.peek(kw::flatten) {
Ok(Self::Flatten {
_keyword: input.parse()?,
})
} else if lookahead.peek(kw::from_lua) {
Ok(Self::FromLua {
_keyword: input.parse()?,
})
} else if lookahead.peek(kw::rename) {
let content;
Ok(Self::Rename {
_keyword: input.parse()?,
_paren: parenthesized!(content in input),
ident: content.parse()?,
})
} else if lookahead.peek(kw::with) {
let content;
Ok(Self::With {
_keyword: input.parse()?,
_paren: parenthesized!(content in input),
expr: content.parse()?,
})
} else if lookahead.peek(kw::from) {
let content;
Ok(Self::From {
_keyword: input.parse()?,
_paren: parenthesized!(content in input),
ty: content.parse()?,
})
} else if lookahead.peek(kw::default) {
let keyword = input.parse()?;
if input.peek(Paren) {
let content;
Ok(Self::DefaultExpr {
_keyword: keyword,
_paren: parenthesized!(content in input),
expr: content.parse()?,
})
} else {
Ok(Self::Default { _keyword: keyword })
}
} else {
Err(lookahead.error())
}
}
}
#[derive(Debug)]
struct Args {
args: Punctuated<Argument, Token![,]>,
}
impl Parse for Args {
fn parse(input: ParseStream) -> Result<Self> {
Ok(Self {
args: input.parse_terminated(Argument::parse, Token![,])?,
})
}
}
fn field_from_lua(field: &Field) -> TokenStream {
let (args, errors): (Vec<_>, Vec<_>) = field
.attrs
.iter()
.filter_map(|attr| {
if attr.path().is_ident("device_config") {
Some(attr.parse_args::<Args>().map(|args| args.args))
} else {
None
}
})
.partition_result();
let errors: Vec<_> = errors
.iter()
.map(|error| error.to_compile_error())
.collect();
if !errors.is_empty() {
return quote! { #(#errors)* };
}
let args: Vec<_> = args.into_iter().flatten().collect();
let table_name = match args
.iter()
.filter_map(|arg| match arg {
Argument::Rename { ident, .. } => Some(ident.value()),
_ => None,
})
.collect::<Vec<_>>()
.as_slice()
{
[] => field.ident.clone().unwrap().to_string(),
[rename] => rename.to_owned(),
_ => {
return quote_spanned! {field.span() => compile_error!("Field contains duplicate 'rename'")}
}
};
// TODO: Detect Option<_> properly and use Default::default() as fallback automatically
let missing = format!("Missing field '{table_name}'");
let default = match args
.iter()
.filter_map(|arg| match arg {
Argument::Default { .. } => Some(quote! { Default::default() }),
Argument::DefaultExpr { expr, .. } => Some(quote! { (#expr) }),
_ => None,
})
.collect::<Vec<_>>()
.as_slice()
{
[] => quote! {panic!(#missing)},
[default] => default.to_owned(),
_ => {
return quote_spanned! {field.span() => compile_error!("Field contains duplicate 'default'")}
}
};
let value = match args
.iter()
.filter_map(|arg| match arg {
Argument::Flatten { .. } => Some(quote! {
mlua::LuaSerdeExt::from_value_with(lua, value.clone(), mlua::DeserializeOptions::new().deny_unsupported_types(false))?
}),
Argument::FromLua { .. } => Some(quote! {
if table.contains_key(#table_name)? {
table.get(#table_name)?
} else {
#default
}
}),
_ => None,
})
.collect::<Vec<_>>()
.as_slice() {
[] => quote! {
{
let value: mlua::Value = table.get(#table_name)?;
if !value.is_nil() {
mlua::LuaSerdeExt::from_value(lua, value)?
} else {
#default
}
}
},
[value] => value.to_owned(),
_ => return quote_spanned! {field.span() => compile_error!("Only one of either 'flatten' or 'from_lua' is allowed")},
};
let value = match args
.iter()
.filter_map(|arg| match arg {
Argument::From { ty, .. } => Some(quote! {
{
let temp: #ty = #value;
temp.into()
}
}),
Argument::With { expr, .. } => Some(quote! {
{
let temp = #value;
(#expr)(temp)
}
}),
_ => None,
})
.collect::<Vec<_>>()
.as_slice()
{
[] => value,
[value] => value.to_owned(),
_ => {
return quote_spanned! {field.span() => compile_error!("Only one of either 'from' or 'with' is allowed")}
}
};
quote! { #value }
}
pub fn impl_lua_device_config_macro(ast: &DeriveInput) -> TokenStream {
let name = &ast.ident;
let fields = if let Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
} else {
return quote_spanned! {ast.span() => compile_error!("This macro only works on named structs")};
};
let lua_fields: Vec<_> = fields
.iter()
.map(|field| {
let name = field.ident.clone().unwrap();
let value = field_from_lua(field);
quote! { #name: #value }
})
.collect();
let impl_from_lua = quote! {
impl<'lua> mlua::FromLua<'lua> for #name {
fn from_lua(value: mlua::Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result<Self> {
if !value.is_table() {
panic!("Expected table");
}
let table = value.as_table().unwrap();
Ok(#name {
#(#lua_fields,)*
})
}
}
};
impl_from_lua
}

173
config.lua Normal file
View File

@ -0,0 +1,173 @@
print("Hello from lua")
automation.fulfillment = {
openid_url = "https://login.huizinga.dev/api/oidc",
}
local debug, value = pcall(automation.util.get_env, "DEBUG")
if debug and value ~= "true" then
debug = false
end
local function mqtt_z2m(topic)
return "zigbee2mqtt/" .. topic
end
local function mqtt_automation(topic)
return "automation/" .. topic
end
local mqtt_client = automation.new_mqtt_client({
host = debug and "olympus.lan.huizinga.dev" or "mosquitto",
port = 8883,
client_name = debug and "automation-debug" or "automation_rs",
username = "mqtt",
password = automation.util.get_env("MQTT_PASSWORD"),
tls = debug and true or false,
})
automation.device_manager:add(Ntfy.new({
topic = automation.util.get_env("NTFY_TOPIC"),
event_channel = automation.device_manager:event_channel(),
}))
automation.device_manager:add(Presence.new({
topic = "automation_dev/presence/+/#",
client = mqtt_client,
event_channel = automation.device_manager:event_channel(),
}))
automation.device_manager:add(DebugBridge.new({
identifier = "debug_bridge",
topic = mqtt_automation("debug"),
client = mqtt_client,
}))
local hue_ip = "10.0.0.146"
local hue_token = automation.util.get_env("HUE_TOKEN")
automation.device_manager:add(HueBridge.new({
identifier = "hue_bridge",
ip = hue_ip,
login = hue_token,
flags = {
presence = 41,
darkness = 43,
},
}))
automation.device_manager:add(LightSensor.new({
identifier = "living_light_sensor",
topic = mqtt_z2m("living/light"),
client = mqtt_client,
min = 22000,
max = 23500,
event_channel = automation.device_manager:event_channel(),
}))
automation.device_manager:add(WakeOnLAN.new({
name = "Zeus",
room = "Living Room",
topic = mqtt_automation("appliance/living_room/zeus"),
client = mqtt_client,
mac_address = "30:9c:23:60:9c:13",
broadcast_ip = "10.0.0.255",
}))
local living_mixer = KasaOutlet.new({ identifier = "living_mixer", ip = "10.0.0.49" })
automation.device_manager:add(living_mixer)
local living_speakers = KasaOutlet.new({ identifier = "living_speakers", ip = "10.0.0.182" })
automation.device_manager:add(living_speakers)
automation.device_manager:add(AudioSetup.new({
identifier = "living_audio",
topic = mqtt_z2m("living/remote"),
client = mqtt_client,
mixer = living_mixer,
speakers = living_speakers,
}))
automation.device_manager:add(IkeaOutlet.new({
outlet_type = "Kettle",
name = "Kettle",
room = "Kitchen",
topic = mqtt_z2m("kitchen/kettle"),
client = mqtt_client,
timeout = debug and 5 or 300,
remotes = {
{ topic = mqtt_z2m("bedroom/remote") },
{ topic = mqtt_z2m("kitchen/remote") },
},
}))
automation.device_manager:add(IkeaOutlet.new({
outlet_type = "Light",
name = "Light",
room = "Bathroom",
topic = mqtt_z2m("batchroom/light"),
client = mqtt_client,
timeout = debug and 60 or 45 * 60,
}))
automation.device_manager:add(Washer.new({
identifier = "bathroom_washer",
topic = mqtt_z2m("batchroom/washer"),
client = mqtt_client,
threshold = 1,
event_channel = automation.device_manager:event_channel(),
}))
automation.device_manager:add(IkeaOutlet.new({
outlet_type = "Charger",
name = "Charger",
room = "Workbench",
topic = mqtt_z2m("workbench/charger"),
client = mqtt_client,
timeout = debug and 5 or 20 * 3600,
}))
automation.device_manager:add(IkeaOutlet.new({
name = "Outlet",
room = "Workbench",
topic = mqtt_z2m("workbench/outlet"),
client = mqtt_client,
}))
local hallway_lights = automation.device_manager:add(HueGroup.new({
identifier = "hallway_lights",
ip = hue_ip,
login = hue_token,
group_id = 81,
scene_id = "3qWKxGVadXFFG4o",
timer_id = 1,
remotes = {
{ topic = mqtt_z2m("hallway/remote") },
},
client = mqtt_client,
}))
automation.device_manager:add(ContactSensor.new({
identifier = "hallway_frontdoor",
topic = mqtt_z2m("hallway/frontdoor"),
client = mqtt_client,
presence = {
topic = mqtt_automation("presence/contact/frontdoor"),
timeout = debug and 10 or 15 * 60,
},
trigger = {
devices = { hallway_lights },
timeout = debug and 10 or 2 * 60,
},
}))
local bedroom_air_filter = AirFilter.new({
name = "Air Filter",
room = "Bedroom",
topic = "pico/filter/bedroom",
client = mqtt_client,
})
automation.device_manager:add(bedroom_air_filter)
automation.device_manager:schedule("0/1 * * * * *", function()
print("Device: " .. bedroom_air_filter:get_id())
end)

View File

@ -1,66 +0,0 @@
openid:
base_url: "https://login.huizinga.dev/api/oidc"
mqtt:
host: "olympus.vpn.huizinga.dev"
port: 8883
client_name: "automation-ares"
username: "mqtt"
password: "${MQTT_PASSWORD}"
tls: true
ntfy:
topic: "${NTFY_TOPIC}"
presence:
topic: "automation_dev/presence/+/#"
devices:
debug_bridge:
!DebugBridge
topic: "automation_dev/debug"
living_light_sensor:
!LightSensor
topic: "zigbee2mqtt_dev/living/light"
min: 23000
max: 25000
kitchen_kettle:
!IkeaOutlet
outlet_type: "Kettle"
name: "Kettle"
room: "Kitchen"
topic: "zigbee2mqtt/kitchen/kettle"
timeout: 5
remotes:
- topic: "zigbee2mqtt/bedroom/remote"
- topic: "zigbee2mqtt/kitchen/remote"
workbench_charger:
!IkeaOutlet
outlet_type: "Charger"
name: "Charger"
room: "Workbench"
topic: "zigbee2mqtt/workbench/charger"
timeout: 5
workbench_outlet:
!IkeaOutlet
name: "Outlet"
room: "Workbench"
topic: "zigbee2mqtt/workbench/outlet"
living_zeus:
!WakeOnLAN
name: "Zeus"
room: "Living Room"
topic: "automation/appliance/living_room/zeus"
mac_address: "30:9c:23:60:9c:13"
hallway_frontdoor:
!ContactSensor
topic: "zigbee2mqtt/hallway/frontdoor"
presence:
topic: "automation_dev/presence/contact/frontdoor"
timeout: 10

View File

@ -1,134 +0,0 @@
openid:
base_url: "https://login.huizinga.dev/api/oidc"
mqtt:
host: "mosquitto"
port: 8883
client_name: "automation_rs"
username: "mqtt"
password: "${MQTT_PASSWORD}"
ntfy:
topic: "${NTFY_TOPIC}"
presence:
topic: "automation/presence/+/#"
devices:
debug_bridge:
!DebugBridge
topic: "automation/debug"
hue_bridge:
!HueBridge
ip: &hue_ip "10.0.0.146"
login: &hue_token "${HUE_TOKEN}"
flags: { presence: 41, darkness: 43 }
living_light_sensor:
!LightSensor
topic: "zigbee2mqtt/living/light"
min: 22000
max: 23500
living_zeus:
!WakeOnLAN
name: "Zeus"
room: "Living Room"
topic: "automation/appliance/living_room/zeus"
mac_address: "30:9c:23:60:9c:13"
broadcast_ip: "10.0.0.255"
&mixer living_mixer:
!KasaOutlet
ip: "10.0.0.49"
&speakers living_speakers:
!KasaOutlet
ip: "10.0.0.182"
living_audio:
!AudioSetup
topic: "zigbee2mqtt/living/remote"
mixer: *mixer
speakers: *speakers
kitchen_kettle:
!IkeaOutlet
outlet_type: "Kettle"
name: "Kettle"
room: "Kitchen"
topic: "zigbee2mqtt/kitchen/kettle"
timeout: 300
remotes:
- topic: "zigbee2mqtt/bedroom/remote"
- topic: "zigbee2mqtt/kitchen/remote"
bathroom_light:
!IkeaOutlet
type: "IkeaOutlet"
outlet_type: "Light"
name: "Light"
room: "Bathroom"
topic: "zigbee2mqtt/bathroom/light"
timeout: 2700
bathroom_washer:
!Washer
topic: "zigbee2mqtt/bathroom/washer"
threshold: 1
workbench_charger:
!IkeaOutlet
outlet_type: "Charger"
name: "Charger"
room: "Workbench"
topic: "zigbee2mqtt/workbench/charger"
timeout: 72000
workbench_outlet:
!IkeaOutlet
name: "Outlet"
room: "Workbench"
topic: "zigbee2mqtt/workbench/outlet"
hallway_lights:
!HueGroup
ip: *hue_ip
login: *hue_token
group_id: 81
scene_id: "3qWKxGVadXFFG4o"
timer_id: 1
remotes:
- topic: "zigbee2mqtt/hallway/remote"
hallway_frontdoor:
!ContactSensor
topic: "zigbee2mqtt/hallway/frontdoor"
presence:
topic: "automation/presence/contact/frontdoor"
timeout: 900
trigger:
devices: ["hallway_lights"]
timeout: 60
&air_filter bedroom_air_filter:
!AirFilter
name: "Air Filter"
room: "Bedroom"
topic: "pico/filter/bedroom"
# Run the air filter everyday for 19:00 to 20:00
schedule:
0 0 19 * * *:
on:
- *air_filter
0 0 20 * * *:
off:
- *air_filter

View File

@ -1,133 +0,0 @@
openid:
base_url: "https://login.huizinga.dev/api/oidc"
mqtt:
host: "olympus.lan.huizinga.dev"
port: 8883
client_name: "automation-zeus"
username: "mqtt"
password: "${MQTT_PASSWORD}"
tls: true
ntfy:
topic: "${NTFY_TOPIC}"
presence:
topic: "automation_dev/presence/+/#"
devices:
debug_bridge:
!DebugBridge
topic: "automation_dev/debug"
hue_bridge:
!HueBridge
ip: &hue_ip "10.0.0.146"
login: &hue_token "${HUE_TOKEN}"
flags: { presence: 41, darkness: 43 }
living_light_sensor:
!LightSensor
topic: "zigbee2mqtt_dev/living/light"
min: 23000
max: 25000
living_zeus:
!WakeOnLAN
name: "Zeus"
room: "Living Room"
topic: "automation/appliance/living_room/zeus"
mac_address: "30:9c:23:60:9c:13"
&mixer living_mixer:
!KasaOutlet
ip: "10.0.0.49"
&speakers living_speakers:
!KasaOutlet
ip: "10.0.0.182"
living_audio:
!AudioSetup
topic: "zigbee2mqtt/living/remote"
mixer: *mixer
speakers: *speakers
kitchen_kettle:
!IkeaOutlet
outlet_type: "Kettle"
name: "Kettle"
room: "Kitchen"
topic: "zigbee2mqtt/kitchen/kettle"
timeout: 5
remotes:
- topic: "zigbee2mqtt/bedroom/remote"
- topic: "zigbee2mqtt/kitchen/remote"
bathroom_light:
!IkeaOutlet
type: "IkeaOutlet"
outlet_type: "Light"
name: "Light"
room: "Bathroom"
topic: "zigbee2mqtt/bathroom/light"
timeout: 60
bathroom_washer:
!Washer
topic: "zigbee2mqtt/bathroom/washer"
threshold: 1
workbench_charger:
!IkeaOutlet
outlet_type: "Charger"
name: "Charger"
room: "Workbench"
topic: "zigbee2mqtt/workbench/charger"
timeout: 5
&outlet workbench_outlet:
!IkeaOutlet
name: "Outlet"
room: "Workbench"
topic: "zigbee2mqtt/workbench/outlet"
hallway_lights:
!HueGroup
ip: *hue_ip
login: *hue_token
group_id: 81
scene_id: "3qWKxGVadXFFG4o"
timer_id: 1
remotes:
- topic: "zigbee2mqtt/hallway/remote"
hallway_frontdoor:
!ContactSensor
topic: "zigbee2mqtt/hallway/frontdoor"
presence:
topic: "automation_dev/presence/contact/frontdoor"
timeout: 10
trigger:
devices: ["hallway_lights"]
timeout: 10
bedroom_air_filter:
!AirFilter
name: "Air Filter"
room: "Bedroom"
topic: "pico/filter/bedroom"
# schedule:
# 0/30 * * * * *:
# on:
# - *outlet
#
# 15/30 * * * * *:
# off:
# - *outlet

View File

@ -46,10 +46,10 @@ where
pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static { pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
fn get_device_type(&self) -> Type; fn get_device_type(&self) -> Type;
fn get_device_name(&self) -> Name; fn get_device_name(&self) -> Name;
fn get_id(&self) -> &str; fn get_id(&self) -> String;
fn is_online(&self) -> bool; fn is_online(&self) -> bool;
// Default values that can optionally be overriden // Default values that can optionally be overridden
fn will_report_state(&self) -> bool { fn will_report_state(&self) -> bool {
false false
} }
@ -63,7 +63,7 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
async fn sync(&self) -> response::sync::Device { async fn sync(&self) -> response::sync::Device {
let name = self.get_device_name(); let name = self.get_device_name();
let mut device = let mut device =
response::sync::Device::new(self.get_id(), &name.name, self.get_device_type()); response::sync::Device::new(&self.get_id(), &name.name, self.get_device_type());
device.name = name; device.name = name;
device.will_report_state = self.will_report_state(); device.will_report_state = self.will_report_state();

View File

@ -17,7 +17,7 @@ pub struct GoogleHome {
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum FullfillmentError { pub enum FulfillmentError {
#[error("Expected at least one ResponsePayload")] #[error("Expected at least one ResponsePayload")]
ExpectedOnePayload, ExpectedOnePayload,
} }
@ -33,7 +33,7 @@ impl GoogleHome {
&self, &self,
request: Request, request: Request,
devices: &HashMap<String, Arc<RwLock<Box<T>>>>, devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
) -> Result<Response, FullfillmentError> { ) -> Result<Response, FulfillmentError> {
// TODO: What do we do if we actually get more then one thing in the input array, right now // TODO: What do we do if we actually get more then one thing in the input array, right now
// we only respond to the first thing // we only respond to the first thing
let intent = request.inputs.into_iter().next(); let intent = request.inputs.into_iter().next();
@ -54,7 +54,7 @@ impl GoogleHome {
payload payload
.await .await
.ok_or(FullfillmentError::ExpectedOnePayload) .ok_or(FulfillmentError::ExpectedOnePayload)
.map(|payload| Response::new(&request.request_id, payload)) .map(|payload| Response::new(&request.request_id, payload))
} }

View File

@ -2,7 +2,7 @@
#![feature(specialization)] #![feature(specialization)]
#![feature(let_chains)] #![feature(let_chains)]
pub mod device; pub mod device;
mod fullfillment; mod fulfillment;
mod request; mod request;
mod response; mod response;
@ -13,6 +13,6 @@ pub mod traits;
pub mod types; pub mod types;
pub use device::GoogleHomeDevice; pub use device::GoogleHomeDevice;
pub use fullfillment::{FullfillmentError, GoogleHome}; pub use fulfillment::{FulfillmentError, GoogleHome};
pub use request::Request; pub use request::Request;
pub use response::Response; pub use response::Response;

View File

@ -6,11 +6,6 @@ use serde::Deserialize;
use crate::error::{ApiError, ApiErrorJson}; use crate::error::{ApiError, ApiErrorJson};
#[derive(Debug, Clone, Deserialize)]
pub struct OpenIDConfig {
pub base_url: String,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct User { pub struct User {
pub preferred_username: String, pub preferred_username: String,
@ -19,18 +14,18 @@ pub struct User {
#[async_trait] #[async_trait]
impl<S> FromRequestParts<S> for User impl<S> FromRequestParts<S> for User
where where
OpenIDConfig: FromRef<S>, String: FromRef<S>,
S: Send + Sync, S: Send + Sync,
{ {
type Rejection = ApiError; type Rejection = ApiError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> { async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
// Get the state // Get the state
let openid = OpenIDConfig::from_ref(state); let openid_url = String::from_ref(state);
// Create a request to the auth server // Create a request to the auth server
// TODO: Do some discovery to find the correct url for this instead of assuming // TODO: Do some discovery to find the correct url for this instead of assuming
let mut req = reqwest::Client::new().get(format!("{}/userinfo", openid.base_url)); let mut req = reqwest::Client::new().get(format!("{}/userinfo", openid_url));
// Add auth header to the request if it exists // Add auth header to the request if it exists
if let Some(auth) = parts.headers.get(axum::http::header::AUTHORIZATION) { if let Some(auth) = parts.headers.get(axum::http::header::AUTHORIZATION) {

View File

@ -1,31 +1,8 @@
use std::fs;
use std::net::{Ipv4Addr, SocketAddr}; use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration; use std::time::Duration;
use indexmap::IndexMap;
use regex::{Captures, Regex};
use rumqttc::{MqttOptions, Transport}; use rumqttc::{MqttOptions, Transport};
use serde::{Deserialize, Deserializer}; use serde::Deserialize;
use tracing::debug;
use crate::auth::OpenIDConfig;
use crate::device_manager::DeviceConfigs;
use crate::devices::PresenceConfig;
use crate::error::{ConfigParseError, MissingEnv};
use crate::schedule::Schedule;
#[derive(Debug, Deserialize)]
pub struct Config {
pub openid: OpenIDConfig,
#[serde(deserialize_with = "deserialize_mqtt_options")]
pub mqtt: MqttOptions,
#[serde(default)]
pub fullfillment: FullfillmentConfig,
pub ntfy: Option<NtfyConfig>,
pub presence: PresenceConfig,
pub devices: IndexMap<String, DeviceConfigs>,
pub schedule: Schedule,
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct MqttConfig { pub struct MqttConfig {
@ -52,90 +29,46 @@ impl From<MqttConfig> for MqttOptions {
} }
} }
fn deserialize_mqtt_options<'de, D>(deserializer: D) -> Result<MqttOptions, D::Error>
where
D: Deserializer<'de>,
{
Ok(MqttOptions::from(MqttConfig::deserialize(deserializer)?))
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct FullfillmentConfig { pub struct FulfillmentConfig {
#[serde(default = "default_fullfillment_ip")] pub openid_url: String,
#[serde(default = "default_fulfillment_ip")]
pub ip: Ipv4Addr, pub ip: Ipv4Addr,
#[serde(default = "default_fullfillment_port")] #[serde(default = "default_fulfillment_port")]
pub port: u16, pub port: u16,
} }
impl From<FullfillmentConfig> for SocketAddr { impl From<FulfillmentConfig> for SocketAddr {
fn from(fullfillment: FullfillmentConfig) -> Self { fn from(fulfillment: FulfillmentConfig) -> Self {
(fullfillment.ip, fullfillment.port).into() (fulfillment.ip, fulfillment.port).into()
} }
} }
impl Default for FullfillmentConfig { fn default_fulfillment_ip() -> Ipv4Addr {
fn default() -> Self {
Self {
ip: default_fullfillment_ip(),
port: default_fullfillment_port(),
}
}
}
fn default_fullfillment_ip() -> Ipv4Addr {
[0, 0, 0, 0].into() [0, 0, 0, 0].into()
} }
fn default_fullfillment_port() -> u16 { fn default_fulfillment_port() -> u16 {
7878 7878
} }
#[derive(Debug, Deserialize)]
pub struct NtfyConfig {
#[serde(default = "default_ntfy_url")]
pub url: String,
pub topic: String,
}
fn default_ntfy_url() -> String {
"https://ntfy.sh".into()
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct InfoConfig { pub struct InfoConfig {
pub name: String, pub name: String,
pub room: Option<String>, pub room: Option<String>,
} }
impl InfoConfig {
pub fn identifier(&self) -> String {
(if let Some(room) = &self.room {
room.to_ascii_lowercase().replace(' ', "_") + "_"
} else {
String::new()
}) + &self.name.to_ascii_lowercase().replace(' ', "_")
}
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct MqttDeviceConfig { pub struct MqttDeviceConfig {
pub topic: String, pub topic: String,
} }
impl Config {
pub fn parse_file(filename: &str) -> Result<Self, ConfigParseError> {
debug!("Loading config: {filename}");
let file = fs::read_to_string(filename)?;
// Substitute in environment variables
let re = Regex::new(r"\$\{(.*)\}").expect("Regex should be valid");
let mut missing = MissingEnv::new();
let file = re.replace_all(&file, |caps: &Captures| {
let key = caps.get(1).expect("Capture group should exist").as_str();
debug!("Substituting '{key}' in config");
match std::env::var(key) {
Ok(value) => value,
Err(_) => {
missing.add_missing(key);
"".into()
}
}
});
missing.has_missing()?;
let config: Config = serde_yaml::from_str(&file)?;
Ok(config)
}
}

View File

@ -1,75 +1,64 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
use enum_dispatch::enum_dispatch;
use futures::future::join_all; use futures::future::join_all;
use google_home::traits::OnOff; use mlua::FromLua;
use rumqttc::{matches, AsyncClient, QoS};
use serde::Deserialize;
use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::sync::{RwLock, RwLockReadGuard};
use tokio_cron_scheduler::{Job, JobScheduler}; use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{debug, error, instrument, trace}; use tracing::{debug, instrument, trace};
use crate::devices::{ use crate::devices::{As, Device};
AirFilterConfig, As, AudioSetupConfig, ContactSensorConfig, DebugBridgeConfig, Device,
HueBridgeConfig, HueGroupConfig, IkeaOutletConfig, KasaOutletConfig, LightSensorConfig,
WakeOnLANConfig, WasherConfig,
};
use crate::error::DeviceConfigError;
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence}; use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
use crate::schedule::{Action, Schedule}; use crate::LUA;
pub struct ConfigExternal<'a> { #[derive(Debug, FromLua, Clone)]
pub client: &'a AsyncClient, pub struct WrappedDevice(Arc<RwLock<Box<dyn Device>>>);
pub device_manager: &'a DeviceManager,
pub event_channel: &'a EventChannel, impl WrappedDevice {
pub fn new(device: Box<dyn Device>) -> Self {
Self(Arc::new(RwLock::new(device)))
}
} }
#[async_trait] impl Deref for WrappedDevice {
#[enum_dispatch] type Target = Arc<RwLock<Box<dyn Device>>>;
pub trait DeviceConfig {
async fn create( fn deref(&self) -> &Self::Target {
self, &self.0
identifier: &str, }
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError>;
} }
#[derive(Debug, Deserialize)] impl DerefMut for WrappedDevice {
#[enum_dispatch(DeviceConfig)] fn deref_mut(&mut self) -> &mut Self::Target {
pub enum DeviceConfigs { &mut self.0
AirFilter(AirFilterConfig), }
AudioSetup(AudioSetupConfig), }
ContactSensor(ContactSensorConfig), impl mlua::UserData for WrappedDevice {
DebugBridge(DebugBridgeConfig), fn add_methods<'lua, M: mlua::prelude::LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
IkeaOutlet(IkeaOutletConfig), methods.add_async_method("get_id", |_lua, this, _: ()| async {
KasaOutlet(KasaOutletConfig), Ok(crate::devices::Device::get_id(this.0.read().await.as_ref()))
WakeOnLAN(WakeOnLANConfig), });
Washer(WasherConfig), }
HueBridge(HueBridgeConfig),
HueGroup(HueGroupConfig),
LightSensor(LightSensorConfig),
} }
pub type WrappedDevice = Arc<RwLock<Box<dyn Device>>>; pub type DeviceMap = HashMap<String, Arc<RwLock<Box<dyn Device>>>>;
pub type DeviceMap = HashMap<String, WrappedDevice>;
#[derive(Debug, Clone)] #[derive(Clone)]
pub struct DeviceManager { pub struct DeviceManager {
devices: Arc<RwLock<DeviceMap>>, devices: Arc<RwLock<DeviceMap>>,
client: AsyncClient,
event_channel: EventChannel, event_channel: EventChannel,
scheduler: JobScheduler,
} }
impl DeviceManager { impl DeviceManager {
pub fn new(client: AsyncClient) -> Self { pub async fn new() -> Self {
let (event_channel, mut event_rx) = EventChannel::new(); let (event_channel, mut event_rx) = EventChannel::new();
let device_manager = Self { let device_manager = Self {
devices: Arc::new(RwLock::new(HashMap::new())), devices: Arc::new(RwLock::new(HashMap::new())),
client,
event_channel, event_channel,
scheduler: JobScheduler::new().await.unwrap(),
}; };
tokio::spawn({ tokio::spawn({
@ -85,97 +74,17 @@ impl DeviceManager {
} }
}); });
device_manager.scheduler.start().await.unwrap();
device_manager device_manager
} }
// TODO: This function is currently extremely cursed... pub async fn add(&self, device: &WrappedDevice) {
pub async fn add_schedule(&self, schedule: Schedule) { let id = device.read().await.get_id();
let sched = JobScheduler::new().await.unwrap();
for (when, actions) in schedule {
let manager = self.clone();
sched
.add(
Job::new_async(when.as_str(), move |_uuid, _l| {
let actions = actions.clone();
let manager = manager.clone();
Box::pin(async move {
for (action, targets) in actions {
for target in targets {
let device = manager.get(&target).await.unwrap();
match action {
Action::On => {
As::<dyn OnOff>::cast_mut(
device.write().await.as_mut(),
)
.unwrap()
.set_on(true)
.await
.unwrap();
}
Action::Off => {
As::<dyn OnOff>::cast_mut(
device.write().await.as_mut(),
)
.unwrap()
.set_on(false)
.await
.unwrap();
}
}
}
}
})
})
.unwrap(),
)
.await
.unwrap();
}
sched.start().await.unwrap();
}
pub async fn add(&self, device: Box<dyn Device>) {
let id = device.get_id().into();
debug!(id, "Adding device"); debug!(id, "Adding device");
// If the device listens to mqtt, subscribe to the topics self.devices.write().await.insert(id, device.0.clone());
if let Some(device) = As::<dyn OnMqtt>::cast(device.as_ref()) {
for topic in device.topics() {
trace!(id, topic, "Subscribing to topic");
if let Err(err) = self.client.subscribe(topic, QoS::AtLeastOnce).await {
// NOTE: Pretty sure that this can only happen if the mqtt client if no longer
// running
error!(id, topic, "Failed to subscribe to topic: {err}");
}
}
}
// Wrap the device
let device = Arc::new(RwLock::new(device));
self.devices.write().await.insert(id, device);
}
pub async fn create(
&self,
identifier: &str,
device_config: DeviceConfigs,
) -> Result<(), DeviceConfigError> {
let ext = ConfigExternal {
client: &self.client,
device_manager: self,
event_channel: &self.event_channel,
};
let device = device_config.create(identifier, &ext).await?;
self.add(device).await;
Ok(())
} }
pub fn event_channel(&self) -> EventChannel { pub fn event_channel(&self) -> EventChannel {
@ -183,7 +92,12 @@ impl DeviceManager {
} }
pub async fn get(&self, name: &str) -> Option<WrappedDevice> { pub async fn get(&self, name: &str) -> Option<WrappedDevice> {
self.devices.read().await.get(name).cloned() self.devices
.read()
.await
.get(name)
.cloned()
.map(WrappedDevice)
} }
pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> { pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> {
@ -201,15 +115,16 @@ impl DeviceManager {
let mut device = device.write().await; let mut device = device.write().await;
let device = device.as_mut(); let device = device.as_mut();
if let Some(device) = As::<dyn OnMqtt>::cast_mut(device) { if let Some(device) = As::<dyn OnMqtt>::cast_mut(device) {
let subscribed = device // let subscribed = device
.topics() // .topics()
.iter() // .iter()
.any(|topic| matches(&message.topic, topic)); // .any(|topic| matches(&message.topic, topic));
//
if subscribed { // if subscribed {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_mqtt(message).await; device.on_mqtt(message).await;
} trace!(id, "Done");
// }
} }
} }
}); });
@ -224,6 +139,7 @@ impl DeviceManager {
if let Some(device) = As::<dyn OnDarkness>::cast_mut(device) { if let Some(device) = As::<dyn OnDarkness>::cast_mut(device) {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_darkness(dark).await; device.on_darkness(dark).await;
trace!(id, "Done");
} }
}); });
@ -237,6 +153,7 @@ impl DeviceManager {
if let Some(device) = As::<dyn OnPresence>::cast_mut(device) { if let Some(device) = As::<dyn OnPresence>::cast_mut(device) {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_presence(presence).await; device.on_presence(presence).await;
trace!(id, "Done");
} }
}); });
@ -252,6 +169,7 @@ impl DeviceManager {
if let Some(device) = As::<dyn OnNotification>::cast_mut(device) { if let Some(device) = As::<dyn OnNotification>::cast_mut(device) {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_notification(notification).await; device.on_notification(notification).await;
trace!(id, "Done");
} }
} }
}); });
@ -261,3 +179,50 @@ impl DeviceManager {
} }
} }
} }
impl mlua::UserData for DeviceManager {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_method("add", |_lua, this, device: WrappedDevice| async move {
this.add(&device).await;
Ok(())
});
methods.add_async_method(
"schedule",
|lua, this, (schedule, f): (String, mlua::Function)| async move {
debug!("schedule = {schedule}");
let uuid = this
.scheduler
.add(
Job::new_async(schedule.as_str(), |uuid, _lock| {
Box::pin(async move {
let lua = LUA.lock().await;
let f: mlua::Function =
lua.named_registry_value(uuid.to_string().as_str()).unwrap();
f.call::<_, ()>(()).unwrap();
})
})
.unwrap(),
)
.await
.unwrap();
// Store the function in the registry
lua.set_named_registry_value(uuid.to_string().as_str(), f)
.unwrap();
Ok(())
},
);
// methods.add_async_method("add_schedule", |lua, this, schedule| async {
// let schedule = lua.from_value(schedule)?;
// this.add_schedule(schedule).await;
// Ok(())
// });
methods.add_method("event_channel", |_lua, this, ()| Ok(this.event_channel()))
}
}

View File

@ -1,57 +1,34 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::device::Name; use google_home::device::Name;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValues}; use google_home::traits::{AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValues};
use google_home::types::Type; use google_home::types::Type;
use google_home::GoogleHomeDevice; use google_home::GoogleHomeDevice;
use rumqttc::{AsyncClient, Publish}; use rumqttc::Publish;
use serde::Deserialize; use tracing::{debug, error, trace, warn};
use tracing::{debug, error, warn};
use super::LuaDeviceCreate;
use crate::config::{InfoConfig, MqttDeviceConfig}; use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::OnMqtt; use crate::event::OnMqtt;
use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState}; use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct AirFilterConfig { pub struct AirFilterConfig {
#[serde(flatten)] #[device_config(flatten)]
info: InfoConfig, pub info: InfoConfig,
#[serde(flatten)] #[device_config(flatten)]
mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
} }
#[async_trait] #[derive(Debug, LuaDevice)]
impl DeviceConfig for AirFilterConfig {
async fn create(
self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = AirFilter {
identifier: identifier.into(),
info: self.info,
mqtt: self.mqtt,
client: ext.client.clone(),
last_known_state: AirFilterState {
state: AirFilterFanState::Off,
humidity: 0.0,
},
};
Ok(Box::new(device))
}
}
#[derive(Debug)]
pub struct AirFilter { pub struct AirFilter {
identifier: String, config: AirFilterConfig,
info: InfoConfig,
mqtt: MqttDeviceConfig,
client: AsyncClient,
last_known_state: AirFilterState, last_known_state: AirFilterState,
} }
@ -59,11 +36,12 @@ impl AirFilter {
async fn set_speed(&self, state: AirFilterFanState) { async fn set_speed(&self, state: AirFilterFanState) {
let message = SetAirFilterFanState::new(state); let message = SetAirFilterFanState::new(state);
let topic = format!("{}/set", self.mqtt.topic); let topic = format!("{}/set", self.config.mqtt.topic);
// TODO: Handle potential errors here // TODO: Handle potential errors here
self.client self.config
.client
.publish( .publish(
topic.clone(), &topic,
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
false, false,
serde_json::to_string(&message).unwrap(), serde_json::to_string(&message).unwrap(),
@ -74,23 +52,46 @@ impl AirFilter {
} }
} }
#[async_trait]
impl LuaDeviceCreate for AirFilter {
type Config = AirFilterConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.info.identifier(), "Setting up AirFilter");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config,
last_known_state: AirFilterState {
state: AirFilterFanState::Off,
humidity: 0.0,
},
})
}
}
impl Device for AirFilter { impl Device for AirFilter {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.info.identifier()
} }
} }
#[async_trait] #[async_trait]
impl OnMqtt for AirFilter { impl OnMqtt for AirFilter {
fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) { async fn on_mqtt(&mut self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let state = match AirFilterState::try_from(message) { let state = match AirFilterState::try_from(message) {
Ok(state) => state, Ok(state) => state,
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(id = Device::get_id(self), "Failed to parse message: {err}");
return; return;
} }
}; };
@ -99,7 +100,7 @@ impl OnMqtt for AirFilter {
return; return;
} }
debug!(id = self.identifier, "Updating state to {state:?}"); debug!(id = Device::get_id(self), "Updating state to {state:?}");
self.last_known_state = state; self.last_known_state = state;
} }
@ -111,10 +112,10 @@ impl GoogleHomeDevice for AirFilter {
} }
fn get_device_name(&self) -> Name { fn get_device_name(&self) -> Name {
Name::new(&self.info.name) Name::new(&self.config.info.name)
} }
fn get_id(&self) -> &str { fn get_id(&self) -> String {
Device::get_id(self) Device::get_id(self)
} }
@ -123,7 +124,7 @@ impl GoogleHomeDevice for AirFilter {
} }
fn get_room_hint(&self) -> Option<&str> { fn get_room_hint(&self) -> Option<&str> {
self.info.room.as_deref() self.config.info.room.as_deref()
} }
fn will_report_state(&self) -> bool { fn will_report_state(&self) -> bool {

View File

@ -1,107 +1,88 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::traits::OnOff; use google_home::traits::OnOff;
use serde::Deserialize;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::Device; use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig, WrappedDevice}; use crate::device_manager::WrappedDevice;
use crate::devices::As; use crate::devices::As;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::messages::{RemoteAction, RemoteMessage}; use crate::messages::{RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct AudioSetupConfig { pub struct AudioSetupConfig {
#[serde(flatten)] pub identifier: String,
mqtt: MqttDeviceConfig, #[device_config(flatten)]
mixer: String, pub mqtt: MqttDeviceConfig,
speakers: String, #[device_config(from_lua)]
pub mixer: WrappedDevice,
#[device_config(from_lua)]
pub speakers: WrappedDevice,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
#[derive(Debug, LuaDevice)]
pub struct AudioSetup {
config: AudioSetupConfig,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for AudioSetupConfig { impl LuaDeviceCreate for AudioSetup {
async fn create( type Config = AudioSetupConfig;
self, type Error = DeviceConfigError;
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up AudioSetup");
// TODO: Make sure they implement OnOff? async fn create(config: Self::Config) -> Result<Self, Self::Error> {
let mixer = ext trace!(id = config.identifier, "Setting up AudioSetup");
.device_manager
.get(&self.mixer)
.await
// NOTE: We need to clone to make the compiler happy, how ever if this clone happens the next one can never happen...
.ok_or(DeviceConfigError::MissingChild(
identifier.into(),
self.mixer.clone(),
))?;
if !As::<dyn OnOff>::is(mixer.read().await.as_ref()) { let mixer_id = config.mixer.read().await.get_id().to_owned();
return Err(DeviceConfigError::MissingTrait(self.mixer, "OnOff".into())); if !As::<dyn OnOff>::is(config.mixer.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into()));
} }
let speakers = let speakers_id = config.speakers.read().await.get_id().to_owned();
ext.device_manager if !As::<dyn OnOff>::is(config.speakers.read().await.as_ref()) {
.get(&self.speakers) return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
.await
.ok_or(DeviceConfigError::MissingChild(
identifier.into(),
self.speakers.clone(),
))?;
if !As::<dyn OnOff>::is(speakers.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait(
self.speakers,
"OnOff".into(),
));
} }
let device = AudioSetup { config
identifier: identifier.into(), .client
mqtt: self.mqtt, .subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
mixer, .await?;
speakers,
};
Ok(Box::new(device)) Ok(AudioSetup { config })
} }
} }
// TODO: We need a better way to store the children devices
#[derive(Debug)]
struct AudioSetup {
identifier: String,
mqtt: MqttDeviceConfig,
mixer: WrappedDevice,
speakers: WrappedDevice,
}
impl Device for AudioSetup { impl Device for AudioSetup {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.identifier.clone()
} }
} }
#[async_trait] #[async_trait]
impl OnMqtt for AudioSetup { impl OnMqtt for AudioSetup {
fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic]
}
async fn on_mqtt(&mut self, message: rumqttc::Publish) { async fn on_mqtt(&mut self, message: rumqttc::Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let action = match RemoteMessage::try_from(message) { let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(), Ok(message) => message.action(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
return; return;
} }
}; };
let mut mixer = self.mixer.write().await; let mut mixer = self.config.mixer.write().await;
let mut speakers = self.speakers.write().await; let mut speakers = self.config.speakers.write().await;
if let (Some(mixer), Some(speakers)) = ( if let (Some(mixer), Some(speakers)) = (
As::<dyn OnOff>::cast_mut(mixer.as_mut()), As::<dyn OnOff>::cast_mut(mixer.as_mut()),
As::<dyn OnOff>::cast_mut(speakers.as_mut()), As::<dyn OnOff>::cast_mut(speakers.as_mut()),
@ -135,8 +116,8 @@ impl OnMqtt for AudioSetup {
#[async_trait] #[async_trait]
impl OnPresence for AudioSetup { impl OnPresence for AudioSetup {
async fn on_presence(&mut self, presence: bool) { async fn on_presence(&mut self, presence: bool) {
let mut mixer = self.mixer.write().await; let mut mixer = self.config.mixer.write().await;
let mut speakers = self.speakers.write().await; let mut speakers = self.config.speakers.write().await;
if let (Some(mixer), Some(speakers)) = ( if let (Some(mixer), Some(speakers)) = (
As::<dyn OnOff>::cast_mut(mixer.as_mut()), As::<dyn OnOff>::cast_mut(mixer.as_mut()),
@ -144,7 +125,7 @@ impl OnPresence for AudioSetup {
) { ) {
// Turn off the audio setup when we leave the house // Turn off the audio setup when we leave the house
if !presence { if !presence {
debug!(id = self.identifier, "Turning devices off"); debug!(id = self.config.identifier, "Turning devices off");
speakers.set_on(false).await.unwrap(); speakers.set_on(false).await.unwrap();
mixer.set_on(false).await.unwrap(); mixer.set_on(false).await.unwrap();
} }

View File

@ -1,130 +1,116 @@
use std::time::Duration; use std::time::Duration;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::traits::OnOff; use google_home::traits::OnOff;
use rumqttc::AsyncClient; use mlua::FromLua;
use serde::Deserialize;
use serde_with::{serde_as, DurationSeconds};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::Device; use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig, WrappedDevice}; use crate::device_manager::WrappedDevice;
use crate::devices::{As, DEFAULT_PRESENCE}; use crate::devices::{As, DEFAULT_PRESENCE};
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::messages::{ContactMessage, PresenceMessage}; use crate::messages::{ContactMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout; use crate::traits::Timeout;
// NOTE: If we add more presence devices we might need to move this out of here // NOTE: If we add more presence devices we might need to move this out of here
#[serde_as] #[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, Deserialize)]
pub struct PresenceDeviceConfig { pub struct PresenceDeviceConfig {
#[serde(flatten)] #[device_config(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[serde_as(as = "DurationSeconds")] #[device_config(with(Duration::from_secs))]
pub timeout: Duration, pub timeout: Duration,
} }
#[serde_as] #[derive(Debug, Clone)]
#[derive(Debug, Clone, Deserialize)] struct TriggerDevicesHelper(Vec<WrappedDevice>);
pub struct TriggerConfig {
devices: Vec<String>,
#[serde(default)]
#[serde_as(as = "DurationSeconds")]
pub timeout: Duration,
}
#[derive(Debug, Clone, Deserialize)] impl<'lua> FromLua<'lua> for TriggerDevicesHelper {
pub struct ContactSensorConfig { fn from_lua(value: mlua::Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result<Self> {
#[serde(flatten)] Ok(TriggerDevicesHelper(mlua::FromLua::from_lua(value, lua)?))
mqtt: MqttDeviceConfig,
presence: Option<PresenceDeviceConfig>,
trigger: Option<TriggerConfig>,
}
#[async_trait]
impl DeviceConfig for ContactSensorConfig {
async fn create(
self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up ContactSensor");
let trigger = if let Some(trigger_config) = &self.trigger {
let mut devices = Vec::new();
for device_name in &trigger_config.devices {
let device = ext.device_manager.get(device_name).await.ok_or(
DeviceConfigError::MissingChild(device_name.into(), "OnOff".into()),
)?;
if !As::<dyn OnOff>::is(device.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait(
device_name.into(),
"OnOff".into(),
));
}
if !trigger_config.timeout.is_zero()
&& !As::<dyn Timeout>::is(device.read().await.as_ref())
{
return Err(DeviceConfigError::MissingTrait(
device_name.into(),
"Timeout".into(),
));
}
devices.push((device, false));
}
Some(Trigger {
devices,
timeout: trigger_config.timeout,
})
} else {
None
};
let device = ContactSensor {
identifier: identifier.into(),
mqtt: self.mqtt,
presence: self.presence,
client: ext.client.clone(),
overall_presence: DEFAULT_PRESENCE,
is_closed: true,
handle: None,
trigger,
};
Ok(Box::new(device))
} }
} }
#[derive(Debug)] impl From<TriggerDevicesHelper> for Vec<(WrappedDevice, bool)> {
struct Trigger { fn from(value: TriggerDevicesHelper) -> Self {
devices: Vec<(WrappedDevice, bool)>, value.0.into_iter().map(|device| (device, false)).collect()
timeout: Duration, // Timeout in seconds }
} }
#[derive(Debug)] #[derive(Debug, Clone, LuaDeviceConfig)]
struct ContactSensor { pub struct TriggerConfig {
identifier: String, #[device_config(from_lua, from(TriggerDevicesHelper))]
mqtt: MqttDeviceConfig, pub devices: Vec<(WrappedDevice, bool)>,
presence: Option<PresenceDeviceConfig>, #[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
pub timeout: Option<Duration>,
}
#[derive(Debug, Clone, LuaDeviceConfig)]
pub struct ContactSensorConfig {
pub identifier: String,
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
pub presence: Option<PresenceDeviceConfig>,
#[device_config(from_lua)]
pub trigger: Option<TriggerConfig>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
#[derive(Debug, LuaDevice)]
pub struct ContactSensor {
config: ContactSensorConfig,
client: AsyncClient,
overall_presence: bool, overall_presence: bool,
is_closed: bool, is_closed: bool,
handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
}
trigger: Option<Trigger>, #[async_trait]
impl LuaDeviceCreate for ContactSensor {
type Config = ContactSensorConfig;
type Error = DeviceConfigError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up ContactSensor");
// Make sure the devices implement the required traits
if let Some(trigger) = &config.trigger {
for (device, _) in &trigger.devices {
let id = device.read().await.get_id().to_owned();
if !As::<dyn OnOff>::is(device.read().await.as_ref()) {
return Err(DeviceConfigError::MissingTrait(id, "OnOff".into()));
}
if trigger.timeout.is_none() && !As::<dyn Timeout>::is(device.read().await.as_ref())
{
return Err(DeviceConfigError::MissingTrait(id, "Timeout".into()));
}
}
}
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config: config.clone(),
overall_presence: DEFAULT_PRESENCE,
is_closed: true,
handle: None,
})
}
} }
impl Device for ContactSensor { impl Device for ContactSensor {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.identifier.clone()
} }
} }
@ -137,15 +123,18 @@ impl OnPresence for ContactSensor {
#[async_trait] #[async_trait]
impl OnMqtt for ContactSensor { impl OnMqtt for ContactSensor {
fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic]
}
async fn on_mqtt(&mut self, message: rumqttc::Publish) { async fn on_mqtt(&mut self, message: rumqttc::Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let is_closed = match ContactMessage::try_from(message) { let is_closed = match ContactMessage::try_from(message) {
Ok(state) => state.is_closed(), Ok(state) => state.is_closed(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
return; return;
} }
}; };
@ -154,10 +143,10 @@ impl OnMqtt for ContactSensor {
return; return;
} }
debug!(id = self.identifier, "Updating state to {is_closed}"); debug!(id = self.config.identifier, "Updating state to {is_closed}");
self.is_closed = is_closed; self.is_closed = is_closed;
if let Some(trigger) = &mut self.trigger { if let Some(trigger) = &mut self.config.trigger {
if !self.is_closed { if !self.is_closed {
for (light, previous) in &mut trigger.devices { for (light, previous) in &mut trigger.devices {
let mut light = light.write().await; let mut light = light.write().await;
@ -171,12 +160,14 @@ impl OnMqtt for ContactSensor {
let mut light = light.write().await; let mut light = light.write().await;
if !previous { if !previous {
// If the timeout is zero just turn the light off directly // If the timeout is zero just turn the light off directly
if trigger.timeout.is_zero() if trigger.timeout.is_none()
&& let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) && let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut())
{ {
light.set_on(false).await.ok(); light.set_on(false).await.ok();
} else if let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut()) { } else if let Some(timeout) = trigger.timeout
light.start_timeout(trigger.timeout).await.unwrap(); && let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut())
{
light.start_timeout(timeout).await.unwrap();
} }
// TODO: Put a warning/error on creation if either of this has to option to fail // TODO: Put a warning/error on creation if either of this has to option to fail
} }
@ -186,7 +177,7 @@ impl OnMqtt for ContactSensor {
// Check if this contact sensor works as a presence device // Check if this contact sensor works as a presence device
// If not we are done here // If not we are done here
let presence = match &self.presence { let presence = match &self.config.presence {
Some(presence) => presence, Some(presence) => presence,
None => return, None => return,
}; };
@ -201,9 +192,10 @@ impl OnMqtt for ContactSensor {
// This is to prevent the house from being marked as present for however long the // This is to prevent the house from being marked as present for however long the
// timeout is set when leaving the house // timeout is set when leaving the house
if !self.overall_presence { if !self.overall_presence {
self.client self.config
.client
.publish( .publish(
presence.mqtt.topic.clone(), &presence.mqtt.topic,
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
false, false,
serde_json::to_string(&PresenceMessage::new(true)).unwrap(), serde_json::to_string(&PresenceMessage::new(true)).unwrap(),
@ -219,8 +211,8 @@ impl OnMqtt for ContactSensor {
} }
} else { } else {
// Once the door is closed again we start a timeout for removing the presence // Once the door is closed again we start a timeout for removing the presence
let client = self.client.clone(); let client = self.config.client.clone();
let id = self.identifier.clone(); let id = self.config.identifier.clone();
let timeout = presence.timeout; let timeout = presence.timeout;
let topic = presence.mqtt.topic.clone(); let topic = presence.mqtt.topic.clone();
self.handle = Some(tokio::spawn(async move { self.handle = Some(tokio::spawn(async move {
@ -228,7 +220,7 @@ impl OnMqtt for ContactSensor {
tokio::time::sleep(timeout).await; tokio::time::sleep(timeout).await;
debug!(id, "Removing door device!"); debug!(id, "Removing door device!");
client client
.publish(topic.clone(), rumqttc::QoS::AtLeastOnce, false, "") .publish(&topic, rumqttc::QoS::AtLeastOnce, false, "")
.await .await
.map_err(|err| warn!("Failed to publish presence on {topic}: {err}")) .map_err(|err| warn!("Failed to publish presence on {topic}: {err}"))
.ok(); .ok();

View File

@ -1,48 +1,44 @@
use async_trait::async_trait; use std::convert::Infallible;
use rumqttc::AsyncClient;
use serde::Deserialize;
use tracing::warn;
use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use tracing::{trace, warn};
use super::LuaDeviceCreate;
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnDarkness, OnPresence}; use crate::event::{OnDarkness, OnPresence};
use crate::messages::{DarknessMessage, PresenceMessage}; use crate::messages::{DarknessMessage, PresenceMessage};
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Deserialize)] #[derive(Debug, LuaDeviceConfig, Clone)]
pub struct DebugBridgeConfig { pub struct DebugBridgeConfig {
#[serde(flatten)] pub identifier: String,
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
}
#[derive(Debug, LuaDevice)]
pub struct DebugBridge {
config: DebugBridgeConfig,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for DebugBridgeConfig { impl LuaDeviceCreate for DebugBridge {
async fn create( type Config = DebugBridgeConfig;
self, type Error = Infallible;
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = DebugBridge {
identifier: identifier.into(),
mqtt: self.mqtt,
client: ext.client.clone(),
};
Ok(Box::new(device)) async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up DebugBridge");
Ok(Self { config })
} }
} }
#[derive(Debug)]
pub struct DebugBridge {
identifier: String,
mqtt: MqttDeviceConfig,
client: AsyncClient,
}
impl Device for DebugBridge { impl Device for DebugBridge {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.identifier.clone()
} }
} }
@ -50,8 +46,9 @@ impl Device for DebugBridge {
impl OnPresence for DebugBridge { impl OnPresence for DebugBridge {
async fn on_presence(&mut self, presence: bool) { async fn on_presence(&mut self, presence: bool) {
let message = PresenceMessage::new(presence); let message = PresenceMessage::new(presence);
let topic = format!("{}/presence", self.mqtt.topic); let topic = format!("{}/presence", self.config.mqtt.topic);
self.client self.config
.client
.publish( .publish(
topic, topic,
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
@ -62,7 +59,7 @@ impl OnPresence for DebugBridge {
.map_err(|err| { .map_err(|err| {
warn!( warn!(
"Failed to update presence on {}/presence: {err}", "Failed to update presence on {}/presence: {err}",
self.mqtt.topic self.config.mqtt.topic
) )
}) })
.ok(); .ok();
@ -73,8 +70,9 @@ impl OnPresence for DebugBridge {
impl OnDarkness for DebugBridge { impl OnDarkness for DebugBridge {
async fn on_darkness(&mut self, dark: bool) { async fn on_darkness(&mut self, dark: bool) {
let message = DarknessMessage::new(dark); let message = DarknessMessage::new(dark);
let topic = format!("{}/darkness", self.mqtt.topic); let topic = format!("{}/darkness", self.config.mqtt.topic);
self.client self.config
.client
.publish( .publish(
topic, topic,
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
@ -85,7 +83,7 @@ impl OnDarkness for DebugBridge {
.map_err(|err| { .map_err(|err| {
warn!( warn!(
"Failed to update presence on {}/presence: {err}", "Failed to update presence on {}/presence: {err}",
self.mqtt.topic self.config.mqtt.topic
) )
}) })
.ok(); .ok();

View File

@ -1,12 +1,13 @@
use std::net::{Ipv4Addr, SocketAddr}; use std::convert::Infallible;
use std::net::SocketAddr;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{error, trace, warn}; use tracing::{error, trace, warn};
use crate::device_manager::{ConfigExternal, DeviceConfig}; use super::LuaDeviceCreate;
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnDarkness, OnPresence}; use crate::event::{OnDarkness, OnPresence};
#[derive(Debug)] #[derive(Debug)]
@ -17,41 +18,22 @@ pub enum Flag {
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct FlagIDs { pub struct FlagIDs {
pub presence: isize, presence: isize,
pub darkness: isize, darkness: isize,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, LuaDeviceConfig, Clone)]
pub struct HueBridgeConfig { pub struct HueBridgeConfig {
pub ip: Ipv4Addr, pub identifier: String,
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
pub addr: SocketAddr,
pub login: String, pub login: String,
pub flags: FlagIDs, pub flags: FlagIDs,
} }
#[async_trait] #[derive(Debug, LuaDevice)]
impl DeviceConfig for HueBridgeConfig { pub struct HueBridge {
async fn create( config: HueBridgeConfig,
self,
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = HueBridge {
identifier: identifier.into(),
addr: (self.ip, 80).into(),
login: self.login,
flag_ids: self.flags,
};
Ok(Box::new(device))
}
}
#[derive(Debug)]
struct HueBridge {
identifier: String,
addr: SocketAddr,
login: String,
flag_ids: FlagIDs,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@ -59,16 +41,27 @@ struct FlagMessage {
flag: bool, flag: bool,
} }
#[async_trait]
impl LuaDeviceCreate for HueBridge {
type Config = HueBridgeConfig;
type Error = Infallible;
async fn create(config: Self::Config) -> Result<Self, Infallible> {
trace!(id = config.identifier, "Setting up HueBridge");
Ok(Self { config })
}
}
impl HueBridge { impl HueBridge {
pub async fn set_flag(&self, flag: Flag, value: bool) { pub async fn set_flag(&self, flag: Flag, value: bool) {
let flag_id = match flag { let flag_id = match flag {
Flag::Presence => self.flag_ids.presence, Flag::Presence => self.config.flags.presence,
Flag::Darkness => self.flag_ids.darkness, Flag::Darkness => self.config.flags.darkness,
}; };
let url = format!( let url = format!(
"http://{}/api/{}/sensors/{flag_id}/state", "http://{}/api/{}/sensors/{flag_id}/state",
self.addr, self.login self.config.addr, self.config.login
); );
trace!(?flag, flag_id, value, "Sending request to change flag"); trace!(?flag, flag_id, value, "Sending request to change flag");
@ -93,8 +86,8 @@ impl HueBridge {
} }
impl Device for HueBridge { impl Device for HueBridge {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.identifier.clone()
} }
} }

View File

@ -1,104 +1,107 @@
use std::net::{Ipv4Addr, SocketAddr}; use std::net::SocketAddr;
use std::time::Duration; use std::time::Duration;
use anyhow::{anyhow, Context, Result}; use anyhow::{anyhow, Context, Result};
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::OnOff; use google_home::traits::OnOff;
use rumqttc::Publish; use rumqttc::{Publish, SubscribeFilter};
use serde::Deserialize; use tracing::{debug, error, trace, warn};
use tracing::{debug, error, warn};
use super::Device; use super::{Device, LuaDeviceCreate};
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::error::DeviceConfigError;
use crate::event::OnMqtt; use crate::event::OnMqtt;
use crate::messages::{RemoteAction, RemoteMessage}; use crate::messages::{RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout; use crate::traits::Timeout;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct HueGroupConfig { pub struct HueGroupConfig {
pub ip: Ipv4Addr, pub identifier: String,
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 80)))]
pub addr: SocketAddr,
pub login: String, pub login: String,
pub group_id: isize, pub group_id: isize,
pub timer_id: isize, pub timer_id: isize,
pub scene_id: String, pub scene_id: String,
#[serde(default)] #[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>, pub remotes: Vec<MqttDeviceConfig>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
} }
#[async_trait] #[derive(Debug, LuaDevice)]
impl DeviceConfig for HueGroupConfig { pub struct HueGroup {
async fn create( config: HueGroupConfig,
self,
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = HueGroup {
identifier: identifier.into(),
addr: (self.ip, 80).into(),
login: self.login,
group_id: self.group_id,
scene_id: self.scene_id,
timer_id: self.timer_id,
remotes: self.remotes,
};
Ok(Box::new(device))
}
}
#[derive(Debug)]
struct HueGroup {
identifier: String,
addr: SocketAddr,
login: String,
group_id: isize,
timer_id: isize,
scene_id: String,
remotes: Vec<MqttDeviceConfig>,
} }
// Couple of helper function to get the correct urls // Couple of helper function to get the correct urls
#[async_trait]
impl LuaDeviceCreate for HueGroup {
type Config = HueGroupConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up AudioSetup");
if !config.remotes.is_empty() {
config
.client
.subscribe_many(config.remotes.iter().map(|remote| SubscribeFilter {
path: remote.topic.clone(),
qos: rumqttc::QoS::AtLeastOnce,
}))
.await?;
}
Ok(Self { config })
}
}
impl HueGroup { impl HueGroup {
fn url_base(&self) -> String { fn url_base(&self) -> String {
format!("http://{}/api/{}", self.addr, self.login) format!("http://{}/api/{}", self.config.addr, self.config.login)
} }
fn url_set_schedule(&self) -> String { fn url_set_schedule(&self) -> String {
format!("{}/schedules/{}", self.url_base(), self.timer_id) format!("{}/schedules/{}", self.url_base(), self.config.timer_id)
} }
fn url_set_action(&self) -> String { fn url_set_action(&self) -> String {
format!("{}/groups/{}/action", self.url_base(), self.group_id) format!("{}/groups/{}/action", self.url_base(), self.config.group_id)
} }
fn url_get_state(&self) -> String { fn url_get_state(&self) -> String {
format!("{}/groups/{}", self.url_base(), self.group_id) format!("{}/groups/{}", self.url_base(), self.config.group_id)
} }
} }
impl Device for HueGroup { impl Device for HueGroup {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.identifier.clone()
} }
} }
#[async_trait] #[async_trait]
impl OnMqtt for HueGroup { impl OnMqtt for HueGroup {
fn topics(&self) -> Vec<&str> {
self.remotes
.iter()
.map(|mqtt| mqtt.topic.as_str())
.collect()
}
async fn on_mqtt(&mut self, message: Publish) { async fn on_mqtt(&mut self, message: Publish) {
if !self
.config
.remotes
.iter()
.any(|remote| rumqttc::matches(&message.topic, &remote.topic))
{
return;
}
let action = match RemoteMessage::try_from(message) { let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(), Ok(message) => message.action(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
return; return;
} }
}; };
@ -122,7 +125,7 @@ impl OnOff for HueGroup {
self.stop_timeout().await.unwrap(); self.stop_timeout().await.unwrap();
let message = if on { let message = if on {
message::Action::scene(self.scene_id.clone()) message::Action::scene(self.config.scene_id.clone())
} else { } else {
message::Action::on(false) message::Action::on(false)
}; };
@ -137,10 +140,13 @@ impl OnOff for HueGroup {
Ok(res) => { Ok(res) => {
let status = res.status(); let status = res.status();
if !status.is_success() { if !status.is_success() {
warn!(id = self.identifier, "Status code is not success: {status}"); warn!(
id = self.config.identifier,
"Status code is not success: {status}"
);
} }
} }
Err(err) => error!(id = self.identifier, "Error: {err}"), Err(err) => error!(id = self.config.identifier, "Error: {err}"),
} }
Ok(()) Ok(())
@ -156,13 +162,19 @@ impl OnOff for HueGroup {
Ok(res) => { Ok(res) => {
let status = res.status(); let status = res.status();
if !status.is_success() { if !status.is_success() {
warn!(id = self.identifier, "Status code is not success: {status}"); warn!(
id = self.config.identifier,
"Status code is not success: {status}"
);
} }
let on = match res.json::<message::Info>().await { let on = match res.json::<message::Info>().await {
Ok(info) => info.any_on(), Ok(info) => info.any_on(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
// TODO: Error code // TODO: Error code
return Ok(false); return Ok(false);
} }
@ -170,7 +182,7 @@ impl OnOff for HueGroup {
return Ok(on); return Ok(on);
} }
Err(err) => error!(id = self.identifier, "Error: {err}"), Err(err) => error!(id = self.config.identifier, "Error: {err}"),
} }
Ok(false) Ok(false)

View File

@ -2,22 +2,22 @@ use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{self, OnOff}; use google_home::traits::{self, OnOff};
use google_home::types::Type; use google_home::types::Type;
use google_home::{device, GoogleHomeDevice}; use google_home::{device, GoogleHomeDevice};
use rumqttc::{matches, AsyncClient, Publish}; use rumqttc::{matches, Publish, SubscribeFilter};
use serde::Deserialize; use serde::Deserialize;
use serde_with::{serde_as, DurationSeconds};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use super::LuaDeviceCreate;
use crate::config::{InfoConfig, MqttDeviceConfig}; use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage}; use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout; use crate::traits::Timeout;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
@ -28,77 +28,39 @@ pub enum OutletType {
Light, Light,
} }
#[serde_as] #[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, Deserialize)]
pub struct IkeaOutletConfig { pub struct IkeaOutletConfig {
#[serde(flatten)] #[device_config(flatten)]
info: InfoConfig, pub info: InfoConfig,
#[serde(flatten)] #[device_config(flatten)]
mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[serde(default = "default_outlet_type")] #[device_config(default(OutletType::Outlet))]
outlet_type: OutletType, pub outlet_type: OutletType,
#[serde_as(as = "Option<DurationSeconds>")] #[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
timeout: Option<Duration>, // Timeout in seconds pub timeout: Option<Duration>,
#[serde(default)] #[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>, pub remotes: Vec<MqttDeviceConfig>,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
} }
fn default_outlet_type() -> OutletType { #[derive(Debug, LuaDevice)]
OutletType::Outlet pub struct IkeaOutlet {
} config: IkeaOutletConfig,
#[async_trait]
impl DeviceConfig for IkeaOutletConfig {
async fn create(
self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(
id = identifier,
name = self.info.name,
room = self.info.room,
"Setting up IkeaOutlet"
);
let device = IkeaOutlet {
identifier: identifier.into(),
info: self.info,
mqtt: self.mqtt,
outlet_type: self.outlet_type,
timeout: self.timeout,
remotes: self.remotes,
client: ext.client.clone(),
last_known_state: false,
handle: None,
};
Ok(Box::new(device))
}
}
#[derive(Debug)]
struct IkeaOutlet {
identifier: String,
info: InfoConfig,
mqtt: MqttDeviceConfig,
outlet_type: OutletType,
timeout: Option<Duration>,
remotes: Vec<MqttDeviceConfig>,
client: AsyncClient,
last_known_state: bool, last_known_state: bool,
handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
} }
async fn set_on(client: AsyncClient, topic: &str, on: bool) { async fn set_on(client: WrappedAsyncClient, topic: &str, on: bool) {
let message = OnOffMessage::new(on); let message = OnOffMessage::new(on);
let topic = format!("{}/set", topic); let topic = format!("{}/set", topic);
// TODO: Handle potential errors here // TODO: Handle potential errors here
client client
.publish( .publish(
topic.clone(), &topic,
rumqttc::QoS::AtLeastOnce, rumqttc::QoS::AtLeastOnce,
false, false,
serde_json::to_string(&message).unwrap(), serde_json::to_string(&message).unwrap(),
@ -108,34 +70,53 @@ async fn set_on(client: AsyncClient, topic: &str, on: bool) {
.ok(); .ok();
} }
#[async_trait]
impl LuaDeviceCreate for IkeaOutlet {
type Config = IkeaOutletConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.info.identifier(), "Setting up IkeaOutlet");
if !config.remotes.is_empty() {
config
.client
.subscribe_many(config.remotes.iter().map(|remote| SubscribeFilter {
path: remote.topic.clone(),
qos: rumqttc::QoS::AtLeastOnce,
}))
.await?;
}
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config,
last_known_state: false,
handle: None,
})
}
}
impl Device for IkeaOutlet { impl Device for IkeaOutlet {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.info.identifier()
} }
} }
#[async_trait] #[async_trait]
impl OnMqtt for IkeaOutlet { impl OnMqtt for IkeaOutlet {
fn topics(&self) -> Vec<&str> {
let mut topics: Vec<_> = self
.remotes
.iter()
.map(|mqtt| mqtt.topic.as_str())
.collect();
topics.push(&self.mqtt.topic);
topics
}
async fn on_mqtt(&mut self, message: Publish) { async fn on_mqtt(&mut self, message: Publish) {
// Check if the message is from the deviec itself or from a remote // Check if the message is from the deviec itself or from a remote
if matches(&message.topic, &self.mqtt.topic) { if matches(&message.topic, &self.config.mqtt.topic) {
// Update the internal state based on what the device has reported // Update the internal state based on what the device has reported
let state = match OnOffMessage::try_from(message) { let state = match OnOffMessage::try_from(message) {
Ok(state) => state.state(), Ok(state) => state.state(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(id = Device::get_id(self), "Failed to parse message: {err}");
return; return;
} }
}; };
@ -148,18 +129,23 @@ impl OnMqtt for IkeaOutlet {
// Abort any timer that is currently running // Abort any timer that is currently running
self.stop_timeout().await.unwrap(); self.stop_timeout().await.unwrap();
debug!(id = self.identifier, "Updating state to {state}"); debug!(id = Device::get_id(self), "Updating state to {state}");
self.last_known_state = state; self.last_known_state = state;
// If this is a kettle start a timeout for turning it of again // If this is a kettle start a timeout for turning it of again
if state && let Some(timeout) = self.timeout { if state && let Some(timeout) = self.config.timeout {
self.start_timeout(timeout).await.unwrap(); self.start_timeout(timeout).await.unwrap();
} }
} else { } else if self
.config
.remotes
.iter()
.any(|remote| rumqttc::matches(&message.topic, &remote.topic))
{
let action = match RemoteMessage::try_from(message) { let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(), Ok(message) => message.action(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(id = Device::get_id(self), "Failed to parse message: {err}");
return; return;
} }
}; };
@ -178,8 +164,8 @@ impl OnMqtt for IkeaOutlet {
impl OnPresence for IkeaOutlet { impl OnPresence for IkeaOutlet {
async fn on_presence(&mut self, presence: bool) { async fn on_presence(&mut self, presence: bool) {
// Turn off the outlet when we leave the house (Not if it is a battery charger) // Turn off the outlet when we leave the house (Not if it is a battery charger)
if !presence && self.outlet_type != OutletType::Charger { if !presence && self.config.outlet_type != OutletType::Charger {
debug!(id = self.identifier, "Turning device off"); debug!(id = Device::get_id(self), "Turning device off");
self.set_on(false).await.ok(); self.set_on(false).await.ok();
} }
} }
@ -187,7 +173,7 @@ impl OnPresence for IkeaOutlet {
impl GoogleHomeDevice for IkeaOutlet { impl GoogleHomeDevice for IkeaOutlet {
fn get_device_type(&self) -> Type { fn get_device_type(&self) -> Type {
match self.outlet_type { match self.config.outlet_type {
OutletType::Outlet => Type::Outlet, OutletType::Outlet => Type::Outlet,
OutletType::Kettle => Type::Kettle, OutletType::Kettle => Type::Kettle,
OutletType::Light => Type::Light, // Find a better device type for this, ideally would like to use charger, but that needs more work OutletType::Light => Type::Light, // Find a better device type for this, ideally would like to use charger, but that needs more work
@ -196,10 +182,10 @@ impl GoogleHomeDevice for IkeaOutlet {
} }
fn get_device_name(&self) -> device::Name { fn get_device_name(&self) -> device::Name {
device::Name::new(&self.info.name) device::Name::new(&self.config.info.name)
} }
fn get_id(&self) -> &str { fn get_id(&self) -> String {
Device::get_id(self) Device::get_id(self)
} }
@ -208,7 +194,7 @@ impl GoogleHomeDevice for IkeaOutlet {
} }
fn get_room_hint(&self) -> Option<&str> { fn get_room_hint(&self) -> Option<&str> {
self.info.room.as_deref() self.config.info.room.as_deref()
} }
fn will_report_state(&self) -> bool { fn will_report_state(&self) -> bool {
@ -224,7 +210,7 @@ impl traits::OnOff for IkeaOutlet {
} }
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
set_on(self.client.clone(), &self.mqtt.topic, on).await; set_on(self.config.client.clone(), &self.config.mqtt.topic, on).await;
Ok(()) Ok(())
} }
@ -239,15 +225,15 @@ impl crate::traits::Timeout for IkeaOutlet {
// Turn the kettle of after the specified timeout // Turn the kettle of after the specified timeout
// TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet // TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet
// get dropped // get dropped
let client = self.client.clone(); let client = self.config.client.clone();
let topic = self.mqtt.topic.clone(); let topic = self.config.mqtt.topic.clone();
let id = self.identifier.clone(); let id = Device::get_id(self).clone();
self.handle = Some(tokio::spawn(async move { self.handle = Some(tokio::spawn(async move {
debug!(id, "Starting timeout ({timeout:?})..."); debug!(id, "Starting timeout ({timeout:?})...");
tokio::time::sleep(timeout).await; tokio::time::sleep(timeout).await;
debug!(id, "Turning outlet off!"); debug!(id, "Turning outlet off!");
// TODO: Idealy we would call self.set_on(false), however since we want to do // TODO: Idealy we would call self.set_on(false), however since we want to do
// it after a timeout we have to put it in a seperate task. // it after a timeout we have to put it in a separate task.
// I don't think we can really get around calling outside function // I don't think we can really get around calling outside function
set_on(client, &topic, false).await; set_on(client, &topic, false).await;
})); }));

View File

@ -1,7 +1,9 @@
use std::net::{Ipv4Addr, SocketAddr}; use std::convert::Infallible;
use std::net::SocketAddr;
use std::str::Utf8Error; use std::str::Utf8Error;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use bytes::{Buf, BufMut}; use bytes::{Buf, BufMut};
use google_home::errors::{self, DeviceError}; use google_home::errors::{self, DeviceError};
use google_home::traits; use google_home::traits;
@ -11,42 +13,34 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream; use tokio::net::TcpStream;
use tracing::trace; use tracing::trace;
use super::Device; use super::{Device, LuaDeviceCreate};
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::error::DeviceConfigError;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct KasaOutletConfig { pub struct KasaOutletConfig {
ip: Ipv4Addr, pub identifier: String,
#[device_config(rename("ip"), with(|ip| SocketAddr::new(ip, 9999)))]
pub addr: SocketAddr,
}
#[derive(Debug, LuaDevice)]
pub struct KasaOutlet {
config: KasaOutletConfig,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for KasaOutletConfig { impl LuaDeviceCreate for KasaOutlet {
async fn create( type Config = KasaOutletConfig;
self, type Error = Infallible;
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(id = identifier, "Setting up KasaOutlet");
let device = KasaOutlet { async fn create(config: Self::Config) -> Result<Self, Self::Error> {
identifier: identifier.into(), trace!(id = config.identifier, "Setting up KasaOutlet");
addr: (self.ip, 9999).into(), Ok(Self { config })
};
Ok(Box::new(device))
} }
} }
#[derive(Debug)]
struct KasaOutlet {
identifier: String,
addr: SocketAddr,
}
impl Device for KasaOutlet { impl Device for KasaOutlet {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.identifier.clone()
} }
} }
@ -214,7 +208,7 @@ impl Response {
#[async_trait] #[async_trait]
impl traits::OnOff for KasaOutlet { impl traits::OnOff for KasaOutlet {
async fn is_on(&self) -> Result<bool, errors::ErrorCode> { async fn is_on(&self) -> Result<bool, errors::ErrorCode> {
let mut stream = TcpStream::connect(self.addr) let mut stream = TcpStream::connect(self.config.addr)
.await .await
.or::<DeviceError>(Err(DeviceError::DeviceOffline))?; .or::<DeviceError>(Err(DeviceError::DeviceOffline))?;
@ -248,7 +242,7 @@ impl traits::OnOff for KasaOutlet {
} }
async fn set_on(&mut self, on: bool) -> Result<(), errors::ErrorCode> { async fn set_on(&mut self, on: bool) -> Result<(), errors::ErrorCode> {
let mut stream = TcpStream::connect(self.addr) let mut stream = TcpStream::connect(self.config.addr)
.await .await
.or::<DeviceError>(Err(DeviceError::DeviceOffline))?; .or::<DeviceError>(Err(DeviceError::DeviceOffline))?;

View File

@ -1,70 +1,70 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use rumqttc::Publish; use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, trace, warn}; use tracing::{debug, trace, warn};
use super::LuaDeviceCreate;
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError; use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::event::{self, Event, OnMqtt};
use crate::messages::BrightnessMessage; use crate::messages::BrightnessMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct LightSensorConfig { pub struct LightSensorConfig {
#[serde(flatten)] pub identifier: String,
#[device_config(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
pub min: isize, pub min: isize,
pub max: isize, pub max: isize,
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
} }
pub const DEFAULT: bool = false; const DEFAULT: bool = false;
// TODO: The light sensor should get a list of devices that it should inform #[derive(Debug, LuaDevice)]
#[async_trait]
impl DeviceConfig for LightSensorConfig {
async fn create(
self,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = LightSensor {
identifier: identifier.into(),
tx: ext.event_channel.get_tx(),
mqtt: self.mqtt,
min: self.min,
max: self.max,
is_dark: DEFAULT,
};
Ok(Box::new(device))
}
}
#[derive(Debug)]
pub struct LightSensor { pub struct LightSensor {
identifier: String, config: LightSensorConfig,
tx: event::Sender,
mqtt: MqttDeviceConfig,
min: isize,
max: isize,
is_dark: bool, is_dark: bool,
} }
#[async_trait]
impl LuaDeviceCreate for LightSensor {
type Config = LightSensorConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up LightSensor");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config,
is_dark: DEFAULT,
})
}
}
impl Device for LightSensor { impl Device for LightSensor {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.identifier.clone()
} }
} }
#[async_trait] #[async_trait]
impl OnMqtt for LightSensor { impl OnMqtt for LightSensor {
fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) { async fn on_mqtt(&mut self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let illuminance = match BrightnessMessage::try_from(message) { let illuminance = match BrightnessMessage::try_from(message) {
Ok(state) => state.illuminance(), Ok(state) => state.illuminance(),
Err(err) => { Err(err) => {
@ -74,17 +74,17 @@ impl OnMqtt for LightSensor {
}; };
debug!("Illuminance: {illuminance}"); debug!("Illuminance: {illuminance}");
let is_dark = if illuminance <= self.min { let is_dark = if illuminance <= self.config.min {
trace!("It is dark"); trace!("It is dark");
true true
} else if illuminance >= self.max { } else if illuminance >= self.config.max {
trace!("It is light"); trace!("It is light");
false false
} else { } else {
trace!( trace!(
"In between min ({}) and max ({}) value, keeping current state: {}", "In between min ({}) and max ({}) value, keeping current state: {}",
self.min, self.config.min,
self.max, self.config.max,
self.is_dark self.is_dark
); );
self.is_dark self.is_dark
@ -94,7 +94,7 @@ impl OnMqtt for LightSensor {
debug!("Dark state has changed: {is_dark}"); debug!("Dark state has changed: {is_dark}");
self.is_dark = is_dark; self.is_dark = is_dark;
if self.tx.send(Event::Darkness(is_dark)).await.is_err() { if self.config.tx.send(Event::Darkness(is_dark)).await.is_err() {
warn!("There are no receivers on the event channel"); warn!("There are no receivers on the event channel");
} }
} }

View File

@ -3,7 +3,7 @@ mod audio_setup;
mod contact_sensor; mod contact_sensor;
mod debug_bridge; mod debug_bridge;
mod hue_bridge; mod hue_bridge;
mod hue_light; mod hue_group;
mod ikea_outlet; mod ikea_outlet;
mod kasa_outlet; mod kasa_outlet;
mod light_sensor; mod light_sensor;
@ -12,26 +12,55 @@ mod presence;
mod wake_on_lan; mod wake_on_lan;
mod washer; mod washer;
use async_trait::async_trait;
use google_home::device::AsGoogleHomeDevice; use google_home::device::AsGoogleHomeDevice;
use google_home::traits::OnOff; use google_home::traits::OnOff;
pub use self::air_filter::AirFilterConfig; pub use self::air_filter::*;
pub use self::audio_setup::AudioSetupConfig; pub use self::audio_setup::*;
pub use self::contact_sensor::ContactSensorConfig; pub use self::contact_sensor::*;
pub use self::debug_bridge::DebugBridgeConfig; pub use self::debug_bridge::*;
pub use self::hue_bridge::HueBridgeConfig; pub use self::hue_bridge::*;
pub use self::hue_light::HueGroupConfig; pub use self::hue_group::*;
pub use self::ikea_outlet::IkeaOutletConfig; pub use self::ikea_outlet::*;
pub use self::kasa_outlet::KasaOutletConfig; pub use self::kasa_outlet::*;
pub use self::light_sensor::{LightSensor, LightSensorConfig}; pub use self::light_sensor::*;
pub use self::ntfy::{Notification, Ntfy}; pub use self::ntfy::{Notification, Ntfy};
pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE}; pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE};
pub use self::wake_on_lan::WakeOnLANConfig; pub use self::wake_on_lan::*;
pub use self::washer::WasherConfig; pub use self::washer::*;
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence}; use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
use crate::traits::Timeout; use crate::traits::Timeout;
#[async_trait]
pub trait LuaDeviceCreate {
type Config;
type Error;
async fn create(config: Self::Config) -> Result<Self, Self::Error>
where
Self: Sized;
}
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
AirFilter::register_with_lua(lua)?;
AudioSetup::register_with_lua(lua)?;
ContactSensor::register_with_lua(lua)?;
DebugBridge::register_with_lua(lua)?;
HueBridge::register_with_lua(lua)?;
HueGroup::register_with_lua(lua)?;
IkeaOutlet::register_with_lua(lua)?;
KasaOutlet::register_with_lua(lua)?;
LightSensor::register_with_lua(lua)?;
Ntfy::register_with_lua(lua)?;
Presence::register_with_lua(lua)?;
WakeOnLAN::register_with_lua(lua)?;
Washer::register_with_lua(lua)?;
Ok(())
}
#[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + OnNotification + OnOff + Timeout)] #[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + OnNotification + OnOff + Timeout)]
pub trait Device: AsGoogleHomeDevice + std::fmt::Debug + Sync + Send { pub trait Device: AsGoogleHomeDevice + std::fmt::Debug + Sync + Send {
fn get_id(&self) -> &str; fn get_id(&self) -> String;
} }

View File

@ -1,21 +1,16 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::convert::Infallible;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use serde::Serialize; use serde::Serialize;
use serde_repr::*; use serde_repr::*;
use tracing::{debug, error, warn}; use tracing::{error, trace, warn};
use crate::config::NtfyConfig; use super::LuaDeviceCreate;
use crate::devices::Device; use crate::devices::Device;
use crate::event::{self, Event, EventChannel, OnNotification, OnPresence}; use crate::event::{self, Event, EventChannel, OnNotification, OnPresence};
#[derive(Debug)]
pub struct Ntfy {
base_url: String,
topic: String,
tx: event::Sender,
}
#[derive(Debug, Serialize_repr, Clone, Copy)] #[derive(Debug, Serialize_repr, Clone, Copy)]
#[repr(u8)] #[repr(u8)]
pub enum Priority { pub enum Priority {
@ -40,9 +35,9 @@ pub enum ActionType {
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
pub struct Action { pub struct Action {
#[serde(flatten)] #[serde(flatten)]
action: ActionType, pub action: ActionType,
label: String, pub label: String,
clear: Option<bool>, pub clear: Option<bool>,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -116,28 +111,50 @@ impl Default for Notification {
} }
} }
impl Ntfy { #[derive(Debug, LuaDeviceConfig)]
pub fn new(config: NtfyConfig, event_channel: &EventChannel) -> Self { pub struct NtfyConfig {
Self { #[device_config(default("https://ntfy.sh".into()))]
base_url: config.url, pub url: String,
topic: config.topic, pub topic: String,
tx: event_channel.get_tx(), #[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
} pub tx: event::Sender,
} }
#[derive(Debug, LuaDevice)]
pub struct Ntfy {
config: NtfyConfig,
}
#[async_trait]
impl LuaDeviceCreate for Ntfy {
type Config = NtfyConfig;
type Error = Infallible;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = "ntfy", "Setting up Ntfy");
Ok(Self { config })
}
}
impl Device for Ntfy {
fn get_id(&self) -> String {
"ntfy".to_string()
}
}
impl Ntfy {
async fn send(&self, notification: Notification) { async fn send(&self, notification: Notification) {
let notification = notification.finalize(&self.topic); let notification = notification.finalize(&self.config.topic);
debug!("Sending notfication");
// Create the request // Create the request
let res = reqwest::Client::new() let res = reqwest::Client::new()
.post(self.base_url.clone()) .post(self.config.url.clone())
.json(&notification) .json(&notification)
.send() .send()
.await; .await;
if let Err(err) = res { if let Err(err) = res {
error!("Something went wrong while sending the notifcation: {err}"); error!("Something went wrong while sending the notification: {err}");
} else if let Ok(res) = res { } else if let Ok(res) = res {
let status = res.status(); let status = res.status();
if !status.is_success() { if !status.is_success() {
@ -147,12 +164,6 @@ impl Ntfy {
} }
} }
impl Device for Ntfy {
fn get_id(&self) -> &str {
"ntfy"
}
}
#[async_trait] #[async_trait]
impl OnPresence for Ntfy { impl OnPresence for Ntfy {
async fn on_presence(&mut self, presence: bool) { async fn on_presence(&mut self, presence: bool) {
@ -177,7 +188,13 @@ impl OnPresence for Ntfy {
.add_action(action) .add_action(action)
.set_priority(Priority::Low); .set_priority(Priority::Low);
if self.tx.send(Event::Ntfy(notification)).await.is_err() { if self
.config
.tx
.send(Event::Ntfy(notification))
.await
.is_err()
{
warn!("There are no receivers on the event channel"); warn!("There are no receivers on the event channel");
} }
} }

View File

@ -1,60 +1,76 @@
use std::collections::HashMap; use std::collections::HashMap;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use rumqttc::Publish; use rumqttc::Publish;
use serde::Deserialize; use tracing::{debug, trace, warn};
use tracing::{debug, warn};
use super::LuaDeviceCreate;
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::devices::Device; use crate::devices::Device;
use crate::event::{self, Event, EventChannel, OnMqtt}; use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::PresenceMessage; use crate::messages::PresenceMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Deserialize)] #[derive(Debug, LuaDeviceConfig)]
pub struct PresenceConfig { pub struct PresenceConfig {
#[serde(flatten)] #[device_config(flatten)]
pub mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
#[device_config(from_lua, rename("event_channel"), with(|ec: EventChannel| ec.get_tx()))]
pub tx: event::Sender,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
} }
pub const DEFAULT_PRESENCE: bool = false; pub const DEFAULT_PRESENCE: bool = false;
#[derive(Debug)] #[derive(Debug, LuaDevice)]
pub struct Presence { pub struct Presence {
tx: event::Sender, config: PresenceConfig,
mqtt: MqttDeviceConfig,
devices: HashMap<String, bool>, devices: HashMap<String, bool>,
current_overall_presence: bool, current_overall_presence: bool,
} }
impl Presence { #[async_trait]
pub fn new(config: PresenceConfig, event_channel: &EventChannel) -> Self { impl LuaDeviceCreate for Presence {
Self { type Config = PresenceConfig;
tx: event_channel.get_tx(), type Error = rumqttc::ClientError;
mqtt: config.mqtt,
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = "ntfy", "Setting up Presence");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self {
config,
devices: HashMap::new(), devices: HashMap::new(),
current_overall_presence: DEFAULT_PRESENCE, current_overall_presence: DEFAULT_PRESENCE,
} })
} }
} }
impl Device for Presence { impl Device for Presence {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
"presence" "presence".to_string()
} }
} }
#[async_trait] #[async_trait]
impl OnMqtt for Presence { impl OnMqtt for Presence {
fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) { async fn on_mqtt(&mut self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let offset = self let offset = self
.config
.mqtt .mqtt
.topic .topic
.find('+') .find('+')
.or(self.mqtt.topic.find('#')) .or(self.config.mqtt.topic.find('#'))
.expect("Presence::create fails if it does not contain wildcards"); .expect("Presence::create fails if it does not contain wildcards");
let device_name = message.topic[offset..].into(); let device_name = message.topic[offset..].into();
@ -81,6 +97,7 @@ impl OnMqtt for Presence {
self.current_overall_presence = overall_presence; self.current_overall_presence = overall_presence;
if self if self
.config
.tx .tx
.send(Event::Presence(overall_presence)) .send(Event::Presence(overall_presence))
.await .await

View File

@ -1,89 +1,73 @@
use std::net::Ipv4Addr; use std::net::Ipv4Addr;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use eui48::MacAddress; use eui48::MacAddress;
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{self, Scene}; use google_home::traits::{self, Scene};
use google_home::types::Type; use google_home::types::Type;
use google_home::{device, GoogleHomeDevice}; use google_home::{device, GoogleHomeDevice};
use rumqttc::Publish; use rumqttc::Publish;
use serde::Deserialize;
use tracing::{debug, error, trace}; use tracing::{debug, error, trace};
use super::Device; use super::{Device, LuaDeviceCreate};
use crate::config::{InfoConfig, MqttDeviceConfig}; use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::error::DeviceConfigError;
use crate::event::OnMqtt; use crate::event::OnMqtt;
use crate::messages::ActivateMessage; use crate::messages::ActivateMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct WakeOnLANConfig { pub struct WakeOnLANConfig {
#[serde(flatten)] #[device_config(flatten)]
info: InfoConfig, pub info: InfoConfig,
#[serde(flatten)] #[device_config(flatten)]
mqtt: MqttDeviceConfig, pub mqtt: MqttDeviceConfig,
mac_address: MacAddress, pub mac_address: MacAddress,
#[serde(default = "default_broadcast_ip")] #[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))]
broadcast_ip: Ipv4Addr, pub broadcast_ip: Ipv4Addr,
#[device_config(from_lua)]
pub client: WrappedAsyncClient,
} }
fn default_broadcast_ip() -> Ipv4Addr { #[derive(Debug, LuaDevice)]
Ipv4Addr::new(255, 255, 255, 255) pub struct WakeOnLAN {
config: WakeOnLANConfig,
} }
#[async_trait] #[async_trait]
impl DeviceConfig for WakeOnLANConfig { impl LuaDeviceCreate for WakeOnLAN {
async fn create( type Config = WakeOnLANConfig;
self, type Error = rumqttc::ClientError;
identifier: &str,
_ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
trace!(
id = identifier,
name = self.info.name,
room = self.info.room,
"Setting up WakeOnLAN"
);
let device = WakeOnLAN { async fn create(config: Self::Config) -> Result<Self, Self::Error> {
identifier: identifier.into(), trace!(id = config.info.identifier(), "Setting up WakeOnLAN");
info: self.info,
mqtt: self.mqtt,
mac_address: self.mac_address,
broadcast_ip: self.broadcast_ip,
};
Ok(Box::new(device)) config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self { config })
} }
} }
#[derive(Debug)]
struct WakeOnLAN {
identifier: String,
info: InfoConfig,
mqtt: MqttDeviceConfig,
mac_address: MacAddress,
broadcast_ip: Ipv4Addr,
}
impl Device for WakeOnLAN { impl Device for WakeOnLAN {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.info.identifier()
} }
} }
#[async_trait] #[async_trait]
impl OnMqtt for WakeOnLAN { impl OnMqtt for WakeOnLAN {
fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) { async fn on_mqtt(&mut self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let activate = match ActivateMessage::try_from(message) { let activate = match ActivateMessage::try_from(message) {
Ok(message) => message.activate(), Ok(message) => message.activate(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(id = Device::get_id(self), "Failed to parse message: {err}");
return; return;
} }
}; };
@ -98,13 +82,13 @@ impl GoogleHomeDevice for WakeOnLAN {
} }
fn get_device_name(&self) -> device::Name { fn get_device_name(&self) -> device::Name {
let mut name = device::Name::new(&self.info.name); let mut name = device::Name::new(&self.config.info.name);
name.add_default_name("Computer"); name.add_default_name("Computer");
name name
} }
fn get_id(&self) -> &str { fn get_id(&self) -> String {
Device::get_id(self) Device::get_id(self)
} }
@ -113,7 +97,7 @@ impl GoogleHomeDevice for WakeOnLAN {
} }
fn get_room_hint(&self) -> Option<&str> { fn get_room_hint(&self) -> Option<&str> {
self.info.room.as_deref() self.config.info.room.as_deref()
} }
} }
@ -122,26 +106,35 @@ impl traits::Scene for WakeOnLAN {
async fn set_active(&self, activate: bool) -> Result<(), ErrorCode> { async fn set_active(&self, activate: bool) -> Result<(), ErrorCode> {
if activate { if activate {
debug!( debug!(
id = self.identifier, id = Device::get_id(self),
"Activating Computer: {} (Sending to {})", self.mac_address, self.broadcast_ip "Activating Computer: {} (Sending to {})",
self.config.mac_address,
self.config.broadcast_ip
); );
let wol = let wol = wakey::WolPacket::from_bytes(&self.config.mac_address.to_array()).map_err(
wakey::WolPacket::from_bytes(&self.mac_address.to_array()).map_err(|err| { |err| {
error!(id = self.identifier, "invalid mac address: {err}"); error!(id = Device::get_id(self), "invalid mac address: {err}");
google_home::errors::DeviceError::TransientError google_home::errors::DeviceError::TransientError
})?; },
)?;
wol.send_magic_to((Ipv4Addr::new(0, 0, 0, 0), 0), (self.broadcast_ip, 9)) wol.send_magic_to(
.await (Ipv4Addr::new(0, 0, 0, 0), 0),
.map_err(|err| { (self.config.broadcast_ip, 9),
error!(id = self.identifier, "Failed to activate computer: {err}"); )
google_home::errors::DeviceError::TransientError.into() .await
}) .map_err(|err| {
.map(|_| debug!(id = self.identifier, "Success!")) error!(
id = Device::get_id(self),
"Failed to activate computer: {err}"
);
google_home::errors::DeviceError::TransientError.into()
})
.map(|_| debug!(id = Device::get_id(self), "Success!"))
} else { } else {
debug!( debug!(
id = self.identifier, id = Device::get_id(self),
"Trying to deactive computer, this is not currently supported" "Trying to deactivate computer, this is not currently supported"
); );
// We do not support deactivating this scene // We do not support deactivating this scene
Err(ErrorCode::DeviceError( Err(ErrorCode::DeviceError(

View File

@ -1,88 +1,90 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use rumqttc::Publish; use rumqttc::Publish;
use serde::Deserialize; use tracing::{debug, error, trace, warn};
use tracing::{debug, error, warn};
use super::ntfy::Priority; use super::ntfy::Priority;
use super::{Device, Notification}; use super::{Device, LuaDeviceCreate, Notification};
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig}; use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::error::DeviceConfigError;
use crate::event::{Event, EventChannel, OnMqtt};
use crate::messages::PowerMessage; use crate::messages::PowerMessage;
use crate::mqtt::WrappedAsyncClient;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct WasherConfig { pub struct WasherConfig {
#[serde(flatten)] pub identifier: String,
mqtt: MqttDeviceConfig, #[device_config(flatten)]
threshold: f32, // Power in Watt pub mqtt: MqttDeviceConfig,
} // Power in Watt
pub threshold: f32,
#[async_trait] #[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
impl DeviceConfig for WasherConfig { pub tx: event::Sender,
async fn create( #[device_config(from_lua)]
self, pub client: WrappedAsyncClient,
identifier: &str,
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError> {
let device = Washer {
identifier: identifier.into(),
mqtt: self.mqtt,
event_channel: ext.event_channel.clone(),
threshold: self.threshold,
running: 0,
};
Ok(Box::new(device))
}
} }
// TODO: Add google home integration // TODO: Add google home integration
#[derive(Debug, LuaDevice)]
pub struct Washer {
config: WasherConfig,
#[derive(Debug)]
struct Washer {
identifier: String,
mqtt: MqttDeviceConfig,
event_channel: EventChannel,
threshold: f32,
running: isize, running: isize,
} }
impl Device for Washer { #[async_trait]
fn get_id(&self) -> &str { impl LuaDeviceCreate for Washer {
&self.identifier type Config = WasherConfig;
type Error = rumqttc::ClientError;
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
trace!(id = config.identifier, "Setting up Washer");
config
.client
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
.await?;
Ok(Self { config, running: 0 })
} }
} }
// The washer needs to have a power draw above the theshold multiple times before the washer is impl Device for Washer {
fn get_id(&self) -> String {
self.config.identifier.clone()
}
}
// The washer needs to have a power draw above the threshold multiple times before the washer is
// actually marked as running // actually marked as running
// This helps prevent false positives // This helps prevent false positives
const HYSTERESIS: isize = 10; const HYSTERESIS: isize = 10;
#[async_trait] #[async_trait]
impl OnMqtt for Washer { impl OnMqtt for Washer {
fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic]
}
async fn on_mqtt(&mut self, message: Publish) { async fn on_mqtt(&mut self, message: Publish) {
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
return;
}
let power = match PowerMessage::try_from(message) { let power = match PowerMessage::try_from(message) {
Ok(state) => state.power(), Ok(state) => state.power(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
return; return;
} }
}; };
// debug!(id = self.identifier, power, "Washer state update"); // debug!(id = self.identifier, power, "Washer state update");
if power < self.threshold && self.running >= HYSTERESIS { if power < self.config.threshold && self.running >= HYSTERESIS {
// The washer is done running // The washer is done running
debug!( debug!(
id = self.identifier, id = self.config.identifier,
power, power,
threshold = self.threshold, threshold = self.config.threshold,
"Washer is done" "Washer is done"
); );
@ -94,23 +96,23 @@ impl OnMqtt for Washer {
.set_priority(Priority::High); .set_priority(Priority::High);
if self if self
.event_channel .config
.get_tx() .tx
.send(Event::Ntfy(notification)) .send(Event::Ntfy(notification))
.await .await
.is_err() .is_err()
{ {
warn!("There are no receivers on the event channel"); warn!("There are no receivers on the event channel");
} }
} else if power < self.threshold { } else if power < self.config.threshold {
// Prevent false positives // Prevent false positives
self.running = 0; self.running = 0;
} else if power >= self.threshold && self.running < HYSTERESIS { } else if power >= self.config.threshold && self.running < HYSTERESIS {
// Washer could be starting // Washer could be starting
debug!( debug!(
id = self.identifier, id = self.config.identifier,
power, power,
threshold = self.threshold, threshold = self.config.threshold,
"Washer is starting" "Washer is starting"
); );

View File

@ -65,16 +65,6 @@ pub enum ParseError {
InvalidPayload(Bytes), InvalidPayload(Bytes),
} }
#[derive(Debug, Error)]
pub enum ConfigParseError {
#[error(transparent)]
MissingEnv(#[from] MissingEnv),
#[error(transparent)]
IoError(#[from] std::io::Error),
#[error(transparent)]
YamlError(#[from] serde_yaml::Error),
}
// TODO: Would be nice to somehow get the line number of the expected wildcard topic // TODO: Would be nice to somehow get the line number of the expected wildcard topic
#[derive(Debug, Error)] #[derive(Debug, Error)]
#[error("Topic '{topic}' is expected to be a wildcard topic")] #[error("Topic '{topic}' is expected to be a wildcard topic")]
@ -92,12 +82,10 @@ impl MissingWildcard {
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum DeviceConfigError { pub enum DeviceConfigError {
#[error("Child '{1}' of device '{0}' does not exist")]
MissingChild(String, String),
#[error("Device '{0}' does not implement expected trait '{1}'")] #[error("Device '{0}' does not implement expected trait '{1}'")]
MissingTrait(String, String), MissingTrait(String, String),
#[error(transparent)] #[error(transparent)]
MissingWildcard(#[from] MissingWildcard), MqttClientError(#[from] rumqttc::ClientError),
} }
#[derive(Debug, Error)] #[derive(Debug, Error)]

View File

@ -1,5 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use impl_cast::device_trait; use impl_cast::device_trait;
use mlua::FromLua;
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@ -16,7 +17,7 @@ pub enum Event {
pub type Sender = mpsc::Sender<Event>; pub type Sender = mpsc::Sender<Event>;
pub type Receiver = mpsc::Receiver<Event>; pub type Receiver = mpsc::Receiver<Event>;
#[derive(Clone, Debug)] #[derive(Clone, Debug, FromLua)]
pub struct EventChannel(Sender); pub struct EventChannel(Sender);
impl EventChannel { impl EventChannel {
@ -31,10 +32,12 @@ impl EventChannel {
} }
} }
impl mlua::UserData for EventChannel {}
#[async_trait] #[async_trait]
#[device_trait] #[device_trait]
pub trait OnMqtt { pub trait OnMqtt {
fn topics(&self) -> Vec<&str>; // fn topics(&self) -> Vec<&str>;
async fn on_mqtt(&mut self, message: Publish); async fn on_mqtt(&mut self, message: Publish);
} }

View File

@ -1,6 +1,9 @@
#![allow(incomplete_features)] #![allow(incomplete_features)]
#![feature(specialization)] #![feature(specialization)]
#![feature(let_chains)] #![feature(let_chains)]
use once_cell::sync::Lazy;
use tokio::sync::Mutex;
pub mod auth; pub mod auth;
pub mod config; pub mod config;
pub mod device_manager; pub mod device_manager;
@ -11,3 +14,5 @@ pub mod messages;
pub mod mqtt; pub mod mqtt;
pub mod schedule; pub mod schedule;
pub mod traits; pub mod traits;
pub static LUA: Lazy<Mutex<mlua::Lua>> = Lazy::new(|| Mutex::new(mlua::Lua::new()));

View File

@ -1,12 +1,14 @@
#![feature(async_closure)] #![feature(async_closure)]
use std::path::Path;
use std::process; use std::process;
use automation::auth::{OpenIDConfig, User}; use anyhow::anyhow;
use automation::config::Config; use automation::auth::User;
use automation::config::{FulfillmentConfig, MqttConfig};
use automation::device_manager::DeviceManager; use automation::device_manager::DeviceManager;
use automation::devices::{Ntfy, Presence};
use automation::error::ApiError; use automation::error::ApiError;
use automation::mqtt; use automation::mqtt::{self, WrappedAsyncClient};
use automation::{devices, LUA};
use axum::extract::FromRef; use axum::extract::FromRef;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
@ -14,17 +16,18 @@ use axum::routing::post;
use axum::{Json, Router}; use axum::{Json, Router};
use dotenvy::dotenv; use dotenvy::dotenv;
use google_home::{GoogleHome, Request}; use google_home::{GoogleHome, Request};
use mlua::LuaSerdeExt;
use rumqttc::AsyncClient; use rumqttc::AsyncClient;
use tracing::{debug, error, info}; use tracing::{debug, error, info, warn};
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
pub openid: OpenIDConfig, pub openid_url: String,
} }
impl FromRef<AppState> for OpenIDConfig { impl FromRef<AppState> for String {
fn from_ref(input: &AppState) -> Self { fn from_ref(input: &AppState) -> Self {
input.openid.clone() input.openid_url.clone()
} }
} }
@ -44,47 +47,73 @@ async fn main() {
async fn app() -> anyhow::Result<()> { async fn app() -> anyhow::Result<()> {
dotenv().ok(); dotenv().ok();
console_subscriber::init(); tracing_subscriber::fmt::init();
// console_subscriber::init();
info!("Starting automation_rs..."); info!("Starting automation_rs...");
let config_filename =
std::env::var("AUTOMATION_CONFIG").unwrap_or("./config/config.yml".into());
let config = Config::parse_file(&config_filename)?;
// Create a mqtt client
// TODO: Since we wait with starting the eventloop we might fill the queue while setting up devices
let (client, eventloop) = AsyncClient::new(config.mqtt.clone(), 100);
// Setup the device handler // Setup the device handler
let device_manager = DeviceManager::new(client.clone()); let device_manager = DeviceManager::new().await;
for (id, device_config) in config.devices { let fulfillment_config = {
device_manager.create(&id, device_config).await?; let lua = LUA.lock().await;
}
device_manager.add_schedule(config.schedule).await; lua.set_warning_function(|_lua, text, _cont| {
warn!("{text}");
Ok(())
});
let event_channel = device_manager.event_channel(); let automation = lua.create_table()?;
let event_channel = device_manager.event_channel();
let new_mqtt_client = lua.create_function(move |lua, config: mlua::Value| {
let config: MqttConfig = lua.from_value(config)?;
// Create and add the presence system // Create a mqtt client
{ // TODO: When starting up, the devices are not yet created, this could lead to a device being out of sync
let presence = Presence::new(config.presence, &event_channel); let (client, eventloop) = AsyncClient::new(config.into(), 100);
device_manager.add(Box::new(presence)).await; mqtt::start(eventloop, &event_channel);
}
// Start the ntfy service if it is configured Ok(WrappedAsyncClient(client))
if let Some(config) = config.ntfy { })?;
let ntfy = Ntfy::new(config, &event_channel);
device_manager.add(Box::new(ntfy)).await;
}
// Wrap the mqtt eventloop and start listening for message automation.set("new_mqtt_client", new_mqtt_client)?;
// NOTE: We wait until all the setup is done, as otherwise we might miss some messages automation.set("device_manager", device_manager.clone())?;
mqtt::start(eventloop, &event_channel);
// Create google home fullfillment route let util = lua.create_table()?;
let fullfillment = Router::new().route( let get_env = lua.create_function(|_lua, name: String| {
std::env::var(name).map_err(mlua::ExternalError::into_lua_err)
})?;
util.set("get_env", get_env)?;
automation.set("util", util)?;
lua.globals().set("automation", automation)?;
devices::register_with_lua(&lua)?;
// TODO: Make this not hardcoded
let config_filename = std::env::var("AUTOMATION_CONFIG").unwrap_or("./config.lua".into());
let config_path = Path::new(&config_filename);
match lua.load(config_path).exec_async().await {
Err(error) => {
println!("{error}");
Err(error)
}
result => result,
}?;
let automation: mlua::Table = lua.globals().get("automation")?;
let fulfillment_config: Option<mlua::Value> = automation.get("fulfillment")?;
if let Some(fulfillment_config) = fulfillment_config {
let fulfillment_config: FulfillmentConfig = lua.from_value(fulfillment_config)?;
debug!("automation.fulfillment = {fulfillment_config:?}");
fulfillment_config
} else {
return Err(anyhow!("Fulfillment is not configured"));
}
};
// Create google home fulfillment route
let fulfillment = Router::new().route(
"/google_home", "/google_home",
post(async move |user: User, Json(payload): Json<Request>| { post(async move |user: User, Json(payload): Json<Request>| {
debug!(username = user.preferred_username, "{payload:#?}"); debug!(username = user.preferred_username, "{payload:#?}");
@ -106,13 +135,13 @@ async fn app() -> anyhow::Result<()> {
// Combine together all the routes // Combine together all the routes
let app = Router::new() let app = Router::new()
.nest("/fullfillment", fullfillment) .nest("/fulfillment", fulfillment)
.with_state(AppState { .with_state(AppState {
openid: config.openid, openid_url: fulfillment_config.openid_url.clone(),
}); });
// Start the web server // Start the web server
let addr = config.fullfillment.into(); let addr = fulfillment_config.into();
info!("Server started on http://{addr}"); info!("Server started on http://{addr}");
axum::Server::try_bind(&addr)? axum::Server::try_bind(&addr)?
.serve(app.into_make_service()) .serve(app.into_make_service())

View File

@ -1,8 +1,30 @@
use rumqttc::{Event, EventLoop, Incoming}; use std::ops::{Deref, DerefMut};
use mlua::FromLua;
use rumqttc::{AsyncClient, Event, EventLoop, Incoming};
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::event::{self, EventChannel}; use crate::event::{self, EventChannel};
#[derive(Debug, Clone, FromLua)]
pub struct WrappedAsyncClient(pub AsyncClient);
impl Deref for WrappedAsyncClient {
type Target = AsyncClient;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for WrappedAsyncClient {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl mlua::UserData for WrappedAsyncClient {}
pub fn start(mut eventloop: EventLoop, event_channel: &EventChannel) { pub fn start(mut eventloop: EventLoop, event_channel: &EventChannel) {
let tx = event_channel.get_tx(); let tx = event_channel.get_tx();