diff --git a/Cargo.lock b/Cargo.lock index 60b55bf..63b3c9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,12 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "aho-corasick" version = "0.7.20" @@ -17,6 +23,88 @@ version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" +[[package]] +name = "async-broadcast" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c48ccdbf6ca6b121e0f586cbc0e73ae440e56c67c30fa0873b4e110d9c26d2b" +dependencies = [ + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" +dependencies = [ + "async-lock", + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c374dda1ed3e7d8f0d9ba58715f924862c63eae6849c92d3a18e7fbde9e2794" +dependencies = [ + "async-lock", + "autocfg", + "concurrent-queue", + "futures-lite", + "libc", + "log", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "windows-sys 0.42.0", +] + +[[package]] +name = "async-lock" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8101efe8695a6c17e02911402145357e718ac92d3ff88ae8419e84b1707b685" +dependencies = [ + "event-listener", + "futures-lite", +] + +[[package]] +name = "async-recursion" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b015a331cc64ebd1774ba119538573603427eaace0a1950c423ab971f903796" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a40729d2133846d9ed0ea60a8b9541bccddab49cd30f0715a1da672fe9a2524" + +[[package]] +name = "async-trait" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd7fce9ba8c3c042128ce72d8b2ddbf3a05747efb67ea0313c635e10bda47a2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -29,18 +117,51 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +[[package]] +name = "bytemuck" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.4.0" @@ -59,6 +180,21 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "concurrent-queue" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c278839b831783b70278b14df4d45e1beb1aad306c07bb796637de9a0e323e8e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -75,6 +211,104 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset 0.8.0", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "directories" version = "4.0.1" @@ -84,6 +318,25 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs-sys" version = "0.3.7" @@ -95,6 +348,44 @@ dependencies = [ "winapi", ] +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.10.0" @@ -129,12 +420,53 @@ dependencies = [ "libc", ] +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + [[package]] name = "exitcode" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" +[[package]] +name = "exr" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8af5ef47e2ed89d23d0ecbc1b681b30390069de70260937877514377fc24feb" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "smallvec", + "threadpool", + "zune-inflate", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.10.14" @@ -205,6 +537,21 @@ version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfb8371b6fb2aeb2d280374607aeabfc99d95c72edfe51692e42d3d7f0d08531" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.26" @@ -246,6 +593,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.8" @@ -259,6 +616,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edd93c6756b4dfaf2709eafcc345ba2636565295c198a9cfbf75fa5e3e00b06" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "half" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b4af3693f1b705df946e9fe5631932443781d0aabb423b62fcd4d73f6d2fd0" +dependencies = [ + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -270,20 +646,34 @@ name = "hassliebe" version = "0.1.0" dependencies = [ "anyhow", + "base64", "directories", "env_logger", "exitcode", + "image", "json", "lazy_static", "log", "mac_address", + "notify-rust", "rand", "regex", "rumqttc", "serde", + "serde_json", "tokio", "toml", "validator", + "void", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", ] [[package]] @@ -301,6 +691,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "humantime" version = "2.1.0" @@ -334,6 +730,25 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "image" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b7ea949b537b0fd0af141fff8c77690f2ce96f4f41f042ccb6c69c6c965945" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-rational", + "num-traits", + "png", + "scoped_threadpool", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.2" @@ -344,6 +759,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + [[package]] name = "io-lifetimes" version = "1.0.5" @@ -372,6 +796,15 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" +[[package]] +name = "jpeg-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0000e42512c92e31c2252315bda326620a4e034105e900c98ec492fa077b3e" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.61" @@ -393,6 +826,12 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libc" version = "0.2.139" @@ -424,16 +863,38 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "mac-notification-sys" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e72d50edb17756489e79d52eb146927bec8eba9dd48faadf9ef08bca3791ad5" +dependencies = [ + "cc", + "dirs-next", + "objc-foundation", + "objc_id", + "time", +] + [[package]] name = "mac_address" version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b238e3235c8382b7653c6408ed1b08dd379bdb9fdf990fb0bbae3db2cc0ae963" dependencies = [ - "nix", + "nix 0.23.2", "winapi", ] +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "matches" version = "0.1.10" @@ -455,6 +916,24 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + [[package]] name = "mio" version = "0.8.6" @@ -486,7 +965,21 @@ dependencies = [ "cc", "cfg-if", "libc", - "memoffset", + "memoffset 0.6.5", +] + +[[package]] +name = "nix" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f346ff70e7dbfd675fe90590b92d59ef2de15a8779ae305ebcbfd3f0caf59be4" +dependencies = [ + "autocfg", + "bitflags", + "cfg-if", + "libc", + "memoffset 0.6.5", + "pin-utils", ] [[package]] @@ -498,6 +991,51 @@ dependencies = [ "memchr", ] +[[package]] +name = "notify-rust" +version = "4.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfa211d18e360f08e36c364308f394b5eb23a6629150690e109a916dc6f610e" +dependencies = [ + "image", + "lazy_static", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.15.0" @@ -508,6 +1046,35 @@ dependencies = [ "libc", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + [[package]] name = "once_cell" version = "1.17.1" @@ -520,6 +1087,22 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.12.1" @@ -581,6 +1164,32 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "png" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d708eaf860a19b19ce538740d2b4bdeeb8337fa53f7738455e706623ad5c638" +dependencies = [ + "bitflags", + "crc32fast", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22122d5ec4f9fe1b3916419b76be1e80bcb93f618d071d2edf841b137b2a2bd6" +dependencies = [ + "autocfg", + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "windows-sys 0.42.0", +] + [[package]] name = "pollster" version = "0.2.5" @@ -593,6 +1202,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -626,6 +1245,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quick-xml" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11bafc859c6815fbaffbbbf4229ecb767ac913fecb27f9ad4343662e9ef099ea" +dependencies = [ + "memchr", +] + [[package]] name = "quote" version = "1.0.23" @@ -665,6 +1293,28 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db3a213adf02b3bcfd2d3846bb41cb22857d131789e01df434fb7e7bc0759b7" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "356a0625f1954f730c0201cdab48611198dc6ce21f4acff55089b5a78e6e835b" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -797,6 +1447,12 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "scoped_threadpool" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d51f5df5af43ab3f1360b429fa5e0152ac5ce8c0bd6485cae490332e96846a8" + [[package]] name = "scopeguard" version = "1.1.0" @@ -867,6 +1523,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a5ec9fa74a20ebbe5d9ac23dac1fc96ba0ecfe9f50f2843b52e537b10fbcb4e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.1" @@ -876,6 +1543,17 @@ dependencies = [ "serde", ] +[[package]] +name = "sha1" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "signal-hook-registry" version = "1.4.1" @@ -885,6 +1563,12 @@ dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14a5df39617d7c8558154693a1bb8157a4aab8179209540cc0b10e5dc24e0b18" + [[package]] name = "slab" version = "0.4.8" @@ -925,6 +1609,33 @@ dependencies = [ "lock_api", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7ac893c7d471c8a21f31cfe213ec4f6d9afeed25537c772e08ef3f005f8729e" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339f799d8b549e3744c7ac7feb216383e4005d94bdb22561b3ab8f3b808ae9fb" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "1.0.107" @@ -936,6 +1647,30 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tauri-winrt-notification" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58de036c4d2e20717024de2a3c4bf56c301f07b21bc8ef9b57189fce06f1f3b" +dependencies = [ + "quick-xml", + "strum", + "windows", +] + +[[package]] +name = "tempfile" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.42.0", +] + [[package]] name = "termcolor" version = "1.2.0" @@ -965,6 +1700,42 @@ dependencies = [ "syn", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "tiff" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7449334f9ff2baf290d55d73983a7d6fa15e01198faef72af07e2a8db851e471" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "serde", + "time-core", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + [[package]] name = "tinyvec" version = "1.6.0" @@ -1056,6 +1827,54 @@ dependencies = [ "toml_datetime", ] +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uds_windows" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce65604324d3cce9b966701489fbd0cf318cb1f7bd9dd07ac9a4ee6fb791930d" +dependencies = [ + "tempfile", + "winapi", +] + [[package]] name = "unicode-bidi" version = "0.3.10" @@ -1077,6 +1896,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + [[package]] name = "untrusted" version = "0.7.1" @@ -1142,6 +1967,18 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -1222,6 +2059,21 @@ dependencies = [ "untrusted", ] +[[package]] +name = "weezl" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9193164d4de03a926d909d3bc7c30543cecb35400c02114792c2cae20d5e2dbb" + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1253,6 +2105,19 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c4bd0a50ac6020f65184721f758dba47bb9fbc2133df715ec74a237b26794a" +dependencies = [ + "windows_aarch64_msvc 0.39.0", + "windows_i686_gnu 0.39.0", + "windows_i686_msvc 0.39.0", + "windows_x86_64_gnu 0.39.0", + "windows_x86_64_msvc 0.39.0", +] + [[package]] name = "windows-sys" version = "0.42.0" @@ -1260,12 +2125,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_x86_64_msvc 0.42.1", ] [[package]] @@ -1284,12 +2149,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" dependencies = [ "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", + "windows_aarch64_msvc 0.42.1", + "windows_i686_gnu 0.42.1", + "windows_i686_msvc 0.42.1", + "windows_x86_64_gnu 0.42.1", "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_x86_64_msvc 0.42.1", ] [[package]] @@ -1298,24 +2163,48 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +[[package]] +name = "windows_aarch64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec7711666096bd4096ffa835238905bb33fb87267910e154b18b44eaabb340f2" + [[package]] name = "windows_aarch64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +[[package]] +name = "windows_i686_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "763fc57100a5f7042e3057e7e8d9bdd7860d330070251a73d003563a3bb49e1b" + [[package]] name = "windows_i686_gnu" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +[[package]] +name = "windows_i686_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bc7cbfe58828921e10a9f446fcaaf649204dcfe6c1ddd712c5eebae6bda1106" + [[package]] name = "windows_i686_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +[[package]] +name = "windows_x86_64_gnu" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6868c165637d653ae1e8dc4d82c25d4f97dd6605eaa8d784b5c6e0ab2a252b65" + [[package]] name = "windows_x86_64_gnu" version = "0.42.1" @@ -1328,8 +2217,111 @@ version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +[[package]] +name = "windows_x86_64_msvc" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e4d40883ae9cae962787ca76ba76390ffa29214667a111db9e0a1ad8377e809" + [[package]] name = "windows_x86_64_msvc" version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" + +[[package]] +name = "zbus" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f770930448dd412a4a7131dd968a8e6df0064db4d7916fbbd2d6c3f26b566938" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-recursion", + "async-task", + "async-trait", + "byteorder", + "derivative", + "dirs", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix 0.25.1", + "once_cell", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "winapi", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4832059b438689017db7340580ebabba07f114eab91bf990c6e55052408b40d8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn", +] + +[[package]] +name = "zbus_names" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f34f314916bd89bdb9934154627fab152f4f28acdda03e7c4c68181b214fe7e3" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zune-inflate" +version = "0.2.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589245df6230839c305984dcc0a8385cc72af1fd223f360ffd5d65efa4216d40" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zvariant" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "903169c05b9ab948ee93fefc9127d08930df4ce031d46c980784274439803e51" +dependencies = [ + "byteorder", + "enumflags2", + "libc", + "serde", + "static_assertions", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce76636e8fab7911be67211cf378c252b115ee7f2bae14b18b84821b39260b5" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml index a29ea5d..5bd7ea5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,17 +10,22 @@ dry_run = [] # will prevent some actions like shutting down [dependencies] anyhow = "1.0.69" +base64 = "0.21.0" directories = "4.0.1" env_logger = "0.10.0" exitcode = "1.1.2" +image = "0.24.5" json = "0.12.4" lazy_static = "1.4.0" log = "0.4.17" mac_address = "1.1.4" +notify-rust = { version = "4.8.0", features = ["images"] } rand = "0.8.5" regex = "1.7.1" rumqttc = "0.20.0" serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1.0.93" tokio = { version = "1.25.0", features = ["full"] } toml = "0.7.2" validator = { version = "0.16.0", features = ["derive"] } +void = "1.0.2" diff --git a/README.md b/README.md index 2562888..2dde45d 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,8 @@ ## Features - [ ] Fallback MQTT broker address - [x] Command buttons -- [ ] Notifications - - [ ] Actions +- [x] Notifications + - [x] Actions - [ ] System stats - [ ] CPU usage - [ ] RAM usage @@ -14,5 +14,11 @@ - [ ] PipeWire - [ ] File watcher +Ideas: + +- Camera video stream +- Idle time + ## License + Hassliebe is licensed under the [Blue Oak Model License 1.0.0](/LICENSE.md). \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 227be26..2db9cfa 100644 --- a/src/config.rs +++ b/src/config.rs @@ -5,13 +5,12 @@ use std::io::{ErrorKind, Read, Write}; use std::path::Path; use anyhow::{bail, Context, Result}; -use rand::distributions::Alphanumeric; -use rand::Rng; use regex::Regex; use serde::{Deserialize, Serialize}; use validator::{Validate, ValidationError}; use crate::modules; +use crate::util::generate_alphanumeric_id; #[derive(Serialize, Deserialize, Validate)] pub struct Mqtt { @@ -37,7 +36,16 @@ pub struct Internal { } #[derive(Serialize, Deserialize, Validate)] -pub(crate) struct Config { +pub struct Modules { + #[validate] + pub buttons: Option, + + #[validate] + pub notifications: Option, +} + +#[derive(Serialize, Deserialize, Validate)] +pub struct Config { #[validate(custom = "validate_unique_id")] pub unique_id: String, @@ -48,15 +56,15 @@ pub(crate) struct Config { #[validate] pub mqtt: Mqtt, + #[validate] + pub modules: Modules, + #[serde(rename = "DO_NOT_CHANGE")] #[validate] pub internal: Internal, - - #[validate] - pub command_buttons: Option, } -pub(crate) fn validate_unique_id(value: &str) -> Result<(), ValidationError> { +pub fn validate_unique_id(value: &str) -> Result<(), ValidationError> { if Regex::new(r"^[a-zA-Z0-9]+(_[a-zA-Z0-9]+)*$").unwrap().is_match(value) { Ok(()) } else { @@ -64,10 +72,6 @@ pub(crate) fn validate_unique_id(value: &str) -> Result<(), ValidationError> { } } -fn generate_unique_id() -> String { - rand::thread_rng().sample_iter(&Alphanumeric).take(12).map(char::from).collect() -} - fn create_example_config() -> Config { Config { unique_id: "my_pc".to_owned(), @@ -79,16 +83,19 @@ fn create_example_config() -> Config { credentials: None, }, internal: Internal { - stable_id: generate_unique_id(), + stable_id: generate_alphanumeric_id(12), + }, + modules: Modules { + buttons: Some(modules::buttons::Config { + enabled: false, + buttons: Vec::new(), + }), + notifications: Some(modules::notifications::Config { enabled: false }), }, - command_buttons: Some(modules::command_buttons::Config { - enabled: false, - buttons: Vec::new(), - }) } } -pub(crate) fn load(config_file_path: &Path) -> Result> { +pub fn load(config_file_path: &Path) -> Result> { match File::open(config_file_path) { Ok(mut file) => { log::info!("Reading config file: {}", config_file_path.to_string_lossy()); @@ -108,7 +115,6 @@ pub(crate) fn load(config_file_path: &Path) -> Result> { } let mut file = File::create(config_file_path).context("while creating the default config file")?; - let default = toml::to_string::(&create_example_config()).expect("create_example_config() should be valid"); file.write_all(default.as_bytes())?; diff --git a/src/main.rs b/src/main.rs index 26994cb..e14385a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,15 +1,17 @@ use std::collections::{HashMap, HashSet}; use std::path::{Path, PathBuf}; use std::process::exit; +use std::sync::Arc; use anyhow::{anyhow, Result}; -use crate::modules::{ModuleContext, ModuleContextMqtt}; +use crate::modules::{InitializationContext, ModuleContext, ModuleContextMqtt}; use crate::mqtt::OwnedTopicsService; mod config; mod modules; mod mqtt; +mod util; struct Paths { data_directory: Box, @@ -58,18 +60,21 @@ async fn main() -> Result<()> { let owned_topics_service = OwnedTopicsService::new(&paths.data_directory).await?; - let mut module_context = ModuleContext { - config: &config, - mqtt: ModuleContextMqtt { - client: &mqtt_client, - availability_topic: availability_topic.as_str(), - discovery_device_object: &discovery_device_object, - message_handler_by_topic: HashMap::new(), - owned_topics: HashSet::new(), - }, + let mut initialization_context = InitializationContext { + owned_mqtt_topics: HashSet::new(), + message_handler_by_mqtt_topic: HashMap::new(), + + full: Arc::new(ModuleContext { + config, + mqtt: ModuleContextMqtt { + client: mqtt_client, + availability_topic, + discovery_device_object, + }, + }), }; - modules::init_all(&mut module_context).await?; + modules::init_all(&mut initialization_context).await?; - mqtt::start_communication(&module_context, event_loop, owned_topics_service).await + mqtt::start_communication(&initialization_context, event_loop, owned_topics_service).await } diff --git a/src/modules/command_buttons.rs b/src/modules/buttons.rs similarity index 71% rename from src/modules/command_buttons.rs rename to src/modules/buttons.rs index 2f0e1cb..92b93b1 100644 --- a/src/modules/command_buttons.rs +++ b/src/modules/buttons.rs @@ -4,14 +4,13 @@ use tokio::process::Command; use validator::Validate; use crate::config::validate_unique_id; +use crate::modules::InitializationContext; -use super::ModuleContext; - -const MODULE_ID: &str = "power"; +const MODULE_ID: &str = "buttons"; const BUTTON_TRIGGER_TEXT: &str = "press"; #[derive(Serialize, Deserialize, Validate, Clone)] -pub(crate) struct ButtonConfig { +pub struct ButtonConfig { #[validate(custom = "validate_unique_id")] pub id: String, @@ -26,7 +25,7 @@ pub(crate) struct ButtonConfig { } #[derive(Serialize, Deserialize, Validate)] -pub(crate) struct Config { +pub struct Config { #[serde(default)] pub enabled: bool, @@ -34,10 +33,11 @@ pub(crate) struct Config { pub buttons: Vec, } -pub(crate) async fn init(context: &mut ModuleContext<'_>) -> Result<()> { - let config = match &context.config.command_buttons { +pub async fn init(context: &mut InitializationContext) -> Result<()> { + let full_context = context.get_full(); + let config = match &full_context.config.modules.buttons { Some(c) if c.enabled => c, - _ => return Ok(()) + _ => return Ok(()), }; log::info!("Initializing…"); @@ -48,18 +48,17 @@ pub(crate) async fn init(context: &mut ModuleContext<'_>) -> Result<()> { Ok(()) } -async fn init_command_button(context: &mut ModuleContext<'_>, config: ButtonConfig) -> Result<()> { - let entity_id = context.get_entity_id(MODULE_ID, &config.id); - let command_topic = context.mqtt.get_topic("button", &entity_id, "trigger"); +async fn init_command_button(context: &mut InitializationContext, config: ButtonConfig) -> Result<()> { + let entity_id = context.full.get_entity_id(MODULE_ID, &config.id); + let command_topic = context.full.mqtt.get_homeassistant_topic("button", &entity_id, "trigger"); context - .mqtt - .send_retained_message( - context.mqtt.get_topic("button", &entity_id, "config"), + .send_retained_mqtt_message( + context.full.mqtt.get_homeassistant_topic("button", &entity_id, "config"), json::stringify(json::object! { - "availability_topic": context.mqtt.availability_topic, + "availability_topic": context.full.mqtt.availability_topic.clone(), "command_topic": command_topic.as_str(), - "device": context.mqtt.discovery_device_object.clone(), + "device": context.full.mqtt.discovery_device_object.clone(), "icon": "mdi:power", "name": config.name, "payload_press": BUTTON_TRIGGER_TEXT, @@ -69,7 +68,7 @@ async fn init_command_button(context: &mut ModuleContext<'_>, config: ButtonConf ) .await?; - context.mqtt.subscribe(command_topic, move |text| { + context.subscribe_mqtt_topic(command_topic, move |text| { if text == BUTTON_TRIGGER_TEXT { run_command(config.command.clone(), config.run_in_shell); } else { diff --git a/src/modules/mod.rs b/src/modules/mod.rs index 270e702..f409ad2 100644 --- a/src/modules/mod.rs +++ b/src/modules/mod.rs @@ -1,53 +1,66 @@ use std::collections::{HashMap, HashSet}; +use std::sync::Arc; use anyhow::Result; use json::JsonValue; use rumqttc::{AsyncClient as MqttClient, ClientError, QoS}; -pub mod command_buttons; +pub mod buttons; +pub mod notifications; -type MqttMessageHandler<'a> = dyn Fn(&str) -> Result<()> + 'a; +type MqttMessageHandler = dyn Fn(&str) -> Result<()>; -pub struct ModuleContextMqtt<'a> { - pub discovery_device_object: &'a JsonValue, - pub availability_topic: &'a str, - pub client: &'a MqttClient, - pub message_handler_by_topic: HashMap>>, +pub struct ModuleContextMqtt { + pub discovery_device_object: JsonValue, + pub availability_topic: String, + pub client: MqttClient, +} +pub struct InitializationContext { // Owned topics are topics which a retained message was sent into. - pub owned_topics: HashSet, + pub owned_mqtt_topics: HashSet, + pub message_handler_by_mqtt_topic: HashMap>, + + pub full: Arc, } -pub(crate) struct ModuleContext<'a> { - pub config: &'a super::config::Config, - pub mqtt: ModuleContextMqtt<'a>, +impl InitializationContext { + fn subscribe_mqtt_topic Result<()> + 'static>(&mut self, topic: impl Into, handler: F) { + self.message_handler_by_mqtt_topic.insert(topic.into(), Box::new(handler)); + } + + async fn send_retained_mqtt_message(&mut self, topic: impl Into, message: impl Into) -> std::result::Result<(), ClientError> { + let topic = topic.into(); + let message = message.into(); + + self.owned_mqtt_topics.insert(topic.to_owned()); + self.full.mqtt.client.publish(topic, QoS::AtLeastOnce, true, message).await + } + + fn get_full(&self) -> Arc { + self.full.clone() + } } -impl<'a> ModuleContext<'a> { +pub struct ModuleContext { + pub config: super::config::Config, + pub mqtt: ModuleContextMqtt, +} + +impl ModuleContext { fn get_entity_id(&self, module_id: &str, sub_id: &str) -> String { format!("{}_{}_{}", self.config.unique_id, module_id, sub_id) } } -impl<'a> ModuleContextMqtt<'a> { - fn get_topic(&self, component_type: &str, entity_id: &str, suffix: &str) -> String { +impl ModuleContextMqtt { + fn get_homeassistant_topic(&self, component_type: &str, entity_id: &str, suffix: &str) -> String { format!("homeassistant/{}/{}/{}", component_type, entity_id, suffix) } - - fn subscribe Result<()> + 'a>(&mut self, topic: impl Into, handler: F) { - self.message_handler_by_topic.insert(topic.into(), Box::new(handler)); - } - - async fn send_retained_message(&mut self, topic: impl Into, message: impl Into) -> std::result::Result<(), ClientError> { - let topic = topic.into(); - let message = message.into(); - - self.owned_topics.insert(topic.to_owned()); - self.client.publish(topic, QoS::AtLeastOnce, true, message).await - } } -pub(crate) async fn init_all(context: &mut ModuleContext<'_>) -> Result<()> { - command_buttons::init(context).await?; +pub async fn init_all(context: &mut InitializationContext) -> Result<()> { + buttons::init(context).await?; + notifications::init(context).await?; Ok(()) } diff --git a/src/modules/notifications.rs b/src/modules/notifications.rs new file mode 100644 index 0000000..2c9fed2 --- /dev/null +++ b/src/modules/notifications.rs @@ -0,0 +1,221 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use anyhow::{Context, Result}; +use base64::Engine; +use notify_rust::Hint; +use rumqttc::QoS; +use serde::{Deserialize, Serialize}; +use tokio::task::spawn_blocking; +use validator::Validate; + +use crate::modules::{InitializationContext, ModuleContext}; +use crate::util::{generate_alphanumeric_id, hash_string_to_u32, spawn_nonessential}; + +const MODULE_ID: &str = "notifications"; + +#[derive(Serialize, Deserialize, Validate)] +pub struct Config { + #[serde(default)] + pub enabled: bool, +} + +#[derive(Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum NotificationTimeout { + Never, + + #[default] + Default, + + Ms(u16), +} + +#[derive(Deserialize, Default)] +#[serde(rename_all = "snake_case")] +pub enum NotificationUrgency { + Low, + + #[default] + Normal, + Critical, +} + +impl From for notify_rust::Urgency { + fn from(value: NotificationUrgency) -> Self { + match value { + NotificationUrgency::Low => notify_rust::Urgency::Low, + NotificationUrgency::Normal => notify_rust::Urgency::Normal, + NotificationUrgency::Critical => notify_rust::Urgency::Critical, + } + } +} + +#[derive(Deserialize, Validate)] +pub struct NotificationMessage { + /// Using the ID of an existing notification will replace the old notification. + #[validate(length(min = 1))] + id: Option, + + #[serde(default)] + timeout: NotificationTimeout, + + #[serde(default)] + urgency: NotificationUrgency, + + /// Usually used as the title of the notification. + #[validate(length(min = 1))] + summary_text: String, + + /// Basic HTML is usually supported. + #[validate(length(min = 1))] + long_content: Option, + + /// If the notification server supports persisting notifications across sessions, this will prevent it from doing so for this notification. + #[serde(default)] + transient: bool, + + /// Padded base64-encoded image + encoded_image: Option, + + #[serde(default)] + actions: HashMap, +} + +pub async fn init(context: &mut InitializationContext) -> Result<()> { + let full_context = context.get_full(); + let _config = match &full_context.config.modules.notifications { + Some(c) if c.enabled => c, + _ => return Ok(()), + }; + + log::info!("Initializing…"); + + let full_context = context.get_full(); + context.subscribe_mqtt_topic(format!("{}/{}/simple", context.full.config.unique_id, MODULE_ID), move |text| { + let (summary_text, long_content) = text.split_once('\n').unwrap_or((text, "")); + + tokio::spawn(handle_notification_message( + full_context.clone(), + NotificationMessage { + id: None, + transient: true, + summary_text: summary_text.to_owned(), + long_content: if long_content.is_empty() { None } else { Some(long_content.to_owned()) }, + timeout: NotificationTimeout::default(), + urgency: NotificationUrgency::default(), + encoded_image: None, + actions: HashMap::new(), + }, + )); + + Ok(()) + }); + + let full_context = context.get_full(); + context.subscribe_mqtt_topic(format!("{}/{}/json", context.full.config.unique_id, MODULE_ID), move |text| { + let message = serde_json::from_str::(text); + + match message { + Err(error) => log::error!("Could not deserialize message: {}", error), + + Ok(message) => { + spawn_nonessential(handle_notification_message(full_context.clone(), message)); + } + } + + Ok(()) + }); + + context.subscribe_mqtt_topic(format!("{}/{}/close", context.full.config.unique_id, MODULE_ID), move |id| { + spawn_nonessential(close_notification(id.to_owned())); + + Ok(()) + }); + + Ok(()) +} + +async fn handle_notification_message(context: Arc, message: NotificationMessage) -> Result<()> { + let mut notification = notify_rust::Notification::new(); + let id = message.id.unwrap_or_else(|| generate_alphanumeric_id(10)); + let internal_id = hash_string_to_u32(id.as_str()); + + notification.id(internal_id); + notification.summary(message.summary_text.as_str()); + notification.urgency(message.urgency.into()); + notification.hint(Hint::Transient(message.transient)); + notification.timeout(match message.timeout { + NotificationTimeout::Default => -1, + NotificationTimeout::Never => 0, + NotificationTimeout::Ms(ms) => ms.into(), + }); + + for (action_id, label) in message.actions { + notification.action(&action_id, &label); + } + + if let Some(encoded_image) = message.encoded_image { + let image_data = base64::engine::general_purpose::STANDARD_NO_PAD + .decode(encoded_image) + .context("while decoding the base64 image")?; + + let image = image::load_from_memory(&image_data).context("while reading the image")?; + + notification.image_data(image.try_into()?); + } + + if let Some(long_content) = message.long_content { + notification.body(long_content.as_str()); + } + + log::debug!("Showing notification: {}", id); + let handle = notification.show_async().await?; + + { + let context = context.clone(); + + spawn_blocking(move || { + handle.wait_for_action(|action_id| { + spawn_nonessential(send_notification_action_message( + context, + id.clone(), + if action_id == "__closed" { "closed".to_owned() } else { action_id.to_owned() }, + )); + }); + }); + } + + Ok(()) +} + +async fn close_notification(id: String) -> Result<()> { + // We create a new notification because otherwise we would need to store old notification handles + // and notify-rust does not expose a method to close a notification by ID. + + let internal_id = hash_string_to_u32(&id); + let mut notification = notify_rust::Notification::new(); + + notification.id(internal_id); + + log::debug!("Closing notification: {}", id); + let handle = notification.show_async().await?; + handle.close(); + + Ok(()) +} + +async fn send_notification_action_message(context: Arc, notification_id: String, action_id: String) -> Result<()> { + context + .mqtt + .client + .publish( + format!("{}/{}/action/{}", context.config.unique_id, MODULE_ID, notification_id), + QoS::ExactlyOnce, + false, + action_id, + ) + .await?; + + Ok(()) +} diff --git a/src/mqtt.rs b/src/mqtt.rs index d5d3751..54ef9ae 100644 --- a/src/mqtt.rs +++ b/src/mqtt.rs @@ -12,23 +12,25 @@ use tokio::fs::{File, OpenOptions}; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; use tokio::time::{Instant, sleep}; -use crate::modules::ModuleContext; +use crate::modules::InitializationContext; use super::config; -pub(crate) async fn create_client(config: &config::Config, availability_topic: &str) -> Result<(MqttClient, EventLoop)> { +pub async fn create_client(config: &config::Config, availability_topic: &str) -> Result<(MqttClient, EventLoop)> { let mut options = MqttOptions::new(&config.internal.stable_id, config.mqtt.host.to_owned(), config.mqtt.port); options.set_clean_session(true); options.set_keep_alive(Duration::from_secs(5)); options.set_last_will(LastWill::new(availability_topic, "offline", QoS::AtLeastOnce, true)); + options.set_max_packet_size(usize::MAX, usize::MAX); + let (mqtt_client, event_loop) = MqttClient::new(options, 10); mqtt_client.publish(availability_topic, QoS::AtLeastOnce, true, "online").await?; Ok((mqtt_client, event_loop)) } -pub(crate) fn create_discovery_device_object(config: &config::Config) -> JsonValue { +pub fn create_discovery_device_object(config: &config::Config) -> JsonValue { json::object! { "connections": if config.announce_mac_address { mac_address::get_mac_address().unwrap_or(None).map(|a| json::array![["mac", a.to_string()]]).unwrap_or(json::array![]) @@ -45,7 +47,7 @@ pub struct OwnedTopicsService { } impl OwnedTopicsService { - pub(crate) async fn new(data_directory_path: &Path) -> Result { + pub async fn new(data_directory_path: &Path) -> Result { let path = data_directory_path.join("owned_topics"); let mut file = OpenOptions::new().write(true).read(true).create(true).open(path).await?; @@ -57,11 +59,11 @@ impl OwnedTopicsService { Ok(OwnedTopicsService { file, old_topics }) } - pub(crate) async fn clear_old_and_save_new(mut self, mqtt_client: &MqttClient, new_topics: &HashSet) -> Result<()> { + pub async fn clear_old_and_save_new(mut self, mqtt_client: &MqttClient, new_topics: &HashSet) -> Result<()> { let unused_topics = self.old_topics.difference(new_topics).map(|s| s.to_owned()).collect::>(); log::info!( - "{} unused owned topics will be cleared. Now using {} owned topics.", + "{} unused owned topic(s) will be cleared. Now using {} owned topic(s).", unused_topics.len(), new_topics.len() ); @@ -100,17 +102,21 @@ const FAST_RETRYING_INTERVAL_MS: u64 = 500; const FAST_RETRYING_LIMIT_SECONDS: u64 = 15; const SLOW_RETRYING_INTERVAL_SECONDS: u64 = 5; -pub(crate) async fn start_communication(context: &ModuleContext<'_>, mut event_loop: EventLoop, owned_topics_service: OwnedTopicsService) -> Result<()> { - log::info!("Connecting to MQTT broker at {}:{}", context.config.mqtt.host, context.config.mqtt.port); +pub async fn start_communication(context: &InitializationContext, mut event_loop: EventLoop, owned_topics_service: OwnedTopicsService) -> Result<()> { + log::info!( + "Connecting to MQTT broker at {}:{}", + context.full.config.mqtt.host, + context.full.config.mqtt.port + ); - if !context.mqtt.message_handler_by_topic.is_empty() { + if !context.message_handler_by_mqtt_topic.is_empty() { context + .full .mqtt .client .subscribe_many( context - .mqtt - .message_handler_by_topic + .message_handler_by_mqtt_topic .keys() .map(|k| SubscribeFilter::new(k.to_owned(), QoS::AtLeastOnce)), ) @@ -173,8 +179,7 @@ pub(crate) async fn start_communication(context: &ModuleContext<'_>, mut event_l } if let Some(service) = owned_topics_service.take() { - service.clear_old_and_save_new(context.mqtt.client, &context.mqtt.owned_topics) - .await?; + service.clear_old_and_save_new(&context.full.mqtt.client, &context.owned_mqtt_topics).await?; } connection_state = ConnectionState::Connected; @@ -183,7 +188,7 @@ pub(crate) async fn start_communication(context: &ModuleContext<'_>, mut event_l Incoming(Packet::Publish(message)) => { let text = std::str::from_utf8(&message.payload)?; - if let Some(handler) = context.mqtt.message_handler_by_topic.get(message.topic.as_str()) { + if let Some(handler) = context.message_handler_by_mqtt_topic.get(message.topic.as_str()) { handler(text)?; } } diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..2c29408 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,27 @@ +use std::collections::hash_map::DefaultHasher; +use std::future::Future; +use std::hash::{Hash, Hasher}; + +use anyhow::Result; +use rand::distributions::Alphanumeric; +use rand::Rng; +use tokio::task::JoinHandle; + +#[inline] +pub fn spawn_nonessential(future: impl Future> + Send + 'static) -> JoinHandle<()> { + tokio::spawn(async { + if let Err(error) = future.await { + log::error!("{:#}", error) + } + }) +} + +pub fn generate_alphanumeric_id(length: usize) -> String { + rand::thread_rng().sample_iter(&Alphanumeric).take(length).map(char::from).collect() +} + +pub fn hash_string_to_u32(string: &str) -> u32 { + let mut hasher = DefaultHasher::new(); + string.hash(&mut hasher); + (hasher.finish() / 2) as u32 +}