From 10e1181c0b52a1e54bacf28ae40d58015ee81ed5 Mon Sep 17 00:00:00 2001 From: Luke Granger-Brown Date: Sun, 20 Oct 2024 23:20:48 +0100 Subject: [PATCH] matrix2mqtt: init --- .hgignore | 1 + ci-root.nix | 1 + default.nix | 1 + ops/nixos/totoro/home-assistant.nix | 1 + rust/default.nix | 9 + rust/matrix2mqtt/Cargo.lock | 702 ++++++++++++++++++++++++++++ rust/matrix2mqtt/Cargo.toml | 9 + rust/matrix2mqtt/default.nix | 7 + rust/matrix2mqtt/package.nix | 21 + rust/matrix2mqtt/src/main.rs | 506 ++++++++++++++++++++ 10 files changed, 1258 insertions(+) create mode 100644 rust/default.nix create mode 100644 rust/matrix2mqtt/Cargo.lock create mode 100644 rust/matrix2mqtt/Cargo.toml create mode 100644 rust/matrix2mqtt/default.nix create mode 100644 rust/matrix2mqtt/package.nix create mode 100644 rust/matrix2mqtt/src/main.rs diff --git a/.hgignore b/.hgignore index 1bcd964b09..69c6b76b68 100644 --- a/.hgignore +++ b/.hgignore @@ -25,6 +25,7 @@ syntax: regexp ^go/trains/.*/start.sh$ ^go/trains/.*/lukegb-trains.json$ ^py/icalfilter/config/.*$ +^rust/.*/target/.*$ ^(.+/)?result(-([a-z]+|[0-9]+))?$ syntax: glob diff --git a/ci-root.nix b/ci-root.nix index 5b066be226..90252ff842 100644 --- a/ci-root.nix +++ b/ci-root.nix @@ -38,6 +38,7 @@ let }; factorio = depot.ops.factorio; home-manager = depot.ops.home-manager-ext.built; + rust = depot.rust; }; aarch64-linux = builtins.removeAttrs x86_64-linux [ "home-manager" "pkg-authentik" "web-barf" ] // { pkgs = builtins.removeAttrs x86_64-linux.pkgs [ "lutris" "plex-pass" "sheepshaver" "fr24feed" "javaws-env" "copybara" ]; diff --git a/default.nix b/default.nix index 2ca200c435..5479dd277e 100644 --- a/default.nix +++ b/default.nix @@ -22,6 +22,7 @@ in fix (self: web = import ./web ch; go = import ./go ch; py = import ./py ch; + rust = import ./rust ch; version = import ./version.nix ch; diff --git a/ops/nixos/totoro/home-assistant.nix b/ops/nixos/totoro/home-assistant.nix index 40944907c1..f5f6c6e813 100644 --- a/ops/nixos/totoro/home-assistant.nix +++ b/ops/nixos/totoro/home-assistant.nix @@ -39,6 +39,7 @@ in { homeassistant = { password = "homeassistant"; acl = [ + "readwrite matrix2mqtt/#" "readwrite zigbee2mqtt/#" "readwrite homeassistant/#" ]; diff --git a/rust/default.nix b/rust/default.nix new file mode 100644 index 0000000000..2e8e6b3b95 --- /dev/null +++ b/rust/default.nix @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2024 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +args: + +{ + matrix2mqtt = import ./matrix2mqtt args; +} diff --git a/rust/matrix2mqtt/Cargo.lock b/rust/matrix2mqtt/Cargo.lock new file mode 100644 index 0000000000..3d135c2454 --- /dev/null +++ b/rust/matrix2mqtt/Cargo.lock @@ -0,0 +1,702 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets", +] + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cc" +version = "1.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "flume" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matrix2mqtt" +version = "0.1.0" +dependencies = [ + "futures", + "rumqttc", + "tokio", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "proc-macro2" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rumqttc" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1568e15fab2d546f940ed3a21f48bbbd1c494c90c99c4481339364a497f94a9" +dependencies = [ + "bytes", + "flume", + "futures-util", + "log", + "rustls-native-certs", + "rustls-pemfile", + "rustls-webpki", + "thiserror", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/rust/matrix2mqtt/Cargo.toml b/rust/matrix2mqtt/Cargo.toml new file mode 100644 index 0000000000..2843f422de --- /dev/null +++ b/rust/matrix2mqtt/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "matrix2mqtt" +version = "0.1.0" +edition = "2021" + +[dependencies] +tokio = { version = "1", features = ["full"] } +rumqttc = "0.24.0" +futures = "0.3.31" \ No newline at end of file diff --git a/rust/matrix2mqtt/default.nix b/rust/matrix2mqtt/default.nix new file mode 100644 index 0000000000..d3a51db6c5 --- /dev/null +++ b/rust/matrix2mqtt/default.nix @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2024 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ depot, ... }: + +depot.pkgs.callPackage ./package.nix { } diff --git a/rust/matrix2mqtt/package.nix b/rust/matrix2mqtt/package.nix new file mode 100644 index 0000000000..5daecc9101 --- /dev/null +++ b/rust/matrix2mqtt/package.nix @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2024 Luke Granger-Brown +# +# SPDX-License-Identifier: Apache-2.0 + +{ lib, rustPlatform }: + +rustPlatform.buildRustPackage rec { + pname = "matrix2mqtt"; + version = "unstable"; + + src = lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.unions [ + ./Cargo.toml + ./Cargo.lock + ./src + ]; + }; + + cargoHash = "sha256-0R6hN5cNlj8+UzZ9oM4f74UGGCS1AKlDgHaQyKq/y0Q="; +} diff --git a/rust/matrix2mqtt/src/main.rs b/rust/matrix2mqtt/src/main.rs new file mode 100644 index 0000000000..1b69bee7d7 --- /dev/null +++ b/rust/matrix2mqtt/src/main.rs @@ -0,0 +1,506 @@ +use futures::future; +use futures::FutureExt; +use std::error::Error; +use std::fmt; +use std::str::FromStr; +use std::time::Duration; +use tokio::io::AsyncBufReadExt; +use tokio::io::AsyncRead; +use tokio::io::AsyncWriteExt; +use tokio::io::BufReader; +use tokio::net::TcpStream; +use tokio::select; +use tokio::sync::broadcast; +use tokio::sync::mpsc; +use tokio::task::JoinSet; + +async fn wait_for_line( + reader: &mut BufReader, + want: &str, +) -> Result<(), Box> { + loop { + let mut line = String::new(); + reader.read_line(&mut line).await?; + if line == want { + return Ok(()); + } + continue; + } +} + +#[derive(Debug)] +enum MatrixCommand { + GetCurrentMappings, + SetOutput(MatrixDestination, MatrixSource), + SetOutputCEC(MatrixDestination, bool), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum MatrixSource { + HDMI1, + HDMI2, + HDMI3, + HDMI4, +} + +impl MatrixSource { + fn matrix_name(self: &MatrixSource) -> &'static str { + match self { + MatrixSource::HDMI1 => "hdmiin1", + MatrixSource::HDMI2 => "hdmiin2", + MatrixSource::HDMI3 => "hdmiin3", + MatrixSource::HDMI4 => "hdmiin4", + } + } + + fn friendly_name(self: &MatrixSource) -> &'static str { + match self { + MatrixSource::HDMI1 => "SHIELD", + MatrixSource::HDMI2 => "totoro", + MatrixSource::HDMI3 => "PS5", + MatrixSource::HDMI4 => "Mac Mini", + } + } +} + +#[derive(Debug, PartialEq, Eq)] +struct ParseMatrixPortError; + +impl fmt::Display for ParseMatrixPortError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "ParseMatrixPortError") + } +} + +impl Error for ParseMatrixPortError {} + +#[derive(Debug, PartialEq, Eq)] +struct UnexpectedMatrixDestinationError; + +impl fmt::Display for UnexpectedMatrixDestinationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "UnexpectedMatrixDestinationError") + } +} + +impl Error for UnexpectedMatrixDestinationError {} + +impl FromStr for MatrixSource { + type Err = ParseMatrixPortError; + + fn from_str(s: &str) -> Result { + match s { + "hdmiin1" => Ok(MatrixSource::HDMI1), + "hdmiin2" => Ok(MatrixSource::HDMI2), + "hdmiin3" => Ok(MatrixSource::HDMI3), + "hdmiin4" => Ok(MatrixSource::HDMI4), + _ => Err(ParseMatrixPortError), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum MatrixDestination { + HDMI1, + HDMI2, + HDMI3, + HDMI4, +} + +impl MatrixDestination { + fn matrix_name(self: &MatrixDestination) -> &'static str { + match self { + MatrixDestination::HDMI1 => "hdmiout1", + MatrixDestination::HDMI2 => "hdmiout2", + MatrixDestination::HDMI3 => "hdmiout3", + MatrixDestination::HDMI4 => "hdmiout4", + } + } + + fn index(self: &MatrixDestination) -> u8 { + match self { + MatrixDestination::HDMI1 => 1, + MatrixDestination::HDMI2 => 2, + MatrixDestination::HDMI3 => 3, + MatrixDestination::HDMI4 => 4, + } + } + + fn friendly_name(self: &MatrixDestination) -> &'static str { + match self { + MatrixDestination::HDMI1 => "Projector", + MatrixDestination::HDMI2 => "Sound Bar", + MatrixDestination::HDMI3 => "Desk Monitor", + MatrixDestination::HDMI4 => "unused", + } + } +} + +impl FromStr for MatrixDestination { + type Err = ParseMatrixPortError; + + fn from_str(s: &str) -> Result { + match s { + "hdmiout1" => Ok(MatrixDestination::HDMI1), + "hdmiout2" => Ok(MatrixDestination::HDMI2), + "hdmiout3" => Ok(MatrixDestination::HDMI3), + "hdmiout4" => Ok(MatrixDestination::HDMI4), + _ => Err(ParseMatrixPortError), + } + } +} + +#[derive(Clone, Debug)] +struct MatrixMappings { + hdmiout1: MatrixSource, + hdmiout2: MatrixSource, + hdmiout3: MatrixSource, + hdmiout4: MatrixSource, +} + +#[derive(Clone, Debug)] +enum MatrixResponse { + CurrentMappings(MatrixMappings), +} + +async fn read_single_mapping( + reader: &mut BufReader, + expected: MatrixDestination, +) -> Result> { + let mut line = String::new(); + reader.read_line(&mut line).await?; + let splitter = line + .strip_prefix("MP ") + .and_then(|s| Some(s.split_whitespace())) + .ok_or(ParseMatrixPortError)? + .collect::>(); + if splitter.len() != 2 { + return Err(Box::new(ParseMatrixPortError)); + } + + let src = MatrixSource::from_str(splitter[0])?; + let dst = MatrixDestination::from_str(splitter[1])?; + if dst != expected { + return Err(Box::new(UnexpectedMatrixDestinationError)); + } + + Ok(src) +} + +async fn query_mappings( + reader: &mut BufReader, + response_tx: &mut broadcast::Sender, +) -> Result<(), Box> { + reader.write_all(b"GET MP ALL\r\n").await?; + + let mappings = MatrixMappings { + hdmiout1: read_single_mapping(reader, MatrixDestination::HDMI1).await?, + hdmiout2: read_single_mapping(reader, MatrixDestination::HDMI2).await?, + hdmiout3: read_single_mapping(reader, MatrixDestination::HDMI3).await?, + hdmiout4: read_single_mapping(reader, MatrixDestination::HDMI4).await?, + }; + response_tx.send(MatrixResponse::CurrentMappings(mappings))?; + + Ok(()) +} + +async fn handle_matrix_telnet( + socket: TcpStream, + command_rx: &mut mpsc::Receiver, + response_tx: &mut broadcast::Sender, +) -> Result<(), Box> { + let mut stream = BufReader::new(socket); + + // Wait for the hello. + println!("awaiting hello"); + wait_for_line(&mut stream, "Welcome to the 4KMX44-H2 Matrix!\r\n").await?; + println!("matrix ready!"); + + while let Some(msg) = command_rx.recv().await { + println!("got command {:#?}", msg); + match msg { + MatrixCommand::GetCurrentMappings => { + query_mappings(&mut stream, response_tx).await?; + } + MatrixCommand::SetOutput(destination, source) => { + stream + .write_all( + format!( + "SET SW {} {}\r\n", + source.matrix_name(), + destination.matrix_name() + ) + .as_bytes(), + ) + .await?; + let expect = format!( + "SW {} {}\r\n", + source.matrix_name(), + destination.matrix_name() + ); + wait_for_line(&mut stream, &expect).await?; + query_mappings(&mut stream, response_tx).await?; + } + MatrixCommand::SetOutputCEC(destination, on_or_off) => { + let on_or_off_str = if on_or_off { "ON" } else { "OFF" }; + stream + .write_all( + format!( + "SET CEC_PWR {} {}\r\n", + destination.matrix_name(), + on_or_off_str, + ) + .as_bytes(), + ) + .await?; + let expect = format!( + "CEC_PWR {} {}\r\n", + destination.matrix_name(), + on_or_off_str, + ); + wait_for_line(&mut stream, &expect).await?; + } + } + } + + println!("process terminating..."); + + Ok(()) +} + +const TOPIC_PREFIX: &str = "matrix2mqtt/lukes-bedroom"; + +async fn handle_publish( + p: &rumqttc::Publish, + matrix_command_tx: &mut mpsc::Sender, +) -> Result<(), Box> { + if let Some(destination_str) = p + .topic + .strip_prefix(TOPIC_PREFIX) + .and_then(|topic| topic.strip_prefix("/")) + .and_then(|topic| topic.strip_suffix("/cec/set")) + { + let destination = MatrixDestination::from_str(destination_str)?; + match std::str::from_utf8(&p.payload)? { + "ON" => { + println!("asked to turn {:#?} on", destination); + matrix_command_tx + .send(MatrixCommand::SetOutputCEC(destination, true)) + .await?; + }, + "OFF" => { + println!("asked to turn {:#?} off", destination); + matrix_command_tx + .send(MatrixCommand::SetOutputCEC(destination, false)) + .await?; + }, + s => { + println!("asked to set {:#?} to unknown value {}", destination, s) + }, + } + } else if let Some(destination_str) = p + .topic + .strip_prefix(TOPIC_PREFIX) + .and_then(|topic| topic.strip_prefix("/")) + .and_then(|topic| topic.strip_suffix("/set")) + { + let destination = MatrixDestination::from_str(destination_str)?; + let source_str = std::str::from_utf8(&p.payload)?; + let source = MatrixSource::from_str(source_str)?; + println!("asked to set {:#?} to {:#?}", destination, source); + matrix_command_tx + .send(MatrixCommand::SetOutput(destination, source)) + .await?; + } + Ok(()) +} + +async fn publish_config( + client: &rumqttc::AsyncClient, + destination: MatrixDestination, +) -> Result<(), Box> { + client + .publish( + format!( + "homeassistant/select/matrix_{}/config", + destination.matrix_name() + ), + rumqttc::QoS::AtLeastOnce, + true, + format!( + r#"{{ + "name": "HDMI {index}: {friendly_name}", + "device": {{ + "identifiers": ["192.168.1.200"], + "name": "HDMI Matrix" + }}, + "device_class": "select", + "state_topic": "{topic_prefix}/{matrix_name}/get", + "command_topic": "{topic_prefix}/{matrix_name}/set", + "icon": "mdi:hdmi-port", + "entity_category": "config", + "unique_id": "{topic_prefix}/{matrix_name}", + "retain": false, + "options": [ + "{option1_friendly}", + "{option2_friendly}", + "{option3_friendly}", + "{option4_friendly}" + ], + "value_template": "{{% set lookup = {{'hdmiin1': '{option1_friendly}', 'hdmiin2': '{option2_friendly}', 'hdmiin3': '{option3_friendly}', 'hdmiin4': '{option4_friendly}'}} %}}{{{{ lookup[value] }}}}", + "command_template": "{{% set lookup = {{'{option1_friendly}': 'hdmiin1', '{option2_friendly}': 'hdmiin2', '{option3_friendly}': 'hdmiin3', '{option4_friendly}': 'hdmiin4'}} %}}{{{{ lookup[value] }}}}" +}}"#, + index = destination.index(), + friendly_name = destination.friendly_name(), + topic_prefix = TOPIC_PREFIX, + matrix_name = destination.matrix_name(), + option1_friendly = MatrixSource::HDMI1.friendly_name(), + option2_friendly = MatrixSource::HDMI2.friendly_name(), + option3_friendly = MatrixSource::HDMI3.friendly_name(), + option4_friendly = MatrixSource::HDMI4.friendly_name(), + ) + .as_bytes(), + ) + .await?; + + client + .publish( + format!( + "homeassistant/switch/matrix_{}_cec/config", + destination.matrix_name() + ), + rumqttc::QoS::AtLeastOnce, + true, + format!( + r#"{{ + "name": "HDMI {index}: {friendly_name} - CEC Power", + "device": {{ + "identifiers": ["192.168.1.200"], + "name": "HDMI Matrix" + }}, + "device_class": "switch", + "command_topic": "{topic_prefix}/{matrix_name}/cec/set", + "state_topic": "{topic_prefix}/{matrix_name}/cec/get", + "unique_id": "{topic_prefix}/{matrix_name}/cec", + "retain": false +}}"#, + index = destination.index(), + friendly_name = destination.friendly_name(), + topic_prefix = TOPIC_PREFIX, + matrix_name = destination.matrix_name(), + ) + .as_bytes(), + ) + .await?; + Ok(()) +} + +async fn select_all(futures: Vec) -> Result<(), E> +where + I: futures::Future> + Unpin, +{ + let mut fvec = futures; + while fvec.len() > 0 { + let (item_resolved, _, remaining_futures) = future::select_all(fvec).await; + item_resolved?; + fvec = remaining_futures; + } + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let mut set = JoinSet::new(); + + let (matrix_command_tx, mut matrix_command_rx) = mpsc::channel(32); + let (mut matrix_response_tx, mut matrix_response_rx) = broadcast::channel(32); + + let mut mqtt_matrix_command_tx = matrix_command_tx.clone(); + set.spawn(async move { + let mut mqttoptions = rumqttc::MqttOptions::new("matrix2mqtt", "localhost", 1883); + mqttoptions.set_keep_alive(Duration::from_secs(5)); + mqttoptions.set_credentials("matrix2mqtt", "matrix2mqtt"); + + let (client, mut connection) = rumqttc::AsyncClient::new(mqttoptions, 10); + + select_all(vec![ + client.subscribe(format!("{}/+/set", TOPIC_PREFIX), rumqttc::QoS::AtLeastOnce).boxed(), + client.subscribe(format!("{}/+/cec/set", TOPIC_PREFIX), rumqttc::QoS::AtLeastOnce).boxed(), + ]).await?; + + // Publish Home Assistant discovery messages. + println!("publishing Home Assistant discovery messages"); + select_all(vec![ + publish_config(&client, MatrixDestination::HDMI1).boxed(), + publish_config(&client, MatrixDestination::HDMI2).boxed(), + publish_config(&client, MatrixDestination::HDMI3).boxed(), + publish_config(&client, MatrixDestination::HDMI4).boxed(), + ]).await?; + println!("done with HA startup!"); + + loop { + select! { + notification = connection.poll() => { + match notification? { + rumqttc::Event::Incoming(rumqttc::Incoming::Publish(p)) => { + match handle_publish(&p, &mut mqtt_matrix_command_tx).await { + Ok(_) => {}, + Err(e) => println!("failed to handle event {:#?}: {}", p, e), + } + }, + _ => {}, + } + }, + Ok(matrix_response) = matrix_response_rx.recv() => { + match matrix_response { + MatrixResponse::CurrentMappings(mappings) => { + println!("publishing mappings {:#?} to MQTT", mappings); + select_all(vec![ + client.publish(format!("{}/hdmiout1/get", TOPIC_PREFIX), rumqttc::QoS::AtLeastOnce, true, mappings.hdmiout1.matrix_name()).boxed(), + client.publish(format!("{}/hdmiout2/get", TOPIC_PREFIX), rumqttc::QoS::AtLeastOnce, true, mappings.hdmiout2.matrix_name()).boxed(), + client.publish(format!("{}/hdmiout3/get", TOPIC_PREFIX), rumqttc::QoS::AtLeastOnce, true, mappings.hdmiout3.matrix_name()).boxed(), + client.publish(format!("{}/hdmiout4/get", TOPIC_PREFIX), rumqttc::QoS::AtLeastOnce, true, mappings.hdmiout4.matrix_name()).boxed(), + ]).await?; + println!("published mappings to MQTT"); + }, + } + }, + } + } + }); + + set.spawn(async move { + let matrix_socket = TcpStream::connect("192.168.1.200:23").await?; + handle_matrix_telnet( + matrix_socket, + &mut matrix_command_rx, + &mut matrix_response_tx, + ) + .await + }); + + let ticker_matrix_command_tx = matrix_command_tx.clone(); + set.spawn(async move { + let mut interval = tokio::time::interval(Duration::from_secs(10)); + + loop { + interval.tick().await; + ticker_matrix_command_tx + .send(MatrixCommand::GetCurrentMappings) + .await?; + } + }); + + // Seed MQTT with the current mapping state. + matrix_command_tx + .send(MatrixCommand::GetCurrentMappings) + .await + .unwrap(); + + // Bail as soon as we get a failure. + while let Some(result) = set.join_next().await { + let _ = result?; + } + + Ok(()) +}