commit 6e89c60a1998319bab69f4a06344e572c9c32030 Author: Dustin C. Hatch Date: Sun May 22 13:06:41 2022 -0500 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..383338e --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +Cargo.lock -diff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fabfb87 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/config.toml diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4f73c40 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,674 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "argh" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb41d85d92dfab96cb95ab023c265c5e4261bb956c0fb49ca06d90c570f1958" +dependencies = [ + "argh_derive", + "argh_shared", +] + +[[package]] +name = "argh_derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be69f70ef5497dd6ab331a50bd95c6ac6b8f7f17a7967838332743fbd58dc3b5" +dependencies = [ + "argh_shared", + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "argh_shared" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6f8c380fa28aa1b36107cd97f0196474bb7241bb95a453c5c01a15ac74b2eac" + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "atomic" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b88d82667eca772c4aa12f0f1348b3ae643424c8876448f3f7bd5787032e234c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "cmake" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb6210b637171dfba4cda12e579ac6dc73f5165ad56133e5d72ef3131f320855" +dependencies = [ + "cc", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if", + "lazy_static", +] + +[[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-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + +[[package]] +name = "figment" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790b4292c72618abbab50f787a477014fe15634f96291de45672ce46afe122df" +dependencies = [ + "atomic", + "serde", + "toml", + "uncased", + "version_check", +] + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mqttdpms" +version = "0.1.0" +dependencies = [ + "argh", + "dirs", + "env_logger", + "figment", + "futures", + "futures-timer", + "hostname", + "log", + "paho-mqtt", + "serde", + "serde_json", + "x11", +] + +[[package]] +name = "openssl-sys" +version = "0.9.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5fd19fb3e0a8191c1e34935718976a3e70c112ab9a24af6d7cadccd9d90bc0" +dependencies = [ + "autocfg", + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "paho-mqtt" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fac58bae33ba9679bb4908ffa7c3950114345860d3f9b98340c4943f18ff324" +dependencies = [ + "async-channel", + "crossbeam-channel", + "futures", + "futures-timer", + "libc", + "log", + "paho-mqtt-sys", + "thiserror", +] + +[[package]] +name = "paho-mqtt-sys" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10e6244f27644eed5709e318a3ad7f785906fbb6030f0a9b9ba50923b456c0c5" +dependencies = [ + "cmake", + "openssl-sys", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" + +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "serde" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "slab" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" + +[[package]] +name = "syn" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbaf6116ab8924f39d52792136fb74fd60a80194cf1b1c6ffa6453eef1c3f942" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +dependencies = [ + "serde", +] + +[[package]] +name = "uncased" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "x11" +version = "2.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd0565fa8bfba8c5efe02725b14dff114c866724eff2cfd44d76cea74bcd87a" +dependencies = [ + "libc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..89ea093 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "mqttdpms" +version = "0.1.0" +edition = "2021" + +[dependencies] +argh = "^0.1" +dirs = "^4.0" +env_logger = "^0.9.0" +futures = "^0.3.21" +futures-timer = "^3.0.2" +hostname = "^0.3.1" +log = "^0.4.17" +paho-mqtt = "^0.11.1" +serde_json = "^1.0" + +[dependencies.figment] +version = "^0.10.6" +features = ["toml"] + +[dependencies.serde] +version = "^1.0" +features = ["derive"] + +[dependencies.x11] +version = "^2.19.1" +features = ["dpms", "xlib"] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..a1ffd27 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 79 diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..a3a9743 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,139 @@ +use std::fmt; +use std::io; +use std::path::Path; + +use figment::providers::Format; +use figment::Figment; +use serde::Deserialize; + +use crate::util; + +const TLS_CA_BUNDLE: &str = + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"; + +#[derive(Debug)] +pub enum ConfigError { + Io(io::Error), + Invalid(figment::Error), +} + +impl fmt::Display for ConfigError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Io(e) => write!(f, "{}", e), + Self::Invalid(e) => write!(f, "{}", e), + } + } +} + +impl From for ConfigError { + fn from(error: io::Error) -> Self { + Self::Io(error) + } +} + +impl From for ConfigError { + fn from(error: figment::Error) -> Self { + Self::Invalid(error) + } +} + +#[derive(Debug, Deserialize)] +pub struct MqttTopicConfig { + pub availability: String, + pub config: String, + pub state: String, + pub command: String, +} + +impl Default for MqttTopicConfig { + fn default() -> Self { + Self { + availability: default_availability_topic(), + config: default_config_topic(), + state: default_state_topic(), + command: default_command_topic(), + } + } +} + +#[derive(Debug, Deserialize)] +pub struct MqttConfiguration { + #[serde(default = "default_mqtt_host")] + pub host: String, + #[serde(default = "default_mqtt_port")] + pub port: u16, + #[serde(default)] + pub tls: bool, + #[serde(default = "default_mqtt_ca_file")] + pub ca_file: String, + #[serde(default)] + pub username: Option, + #[serde(default)] + pub password: Option, +} + +impl Default for MqttConfiguration { + fn default() -> Self { + Self { + host: default_mqtt_host(), + port: default_mqtt_port(), + tls: false, + ca_file: default_mqtt_ca_file(), + username: None, + password: None, + } + } +} + +#[derive(Debug, Deserialize)] +pub struct Configuration { + #[serde(default = "default_unique_id")] + pub name: String, + #[serde(default = "default_unique_id")] + pub unique_id: String, + #[serde(default)] + pub mqtt: MqttConfiguration, + #[serde(default)] + pub topics: MqttTopicConfig, +} + +impl Configuration { + pub fn load>(path: P) -> Result { + let figment = Figment::new() + .merge(figment::providers::Toml::file(path.as_ref())); + Ok(figment.extract()?) + } +} + +fn default_unique_id() -> String { + util::hostname() +} + +fn default_mqtt_host() -> String { + "localhost".into() +} + +fn default_mqtt_port() -> u16 { + 1883 +} + +fn default_mqtt_ca_file() -> String { + TLS_CA_BUNDLE.into() +} + +fn default_availability_topic() -> String { + "mqttdpms/@UNIQUEID@/available".into() +} + +fn default_config_topic() -> String { + "homeassistant/switch/dpms_@UNIQUEID@/config".into() +} + +fn default_state_topic() -> String { + "mqttdpms/@UNIQUEID@/state".into() +} + +fn default_command_topic() -> String { + "mqttdpms/@UNIQUEID@/command".into() +} diff --git a/src/dpms.rs b/src/dpms.rs new file mode 100644 index 0000000..bf3bbca --- /dev/null +++ b/src/dpms.rs @@ -0,0 +1,161 @@ +#![allow(dead_code)] +use std::ffi::CStr; + +use x11::dpms::{ + DPMSCapable, DPMSDisable, DPMSEnable, DPMSForceLevel, DPMSGetTimeouts, + DPMSInfo, DPMSQueryExtension, +}; +use x11::dpms::{DPMSModeOff, DPMSModeOn, DPMSModeStandby, DPMSModeSuspend}; +use x11::xlib::{XCloseDisplay, XDisplayName, XOpenDisplay, _XDisplay}; +use x11::xmd::{BOOL, CARD16}; + +/// Error returned if connecting to the X server fails +#[derive(Debug)] +pub struct OpenDisplayError; + +/// Wrapper for an X display pointer +pub struct Display { + display: *mut _XDisplay, +} + +impl Display { + /// Open a connection to the X server + /// + /// If the connection succeeds, a [`Display`] is returned. Otherwise, an + /// [`OpenDisplayError`] is returned. + pub fn open() -> Result { + let display = unsafe { XOpenDisplay(std::ptr::null()) }; + if display.is_null() { + Err(OpenDisplayError) + } else { + Ok(Self { display }) + } + } + + /// Return the name of the X server display + /// + /// If the display name cannot be determined, an empty string is returned. + pub fn name() -> String { + let name = unsafe { CStr::from_ptr(XDisplayName(std::ptr::null())) }; + let name = name.to_str().unwrap_or(""); + String::from(name) + } +} + +impl Drop for Display { + fn drop(&mut self) { + unsafe { XCloseDisplay(self.display) }; + } +} + +/// DPMS Power Level +/// +/// There are four power levels specified by the Video Electronics Standards +/// Association (VESA) Display Power Management Signaling (DPMS) standard. +/// These are mapped onto the X DPMS Extension +#[derive(PartialEq)] +pub enum DpmsPowerLevel { + /// In use + On = DPMSModeOn as isize, + /// Blanked, low power + Standby = DPMSModeStandby as isize, + /// Blanked, lower power + Suspend = DPMSModeSuspend as isize, + /// Shut off, awaiting activity + Off = DPMSModeOff as isize, + Unknown = -1, +} + +impl From for DpmsPowerLevel { + fn from(v: u16) -> Self { + #[allow(non_snake_case)] + match v { + x if x == DpmsPowerLevel::On as u16 => Self::On, + x if x == DpmsPowerLevel::Standby as u16 => Self::Standby, + x if x == DpmsPowerLevel::Suspend as u16 => Self::Suspend, + x if x == DpmsPowerLevel::Off as u16 => Self::Off, + _ => Self::Unknown, + } + } +} + +/// Result from [`get_info`] function (`DPMSInfo`) +pub struct DpmsInfo { + /// Current power level + pub power_level: DpmsPowerLevel, + /// DPMS enabled/disabled state + pub state: bool, +} + +/// Result from [`get_timeouts`] function (`DPMSGetTimeouts`) +pub struct DpmsTimeouts { + /// Amount of time of inactivity in seconds before standby mode is invoked + pub standby: u16, + /// Amount of time of inactivity in seconds before the second level of power + /// savings is invoked + pub suspend: u16, + /// Amount of time of inactivity in seconds before the third and final level + /// of power savings is invoked + pub off: u16, +} + +/// Queries the X server to determine the availability of the DPMS Extension +pub fn query_extension(display: &Display) -> bool { + let mut event_base = 0; + let mut error_base = 0; + let r = unsafe { + DPMSQueryExtension(display.display, &mut event_base, &mut error_base) + }; + r != 0 +} + +/// Returns the DPMS capability of the X server, either TRUE (capable of DPMS) +/// or FALSE (incapable of DPMS) +pub fn dpms_capable(display: &Display) -> bool { + let r = unsafe { DPMSCapable(display.display) }; + r != 0 +} + +/// Returns information about the current DPMS state +pub fn get_info(display: &Display) -> DpmsInfo { + let mut power_level: CARD16 = 0; + let mut state: BOOL = 0; + unsafe { DPMSInfo(display.display, &mut power_level, &mut state) }; + DpmsInfo { + power_level: power_level.into(), + state: state != 0, + } +} + +/// Retrieves the timeout values used by the X server for DPMS timings +pub fn get_timeouts(display: &Display) -> DpmsTimeouts { + let mut standby: CARD16 = 0; + let mut suspend: CARD16 = 0; + let mut off: CARD16 = 0; + unsafe { + DPMSGetTimeouts(display.display, &mut standby, &mut suspend, &mut off) + }; + DpmsTimeouts { + standby, + suspend, + off, + } +} + +/// Forces a DPMS capable display into the specified power level +pub fn force_level(display: &Display, level: DpmsPowerLevel) -> bool { + let r = unsafe { DPMSForceLevel(display.display, level as u16) }; + r != 0 +} + +/// Enables DPMS on the specified display +pub fn enable(display: &Display) -> bool { + let r = unsafe { DPMSEnable(display.display) }; + r != 0 +} + +/// Disables DPMS on the specified display +pub fn disable(display: &Display) -> bool { + let r = unsafe { DPMSDisable(display.display) }; + r != 0 +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..7ea019b --- /dev/null +++ b/src/error.rs @@ -0,0 +1,30 @@ +use std::fmt; + +use paho_mqtt as mqtt; + +#[derive(Debug)] +pub enum MqttDpmsError { + Mqtt(mqtt::Error), + Json(serde_json::Error), +} + +impl From for MqttDpmsError { + fn from(e: serde_json::Error) -> Self { + Self::Json(e) + } +} + +impl From for MqttDpmsError { + fn from(e: mqtt::Error) -> Self { + Self::Mqtt(e) + } +} + +impl fmt::Display for MqttDpmsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Mqtt(e) => write!(f, "MQTT error: {}", e), + Self::Json(e) => write!(f, "JSON de/serialization error: {}", e), + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2ad5ee7 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,58 @@ +mod config; +mod dpms; +mod error; +mod mqttdpms; +mod util; + +use std::path::PathBuf; + +use argh::FromArgs; +use env_logger; +use futures::executor::block_on; +use log::{debug, error}; + +#[derive(FromArgs)] +#[argh(description = "MQTT DPMS")] +struct Arguments { + /// path to configuration + #[argh(option, default = "default_config_path()")] + config: PathBuf, +} + +fn main() { + let args: Arguments = argh::from_env(); + env_logger::init(); + let config = match config::Configuration::load(args.config) { + Ok(c) => c, + Err(e) => { + eprintln!("Failed to load configuration file: {}", e); + std::process::exit(1); + } + }; + debug!("Configuration: {:?}", config); + + let mqttdpms = mqttdpms::MqttDpms::new(config); + + if let Err(e) = block_on(async move { mqttdpms.run().await }) { + eprintln!("{}", e); + std::process::exit(1); + } +} + +fn default_config_path() -> PathBuf { + let mut path = match dirs::config_dir() { + Some(mut d) => { + d.push(env!("CARGO_PKG_NAME")); + d + } + None => match std::env::current_dir() { + Ok(d) => d, + Err(e) => { + error!("Could not get current working directory: {}", e); + "/".into() + } + }, + }; + path.push("config.toml"); + path +} diff --git a/src/mqttdpms.rs b/src/mqttdpms.rs new file mode 100644 index 0000000..c4615ba --- /dev/null +++ b/src/mqttdpms.rs @@ -0,0 +1,246 @@ +use futures::stream::StreamExt; +use log::{debug, error, info, warn}; +use paho_mqtt as mqtt; +use serde::Serialize; + +use crate::config::Configuration; +use crate::dpms; +use crate::error::MqttDpmsError; +use crate::util; + +#[derive(Serialize)] +struct HassDevice { + identifiers: Vec, + manufacturer: String, + model: String, + name: String, + sw_version: String, +} + +impl Default for HassDevice { + fn default() -> Self { + Self { + identifiers: vec![util::hostname()], + manufacturer: "Dustin C. Hatch".into(), + model: env!("CARGO_PKG_VERSION").into(), + name: "MQTT DPMS".into(), + sw_version: env!("CARGO_PKG_VERSION").into(), + } + } +} + +#[derive(Serialize)] +struct HassConfig { + availability_topic: String, + command_topic: String, + device: HassDevice, + name: String, + state_topic: String, + unique_id: String, + icon: String, +} + +impl HassConfig { + pub fn from_config(config: &Configuration) -> Self { + let unique_id = &config.unique_id; + let availability_topic = + config.topics.availability.replace("@UNIQUEID@", &unique_id); + let command_topic = + config.topics.command.replace("@UNIQUEID@", &unique_id); + let state_topic = + config.topics.state.replace("@UNIQUEID@", &unique_id); + Self { + availability_topic, + command_topic, + device: Default::default(), + icon: "mdi:monitor".into(), + name: config.name.clone(), + state_topic, + unique_id: format!("switch.dpms_{}", unique_id), + } + } +} + +pub struct MqttDpms { + config: Configuration, +} + +impl MqttDpms { + pub fn new(config: Configuration) -> Self { + Self { config } + } + + pub async fn run(self) -> Result<(), MqttDpmsError> { + let uri = format!( + "{}://{}:{}", + if self.config.mqtt.tls { "ssl" } else { "tcp" }, + self.config.mqtt.host, + self.config.mqtt.port + ); + info!("Connecting to MQTT server {}", uri); + let client_opts = + mqtt::CreateOptionsBuilder::new().server_uri(uri).finalize(); + let mut client = mqtt::AsyncClient::new(client_opts)?; + let mut conn_opts = mqtt::ConnectOptionsBuilder::new(); + conn_opts.will_message(self.will_message()); + if self.config.mqtt.tls { + let ssl_opts = mqtt::SslOptionsBuilder::new() + .trust_store(&self.config.mqtt.ca_file)? + .finalize(); + conn_opts.ssl_options(ssl_opts); + } + if let [Some(username), Some(password)] = + [&self.config.mqtt.username, &self.config.mqtt.password] + { + conn_opts.user_name(username).password(password); + } + + let mut stream = client.get_stream(10); + client.connect(conn_opts.finalize()).await?; + info!("Successfully connected to MQTT broker"); + + let cmd_topic = self + .config + .topics + .command + .replace("@UNIQUEID@", &self.config.unique_id); + info!("Subscribing to command topic {}", cmd_topic); + client.subscribe(cmd_topic, 0).await?; + self.publish_config(&client).await?; + self.publish_online(&client).await?; + self.publish_state(&client).await?; + + while let Some(msg) = stream.next().await { + if let Some(msg) = msg { + debug!("Got message {:?}", msg); + match msg.payload_str().as_ref() { + "ON" => { + self.turn_on(); + self.publish_state(&client).await?; + } + "OFF" => { + self.turn_off(); + self.publish_state(&client).await?; + } + other => { + warn!("Unexpected command message: {}", other); + } + } + } + } + + Ok(()) + } + + async fn publish_config( + &self, + client: &mqtt::AsyncClient, + ) -> Result<(), MqttDpmsError> { + let topic = self + .config + .topics + .config + .replace("@UNIQUEID@", &self.config.unique_id); + let config = HassConfig::from_config(&self.config); + let payload = serde_json::to_string(&config)?; + let message = mqtt::Message::new_retained(topic, payload, 0); + Ok(client.publish(message).await?) + } + + async fn publish_state( + &self, + client: &mqtt::AsyncClient, + ) -> Result<(), MqttDpmsError> { + let topic = self + .config + .topics + .state + .replace("@UNIQUEID@", &self.config.unique_id); + let state = if is_screen_on() { "ON" } else { "OFF" }; + let message = mqtt::Message::new_retained(topic, state, 0); + Ok(client.publish(message).await?) + } + + async fn publish_online( + &self, + client: &mqtt::AsyncClient, + ) -> Result<(), MqttDpmsError> { + let topic = self + .config + .topics + .availability + .replace("@UNIQUEID@", &self.config.unique_id); + let message = mqtt::Message::new(topic, "online", 0); + Ok(client.publish(message).await?) + } + + fn turn_off(&self) { + info!("Turning off display"); + let display = match dpms::Display::open() { + Ok(d) => d, + Err(_) => { + error!("unable to open display \"{}\"", dpms::Display::name()); + return; + } + }; + if !dpms::enable(&display) { + error!( + "Failed to enable DPMS on display \"{}\"", + dpms::Display::name() + ); + } + if !dpms::force_level(&display, dpms::DpmsPowerLevel::Off) { + error!("Failed to turn off display \"{}\"", dpms::Display::name()); + } + } + + fn turn_on(&self) { + info!("Turning on display"); + let display = match dpms::Display::open() { + Ok(d) => d, + Err(_) => { + error!("unable to open display \"{}\"", dpms::Display::name()); + return; + } + }; + if !dpms::force_level(&display, dpms::DpmsPowerLevel::On) { + error!("Failed to turn on display \"{}\"", dpms::Display::name()); + } + if !dpms::disable(&display) { + error!( + "Failed to disable DPMS on display \"{}\"", + dpms::Display::name() + ); + } + } + + fn will_message(&self) -> mqtt::Message { + let avail_topic = self + .config + .topics + .availability + .replace("@UNIQUEID@", &self.config.unique_id); + mqtt::Message::new_retained(avail_topic, "offline", 0) + } +} + +fn is_screen_on() -> bool { + let display = match dpms::Display::open() { + Ok(d) => d, + Err(_) => { + error!("unable to open display \"{}\"", dpms::Display::name()); + return false; + } + }; + if dpms::query_extension(&display) { + if dpms::dpms_capable(&display) { + let info = dpms::get_info(&display); + if info.state { + if info.power_level != dpms::DpmsPowerLevel::On { + return false; + } + } + } + } + true +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..575544b --- /dev/null +++ b/src/util.rs @@ -0,0 +1,10 @@ +pub fn hostname() -> String { + if let Ok(h) = hostname::get() { + if let Some(h) = h.to_str() { + if let Some((h, _)) = h.split_once('.') { + return h.into(); + }; + }; + }; + "localhost".into() +}