Compare commits
24 Commits
b5274b9fea
...
5cbd838e9a
Author | SHA1 | Date | |
---|---|---|---|
5cbd838e9a | |||
2aee44cba2 | |||
27324e2c70 | |||
2e553038ba | |||
808549bcba | |||
7e152599a1 | |||
8bc529cc1a | |||
aafdfcba1f | |||
3dbc14ccac | |||
55025a67fc | |||
742e3bd62a | |||
430355bb40 | |||
870da3eea9 | |||
c4978ab9ca | |||
e43bd4ea2a | |||
68b9676616 | |||
8377e1d696 | |||
c6b9005d1e | |||
4273a25acd | |||
92b7a2830a | |||
dec08fe0a2 | |||
f115e0e6e8 | |||
e79787561d | |||
668f13863a |
|
@ -10,6 +10,7 @@ shopt -s inherit_errexit # Inherit the errexit option status in subshells.
|
|||
|
||||
set -x
|
||||
|
||||
cargo run --bin generate_definitions > ./definitions/generated.lua
|
||||
git update-index --refresh
|
||||
cargo clippy --all-targets --all -- -D warnings
|
||||
cargo fmt -- --check
|
||||
|
|
2
.typos.toml
Normal file
2
.typos.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
[default.extend-words]
|
||||
mosquitto = "mosquitto"
|
443
Cargo.lock
generated
443
Cargo.lock
generated
|
@ -61,7 +61,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -76,6 +76,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"automation_cast",
|
||||
"automation_macro",
|
||||
"axum",
|
||||
"bytes",
|
||||
"console-subscriber",
|
||||
|
@ -84,8 +86,10 @@ dependencies = [
|
|||
"eui48",
|
||||
"futures",
|
||||
"google-home",
|
||||
"impl_cast",
|
||||
"hostname",
|
||||
"indexmap 2.0.0",
|
||||
"mlua",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"pollster",
|
||||
"regex",
|
||||
|
@ -104,6 +108,20 @@ dependencies = [
|
|||
"wakey",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "automation_cast"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "automation_macro"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"itertools 0.12.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.6.20"
|
||||
|
@ -112,7 +130,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
|
|||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-util",
|
||||
"http",
|
||||
|
@ -186,6 +204,22 @@ version = "1.3.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "bumpalo"
|
||||
version = "3.13.0"
|
||||
|
@ -344,7 +378,7 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -355,7 +389,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
|
|||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -397,7 +431,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -406,6 +440,25 @@ version = "1.0.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "eui48"
|
||||
version = "1.1.0"
|
||||
|
@ -510,7 +563,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -568,8 +621,8 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"automation_cast",
|
||||
"futures",
|
||||
"impl_cast",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
|
@ -632,6 +685,26 @@ version = "0.4.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "hostname"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"windows 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http"
|
||||
version = "0.2.9"
|
||||
|
@ -733,7 +806,7 @@ dependencies = [
|
|||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows",
|
||||
"windows 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -761,14 +834,6 @@ dependencies = [
|
|||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "impl_cast"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
|
@ -806,6 +871,15 @@ dependencies = [
|
|||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.9"
|
||||
|
@ -829,9 +903,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
|||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.147"
|
||||
version = "0.2.153"
|
||||
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]]
|
||||
name = "lock_api"
|
||||
|
@ -849,6 +929,25 @@ version = "0.4.19"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "matchers"
|
||||
version = "0.1.0"
|
||||
|
@ -899,7 +998,53 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"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]]
|
||||
|
@ -972,9 +1117,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.18.0"
|
||||
version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-probe"
|
||||
|
@ -982,6 +1127,15 @@ version = "0.1.5"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
|
@ -1017,7 +1171,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1032,6 +1186,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "pollster"
|
||||
version = "0.2.5"
|
||||
|
@ -1045,10 +1205,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.66"
|
||||
name = "proc-macro-error"
|
||||
version = "1.0.4"
|
||||
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 = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
@ -1070,7 +1254,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools",
|
||||
"itertools 0.10.5",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
|
@ -1087,9 +1271,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.32"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
@ -1246,6 +1430,25 @@ version = "0.1.23"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "rustls"
|
||||
version = "0.20.8"
|
||||
|
@ -1328,7 +1531,7 @@ version = "0.1.22"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1353,7 +1556,7 @@ version = "2.9.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
|
@ -1372,22 +1575,32 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.190"
|
||||
version = "1.0.198"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
|
||||
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.190"
|
||||
name = "serde-value"
|
||||
version = "0.7.0"
|
||||
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 = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1419,7 +1632,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1460,7 +1673,7 @@ dependencies = [
|
|||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1517,7 +1730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1554,9 +1767,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.28"
|
||||
version = "2.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567"
|
||||
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -1586,7 +1799,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1657,7 +1870,7 @@ dependencies = [
|
|||
"socket2 0.5.3",
|
||||
"tokio-macros",
|
||||
"tracing",
|
||||
"windows-sys",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1693,7 +1906,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1823,7 +2036,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1930,6 +2143,12 @@ version = "0.1.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "wakey"
|
||||
version = "0.3.0"
|
||||
|
@ -1976,7 +2195,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
|
@ -2010,7 +2229,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.28",
|
||||
"syn 2.0.60",
|
||||
"wasm-bindgen-backend",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
@ -2050,6 +2269,18 @@ dependencies = [
|
|||
"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]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
|
@ -2078,7 +2309,26 @@ version = "0.48.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
"windows-targets 0.48.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets 0.52.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2087,7 +2337,16 @@ version = "0.48.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
|
||||
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]]
|
||||
|
@ -2096,13 +2355,29 @@ version = "0.48.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
"windows_aarch64_gnullvm 0.48.0",
|
||||
"windows_aarch64_msvc 0.48.0",
|
||||
"windows_i686_gnu 0.48.0",
|
||||
"windows_i686_msvc 0.48.0",
|
||||
"windows_x86_64_gnu 0.48.0",
|
||||
"windows_x86_64_gnullvm 0.48.0",
|
||||
"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]]
|
||||
|
@ -2111,42 +2386,90 @@ version = "0.48.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
|
@ -2155,3 +2478,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
|||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winsafe"
|
||||
version = "0.0.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
|
||||
|
|
24
Cargo.toml
24
Cargo.toml
|
@ -2,22 +2,24 @@
|
|||
name = "automation"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
default-run = "automation"
|
||||
|
||||
[workspace]
|
||||
members = ["impl_cast", "google-home"]
|
||||
members = ["google-home", "automation_macro", "automation_cast"]
|
||||
|
||||
[dependencies]
|
||||
automation_macro = { path = "./automation_macro" }
|
||||
automation_cast = { path = "./automation_cast/" }
|
||||
rumqttc = "0.18"
|
||||
serde = { version = "1.0.149", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
impl_cast = { path = "./impl_cast", features = ["debug"] }
|
||||
google-home = { path = "./google-home" }
|
||||
paste = "1.0.10"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
dotenvy = "0.15.0"
|
||||
reqwest = { version = "0.11.13", features = [
|
||||
"json",
|
||||
"rustls-tls",
|
||||
"json",
|
||||
"rustls-tls",
|
||||
], default-features = false } # Use rustls, since the other packages also use rustls
|
||||
axum = "0.6.1"
|
||||
serde_repr = "0.1.10"
|
||||
|
@ -28,8 +30,8 @@ regex = "1.7.0"
|
|||
async-trait = "0.1.61"
|
||||
futures = "0.3.25"
|
||||
eui48 = { version = "1.1.0", default-features = false, features = [
|
||||
"disp_hexstring",
|
||||
"serde",
|
||||
"disp_hexstring",
|
||||
"serde",
|
||||
] }
|
||||
thiserror = "1.0.38"
|
||||
anyhow = "1.0.68"
|
||||
|
@ -41,6 +43,16 @@ enum_dispatch = "0.3.12"
|
|||
indexmap = { version = "2.0.0", features = ["serde"] }
|
||||
serde_yaml = "0.9.27"
|
||||
tokio-cron-scheduler = "0.9.4"
|
||||
mlua = { version = "0.9.7", features = [
|
||||
"lua54",
|
||||
"vendored",
|
||||
"macros",
|
||||
"serialize",
|
||||
"async",
|
||||
"send",
|
||||
] }
|
||||
once_cell = "1.19.0"
|
||||
hostname = "0.4.0"
|
||||
|
||||
[patch.crates-io]
|
||||
wakey = { git = "https://git.huizinga.dev/Dreaded_X/wakey" }
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
FROM gcr.io/distroless/cc-debian12:nonroot
|
||||
|
||||
ENV AUTOMATION_CONFIG=/app/config.yml
|
||||
COPY ./config/config.yml /app/config.yml
|
||||
ENV AUTOMATION_CONFIG=/app/config.lua
|
||||
COPY ./config.lua /app/config.lua
|
||||
|
||||
COPY ./build/automation /app/automation
|
||||
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
# automation_rs
|
||||
Custom home automation solution with google-home intergration
|
||||
|
||||
Custom home automation solution with google-home integration
|
||||
|
||||
## Development
|
||||
|
||||
Make sure to setup git hooks by running
|
||||
|
||||
```sh
|
||||
git config --local core.hooksPath .git-hooks/
|
||||
```
|
||||
|
|
8
automation_cast/Cargo.toml
Normal file
8
automation_cast/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "automation_cast"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
37
automation_cast/src/lib.rs
Normal file
37
automation_cast/src/lib.rs
Normal file
|
@ -0,0 +1,37 @@
|
|||
#![allow(incomplete_features)]
|
||||
#![feature(specialization)]
|
||||
#![feature(unsize)]
|
||||
|
||||
use std::marker::Unsize;
|
||||
|
||||
pub trait Cast<P: ?Sized> {
|
||||
fn cast(&self) -> Option<&P>;
|
||||
fn cast_mut(&mut self) -> Option<&mut P>;
|
||||
}
|
||||
|
||||
impl<D, P> Cast<P> for D
|
||||
where
|
||||
P: ?Sized,
|
||||
{
|
||||
default fn cast(&self) -> Option<&P> {
|
||||
None
|
||||
}
|
||||
|
||||
default fn cast_mut(&mut self) -> Option<&mut P> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<D, P> Cast<P> for D
|
||||
where
|
||||
D: Unsize<P>,
|
||||
P: ?Sized,
|
||||
{
|
||||
fn cast(&self) -> Option<&P> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn cast_mut(&mut self) -> Option<&mut P> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
13
automation_macro/Cargo.toml
Normal file
13
automation_macro/Cargo.toml
Normal 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"] }
|
29
automation_macro/src/lib.rs
Normal file
29
automation_macro/src/lib.rs
Normal file
|
@ -0,0 +1,29 @@
|
|||
mod lua_device;
|
||||
mod lua_device_config;
|
||||
mod lua_type_definition;
|
||||
|
||||
use lua_device::impl_lua_device_macro;
|
||||
use lua_device_config::impl_lua_device_config_macro;
|
||||
use lua_type_definition::impl_lua_type_definition;
|
||||
use syn::{parse_macro_input, DeriveInput};
|
||||
|
||||
#[proc_macro_derive(LuaDevice)]
|
||||
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()
|
||||
}
|
||||
|
||||
#[proc_macro_derive(LuaTypeDefinition, attributes(device_config))]
|
||||
pub fn lua_type_definition_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
let ast = parse_macro_input!(input as DeriveInput);
|
||||
|
||||
impl_lua_type_definition(&ast).into()
|
||||
}
|
42
automation_macro/src/lua_device.rs
Normal file
42
automation_macro/src/lua_device.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
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>()?)
|
||||
}
|
||||
|
||||
pub fn generate_lua_definition() -> String {
|
||||
// TODO: Do not hardcode the name of the config type
|
||||
let def = format!(
|
||||
r#"--- @class {0}
|
||||
{0} = {{}}
|
||||
--- @param config {0}Config
|
||||
--- @return WrappedDevice
|
||||
function {0}.new(config) end
|
||||
"#, stringify!(#name)
|
||||
);
|
||||
|
||||
def
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
295
automation_macro/src/lua_device_config.rs
Normal file
295
automation_macro/src/lua_device_config.rs
Normal file
|
@ -0,0 +1,295 @@
|
|||
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)]
|
||||
pub 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)]
|
||||
pub(crate) struct Args {
|
||||
pub(crate) 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()
|
||||
}
|
||||
}),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.as_slice()
|
||||
{
|
||||
[] => value,
|
||||
[value] => value.to_owned(),
|
||||
_ => {
|
||||
return quote_spanned! {field.span() => compile_error!("Field contains duplicate 'from'")}
|
||||
}
|
||||
};
|
||||
|
||||
let value = match args
|
||||
.iter()
|
||||
.filter_map(|arg| match arg {
|
||||
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!("Field contains duplicate 'with'")}
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
}
|
137
automation_macro/src/lua_type_definition.rs
Normal file
137
automation_macro/src/lua_type_definition.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
use itertools::Itertools;
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{quote, quote_spanned};
|
||||
use syn::spanned::Spanned;
|
||||
use syn::{
|
||||
AngleBracketedGenericArguments, Data, DataStruct, DeriveInput, Field, Fields, FieldsNamed,
|
||||
PathArguments, Type, TypePath,
|
||||
};
|
||||
|
||||
use crate::lua_device_config::{Args, Argument};
|
||||
|
||||
fn field_definition(field: &Field) -> TokenStream {
|
||||
let (args, _): (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 args: Vec<_> = args.into_iter().flatten().collect();
|
||||
|
||||
let field_name = if let Some(field_name) = args.iter().find_map(|arg| match arg {
|
||||
Argument::Rename { ident, .. } => Some(ident),
|
||||
_ => None,
|
||||
}) {
|
||||
field_name.value()
|
||||
} else {
|
||||
format!("{}", field.ident.clone().unwrap())
|
||||
};
|
||||
|
||||
let mut optional = args
|
||||
.iter()
|
||||
.filter(|arg| matches!(arg, Argument::Default { .. } | Argument::DefaultExpr { .. }))
|
||||
.count()
|
||||
>= 1;
|
||||
|
||||
if args
|
||||
.iter()
|
||||
.filter(|arg| matches!(arg, Argument::Flatten { .. }))
|
||||
.count()
|
||||
>= 1
|
||||
{
|
||||
let field_type = &field.ty;
|
||||
quote! {
|
||||
#field_type::generate_lua_fields().as_str()
|
||||
}
|
||||
} else {
|
||||
let path = if let Some(ty) = args.iter().find_map(|arg| match arg {
|
||||
Argument::From { ty, .. } => Some(ty),
|
||||
_ => None,
|
||||
}) {
|
||||
if let Type::Path(TypePath { path, .. }) = ty {
|
||||
path.clone()
|
||||
} else {
|
||||
todo!();
|
||||
}
|
||||
} else if let Type::Path(TypePath { path, .. }) = field.ty.clone() {
|
||||
path
|
||||
} else {
|
||||
todo!()
|
||||
};
|
||||
|
||||
let seg = path.segments.first().unwrap();
|
||||
let field_type = if seg.ident == "Option" {
|
||||
if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
|
||||
seg.arguments.clone()
|
||||
{
|
||||
optional = true;
|
||||
quote! { stringify!(#args) }
|
||||
} else {
|
||||
unreachable!("Option should always have angle brackets");
|
||||
}
|
||||
} else if seg.ident == "Vec" {
|
||||
if let PathArguments::AngleBracketed(AngleBracketedGenericArguments { args, .. }) =
|
||||
seg.arguments.clone()
|
||||
{
|
||||
optional = true;
|
||||
quote! { stringify!(#args[]) }
|
||||
} else {
|
||||
unreachable!("Option should always have angle brackets");
|
||||
}
|
||||
} else {
|
||||
quote! { stringify!(#path).replace(" :: ", "_") }
|
||||
};
|
||||
|
||||
let mut format = "--- @field {} {}".to_string();
|
||||
if optional {
|
||||
format += "|nil";
|
||||
}
|
||||
format += "\n";
|
||||
|
||||
quote! {
|
||||
format!(#format, #field_name, #field_type).as_str()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn impl_lua_type_definition(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 fields: Vec<_> = fields.iter().map(field_definition).collect();
|
||||
|
||||
let gen = quote! {
|
||||
impl #name {
|
||||
pub fn generate_lua_definition() -> String {
|
||||
let mut def = format!("--- @class {}\n", stringify!(#name));
|
||||
|
||||
def += #name::generate_lua_fields().as_str();
|
||||
|
||||
def
|
||||
}
|
||||
|
||||
pub fn generate_lua_fields() -> String {
|
||||
let mut def = String::new();
|
||||
|
||||
#(def += #fields;)*
|
||||
|
||||
def
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
gen
|
||||
}
|
178
config.lua
Normal file
178
config.lua
Normal file
|
@ -0,0 +1,178 @@
|
|||
print("Hello from lua")
|
||||
|
||||
local host = automation.util.get_hostname()
|
||||
print("Running @" .. host)
|
||||
|
||||
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
|
||||
|
||||
automation.fulfillment = {
|
||||
openid_url = "https://login.huizinga.dev/api/oidc",
|
||||
}
|
||||
|
||||
local mqtt_client = automation.new_mqtt_client({
|
||||
host = (host == "zeus" and "olympus.lan.huizinga.dev")
|
||||
or (host == "hephaestus" and "olympus.vpn.huizinga.dev")
|
||||
or "mosquitto",
|
||||
port = 8883,
|
||||
client_name = "automation-" .. host,
|
||||
username = "mqtt",
|
||||
password = automation.util.get_env("MQTT_PASSWORD"),
|
||||
tls = host == "zeus" or host == "hephaestus",
|
||||
})
|
||||
|
||||
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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
40
definitions/definitions.lua
Normal file
40
definitions/definitions.lua
Normal file
|
@ -0,0 +1,40 @@
|
|||
--- @meta
|
||||
|
||||
--- @class WrappedDevice
|
||||
WrappedDevice = {}
|
||||
--- @return string
|
||||
function WrappedDevice:get_id() end
|
||||
|
||||
--- @class WrappedAsyncClient
|
||||
|
||||
--- @class EventChannel
|
||||
--- @return EventChannel
|
||||
function automation.device_manager:event_channel() end
|
||||
|
||||
automation = {}
|
||||
|
||||
automation.device_manager = {}
|
||||
--- @param device WrappedDevice
|
||||
function automation.device_manager:add(device) end
|
||||
|
||||
--- @param when string
|
||||
--- @param func function
|
||||
function automation.device_manager:schedule(when, func) end
|
||||
|
||||
automation.util = {}
|
||||
--- @param env string
|
||||
--- @return string
|
||||
function automation.util.get_env(env) end
|
||||
|
||||
--- @class Fulfillment
|
||||
--- @field openid_url string|nil
|
||||
automation.fulfillment = {}
|
||||
|
||||
--- @class MqttConfig
|
||||
--- @param config MqttConfig
|
||||
--- @return WrappedAsyncClient
|
||||
function automation.new_mqtt_client(config) end
|
||||
|
||||
--- TODO: Generate this automatically
|
||||
--- @alias OutletType "Outlet"|"Kettle"|"Charger"|"Light"
|
||||
--- @alias TriggerDevicesHelper WrappedDevice[]
|
183
definitions/generated.lua
Normal file
183
definitions/generated.lua
Normal file
|
@ -0,0 +1,183 @@
|
|||
-- WARN: This file is automatically generated, do not manually edit
|
||||
|
||||
---@meta
|
||||
--- @class MqttDeviceConfig
|
||||
--- @field topic String
|
||||
|
||||
--- @class AirFilter
|
||||
AirFilter = {}
|
||||
--- @param config AirFilterConfig
|
||||
--- @return WrappedDevice
|
||||
function AirFilter.new(config) end
|
||||
|
||||
--- @class AirFilterConfig
|
||||
--- @field name String
|
||||
--- @field room String|nil
|
||||
--- @field topic String
|
||||
--- @field client WrappedAsyncClient
|
||||
|
||||
--- @class AudioSetup
|
||||
AudioSetup = {}
|
||||
--- @param config AudioSetupConfig
|
||||
--- @return WrappedDevice
|
||||
function AudioSetup.new(config) end
|
||||
|
||||
--- @class AudioSetupConfig
|
||||
--- @field identifier String
|
||||
--- @field topic String
|
||||
--- @field mixer WrappedDevice
|
||||
--- @field speakers WrappedDevice
|
||||
--- @field client WrappedAsyncClient
|
||||
|
||||
--- @class ContactSensor
|
||||
ContactSensor = {}
|
||||
--- @param config ContactSensorConfig
|
||||
--- @return WrappedDevice
|
||||
function ContactSensor.new(config) end
|
||||
|
||||
--- @class ContactSensorConfig
|
||||
--- @field identifier String
|
||||
--- @field topic String
|
||||
--- @field presence PresenceDeviceConfig|nil
|
||||
--- @field trigger TriggerConfig|nil
|
||||
--- @field client WrappedAsyncClient
|
||||
|
||||
--- @class PresenceDeviceConfig
|
||||
--- @field topic String
|
||||
--- @field timeout u64
|
||||
|
||||
--- @class TriggerConfig
|
||||
--- @field devices TriggerDevicesHelper
|
||||
--- @field timeout u64|nil
|
||||
|
||||
--- @class DebugBridge
|
||||
DebugBridge = {}
|
||||
--- @param config DebugBridgeConfig
|
||||
--- @return WrappedDevice
|
||||
function DebugBridge.new(config) end
|
||||
|
||||
--- @class DebugBridgeConfig
|
||||
--- @field identifier String
|
||||
--- @field topic String
|
||||
--- @field client WrappedAsyncClient
|
||||
|
||||
--- @class HueBridge
|
||||
HueBridge = {}
|
||||
--- @param config HueBridgeConfig
|
||||
--- @return WrappedDevice
|
||||
function HueBridge.new(config) end
|
||||
|
||||
--- @class HueBridgeConfig
|
||||
--- @field identifier String
|
||||
--- @field ip Ipv4Addr
|
||||
--- @field login String
|
||||
--- @field flags FlagIDs
|
||||
|
||||
--- @class FlagIDs
|
||||
--- @field presence isize
|
||||
--- @field darkness isize
|
||||
|
||||
--- @class HueGroup
|
||||
HueGroup = {}
|
||||
--- @param config HueGroupConfig
|
||||
--- @return WrappedDevice
|
||||
function HueGroup.new(config) end
|
||||
|
||||
--- @class HueGroupConfig
|
||||
--- @field identifier String
|
||||
--- @field ip Ipv4Addr
|
||||
--- @field login String
|
||||
--- @field group_id isize
|
||||
--- @field timer_id isize
|
||||
--- @field scene_id String
|
||||
--- @field remotes MqttDeviceConfig []|nil
|
||||
--- @field client WrappedAsyncClient
|
||||
|
||||
--- @class IkeaOutlet
|
||||
IkeaOutlet = {}
|
||||
--- @param config IkeaOutletConfig
|
||||
--- @return WrappedDevice
|
||||
function IkeaOutlet.new(config) end
|
||||
|
||||
--- @class IkeaOutletConfig
|
||||
--- @field name String
|
||||
--- @field room String|nil
|
||||
--- @field topic String
|
||||
--- @field outlet_type OutletType|nil
|
||||
--- @field timeout u64|nil
|
||||
--- @field remotes MqttDeviceConfig []|nil
|
||||
--- @field client WrappedAsyncClient
|
||||
|
||||
--- @class KasaOutlet
|
||||
KasaOutlet = {}
|
||||
--- @param config KasaOutletConfig
|
||||
--- @return WrappedDevice
|
||||
function KasaOutlet.new(config) end
|
||||
|
||||
--- @class KasaOutletConfig
|
||||
--- @field identifier String
|
||||
--- @field ip Ipv4Addr
|
||||
|
||||
--- @class LightSensor
|
||||
LightSensor = {}
|
||||
--- @param config LightSensorConfig
|
||||
--- @return WrappedDevice
|
||||
function LightSensor.new(config) end
|
||||
|
||||
--- @class LightSensorConfig
|
||||
--- @field identifier String
|
||||
--- @field topic String
|
||||
--- @field min isize
|
||||
--- @field max isize
|
||||
--- @field event_channel EventChannel
|
||||
--- @field client WrappedAsyncClient
|
||||
|
||||
--- @class Ntfy
|
||||
Ntfy = {}
|
||||
--- @param config NtfyConfig
|
||||
--- @return WrappedDevice
|
||||
function Ntfy.new(config) end
|
||||
|
||||
--- @class NtfyConfig
|
||||
--- @field url String|nil
|
||||
--- @field topic String
|
||||
--- @field event_channel EventChannel
|
||||
|
||||
--- @class Presence
|
||||
Presence = {}
|
||||
--- @param config PresenceConfig
|
||||
--- @return WrappedDevice
|
||||
function Presence.new(config) end
|
||||
|
||||
--- @class PresenceConfig
|
||||
--- @field topic String
|
||||
--- @field event_channel EventChannel
|
||||
--- @field client WrappedAsyncClient
|
||||
|
||||
--- @class WakeOnLAN
|
||||
WakeOnLAN = {}
|
||||
--- @param config WakeOnLANConfig
|
||||
--- @return WrappedDevice
|
||||
function WakeOnLAN.new(config) end
|
||||
|
||||
--- @class WakeOnLANConfig
|
||||
--- @field name String
|
||||
--- @field room String|nil
|
||||
--- @field topic String
|
||||
--- @field mac_address MacAddress
|
||||
--- @field broadcast_ip Ipv4Addr|nil
|
||||
--- @field client WrappedAsyncClient
|
||||
|
||||
--- @class Washer
|
||||
Washer = {}
|
||||
--- @param config WasherConfig
|
||||
--- @return WrappedDevice
|
||||
function Washer.new(config) end
|
||||
|
||||
--- @class WasherConfig
|
||||
--- @field identifier String
|
||||
--- @field topic String
|
||||
--- @field threshold f32
|
||||
--- @field event_channel EventChannel
|
||||
--- @field client WrappedAsyncClient
|
||||
|
10
definitions/rust.lua
Normal file
10
definitions/rust.lua
Normal file
|
@ -0,0 +1,10 @@
|
|||
--- @meta
|
||||
|
||||
--- @alias String string
|
||||
|
||||
--- @alias u64 number
|
||||
--- @alias isize number
|
||||
--- @alias f32 number
|
||||
|
||||
--- @alias Ipv4Addr string
|
||||
--- @alias MacAddress string
|
|
@ -6,7 +6,7 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
impl_cast = { path = "../impl_cast" }
|
||||
automation_cast = { path = "../automation_cast/" }
|
||||
serde = { version = "1.0.149", features = ["derive"] }
|
||||
serde_json = "1.0.89"
|
||||
thiserror = "1.0.37"
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use async_trait::async_trait;
|
||||
use automation_cast::Cast;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::errors::{DeviceError, ErrorCode};
|
||||
|
@ -7,49 +8,16 @@ use crate::response;
|
|||
use crate::traits::{FanSpeed, HumiditySetting, OnOff, Scene, Trait};
|
||||
use crate::types::Type;
|
||||
|
||||
// TODO: Find a more elegant way to do this
|
||||
pub trait AsGoogleHomeDevice {
|
||||
fn cast(&self) -> Option<&dyn GoogleHomeDevice>;
|
||||
fn cast_mut(&mut self) -> Option<&mut dyn GoogleHomeDevice>;
|
||||
}
|
||||
|
||||
// Default impl
|
||||
impl<T> AsGoogleHomeDevice for T
|
||||
where
|
||||
T: 'static,
|
||||
{
|
||||
default fn cast(&self) -> Option<&(dyn GoogleHomeDevice + 'static)> {
|
||||
None
|
||||
}
|
||||
|
||||
default fn cast_mut(&mut self) -> Option<&mut (dyn GoogleHomeDevice + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Specialization
|
||||
impl<T> AsGoogleHomeDevice for T
|
||||
where
|
||||
T: GoogleHomeDevice + 'static,
|
||||
{
|
||||
fn cast(&self) -> Option<&(dyn GoogleHomeDevice + 'static)> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn cast_mut(&mut self) -> Option<&mut (dyn GoogleHomeDevice + 'static)> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[impl_cast::device(As: OnOff + Scene + FanSpeed + HumiditySetting)]
|
||||
pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
||||
pub trait GoogleHomeDevice:
|
||||
Sync + Send + Cast<dyn OnOff> + Cast<dyn Scene> + Cast<dyn FanSpeed> + Cast<dyn HumiditySetting>
|
||||
{
|
||||
fn get_device_type(&self) -> Type;
|
||||
fn get_device_name(&self) -> Name;
|
||||
fn get_id(&self) -> &str;
|
||||
fn get_id(&self) -> String;
|
||||
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 {
|
||||
false
|
||||
}
|
||||
|
@ -63,7 +31,7 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
|||
async fn sync(&self) -> response::sync::Device {
|
||||
let name = self.get_device_name();
|
||||
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.will_report_state = self.will_report_state();
|
||||
|
@ -76,26 +44,26 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
|||
let mut traits = Vec::new();
|
||||
|
||||
// OnOff
|
||||
if let Some(on_off) = As::<dyn OnOff>::cast(self) {
|
||||
if let Some(on_off) = self.cast() as Option<&dyn OnOff> {
|
||||
traits.push(Trait::OnOff);
|
||||
device.attributes.command_only_on_off = on_off.is_command_only();
|
||||
device.attributes.query_only_on_off = on_off.is_query_only();
|
||||
}
|
||||
|
||||
// Scene
|
||||
if let Some(scene) = As::<dyn Scene>::cast(self) {
|
||||
if let Some(scene) = self.cast() as Option<&dyn Scene> {
|
||||
traits.push(Trait::Scene);
|
||||
device.attributes.scene_reversible = scene.is_scene_reversible();
|
||||
}
|
||||
|
||||
// FanSpeed
|
||||
if let Some(fan_speed) = As::<dyn FanSpeed>::cast(self) {
|
||||
if let Some(fan_speed) = self.cast() as Option<&dyn FanSpeed> {
|
||||
traits.push(Trait::FanSpeed);
|
||||
device.attributes.command_only_fan_speed = fan_speed.command_only_fan_speed();
|
||||
device.attributes.available_fan_speeds = Some(fan_speed.available_speeds());
|
||||
}
|
||||
|
||||
if let Some(humidity_setting) = As::<dyn HumiditySetting>::cast(self) {
|
||||
if let Some(humidity_setting) = self.cast() as Option<&dyn HumiditySetting> {
|
||||
traits.push(Trait::HumiditySetting);
|
||||
device.attributes.query_only_humidity_setting =
|
||||
humidity_setting.query_only_humidity_setting();
|
||||
|
@ -113,7 +81,7 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
|||
}
|
||||
|
||||
// OnOff
|
||||
if let Some(on_off) = As::<dyn OnOff>::cast(self) {
|
||||
if let Some(on_off) = self.cast() as Option<&dyn OnOff> {
|
||||
device.state.on = on_off
|
||||
.is_on()
|
||||
.await
|
||||
|
@ -122,11 +90,11 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
|||
}
|
||||
|
||||
// FanSpeed
|
||||
if let Some(fan_speed) = As::<dyn FanSpeed>::cast(self) {
|
||||
if let Some(fan_speed) = self.cast() as Option<&dyn FanSpeed> {
|
||||
device.state.current_fan_speed_setting = Some(fan_speed.current_speed().await);
|
||||
}
|
||||
|
||||
if let Some(humidity_setting) = As::<dyn HumiditySetting>::cast(self) {
|
||||
if let Some(humidity_setting) = self.cast() as Option<&dyn HumiditySetting> {
|
||||
device.state.humidity_ambient_percent =
|
||||
Some(humidity_setting.humidity_ambient_percent().await);
|
||||
}
|
||||
|
@ -137,21 +105,21 @@ pub trait GoogleHomeDevice: AsGoogleHomeDevice + Sync + Send + 'static {
|
|||
async fn execute(&mut self, command: &CommandType) -> Result<(), ErrorCode> {
|
||||
match command {
|
||||
CommandType::OnOff { on } => {
|
||||
if let Some(t) = As::<dyn OnOff>::cast_mut(self) {
|
||||
if let Some(t) = self.cast_mut() as Option<&mut dyn OnOff> {
|
||||
t.set_on(*on).await?;
|
||||
} else {
|
||||
return Err(DeviceError::ActionNotAvailable.into());
|
||||
}
|
||||
}
|
||||
CommandType::ActivateScene { deactivate } => {
|
||||
if let Some(t) = As::<dyn Scene>::cast(self) {
|
||||
if let Some(t) = self.cast_mut() as Option<&mut dyn Scene> {
|
||||
t.set_active(!deactivate).await?;
|
||||
} else {
|
||||
return Err(DeviceError::ActionNotAvailable.into());
|
||||
}
|
||||
}
|
||||
CommandType::SetFanSpeed { fan_speed } => {
|
||||
if let Some(t) = As::<dyn FanSpeed>::cast(self) {
|
||||
if let Some(t) = self.cast_mut() as Option<&mut dyn FanSpeed> {
|
||||
t.set_speed(fan_speed).await?;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use automation_cast::Cast;
|
||||
use futures::future::{join_all, OptionFuture};
|
||||
use thiserror::Error;
|
||||
use tokio::sync::{Mutex, RwLock};
|
||||
|
||||
use crate::device::AsGoogleHomeDevice;
|
||||
use crate::errors::{DeviceError, ErrorCode};
|
||||
use crate::request::{self, Intent, Request};
|
||||
use crate::response::{self, execute, query, sync, Response, ResponsePayload, State};
|
||||
use crate::GoogleHomeDevice;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct GoogleHome {
|
||||
|
@ -17,7 +18,7 @@ pub struct GoogleHome {
|
|||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum FullfillmentError {
|
||||
pub enum FulfillmentError {
|
||||
#[error("Expected at least one ResponsePayload")]
|
||||
ExpectedOnePayload,
|
||||
}
|
||||
|
@ -29,11 +30,11 @@ impl GoogleHome {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn handle_request<T: AsGoogleHomeDevice + ?Sized + 'static>(
|
||||
pub async fn handle_request<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>(
|
||||
&self,
|
||||
request: Request,
|
||||
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
|
||||
// we only respond to the first thing
|
||||
let intent = request.inputs.into_iter().next();
|
||||
|
@ -54,11 +55,11 @@ impl GoogleHome {
|
|||
|
||||
payload
|
||||
.await
|
||||
.ok_or(FullfillmentError::ExpectedOnePayload)
|
||||
.ok_or(FulfillmentError::ExpectedOnePayload)
|
||||
.map(|payload| Response::new(&request.request_id, payload))
|
||||
}
|
||||
|
||||
async fn sync<T: AsGoogleHomeDevice + ?Sized + 'static>(
|
||||
async fn sync<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>(
|
||||
&self,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||
) -> sync::Payload {
|
||||
|
@ -75,7 +76,7 @@ impl GoogleHome {
|
|||
resp_payload
|
||||
}
|
||||
|
||||
async fn query<T: AsGoogleHomeDevice + ?Sized + 'static>(
|
||||
async fn query<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>(
|
||||
&self,
|
||||
payload: request::query::Payload,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
||||
|
@ -107,7 +108,7 @@ impl GoogleHome {
|
|||
resp_payload
|
||||
}
|
||||
|
||||
async fn execute<T: AsGoogleHomeDevice + ?Sized + 'static>(
|
||||
async fn execute<T: Cast<dyn GoogleHomeDevice> + ?Sized + 'static>(
|
||||
&self,
|
||||
payload: request::execute::Payload,
|
||||
devices: &HashMap<String, Arc<RwLock<Box<T>>>>,
|
|
@ -2,7 +2,7 @@
|
|||
#![feature(specialization)]
|
||||
#![feature(let_chains)]
|
||||
pub mod device;
|
||||
mod fullfillment;
|
||||
mod fulfillment;
|
||||
|
||||
mod request;
|
||||
mod response;
|
||||
|
@ -13,6 +13,6 @@ pub mod traits;
|
|||
pub mod types;
|
||||
|
||||
pub use device::GoogleHomeDevice;
|
||||
pub use fullfillment::{FullfillmentError, GoogleHome};
|
||||
pub use fulfillment::{FulfillmentError, GoogleHome};
|
||||
pub use request::Request;
|
||||
pub use response::Response;
|
||||
|
|
|
@ -16,8 +16,7 @@ pub enum Trait {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
#[impl_cast::device_trait]
|
||||
pub trait OnOff {
|
||||
pub trait OnOff: Sync + Send {
|
||||
fn is_command_only(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
@ -32,8 +31,7 @@ pub trait OnOff {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
#[impl_cast::device_trait]
|
||||
pub trait Scene {
|
||||
pub trait Scene: Sync + Send {
|
||||
fn is_scene_reversible(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
@ -60,8 +58,7 @@ pub struct AvailableSpeeds {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
#[impl_cast::device_trait]
|
||||
pub trait FanSpeed {
|
||||
pub trait FanSpeed: Sync + Send {
|
||||
fn reversible(&self) -> Option<bool> {
|
||||
None
|
||||
}
|
||||
|
@ -76,8 +73,7 @@ pub trait FanSpeed {
|
|||
}
|
||||
|
||||
#[async_trait]
|
||||
#[impl_cast::device_trait]
|
||||
pub trait HumiditySetting {
|
||||
pub trait HumiditySetting: Sync + Send {
|
||||
// TODO: This implementation is not complete, I have only implemented what I need right now
|
||||
fn query_only_humidity_setting(&self) -> Option<bool> {
|
||||
None
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
[package]
|
||||
name = "impl_cast"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2.0", features = ["extra-traits", "full"] }
|
||||
quote = "1.0"
|
||||
|
||||
[features]
|
||||
debug = [
|
||||
] # If enabled it will add std::fmt::Debug as a trait bound to device_traits
|
|
@ -1,165 +0,0 @@
|
|||
use proc_macro::TokenStream;
|
||||
use quote::{format_ident, quote, ToTokens};
|
||||
use syn::parse::Parse;
|
||||
use syn::{parse_macro_input, Ident, ItemTrait, Path, Token, TypeParamBound};
|
||||
|
||||
struct Attr {
|
||||
name: Ident,
|
||||
traits: Vec<Path>,
|
||||
}
|
||||
|
||||
impl Parse for Attr {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let mut traits = Vec::new();
|
||||
|
||||
let name = input.parse::<Ident>()?;
|
||||
input.parse::<Token![:]>()?;
|
||||
|
||||
loop {
|
||||
let ty = input.parse()?;
|
||||
traits.push(ty);
|
||||
|
||||
if input.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
input.parse::<Token![+]>()?;
|
||||
}
|
||||
|
||||
Ok(Attr { name, traits })
|
||||
}
|
||||
}
|
||||
|
||||
/// This macro enables optional trait bounds on a trait with an appropriate cast trait to convert
|
||||
/// to the optional traits
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// #![feature(specialization)]
|
||||
///
|
||||
/// // Create some traits
|
||||
/// #[impl_cast::device_trait]
|
||||
/// trait OnOff {}
|
||||
/// #[impl_cast::device_trait]
|
||||
/// trait Brightness {}
|
||||
///
|
||||
/// // Create the main device trait
|
||||
/// #[impl_cast::device(As: OnOff + Brightness)]
|
||||
/// trait Device {}
|
||||
///
|
||||
/// // Create an implementation
|
||||
/// struct ExampleDevice {}
|
||||
/// impl Device for ExampleDevice {}
|
||||
/// impl OnOff for ExampleDevice {}
|
||||
///
|
||||
/// // Creates a boxed instance of the example device
|
||||
/// let example_device: Box<dyn Device> = Box::new(ExampleDevice {});
|
||||
///
|
||||
/// // Cast to the OnOff trait, which is implemented
|
||||
/// let as_on_off = As::<dyn OnOff>::cast(example_device.as_ref());
|
||||
/// assert!(as_on_off.is_some());
|
||||
///
|
||||
/// // Cast to the Brightness trait, which is not implemented
|
||||
/// let as_on_off = As::<dyn Brightness>::cast(example_device.as_ref());
|
||||
/// assert!(as_on_off.is_none());
|
||||
///
|
||||
/// // Finally we are going to consume the example device into an instance of the OnOff trait
|
||||
/// let consumed = As::<dyn OnOff>::consume(example_device);
|
||||
/// assert!(consumed.is_some())
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn device(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let Attr { name, traits } = parse_macro_input!(attr);
|
||||
let mut interface: ItemTrait = parse_macro_input!(item);
|
||||
|
||||
let prefix = quote! {
|
||||
pub trait #name<T: ?Sized + 'static> {
|
||||
fn is(&self) -> bool;
|
||||
fn cast(&self) -> Option<&T>;
|
||||
fn cast_mut(&mut self) -> Option<&mut T>;
|
||||
}
|
||||
};
|
||||
|
||||
traits.iter().for_each(|device_trait| {
|
||||
interface.supertraits.push(TypeParamBound::Verbatim(quote! {
|
||||
#name<dyn #device_trait>
|
||||
}));
|
||||
});
|
||||
|
||||
let interface_ident = format_ident!("{}", interface.ident);
|
||||
let impls = traits
|
||||
.iter()
|
||||
.map(|device_trait| {
|
||||
quote! {
|
||||
// Default impl
|
||||
impl<T> #name<dyn #device_trait> for T
|
||||
where
|
||||
T: #interface_ident + 'static,
|
||||
{
|
||||
default fn is(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
default fn cast(&self) -> Option<&(dyn #device_trait + 'static)> {
|
||||
None
|
||||
}
|
||||
|
||||
default fn cast_mut(&mut self) -> Option<&mut (dyn #device_trait + 'static)> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// Specialization, should not cause any unsoundness as we dispatch based on
|
||||
// #device_trait
|
||||
impl<T> #name<dyn #device_trait> for T
|
||||
where
|
||||
T: #interface_ident + #device_trait + 'static,
|
||||
{
|
||||
fn is(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn cast(&self) -> Option<&(dyn #device_trait + 'static)> {
|
||||
Some(self)
|
||||
}
|
||||
|
||||
fn cast_mut(&mut self) -> Option<&mut (dyn #device_trait + 'static)> {
|
||||
Some(self)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.fold(quote! {}, |acc, x| {
|
||||
quote! {
|
||||
// Not sure if this is the right way to do this
|
||||
#acc
|
||||
#x
|
||||
}
|
||||
});
|
||||
|
||||
let tokens = quote! {
|
||||
#interface
|
||||
#prefix
|
||||
#impls
|
||||
};
|
||||
|
||||
tokens.into()
|
||||
}
|
||||
|
||||
// TODO: Not sure if this makes sense to have?
|
||||
/// This macro ensures that the device traits have the correct trait bounds
|
||||
#[proc_macro_attribute]
|
||||
pub fn device_trait(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let mut interface: ItemTrait = parse_macro_input!(item);
|
||||
|
||||
interface.supertraits.push(TypeParamBound::Verbatim(quote! {
|
||||
::core::marker::Sync + ::core::marker::Send
|
||||
}));
|
||||
|
||||
#[cfg(feature = "debug")]
|
||||
interface.supertraits.push(TypeParamBound::Verbatim(quote! {
|
||||
::std::fmt::Debug
|
||||
}));
|
||||
|
||||
interface.into_token_stream().into()
|
||||
}
|
11
src/auth.rs
11
src/auth.rs
|
@ -6,11 +6,6 @@ use serde::Deserialize;
|
|||
|
||||
use crate::error::{ApiError, ApiErrorJson};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct OpenIDConfig {
|
||||
pub base_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct User {
|
||||
pub preferred_username: String,
|
||||
|
@ -19,18 +14,18 @@ pub struct User {
|
|||
#[async_trait]
|
||||
impl<S> FromRequestParts<S> for User
|
||||
where
|
||||
OpenIDConfig: FromRef<S>,
|
||||
String: FromRef<S>,
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = ApiError;
|
||||
|
||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
// Get the state
|
||||
let openid = OpenIDConfig::from_ref(state);
|
||||
let openid_url = String::from_ref(state);
|
||||
|
||||
// Create a request to the auth server
|
||||
// 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
|
||||
if let Some(auth) = parts.headers.get(axum::http::header::AUTHORIZATION) {
|
||||
|
|
57
src/bin/generate_definitions.rs
Normal file
57
src/bin/generate_definitions.rs
Normal file
|
@ -0,0 +1,57 @@
|
|||
use automation::config::MqttDeviceConfig;
|
||||
use automation::devices::{
|
||||
AirFilter, AirFilterConfig, AudioSetup, AudioSetupConfig, ContactSensor, ContactSensorConfig,
|
||||
DebugBridge, DebugBridgeConfig, FlagIDs, HueBridge, HueBridgeConfig, HueGroup, HueGroupConfig,
|
||||
IkeaOutlet, IkeaOutletConfig, KasaOutlet, KasaOutletConfig, LightSensor, LightSensorConfig,
|
||||
Ntfy, NtfyConfig, Presence, PresenceConfig, PresenceDeviceConfig, TriggerConfig, WakeOnLAN,
|
||||
WakeOnLANConfig, Washer, WasherConfig,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
println!("-- WARN: This file is automatically generated, do not manually edit\n");
|
||||
println!("---@meta");
|
||||
|
||||
println!("{}", MqttDeviceConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", AirFilter::generate_lua_definition());
|
||||
println!("{}", AirFilterConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", AudioSetup::generate_lua_definition());
|
||||
println!("{}", AudioSetupConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", ContactSensor::generate_lua_definition());
|
||||
println!("{}", ContactSensorConfig::generate_lua_definition());
|
||||
println!("{}", PresenceDeviceConfig::generate_lua_definition());
|
||||
println!("{}", TriggerConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", DebugBridge::generate_lua_definition());
|
||||
println!("{}", DebugBridgeConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", HueBridge::generate_lua_definition());
|
||||
println!("{}", HueBridgeConfig::generate_lua_definition());
|
||||
println!("{}", FlagIDs::generate_lua_definition());
|
||||
|
||||
println!("{}", HueGroup::generate_lua_definition());
|
||||
println!("{}", HueGroupConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", IkeaOutlet::generate_lua_definition());
|
||||
println!("{}", IkeaOutletConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", KasaOutlet::generate_lua_definition());
|
||||
println!("{}", KasaOutletConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", LightSensor::generate_lua_definition());
|
||||
println!("{}", LightSensorConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", Ntfy::generate_lua_definition());
|
||||
println!("{}", NtfyConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", Presence::generate_lua_definition());
|
||||
println!("{}", PresenceConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", WakeOnLAN::generate_lua_definition());
|
||||
println!("{}", WakeOnLANConfig::generate_lua_definition());
|
||||
|
||||
println!("{}", Washer::generate_lua_definition());
|
||||
println!("{}", WasherConfig::generate_lua_definition());
|
||||
}
|
112
src/config.rs
112
src/config.rs
|
@ -1,31 +1,9 @@
|
|||
use std::fs;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::time::Duration;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use regex::{Captures, Regex};
|
||||
use automation_macro::LuaTypeDefinition;
|
||||
use rumqttc::{MqttOptions, Transport};
|
||||
use serde::{Deserialize, Deserializer};
|
||||
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,
|
||||
}
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct MqttConfig {
|
||||
|
@ -52,90 +30,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)]
|
||||
pub struct FullfillmentConfig {
|
||||
#[serde(default = "default_fullfillment_ip")]
|
||||
pub struct FulfillmentConfig {
|
||||
pub openid_url: String,
|
||||
#[serde(default = "default_fulfillment_ip")]
|
||||
pub ip: Ipv4Addr,
|
||||
#[serde(default = "default_fullfillment_port")]
|
||||
#[serde(default = "default_fulfillment_port")]
|
||||
pub port: u16,
|
||||
}
|
||||
|
||||
impl From<FullfillmentConfig> for SocketAddr {
|
||||
fn from(fullfillment: FullfillmentConfig) -> Self {
|
||||
(fullfillment.ip, fullfillment.port).into()
|
||||
impl From<FulfillmentConfig> for SocketAddr {
|
||||
fn from(fulfillment: FulfillmentConfig) -> Self {
|
||||
(fulfillment.ip, fulfillment.port).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for FullfillmentConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
ip: default_fullfillment_ip(),
|
||||
port: default_fullfillment_port(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_fullfillment_ip() -> Ipv4Addr {
|
||||
fn default_fulfillment_ip() -> Ipv4Addr {
|
||||
[0, 0, 0, 0].into()
|
||||
}
|
||||
|
||||
fn default_fullfillment_port() -> u16 {
|
||||
fn default_fulfillment_port() -> u16 {
|
||||
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, LuaTypeDefinition)]
|
||||
pub struct InfoConfig {
|
||||
pub name: String,
|
||||
pub room: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
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, LuaTypeDefinition)]
|
||||
pub struct MqttDeviceConfig {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,75 +1,64 @@
|
|||
use std::collections::HashMap;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use enum_dispatch::enum_dispatch;
|
||||
use futures::future::join_all;
|
||||
use google_home::traits::OnOff;
|
||||
use rumqttc::{matches, AsyncClient, QoS};
|
||||
use serde::Deserialize;
|
||||
use mlua::FromLua;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use tokio_cron_scheduler::{Job, JobScheduler};
|
||||
use tracing::{debug, error, instrument, trace};
|
||||
use tracing::{debug, instrument, trace};
|
||||
|
||||
use crate::devices::{
|
||||
AirFilterConfig, As, AudioSetupConfig, ContactSensorConfig, DebugBridgeConfig, Device,
|
||||
HueBridgeConfig, HueGroupConfig, IkeaOutletConfig, KasaOutletConfig, LightSensorConfig,
|
||||
WakeOnLANConfig, WasherConfig,
|
||||
};
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::devices::Device;
|
||||
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
|
||||
use crate::schedule::{Action, Schedule};
|
||||
use crate::LUA;
|
||||
|
||||
pub struct ConfigExternal<'a> {
|
||||
pub client: &'a AsyncClient,
|
||||
pub device_manager: &'a DeviceManager,
|
||||
pub event_channel: &'a EventChannel,
|
||||
#[derive(Debug, FromLua, Clone)]
|
||||
pub struct WrappedDevice(Arc<RwLock<Box<dyn Device>>>);
|
||||
|
||||
impl WrappedDevice {
|
||||
pub fn new(device: Box<dyn Device>) -> Self {
|
||||
Self(Arc::new(RwLock::new(device)))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[enum_dispatch]
|
||||
pub trait DeviceConfig {
|
||||
async fn create(
|
||||
self,
|
||||
identifier: &str,
|
||||
ext: &ConfigExternal,
|
||||
) -> Result<Box<dyn Device>, DeviceConfigError>;
|
||||
impl Deref for WrappedDevice {
|
||||
type Target = Arc<RwLock<Box<dyn Device>>>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[enum_dispatch(DeviceConfig)]
|
||||
pub enum DeviceConfigs {
|
||||
AirFilter(AirFilterConfig),
|
||||
AudioSetup(AudioSetupConfig),
|
||||
ContactSensor(ContactSensorConfig),
|
||||
DebugBridge(DebugBridgeConfig),
|
||||
IkeaOutlet(IkeaOutletConfig),
|
||||
KasaOutlet(KasaOutletConfig),
|
||||
WakeOnLAN(WakeOnLANConfig),
|
||||
Washer(WasherConfig),
|
||||
HueBridge(HueBridgeConfig),
|
||||
HueGroup(HueGroupConfig),
|
||||
LightSensor(LightSensorConfig),
|
||||
impl DerefMut for WrappedDevice {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
impl mlua::UserData for WrappedDevice {
|
||||
fn add_methods<'lua, M: mlua::prelude::LuaUserDataMethods<'lua, Self>>(methods: &mut M) {
|
||||
methods.add_async_method("get_id", |_lua, this, _: ()| async {
|
||||
Ok(crate::devices::Device::get_id(this.0.read().await.as_ref()))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub type WrappedDevice = Arc<RwLock<Box<dyn Device>>>;
|
||||
pub type DeviceMap = HashMap<String, WrappedDevice>;
|
||||
pub type DeviceMap = HashMap<String, Arc<RwLock<Box<dyn Device>>>>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct DeviceManager {
|
||||
devices: Arc<RwLock<DeviceMap>>,
|
||||
client: AsyncClient,
|
||||
event_channel: EventChannel,
|
||||
scheduler: JobScheduler,
|
||||
}
|
||||
|
||||
impl DeviceManager {
|
||||
pub fn new(client: AsyncClient) -> Self {
|
||||
pub async fn new() -> Self {
|
||||
let (event_channel, mut event_rx) = EventChannel::new();
|
||||
|
||||
let device_manager = Self {
|
||||
devices: Arc::new(RwLock::new(HashMap::new())),
|
||||
client,
|
||||
event_channel,
|
||||
scheduler: JobScheduler::new().await.unwrap(),
|
||||
};
|
||||
|
||||
tokio::spawn({
|
||||
|
@ -85,97 +74,17 @@ impl DeviceManager {
|
|||
}
|
||||
});
|
||||
|
||||
device_manager.scheduler.start().await.unwrap();
|
||||
|
||||
device_manager
|
||||
}
|
||||
|
||||
// TODO: This function is currently extremely cursed...
|
||||
pub async fn add_schedule(&self, schedule: Schedule) {
|
||||
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();
|
||||
pub async fn add(&self, device: &WrappedDevice) {
|
||||
let id = device.read().await.get_id().to_owned();
|
||||
|
||||
debug!(id, "Adding device");
|
||||
|
||||
// If the device listens to mqtt, subscribe to the topics
|
||||
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(())
|
||||
self.devices.write().await.insert(id, device.0.clone());
|
||||
}
|
||||
|
||||
pub fn event_channel(&self) -> EventChannel {
|
||||
|
@ -183,7 +92,12 @@ impl DeviceManager {
|
|||
}
|
||||
|
||||
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> {
|
||||
|
@ -199,17 +113,18 @@ impl DeviceManager {
|
|||
let message = message.clone();
|
||||
async move {
|
||||
let mut device = device.write().await;
|
||||
let device = device.as_mut();
|
||||
if let Some(device) = As::<dyn OnMqtt>::cast_mut(device) {
|
||||
let subscribed = device
|
||||
.topics()
|
||||
.iter()
|
||||
.any(|topic| matches(&message.topic, topic));
|
||||
|
||||
if subscribed {
|
||||
trace!(id, "Handling");
|
||||
device.on_mqtt(message).await;
|
||||
}
|
||||
let device: Option<&mut dyn OnMqtt> = device.as_mut().cast_mut();
|
||||
if let Some(device) = device {
|
||||
// let subscribed = device
|
||||
// .topics()
|
||||
// .iter()
|
||||
// .any(|topic| matches(&message.topic, topic));
|
||||
//
|
||||
// if subscribed {
|
||||
trace!(id, "Handling");
|
||||
device.on_mqtt(message).await;
|
||||
trace!(id, "Done");
|
||||
// }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -220,10 +135,11 @@ impl DeviceManager {
|
|||
let devices = self.devices.read().await;
|
||||
let iter = devices.iter().map(|(id, device)| async move {
|
||||
let mut device = device.write().await;
|
||||
let device = device.as_mut();
|
||||
if let Some(device) = As::<dyn OnDarkness>::cast_mut(device) {
|
||||
let device: Option<&mut dyn OnDarkness> = device.as_mut().cast_mut();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_darkness(dark).await;
|
||||
trace!(id, "Done");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -233,10 +149,11 @@ impl DeviceManager {
|
|||
let devices = self.devices.read().await;
|
||||
let iter = devices.iter().map(|(id, device)| async move {
|
||||
let mut device = device.write().await;
|
||||
let device = device.as_mut();
|
||||
if let Some(device) = As::<dyn OnPresence>::cast_mut(device) {
|
||||
let device: Option<&mut dyn OnPresence> = device.as_mut().cast_mut();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
device.on_presence(presence).await;
|
||||
trace!(id, "Done");
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -248,10 +165,11 @@ impl DeviceManager {
|
|||
let notification = notification.clone();
|
||||
async move {
|
||||
let mut device = device.write().await;
|
||||
let device = device.as_mut();
|
||||
if let Some(device) = As::<dyn OnNotification>::cast_mut(device) {
|
||||
let device: Option<&mut dyn OnNotification> = device.as_mut().cast_mut();
|
||||
if let Some(device) = device {
|
||||
trace!(id, "Handling");
|
||||
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()))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,57 +1,34 @@
|
|||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use google_home::device::Name;
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::{AvailableSpeeds, FanSpeed, HumiditySetting, OnOff, Speed, SpeedValues};
|
||||
use google_home::types::Type;
|
||||
use google_home::GoogleHomeDevice;
|
||||
use rumqttc::{AsyncClient, Publish};
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, error, warn};
|
||||
use rumqttc::Publish;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig};
|
||||
use crate::devices::Device;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::OnMqtt;
|
||||
use crate::messages::{AirFilterFanState, AirFilterState, SetAirFilterFanState};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct AirFilterConfig {
|
||||
#[serde(flatten)]
|
||||
info: InfoConfig,
|
||||
#[serde(flatten)]
|
||||
mqtt: MqttDeviceConfig,
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
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)]
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct AirFilter {
|
||||
identifier: String,
|
||||
info: InfoConfig,
|
||||
mqtt: MqttDeviceConfig,
|
||||
config: AirFilterConfig,
|
||||
|
||||
client: AsyncClient,
|
||||
last_known_state: AirFilterState,
|
||||
}
|
||||
|
||||
|
@ -59,11 +36,12 @@ impl AirFilter {
|
|||
async fn set_speed(&self, state: AirFilterFanState) {
|
||||
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
|
||||
self.client
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
topic.clone(),
|
||||
&topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
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 {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
fn get_id(&self) -> String {
|
||||
self.config.info.identifier()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for AirFilter {
|
||||
fn topics(&self) -> Vec<&str> {
|
||||
vec![&self.mqtt.topic]
|
||||
}
|
||||
|
||||
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) {
|
||||
Ok(state) => state,
|
||||
Err(err) => {
|
||||
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -99,7 +100,7 @@ impl OnMqtt for AirFilter {
|
|||
return;
|
||||
}
|
||||
|
||||
debug!(id = self.identifier, "Updating state to {state:?}");
|
||||
debug!(id = Device::get_id(self), "Updating state to {state:?}");
|
||||
|
||||
self.last_known_state = state;
|
||||
}
|
||||
|
@ -111,10 +112,10 @@ impl GoogleHomeDevice for AirFilter {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -123,7 +124,7 @@ impl GoogleHomeDevice for AirFilter {
|
|||
}
|
||||
|
||||
fn get_room_hint(&self) -> Option<&str> {
|
||||
self.info.room.as_deref()
|
||||
self.config.info.room.as_deref()
|
||||
}
|
||||
|
||||
fn will_report_state(&self) -> bool {
|
||||
|
|
|
@ -1,110 +1,94 @@
|
|||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use google_home::traits::OnOff;
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::Device;
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig, WrappedDevice};
|
||||
use crate::devices::As;
|
||||
use crate::device_manager::WrappedDevice;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{OnMqtt, OnPresence};
|
||||
use crate::messages::{RemoteAction, RemoteMessage};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct AudioSetupConfig {
|
||||
#[serde(flatten)]
|
||||
mqtt: MqttDeviceConfig,
|
||||
mixer: String,
|
||||
speakers: String,
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[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]
|
||||
impl DeviceConfig for AudioSetupConfig {
|
||||
async fn create(
|
||||
self,
|
||||
identifier: &str,
|
||||
ext: &ConfigExternal,
|
||||
) -> Result<Box<dyn Device>, DeviceConfigError> {
|
||||
trace!(id = identifier, "Setting up AudioSetup");
|
||||
impl LuaDeviceCreate for AudioSetup {
|
||||
type Config = AudioSetupConfig;
|
||||
type Error = DeviceConfigError;
|
||||
|
||||
// TODO: Make sure they implement OnOff?
|
||||
let mixer = ext
|
||||
.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(),
|
||||
))?;
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.identifier, "Setting up AudioSetup");
|
||||
|
||||
if !As::<dyn OnOff>::is(mixer.read().await.as_ref()) {
|
||||
return Err(DeviceConfigError::MissingTrait(self.mixer, "OnOff".into()));
|
||||
{
|
||||
let mixer = config.mixer.read().await;
|
||||
let mixer_id = mixer.get_id().to_owned();
|
||||
if (mixer.as_ref().cast() as Option<&dyn OnOff>).is_none() {
|
||||
return Err(DeviceConfigError::MissingTrait(mixer_id, "OnOff".into()));
|
||||
}
|
||||
|
||||
let speakers = config.speakers.read().await;
|
||||
let speakers_id = speakers.get_id().to_owned();
|
||||
if (speakers.as_ref().cast() as Option<&dyn OnOff>).is_none() {
|
||||
return Err(DeviceConfigError::MissingTrait(speakers_id, "OnOff".into()));
|
||||
}
|
||||
}
|
||||
|
||||
let speakers =
|
||||
ext.device_manager
|
||||
.get(&self.speakers)
|
||||
.await
|
||||
.ok_or(DeviceConfigError::MissingChild(
|
||||
identifier.into(),
|
||||
self.speakers.clone(),
|
||||
))?;
|
||||
config
|
||||
.client
|
||||
.subscribe(&config.mqtt.topic, rumqttc::QoS::AtLeastOnce)
|
||||
.await?;
|
||||
|
||||
if !As::<dyn OnOff>::is(speakers.read().await.as_ref()) {
|
||||
return Err(DeviceConfigError::MissingTrait(
|
||||
self.speakers,
|
||||
"OnOff".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let device = AudioSetup {
|
||||
identifier: identifier.into(),
|
||||
mqtt: self.mqtt,
|
||||
mixer,
|
||||
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 {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for AudioSetup {
|
||||
fn topics(&self) -> Vec<&str> {
|
||||
vec![&self.mqtt.topic]
|
||||
}
|
||||
|
||||
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) {
|
||||
Ok(message) => message.action(),
|
||||
Err(err) => {
|
||||
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||
error!(
|
||||
id = self.config.identifier,
|
||||
"Failed to parse message: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let mut mixer = self.mixer.write().await;
|
||||
let mut speakers = self.speakers.write().await;
|
||||
let mut mixer = self.config.mixer.write().await;
|
||||
let mut speakers = self.config.speakers.write().await;
|
||||
if let (Some(mixer), Some(speakers)) = (
|
||||
As::<dyn OnOff>::cast_mut(mixer.as_mut()),
|
||||
As::<dyn OnOff>::cast_mut(speakers.as_mut()),
|
||||
mixer.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
speakers.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
) {
|
||||
match action {
|
||||
RemoteAction::On => {
|
||||
|
@ -135,16 +119,16 @@ impl OnMqtt for AudioSetup {
|
|||
#[async_trait]
|
||||
impl OnPresence for AudioSetup {
|
||||
async fn on_presence(&mut self, presence: bool) {
|
||||
let mut mixer = self.mixer.write().await;
|
||||
let mut speakers = self.speakers.write().await;
|
||||
let mut mixer = self.config.mixer.write().await;
|
||||
let mut speakers = self.config.speakers.write().await;
|
||||
|
||||
if let (Some(mixer), Some(speakers)) = (
|
||||
As::<dyn OnOff>::cast_mut(mixer.as_mut()),
|
||||
As::<dyn OnOff>::cast_mut(speakers.as_mut()),
|
||||
mixer.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
speakers.as_mut().cast_mut() as Option<&mut dyn OnOff>,
|
||||
) {
|
||||
// Turn off the audio setup when we leave the house
|
||||
if !presence {
|
||||
debug!(id = self.identifier, "Turning devices off");
|
||||
debug!(id = self.config.identifier, "Turning devices off");
|
||||
speakers.set_on(false).await.unwrap();
|
||||
mixer.set_on(false).await.unwrap();
|
||||
}
|
||||
|
|
|
@ -1,130 +1,120 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use google_home::traits::OnOff;
|
||||
use rumqttc::AsyncClient;
|
||||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DurationSeconds};
|
||||
use mlua::FromLua;
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::Device;
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig, WrappedDevice};
|
||||
use crate::devices::{As, DEFAULT_PRESENCE};
|
||||
use crate::device_manager::WrappedDevice;
|
||||
use crate::devices::DEFAULT_PRESENCE;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{OnMqtt, OnPresence};
|
||||
use crate::messages::{ContactMessage, PresenceMessage};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
use crate::traits::Timeout;
|
||||
|
||||
// NOTE: If we add more presence devices we might need to move this out of here
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct PresenceDeviceConfig {
|
||||
#[serde(flatten)]
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[serde_as(as = "DurationSeconds")]
|
||||
#[device_config(from(u64), with(Duration::from_secs))]
|
||||
pub timeout: Duration,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct TriggerConfig {
|
||||
devices: Vec<String>,
|
||||
#[serde(default)]
|
||||
#[serde_as(as = "DurationSeconds")]
|
||||
pub timeout: Duration,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
struct TriggerDevicesHelper(Vec<WrappedDevice>);
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ContactSensorConfig {
|
||||
#[serde(flatten)]
|
||||
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))
|
||||
impl<'lua> FromLua<'lua> for TriggerDevicesHelper {
|
||||
fn from_lua(value: mlua::Value<'lua>, lua: &'lua mlua::Lua) -> mlua::Result<Self> {
|
||||
Ok(TriggerDevicesHelper(mlua::FromLua::from_lua(value, lua)?))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Trigger {
|
||||
devices: Vec<(WrappedDevice, bool)>,
|
||||
timeout: Duration, // Timeout in seconds
|
||||
impl From<TriggerDevicesHelper> for Vec<(WrappedDevice, bool)> {
|
||||
fn from(value: TriggerDevicesHelper) -> Self {
|
||||
value.0.into_iter().map(|device| (device, false)).collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ContactSensor {
|
||||
identifier: String,
|
||||
mqtt: MqttDeviceConfig,
|
||||
presence: Option<PresenceDeviceConfig>,
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct TriggerConfig {
|
||||
#[device_config(from_lua, from(TriggerDevicesHelper))]
|
||||
pub devices: Vec<(WrappedDevice, bool)>,
|
||||
#[device_config(default, from(Option<u64>), with(|t: Option<_>| t.map(Duration::from_secs)))]
|
||||
pub timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
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,
|
||||
is_closed: bool,
|
||||
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 device = device.read().await;
|
||||
let id = device.get_id().to_owned();
|
||||
if (device.as_ref().cast() as Option<&dyn OnOff>).is_none() {
|
||||
return Err(DeviceConfigError::MissingTrait(id, "OnOff".into()));
|
||||
}
|
||||
|
||||
if trigger.timeout.is_none()
|
||||
&& (device.as_ref().cast() as Option<&dyn Timeout>).is_none()
|
||||
{
|
||||
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 {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -137,15 +127,18 @@ impl OnPresence for ContactSensor {
|
|||
|
||||
#[async_trait]
|
||||
impl OnMqtt for ContactSensor {
|
||||
fn topics(&self) -> Vec<&str> {
|
||||
vec![&self.mqtt.topic]
|
||||
}
|
||||
|
||||
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) {
|
||||
Ok(state) => state.is_closed(),
|
||||
Err(err) => {
|
||||
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||
error!(
|
||||
id = self.config.identifier,
|
||||
"Failed to parse message: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -154,14 +147,14 @@ impl OnMqtt for ContactSensor {
|
|||
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;
|
||||
|
||||
if let Some(trigger) = &mut self.trigger {
|
||||
if let Some(trigger) = &mut self.config.trigger {
|
||||
if !self.is_closed {
|
||||
for (light, previous) in &mut trigger.devices {
|
||||
let mut light = light.write().await;
|
||||
if let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut()) {
|
||||
if let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff> {
|
||||
*previous = light.is_on().await.unwrap();
|
||||
light.set_on(true).await.ok();
|
||||
}
|
||||
|
@ -171,12 +164,15 @@ impl OnMqtt for ContactSensor {
|
|||
let mut light = light.write().await;
|
||||
if !previous {
|
||||
// If the timeout is zero just turn the light off directly
|
||||
if trigger.timeout.is_zero()
|
||||
&& let Some(light) = As::<dyn OnOff>::cast_mut(light.as_mut())
|
||||
if trigger.timeout.is_none()
|
||||
&& let Some(light) = light.as_mut().cast_mut() as Option<&mut dyn OnOff>
|
||||
{
|
||||
light.set_on(false).await.ok();
|
||||
} else if let Some(light) = As::<dyn Timeout>::cast_mut(light.as_mut()) {
|
||||
light.start_timeout(trigger.timeout).await.unwrap();
|
||||
} else if let Some(timeout) = trigger.timeout
|
||||
&& let Some(light) =
|
||||
light.as_mut().cast_mut() as Option<&mut dyn Timeout>
|
||||
{
|
||||
light.start_timeout(timeout).await.unwrap();
|
||||
}
|
||||
// TODO: Put a warning/error on creation if either of this has to option to fail
|
||||
}
|
||||
|
@ -186,7 +182,7 @@ impl OnMqtt for ContactSensor {
|
|||
|
||||
// Check if this contact sensor works as a presence device
|
||||
// If not we are done here
|
||||
let presence = match &self.presence {
|
||||
let presence = match &self.config.presence {
|
||||
Some(presence) => presence,
|
||||
None => return,
|
||||
};
|
||||
|
@ -201,9 +197,10 @@ impl OnMqtt for ContactSensor {
|
|||
// This is to prevent the house from being marked as present for however long the
|
||||
// timeout is set when leaving the house
|
||||
if !self.overall_presence {
|
||||
self.client
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
presence.mqtt.topic.clone(),
|
||||
&presence.mqtt.topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
serde_json::to_string(&PresenceMessage::new(true)).unwrap(),
|
||||
|
@ -219,8 +216,8 @@ impl OnMqtt for ContactSensor {
|
|||
}
|
||||
} else {
|
||||
// Once the door is closed again we start a timeout for removing the presence
|
||||
let client = self.client.clone();
|
||||
let id = self.identifier.clone();
|
||||
let client = self.config.client.clone();
|
||||
let id = self.config.identifier.clone();
|
||||
let timeout = presence.timeout;
|
||||
let topic = presence.mqtt.topic.clone();
|
||||
self.handle = Some(tokio::spawn(async move {
|
||||
|
@ -228,7 +225,7 @@ impl OnMqtt for ContactSensor {
|
|||
tokio::time::sleep(timeout).await;
|
||||
debug!(id, "Removing door device!");
|
||||
client
|
||||
.publish(topic.clone(), rumqttc::QoS::AtLeastOnce, false, "")
|
||||
.publish(&topic, rumqttc::QoS::AtLeastOnce, false, "")
|
||||
.await
|
||||
.map_err(|err| warn!("Failed to publish presence on {topic}: {err}"))
|
||||
.ok();
|
||||
|
|
|
@ -1,48 +1,44 @@
|
|||
use async_trait::async_trait;
|
||||
use rumqttc::AsyncClient;
|
||||
use serde::Deserialize;
|
||||
use tracing::warn;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use tracing::{trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig};
|
||||
use crate::devices::Device;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{OnDarkness, OnPresence};
|
||||
use crate::messages::{DarknessMessage, PresenceMessage};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, LuaDeviceConfig, Clone, LuaTypeDefinition)]
|
||||
pub struct DebugBridgeConfig {
|
||||
#[serde(flatten)]
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct DebugBridge {
|
||||
config: DebugBridgeConfig,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DeviceConfig for DebugBridgeConfig {
|
||||
async fn create(
|
||||
self,
|
||||
identifier: &str,
|
||||
ext: &ConfigExternal,
|
||||
) -> Result<Box<dyn Device>, DeviceConfigError> {
|
||||
let device = DebugBridge {
|
||||
identifier: identifier.into(),
|
||||
mqtt: self.mqtt,
|
||||
client: ext.client.clone(),
|
||||
};
|
||||
impl LuaDeviceCreate for DebugBridge {
|
||||
type Config = DebugBridgeConfig;
|
||||
type Error = Infallible;
|
||||
|
||||
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 {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,8 +46,9 @@ impl Device for DebugBridge {
|
|||
impl OnPresence for DebugBridge {
|
||||
async fn on_presence(&mut self, presence: bool) {
|
||||
let message = PresenceMessage::new(presence);
|
||||
let topic = format!("{}/presence", self.mqtt.topic);
|
||||
self.client
|
||||
let topic = format!("{}/presence", self.config.mqtt.topic);
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
|
@ -62,7 +59,7 @@ impl OnPresence for DebugBridge {
|
|||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to update presence on {}/presence: {err}",
|
||||
self.mqtt.topic
|
||||
self.config.mqtt.topic
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
|
@ -73,8 +70,9 @@ impl OnPresence for DebugBridge {
|
|||
impl OnDarkness for DebugBridge {
|
||||
async fn on_darkness(&mut self, dark: bool) {
|
||||
let message = DarknessMessage::new(dark);
|
||||
let topic = format!("{}/darkness", self.mqtt.topic);
|
||||
self.client
|
||||
let topic = format!("{}/darkness", self.config.mqtt.topic);
|
||||
self.config
|
||||
.client
|
||||
.publish(
|
||||
topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
|
@ -85,7 +83,7 @@ impl OnDarkness for DebugBridge {
|
|||
.map_err(|err| {
|
||||
warn!(
|
||||
"Failed to update presence on {}/presence: {err}",
|
||||
self.mqtt.topic
|
||||
self.config.mqtt.topic
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use std::convert::Infallible;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::{error, trace, warn};
|
||||
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig};
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::devices::Device;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{OnDarkness, OnPresence};
|
||||
|
||||
#[derive(Debug)]
|
||||
|
@ -15,43 +16,24 @@ pub enum Flag {
|
|||
Darkness,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize, LuaTypeDefinition)]
|
||||
pub struct FlagIDs {
|
||||
pub presence: isize,
|
||||
pub darkness: isize,
|
||||
presence: isize,
|
||||
darkness: isize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, LuaDeviceConfig, Clone, LuaTypeDefinition)]
|
||||
pub struct HueBridgeConfig {
|
||||
pub ip: Ipv4Addr,
|
||||
pub identifier: String,
|
||||
#[device_config(rename("ip"), from(Ipv4Addr), with(|ip| SocketAddr::new(ip, 80)))]
|
||||
pub addr: SocketAddr,
|
||||
pub login: String,
|
||||
pub flags: FlagIDs,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DeviceConfig for HueBridgeConfig {
|
||||
async fn create(
|
||||
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, LuaDevice)]
|
||||
pub struct HueBridge {
|
||||
config: HueBridgeConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
|
@ -59,16 +41,27 @@ struct FlagMessage {
|
|||
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 {
|
||||
pub async fn set_flag(&self, flag: Flag, value: bool) {
|
||||
let flag_id = match flag {
|
||||
Flag::Presence => self.flag_ids.presence,
|
||||
Flag::Darkness => self.flag_ids.darkness,
|
||||
Flag::Presence => self.config.flags.presence,
|
||||
Flag::Darkness => self.config.flags.darkness,
|
||||
};
|
||||
|
||||
let url = format!(
|
||||
"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");
|
||||
|
@ -93,8 +86,8 @@ impl HueBridge {
|
|||
}
|
||||
|
||||
impl Device for HueBridge {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,102 +3,105 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::OnOff;
|
||||
use rumqttc::Publish;
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, error, warn};
|
||||
use rumqttc::{Publish, SubscribeFilter};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::Device;
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig};
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::OnMqtt;
|
||||
use crate::messages::{RemoteAction, RemoteMessage};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
use crate::traits::Timeout;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct HueGroupConfig {
|
||||
pub ip: Ipv4Addr,
|
||||
pub identifier: String,
|
||||
#[device_config(rename("ip"), from(Ipv4Addr), with(|ip| SocketAddr::new(ip, 80)))]
|
||||
pub addr: SocketAddr,
|
||||
pub login: String,
|
||||
pub group_id: isize,
|
||||
pub timer_id: isize,
|
||||
pub scene_id: String,
|
||||
#[serde(default)]
|
||||
#[device_config(default)]
|
||||
pub remotes: Vec<MqttDeviceConfig>,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DeviceConfig for HueGroupConfig {
|
||||
async fn create(
|
||||
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>,
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct HueGroup {
|
||||
config: HueGroupConfig,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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 {
|
||||
format!("{}/schedules/{}", self.url_base(), self.timer_id)
|
||||
format!("{}/schedules/{}", self.url_base(), self.config.timer_id)
|
||||
}
|
||||
|
||||
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 {
|
||||
format!("{}/groups/{}", self.url_base(), self.group_id)
|
||||
format!("{}/groups/{}", self.url_base(), self.config.group_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for HueGroup {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
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) {
|
||||
if !self
|
||||
.config
|
||||
.remotes
|
||||
.iter()
|
||||
.any(|remote| rumqttc::matches(&message.topic, &remote.topic))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
let action = match RemoteMessage::try_from(message) {
|
||||
Ok(message) => message.action(),
|
||||
Err(err) => {
|
||||
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||
error!(
|
||||
id = self.config.identifier,
|
||||
"Failed to parse message: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -122,7 +125,7 @@ impl OnOff for HueGroup {
|
|||
self.stop_timeout().await.unwrap();
|
||||
|
||||
let message = if on {
|
||||
message::Action::scene(self.scene_id.clone())
|
||||
message::Action::scene(self.config.scene_id.clone())
|
||||
} else {
|
||||
message::Action::on(false)
|
||||
};
|
||||
|
@ -137,10 +140,13 @@ impl OnOff for HueGroup {
|
|||
Ok(res) => {
|
||||
let status = res.status();
|
||||
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(())
|
||||
|
@ -156,13 +162,19 @@ impl OnOff for HueGroup {
|
|||
Ok(res) => {
|
||||
let status = res.status();
|
||||
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 {
|
||||
Ok(info) => info.any_on(),
|
||||
Err(err) => {
|
||||
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||
error!(
|
||||
id = self.config.identifier,
|
||||
"Failed to parse message: {err}"
|
||||
);
|
||||
// TODO: Error code
|
||||
return Ok(false);
|
||||
}
|
||||
|
@ -170,7 +182,7 @@ impl OnOff for HueGroup {
|
|||
|
||||
return Ok(on);
|
||||
}
|
||||
Err(err) => error!(id = self.identifier, "Error: {err}"),
|
||||
Err(err) => error!(id = self.config.identifier, "Error: {err}"),
|
||||
}
|
||||
|
||||
Ok(false)
|
|
@ -2,22 +2,22 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::{self, OnOff};
|
||||
use google_home::types::Type;
|
||||
use google_home::{device, GoogleHomeDevice};
|
||||
use rumqttc::{matches, AsyncClient, Publish};
|
||||
use rumqttc::{matches, Publish, SubscribeFilter};
|
||||
use serde::Deserialize;
|
||||
use serde_with::{serde_as, DurationSeconds};
|
||||
use tokio::task::JoinHandle;
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig};
|
||||
use crate::devices::Device;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{OnMqtt, OnPresence};
|
||||
use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage};
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
use crate::traits::Timeout;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
|
||||
|
@ -28,77 +28,39 @@ pub enum OutletType {
|
|||
Light,
|
||||
}
|
||||
|
||||
#[serde_as]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct IkeaOutletConfig {
|
||||
#[serde(flatten)]
|
||||
info: InfoConfig,
|
||||
#[serde(flatten)]
|
||||
mqtt: MqttDeviceConfig,
|
||||
#[serde(default = "default_outlet_type")]
|
||||
outlet_type: OutletType,
|
||||
#[serde_as(as = "Option<DurationSeconds>")]
|
||||
timeout: Option<Duration>, // Timeout in seconds
|
||||
#[serde(default)]
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(default(OutletType::Outlet))]
|
||||
pub outlet_type: OutletType,
|
||||
#[device_config(default, from(Option<u64>), with(|t: Option<_>| t.map(Duration::from_secs)))]
|
||||
pub timeout: Option<Duration>,
|
||||
#[device_config(default)]
|
||||
pub remotes: Vec<MqttDeviceConfig>,
|
||||
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
fn default_outlet_type() -> OutletType {
|
||||
OutletType::Outlet
|
||||
}
|
||||
#[derive(Debug, LuaDevice)]
|
||||
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,
|
||||
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 topic = format!("{}/set", topic);
|
||||
// TODO: Handle potential errors here
|
||||
client
|
||||
.publish(
|
||||
topic.clone(),
|
||||
&topic,
|
||||
rumqttc::QoS::AtLeastOnce,
|
||||
false,
|
||||
serde_json::to_string(&message).unwrap(),
|
||||
|
@ -108,34 +70,53 @@ async fn set_on(client: AsyncClient, topic: &str, on: bool) {
|
|||
.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 {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
fn get_id(&self) -> String {
|
||||
self.config.info.identifier()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
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) {
|
||||
// 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
|
||||
let state = match OnOffMessage::try_from(message) {
|
||||
Ok(state) => state.state(),
|
||||
Err(err) => {
|
||||
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -148,18 +129,23 @@ impl OnMqtt for IkeaOutlet {
|
|||
// Abort any timer that is currently running
|
||||
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;
|
||||
|
||||
// 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();
|
||||
}
|
||||
} else {
|
||||
} else if self
|
||||
.config
|
||||
.remotes
|
||||
.iter()
|
||||
.any(|remote| rumqttc::matches(&message.topic, &remote.topic))
|
||||
{
|
||||
let action = match RemoteMessage::try_from(message) {
|
||||
Ok(message) => message.action(),
|
||||
Err(err) => {
|
||||
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -178,8 +164,8 @@ impl OnMqtt for IkeaOutlet {
|
|||
impl OnPresence for IkeaOutlet {
|
||||
async fn on_presence(&mut self, presence: bool) {
|
||||
// Turn off the outlet when we leave the house (Not if it is a battery charger)
|
||||
if !presence && self.outlet_type != OutletType::Charger {
|
||||
debug!(id = self.identifier, "Turning device off");
|
||||
if !presence && self.config.outlet_type != OutletType::Charger {
|
||||
debug!(id = Device::get_id(self), "Turning device off");
|
||||
self.set_on(false).await.ok();
|
||||
}
|
||||
}
|
||||
|
@ -187,7 +173,7 @@ impl OnPresence for IkeaOutlet {
|
|||
|
||||
impl GoogleHomeDevice for IkeaOutlet {
|
||||
fn get_device_type(&self) -> Type {
|
||||
match self.outlet_type {
|
||||
match self.config.outlet_type {
|
||||
OutletType::Outlet => Type::Outlet,
|
||||
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
|
||||
|
@ -196,10 +182,10 @@ impl GoogleHomeDevice for IkeaOutlet {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -208,7 +194,7 @@ impl GoogleHomeDevice for IkeaOutlet {
|
|||
}
|
||||
|
||||
fn get_room_hint(&self) -> Option<&str> {
|
||||
self.info.room.as_deref()
|
||||
self.config.info.room.as_deref()
|
||||
}
|
||||
|
||||
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> {
|
||||
set_on(self.client.clone(), &self.mqtt.topic, on).await;
|
||||
set_on(self.config.client.clone(), &self.config.mqtt.topic, on).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -239,15 +225,15 @@ impl crate::traits::Timeout for IkeaOutlet {
|
|||
// Turn the kettle of after the specified timeout
|
||||
// TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet
|
||||
// get dropped
|
||||
let client = self.client.clone();
|
||||
let topic = self.mqtt.topic.clone();
|
||||
let id = self.identifier.clone();
|
||||
let client = self.config.client.clone();
|
||||
let topic = self.config.mqtt.topic.clone();
|
||||
let id = Device::get_id(self).clone();
|
||||
self.handle = Some(tokio::spawn(async move {
|
||||
debug!(id, "Starting timeout ({timeout:?})...");
|
||||
tokio::time::sleep(timeout).await;
|
||||
debug!(id, "Turning outlet off!");
|
||||
// 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
|
||||
set_on(client, &topic, false).await;
|
||||
}));
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
use std::convert::Infallible;
|
||||
use std::net::{Ipv4Addr, SocketAddr};
|
||||
use std::str::Utf8Error;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use bytes::{Buf, BufMut};
|
||||
use google_home::errors::{self, DeviceError};
|
||||
use google_home::traits;
|
||||
|
@ -11,42 +13,34 @@ use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
|||
use tokio::net::TcpStream;
|
||||
use tracing::trace;
|
||||
|
||||
use super::Device;
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig};
|
||||
use crate::error::DeviceConfigError;
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct KasaOutletConfig {
|
||||
ip: Ipv4Addr,
|
||||
pub identifier: String,
|
||||
#[device_config(rename("ip"), from(Ipv4Addr), with(|ip| SocketAddr::new(ip, 9999)))]
|
||||
pub addr: SocketAddr,
|
||||
}
|
||||
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct KasaOutlet {
|
||||
config: KasaOutletConfig,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DeviceConfig for KasaOutletConfig {
|
||||
async fn create(
|
||||
self,
|
||||
identifier: &str,
|
||||
_ext: &ConfigExternal,
|
||||
) -> Result<Box<dyn Device>, DeviceConfigError> {
|
||||
trace!(id = identifier, "Setting up KasaOutlet");
|
||||
impl LuaDeviceCreate for KasaOutlet {
|
||||
type Config = KasaOutletConfig;
|
||||
type Error = Infallible;
|
||||
|
||||
let device = KasaOutlet {
|
||||
identifier: identifier.into(),
|
||||
addr: (self.ip, 9999).into(),
|
||||
};
|
||||
|
||||
Ok(Box::new(device))
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.identifier, "Setting up KasaOutlet");
|
||||
Ok(Self { config })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct KasaOutlet {
|
||||
identifier: String,
|
||||
addr: SocketAddr,
|
||||
}
|
||||
|
||||
impl Device for KasaOutlet {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -214,7 +208,7 @@ impl Response {
|
|||
#[async_trait]
|
||||
impl traits::OnOff for KasaOutlet {
|
||||
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
|
||||
.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> {
|
||||
let mut stream = TcpStream::connect(self.addr)
|
||||
let mut stream = TcpStream::connect(self.config.addr)
|
||||
.await
|
||||
.or::<DeviceError>(Err(DeviceError::DeviceOffline))?;
|
||||
|
||||
|
|
|
@ -1,70 +1,70 @@
|
|||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use rumqttc::Publish;
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig};
|
||||
use crate::devices::Device;
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{self, Event, OnMqtt};
|
||||
use crate::event::{self, Event, EventChannel, OnMqtt};
|
||||
use crate::messages::BrightnessMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct LightSensorConfig {
|
||||
#[serde(flatten)]
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
pub min: isize,
|
||||
pub max: isize,
|
||||
#[device_config(rename("event_channel"), from(EventChannel), 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
|
||||
|
||||
#[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)]
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct LightSensor {
|
||||
identifier: String,
|
||||
tx: event::Sender,
|
||||
mqtt: MqttDeviceConfig,
|
||||
min: isize,
|
||||
max: isize,
|
||||
config: LightSensorConfig,
|
||||
|
||||
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 {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
fn get_id(&self) -> String {
|
||||
self.config.identifier.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for LightSensor {
|
||||
fn topics(&self) -> Vec<&str> {
|
||||
vec![&self.mqtt.topic]
|
||||
}
|
||||
|
||||
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) {
|
||||
Ok(state) => state.illuminance(),
|
||||
Err(err) => {
|
||||
|
@ -74,17 +74,17 @@ impl OnMqtt for LightSensor {
|
|||
};
|
||||
|
||||
debug!("Illuminance: {illuminance}");
|
||||
let is_dark = if illuminance <= self.min {
|
||||
let is_dark = if illuminance <= self.config.min {
|
||||
trace!("It is dark");
|
||||
true
|
||||
} else if illuminance >= self.max {
|
||||
} else if illuminance >= self.config.max {
|
||||
trace!("It is light");
|
||||
false
|
||||
} else {
|
||||
trace!(
|
||||
"In between min ({}) and max ({}) value, keeping current state: {}",
|
||||
self.min,
|
||||
self.max,
|
||||
self.config.min,
|
||||
self.config.max,
|
||||
self.is_dark
|
||||
);
|
||||
self.is_dark
|
||||
|
@ -94,7 +94,7 @@ impl OnMqtt for LightSensor {
|
|||
debug!("Dark state has changed: {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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,35 +3,78 @@ mod audio_setup;
|
|||
mod contact_sensor;
|
||||
mod debug_bridge;
|
||||
mod hue_bridge;
|
||||
mod hue_light;
|
||||
mod hue_group;
|
||||
mod ikea_outlet;
|
||||
mod kasa_outlet;
|
||||
mod light_sensor;
|
||||
mod ntfy;
|
||||
pub mod ntfy;
|
||||
mod presence;
|
||||
mod wake_on_lan;
|
||||
mod washer;
|
||||
|
||||
use google_home::device::AsGoogleHomeDevice;
|
||||
use google_home::traits::OnOff;
|
||||
use std::fmt::Debug;
|
||||
|
||||
pub use self::air_filter::AirFilterConfig;
|
||||
pub use self::audio_setup::AudioSetupConfig;
|
||||
pub use self::contact_sensor::ContactSensorConfig;
|
||||
pub use self::debug_bridge::DebugBridgeConfig;
|
||||
pub use self::hue_bridge::HueBridgeConfig;
|
||||
pub use self::hue_light::HueGroupConfig;
|
||||
pub use self::ikea_outlet::IkeaOutletConfig;
|
||||
pub use self::kasa_outlet::KasaOutletConfig;
|
||||
pub use self::light_sensor::{LightSensor, LightSensorConfig};
|
||||
pub use self::ntfy::{Notification, Ntfy};
|
||||
use async_trait::async_trait;
|
||||
use automation_cast::Cast;
|
||||
use google_home::traits::OnOff;
|
||||
use google_home::GoogleHomeDevice;
|
||||
|
||||
pub use self::air_filter::*;
|
||||
pub use self::audio_setup::*;
|
||||
pub use self::contact_sensor::*;
|
||||
pub use self::debug_bridge::*;
|
||||
pub use self::hue_bridge::*;
|
||||
pub use self::hue_group::*;
|
||||
pub use self::ikea_outlet::*;
|
||||
pub use self::kasa_outlet::*;
|
||||
pub use self::light_sensor::*;
|
||||
pub use self::ntfy::{Ntfy, NtfyConfig};
|
||||
pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE};
|
||||
pub use self::wake_on_lan::WakeOnLANConfig;
|
||||
pub use self::washer::WasherConfig;
|
||||
pub use self::wake_on_lan::*;
|
||||
pub use self::washer::*;
|
||||
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
|
||||
use crate::traits::Timeout;
|
||||
|
||||
#[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + OnNotification + OnOff + Timeout)]
|
||||
pub trait Device: AsGoogleHomeDevice + std::fmt::Debug + Sync + Send {
|
||||
fn get_id(&self) -> &str;
|
||||
#[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(())
|
||||
}
|
||||
|
||||
pub trait Device:
|
||||
Debug
|
||||
+ Sync
|
||||
+ Send
|
||||
+ Cast<dyn GoogleHomeDevice>
|
||||
+ Cast<dyn OnMqtt>
|
||||
+ Cast<dyn OnMqtt>
|
||||
+ Cast<dyn OnPresence>
|
||||
+ Cast<dyn OnDarkness>
|
||||
+ Cast<dyn OnNotification>
|
||||
+ Cast<dyn OnOff>
|
||||
+ Cast<dyn Timeout>
|
||||
{
|
||||
fn get_id(&self) -> String;
|
||||
}
|
||||
|
|
|
@ -1,21 +1,16 @@
|
|||
use std::collections::HashMap;
|
||||
use std::convert::Infallible;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use serde::Serialize;
|
||||
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::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)]
|
||||
#[repr(u8)]
|
||||
pub enum Priority {
|
||||
|
@ -40,9 +35,9 @@ pub enum ActionType {
|
|||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct Action {
|
||||
#[serde(flatten)]
|
||||
action: ActionType,
|
||||
label: String,
|
||||
clear: Option<bool>,
|
||||
pub action: ActionType,
|
||||
pub label: String,
|
||||
pub clear: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -116,28 +111,50 @@ impl Default for Notification {
|
|||
}
|
||||
}
|
||||
|
||||
impl Ntfy {
|
||||
pub fn new(config: NtfyConfig, event_channel: &EventChannel) -> Self {
|
||||
Self {
|
||||
base_url: config.url,
|
||||
topic: config.topic,
|
||||
tx: event_channel.get_tx(),
|
||||
}
|
||||
}
|
||||
#[derive(Debug, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct NtfyConfig {
|
||||
#[device_config(default("https://ntfy.sh".into()))]
|
||||
pub url: String,
|
||||
pub topic: String,
|
||||
#[device_config(rename("event_channel"), from_lua, from(EventChannel), 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) {
|
||||
let notification = notification.finalize(&self.topic);
|
||||
debug!("Sending notfication");
|
||||
let notification = notification.finalize(&self.config.topic);
|
||||
|
||||
// Create the request
|
||||
let res = reqwest::Client::new()
|
||||
.post(self.base_url.clone())
|
||||
.post(self.config.url.clone())
|
||||
.json(¬ification)
|
||||
.send()
|
||||
.await;
|
||||
|
||||
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 {
|
||||
let status = res.status();
|
||||
if !status.is_success() {
|
||||
|
@ -147,12 +164,6 @@ impl Ntfy {
|
|||
}
|
||||
}
|
||||
|
||||
impl Device for Ntfy {
|
||||
fn get_id(&self) -> &str {
|
||||
"ntfy"
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnPresence for Ntfy {
|
||||
async fn on_presence(&mut self, presence: bool) {
|
||||
|
@ -177,7 +188,13 @@ impl OnPresence for Ntfy {
|
|||
.add_action(action)
|
||||
.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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,60 +1,76 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use rumqttc::Publish;
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, warn};
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
use super::LuaDeviceCreate;
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::devices::Device;
|
||||
use crate::event::{self, Event, EventChannel, OnMqtt};
|
||||
use crate::messages::PresenceMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Debug, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct PresenceConfig {
|
||||
#[serde(flatten)]
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
#[device_config(from_lua, rename("event_channel"), from(EventChannel), with(|ec: EventChannel| ec.get_tx()))]
|
||||
pub tx: event::Sender,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
pub const DEFAULT_PRESENCE: bool = false;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct Presence {
|
||||
tx: event::Sender,
|
||||
mqtt: MqttDeviceConfig,
|
||||
config: PresenceConfig,
|
||||
devices: HashMap<String, bool>,
|
||||
current_overall_presence: bool,
|
||||
}
|
||||
|
||||
impl Presence {
|
||||
pub fn new(config: PresenceConfig, event_channel: &EventChannel) -> Self {
|
||||
Self {
|
||||
tx: event_channel.get_tx(),
|
||||
mqtt: config.mqtt,
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for Presence {
|
||||
type Config = PresenceConfig;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
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(),
|
||||
current_overall_presence: DEFAULT_PRESENCE,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Device for Presence {
|
||||
fn get_id(&self) -> &str {
|
||||
"presence"
|
||||
fn get_id(&self) -> String {
|
||||
"presence".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for Presence {
|
||||
fn topics(&self) -> Vec<&str> {
|
||||
vec![&self.mqtt.topic]
|
||||
}
|
||||
|
||||
async fn on_mqtt(&mut self, message: Publish) {
|
||||
if !rumqttc::matches(&message.topic, &self.config.mqtt.topic) {
|
||||
return;
|
||||
}
|
||||
|
||||
let offset = self
|
||||
.config
|
||||
.mqtt
|
||||
.topic
|
||||
.find('+')
|
||||
.or(self.mqtt.topic.find('#'))
|
||||
.or(self.config.mqtt.topic.find('#'))
|
||||
.expect("Presence::create fails if it does not contain wildcards");
|
||||
let device_name = message.topic[offset..].into();
|
||||
|
||||
|
@ -81,6 +97,7 @@ impl OnMqtt for Presence {
|
|||
self.current_overall_presence = overall_presence;
|
||||
|
||||
if self
|
||||
.config
|
||||
.tx
|
||||
.send(Event::Presence(overall_presence))
|
||||
.await
|
||||
|
|
|
@ -1,89 +1,73 @@
|
|||
use std::net::Ipv4Addr;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use eui48::MacAddress;
|
||||
use google_home::errors::ErrorCode;
|
||||
use google_home::traits::{self, Scene};
|
||||
use google_home::types::Type;
|
||||
use google_home::{device, GoogleHomeDevice};
|
||||
use rumqttc::Publish;
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, error, trace};
|
||||
|
||||
use super::Device;
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::config::{InfoConfig, MqttDeviceConfig};
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig};
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::OnMqtt;
|
||||
use crate::messages::ActivateMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct WakeOnLANConfig {
|
||||
#[serde(flatten)]
|
||||
info: InfoConfig,
|
||||
#[serde(flatten)]
|
||||
mqtt: MqttDeviceConfig,
|
||||
mac_address: MacAddress,
|
||||
#[serde(default = "default_broadcast_ip")]
|
||||
broadcast_ip: Ipv4Addr,
|
||||
#[device_config(flatten)]
|
||||
pub info: InfoConfig,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
pub mac_address: MacAddress,
|
||||
#[device_config(default(Ipv4Addr::new(255, 255, 255, 255)))]
|
||||
pub broadcast_ip: Ipv4Addr,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
fn default_broadcast_ip() -> Ipv4Addr {
|
||||
Ipv4Addr::new(255, 255, 255, 255)
|
||||
#[derive(Debug, LuaDevice)]
|
||||
pub struct WakeOnLAN {
|
||||
config: WakeOnLANConfig,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DeviceConfig for WakeOnLANConfig {
|
||||
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 WakeOnLAN"
|
||||
);
|
||||
impl LuaDeviceCreate for WakeOnLAN {
|
||||
type Config = WakeOnLANConfig;
|
||||
type Error = rumqttc::ClientError;
|
||||
|
||||
let device = WakeOnLAN {
|
||||
identifier: identifier.into(),
|
||||
info: self.info,
|
||||
mqtt: self.mqtt,
|
||||
mac_address: self.mac_address,
|
||||
broadcast_ip: self.broadcast_ip,
|
||||
};
|
||||
async fn create(config: Self::Config) -> Result<Self, Self::Error> {
|
||||
trace!(id = config.info.identifier(), "Setting up WakeOnLAN");
|
||||
|
||||
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 {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
fn get_id(&self) -> String {
|
||||
self.config.info.identifier()
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for WakeOnLAN {
|
||||
fn topics(&self) -> Vec<&str> {
|
||||
vec![&self.mqtt.topic]
|
||||
}
|
||||
|
||||
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) {
|
||||
Ok(message) => message.activate(),
|
||||
Err(err) => {
|
||||
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||
error!(id = Device::get_id(self), "Failed to parse message: {err}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
@ -98,13 +82,13 @@ impl GoogleHomeDevice for WakeOnLAN {
|
|||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn get_id(&self) -> &str {
|
||||
fn get_id(&self) -> String {
|
||||
Device::get_id(self)
|
||||
}
|
||||
|
||||
|
@ -113,7 +97,7 @@ impl GoogleHomeDevice for WakeOnLAN {
|
|||
}
|
||||
|
||||
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> {
|
||||
if activate {
|
||||
debug!(
|
||||
id = self.identifier,
|
||||
"Activating Computer: {} (Sending to {})", self.mac_address, self.broadcast_ip
|
||||
id = Device::get_id(self),
|
||||
"Activating Computer: {} (Sending to {})",
|
||||
self.config.mac_address,
|
||||
self.config.broadcast_ip
|
||||
);
|
||||
let wol =
|
||||
wakey::WolPacket::from_bytes(&self.mac_address.to_array()).map_err(|err| {
|
||||
error!(id = self.identifier, "invalid mac address: {err}");
|
||||
let wol = wakey::WolPacket::from_bytes(&self.config.mac_address.to_array()).map_err(
|
||||
|err| {
|
||||
error!(id = Device::get_id(self), "invalid mac address: {err}");
|
||||
google_home::errors::DeviceError::TransientError
|
||||
})?;
|
||||
},
|
||||
)?;
|
||||
|
||||
wol.send_magic_to((Ipv4Addr::new(0, 0, 0, 0), 0), (self.broadcast_ip, 9))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!(id = self.identifier, "Failed to activate computer: {err}");
|
||||
google_home::errors::DeviceError::TransientError.into()
|
||||
})
|
||||
.map(|_| debug!(id = self.identifier, "Success!"))
|
||||
wol.send_magic_to(
|
||||
(Ipv4Addr::new(0, 0, 0, 0), 0),
|
||||
(self.config.broadcast_ip, 9),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
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 {
|
||||
debug!(
|
||||
id = self.identifier,
|
||||
"Trying to deactive computer, this is not currently supported"
|
||||
id = Device::get_id(self),
|
||||
"Trying to deactivate computer, this is not currently supported"
|
||||
);
|
||||
// We do not support deactivating this scene
|
||||
Err(ErrorCode::DeviceError(
|
||||
|
|
|
@ -1,88 +1,91 @@
|
|||
use async_trait::async_trait;
|
||||
use automation_macro::{LuaDevice, LuaDeviceConfig, LuaTypeDefinition};
|
||||
use rumqttc::Publish;
|
||||
use serde::Deserialize;
|
||||
use tracing::{debug, error, warn};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
use super::ntfy::Priority;
|
||||
use super::{Device, Notification};
|
||||
use super::{Device, LuaDeviceCreate};
|
||||
use crate::config::MqttDeviceConfig;
|
||||
use crate::device_manager::{ConfigExternal, DeviceConfig};
|
||||
use crate::error::DeviceConfigError;
|
||||
use crate::event::{Event, EventChannel, OnMqtt};
|
||||
use crate::devices::ntfy::Notification;
|
||||
use crate::event::{self, Event, EventChannel, OnMqtt};
|
||||
use crate::messages::PowerMessage;
|
||||
use crate::mqtt::WrappedAsyncClient;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone, LuaDeviceConfig, LuaTypeDefinition)]
|
||||
pub struct WasherConfig {
|
||||
#[serde(flatten)]
|
||||
mqtt: MqttDeviceConfig,
|
||||
threshold: f32, // Power in Watt
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl DeviceConfig for WasherConfig {
|
||||
async fn create(
|
||||
self,
|
||||
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))
|
||||
}
|
||||
pub identifier: String,
|
||||
#[device_config(flatten)]
|
||||
pub mqtt: MqttDeviceConfig,
|
||||
// Power in Watt
|
||||
pub threshold: f32,
|
||||
#[device_config(rename("event_channel"), from_lua, from(EventChannel), with(|ec: EventChannel| ec.get_tx()))]
|
||||
pub tx: event::Sender,
|
||||
#[device_config(from_lua)]
|
||||
pub client: WrappedAsyncClient,
|
||||
}
|
||||
|
||||
// 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,
|
||||
}
|
||||
|
||||
impl Device for Washer {
|
||||
fn get_id(&self) -> &str {
|
||||
&self.identifier
|
||||
#[async_trait]
|
||||
impl LuaDeviceCreate for Washer {
|
||||
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
|
||||
// This helps prevent false positives
|
||||
const HYSTERESIS: isize = 10;
|
||||
|
||||
#[async_trait]
|
||||
impl OnMqtt for Washer {
|
||||
fn topics(&self) -> Vec<&str> {
|
||||
vec![&self.mqtt.topic]
|
||||
}
|
||||
|
||||
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) {
|
||||
Ok(state) => state.power(),
|
||||
Err(err) => {
|
||||
error!(id = self.identifier, "Failed to parse message: {err}");
|
||||
error!(
|
||||
id = self.config.identifier,
|
||||
"Failed to parse message: {err}"
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// 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
|
||||
debug!(
|
||||
id = self.identifier,
|
||||
id = self.config.identifier,
|
||||
power,
|
||||
threshold = self.threshold,
|
||||
threshold = self.config.threshold,
|
||||
"Washer is done"
|
||||
);
|
||||
|
||||
|
@ -94,23 +97,23 @@ impl OnMqtt for Washer {
|
|||
.set_priority(Priority::High);
|
||||
|
||||
if self
|
||||
.event_channel
|
||||
.get_tx()
|
||||
.config
|
||||
.tx
|
||||
.send(Event::Ntfy(notification))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!("There are no receivers on the event channel");
|
||||
}
|
||||
} else if power < self.threshold {
|
||||
} else if power < self.config.threshold {
|
||||
// Prevent false positives
|
||||
self.running = 0;
|
||||
} else if power >= self.threshold && self.running < HYSTERESIS {
|
||||
} else if power >= self.config.threshold && self.running < HYSTERESIS {
|
||||
// Washer could be starting
|
||||
debug!(
|
||||
id = self.identifier,
|
||||
id = self.config.identifier,
|
||||
power,
|
||||
threshold = self.threshold,
|
||||
threshold = self.config.threshold,
|
||||
"Washer is starting"
|
||||
);
|
||||
|
||||
|
|
14
src/error.rs
14
src/error.rs
|
@ -65,16 +65,6 @@ pub enum ParseError {
|
|||
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
|
||||
#[derive(Debug, Error)]
|
||||
#[error("Topic '{topic}' is expected to be a wildcard topic")]
|
||||
|
@ -92,12 +82,10 @@ impl MissingWildcard {
|
|||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DeviceConfigError {
|
||||
#[error("Child '{1}' of device '{0}' does not exist")]
|
||||
MissingChild(String, String),
|
||||
#[error("Device '{0}' does not implement expected trait '{1}'")]
|
||||
MissingTrait(String, String),
|
||||
#[error(transparent)]
|
||||
MissingWildcard(#[from] MissingWildcard),
|
||||
MqttClientError(#[from] rumqttc::ClientError),
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
22
src/event.rs
22
src/event.rs
|
@ -1,9 +1,9 @@
|
|||
use async_trait::async_trait;
|
||||
use impl_cast::device_trait;
|
||||
use mlua::FromLua;
|
||||
use rumqttc::Publish;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::devices::Notification;
|
||||
use crate::devices::ntfy::Notification;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Event {
|
||||
|
@ -16,7 +16,7 @@ pub enum Event {
|
|||
pub type Sender = mpsc::Sender<Event>;
|
||||
pub type Receiver = mpsc::Receiver<Event>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, FromLua)]
|
||||
pub struct EventChannel(Sender);
|
||||
|
||||
impl EventChannel {
|
||||
|
@ -31,27 +31,25 @@ impl EventChannel {
|
|||
}
|
||||
}
|
||||
|
||||
impl mlua::UserData for EventChannel {}
|
||||
|
||||
#[async_trait]
|
||||
#[device_trait]
|
||||
pub trait OnMqtt {
|
||||
fn topics(&self) -> Vec<&str>;
|
||||
pub trait OnMqtt: Sync + Send {
|
||||
// fn topics(&self) -> Vec<&str>;
|
||||
async fn on_mqtt(&mut self, message: Publish);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[device_trait]
|
||||
pub trait OnPresence {
|
||||
pub trait OnPresence: Sync + Send {
|
||||
async fn on_presence(&mut self, presence: bool);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[device_trait]
|
||||
pub trait OnDarkness {
|
||||
pub trait OnDarkness: Sync + Send {
|
||||
async fn on_darkness(&mut self, dark: bool);
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
#[device_trait]
|
||||
pub trait OnNotification {
|
||||
pub trait OnNotification: Sync + Send {
|
||||
async fn on_notification(&mut self, notification: Notification);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
#![allow(incomplete_features)]
|
||||
#![feature(specialization)]
|
||||
#![feature(let_chains)]
|
||||
|
||||
use once_cell::sync::Lazy;
|
||||
use tokio::sync::Mutex;
|
||||
pub mod auth;
|
||||
pub mod config;
|
||||
pub mod device_manager;
|
||||
|
@ -11,3 +14,5 @@ pub mod messages;
|
|||
pub mod mqtt;
|
||||
pub mod schedule;
|
||||
pub mod traits;
|
||||
|
||||
pub static LUA: Lazy<Mutex<mlua::Lua>> = Lazy::new(|| Mutex::new(mlua::Lua::new()));
|
||||
|
|
114
src/main.rs
114
src/main.rs
|
@ -1,12 +1,14 @@
|
|||
#![feature(async_closure)]
|
||||
use std::path::Path;
|
||||
use std::process;
|
||||
|
||||
use automation::auth::{OpenIDConfig, User};
|
||||
use automation::config::Config;
|
||||
use anyhow::anyhow;
|
||||
use automation::auth::User;
|
||||
use automation::config::{FulfillmentConfig, MqttConfig};
|
||||
use automation::device_manager::DeviceManager;
|
||||
use automation::devices::{Ntfy, Presence};
|
||||
use automation::error::ApiError;
|
||||
use automation::mqtt;
|
||||
use automation::mqtt::{self, WrappedAsyncClient};
|
||||
use automation::{devices, LUA};
|
||||
use axum::extract::FromRef;
|
||||
use axum::http::StatusCode;
|
||||
use axum::response::IntoResponse;
|
||||
|
@ -14,17 +16,18 @@ use axum::routing::post;
|
|||
use axum::{Json, Router};
|
||||
use dotenvy::dotenv;
|
||||
use google_home::{GoogleHome, Request};
|
||||
use mlua::LuaSerdeExt;
|
||||
use rumqttc::AsyncClient;
|
||||
use tracing::{debug, error, info};
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
#[derive(Clone)]
|
||||
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 {
|
||||
input.openid.clone()
|
||||
input.openid_url.clone()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,43 +52,74 @@ async fn app() -> anyhow::Result<()> {
|
|||
|
||||
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
|
||||
let device_manager = DeviceManager::new(client.clone());
|
||||
let device_manager = DeviceManager::new().await;
|
||||
|
||||
for (id, device_config) in config.devices {
|
||||
device_manager.create(&id, device_config).await?;
|
||||
}
|
||||
let fulfillment_config = {
|
||||
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
|
||||
{
|
||||
let presence = Presence::new(config.presence, &event_channel);
|
||||
device_manager.add(Box::new(presence)).await;
|
||||
}
|
||||
// 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 (client, eventloop) = AsyncClient::new(config.into(), 100);
|
||||
mqtt::start(eventloop, &event_channel);
|
||||
|
||||
// Start the ntfy service if it is configured
|
||||
if let Some(config) = config.ntfy {
|
||||
let ntfy = Ntfy::new(config, &event_channel);
|
||||
device_manager.add(Box::new(ntfy)).await;
|
||||
}
|
||||
Ok(WrappedAsyncClient(client))
|
||||
})?;
|
||||
|
||||
// Wrap the mqtt eventloop and start listening for message
|
||||
// NOTE: We wait until all the setup is done, as otherwise we might miss some messages
|
||||
mqtt::start(eventloop, &event_channel);
|
||||
automation.set("new_mqtt_client", new_mqtt_client)?;
|
||||
automation.set("device_manager", device_manager.clone())?;
|
||||
|
||||
// Create google home fullfillment route
|
||||
let fullfillment = Router::new().route(
|
||||
let util = lua.create_table()?;
|
||||
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)?;
|
||||
let get_hostname = lua.create_function(|_lua, ()| {
|
||||
hostname::get()
|
||||
.map(|name| name.to_str().unwrap_or("unknown").to_owned())
|
||||
.map_err(mlua::ExternalError::into_lua_err)
|
||||
})?;
|
||||
util.set("get_hostname", get_hostname)?;
|
||||
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",
|
||||
post(async move |user: User, Json(payload): Json<Request>| {
|
||||
debug!(username = user.preferred_username, "{payload:#?}");
|
||||
|
@ -107,13 +141,13 @@ async fn app() -> anyhow::Result<()> {
|
|||
|
||||
// Combine together all the routes
|
||||
let app = Router::new()
|
||||
.nest("/fullfillment", fullfillment)
|
||||
.nest("/fulfillment", fulfillment)
|
||||
.with_state(AppState {
|
||||
openid: config.openid,
|
||||
openid_url: fulfillment_config.openid_url.clone(),
|
||||
});
|
||||
|
||||
// Start the web server
|
||||
let addr = config.fullfillment.into();
|
||||
let addr = fulfillment_config.into();
|
||||
info!("Server started on http://{addr}");
|
||||
axum::Server::try_bind(&addr)?
|
||||
.serve(app.into_make_service())
|
||||
|
|
24
src/mqtt.rs
24
src/mqtt.rs
|
@ -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 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) {
|
||||
let tx = event_channel.get_tx();
|
||||
|
||||
|
|
|
@ -2,11 +2,9 @@ use std::time::Duration;
|
|||
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use impl_cast::device_trait;
|
||||
|
||||
#[async_trait]
|
||||
#[device_trait]
|
||||
pub trait Timeout {
|
||||
pub trait Timeout: Sync + Send {
|
||||
async fn start_timeout(&mut self, _timeout: Duration) -> Result<()>;
|
||||
async fn stop_timeout(&mut self) -> Result<()>;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user