Initial commit

master
Dustin 2022-05-22 13:06:41 -05:00
commit 6e89c60a19
11 changed files with 1349 additions and 0 deletions

1
.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
Cargo.lock -diff

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/config.toml

674
Cargo.lock generated Normal file
View File

@ -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",
]

27
Cargo.toml Normal file
View File

@ -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"]

1
rustfmt.toml Normal file
View File

@ -0,0 +1 @@
max_width = 79

139
src/config.rs Normal file
View File

@ -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<io::Error> for ConfigError {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
impl From<figment::Error> 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<String>,
#[serde(default)]
pub password: Option<String>,
}
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<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
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()
}

161
src/dpms.rs Normal file
View File

@ -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<Self, OpenDisplayError> {
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<u16> 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
}

30
src/error.rs Normal file
View File

@ -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<serde_json::Error> for MqttDpmsError {
fn from(e: serde_json::Error) -> Self {
Self::Json(e)
}
}
impl From<mqtt::Error> 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),
}
}
}

58
src/main.rs Normal file
View File

@ -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
}

246
src/mqttdpms.rs Normal file
View File

@ -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<String>,
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
}

10
src/util.rs Normal file
View File

@ -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()
}