From a780ad3b3357137b812c72972f360c77521ca97a Mon Sep 17 00:00:00 2001 From: Joakim Hulthe Date: Tue, 5 Jul 2022 14:18:52 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 + Cargo.lock | 630 ++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 17 ++ example.config.toml | 8 + src/bulb.rs | 120 +++++++++ src/lib.rs | 6 + src/main.rs | 114 ++++++++ src/manager.rs | 303 +++++++++++++++++++++ src/mqtt_conf.rs | 43 +++ 9 files changed, 1243 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 example.config.toml create mode 100644 src/bulb.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/manager.rs create mode 100644 src/mqtt_conf.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a3b37d --- /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..def25e1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,630 @@ +# 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 = "anyhow" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" + +[[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 = "bounded-integer" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1cf2ba51439a5e5108672f8004c29924ec5999f79070d59ee54e0c7b7f9948" +dependencies = [ + "bounded-integer-macro", +] + +[[package]] +name = "bounded-integer-macro" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44fed06dcd354528d1ce96b9396c5ea3b744232357aee14be3f1002335d0412" +dependencies = [ + "num-bigint", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clap" +version = "3.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[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 = "lighter" +version = "0.1.0" +dependencies = [ + "anyhow", + "bounded-integer", + "clap", + "log", + "mqtt-protocol", + "pretty_env_logger", + "serde", + "serde_json", + "tokio", + "toml", + "uuid", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "mio" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys", +] + +[[package]] +name = "mqtt-protocol" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca0b17380dc69fbcf5f967828cfd10e55028ba83a57da1f580c5b0792ab807ac" +dependencies = [ + "byteorder", + "lazy_static", + "log", + "regex", + "thiserror", + "tokio", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" + +[[package]] +name = "os_str_bytes" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" + +[[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.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" +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 = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[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 = "tokio" +version = "1.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "pin-project-lite", + "socket2", + "tokio-macros", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +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 = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + +[[package]] +name = "uuid" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd6469f4314d5f1ffec476e05f17cc9a78bc7a27a6a857842170bdf8d6f98d2f" +dependencies = [ + "getrandom", +] + +[[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.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[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 = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e3837b9 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "lighter" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1" +bounded-integer = { version = "0.5", features = ["macro", "std"] } +clap = { version = "3.2", features = ["derive"] } +log = "0.4" +mqtt-protocol = { version = "0.11", features = ["tokio"] } +pretty_env_logger = "0.4.0" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tokio = { version = "1.19.2", features = ["net", "rt-multi-thread", "macros", "sync", "io-std", "io-util", "time"] } +toml = "0.5" +uuid = { version = "1.1", features = ["v4"] } diff --git a/example.config.toml b/example.config.toml new file mode 100644 index 0000000..4c8a2d7 --- /dev/null +++ b/example.config.toml @@ -0,0 +1,8 @@ +[mqtt] +address = "light.lan" +#port = 1883 +#username = "user" +#password = "pass" + +[[bulbs]] +id = "light/bedroom" diff --git a/src/bulb.rs b/src/bulb.rs new file mode 100644 index 0000000..7b5c480 --- /dev/null +++ b/src/bulb.rs @@ -0,0 +1,120 @@ +use std::str::FromStr; + +#[derive(Default, Debug)] +pub struct BulbMode { + pub power: bool, + pub color: BulbColor, +} + +#[derive(Debug, Clone, Copy)] +pub enum BulbColor { + /// Light temperature, brightness + Kelvin { t: f32, b: f32 }, + + /// Hue, Saturation, Brightness + HSB { h: f32, s: f32, b: f32 }, +} + +impl FromStr for BulbColor { + type Err = anyhow::Error; + fn from_str(s: &str) -> anyhow::Result { + let parsed = u64::from_str_radix(s, 16)?; + + let to_f32 = |byte: u8| byte as f32 / 255.; + let [.., red, green, blue, white, warm] = parsed.to_be_bytes().map(to_f32); + + if (red + green + blue) != 0. { + let (h, s, b) = rgb_to_hsb(red, green, blue); + Ok(BulbColor::hsb(h, s, b)) + } else { + let b = warm + white; + let t = white / b; + + Ok(BulbColor::kelvin(t, b)) + } + } +} + +impl BulbColor { + pub fn color_string(self) -> String { + let [mut red, mut green, mut blue, mut white, mut warm] = [0u8; 5]; + + match self { + BulbColor::HSB { h, s, b } => (red, green, blue) = hsb_to_rgb(h, s, b), + BulbColor::Kelvin { t, b } => { + (white, warm) = ((t * b * 255.0) as u8, ((1. - t) * b * 255.0) as u8) + } + } + + let s = format!("{red:02x}{green:02x}{blue:02x}{white:02x}{warm:02x}"); + + s + } +} + +impl BulbColor { + pub fn hsb(h: f32, s: f32, b: f32) -> Self { + BulbColor::HSB { + h: h.clamp(0.0, 1.0), + s: s.clamp(0.0, 1.0), + b: b.clamp(0.0, 1.0), + } + } + + pub fn kelvin(t: f32, b: f32) -> Self { + BulbColor::Kelvin { + t: t.clamp(0.0, 1.0), + b: b.clamp(0.0, 1.0), + } + } +} + +impl Default for BulbColor { + fn default() -> Self { + BulbColor::Kelvin { t: 0.0, b: 0.0 } + } +} + +fn rgb_to_hsb(red: f32, green: f32, blue: f32) -> (f32, f32, f32) { + let x_max = red.max(green).max(blue); + let x_min = red.min(green).min(blue); + + let b = x_max; + let c = x_max - x_min; + + let h = match b { + _ if c == 0. => 0., + _ if b == red => (green - blue) / c, + _ if b == green => 2. + (blue - red) / c, + _ => 4. + (red - green) / c, + }; + let mut h = h * 60.0 / 360.0; + if h < 0.0 { + h += 1.0; + } + + let s = if b == 0.0 { 0.0 } else { c / b }; + + (h, s, b) +} + +fn hsb_to_rgb(h: f32, s: f32, b: f32) -> (u8, u8, u8) { + let h = 360.0 * h.clamp(0.0, 1.0); + + let c = b * s; // chroma + + let x = c * (1. - ((h / 60.) % 2. - 1.).abs()); + let m = b - c; + let m = |v| ((v + m) * 255.0) as u8; + + let (r, g, b) = match h { + _ if h < 60. => (c, x, 0.0), + _ if h < 120.0 => (x, c, 0.0), + _ if h < 180.0 => (0.0, c, x), + _ if h < 240.0 => (0.0, x, c), + _ if h < 300.0 => (x, 0.0, c), + _ => (c, 0.0, x), + }; + + (m(r), m(g), m(b)) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..22d3dce --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,6 @@ +#[macro_use] +extern crate log; + +pub mod bulb; +pub mod manager; +pub mod mqtt_conf; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..753299b --- /dev/null +++ b/src/main.rs @@ -0,0 +1,114 @@ +#[macro_use] +extern crate log; + +use clap::Parser; +use lighter::bulb::BulbColor; +use lighter::manager::{BulbCommand, BulbManager, BulbSelector, BulbsConfig}; +use lighter::mqtt_conf::MqttConfig; +use log::LevelFilter; +use serde::Deserialize; +use std::error::Error; +use std::fmt::Display; +use std::fs; +use std::io::Write; +use std::path::PathBuf; +use std::str::FromStr; +use tokio::io::{stdin, AsyncBufReadExt, BufReader}; + +async fn ask(for_what: &str) -> anyhow::Result +where + T: FromStr, + ::Err: Display + Error + Send + Sync + 'static, +{ + print!("{for_what}: "); + std::io::stdout().flush()?; + let mut input = String::new(); + BufReader::new(stdin()).read_line(&mut input).await?; + + Ok(input.trim().parse()?) +} + +#[derive(Parser)] +struct Opt { + /// More logging + #[clap(short, long, parse(from_occurrences))] + verbose: u8, + + /// Supress non-error logs + #[clap(short, long)] + quiet: bool, + + #[clap(short, long)] + config: PathBuf, +} + +#[derive(Deserialize)] +struct Config { + #[serde(flatten)] + bulbs: BulbsConfig, + + mqtt: MqttConfig, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let opt = Opt::parse(); + + let log_level = match opt.verbose { + _ if opt.quiet => LevelFilter::Error, + 0 => LevelFilter::Info, + 1 => LevelFilter::Debug, + 2.. => LevelFilter::Trace, + }; + + pretty_env_logger::formatted_builder() + .default_format() + .filter_level(log_level) + .init(); + + let config: String = fs::read_to_string(&opt.config)?; + let config: Config = toml::from_str(&config)?; + + let (commands, state) = BulbManager::launch(config.bulbs, config.mqtt).await?; + + loop { + let command: String = ask("command").await?; + + let command = match command.as_str() { + "power" => { + let power: bool = ask("on").await?; + commands.send(BulbCommand::SetPower(BulbSelector::All, power)) + } + "kelvin" => { + let t: f32 = ask("temperature").await?; + let b: f32 = ask("brightness").await?; + + commands.send(BulbCommand::SetColor( + BulbSelector::All, + BulbColor::kelvin(t, b), + )) + } + "hsb" => { + let h: f32 = ask("hue").await?; + let s: f32 = ask("saturation").await?; + let b: f32 = ask("brightness").await?; + + commands.send(BulbCommand::SetColor( + BulbSelector::All, + BulbColor::hsb(h, s, b), + )) + } + _ => { + error!("unknown command: {command}"); + continue; + } + }; + + let notify = state.notify_on_change(); + command.await?; + notify.await; + + let bulbs = state.bulbs().await; + info!("bulbs: {bulbs:?}"); + } +} diff --git a/src/manager.rs b/src/manager.rs new file mode 100644 index 0000000..af080ae --- /dev/null +++ b/src/manager.rs @@ -0,0 +1,303 @@ +use crate::bulb::{BulbColor, BulbMode}; +use crate::mqtt_conf::MqttConfig; +use mqtt::{ + packet::{PublishPacket, QoSWithPacketIdentifier, SubscribePacket, VariablePacket}, + Encodable, QualityOfService, TopicFilter, TopicName, +}; +use serde::Deserialize; +use std::collections::BTreeMap; +use std::str::from_utf8; +use std::sync::Arc; +use tokio::{ + io::AsyncWriteExt, + net::TcpStream, + sync::{futures::Notified, mpsc, Notify, RwLock, RwLockReadGuard}, + task, + time::{sleep, Duration}, +}; + +/// The mqtt publish id of the bulb +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd)] +pub struct BulbId(pub String); + +#[derive(Debug)] +pub enum BulbSelector { + All, + Id(BulbId), +} + +#[derive(Deserialize)] +pub struct BulbsConfig { + pub bulbs: Vec, +} + +#[derive(Deserialize)] +pub struct BulbConfig { + pub id: BulbId, +} + +pub struct BulbManager { + config: BulbsConfig, + mqtt: MqttConfig, + command_rx: mpsc::Receiver, + socket: TcpStream, + state: Arc, +} + +#[derive(Debug)] +pub enum BulbCommand { + SetPower(BulbSelector, bool), + SetColor(BulbSelector, BulbColor), +} + +pub struct BulbsState { + /// Notify on any change to the bulbs + notify: Notify, + + /// State of all bulbs + bulbs: RwLock>, +} + +enum Loop { + Break, + Continue, +} + +#[derive(Deserialize)] +struct BulbResult { + #[serde(rename(deserialize = "POWER"))] + power: Option, + + #[serde(rename(deserialize = "Color"))] + color: Option, +} + +impl BulbManager { + pub async fn launch( + bulbs: BulbsConfig, + mqtt: MqttConfig, + ) -> anyhow::Result<(mpsc::Sender, Arc)> { + info!("launching"); + let socket = mqtt.connect().await?; + + let (command_tx, command_rx) = mpsc::channel(100); + + let bulbs_state = BulbsState { + notify: Notify::new(), + bulbs: RwLock::new( + bulbs + .bulbs + .iter() + .map(|config| config.id.clone()) + .map(|id| (id, Default::default())) + .collect(), + ), + }; + let bulbs_state = Arc::new(bulbs_state); + + let mut manager = BulbManager { + config: bulbs, + mqtt, + command_rx, + socket, + state: Arc::clone(&bulbs_state), + }; + manager.subscribe().await?; + + task::spawn(manager.run()); + + Ok((command_tx, bulbs_state)) + } + + async fn subscribe(&mut self) -> anyhow::Result<()> { + let packet = SubscribePacket::new( + 1, + vec![(TopicFilter::new("+/lampa/#")?, QualityOfService::Level0)], + ); + let mut buf = vec![]; + packet.encode(&mut buf)?; + self.socket.write_all(&buf).await?; + + Ok(()) + } + + async fn run(mut self) -> anyhow::Result<()> { + loop { + match self.run_loop().await { + Ok(Loop::Continue) => {} + Ok(Loop::Break) => break, + Err(e) => { + const ERROR_TIMEOUT: u64 = 10; + error!("{e}"); + + loop { + info!("waiting for {ERROR_TIMEOUT} seconds before trying again"); + + sleep(Duration::from_secs(ERROR_TIMEOUT)).await; + + match self.mqtt.connect().await { + Ok(new_socket) => { + self.socket = new_socket; + self.subscribe().await?; + break; + } + Err(e) => { + error!("failed to re-establish connections: {e}"); + } + } + } + } + } + } + + info!("exiting"); + + Ok(()) + } + + async fn run_loop(&mut self) -> anyhow::Result { + let receive_packet = VariablePacket::parse(&mut self.socket); + let receive_command = self.command_rx.recv(); + + struct Publish<'a, P: ToString> { + pub topic_prefix: &'static str, + pub topic_suffix: &'static str, + pub payload: P, + pub socket: &'a mut TcpStream, + } + + impl Publish<'_, P> { + async fn send(&mut self, id: &BulbId) -> anyhow::Result<()> { + let topic_name = TopicName::new(format!( + "{}/{}/{}", + self.topic_prefix, id, self.topic_suffix + ))?; + let qos = QoSWithPacketIdentifier::Level0; + let packet = PublishPacket::new(topic_name, qos, self.payload.to_string()); + let mut buf = vec![]; + packet.encode(&mut buf)?; + self.socket.write_all(&buf).await?; + + anyhow::Ok(()) + } + } + + tokio::select!( + packet = receive_packet => { + debug!("packet received: {packet:?}"); + match packet? { + VariablePacket::PublishPacket(publish) => { + let topic_name = publish.topic_name(); + let topic_segments: Vec<&str> = topic_name.split('/').collect(); + match &topic_segments[..] { + [prefix, id@.., suffix] => { + let id = BulbId(id.join("/")); + let mut bulbs = self.state.bulbs.write().await; + let bulb = match bulbs.get_mut(&id) { + None => { + warn!("unknown bulb: {id}"); + return Ok(Loop::Continue); + } + Some(bulb) => bulb, + }; + + let payload = from_utf8(publish.payload())?; + + match (*prefix, *suffix) { + ("cmnd", _) => {} + ("stat", "POWER") => { + bulb.power = payload == "ON"; + } + ("stat", "RESULT") => { + let result: BulbResult = serde_json::from_str(payload)?; + if let Some(power) = result.power { + bulb.power = power == "ON"; + } + if let Some(color) = result.color { + bulb.color = color.parse()?; + } + } + ("tele", "STATE") => {}, + _ => { + warn!("unrecognized topic: {topic_name}"); + return Ok(Loop::Continue); + } + } + } + _ => { + warn!("unrecognized topic: {topic_name}"); + return Ok(Loop::Continue); + } + } + } + packet => warn!("unhandled packet: {packet:?}"), + } + self.state.notify.notify_waiters(); + } + command = receive_command => { + info!("command received: {command:?}"); + + async fn send(config: &BulbsConfig, selector: BulbSelector, publish: &mut Publish<'_, P>) -> anyhow::Result<()>{ + match selector { + BulbSelector::All => { + for bulb in &config.bulbs { + publish.send(&bulb.id).await?; + } + } + BulbSelector::Id(id) =>publish.send(&id).await?, + } + Ok(()) + } + + match command { + Some(BulbCommand::SetPower(selector, power)) => { + let payload = if power { "ON" } else { "OFF" }; + let mut publish = Publish { + topic_prefix: "cmnd", + topic_suffix: "POWER", + payload, + socket: &mut self.socket, + }; + send(&self.config, selector, &mut publish).await?; + } + Some(BulbCommand::SetColor(selector, color)) => { + let mut publish = Publish { + topic_prefix: "cmnd", + topic_suffix: "COLOR", + payload: color.color_string(), + socket: &mut self.socket, + }; + send(&self.config, selector, &mut publish).await?; + } + None => return Ok(Loop::Break), + } + } + ); + + Ok(Loop::Continue) + } +} + +impl BulbsState { + pub fn notify_on_change(&self) -> Notified { + self.notify.notified() + } + + pub async fn bulbs(&self) -> RwLockReadGuard<'_, BTreeMap> { + self.bulbs.read().await + } +} + +use std::fmt::{self, Display, Formatter}; + +impl Display for BulbId { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for BulbId { + fn from(s: String) -> Self { + BulbId(s) + } +} diff --git a/src/mqtt_conf.rs b/src/mqtt_conf.rs new file mode 100644 index 0000000..a741761 --- /dev/null +++ b/src/mqtt_conf.rs @@ -0,0 +1,43 @@ +use mqtt::{ + control::variable_header::ConnectReturnCode, + packet::{ConnectPacket, VariablePacket}, + Encodable, +}; +use serde::Deserialize; +use tokio::io::AsyncWriteExt; +use tokio::net::TcpStream; +use uuid::Uuid; + +#[derive(Deserialize)] +pub struct MqttConfig { + pub address: String, + pub port: Option, + pub username: Option, + pub password: Option, +} + +impl MqttConfig { + pub async fn connect(&self) -> anyhow::Result { + let mut socket = + TcpStream::connect((self.address.as_str(), self.port.unwrap_or(1883))).await?; + + let id = format!("varde-{}", Uuid::new_v4()); + let mut packet = ConnectPacket::new(id); + packet.set_user_name(self.username.clone()); + packet.set_password(self.password.clone()); + packet.set_keep_alive(5000); + + let mut buf = vec![]; + packet.encode(&mut buf)?; + + socket.write_all(&buf).await?; + + match VariablePacket::parse(&mut socket).await? { + VariablePacket::ConnackPacket(ack) => match ack.connect_return_code() { + ConnectReturnCode::ConnectionAccepted => Ok(socket), + return_code => anyhow::bail!("connection refused: {return_code:?}"), + }, + response => anyhow::bail!("mqtt connect, unexpected response: {response:?}"), + } + } +}