Compare commits

...

10 Commits

Author SHA1 Message Date
9385f27125 Improved how devices are created, ntfy and presence are now treated like any other device
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 5m30s
Build and deploy automation_rs / Build Docker image (push) Successful in 55s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-27 02:55:53 +02:00
8c327095fd Moved schedule config from yml to lua 2024-04-26 23:16:39 +02:00
57596ae531 Set lua warning function 2024-04-26 21:54:55 +02:00
8762a680a8 Slight macro cleanup
All checks were successful
Build and deploy automation_rs / Build automation_rs (push) Successful in 4m26s
Build and deploy automation_rs / Build Docker image (push) Successful in 34s
Build and deploy automation_rs / Deploy Docker container (push) Has been skipped
2024-04-26 06:03:54 +02:00
e7fb8bfb8d Improved the internals of the LuaDeviceConfig macro and improve the
usability of the macro
2024-04-26 06:03:54 +02:00
dc3a7e5407 Use helper types to process config input into the right type 2024-04-26 06:03:54 +02:00
20606c6356 Added helper type to convert from ip addr to socketaddr with the correct port 2024-04-26 06:03:54 +02:00
d8198bf5b0 Added rename option to macro 2024-04-26 06:03:54 +02:00
9449a83f61 Everything needed to construct a new device is passed in through lua 2024-04-26 06:03:54 +02:00
2bc2dc6be1 Device config is now done through lua 2024-04-26 06:03:54 +02:00
30 changed files with 1534 additions and 1109 deletions

391
Cargo.lock generated
View File

@@ -61,7 +61,7 @@ checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -76,6 +76,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
"automation_macro",
"axum", "axum",
"bytes", "bytes",
"console-subscriber", "console-subscriber",
@@ -86,6 +87,7 @@ dependencies = [
"google-home", "google-home",
"impl_cast", "impl_cast",
"indexmap 2.0.0", "indexmap 2.0.0",
"mlua",
"paste", "paste",
"pollster", "pollster",
"regex", "regex",
@@ -104,6 +106,16 @@ dependencies = [
"wakey", "wakey",
] ]
[[package]]
name = "automation_macro"
version = "0.1.0"
dependencies = [
"itertools 0.12.1",
"proc-macro2",
"quote",
"syn 2.0.60",
]
[[package]] [[package]]
name = "axum" name = "axum"
version = "0.6.20" version = "0.6.20"
@@ -112,7 +124,7 @@ checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum-core", "axum-core",
"bitflags", "bitflags 1.3.2",
"bytes", "bytes",
"futures-util", "futures-util",
"http", "http",
@@ -186,6 +198,22 @@ version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "bstr"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
dependencies = [
"memchr",
"serde",
]
[[package]] [[package]]
name = "bumpalo" name = "bumpalo"
version = "3.13.0" version = "3.13.0"
@@ -344,7 +372,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -355,7 +383,7 @@ checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -397,7 +425,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -406,6 +434,25 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
[[package]]
name = "erased-serde"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b73807008a3c7f171cc40312f37d95ef0396e048b5848d775f54b1a4dd4a0d3"
dependencies = [
"serde",
]
[[package]]
name = "errno"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245"
dependencies = [
"libc",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "eui48" name = "eui48"
version = "1.1.0" version = "1.1.0"
@@ -510,7 +557,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -632,6 +679,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "home"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
dependencies = [
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "http" name = "http"
version = "0.2.9" version = "0.2.9"
@@ -766,7 +822,7 @@ name = "impl_cast"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -806,6 +862,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.9" version = "1.0.9"
@@ -829,9 +894,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.147" version = "0.2.153"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
[[package]]
name = "linux-raw-sys"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@@ -849,6 +920,25 @@ version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4"
[[package]]
name = "lua-src"
version = "546.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da0daa7eee611a4c30c8f5ee31af55266e26e573971ba9336d2993e2da129b2"
dependencies = [
"cc",
]
[[package]]
name = "luajit-src"
version = "210.5.7+d06beb0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d251fdacdabbf87704cf48ac1f8b1eb23d6e10855c3ee08e5beb25b4be2e9e4"
dependencies = [
"cc",
"which",
]
[[package]] [[package]]
name = "matchers" name = "matchers"
version = "0.1.0" version = "0.1.0"
@@ -899,7 +989,53 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [ dependencies = [
"libc", "libc",
"wasi", "wasi",
"windows-sys", "windows-sys 0.48.0",
]
[[package]]
name = "mlua"
version = "0.9.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d9bed6bce296397a9d6a86f995dd10a547a4e6949825d45225906bdcbfe7367"
dependencies = [
"bstr",
"erased-serde",
"futures-util",
"mlua-sys",
"mlua_derive",
"num-traits",
"once_cell",
"rustc-hash",
"serde",
"serde-value",
]
[[package]]
name = "mlua-sys"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d16a9ba1dd2c6ac971b204262d434c24d65067038598f0638b64e5dca28d52b8"
dependencies = [
"cc",
"cfg-if",
"lua-src",
"luajit-src",
"pkg-config",
]
[[package]]
name = "mlua_derive"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaade5f94e5829db58791664ba98f35fea6a3ffebc783becb51dc97c7a21abee"
dependencies = [
"itertools 0.12.1",
"once_cell",
"proc-macro-error",
"proc-macro2",
"quote",
"regex",
"syn 2.0.60",
] ]
[[package]] [[package]]
@@ -982,6 +1118,15 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "ordered-float"
version = "2.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c"
dependencies = [
"num-traits",
]
[[package]] [[package]]
name = "overload" name = "overload"
version = "0.1.1" version = "0.1.1"
@@ -1017,7 +1162,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -1032,6 +1177,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "pkg-config"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]] [[package]]
name = "pollster" name = "pollster"
version = "0.2.5" version = "0.2.5"
@@ -1045,10 +1196,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro-error"
version = "1.0.66" version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn 1.0.109",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@@ -1070,7 +1245,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"itertools", "itertools 0.10.5",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 1.0.109", "syn 1.0.109",
@@ -1087,9 +1262,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.32" version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@@ -1246,6 +1421,25 @@ version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustc-hash"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]]
name = "rustix"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.5.0",
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.20.8" version = "0.20.8"
@@ -1328,7 +1522,7 @@ version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [ dependencies = [
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@@ -1353,7 +1547,7 @@ version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [ dependencies = [
"bitflags", "bitflags 1.3.2",
"core-foundation", "core-foundation",
"core-foundation-sys", "core-foundation-sys",
"libc", "libc",
@@ -1372,22 +1566,32 @@ dependencies = [
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.190" version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7" checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde-value"
version = "1.0.190" version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c"
dependencies = [
"ordered-float",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.198"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -1419,7 +1623,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -1460,7 +1664,7 @@ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -1517,7 +1721,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@@ -1554,9 +1758,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.28" version = "2.0.60"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -1586,7 +1790,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -1657,7 +1861,7 @@ dependencies = [
"socket2 0.5.3", "socket2 0.5.3",
"tokio-macros", "tokio-macros",
"tracing", "tracing",
"windows-sys", "windows-sys 0.48.0",
] ]
[[package]] [[package]]
@@ -1693,7 +1897,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -1823,7 +2027,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
] ]
[[package]] [[package]]
@@ -1930,6 +2134,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]] [[package]]
name = "wakey" name = "wakey"
version = "0.3.0" version = "0.3.0"
@@ -1976,7 +2186,7 @@ dependencies = [
"once_cell", "once_cell",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -2010,7 +2220,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.28", "syn 2.0.60",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@@ -2050,6 +2260,18 @@ dependencies = [
"webpki", "webpki",
] ]
[[package]]
name = "which"
version = "6.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7"
dependencies = [
"either",
"home",
"rustix",
"winsafe",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"
@@ -2078,7 +2300,7 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
dependencies = [ dependencies = [
"windows-targets", "windows-targets 0.48.1",
] ]
[[package]] [[package]]
@@ -2087,7 +2309,16 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [ dependencies = [
"windows-targets", "windows-targets 0.48.1",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets 0.52.5",
] ]
[[package]] [[package]]
@@ -2096,13 +2327,29 @@ version = "0.48.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm", "windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_msvc", "windows_aarch64_msvc 0.48.0",
"windows_i686_gnu", "windows_i686_gnu 0.48.0",
"windows_i686_msvc", "windows_i686_msvc 0.48.0",
"windows_x86_64_gnu", "windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm", "windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_msvc", "windows_x86_64_msvc 0.48.0",
]
[[package]]
name = "windows-targets"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
dependencies = [
"windows_aarch64_gnullvm 0.52.5",
"windows_aarch64_msvc 0.52.5",
"windows_i686_gnu 0.52.5",
"windows_i686_gnullvm",
"windows_i686_msvc 0.52.5",
"windows_x86_64_gnu 0.52.5",
"windows_x86_64_gnullvm 0.52.5",
"windows_x86_64_msvc 0.52.5",
] ]
[[package]] [[package]]
@@ -2111,42 +2358,90 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]]
name = "windows_i686_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]]
name = "windows_i686_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.0" version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]] [[package]]
name = "winreg" name = "winreg"
version = "0.10.1" version = "0.10.1"
@@ -2155,3 +2450,9 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "winsafe"
version = "0.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,47 @@
use proc_macro2::TokenStream;
use quote::quote;
use syn::{Data, DataStruct, DeriveInput, Fields, FieldsNamed};
pub fn impl_lua_device_macro(ast: &DeriveInput) -> TokenStream {
let name = &ast.ident;
// TODO: Handle errors properly
// This includes making sure one, and only one config is specified
let config = if let Data::Struct(DataStruct {
fields: Fields::Named(FieldsNamed { ref named, .. }),
..
}) = ast.data
{
named
.iter()
.find(|&field| {
field
.attrs
.iter()
.any(|attr| attr.path().is_ident("config"))
})
.map(|field| field.ty.clone())
.unwrap()
} else {
unimplemented!()
};
let gen = quote! {
impl #name {
pub fn register_with_lua(lua: &mlua::Lua) -> mlua::Result<()> {
lua.globals().set(stringify!(#name), lua.create_proxy::<#name>()?)
}
}
impl mlua::UserData for #name {
fn add_methods<'lua, M: mlua::UserDataMethods<'lua, Self>>(methods: &mut M) {
methods.add_async_function("new", |lua, config: mlua::Value| async {
let config: #config = mlua::FromLua::from_lua(config, lua)?;
let device = #name::create(config).await.map_err(mlua::ExternalError::into_lua_err)?;
Ok(crate::device_manager::WrappedDevice::new(Box::new(device)))
});
}
}
};
gen
}

View File

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

163
config.lua Normal file
View File

@@ -0,0 +1,163 @@
print("Hello from lua")
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.device_manager:add(Ntfy.new({
topic = automation.util.get_env("NTFY_TOPIC"),
event_channel = automation.event_channel,
}))
automation.device_manager:add(Presence.new({
topic = "automation_dev/presence/+/#",
event_channel = automation.event_channel,
}))
automation.device_manager:add(DebugBridge.new({
identifier = "debug_bridge",
topic = mqtt_automation("debug"),
client = automation.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"),
min = 22000,
max = 23500,
event_channel = automation.event_channel,
}))
automation.device_manager:add(WakeOnLAN.new({
name = "Zeus",
room = "Living Room",
topic = mqtt_automation("appliance/living_room/zeus"),
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"),
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 = automation.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 = automation.mqtt_client,
timeout = debug and 60 or 45 * 60,
}))
automation.device_manager:add(Washer.new({
identifier = "bathroom_washer",
topic = mqtt_z2m("batchroom/washer"),
threshold = 1,
event_channel = automation.event_channel,
}))
automation.device_manager:add(IkeaOutlet.new({
outlet_type = "Charger",
name = "Charger",
room = "Workbench",
topic = mqtt_z2m("workbench/charger"),
client = automation.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 = automation.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") },
},
}))
automation.device_manager:add(ContactSensor.new({
identifier = "hallway_frontdoor",
topic = mqtt_z2m("hallway/frontdoor"),
client = automation.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 = automation.device_manager:add(AirFilter.new({
name = "Air Filter",
room = "Bedroom",
topic = "pico/filter/bedroom",
client = automation.mqtt_client,
}))
-- TODO: Use the wrapped device bedroom_air_filter instead of the string
automation.device_manager:add_schedule({
["0 0 19 * * *"] = {
on = {
"bedroom_air_filter",
},
},
["0 0 20 * * *"] = {
off = {
"bedroom_air_filter",
},
},
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,17 +2,13 @@ use std::fs;
use std::net::{Ipv4Addr, SocketAddr}; use std::net::{Ipv4Addr, SocketAddr};
use std::time::Duration; use std::time::Duration;
use indexmap::IndexMap;
use regex::{Captures, Regex}; use regex::{Captures, Regex};
use rumqttc::{MqttOptions, Transport}; use rumqttc::{MqttOptions, Transport};
use serde::{Deserialize, Deserializer}; use serde::{Deserialize, Deserializer};
use tracing::debug; use tracing::debug;
use crate::auth::OpenIDConfig; use crate::auth::OpenIDConfig;
use crate::device_manager::DeviceConfigs;
use crate::devices::PresenceConfig;
use crate::error::{ConfigParseError, MissingEnv}; use crate::error::{ConfigParseError, MissingEnv};
use crate::schedule::Schedule;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
pub struct Config { pub struct Config {
@@ -21,10 +17,6 @@ pub struct Config {
pub mqtt: MqttOptions, pub mqtt: MqttOptions,
#[serde(default)] #[serde(default)]
pub fullfillment: FullfillmentConfig, pub fullfillment: FullfillmentConfig,
pub ntfy: Option<NtfyConfig>,
pub presence: PresenceConfig,
pub devices: IndexMap<String, DeviceConfigs>,
pub schedule: Schedule,
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
@@ -90,23 +82,22 @@ fn default_fullfillment_port() -> u16 {
7878 7878
} }
#[derive(Debug, Deserialize)]
pub struct NtfyConfig {
#[serde(default = "default_ntfy_url")]
pub url: String,
pub topic: String,
}
fn default_ntfy_url() -> String {
"https://ntfy.sh".into()
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct InfoConfig { pub struct InfoConfig {
pub name: String, pub name: String,
pub room: Option<String>, pub room: Option<String>,
} }
impl InfoConfig {
pub fn identifier(&self) -> String {
(if let Some(room) = &self.room {
room.to_ascii_lowercase().replace(' ', "_") + "_"
} else {
String::new()
}) + &self.name.to_ascii_lowercase().replace(' ', "_")
}
}
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, Deserialize)]
pub struct MqttDeviceConfig { pub struct MqttDeviceConfig {
pub topic: String, pub topic: String,

View File

@@ -1,59 +1,44 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::sync::Arc; use std::sync::Arc;
use async_trait::async_trait;
use enum_dispatch::enum_dispatch;
use futures::future::join_all; use futures::future::join_all;
use google_home::traits::OnOff; use google_home::traits::OnOff;
use mlua::{FromLua, LuaSerdeExt};
use rumqttc::{matches, AsyncClient, QoS}; use rumqttc::{matches, AsyncClient, QoS};
use serde::Deserialize;
use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::sync::{RwLock, RwLockReadGuard};
use tokio_cron_scheduler::{Job, JobScheduler}; use tokio_cron_scheduler::{Job, JobScheduler};
use tracing::{debug, error, instrument, trace}; use tracing::{debug, error, instrument, trace};
use crate::devices::{ use crate::devices::{As, Device};
AirFilterConfig, As, AudioSetupConfig, ContactSensorConfig, DebugBridgeConfig, Device,
HueBridgeConfig, HueGroupConfig, IkeaOutletConfig, KasaOutletConfig, LightSensorConfig,
WakeOnLANConfig, WasherConfig,
};
use crate::error::DeviceConfigError;
use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence}; use crate::event::{Event, EventChannel, OnDarkness, OnMqtt, OnNotification, OnPresence};
use crate::schedule::{Action, Schedule}; use crate::schedule::{Action, Schedule};
pub struct ConfigExternal<'a> { #[derive(Debug, FromLua, Clone)]
pub client: &'a AsyncClient, pub struct WrappedDevice(Arc<RwLock<Box<dyn Device>>>);
pub device_manager: &'a DeviceManager,
pub event_channel: &'a EventChannel, impl WrappedDevice {
pub fn new(device: Box<dyn Device>) -> Self {
Self(Arc::new(RwLock::new(device)))
}
} }
#[async_trait] impl Deref for WrappedDevice {
#[enum_dispatch] type Target = Arc<RwLock<Box<dyn Device>>>;
pub trait DeviceConfig {
async fn create( fn deref(&self) -> &Self::Target {
self, &self.0
identifier: &str, }
ext: &ConfigExternal,
) -> Result<Box<dyn Device>, DeviceConfigError>;
} }
#[derive(Debug, Deserialize)] impl DerefMut for WrappedDevice {
#[enum_dispatch(DeviceConfig)] fn deref_mut(&mut self) -> &mut Self::Target {
pub enum DeviceConfigs { &mut self.0
AirFilter(AirFilterConfig), }
AudioSetup(AudioSetupConfig),
ContactSensor(ContactSensorConfig),
DebugBridge(DebugBridgeConfig),
IkeaOutlet(IkeaOutletConfig),
KasaOutlet(KasaOutletConfig),
WakeOnLAN(WakeOnLANConfig),
Washer(WasherConfig),
HueBridge(HueBridgeConfig),
HueGroup(HueGroupConfig),
LightSensor(LightSensorConfig),
} }
impl mlua::UserData for WrappedDevice {}
pub type WrappedDevice = Arc<RwLock<Box<dyn Device>>>; pub type DeviceMap = HashMap<String, Arc<RwLock<Box<dyn Device>>>>;
pub type DeviceMap = HashMap<String, WrappedDevice>;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DeviceManager { pub struct DeviceManager {
@@ -137,13 +122,13 @@ impl DeviceManager {
sched.start().await.unwrap(); sched.start().await.unwrap();
} }
pub async fn add(&self, device: Box<dyn Device>) { pub async fn add(&self, device: &WrappedDevice) {
let id = device.get_id().into(); let id = device.read().await.get_id();
debug!(id, "Adding device"); debug!(id, "Adding device");
// If the device listens to mqtt, subscribe to the topics // If the device listens to mqtt, subscribe to the topics
if let Some(device) = As::<dyn OnMqtt>::cast(device.as_ref()) { if let Some(device) = As::<dyn OnMqtt>::cast(device.read().await.as_ref()) {
for topic in device.topics() { for topic in device.topics() {
trace!(id, topic, "Subscribing to topic"); trace!(id, topic, "Subscribing to topic");
if let Err(err) = self.client.subscribe(topic, QoS::AtLeastOnce).await { if let Err(err) = self.client.subscribe(topic, QoS::AtLeastOnce).await {
@@ -154,28 +139,7 @@ impl DeviceManager {
} }
} }
// Wrap the device self.devices.write().await.insert(id, device.0.clone());
let device = Arc::new(RwLock::new(device));
self.devices.write().await.insert(id, device);
}
pub async fn create(
&self,
identifier: &str,
device_config: DeviceConfigs,
) -> Result<(), DeviceConfigError> {
let ext = ConfigExternal {
client: &self.client,
device_manager: self,
event_channel: &self.event_channel,
};
let device = device_config.create(identifier, &ext).await?;
self.add(device).await;
Ok(())
} }
pub fn event_channel(&self) -> EventChannel { pub fn event_channel(&self) -> EventChannel {
@@ -183,7 +147,12 @@ impl DeviceManager {
} }
pub async fn get(&self, name: &str) -> Option<WrappedDevice> { pub async fn get(&self, name: &str) -> Option<WrappedDevice> {
self.devices.read().await.get(name).cloned() self.devices
.read()
.await
.get(name)
.cloned()
.map(WrappedDevice)
} }
pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> { pub async fn devices(&self) -> RwLockReadGuard<DeviceMap> {
@@ -209,6 +178,7 @@ impl DeviceManager {
if subscribed { if subscribed {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_mqtt(message).await; device.on_mqtt(message).await;
trace!(id, "Done");
} }
} }
} }
@@ -224,6 +194,7 @@ impl DeviceManager {
if let Some(device) = As::<dyn OnDarkness>::cast_mut(device) { if let Some(device) = As::<dyn OnDarkness>::cast_mut(device) {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_darkness(dark).await; device.on_darkness(dark).await;
trace!(id, "Done");
} }
}); });
@@ -237,6 +208,7 @@ impl DeviceManager {
if let Some(device) = As::<dyn OnPresence>::cast_mut(device) { if let Some(device) = As::<dyn OnPresence>::cast_mut(device) {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_presence(presence).await; device.on_presence(presence).await;
trace!(id, "Done");
} }
}); });
@@ -252,6 +224,7 @@ impl DeviceManager {
if let Some(device) = As::<dyn OnNotification>::cast_mut(device) { if let Some(device) = As::<dyn OnNotification>::cast_mut(device) {
trace!(id, "Handling"); trace!(id, "Handling");
device.on_notification(notification).await; device.on_notification(notification).await;
trace!(id, "Done");
} }
} }
}); });
@@ -261,3 +234,19 @@ 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("add_schedule", |lua, this, schedule| async {
let schedule = lua.from_value(schedule)?;
this.add_schedule(schedule).await;
Ok(())
})
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,22 +2,22 @@ use std::time::Duration;
use anyhow::Result; use anyhow::Result;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use google_home::errors::ErrorCode; use google_home::errors::ErrorCode;
use google_home::traits::{self, OnOff}; use google_home::traits::{self, OnOff};
use google_home::types::Type; use google_home::types::Type;
use google_home::{device, GoogleHomeDevice}; use google_home::{device, GoogleHomeDevice};
use rumqttc::{matches, AsyncClient, Publish}; use rumqttc::{matches, Publish};
use serde::Deserialize; use serde::Deserialize;
use serde_with::{serde_as, DurationSeconds};
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
use tracing::{debug, error, trace, warn}; use tracing::{debug, error, trace, warn};
use crate::config::{InfoConfig, MqttDeviceConfig}; use crate::config::{InfoConfig, MqttDeviceConfig};
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{OnMqtt, OnPresence}; use crate::event::{OnMqtt, OnPresence};
use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage}; use crate::messages::{OnOffMessage, RemoteAction, RemoteMessage};
use crate::mqtt::WrappedAsyncClient;
use crate::traits::Timeout; use crate::traits::Timeout;
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)] #[derive(Debug, Clone, Deserialize, PartialEq, Eq, Copy)]
@@ -28,70 +28,33 @@ pub enum OutletType {
Light, Light,
} }
#[serde_as] #[derive(Debug, Clone, LuaDeviceConfig)]
#[derive(Debug, Clone, Deserialize)]
pub struct IkeaOutletConfig { pub struct IkeaOutletConfig {
#[serde(flatten)] #[device_config(flatten)]
info: InfoConfig,
#[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)]
pub remotes: Vec<MqttDeviceConfig>,
}
fn default_outlet_type() -> OutletType {
OutletType::Outlet
}
#[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, info: InfoConfig,
#[device_config(flatten)]
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
#[device_config(default(OutletType::Outlet))]
outlet_type: OutletType, outlet_type: OutletType,
#[device_config(default, with(|t: Option<_>| t.map(Duration::from_secs)))]
timeout: Option<Duration>, timeout: Option<Duration>,
remotes: Vec<MqttDeviceConfig>, #[device_config(default)]
pub remotes: Vec<MqttDeviceConfig>,
#[device_config(from_lua)]
client: WrappedAsyncClient,
}
#[derive(Debug, LuaDevice)]
pub struct IkeaOutlet {
#[config]
config: IkeaOutletConfig,
client: AsyncClient,
last_known_state: bool, last_known_state: bool,
handle: Option<JoinHandle<()>>, handle: Option<JoinHandle<()>>,
} }
async fn set_on(client: AsyncClient, topic: &str, on: bool) { async fn set_on(client: WrappedAsyncClient, topic: &str, on: bool) {
let message = OnOffMessage::new(on); let message = OnOffMessage::new(on);
let topic = format!("{}/set", topic); let topic = format!("{}/set", topic);
@@ -108,9 +71,21 @@ async fn set_on(client: AsyncClient, topic: &str, on: bool) {
.ok(); .ok();
} }
impl IkeaOutlet {
async fn create(config: IkeaOutletConfig) -> Result<Self, DeviceConfigError> {
trace!(id = config.info.identifier(), "Setting up IkeaOutlet");
Ok(Self {
config,
last_known_state: false,
handle: None,
})
}
}
impl Device for IkeaOutlet { impl Device for IkeaOutlet {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.info.identifier()
} }
} }
@@ -118,24 +93,25 @@ impl Device for IkeaOutlet {
impl OnMqtt for IkeaOutlet { impl OnMqtt for IkeaOutlet {
fn topics(&self) -> Vec<&str> { fn topics(&self) -> Vec<&str> {
let mut topics: Vec<_> = self let mut topics: Vec<_> = self
.config
.remotes .remotes
.iter() .iter()
.map(|mqtt| mqtt.topic.as_str()) .map(|mqtt| mqtt.topic.as_str())
.collect(); .collect();
topics.push(&self.mqtt.topic); topics.push(&self.config.mqtt.topic);
topics topics
} }
async fn on_mqtt(&mut self, message: Publish) { async fn on_mqtt(&mut self, message: Publish) {
// Check if the message is from the deviec itself or from a remote // Check if the message is from the deviec itself or from a remote
if matches(&message.topic, &self.mqtt.topic) { if matches(&message.topic, &self.config.mqtt.topic) {
// Update the internal state based on what the device has reported // Update the internal state based on what the device has reported
let state = match OnOffMessage::try_from(message) { let state = match OnOffMessage::try_from(message) {
Ok(state) => state.state(), Ok(state) => state.state(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(id = Device::get_id(self), "Failed to parse message: {err}");
return; return;
} }
}; };
@@ -148,18 +124,18 @@ impl OnMqtt for IkeaOutlet {
// Abort any timer that is currently running // Abort any timer that is currently running
self.stop_timeout().await.unwrap(); self.stop_timeout().await.unwrap();
debug!(id = self.identifier, "Updating state to {state}"); debug!(id = Device::get_id(self), "Updating state to {state}");
self.last_known_state = state; self.last_known_state = state;
// If this is a kettle start a timeout for turning it of again // If this is a kettle start a timeout for turning it of again
if state && let Some(timeout) = self.timeout { if state && let Some(timeout) = self.config.timeout {
self.start_timeout(timeout).await.unwrap(); self.start_timeout(timeout).await.unwrap();
} }
} else { } else {
let action = match RemoteMessage::try_from(message) { let action = match RemoteMessage::try_from(message) {
Ok(message) => message.action(), Ok(message) => message.action(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(id = Device::get_id(self), "Failed to parse message: {err}");
return; return;
} }
}; };
@@ -178,8 +154,8 @@ impl OnMqtt for IkeaOutlet {
impl OnPresence for IkeaOutlet { impl OnPresence for IkeaOutlet {
async fn on_presence(&mut self, presence: bool) { async fn on_presence(&mut self, presence: bool) {
// Turn off the outlet when we leave the house (Not if it is a battery charger) // Turn off the outlet when we leave the house (Not if it is a battery charger)
if !presence && self.outlet_type != OutletType::Charger { if !presence && self.config.outlet_type != OutletType::Charger {
debug!(id = self.identifier, "Turning device off"); debug!(id = Device::get_id(self), "Turning device off");
self.set_on(false).await.ok(); self.set_on(false).await.ok();
} }
} }
@@ -187,7 +163,7 @@ impl OnPresence for IkeaOutlet {
impl GoogleHomeDevice for IkeaOutlet { impl GoogleHomeDevice for IkeaOutlet {
fn get_device_type(&self) -> Type { fn get_device_type(&self) -> Type {
match self.outlet_type { match self.config.outlet_type {
OutletType::Outlet => Type::Outlet, OutletType::Outlet => Type::Outlet,
OutletType::Kettle => Type::Kettle, OutletType::Kettle => Type::Kettle,
OutletType::Light => Type::Light, // Find a better device type for this, ideally would like to use charger, but that needs more work OutletType::Light => Type::Light, // Find a better device type for this, ideally would like to use charger, but that needs more work
@@ -196,10 +172,10 @@ impl GoogleHomeDevice for IkeaOutlet {
} }
fn get_device_name(&self) -> device::Name { fn get_device_name(&self) -> device::Name {
device::Name::new(&self.info.name) device::Name::new(&self.config.info.name)
} }
fn get_id(&self) -> &str { fn get_id(&self) -> String {
Device::get_id(self) Device::get_id(self)
} }
@@ -208,7 +184,7 @@ impl GoogleHomeDevice for IkeaOutlet {
} }
fn get_room_hint(&self) -> Option<&str> { fn get_room_hint(&self) -> Option<&str> {
self.info.room.as_deref() self.config.info.room.as_deref()
} }
fn will_report_state(&self) -> bool { fn will_report_state(&self) -> bool {
@@ -224,7 +200,7 @@ impl traits::OnOff for IkeaOutlet {
} }
async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> { async fn set_on(&mut self, on: bool) -> Result<(), ErrorCode> {
set_on(self.client.clone(), &self.mqtt.topic, on).await; set_on(self.config.client.clone(), &self.config.mqtt.topic, on).await;
Ok(()) Ok(())
} }
@@ -239,9 +215,9 @@ impl crate::traits::Timeout for IkeaOutlet {
// Turn the kettle of after the specified timeout // Turn the kettle of after the specified timeout
// TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet // TODO: Impl Drop for IkeaOutlet that will abort the handle if the IkeaOutlet
// get dropped // get dropped
let client = self.client.clone(); let client = self.config.client.clone();
let topic = self.mqtt.topic.clone(); let topic = self.config.mqtt.topic.clone();
let id = self.identifier.clone(); let id = Device::get_id(self).clone();
self.handle = Some(tokio::spawn(async move { self.handle = Some(tokio::spawn(async move {
debug!(id, "Starting timeout ({timeout:?})..."); debug!(id, "Starting timeout ({timeout:?})...");
tokio::time::sleep(timeout).await; tokio::time::sleep(timeout).await;

View File

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

View File

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

View File

@@ -15,23 +15,23 @@ mod washer;
use google_home::device::AsGoogleHomeDevice; use google_home::device::AsGoogleHomeDevice;
use google_home::traits::OnOff; use google_home::traits::OnOff;
pub use self::air_filter::AirFilterConfig; pub use self::air_filter::*;
pub use self::audio_setup::AudioSetupConfig; pub use self::audio_setup::*;
pub use self::contact_sensor::ContactSensorConfig; pub use self::contact_sensor::*;
pub use self::debug_bridge::DebugBridgeConfig; pub use self::debug_bridge::*;
pub use self::hue_bridge::HueBridgeConfig; pub use self::hue_bridge::*;
pub use self::hue_light::HueGroupConfig; pub use self::hue_light::*;
pub use self::ikea_outlet::IkeaOutletConfig; pub use self::ikea_outlet::*;
pub use self::kasa_outlet::KasaOutletConfig; pub use self::kasa_outlet::*;
pub use self::light_sensor::{LightSensor, LightSensorConfig}; pub use self::light_sensor::*;
pub use self::ntfy::{Notification, Ntfy}; pub use self::ntfy::{Notification, Ntfy};
pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE}; pub use self::presence::{Presence, PresenceConfig, DEFAULT_PRESENCE};
pub use self::wake_on_lan::WakeOnLANConfig; pub use self::wake_on_lan::*;
pub use self::washer::WasherConfig; pub use self::washer::*;
use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence}; use crate::event::{OnDarkness, OnMqtt, OnNotification, OnPresence};
use crate::traits::Timeout; use crate::traits::Timeout;
#[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + OnNotification + OnOff + Timeout)] #[impl_cast::device(As: OnMqtt + OnPresence + OnDarkness + OnNotification + OnOff + Timeout)]
pub trait Device: AsGoogleHomeDevice + std::fmt::Debug + Sync + Send { pub trait Device: AsGoogleHomeDevice + std::fmt::Debug + Sync + Send {
fn get_id(&self) -> &str; fn get_id(&self) -> String;
} }

View File

@@ -1,21 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use serde::Serialize; use serde::Serialize;
use serde_repr::*; use serde_repr::*;
use tracing::{debug, error, warn}; use tracing::{error, trace, warn};
use crate::config::NtfyConfig;
use crate::devices::Device; use crate::devices::Device;
use crate::error::DeviceConfigError;
use crate::event::{self, Event, EventChannel, OnNotification, OnPresence}; use crate::event::{self, Event, EventChannel, OnNotification, OnPresence};
#[derive(Debug)]
pub struct Ntfy {
base_url: String,
topic: String,
tx: event::Sender,
}
#[derive(Debug, Serialize_repr, Clone, Copy)] #[derive(Debug, Serialize_repr, Clone, Copy)]
#[repr(u8)] #[repr(u8)]
pub enum Priority { pub enum Priority {
@@ -116,22 +110,41 @@ impl Default for Notification {
} }
} }
impl Ntfy { #[derive(Debug, LuaDeviceConfig)]
pub fn new(config: NtfyConfig, event_channel: &EventChannel) -> Self { pub struct NtfyConfig {
Self { #[device_config(default("https://ntfy.sh".into()))]
base_url: config.url, url: String,
topic: config.topic, topic: String,
tx: event_channel.get_tx(), #[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
} tx: event::Sender,
} }
#[derive(Debug, LuaDevice)]
pub struct Ntfy {
#[config]
config: NtfyConfig,
}
impl Ntfy {
async fn create(config: NtfyConfig) -> Result<Self, DeviceConfigError> {
trace!(id = "ntfy", "Setting up Ntfy");
Ok(Self { config })
}
}
impl Device for Ntfy {
fn get_id(&self) -> String {
"ntfy".to_string()
}
}
impl Ntfy {
async fn send(&self, notification: Notification) { async fn send(&self, notification: Notification) {
let notification = notification.finalize(&self.topic); let notification = notification.finalize(&self.config.topic);
debug!("Sending notfication");
// Create the request // Create the request
let res = reqwest::Client::new() let res = reqwest::Client::new()
.post(self.base_url.clone()) .post(self.config.url.clone())
.json(&notification) .json(&notification)
.send() .send()
.await; .await;
@@ -147,12 +160,6 @@ impl Ntfy {
} }
} }
impl Device for Ntfy {
fn get_id(&self) -> &str {
"ntfy"
}
}
#[async_trait] #[async_trait]
impl OnPresence for Ntfy { impl OnPresence for Ntfy {
async fn on_presence(&mut self, presence: bool) { async fn on_presence(&mut self, presence: bool) {
@@ -177,7 +184,13 @@ impl OnPresence for Ntfy {
.add_action(action) .add_action(action)
.set_priority(Priority::Low); .set_priority(Priority::Low);
if self.tx.send(Event::Ntfy(notification)).await.is_err() { if self
.config
.tx
.send(Event::Ntfy(notification))
.await
.is_err()
{
warn!("There are no receivers on the event channel"); warn!("There are no receivers on the event channel");
} }
} }

View File

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

View File

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

View File

@@ -1,57 +1,45 @@
use async_trait::async_trait; use async_trait::async_trait;
use automation_macro::{LuaDevice, LuaDeviceConfig};
use rumqttc::Publish; use rumqttc::Publish;
use serde::Deserialize; use tracing::{debug, error, trace, warn};
use tracing::{debug, error, warn};
use super::ntfy::Priority; use super::ntfy::Priority;
use super::{Device, Notification}; use super::{Device, Notification};
use crate::config::MqttDeviceConfig; use crate::config::MqttDeviceConfig;
use crate::device_manager::{ConfigExternal, DeviceConfig};
use crate::error::DeviceConfigError; use crate::error::DeviceConfigError;
use crate::event::{Event, EventChannel, OnMqtt}; use crate::event::{self, Event, EventChannel, OnMqtt};
use crate::messages::PowerMessage; use crate::messages::PowerMessage;
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone, LuaDeviceConfig)]
pub struct WasherConfig { pub struct WasherConfig {
#[serde(flatten)] identifier: String,
#[device_config(flatten)]
mqtt: MqttDeviceConfig, mqtt: MqttDeviceConfig,
threshold: f32, // Power in Watt // Power in Watt
} threshold: f32,
#[device_config(rename("event_channel"), from_lua, with(|ec: EventChannel| ec.get_tx()))]
#[async_trait] pub tx: event::Sender,
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))
}
} }
// TODO: Add google home integration // TODO: Add google home integration
#[derive(Debug, LuaDevice)]
pub struct Washer {
#[config]
config: WasherConfig,
#[derive(Debug)]
struct Washer {
identifier: String,
mqtt: MqttDeviceConfig,
event_channel: EventChannel,
threshold: f32,
running: isize, running: isize,
} }
impl Washer {
async fn create(config: WasherConfig) -> Result<Self, DeviceConfigError> {
trace!(id = config.identifier, "Setting up Washer");
Ok(Self { config, running: 0 })
}
}
impl Device for Washer { impl Device for Washer {
fn get_id(&self) -> &str { fn get_id(&self) -> String {
&self.identifier self.config.identifier.clone()
} }
} }
@@ -63,26 +51,29 @@ const HYSTERESIS: isize = 10;
#[async_trait] #[async_trait]
impl OnMqtt for Washer { impl OnMqtt for Washer {
fn topics(&self) -> Vec<&str> { fn topics(&self) -> Vec<&str> {
vec![&self.mqtt.topic] vec![&self.config.mqtt.topic]
} }
async fn on_mqtt(&mut self, message: Publish) { async fn on_mqtt(&mut self, message: Publish) {
let power = match PowerMessage::try_from(message) { let power = match PowerMessage::try_from(message) {
Ok(state) => state.power(), Ok(state) => state.power(),
Err(err) => { Err(err) => {
error!(id = self.identifier, "Failed to parse message: {err}"); error!(
id = self.config.identifier,
"Failed to parse message: {err}"
);
return; return;
} }
}; };
// debug!(id = self.identifier, power, "Washer state update"); // debug!(id = self.identifier, power, "Washer state update");
if power < self.threshold && self.running >= HYSTERESIS { if power < self.config.threshold && self.running >= HYSTERESIS {
// The washer is done running // The washer is done running
debug!( debug!(
id = self.identifier, id = self.config.identifier,
power, power,
threshold = self.threshold, threshold = self.config.threshold,
"Washer is done" "Washer is done"
); );
@@ -94,23 +85,23 @@ impl OnMqtt for Washer {
.set_priority(Priority::High); .set_priority(Priority::High);
if self if self
.event_channel .config
.get_tx() .tx
.send(Event::Ntfy(notification)) .send(Event::Ntfy(notification))
.await .await
.is_err() .is_err()
{ {
warn!("There are no receivers on the event channel"); warn!("There are no receivers on the event channel");
} }
} else if power < self.threshold { } else if power < self.config.threshold {
// Prevent false positives // Prevent false positives
self.running = 0; self.running = 0;
} else if power >= self.threshold && self.running < HYSTERESIS { } else if power >= self.config.threshold && self.running < HYSTERESIS {
// Washer could be starting // Washer could be starting
debug!( debug!(
id = self.identifier, id = self.config.identifier,
power, power,
threshold = self.threshold, threshold = self.config.threshold,
"Washer is starting" "Washer is starting"
); );

View File

@@ -1,5 +1,6 @@
use async_trait::async_trait; use async_trait::async_trait;
use impl_cast::device_trait; use impl_cast::device_trait;
use mlua::FromLua;
use rumqttc::Publish; use rumqttc::Publish;
use tokio::sync::mpsc; use tokio::sync::mpsc;
@@ -16,7 +17,7 @@ pub enum Event {
pub type Sender = mpsc::Sender<Event>; pub type Sender = mpsc::Sender<Event>;
pub type Receiver = mpsc::Receiver<Event>; pub type Receiver = mpsc::Receiver<Event>;
#[derive(Clone, Debug)] #[derive(Clone, Debug, FromLua)]
pub struct EventChannel(Sender); pub struct EventChannel(Sender);
impl EventChannel { impl EventChannel {
@@ -31,6 +32,8 @@ impl EventChannel {
} }
} }
impl mlua::UserData for EventChannel {}
#[async_trait] #[async_trait]
#[device_trait] #[device_trait]
pub trait OnMqtt { pub trait OnMqtt {

View File

@@ -1,12 +1,15 @@
#![feature(async_closure)] #![feature(async_closure)]
use std::process; use std::{fs, process};
use automation::auth::{OpenIDConfig, User}; use automation::auth::{OpenIDConfig, User};
use automation::config::Config; use automation::config::Config;
use automation::device_manager::DeviceManager; use automation::device_manager::DeviceManager;
use automation::devices::{Ntfy, Presence}; use automation::devices::{
AirFilter, AudioSetup, ContactSensor, DebugBridge, HueBridge, HueGroup, IkeaOutlet, KasaOutlet,
LightSensor, Ntfy, Presence, WakeOnLAN, Washer,
};
use automation::error::ApiError; use automation::error::ApiError;
use automation::mqtt; use automation::mqtt::{self, WrappedAsyncClient};
use axum::extract::FromRef; use axum::extract::FromRef;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
@@ -15,7 +18,7 @@ use axum::{Json, Router};
use dotenvy::dotenv; use dotenvy::dotenv;
use google_home::{GoogleHome, Request}; use google_home::{GoogleHome, Request};
use rumqttc::AsyncClient; use rumqttc::AsyncClient;
use tracing::{debug, error, info}; use tracing::{debug, error, info, warn};
#[derive(Clone)] #[derive(Clone)]
struct AppState { struct AppState {
@@ -59,24 +62,57 @@ async fn app() -> anyhow::Result<()> {
// Setup the device handler // Setup the device handler
let device_manager = DeviceManager::new(client.clone()); let device_manager = DeviceManager::new(client.clone());
for (id, device_config) in config.devices {
device_manager.create(&id, device_config).await?;
}
device_manager.add_schedule(config.schedule).await;
let event_channel = device_manager.event_channel(); let event_channel = device_manager.event_channel();
// Create and add the presence system // Lua testing
{ {
let presence = Presence::new(config.presence, &event_channel); let lua = mlua::Lua::new();
device_manager.add(Box::new(presence)).await;
}
// Start the ntfy service if it is configured lua.set_warning_function(|_lua, text, _cont| {
if let Some(config) = config.ntfy { warn!("{text}");
let ntfy = Ntfy::new(config, &event_channel); Ok(())
device_manager.add(Box::new(ntfy)).await; });
let automation = lua.create_table()?;
automation.set("device_manager", device_manager.clone())?;
automation.set("mqtt_client", WrappedAsyncClient(client.clone()))?;
automation.set("event_channel", device_manager.event_channel())?;
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)?;
automation.set("util", util)?;
lua.globals().set("automation", automation)?;
// Register all the device types
Ntfy::register_with_lua(&lua)?;
Presence::register_with_lua(&lua)?;
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)?;
WakeOnLAN::register_with_lua(&lua)?;
Washer::register_with_lua(&lua)?;
// TODO: Make this not hardcoded
let filename = "config.lua";
let file = fs::read_to_string(filename)?;
match lua.load(file).set_name(filename).exec_async().await {
Err(error) => {
println!("{error}");
Err(error)
}
result => result,
}?;
} }
// Wrap the mqtt eventloop and start listening for message // Wrap the mqtt eventloop and start listening for message

View File

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