From 6b19cdbfc2183af1a6760f135a62aec071fb7a7a Mon Sep 17 00:00:00 2001 From: Sachymetsu Date: Mon, 15 Dec 2025 17:20:47 +0100 Subject: [PATCH] feat: ESPHome Protocol crate Change-Id: xtxtsnxlluozzsmuwqmtqwswmorlvwpp --- .tangled/workflows/test.yml | 1 + Cargo.lock | 732 ++++++- Cargo.toml | 10 +- sachy-esphome/Cargo.toml | 32 + sachy-esphome/build.rs | 99 + sachy-esphome/protos/api.proto | 2166 ++++++++++++++++++++ sachy-esphome/protos/api_options.proto | 25 + sachy-esphome/src/lib.rs | 166 ++ sachy-esphome/templates/message.handlebars | 66 + 9 files changed, 3292 insertions(+), 5 deletions(-) create mode 100644 sachy-esphome/Cargo.toml create mode 100644 sachy-esphome/build.rs create mode 100644 sachy-esphome/protos/api.proto create mode 100644 sachy-esphome/protos/api_options.proto create mode 100644 sachy-esphome/src/lib.rs create mode 100644 sachy-esphome/templates/message.handlebars diff --git a/.tangled/workflows/test.yml b/.tangled/workflows/test.yml index 5e234d6..079568f 100644 --- a/.tangled/workflows/test.yml +++ b/.tangled/workflows/test.yml @@ -9,6 +9,7 @@ dependencies: - clang - cargo - rustfmt + - protobuf steps: - name: Format check diff --git a/Cargo.lock b/Cargo.lock index dd41970..7dee9c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,21 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + [[package]] name = "autocfg" version = "1.5.0" @@ -20,12 +35,30 @@ version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +dependencies = [ + "portable-atomic", +] + [[package]] name = "cast" version = "0.3.0" @@ -73,12 +106,66 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "critical-section" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn", +] + [[package]] name = "defmt" version = "0.3.100" @@ -117,7 +204,48 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e" dependencies = [ - "thiserror", + "thiserror 2.0.17", +] + +[[package]] +name = "derive_builder" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_builder_macro" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c" +dependencies = [ + "derive_builder_core", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", ] [[package]] @@ -129,6 +257,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "embassy-net" version = "0.7.1" @@ -273,12 +407,46 @@ dependencies = [ "embedded-nal", ] +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "futures-core" version = "0.3.31" @@ -291,6 +459,28 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + [[package]] name = "gpio-cdev" version = "0.6.0" @@ -302,6 +492,22 @@ dependencies = [ "nix 0.27.1", ] +[[package]] +name = "handlebars" +version = "6.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759e2d5aea3287cb1190c8ec394f42866cb5bf74fcbf213f354e3c856ea26098" +dependencies = [ + "derive_builder", + "log", + "num-order", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror 2.0.17", +] + [[package]] name = "hash32" version = "0.3.1" @@ -311,6 +517,12 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heapless" version = "0.8.0" @@ -332,6 +544,21 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "i2cdev" version = "0.6.2" @@ -344,6 +571,22 @@ dependencies = [ "nix 0.26.4", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "io-kit-sys" version = "0.4.1" @@ -354,12 +597,33 @@ dependencies = [ "mach2", ] +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + [[package]] name = "libc" version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "linux-embedded-hal" version = "0.4.1" @@ -378,12 +642,30 @@ dependencies = [ "sysfs_gpio", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + [[package]] name = "litrs" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + [[package]] name = "mach2" version = "0.4.3" @@ -399,6 +681,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ca88d725a0a943b096803bd34e73a4437208b6077654cc4ecb2947a5f91618d" +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + [[package]] name = "memoffset" version = "0.6.5" @@ -417,6 +705,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + [[package]] name = "nb" version = "0.1.3" @@ -469,6 +763,32 @@ dependencies = [ "libc", ] +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -478,12 +798,87 @@ dependencies = [ "autocfg", ] +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "pest" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcfd20a6d4eeba40179f05735784ad32bdaef05ce8e8af05f180d45bb3e7e22" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f72981ade67b1ca6adc26ec221be9f463f2b5839c7508998daa17c23d94d7f" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dee9efd8cdb50d719a80088b76f81aec7c41ed6d522ee750178f83883d271625" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1d70880e76bdc13ba52eafa6239ce793d85c8e43896507e43dd8984ff05b82" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap", +] + [[package]] name = "pin-utils" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -515,6 +910,94 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7231bd9b3d3d33c86b58adbac74b5ec0ad9f496b19d22801d773636feaa95f3d" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6c3320f9abac597dcbc668774ef006702672474aad53c6d596b62e487b40b1" +dependencies = [ + "heck", + "itertools", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b4db3d6da204ed77bb26ba83b6122a73aeb2e87e25fbf7ad2e84c4ccbf8f72" +dependencies = [ + "prost", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "quote" version = "1.0.40" @@ -524,6 +1007,73 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys 0.11.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + [[package]] name = "sachy-battery" version = "0.1.0" @@ -537,6 +1087,27 @@ dependencies = [ "sachy-fmt", ] +[[package]] +name = "sachy-esphome" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "defmt 1.0.1", + "handlebars", + "heck", + "libm", + "num-derive", + "num-traits", + "prost", + "prost-build", + "prost-types", + "protobuf", + "protobuf-parse", + "serde", + "thiserror 2.0.17", +] + [[package]] name = "sachy-fmt" version = "0.1.0" @@ -576,6 +1147,49 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + [[package]] name = "serialport" version = "4.8.1" @@ -592,7 +1206,18 @@ dependencies = [ "quote", "scopeguard", "unescaper", - "windows-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", ] [[package]] @@ -631,6 +1256,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "syn" version = "2.0.111" @@ -651,13 +1282,46 @@ dependencies = [ "nix 0.23.2", ] +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix 1.1.2", + "windows-sys 0.52.0", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.17", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -671,13 +1335,25 @@ dependencies = [ "syn", ] +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + [[package]] name = "unescaper" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c01d12e3a56a4432a8b436f293c25f4808bdf9e9f9f98f9260bba1f1bc5a1f26" dependencies = [ - "thiserror", + "thiserror 2.0.17", ] [[package]] @@ -686,12 +1362,45 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "void" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + [[package]] name = "windows-sys" version = "0.52.0" @@ -701,6 +1410,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -764,3 +1482,9 @@ name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" diff --git a/Cargo.toml b/Cargo.toml index 8b4b321..2ab4e4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,14 @@ [workspace] resolver = "3" -members = ["sachy-battery","sachy-bthome", "sachy-fmt", "sachy-fnv", "sachy-shtc3", "sachy-sntp"] +members = [ + "sachy-battery", + "sachy-bthome", + "sachy-esphome", + "sachy-fmt", + "sachy-fnv", + "sachy-shtc3", + "sachy-sntp", +] [workspace.package] authors = ["Sachymetsu "] diff --git a/sachy-esphome/Cargo.toml b/sachy-esphome/Cargo.toml new file mode 100644 index 0000000..6c70d3e --- /dev/null +++ b/sachy-esphome/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "sachy-esphome" +authors.workspace = true +edition.workspace = true +repository.workspace = true +license.workspace = true +version.workspace = true +rust-version.workspace = true + +[dependencies] +defmt = { workspace = true, features = ["alloc"], optional = true } +bytes = { version = "1", default-features = false, features = ["extra-platforms"] } +libm = { version = "0.2.15", optional = true } +num-derive = "0.4.2" +num-traits = { version = "0.2.19", default-features = false } +prost = { version = "0.14.1", default-features = false, features = ["derive"] } +prost-types = { version = "0.14.1", default-features = false } +thiserror = { version = "2.0.12", default-features = false } + +[build-dependencies] +anyhow = "1.0.98" +handlebars = "6.3.2" +heck = "0.5.0" +prost-build = "0.14.1" +protobuf = "3.7.2" +protobuf-parse = "3.7.2" +serde = { version = "1.0.219", features = ["derive"] } + +[features] +default = [] +libm = ["dep:libm"] +defmt = ["dep:defmt"] diff --git a/sachy-esphome/build.rs b/sachy-esphome/build.rs new file mode 100644 index 0000000..fe61ccd --- /dev/null +++ b/sachy-esphome/build.rs @@ -0,0 +1,99 @@ +use heck::ToUpperCamelCase; +use std::{io::BufWriter, path::PathBuf}; + +static MESSAGE_TEMPLATE: &str = include_str!("./templates/message.handlebars"); + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] +struct ParsedMessageKind { + name: String, + id: u64, +} + +fn main() -> anyhow::Result<()> { + let proto_files = ["protos/api.proto", "protos/api_options.proto"]; + let includes = ["protos"]; + + prost_build::Config::new() + .btree_map(["."]) + .message_attribute( + ".", + "#[cfg_attr(feature = \"defmt\", derive(defmt::Format))]", + ) + .default_package_filename("api") + .compile_protos(&proto_files, &includes)?; + + let valid_sources = 0..=2; + + let mut message_kinds: Vec = protobuf_parse::Parser::new() + .inputs(proto_files) + .includes(includes) + .parse_and_typecheck()? + .file_descriptors + .into_iter() + .flat_map(|fd| fd.message_type) + .filter_map(|message| { + if let Some(protobuf::UnknownValueRef::Varint(id)) = message + .options + .get_or_default() + .special_fields + .unknown_fields() + .get(1036) + && let Some(protobuf::UnknownValueRef::Varint(source)) = message + .options + .get_or_default() + .special_fields + .unknown_fields() + .get(1037) + && valid_sources.contains(&source) + { + Some(ParsedMessageKind { + name: sanitize_identifier(message.name().to_upper_camel_case()), + id, + }) + } else { + None + } + }) + .collect(); + + message_kinds.sort(); + + let out_dir = PathBuf::from(std::env::var("OUT_DIR")?).join("api.rs"); + + let api_constants = BufWriter::new(std::fs::OpenOptions::new().append(true).open(out_dir)?); + + handlebars::Handlebars::new().render_template_to_write( + MESSAGE_TEMPLATE, + &message_kinds, + api_constants, + )?; + + Ok(()) +} + +/// From prost-build, as it is in a private module. +fn sanitize_identifier(ident: String) -> String { + // Use a raw identifier if the identifier matches a Rust keyword: + // https://doc.rust-lang.org/reference/keywords.html. + match ident.as_str() { + // 2015 strict keywords. + | "as" | "break" | "const" | "continue" | "else" | "enum" | "false" + | "fn" | "for" | "if" | "impl" | "in" | "let" | "loop" | "match" | "mod" | "move" | "mut" + | "pub" | "ref" | "return" | "static" | "struct" | "trait" | "true" + | "type" | "unsafe" | "use" | "where" | "while" + // 2018 strict keywords. + | "dyn" + // 2015 reserved keywords. + | "abstract" | "become" | "box" | "do" | "final" | "macro" | "override" | "priv" | "typeof" + | "unsized" | "virtual" | "yield" + // 2018 reserved keywords. + | "async" | "await" | "try" + // 2024 reserved keywords. + | "gen" => format!("r#{ident}"), + // the following keywords are not supported as raw identifiers and are therefore suffixed with an underscore. + "_" | "super" | "self" | "Self" | "extern" | "crate" => format!("{ident}_"), + // the following keywords begin with a number and are therefore prefixed with an underscore. + s if s.starts_with(char::is_numeric) => format!("_{ident}"), + _ => ident, + } +} diff --git a/sachy-esphome/protos/api.proto b/sachy-esphome/protos/api.proto new file mode 100644 index 0000000..c3795bb --- /dev/null +++ b/sachy-esphome/protos/api.proto @@ -0,0 +1,2166 @@ +syntax = "proto3"; + +import "api_options.proto"; + +service APIConnection { + rpc hello (HelloRequest) returns (HelloResponse) { + option (needs_setup_connection) = false; + option (needs_authentication) = false; + } + rpc connect (ConnectRequest) returns (ConnectResponse) { + option (needs_setup_connection) = false; + option (needs_authentication) = false; + } + rpc disconnect (DisconnectRequest) returns (DisconnectResponse) { + option (needs_setup_connection) = false; + option (needs_authentication) = false; + } + rpc ping (PingRequest) returns (PingResponse) { + option (needs_setup_connection) = false; + option (needs_authentication) = false; + } + rpc device_info (DeviceInfoRequest) returns (DeviceInfoResponse) { + option (needs_authentication) = false; + } + rpc list_entities (ListEntitiesRequest) returns (void) {} + rpc subscribe_states (SubscribeStatesRequest) returns (void) {} + rpc subscribe_logs (SubscribeLogsRequest) returns (void) {} + rpc subscribe_homeassistant_services (SubscribeHomeassistantServicesRequest) returns (void) {} + rpc subscribe_home_assistant_states (SubscribeHomeAssistantStatesRequest) returns (void) {} + rpc get_time (GetTimeRequest) returns (GetTimeResponse) { + option (needs_authentication) = false; + } + rpc execute_service (ExecuteServiceRequest) returns (void) {} + rpc noise_encryption_set_key (NoiseEncryptionSetKeyRequest) returns (NoiseEncryptionSetKeyResponse) {} + + rpc button_command (ButtonCommandRequest) returns (void) {} + rpc camera_image (CameraImageRequest) returns (void) {} + rpc climate_command (ClimateCommandRequest) returns (void) {} + rpc cover_command (CoverCommandRequest) returns (void) {} + rpc date_command (DateCommandRequest) returns (void) {} + rpc datetime_command (DateTimeCommandRequest) returns (void) {} + rpc fan_command (FanCommandRequest) returns (void) {} + rpc light_command (LightCommandRequest) returns (void) {} + rpc lock_command (LockCommandRequest) returns (void) {} + rpc media_player_command (MediaPlayerCommandRequest) returns (void) {} + rpc number_command (NumberCommandRequest) returns (void) {} + rpc select_command (SelectCommandRequest) returns (void) {} + rpc siren_command (SirenCommandRequest) returns (void) {} + rpc switch_command (SwitchCommandRequest) returns (void) {} + rpc text_command (TextCommandRequest) returns (void) {} + rpc time_command (TimeCommandRequest) returns (void) {} + rpc update_command (UpdateCommandRequest) returns (void) {} + rpc valve_command (ValveCommandRequest) returns (void) {} + + rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {} + rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {} + rpc bluetooth_gatt_get_services(BluetoothGATTGetServicesRequest) returns (void) {} + rpc bluetooth_gatt_read(BluetoothGATTReadRequest) returns (void) {} + rpc bluetooth_gatt_write(BluetoothGATTWriteRequest) returns (void) {} + rpc bluetooth_gatt_read_descriptor(BluetoothGATTReadDescriptorRequest) returns (void) {} + rpc bluetooth_gatt_write_descriptor(BluetoothGATTWriteDescriptorRequest) returns (void) {} + rpc bluetooth_gatt_notify(BluetoothGATTNotifyRequest) returns (void) {} + rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} + rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} + rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {} + + rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} + rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} + rpc voice_assistant_set_configuration(VoiceAssistantSetConfiguration) returns (void) {} + + rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {} +} + + +// ==================== BASE PACKETS ==================== + +// The Home Assistant protocol is structured as a simple +// TCP socket with short binary messages encoded in the protocol buffers format +// First, a message in this protocol has a specific format: +// * A zero byte. +// * VarInt denoting the size of the message object. (type is not part of this) +// * VarInt denoting the type of message. +// * The message object encoded as a ProtoBuf message + +// The connection is established in 4 steps: +// * First, the client connects to the server and sends a "Hello Request" identifying itself +// * The server responds with a "Hello Response" and selects the protocol version +// * After receiving this message, the client attempts to authenticate itself using +// the password and a "Connect Request" +// * The server responds with a "Connect Response" and notifies of invalid password. +// If anything in this initial process fails, the connection must immediately closed +// by both sides and _no_ disconnection message is to be sent. + +// Message sent at the beginning of each connection +// Can only be sent by the client and only at the beginning of the connection +message HelloRequest { + option (id) = 1; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + + // Description of client (like User Agent) + // For example "Home Assistant" + // Not strictly necessary to send but nice for debugging + // purposes. + string client_info = 1; + uint32 api_version_major = 2; + uint32 api_version_minor = 3; +} + +// Confirmation of successful connection request. +// Can only be sent by the server and only at the beginning of the connection +message HelloResponse { + option (id) = 2; + option (source) = SOURCE_SERVER; + option (no_delay) = true; + + // The version of the API to use. The _client_ (for example Home Assistant) needs to check + // for compatibility and if necessary adopt to an older API. + // Major is for breaking changes in the base protocol - a mismatch will lead to immediate disconnect_client_ + // Minor is for breaking changes in individual messages - a mismatch will lead to a warning message + uint32 api_version_major = 1; + uint32 api_version_minor = 2; + + // A string identifying the server (ESP); like client info this may be empty + // and only exists for debugging/logging purposes. + // For example "ESPHome v1.10.0 on ESP8266" + string server_info = 3; + + // The name of the server (App.get_name()) + string name = 4; +} + +// Message sent at the beginning of each connection to authenticate the client +// Can only be sent by the client and only at the beginning of the connection +message ConnectRequest { + option (id) = 3; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + + // The password to log in with + string password = 1; +} + +// Confirmation of successful connection. After this the connection is available for all traffic. +// Can only be sent by the server and only at the beginning of the connection +message ConnectResponse { + option (id) = 4; + option (source) = SOURCE_SERVER; + option (no_delay) = true; + + bool invalid_password = 1; +} + +// Request to close the connection. +// Can be sent by both the client and server +message DisconnectRequest { + option (id) = 5; + option (source) = SOURCE_BOTH; + option (no_delay) = true; + + // Do not close the connection before the acknowledgement arrives +} + +message DisconnectResponse { + option (id) = 6; + option (source) = SOURCE_BOTH; + option (no_delay) = true; + + // Empty - Both parties are required to close the connection after this + // message has been received. +} + +message PingRequest { + option (id) = 7; + option (source) = SOURCE_BOTH; + // Empty +} + +message PingResponse { + option (id) = 8; + option (source) = SOURCE_BOTH; + // Empty +} + +message DeviceInfoRequest { + option (id) = 9; + option (source) = SOURCE_CLIENT; + // Empty +} + +message AreaInfo { + uint32 area_id = 1; + string name = 2; +} + +message DeviceInfo { + uint32 device_id = 1; + string name = 2; + uint32 area_id = 3; +} + +message DeviceInfoResponse { + option (id) = 10; + option (source) = SOURCE_SERVER; + + bool uses_password = 1; + + // The name of the node, given by "App.set_name()" + string name = 2; + + // The mac address of the device. For example "AC:BC:32:89:0E:A9" + string mac_address = 3; + + // A string describing the ESPHome version. For example "1.10.0" + string esphome_version = 4; + + // A string describing the date of compilation, this is generated by the compiler + // and therefore may not be in the same format all the time. + // If the user isn't using ESPHome, this will also not be set. + string compilation_time = 5; + + // The model of the board. For example NodeMCU + string model = 6; + + bool has_deep_sleep = 7; + + // The esphome project details if set + string project_name = 8; + string project_version = 9; + + uint32 webserver_port = 10; + + uint32 legacy_bluetooth_proxy_version = 11; + uint32 bluetooth_proxy_feature_flags = 15; + + string manufacturer = 12; + + string friendly_name = 13; + + uint32 legacy_voice_assistant_version = 14; + uint32 voice_assistant_feature_flags = 17; + + string suggested_area = 16; + + // The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA" + string bluetooth_mac_address = 18; + + // Supports receiving and saving api encryption key + bool api_encryption_supported = 19; + + repeated DeviceInfo devices = 20; + repeated AreaInfo areas = 21; + + // Top-level area info to phase out suggested_area + AreaInfo area = 22; +} + +message ListEntitiesRequest { + option (id) = 11; + option (source) = SOURCE_CLIENT; + // Empty +} +message ListEntitiesDoneResponse { + option (id) = 19; + option (source) = SOURCE_SERVER; + option (no_delay) = true; + // Empty +} +message SubscribeStatesRequest { + option (id) = 20; + option (source) = SOURCE_CLIENT; + // Empty +} + +// ==================== COMMON ===================== + +enum EntityCategory { + ENTITY_CATEGORY_NONE = 0; + ENTITY_CATEGORY_CONFIG = 1; + ENTITY_CATEGORY_DIAGNOSTIC = 2; +} + +// ==================== BINARY SENSOR ==================== +message ListEntitiesBinarySensorResponse { + option (id) = 12; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BINARY_SENSOR"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string device_class = 5; + bool is_status_binary_sensor = 6; + bool disabled_by_default = 7; + string icon = 8; + EntityCategory entity_category = 9; + uint32 device_id = 10; +} +message BinarySensorStateResponse { + option (id) = 21; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BINARY_SENSOR"; + option (no_delay) = true; + + fixed32 key = 1; + bool state = 2; + // If the binary sensor does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; + uint32 device_id = 4; +} + +// ==================== COVER ==================== +message ListEntitiesCoverResponse { + option (id) = 13; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_COVER"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + bool assumed_state = 5; + bool supports_position = 6; + bool supports_tilt = 7; + string device_class = 8; + bool disabled_by_default = 9; + string icon = 10; + EntityCategory entity_category = 11; + bool supports_stop = 12; + uint32 device_id = 13; +} + +enum LegacyCoverState { + LEGACY_COVER_STATE_OPEN = 0; + LEGACY_COVER_STATE_CLOSED = 1; +} +enum CoverOperation { + COVER_OPERATION_IDLE = 0; + COVER_OPERATION_IS_OPENING = 1; + COVER_OPERATION_IS_CLOSING = 2; +} +message CoverStateResponse { + option (id) = 22; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_COVER"; + option (no_delay) = true; + + fixed32 key = 1; + // legacy: state has been removed in 1.13 + // clients/servers must still send/accept it until the next protocol change + LegacyCoverState legacy_state = 2; + + float position = 3; + float tilt = 4; + CoverOperation current_operation = 5; + uint32 device_id = 6; +} + +enum LegacyCoverCommand { + LEGACY_COVER_COMMAND_OPEN = 0; + LEGACY_COVER_COMMAND_CLOSE = 1; + LEGACY_COVER_COMMAND_STOP = 2; +} +message CoverCommandRequest { + option (id) = 30; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_COVER"; + option (no_delay) = true; + + fixed32 key = 1; + + // legacy: command has been removed in 1.13 + // clients/servers must still send/accept it until the next protocol change + bool has_legacy_command = 2; + LegacyCoverCommand legacy_command = 3; + + bool has_position = 4; + float position = 5; + bool has_tilt = 6; + float tilt = 7; + bool stop = 8; +} + +// ==================== FAN ==================== +message ListEntitiesFanResponse { + option (id) = 14; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_FAN"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + bool supports_oscillation = 5; + bool supports_speed = 6; + bool supports_direction = 7; + int32 supported_speed_count = 8; + bool disabled_by_default = 9; + string icon = 10; + EntityCategory entity_category = 11; + repeated string supported_preset_modes = 12; + uint32 device_id = 13; +} +enum FanSpeed { + FAN_SPEED_LOW = 0; + FAN_SPEED_MEDIUM = 1; + FAN_SPEED_HIGH = 2; +} +enum FanDirection { + FAN_DIRECTION_FORWARD = 0; + FAN_DIRECTION_REVERSE = 1; +} +message FanStateResponse { + option (id) = 23; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_FAN"; + option (no_delay) = true; + + fixed32 key = 1; + bool state = 2; + bool oscillating = 3; + FanSpeed speed = 4 [deprecated = true]; + FanDirection direction = 5; + int32 speed_level = 6; + string preset_mode = 7; + uint32 device_id = 8; +} +message FanCommandRequest { + option (id) = 31; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_FAN"; + option (no_delay) = true; + + fixed32 key = 1; + bool has_state = 2; + bool state = 3; + bool has_speed = 4 [deprecated = true]; + FanSpeed speed = 5 [deprecated = true]; + bool has_oscillating = 6; + bool oscillating = 7; + bool has_direction = 8; + FanDirection direction = 9; + bool has_speed_level = 10; + int32 speed_level = 11; + bool has_preset_mode = 12; + string preset_mode = 13; +} + +// ==================== LIGHT ==================== +enum ColorMode { + COLOR_MODE_UNKNOWN = 0; + COLOR_MODE_ON_OFF = 1; + COLOR_MODE_LEGACY_BRIGHTNESS = 2; + COLOR_MODE_BRIGHTNESS = 3; + COLOR_MODE_WHITE = 7; + COLOR_MODE_COLOR_TEMPERATURE = 11; + COLOR_MODE_COLD_WARM_WHITE = 19; + COLOR_MODE_RGB = 35; + COLOR_MODE_RGB_WHITE = 39; + COLOR_MODE_RGB_COLOR_TEMPERATURE = 47; + COLOR_MODE_RGB_COLD_WARM_WHITE = 51; +} +message ListEntitiesLightResponse { + option (id) = 15; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_LIGHT"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + repeated ColorMode supported_color_modes = 12; + // next four supports_* are for legacy clients, newer clients should use color modes + bool legacy_supports_brightness = 5 [deprecated=true]; + bool legacy_supports_rgb = 6 [deprecated=true]; + bool legacy_supports_white_value = 7 [deprecated=true]; + bool legacy_supports_color_temperature = 8 [deprecated=true]; + float min_mireds = 9; + float max_mireds = 10; + repeated string effects = 11; + bool disabled_by_default = 13; + string icon = 14; + EntityCategory entity_category = 15; + uint32 device_id = 16; +} +message LightStateResponse { + option (id) = 24; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_LIGHT"; + option (no_delay) = true; + + fixed32 key = 1; + bool state = 2; + float brightness = 3; + ColorMode color_mode = 11; + float color_brightness = 10; + float red = 4; + float green = 5; + float blue = 6; + float white = 7; + float color_temperature = 8; + float cold_white = 12; + float warm_white = 13; + string effect = 9; + uint32 device_id = 14; +} +message LightCommandRequest { + option (id) = 32; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_LIGHT"; + option (no_delay) = true; + + fixed32 key = 1; + bool has_state = 2; + bool state = 3; + bool has_brightness = 4; + float brightness = 5; + bool has_color_mode = 22; + ColorMode color_mode = 23; + bool has_color_brightness = 20; + float color_brightness = 21; + bool has_rgb = 6; + float red = 7; + float green = 8; + float blue = 9; + bool has_white = 10; + float white = 11; + bool has_color_temperature = 12; + float color_temperature = 13; + bool has_cold_white = 24; + float cold_white = 25; + bool has_warm_white = 26; + float warm_white = 27; + bool has_transition_length = 14; + uint32 transition_length = 15; + bool has_flash_length = 16; + uint32 flash_length = 17; + bool has_effect = 18; + string effect = 19; +} + +// ==================== SENSOR ==================== +enum SensorStateClass { + STATE_CLASS_NONE = 0; + STATE_CLASS_MEASUREMENT = 1; + STATE_CLASS_TOTAL_INCREASING = 2; + STATE_CLASS_TOTAL = 3; +} + +enum SensorLastResetType { + LAST_RESET_NONE = 0; + LAST_RESET_NEVER = 1; + LAST_RESET_AUTO = 2; +} + +message ListEntitiesSensorResponse { + option (id) = 16; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SENSOR"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + string unit_of_measurement = 6; + int32 accuracy_decimals = 7; + bool force_update = 8; + string device_class = 9; + SensorStateClass state_class = 10; + // Last reset type removed in 2021.9.0 + SensorLastResetType legacy_last_reset_type = 11; + bool disabled_by_default = 12; + EntityCategory entity_category = 13; + uint32 device_id = 14; +} +message SensorStateResponse { + option (id) = 25; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SENSOR"; + option (no_delay) = true; + + fixed32 key = 1; + float state = 2; + // If the sensor does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; + uint32 device_id = 4; +} + +// ==================== SWITCH ==================== +message ListEntitiesSwitchResponse { + option (id) = 17; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SWITCH"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool assumed_state = 6; + bool disabled_by_default = 7; + EntityCategory entity_category = 8; + string device_class = 9; + uint32 device_id = 10; +} +message SwitchStateResponse { + option (id) = 26; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SWITCH"; + option (no_delay) = true; + + fixed32 key = 1; + bool state = 2; + uint32 device_id = 3; +} +message SwitchCommandRequest { + option (id) = 33; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SWITCH"; + option (no_delay) = true; + + fixed32 key = 1; + bool state = 2; +} + +// ==================== TEXT SENSOR ==================== +message ListEntitiesTextSensorResponse { + option (id) = 18; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_TEXT_SENSOR"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + string device_class = 8; + uint32 device_id = 9; +} +message TextSensorStateResponse { + option (id) = 27; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_TEXT_SENSOR"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; + // If the text sensor does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; + uint32 device_id = 4; +} + +// ==================== SUBSCRIBE LOGS ==================== +enum LogLevel { + LOG_LEVEL_NONE = 0; + LOG_LEVEL_ERROR = 1; + LOG_LEVEL_WARN = 2; + LOG_LEVEL_INFO = 3; + LOG_LEVEL_CONFIG = 4; + LOG_LEVEL_DEBUG = 5; + LOG_LEVEL_VERBOSE = 6; + LOG_LEVEL_VERY_VERBOSE = 7; +} +message SubscribeLogsRequest { + option (id) = 28; + option (source) = SOURCE_CLIENT; + LogLevel level = 1; + bool dump_config = 2; +} +message SubscribeLogsResponse { + option (id) = 29; + option (source) = SOURCE_SERVER; + option (log) = false; + option (no_delay) = false; + + LogLevel level = 1; + bytes message = 3; + bool send_failed = 4; +} + +// ==================== NOISE ENCRYPTION ==================== +message NoiseEncryptionSetKeyRequest { + option (id) = 124; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_API_NOISE"; + + bytes key = 1; +} + +message NoiseEncryptionSetKeyResponse { + option (id) = 125; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_API_NOISE"; + + bool success = 1; +} + +// ==================== HOMEASSISTANT.SERVICE ==================== +message SubscribeHomeassistantServicesRequest { + option (id) = 34; + option (source) = SOURCE_CLIENT; +} + +message HomeassistantServiceMap { + string key = 1; + string value = 2; +} + +message HomeassistantServiceResponse { + option (id) = 35; + option (source) = SOURCE_SERVER; + option (no_delay) = true; + + string service = 1; + repeated HomeassistantServiceMap data = 2; + repeated HomeassistantServiceMap data_template = 3; + repeated HomeassistantServiceMap variables = 4; + bool is_event = 5; +} + +// ==================== IMPORT HOME ASSISTANT STATES ==================== +// 1. Client sends SubscribeHomeAssistantStatesRequest +// 2. Server responds with zero or more SubscribeHomeAssistantStateResponse (async) +// 3. Client sends HomeAssistantStateResponse for state changes. +message SubscribeHomeAssistantStatesRequest { + option (id) = 38; + option (source) = SOURCE_CLIENT; +} + +message SubscribeHomeAssistantStateResponse { + option (id) = 39; + option (source) = SOURCE_SERVER; + string entity_id = 1; + string attribute = 2; + bool once = 3; +} + +message HomeAssistantStateResponse { + option (id) = 40; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + + string entity_id = 1; + string state = 2; + string attribute = 3; +} + +// ==================== IMPORT TIME ==================== +message GetTimeRequest { + option (id) = 36; + option (source) = SOURCE_BOTH; +} + +message GetTimeResponse { + option (id) = 37; + option (source) = SOURCE_BOTH; + option (no_delay) = true; + + fixed32 epoch_seconds = 1; +} + +// ==================== USER-DEFINES SERVICES ==================== +enum ServiceArgType { + SERVICE_ARG_TYPE_BOOL = 0; + SERVICE_ARG_TYPE_INT = 1; + SERVICE_ARG_TYPE_FLOAT = 2; + SERVICE_ARG_TYPE_STRING = 3; + SERVICE_ARG_TYPE_BOOL_ARRAY = 4; + SERVICE_ARG_TYPE_INT_ARRAY = 5; + SERVICE_ARG_TYPE_FLOAT_ARRAY = 6; + SERVICE_ARG_TYPE_STRING_ARRAY = 7; +} +message ListEntitiesServicesArgument { + string name = 1; + ServiceArgType type = 2; +} +message ListEntitiesServicesResponse { + option (id) = 41; + option (source) = SOURCE_SERVER; + + string name = 1; + fixed32 key = 2; + repeated ListEntitiesServicesArgument args = 3; +} +message ExecuteServiceArgument { + bool bool_ = 1; + int32 legacy_int = 2; + float float_ = 3; + string string_ = 4; + // ESPHome 1.14 (api v1.3) make int a signed value + sint32 int_ = 5; + repeated bool bool_array = 6 [packed=false]; + repeated sint32 int_array = 7 [packed=false]; + repeated float float_array = 8 [packed=false]; + repeated string string_array = 9; +} +message ExecuteServiceRequest { + option (id) = 42; + option (source) = SOURCE_CLIENT; + option (no_delay) = true; + + fixed32 key = 1; + repeated ExecuteServiceArgument args = 2; +} + +// ==================== CAMERA ==================== +message ListEntitiesCameraResponse { + option (id) = 43; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_CAMERA"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + bool disabled_by_default = 5; + string icon = 6; + EntityCategory entity_category = 7; + uint32 device_id = 8; +} + +message CameraImageResponse { + option (id) = 44; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_CAMERA"; + + fixed32 key = 1; + bytes data = 2; + bool done = 3; +} +message CameraImageRequest { + option (id) = 45; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_CAMERA"; + option (no_delay) = true; + + bool single = 1; + bool stream = 2; +} + +// ==================== CLIMATE ==================== +enum ClimateMode { + CLIMATE_MODE_OFF = 0; + CLIMATE_MODE_HEAT_COOL = 1; + CLIMATE_MODE_COOL = 2; + CLIMATE_MODE_HEAT = 3; + CLIMATE_MODE_FAN_ONLY = 4; + CLIMATE_MODE_DRY = 5; + CLIMATE_MODE_AUTO = 6; +} +enum ClimateFanMode { + CLIMATE_FAN_ON = 0; + CLIMATE_FAN_OFF = 1; + CLIMATE_FAN_AUTO = 2; + CLIMATE_FAN_LOW = 3; + CLIMATE_FAN_MEDIUM = 4; + CLIMATE_FAN_HIGH = 5; + CLIMATE_FAN_MIDDLE = 6; + CLIMATE_FAN_FOCUS = 7; + CLIMATE_FAN_DIFFUSE = 8; + CLIMATE_FAN_QUIET = 9; +} +enum ClimateSwingMode { + CLIMATE_SWING_OFF = 0; + CLIMATE_SWING_BOTH = 1; + CLIMATE_SWING_VERTICAL = 2; + CLIMATE_SWING_HORIZONTAL = 3; +} +enum ClimateAction { + CLIMATE_ACTION_OFF = 0; + // values same as mode for readability + CLIMATE_ACTION_COOLING = 2; + CLIMATE_ACTION_HEATING = 3; + CLIMATE_ACTION_IDLE = 4; + CLIMATE_ACTION_DRYING = 5; + CLIMATE_ACTION_FAN = 6; +} +enum ClimatePreset { + CLIMATE_PRESET_NONE = 0; + CLIMATE_PRESET_HOME = 1; + CLIMATE_PRESET_AWAY = 2; + CLIMATE_PRESET_BOOST = 3; + CLIMATE_PRESET_COMFORT = 4; + CLIMATE_PRESET_ECO = 5; + CLIMATE_PRESET_SLEEP = 6; + CLIMATE_PRESET_ACTIVITY = 7; +} +message ListEntitiesClimateResponse { + option (id) = 46; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_CLIMATE"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + bool supports_current_temperature = 5; + bool supports_two_point_target_temperature = 6; + repeated ClimateMode supported_modes = 7; + float visual_min_temperature = 8; + float visual_max_temperature = 9; + float visual_target_temperature_step = 10; + // for older peer versions - in new system this + // is if CLIMATE_PRESET_AWAY exists is supported_presets + bool legacy_supports_away = 11; + bool supports_action = 12; + repeated ClimateFanMode supported_fan_modes = 13; + repeated ClimateSwingMode supported_swing_modes = 14; + repeated string supported_custom_fan_modes = 15; + repeated ClimatePreset supported_presets = 16; + repeated string supported_custom_presets = 17; + bool disabled_by_default = 18; + string icon = 19; + EntityCategory entity_category = 20; + float visual_current_temperature_step = 21; + bool supports_current_humidity = 22; + bool supports_target_humidity = 23; + float visual_min_humidity = 24; + float visual_max_humidity = 25; + uint32 device_id = 26; +} +message ClimateStateResponse { + option (id) = 47; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_CLIMATE"; + option (no_delay) = true; + + fixed32 key = 1; + ClimateMode mode = 2; + float current_temperature = 3; + float target_temperature = 4; + float target_temperature_low = 5; + float target_temperature_high = 6; + // For older peers, equal to preset == CLIMATE_PRESET_AWAY + bool unused_legacy_away = 7; + ClimateAction action = 8; + ClimateFanMode fan_mode = 9; + ClimateSwingMode swing_mode = 10; + string custom_fan_mode = 11; + ClimatePreset preset = 12; + string custom_preset = 13; + float current_humidity = 14; + float target_humidity = 15; + uint32 device_id = 16; +} +message ClimateCommandRequest { + option (id) = 48; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_CLIMATE"; + option (no_delay) = true; + + fixed32 key = 1; + bool has_mode = 2; + ClimateMode mode = 3; + bool has_target_temperature = 4; + float target_temperature = 5; + bool has_target_temperature_low = 6; + float target_temperature_low = 7; + bool has_target_temperature_high = 8; + float target_temperature_high = 9; + // legacy, for older peers, newer ones should use CLIMATE_PRESET_AWAY in preset + bool unused_has_legacy_away = 10; + bool unused_legacy_away = 11; + bool has_fan_mode = 12; + ClimateFanMode fan_mode = 13; + bool has_swing_mode = 14; + ClimateSwingMode swing_mode = 15; + bool has_custom_fan_mode = 16; + string custom_fan_mode = 17; + bool has_preset = 18; + ClimatePreset preset = 19; + bool has_custom_preset = 20; + string custom_preset = 21; + bool has_target_humidity = 22; + float target_humidity = 23; +} + +// ==================== NUMBER ==================== +enum NumberMode { + NUMBER_MODE_AUTO = 0; + NUMBER_MODE_BOX = 1; + NUMBER_MODE_SLIDER = 2; +} +message ListEntitiesNumberResponse { + option (id) = 49; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_NUMBER"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + float min_value = 6; + float max_value = 7; + float step = 8; + bool disabled_by_default = 9; + EntityCategory entity_category = 10; + string unit_of_measurement = 11; + NumberMode mode = 12; + string device_class = 13; + uint32 device_id = 14; +} +message NumberStateResponse { + option (id) = 50; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_NUMBER"; + option (no_delay) = true; + + fixed32 key = 1; + float state = 2; + // If the number does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; + uint32 device_id = 4; +} +message NumberCommandRequest { + option (id) = 51; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_NUMBER"; + option (no_delay) = true; + + fixed32 key = 1; + float state = 2; +} + +// ==================== SELECT ==================== +message ListEntitiesSelectResponse { + option (id) = 52; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SELECT"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + repeated string options = 6; + bool disabled_by_default = 7; + EntityCategory entity_category = 8; + uint32 device_id = 9; +} +message SelectStateResponse { + option (id) = 53; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SELECT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; + // If the select does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; + uint32 device_id = 4; +} +message SelectCommandRequest { + option (id) = 54; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SELECT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; +} + +// ==================== SIREN ==================== +message ListEntitiesSirenResponse { + option (id) = 55; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SIREN"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + repeated string tones = 7; + bool supports_duration = 8; + bool supports_volume = 9; + EntityCategory entity_category = 10; + uint32 device_id = 11; +} +message SirenStateResponse { + option (id) = 56; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SIREN"; + option (no_delay) = true; + + fixed32 key = 1; + bool state = 2; + uint32 device_id = 3; +} +message SirenCommandRequest { + option (id) = 57; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SIREN"; + option (no_delay) = true; + + fixed32 key = 1; + bool has_state = 2; + bool state = 3; + bool has_tone = 4; + string tone = 5; + bool has_duration = 6; + uint32 duration = 7; + bool has_volume = 8; + float volume = 9; +} + +// ==================== LOCK ==================== +enum LockState { + LOCK_STATE_NONE = 0; + LOCK_STATE_LOCKED = 1; + LOCK_STATE_UNLOCKED = 2; + LOCK_STATE_JAMMED = 3; + LOCK_STATE_LOCKING = 4; + LOCK_STATE_UNLOCKING = 5; +} +enum LockCommand { + LOCK_UNLOCK = 0; + LOCK_LOCK = 1; + LOCK_OPEN = 2; +} +message ListEntitiesLockResponse { + option (id) = 58; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_LOCK"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + bool assumed_state = 8; + + bool supports_open = 9; + bool requires_code = 10; + + // Not yet implemented: + string code_format = 11; + uint32 device_id = 12; +} +message LockStateResponse { + option (id) = 59; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_LOCK"; + option (no_delay) = true; + fixed32 key = 1; + LockState state = 2; + uint32 device_id = 3; +} +message LockCommandRequest { + option (id) = 60; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_LOCK"; + option (no_delay) = true; + fixed32 key = 1; + LockCommand command = 2; + + // Not yet implemented: + bool has_code = 3; + string code = 4; +} + +// ==================== BUTTON ==================== +message ListEntitiesButtonResponse { + option (id) = 61; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BUTTON"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + string device_class = 8; + uint32 device_id = 9; +} +message ButtonCommandRequest { + option (id) = 62; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BUTTON"; + option (no_delay) = true; + + fixed32 key = 1; +} + +// ==================== MEDIA PLAYER ==================== +enum MediaPlayerState { + MEDIA_PLAYER_STATE_NONE = 0; + MEDIA_PLAYER_STATE_IDLE = 1; + MEDIA_PLAYER_STATE_PLAYING = 2; + MEDIA_PLAYER_STATE_PAUSED = 3; +} +enum MediaPlayerCommand { + MEDIA_PLAYER_COMMAND_PLAY = 0; + MEDIA_PLAYER_COMMAND_PAUSE = 1; + MEDIA_PLAYER_COMMAND_STOP = 2; + MEDIA_PLAYER_COMMAND_MUTE = 3; + MEDIA_PLAYER_COMMAND_UNMUTE = 4; +} +enum MediaPlayerFormatPurpose { + MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0; + MEDIA_PLAYER_FORMAT_PURPOSE_ANNOUNCEMENT = 1; +} +message MediaPlayerSupportedFormat { + option (ifdef) = "USE_MEDIA_PLAYER"; + + string format = 1; + uint32 sample_rate = 2; + uint32 num_channels = 3; + MediaPlayerFormatPurpose purpose = 4; + uint32 sample_bytes = 5; +} +message ListEntitiesMediaPlayerResponse { + option (id) = 63; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_MEDIA_PLAYER"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + + bool supports_pause = 8; + + repeated MediaPlayerSupportedFormat supported_formats = 9; + + uint32 device_id = 10; +} +message MediaPlayerStateResponse { + option (id) = 64; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_MEDIA_PLAYER"; + option (no_delay) = true; + fixed32 key = 1; + MediaPlayerState state = 2; + float volume = 3; + bool muted = 4; + uint32 device_id = 5; +} +message MediaPlayerCommandRequest { + option (id) = 65; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_MEDIA_PLAYER"; + option (no_delay) = true; + + fixed32 key = 1; + + bool has_command = 2; + MediaPlayerCommand command = 3; + + bool has_volume = 4; + float volume = 5; + + bool has_media_url = 6; + string media_url = 7; + + bool has_announcement = 8; + bool announcement = 9; +} + +// ==================== BLUETOOTH ==================== +message SubscribeBluetoothLEAdvertisementsRequest { + option (id) = 66; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint32 flags = 1; +} + +message BluetoothServiceData { + string uuid = 1; + repeated uint32 legacy_data = 2 [deprecated = true]; // Removed in api version 1.7 + bytes data = 3; // Added in api version 1.7 +} +message BluetoothLEAdvertisementResponse { + option (id) = 67; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + option (no_delay) = true; + + uint64 address = 1; + bytes name = 2; + sint32 rssi = 3; + + repeated string service_uuids = 4; + repeated BluetoothServiceData service_data = 5; + repeated BluetoothServiceData manufacturer_data = 6; + + uint32 address_type = 7; +} + +message BluetoothLERawAdvertisement { + uint64 address = 1; + sint32 rssi = 2; + uint32 address_type = 3; + + bytes data = 4; +} + +message BluetoothLERawAdvertisementsResponse { + option (id) = 93; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + option (no_delay) = true; + + repeated BluetoothLERawAdvertisement advertisements = 1; +} + +enum BluetoothDeviceRequestType { + BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; + BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; + BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2; + BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3; + BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE = 4; + BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE = 5; + BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE = 6; +} + +message BluetoothDeviceRequest { + option (id) = 68; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + BluetoothDeviceRequestType request_type = 2; + bool has_address_type = 3; + uint32 address_type = 4; +} + +message BluetoothDeviceConnectionResponse { + option (id) = 69; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + bool connected = 2; + uint32 mtu = 3; + int32 error = 4; +} + +message BluetoothGATTGetServicesRequest { + option (id) = 70; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; +} + +message BluetoothGATTDescriptor { + repeated uint64 uuid = 1; + uint32 handle = 2; +} + +message BluetoothGATTCharacteristic { + repeated uint64 uuid = 1; + uint32 handle = 2; + uint32 properties = 3; + repeated BluetoothGATTDescriptor descriptors = 4; +} + +message BluetoothGATTService { + repeated uint64 uuid = 1; + uint32 handle = 2; + repeated BluetoothGATTCharacteristic characteristics = 3; +} + +message BluetoothGATTGetServicesResponse { + option (id) = 71; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + repeated BluetoothGATTService services = 2; +} + +message BluetoothGATTGetServicesDoneResponse { + option (id) = 72; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; +} + +message BluetoothGATTReadRequest { + option (id) = 73; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; +} + +message BluetoothGATTReadResponse { + option (id) = 74; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + + bytes data = 3; + +} + +message BluetoothGATTWriteRequest { + option (id) = 75; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + bool response = 3; + + bytes data = 4; +} + +message BluetoothGATTReadDescriptorRequest { + option (id) = 76; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; +} + +message BluetoothGATTWriteDescriptorRequest { + option (id) = 77; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + + bytes data = 3; +} + +message BluetoothGATTNotifyRequest { + option (id) = 78; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + bool enable = 3; +} + +message BluetoothGATTNotifyDataResponse { + option (id) = 79; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + + bytes data = 3; +} + +message SubscribeBluetoothConnectionsFreeRequest { + option (id) = 80; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; +} + +message BluetoothConnectionsFreeResponse { + option (id) = 81; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint32 free = 1; + uint32 limit = 2; + repeated uint64 allocated = 3; +} + +message BluetoothGATTErrorResponse { + option (id) = 82; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; + int32 error = 3; +} + +message BluetoothGATTWriteResponse { + option (id) = 83; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; +} + +message BluetoothGATTNotifyResponse { + option (id) = 84; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 handle = 2; +} + +message BluetoothDevicePairingResponse { + option (id) = 85; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + bool paired = 2; + int32 error = 3; +} + +message BluetoothDeviceUnpairingResponse { + option (id) = 86; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + bool success = 2; + int32 error = 3; +} + +message UnsubscribeBluetoothLEAdvertisementsRequest { + option (id) = 87; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; +} + +message BluetoothDeviceClearCacheResponse { + option (id) = 88; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + bool success = 2; + int32 error = 3; +} + +enum BluetoothScannerState { + BLUETOOTH_SCANNER_STATE_IDLE = 0; + BLUETOOTH_SCANNER_STATE_STARTING = 1; + BLUETOOTH_SCANNER_STATE_RUNNING = 2; + BLUETOOTH_SCANNER_STATE_FAILED = 3; + BLUETOOTH_SCANNER_STATE_STOPPING = 4; + BLUETOOTH_SCANNER_STATE_STOPPED = 5; +} + +enum BluetoothScannerMode { + BLUETOOTH_SCANNER_MODE_PASSIVE = 0; + BLUETOOTH_SCANNER_MODE_ACTIVE = 1; +} + +message BluetoothScannerStateResponse { + option(id) = 126; + option(source) = SOURCE_SERVER; + option(ifdef) = "USE_BLUETOOTH_PROXY"; + + BluetoothScannerState state = 1; + BluetoothScannerMode mode = 2; +} + +message BluetoothScannerSetModeRequest { + option(id) = 127; + option(source) = SOURCE_CLIENT; + option(ifdef) = "USE_BLUETOOTH_PROXY"; + + BluetoothScannerMode mode = 1; +} + +// ==================== VOICE ASSISTANT ==================== +enum VoiceAssistantSubscribeFlag { + VOICE_ASSISTANT_SUBSCRIBE_NONE = 0; + VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1; +} + +message SubscribeVoiceAssistantRequest { + option (id) = 89; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + bool subscribe = 1; + uint32 flags = 2; +} + +enum VoiceAssistantRequestFlag { + VOICE_ASSISTANT_REQUEST_NONE = 0; + VOICE_ASSISTANT_REQUEST_USE_VAD = 1; + VOICE_ASSISTANT_REQUEST_USE_WAKE_WORD = 2; +} + +message VoiceAssistantAudioSettings { + uint32 noise_suppression_level = 1; + uint32 auto_gain = 2; + float volume_multiplier = 3; +} + +message VoiceAssistantRequest { + option (id) = 90; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + bool start = 1; + string conversation_id = 2; + uint32 flags = 3; + VoiceAssistantAudioSettings audio_settings = 4; + string wake_word_phrase = 5; +} + +message VoiceAssistantResponse { + option (id) = 91; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + uint32 port = 1; + bool error = 2; +} + +enum VoiceAssistantEvent { + VOICE_ASSISTANT_ERROR = 0; + VOICE_ASSISTANT_RUN_START = 1; + VOICE_ASSISTANT_RUN_END = 2; + VOICE_ASSISTANT_STT_START = 3; + VOICE_ASSISTANT_STT_END = 4; + VOICE_ASSISTANT_INTENT_START = 5; + VOICE_ASSISTANT_INTENT_END = 6; + VOICE_ASSISTANT_TTS_START = 7; + VOICE_ASSISTANT_TTS_END = 8; + VOICE_ASSISTANT_WAKE_WORD_START = 9; + VOICE_ASSISTANT_WAKE_WORD_END = 10; + VOICE_ASSISTANT_STT_VAD_START = 11; + VOICE_ASSISTANT_STT_VAD_END = 12; + VOICE_ASSISTANT_TTS_STREAM_START = 98; + VOICE_ASSISTANT_TTS_STREAM_END = 99; + VOICE_ASSISTANT_INTENT_PROGRESS = 100; +} + +message VoiceAssistantEventData { + string name = 1; + string value = 2; +} + +message VoiceAssistantEventResponse { + option (id) = 92; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + VoiceAssistantEvent event_type = 1; + repeated VoiceAssistantEventData data = 2; +} + +message VoiceAssistantAudio { + option (id) = 106; + option (source) = SOURCE_BOTH; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + bytes data = 1; + bool end = 2; +} + +enum VoiceAssistantTimerEvent { + VOICE_ASSISTANT_TIMER_STARTED = 0; + VOICE_ASSISTANT_TIMER_UPDATED = 1; + VOICE_ASSISTANT_TIMER_CANCELLED = 2; + VOICE_ASSISTANT_TIMER_FINISHED = 3; +} + +message VoiceAssistantTimerEventResponse { + option (id) = 115; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + VoiceAssistantTimerEvent event_type = 1; + string timer_id = 2; + string name = 3; + uint32 total_seconds = 4; + uint32 seconds_left = 5; + bool is_active = 6; +} + +message VoiceAssistantAnnounceRequest { + option (id) = 119; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + string media_id = 1; + string text = 2; + string preannounce_media_id = 3; + bool start_conversation = 4; +} + +message VoiceAssistantAnnounceFinished { + option (id) = 120; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + bool success = 1; +} + +message VoiceAssistantWakeWord { + string id = 1; + string wake_word = 2; + repeated string trained_languages = 3; +} + +message VoiceAssistantConfigurationRequest { + option (id) = 121; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; +} + +message VoiceAssistantConfigurationResponse { + option (id) = 122; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + repeated VoiceAssistantWakeWord available_wake_words = 1; + repeated string active_wake_words = 2; + uint32 max_active_wake_words = 3; +} + +message VoiceAssistantSetConfiguration { + option (id) = 123; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VOICE_ASSISTANT"; + + repeated string active_wake_words = 1; +} + +// ==================== ALARM CONTROL PANEL ==================== +enum AlarmControlPanelState { + ALARM_STATE_DISARMED = 0; + ALARM_STATE_ARMED_HOME = 1; + ALARM_STATE_ARMED_AWAY = 2; + ALARM_STATE_ARMED_NIGHT = 3; + ALARM_STATE_ARMED_VACATION = 4; + ALARM_STATE_ARMED_CUSTOM_BYPASS = 5; + ALARM_STATE_PENDING = 6; + ALARM_STATE_ARMING = 7; + ALARM_STATE_DISARMING = 8; + ALARM_STATE_TRIGGERED = 9; +} + +enum AlarmControlPanelStateCommand { + ALARM_CONTROL_PANEL_DISARM = 0; + ALARM_CONTROL_PANEL_ARM_AWAY = 1; + ALARM_CONTROL_PANEL_ARM_HOME = 2; + ALARM_CONTROL_PANEL_ARM_NIGHT = 3; + ALARM_CONTROL_PANEL_ARM_VACATION = 4; + ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5; + ALARM_CONTROL_PANEL_TRIGGER = 6; +} + +message ListEntitiesAlarmControlPanelResponse { + option (id) = 94; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + uint32 supported_features = 8; + bool requires_code = 9; + bool requires_code_to_arm = 10; + uint32 device_id = 11; +} + +message AlarmControlPanelStateResponse { + option (id) = 95; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + option (no_delay) = true; + fixed32 key = 1; + AlarmControlPanelState state = 2; + uint32 device_id = 3; +} + +message AlarmControlPanelCommandRequest { + option (id) = 96; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_ALARM_CONTROL_PANEL"; + option (no_delay) = true; + fixed32 key = 1; + AlarmControlPanelStateCommand command = 2; + string code = 3; +} + +// ===================== TEXT ===================== +enum TextMode { + TEXT_MODE_TEXT = 0; + TEXT_MODE_PASSWORD = 1; +} +message ListEntitiesTextResponse { + option (id) = 97; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_TEXT"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + + uint32 min_length = 8; + uint32 max_length = 9; + string pattern = 10; + TextMode mode = 11; + uint32 device_id = 12; +} +message TextStateResponse { + option (id) = 98; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_TEXT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; + // If the Text does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 3; + uint32 device_id = 4; +} +message TextCommandRequest { + option (id) = 99; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_TEXT"; + option (no_delay) = true; + + fixed32 key = 1; + string state = 2; +} + + +// ==================== DATETIME DATE ==================== +message ListEntitiesDateResponse { + option (id) = 100; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATE"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + uint32 device_id = 8; +} +message DateStateResponse { + option (id) = 101; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATE"; + option (no_delay) = true; + + fixed32 key = 1; + // If the date does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 2; + uint32 year = 3; + uint32 month = 4; + uint32 day = 5; + uint32 device_id = 6; +} +message DateCommandRequest { + option (id) = 102; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_DATETIME_DATE"; + option (no_delay) = true; + + fixed32 key = 1; + uint32 year = 2; + uint32 month = 3; + uint32 day = 4; +} + +// ==================== DATETIME TIME ==================== +message ListEntitiesTimeResponse { + option (id) = 103; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_TIME"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + uint32 device_id = 8; +} +message TimeStateResponse { + option (id) = 104; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_TIME"; + option (no_delay) = true; + + fixed32 key = 1; + // If the time does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 2; + uint32 hour = 3; + uint32 minute = 4; + uint32 second = 5; + uint32 device_id = 6; +} +message TimeCommandRequest { + option (id) = 105; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_DATETIME_TIME"; + option (no_delay) = true; + + fixed32 key = 1; + uint32 hour = 2; + uint32 minute = 3; + uint32 second = 4; +} + +// ==================== EVENT ==================== +message ListEntitiesEventResponse { + option (id) = 107; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_EVENT"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + string device_class = 8; + + repeated string event_types = 9; + uint32 device_id = 10; +} +message EventResponse { + option (id) = 108; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_EVENT"; + + fixed32 key = 1; + string event_type = 2; + uint32 device_id = 3; +} + +// ==================== VALVE ==================== +message ListEntitiesValveResponse { + option (id) = 109; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VALVE"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + string device_class = 8; + + bool assumed_state = 9; + bool supports_position = 10; + bool supports_stop = 11; + uint32 device_id = 12; +} + +enum ValveOperation { + VALVE_OPERATION_IDLE = 0; + VALVE_OPERATION_IS_OPENING = 1; + VALVE_OPERATION_IS_CLOSING = 2; +} +message ValveStateResponse { + option (id) = 110; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_VALVE"; + option (no_delay) = true; + + fixed32 key = 1; + float position = 2; + ValveOperation current_operation = 3; + uint32 device_id = 4; +} + +message ValveCommandRequest { + option (id) = 111; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_VALVE"; + option (no_delay) = true; + + fixed32 key = 1; + bool has_position = 2; + float position = 3; + bool stop = 4; +} + +// ==================== DATETIME DATETIME ==================== +message ListEntitiesDateTimeResponse { + option (id) = 112; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATETIME"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + uint32 device_id = 8; +} +message DateTimeStateResponse { + option (id) = 113; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_DATETIME_DATETIME"; + option (no_delay) = true; + + fixed32 key = 1; + // If the datetime does not have a valid state yet. + // Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller + bool missing_state = 2; + fixed32 epoch_seconds = 3; + uint32 device_id = 4; +} +message DateTimeCommandRequest { + option (id) = 114; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_DATETIME_DATETIME"; + option (no_delay) = true; + + fixed32 key = 1; + fixed32 epoch_seconds = 2; +} + +// ==================== UPDATE ==================== +message ListEntitiesUpdateResponse { + option (id) = 116; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_UPDATE"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string unique_id = 4; + + string icon = 5; + bool disabled_by_default = 6; + EntityCategory entity_category = 7; + string device_class = 8; + uint32 device_id = 9; +} +message UpdateStateResponse { + option (id) = 117; + option (base_class) = "StateResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_UPDATE"; + option (no_delay) = true; + + fixed32 key = 1; + bool missing_state = 2; + bool in_progress = 3; + bool has_progress = 4; + float progress = 5; + string current_version = 6; + string latest_version = 7; + string title = 8; + string release_summary = 9; + string release_url = 10; + uint32 device_id = 11; +} +enum UpdateCommand { + UPDATE_COMMAND_NONE = 0; + UPDATE_COMMAND_UPDATE = 1; + UPDATE_COMMAND_CHECK = 2; +} +message UpdateCommandRequest { + option (id) = 118; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_UPDATE"; + option (no_delay) = true; + + fixed32 key = 1; + UpdateCommand command = 2; +} diff --git a/sachy-esphome/protos/api_options.proto b/sachy-esphome/protos/api_options.proto new file mode 100644 index 0000000..2f906cb --- /dev/null +++ b/sachy-esphome/protos/api_options.proto @@ -0,0 +1,25 @@ +syntax = "proto2"; + +import "google/protobuf/descriptor.proto"; + +enum APISourceType { + SOURCE_BOTH = 0; + SOURCE_SERVER = 1; + SOURCE_CLIENT = 2; +} + +message void {} + +extend google.protobuf.MethodOptions { + optional bool needs_setup_connection = 1038 [default=true]; + optional bool needs_authentication = 1039 [default=true]; +} + +extend google.protobuf.MessageOptions { + optional uint32 id = 1036 [default=0]; + optional APISourceType source = 1037 [default=SOURCE_BOTH]; + optional string ifdef = 1038; + optional bool log = 1039 [default=true]; + optional bool no_delay = 1040 [default=false]; + optional string base_class = 1041; +} diff --git a/sachy-esphome/src/lib.rs b/sachy-esphome/src/lib.rs new file mode 100644 index 0000000..3fdaf94 --- /dev/null +++ b/sachy-esphome/src/lib.rs @@ -0,0 +1,166 @@ +#![no_std] + +use core::ops::{Index, RangeTo}; + +use num_traits::FromPrimitive; +pub use prost; +use prost::{bytes::Buf, bytes::BufMut}; +use thiserror::Error; + +#[allow(deprecated)] +pub mod api { + use prost::Message as ProstMessage; + include!(concat!(env!("OUT_DIR"), "/api.rs")); +} + +pub use api::MessageKind; + +use crate::api::Message; + +#[derive(Error, Debug)] +pub enum EspHomeError { + #[error("First byte of message should be zero")] + InvalidStartByte, + + #[error("Buffer is too small")] + BufferTooSmall, + + #[error("Unable to decode protobuf")] + Decode(prost::DecodeError), + + #[error("Unable to encode protobuf")] + Encode(prost::EncodeError), + + #[error("Unknown message kind: {0}")] + UnknownMessageKind(u64), +} + +#[cfg(feature = "defmt")] +impl defmt::Format for EspHomeError { + fn format(&self, fmt: defmt::Formatter) { + match self { + EspHomeError::InvalidStartByte => defmt::write!(fmt, "InvalidStartByte"), + EspHomeError::BufferTooSmall => defmt::write!(fmt, "BufferTooSmal"), + EspHomeError::Decode(_) => defmt::write!(fmt, "DecodeError"), + EspHomeError::Encode(_) => defmt::write!(fmt, "EncodeError"), + EspHomeError::UnknownMessageKind(msg) => { + defmt::write!(fmt, "UnknownMessageKind({})", msg) + } + } + } +} + +impl From for EspHomeError { + #[inline] + fn from(value: prost::DecodeError) -> Self { + Self::Decode(value) + } +} + +impl From for EspHomeError { + #[inline] + fn from(value: prost::EncodeError) -> Self { + Self::Encode(value) + } +} + +pub type EspHomeResult = core::result::Result; + +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +struct EspHomeHeader { + _type: api::MessageKind, + size: u64, +} + +impl EspHomeHeader { + pub fn encode_header(&self, buffer: &mut impl BufMut) -> EspHomeResult<()> { + buffer.put_u8(0); + + try_encode_esp_varint(self.size, buffer)?; + try_encode_esp_varint(self._type as u64, buffer) + } + + pub fn decode(buffer: &mut &[u8]) -> EspHomeResult { + if buffer.is_empty() { + return Err(EspHomeError::BufferTooSmall); + } + + if buffer.get_u8() != 0 { + return Err(EspHomeError::InvalidStartByte); + } + + let size = prost::encoding::decode_varint(buffer)?; + let kind = prost::encoding::decode_varint(buffer)?; + + Ok(EspHomeHeader { + _type: api::MessageKind::from_u64(kind) + .ok_or(EspHomeError::UnknownMessageKind(kind))?, + size, + }) + } +} + +pub fn decode_esp_request(buffer: &mut &[u8]) -> EspHomeResult { + let header = EspHomeHeader::decode(buffer)?; + + if header.size as usize > buffer.len() { + return Err(EspHomeError::BufferTooSmall); + } + + let body = api::Message::decode(header._type, &mut &buffer[..header.size as usize])?; + + buffer.advance(header.size as usize); + + Ok(body) +} + +pub struct EspEncoder<'any, B> { + buffer: &'any mut B, + written: usize, +} + +impl<'any, B> EspEncoder<'any, B> +where + B: BufMut + Index, Output = [u8]>, +{ + pub fn new(buffer: &'any mut B) -> Self { + Self { buffer, written: 0 } + } + + pub fn encode(mut self, body: Message) -> EspHomeResult { + let remaining = self.buffer.remaining_mut(); + + let header = EspHomeHeader { + _type: MessageKind::from(&body), + size: body.encoded_len() as u64, + }; + + header.encode_header(self.buffer)?; + + if self.buffer.remaining_mut() < header.size as usize { + return Err(EspHomeError::BufferTooSmall); + } + + body.encode(self.buffer)?; + + self.written += remaining - self.buffer.remaining_mut(); + + Ok(self) + } + + pub fn finish(self) -> &'any [u8] { + &self.buffer[..self.written] + } +} + +fn try_encode_esp_varint(value: u64, buf: &mut impl BufMut) -> EspHomeResult<()> { + let len = prost::encoding::encoded_len_varint(value); + + if buf.remaining_mut() < len { + return Err(EspHomeError::BufferTooSmall); + } + + prost::encoding::encode_varint(value, buf); + + Ok(()) +} diff --git a/sachy-esphome/templates/message.handlebars b/sachy-esphome/templates/message.handlebars new file mode 100644 index 0000000..e520825 --- /dev/null +++ b/sachy-esphome/templates/message.handlebars @@ -0,0 +1,66 @@ +#[derive(num_derive::FromPrimitive, num_derive::ToPrimitive, Copy, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +#[repr(u64)] +pub enum MessageKind { + {{#each this}} + {{ name }} = {{ id }}, + {{/each}} +} + +#[derive(Debug, Clone)] +#[cfg_attr(feature = "defmt", derive(defmt::Format))] +pub enum Message { + {{#each this}} + {{ name }}({{ name }}), + {{/each}} +} + +impl Message { + pub fn decode(message_kind: MessageKind, buffer: &mut &[u8]) -> Result { + match message_kind { + {{#each this}} + MessageKind::{{ name }} => Ok(Self::{{ name }}({{name}}::decode(buffer)?)), + {{/each}} + } + } + + pub fn encoded_len(&self) -> usize { + match self { + {{#each this}} + Self::{{ name }}(message) => message.encoded_len(), + {{/each}} + } + } + + pub fn encode(&self, buf: &mut impl prost::bytes::BufMut) -> Result<(), prost::EncodeError> { + match self { + {{#each this}} + Self::{{ name }}(message) => message.encode(buf), + {{/each}} + } + } +} + +impl From<&Message> for MessageKind { + fn from(value: &Message) -> Self { + match value { + {{#each this}} + Message::{{ name }}(message) => Self::from(message), + {{/each}} + } + } +} + +{{#each this}} +impl From<&{{ name }}> for MessageKind { + fn from(_value: &{{name}}) -> Self { + Self::{{name}} + } +} + +impl From<{{ name }}> for Message { + fn from(value: {{ name }}) -> Self { + Message::{{ name }}(value) + } +} +{{/each}} -- 2.52.0