mirror of
https://github.com/esphome/esphome.git
synced 2026-10-14 18:22:48 +00:00
Compare commits
550
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74847eeccc | ||
|
|
d9ae0e2cd4 | ||
|
|
857564a23d | ||
|
|
6c5010032b | ||
|
|
746691d5c2 | ||
|
|
93ec8d9d95 | ||
|
|
dea1200c35 | ||
|
|
abd95c5154 | ||
|
|
cdc77e2126 | ||
|
|
04a53264b4 | ||
|
|
499ce56c2b | ||
|
|
72ac32a2c4 | ||
|
|
a7e0c0f8a2 | ||
|
|
1ea45be19e | ||
|
|
7d5117e570 | ||
|
|
8132fbef52 | ||
|
|
9744058201 | ||
|
|
1de5a58dbe | ||
|
|
d2c33b0f6f | ||
|
|
0d445fb3bc | ||
|
|
76de6a672c | ||
|
|
2dc7a64e52 | ||
|
|
a61746045b | ||
|
|
a35299af0b | ||
|
|
3f2de92e5d | ||
|
|
b8c32854ed | ||
|
|
0b2e5bfbbe | ||
|
|
a1d8ea6072 | ||
|
|
93fabe8b84 | ||
|
|
2fc48a3d9e | ||
|
|
1a158e5ba0 | ||
|
|
0569a1262f | ||
|
|
0b5a1748f7 | ||
|
|
d71a6e662b | ||
|
|
c8d3f43712 | ||
|
|
01a2aaef08 | ||
|
|
447348d617 | ||
|
|
c902083ef3 | ||
|
|
d1dd4e53ce | ||
|
|
c0c1643efe | ||
|
|
532a6ed239 | ||
|
|
3a9048cc62 | ||
|
|
d34b21cb7e | ||
|
|
5043e12690 | ||
|
|
3e85af29c6 | ||
|
|
1889f6c9b5 | ||
|
|
2f4eb3e1ee | ||
|
|
bcae1f28ef | ||
|
|
87b10d70a5 | ||
|
|
cfb7d8089e | ||
|
|
bfe1f7bd17 | ||
|
|
d5d0901e2c | ||
|
|
64388318e8 | ||
|
|
e83dc25301 | ||
|
|
a81cf8f5cc | ||
|
|
6925c5127e | ||
|
|
d96d0b80f7 | ||
|
|
27ea1a90e3 | ||
|
|
4f47913745 | ||
|
|
447a30da0f | ||
|
|
d8696c4f28 | ||
|
|
0ef68c17fd | ||
|
|
aef3a586de | ||
|
|
5aeab5f361 | ||
|
|
9654dd3912 | ||
|
|
6622e1fe64 | ||
|
|
0c548f77fa | ||
|
|
dfd9d2639f | ||
|
|
cf9d807bf0 | ||
|
|
29512cf323 | ||
|
|
456e13adee | ||
|
|
a1b0e85a9e | ||
|
|
14bd50d469 | ||
|
|
1edc327835 | ||
|
|
288a7b376a | ||
|
|
c16f75f0e2 | ||
|
|
432cae58ae | ||
|
|
11c8e8c0c0 | ||
|
|
6a115e6b25 | ||
|
|
9c2616ef7e | ||
|
|
9ae83f2830 | ||
|
|
340e105a76 | ||
|
|
857a519946 | ||
|
|
15d756d559 | ||
|
|
22035cb9f5 | ||
|
|
32624e248c | ||
|
|
ad6996a63c | ||
|
|
262ade0598 | ||
|
|
2157ff5b4b | ||
|
|
63796e74bb | ||
|
|
a088eff428 | ||
|
|
0cb421f793 | ||
|
|
23c317f163 | ||
|
|
11e016be59 | ||
|
|
bdb492c196 | ||
|
|
d6831c8c75 | ||
|
|
c73a71e037 | ||
|
|
4bb6c0f877 | ||
|
|
eb85778a60 | ||
|
|
c561c6bbe8 | ||
|
|
7294da9f49 | ||
|
|
96d0d677b6 | ||
|
|
c598d2dee5 | ||
|
|
73548b0f15 | ||
|
|
9a9169305e | ||
|
|
54d05180b7 | ||
|
|
1dcf0a0f9d | ||
|
|
bb84409261 | ||
|
|
044c4cb9bb | ||
|
|
d346835e79 | ||
|
|
db566b25d0 | ||
|
|
45bcff2c2c | ||
|
|
af4a8dac8e | ||
|
|
d3fd124c59 | ||
|
|
2d8e43ef78 | ||
|
|
370d76d013 | ||
|
|
3fb48e0bfa | ||
|
|
a17285afc7 | ||
|
|
f7dd9c21c5 | ||
|
|
898ab7c701 | ||
|
|
28343e1bc9 | ||
|
|
6fa56e18bd | ||
|
|
cc5ed93247 | ||
|
|
91210c5ef5 | ||
|
|
8db901cbfc | ||
|
|
4a2f17bc8b | ||
|
|
253793c4df | ||
|
|
3ef0acd4b2 | ||
|
|
73ca04579c | ||
|
|
4ccc5fa37e | ||
|
|
f70880ce96 | ||
|
|
057bf88e4a | ||
|
|
16757a84e6 | ||
|
|
a9111a8993 | ||
|
|
211fd3ce54 | ||
|
|
9df7e825d3 | ||
|
|
e63428ddce | ||
|
|
f8e8295088 | ||
|
|
9f5ac44db1 | ||
|
|
6f71b9102b | ||
|
|
04da1827e3 | ||
|
|
d2fefe2299 | ||
|
|
f1f83cda07 | ||
|
|
7dc98c683b | ||
|
|
071fc08e32 | ||
|
|
c27cbb132d | ||
|
|
7e308bb83d | ||
|
|
68235f34c5 | ||
|
|
b59518f6ab | ||
|
|
da148bedfe | ||
|
|
b0c9a7d5bd | ||
|
|
22177680c4 | ||
|
|
e92b1e27ca | ||
|
|
fed5dd6a9d | ||
|
|
cbd985826c | ||
|
|
2ce2b7b25c | ||
|
|
acc9324fa8 | ||
|
|
cbb4c49011 | ||
|
|
afc5c29569 | ||
|
|
993c96875c | ||
|
|
75360a5d52 | ||
|
|
8b11d3da39 | ||
|
|
5790181f44 | ||
|
|
4780936867 | ||
|
|
22959ea1cf | ||
|
|
e9aecb5fe8 | ||
|
|
6768cb818b | ||
|
|
1861d8282f | ||
|
|
c39ffd7099 | ||
|
|
1671bb6dd3 | ||
|
|
0ceefd5f91 | ||
|
|
4477e35d66 | ||
|
|
e98c4bd375 | ||
|
|
eae8ff7f91 | ||
|
|
f24e1f31f2 | ||
|
|
4cbced407d | ||
|
|
64127632f5 | ||
|
|
cd5ff787f4 | ||
|
|
698eb71862 | ||
|
|
e1126a152e | ||
|
|
807556171b | ||
|
|
f4300d9cee | ||
|
|
e3951f2ab4 | ||
|
|
438307f548 | ||
|
|
b7b1507daf | ||
|
|
1be1f67516 | ||
|
|
21dc6af22e | ||
|
|
20703f32bb | ||
|
|
2ddc6a0741 | ||
|
|
c1d82168d8 | ||
|
|
281c54bcb8 | ||
|
|
a47271b1f1 | ||
|
|
22efbd9778 | ||
|
|
6a97172867 | ||
|
|
c275365258 | ||
|
|
4efeee245c | ||
|
|
bd91d3ff23 | ||
|
|
2a26d5ab62 | ||
|
|
f58fe2e3ea | ||
|
|
07c8cfdde3 | ||
|
|
802f2b9f24 | ||
|
|
7df946b665 | ||
|
|
aaed14d018 | ||
|
|
62b501049f | ||
|
|
606525ed18 | ||
|
|
bd1efeae94 | ||
|
|
0110215e31 | ||
|
|
170bb02fa3 | ||
|
|
4f6af3cfab | ||
|
|
8f14adaa0c | ||
|
|
761dd9dd9c | ||
|
|
2e809c3626 | ||
|
|
bbd8472b66 | ||
|
|
01f9416a79 | ||
|
|
4e2d1cd7fc | ||
|
|
5e8f620362 | ||
|
|
9d87fcd3b7 | ||
|
|
fa7ccc934e | ||
|
|
4a7fe83a17 | ||
|
|
c33ccc7043 | ||
|
|
4f95ffaa1f | ||
|
|
487ef61d0a | ||
|
|
ce6a66959a | ||
|
|
70f8ec73c9 | ||
|
|
d78d640abd | ||
|
|
cf3552b46e | ||
|
|
f6e97b7ada | ||
|
|
35607e99a7 | ||
|
|
b36c6bd7ca | ||
|
|
9c5479287e | ||
|
|
93df1e1259 | ||
|
|
26abc1325e | ||
|
|
3da7533159 | ||
|
|
1a8a661b56 | ||
|
|
3b570d84ae | ||
|
|
547624edf9 | ||
|
|
14baf03aad | ||
|
|
418ae73870 | ||
|
|
e7ff903284 | ||
|
|
56f7280d55 | ||
|
|
01fc7511ae | ||
|
|
8c3ebd3414 | ||
|
|
ca46954335 | ||
|
|
a4bebc3083 | ||
|
|
c609db16b3 | ||
|
|
00fa63ea19 | ||
|
|
e8d3c06b60 | ||
|
|
359eba7145 | ||
|
|
e76edeb19e | ||
|
|
79532f2a85 | ||
|
|
2b805b897b | ||
|
|
44cf2b594a | ||
|
|
f4aaa595f8 | ||
|
|
a78e955691 | ||
|
|
632f63b34b | ||
|
|
cd50f949ae | ||
|
|
352121d60f | ||
|
|
3b4a6f21f9 | ||
|
|
cd012b57ed | ||
|
|
8f3f25b832 | ||
|
|
81dd57b402 | ||
|
|
5764a72f1b | ||
|
|
7a27d5dde1 | ||
|
|
a00d241cad | ||
|
|
c10c06eeb5 | ||
|
|
f94661561d | ||
|
|
a53efcf646 | ||
|
|
623aec2984 | ||
|
|
aba49c3d4f | ||
|
|
28189ea026 | ||
|
|
f6897f2257 | ||
|
|
9309cf96b9 | ||
|
|
a63dc0d9c9 | ||
|
|
f433f99e90 | ||
|
|
1dbbf3f45a | ||
|
|
91ab229086 | ||
|
|
8ac3dba11a | ||
|
|
7d17efc15f | ||
|
|
271f85185d | ||
|
|
28b2a689a7 | ||
|
|
40934484c6 | ||
|
|
05bba3fdc2 | ||
|
|
508c24c5e3 | ||
|
|
2a8d6b69cd | ||
|
|
a7559547b2 | ||
|
|
942322738a | ||
|
|
7e3d4a48d1 | ||
|
|
8cd22eacea | ||
|
|
cf0a87de28 | ||
|
|
4be8302190 | ||
|
|
34158e17b8 | ||
|
|
1a04359454 | ||
|
|
8bcb5004da | ||
|
|
b001514cf3 | ||
|
|
ffb1ad288f | ||
|
|
3fe87688d9 | ||
|
|
abfe350119 | ||
|
|
f8012bc467 | ||
|
|
bb5c06c58a | ||
|
|
9288311978 | ||
|
|
7386daed5f | ||
|
|
dc300ae5d4 | ||
|
|
cfcaeff27b | ||
|
|
417b841db7 | ||
|
|
711733f9af | ||
|
|
2f248390b2 | ||
|
|
3d4765d05a | ||
|
|
59a6760f5e | ||
|
|
bafa096a1d | ||
|
|
845de7b7dd | ||
|
|
6f15dc331f | ||
|
|
fdf6998a86 | ||
|
|
3804ec423f | ||
|
|
e13d424768 | ||
|
|
544f8ae771 | ||
|
|
3a9bb7e5bc | ||
|
|
4c7aee1a77 | ||
|
|
9b21b64836 | ||
|
|
c6e4c87e52 | ||
|
|
237d87880f | ||
|
|
dd40678ad7 | ||
|
|
0d4c9f3243 | ||
|
|
0d5683525e | ||
|
|
0f01fa89b8 | ||
|
|
fd011ec337 | ||
|
|
17c5daa245 | ||
|
|
a5931ea20e | ||
|
|
7e750ec611 | ||
|
|
3180381a26 | ||
|
|
925ddd6c09 | ||
|
|
a495fe56f5 | ||
|
|
cbb6600263 | ||
|
|
da73140c9c | ||
|
|
b7064f2bed | ||
|
|
4714b77f45 | ||
|
|
569001d554 | ||
|
|
ac3ef0a02d | ||
|
|
7eebe18e1a | ||
|
|
ba679d91e4 | ||
|
|
5b53c63eab | ||
|
|
4e0245da0d | ||
|
|
df4176f9e1 | ||
|
|
25a3f7068e | ||
|
|
ba50e23b81 | ||
|
|
14e640c903 | ||
|
|
7f61909415 | ||
|
|
8ffa10f86c | ||
|
|
0c1cf6d0a9 | ||
|
|
9a76f1be4f | ||
|
|
9162dc38ce | ||
|
|
e50428fce5 | ||
|
|
511095d3da | ||
|
|
2fde80454f | ||
|
|
43e35c4fc2 | ||
|
|
b0f87615a2 | ||
|
|
bd47f04479 | ||
|
|
313d0c1eb3 | ||
|
|
794aefb276 | ||
|
|
4d648bf872 | ||
|
|
7d1c9d6488 | ||
|
|
e77a852599 | ||
|
|
711d016a50 | ||
|
|
d16a1ec288 | ||
|
|
644f0ededb | ||
|
|
42aea9efbc | ||
|
|
274bfe7aee | ||
|
|
11ce3594c0 | ||
|
|
5198ef8dd8 | ||
|
|
f686fb606b | ||
|
|
ad20711a22 | ||
|
|
f30a3bc3e1 | ||
|
|
2ffa57c907 | ||
|
|
f0315ea1ec | ||
|
|
d61e0e4695 | ||
|
|
4fba837d40 | ||
|
|
7ed4950c69 | ||
|
|
02435e7455 | ||
|
|
fdb86cbd2e | ||
|
|
37a08e6cb6 | ||
|
|
d186b4ba9f | ||
|
|
1e8f5fa481 | ||
|
|
534b4a0f44 | ||
|
|
7e3a1cf272 | ||
|
|
456119ec38 | ||
|
|
031b980421 | ||
|
|
cc9f4730b4 | ||
|
|
ee6e675d58 | ||
|
|
2f76ac6362 | ||
|
|
a6fa67b7ca | ||
|
|
da3ddee767 | ||
|
|
f95a876ef2 | ||
|
|
70097ae02f | ||
|
|
1eb445bacb | ||
|
|
b0996ac404 | ||
|
|
160bc29c7c | ||
|
|
a264093d86 | ||
|
|
47be8c811f | ||
|
|
b0cc913884 | ||
|
|
d824a32ef1 | ||
|
|
ec1d8fa953 | ||
|
|
3f7725a684 | ||
|
|
0f500628dd | ||
|
|
fe0f04b2e4 | ||
|
|
45362dbc5b | ||
|
|
a1ad794d03 | ||
|
|
f8bda9fbad | ||
|
|
808b7210db | ||
|
|
81be397056 | ||
|
|
eae6af437b | ||
|
|
6582c618f1 | ||
|
|
8c999d3152 | ||
|
|
076dc017ab | ||
|
|
b7acd9c0dc | ||
|
|
20d199ae20 | ||
|
|
57860dc06c | ||
|
|
565dc8092c | ||
|
|
f19403192d | ||
|
|
457bb3ecc9 | ||
|
|
6362ae71c0 | ||
|
|
e163ae5299 | ||
|
|
e737dc8dac | ||
|
|
8d61b35cdc | ||
|
|
0af1f22e77 | ||
|
|
7a25ca0741 | ||
|
|
2472838e13 | ||
|
|
328077c4f8 | ||
|
|
588ad529e0 | ||
|
|
5d713ad9ad | ||
|
|
e66b590842 | ||
|
|
00dfe0f712 | ||
|
|
2c0e97421d | ||
|
|
5d89480dba | ||
|
|
358e240d42 | ||
|
|
df481eab00 | ||
|
|
54b8e2e6dc | ||
|
|
651863323b | ||
|
|
1e627a31f1 | ||
|
|
e5eb577b49 | ||
|
|
c378ea1300 | ||
|
|
cba4f5bc05 | ||
|
|
8109aa96f6 | ||
|
|
4067f572cc | ||
|
|
1e22861d11 | ||
|
|
282be54d1e | ||
|
|
abadfbfd20 | ||
|
|
93b3dc1876 | ||
|
|
e7c9c5ad0b | ||
|
|
82b608706c | ||
|
|
02648d3547 | ||
|
|
41e34c19eb | ||
|
|
a0821c225a | ||
|
|
7fe0689fb8 | ||
|
|
93fa95c833 | ||
|
|
501009073d | ||
|
|
81a54ea9db | ||
|
|
e28b4eb2a0 | ||
|
|
ebe72c3cef | ||
|
|
977542061d | ||
|
|
b0b75f705a | ||
|
|
be2c3dda94 | ||
|
|
64fd87f436 | ||
|
|
cd5d4ff254 | ||
|
|
1a555d5848 | ||
|
|
b17cd89469 | ||
|
|
9064bfcc85 | ||
|
|
0803d7b37c | ||
|
|
9814966fe7 | ||
|
|
3eda3060b8 | ||
|
|
6cc2b9bf17 | ||
|
|
1975b17eac | ||
|
|
3b499ecb3e | ||
|
|
6bd6603d52 | ||
|
|
ad4ee1d34e | ||
|
|
ccec6e72bf | ||
|
|
91b1a82a66 | ||
|
|
7801cf4a8a | ||
|
|
e431bfcb38 | ||
|
|
f5afd141de | ||
|
|
e38ee343b4 | ||
|
|
cf398ea8b2 | ||
|
|
bc1841c1b5 | ||
|
|
ed5a570e17 | ||
|
|
16df92212e | ||
|
|
67871bcf55 | ||
|
|
c51020dbaf | ||
|
|
eb1ea4aefe | ||
|
|
b1bfc512ac | ||
|
|
ce8fad1435 | ||
|
|
4f0faac148 | ||
|
|
fc8611a212 | ||
|
|
6b08aa60e6 | ||
|
|
eea66fc32b | ||
|
|
5bbfe12e4f | ||
|
|
533002c41e | ||
|
|
ebb9037ea1 | ||
|
|
eecea15f4f | ||
|
|
ff9b2a1c83 | ||
|
|
9f9df85aa0 | ||
|
|
2b71d5496d | ||
|
|
2578f17dc8 | ||
|
|
37e9b2b7af | ||
|
|
6a21ab4ea7 | ||
|
|
380938177c | ||
|
|
f66ef23256 | ||
|
|
3e3822e554 | ||
|
|
7564f5ff1a | ||
|
|
a807a8f945 | ||
|
|
280fac11e6 | ||
|
|
9924148302 | ||
|
|
66f829c760 | ||
|
|
a88ec7d90b | ||
|
|
54706e869c | ||
|
|
05f7d5e4f1 | ||
|
|
ddbd89dd2a | ||
|
|
5ed59af920 | ||
|
|
4868b498cf | ||
|
|
c66fa81208 | ||
|
|
58ca345684 | ||
|
|
fd598057ef | ||
|
|
ef7279341f | ||
|
|
69f2a7d556 | ||
|
|
008677298a | ||
|
|
d2bc056f0a | ||
|
|
42fffd16fe | ||
|
|
9c16aba6f7 | ||
|
|
ac79173f4a | ||
|
|
0a1e2acbcb | ||
|
|
9b6facb20d | ||
|
|
f89b9e704c | ||
|
|
f8b2e53609 | ||
|
|
7660dd7fa7 | ||
|
|
c3ce07755f | ||
|
|
f8a4cfa945 | ||
|
|
866ddb6e57 | ||
|
|
ca864c22b4 | ||
|
|
c9729244af | ||
|
|
442e4a1ec2 | ||
|
|
199acdf522 | ||
|
|
9340863652 | ||
|
|
d5cff6e9df | ||
|
|
e7f45a0d31 | ||
|
|
628ebe23ec | ||
|
|
823d79c948 | ||
|
|
b947094f45 | ||
|
|
6c5ab89d5f | ||
|
|
8f511a365a | ||
|
|
006f31af93 | ||
|
|
5bb112f407 | ||
|
|
4ab9298ab3 | ||
|
|
3926612281 |
@@ -0,0 +1,116 @@
|
||||
---
|
||||
name: code-review
|
||||
description: Review guidance for ESPHome pull requests. Use this when reviewing a pull request that changes ESPHome Python, C++, or component code, to check it against the project's coding conventions, embedded-systems memory rules, testing requirements, and breaking-change policy.
|
||||
---
|
||||
|
||||
# Reviewing ESPHome pull requests
|
||||
|
||||
ESPHome parses YAML into C++ firmware for memory-constrained microcontrollers
|
||||
(ESP32, ESP8266, RP2040, LibreTiny). Review changes with that in mind: RAM and
|
||||
flash are scarce, and code runs unattended for months.
|
||||
|
||||
`AGENTS.md` in the repository root is the full contributor guide and the
|
||||
authority when it disagrees with this summary. The developer documentation at
|
||||
https://developers.esphome.io explains the component lifecycle and the reasoning
|
||||
behind these rules. This skill lists the concrete things worth flagging in a
|
||||
review; read `AGENTS.md` for the detail behind any item.
|
||||
|
||||
Only raise findings that the diff actually introduces or changes. Do not ask for
|
||||
drive-by cleanup of pre-existing code the PR did not touch.
|
||||
|
||||
## Memory and embedded constraints (highest value)
|
||||
|
||||
Heap allocation after `setup()` is treated as a reliability bug, not a
|
||||
performance nit, because it fragments a small shared heap. Flag:
|
||||
|
||||
- New heap allocation on a hot path or after setup that could be avoided.
|
||||
- `std::vector` where the size is known at compile time (use `std::array`, or
|
||||
`StaticVector<T, N>` when a `push_back` API is needed) or fixed at runtime
|
||||
init (use `FixedVector<T>`).
|
||||
- Listener / child-entity registration lists stored as `std::vector`; these have
|
||||
a compile-time-known count and should use `cg.slot_counter()` plus
|
||||
`StaticVector`.
|
||||
- `std::vector<uint8_t>` for a byte buffer that never grows: prefer
|
||||
`std::unique_ptr<uint8_t[]>` or `std::array`.
|
||||
- `std::map` / `std::set` / `std::unordered_map` for small datasets (1-16
|
||||
elements): a `std::vector` of a small struct with linear search is lighter.
|
||||
- `std::deque` anywhere: it allocates 512-byte blocks and should be avoided.
|
||||
- `std::string` storing a value set once from config: prefer `StringRef` (the
|
||||
literal already lives in flash).
|
||||
- `std::string` / `std::to_string` / string-returning helpers on hot paths where
|
||||
a buffer or view API exists.
|
||||
|
||||
## C++ conventions
|
||||
|
||||
- Include what you use: a file referencing a symbol must include the header that
|
||||
declares it, even if it currently arrives transitively. New or changed uses of
|
||||
a symbol need the matching include.
|
||||
- Prefix all member access with `this->`.
|
||||
- Naming: `lower_snake_case` for functions/methods/variables, `UpperCamelCase`
|
||||
for classes/structs/enums, `UPPER_SNAKE_CASE` for namespace-scope constants,
|
||||
trailing underscore on protected/private fields.
|
||||
- `enum class` values must be prefixed with the enum name in `UPPER_SNAKE_CASE`
|
||||
(e.g. `UARTFlushResult::UART_FLUSH_RESULT_SUCCESS`). Bare names like `SUCCESS`,
|
||||
`FAIL`, or `OK` collide with SDK macros on some platforms and break the build.
|
||||
- Prefer `const`/`enum` over `#define`; `#define` is only for conditional
|
||||
compilation and code-generation sizes.
|
||||
- Never call `millis()` in a `loop()` body; use
|
||||
`App.get_loop_component_start_time()`. A rate-limit gate below ~16 ms (the loop
|
||||
period) does nothing.
|
||||
- Pick the timing primitive by cadence: gated `loop()` under 250 ms,
|
||||
`set_interval` at 500 ms and above.
|
||||
- Do not override a base method to return the value it already returns (e.g.
|
||||
`get_setup_priority()` returning `setup_priority::DATA`).
|
||||
- Wrap string literals passed as printf `%s` args in `LOG_STR_LITERAL()`.
|
||||
- Required, invariant dependencies should be constructor parameters, not setters.
|
||||
- Callback registration methods must be templated (`template<typename F>`), not
|
||||
typed as `std::function`, so lightweight forwarders avoid a heap allocation.
|
||||
- Two-space indent, `using` over `typedef`, wrap at 120 columns.
|
||||
|
||||
## Python conventions
|
||||
|
||||
- Type-annotate every new function signature (params and return), new dataclass
|
||||
fields, and new module-level variables. Import `ConfigType` from
|
||||
`esphome.types`.
|
||||
- Use the walrus operator to avoid a double lookup, e.g.
|
||||
`if (blah := config.get(CONF_BLAH)) is not None:`.
|
||||
- Reuse existing validators from `config_validation.py` (`cv.rename_key`,
|
||||
`cv.has_exactly_one_key`, etc.) via `cv.All(...)` instead of hand-rolling.
|
||||
- `esphome/const.py` is frozen: no new `CONF_` constants there. Define them in
|
||||
the component's own `.py`, or in `esphome/components/const/__init__.py` when
|
||||
shared. The same constant defined in three or more component files fails CI.
|
||||
- State that must persist during code generation goes in `CORE.data` namespaced
|
||||
under the component `DOMAIN` (a `@dataclass`), not module-level mutable globals.
|
||||
- Prefer callback-based triggers via `build_callback_automation()`; only use a
|
||||
`Trigger<Ts...>` subclass when the forwarder needs mutable state.
|
||||
|
||||
## Testing and coverage
|
||||
|
||||
- New and changed lines and branches need test coverage, including defensive
|
||||
early-returns, error paths, and no-op guards. A mocked-out function is not
|
||||
covered; exercise the real call path too.
|
||||
- Component YAML tests live in `tests/components/<component>/`. Never define
|
||||
buses (uart, i2c, spi, modbus) directly in a test file: pull them from
|
||||
`tests/test_build_components/common/` through dict-style `packages:` so CI can
|
||||
group builds. List-style packages or top-level merge keys block grouping.
|
||||
- Config-only checks use the `validate.*.yaml` prefix; compiled checks use
|
||||
`test.*.yaml`.
|
||||
|
||||
## Breaking changes and public API
|
||||
|
||||
- Base classes under `esphome/core/` and documented config options are public
|
||||
API. Undocumented `public` members of a component are internal.
|
||||
- A breaking change needs justification, a migration path in the PR description,
|
||||
and a deprecation window where feasible (`ESPDEPRECATED` in C++,
|
||||
`cv.rename_key(..., removed_in=...)` in Python). Changing a codegen-injected
|
||||
lambda signature is not a breaking change.
|
||||
|
||||
## Process and PR hygiene
|
||||
|
||||
- PR titles start with a `[tag]` prefix: the component name (e.g. `[uart] ...`)
|
||||
or `[core]` for shared code.
|
||||
- Prose in docs, comments, and commit messages should be plain English. Keep
|
||||
inline comments short and only where the code is not self-explanatory; do not
|
||||
restate what the code says.
|
||||
- Verify the PR fills out `.github/PULL_REQUEST_TEMPLATE.md` and adds
|
||||
`CODEOWNERS` entries for a new component.
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../.agents/skills
|
||||
@@ -31,7 +31,4 @@ RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
&& platformio settings set check_platformio_interval 1000000
|
||||
|
||||
COPY script/platformio_install_deps.py platformio.ini ./
|
||||
RUN ./platformio_install_deps.py platformio.ini --libraries --platforms --tools
|
||||
|
||||
WORKDIR /workspaces
|
||||
|
||||
@@ -9,6 +9,29 @@ body:
|
||||
If you have a feature request or enhancement, please [request them here instead][fr].
|
||||
|
||||
[fr]: https://github.com/orgs/esphome/discussions
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
## Use of AI in bug reports
|
||||
|
||||
AI tools are good at carrying out well-defined tasks, but they are not good at troubleshooting.
|
||||
Please do NOT paste an AI-generated wall of text into the issue template - if the AI hasn't solved
|
||||
your problem, its wild guesses are not likely to help.
|
||||
|
||||
Please DO include your own words and observations, compile/boot logs, and
|
||||
especially a minimal reproducible example of your YAML configuration that demonstrates the problem.
|
||||
|
||||
It is however quite acceptable to use AI to translate your *own* report,
|
||||
if you aren't a competent English speaker.
|
||||
|
||||
If you really think it will be useful to include an AI's analysis, preferably wrap it in a `<details>` block which will be collapsed by default.
|
||||
|
||||
If you are using AI to help solve a problem, rather than asking it to speculate about what the problem is,
|
||||
it can be more useful to ask it to create a step-by-step troubleshooting procedure.
|
||||
AI is also useful for generating boilerplate code, such as a minimal reproducible example of your YAML
|
||||
configuration that demonstrates the problem.
|
||||
|
||||
Used properly, AI can be a useful tool to help you solve your problem, but don't let it get in the way.
|
||||
- type: textarea
|
||||
validations:
|
||||
required: true
|
||||
|
||||
@@ -42,7 +42,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@@ -67,7 +67,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
|
||||
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
@@ -32,7 +32,7 @@ runs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||
# downstream jobs rely on is preserved.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../.agents/skills
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull-request-only workflow: a save could never be shared and
|
||||
|
||||
@@ -12,9 +12,7 @@ on:
|
||||
- ".github/workflows/ci-docker.yml"
|
||||
- "requirements*.txt"
|
||||
- "pyproject.toml"
|
||||
- "platformio.ini"
|
||||
- "esphome/idf_component.yml"
|
||||
- "script/platformio_install_deps.py"
|
||||
# Core, build pipeline, toolchain, and target-platform changes can change
|
||||
# how a toolchain is set up or built, so re-run the per-toolchain compile
|
||||
# smoke test when they change.
|
||||
@@ -67,7 +65,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
|
||||
- name: Determine tag and whether to push
|
||||
id: tag
|
||||
@@ -159,7 +157,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||
# that ``. venv/bin/activate`` see an identical layout.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
@@ -350,7 +350,7 @@ jobs:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Report empty upload to Codecov
|
||||
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
|
||||
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
|
||||
with:
|
||||
run_command: empty-upload
|
||||
force: true
|
||||
@@ -413,7 +413,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -1274,7 +1274,7 @@ jobs:
|
||||
# install step (order-of-magnitude faster on cold boots,
|
||||
# with its own wheel cache). actions/setup-python still
|
||||
# provides the interpreter.
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -14,4 +14,4 @@ jobs:
|
||||
permissions:
|
||||
issues: write # issues.lock on closed issues
|
||||
pull-requests: write # issues.lock on closed pull requests
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@0fdd5e311b7e744069166696072a1a9cbc5fbeb6 # 2026.8.1
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # 2026.9.0
|
||||
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
|
||||
# GitHub App token so the labels, comments and closures come from
|
||||
# esphome[bot] instead of github-actions[bot].
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@a1c1485ab46ef41a84a6a9d8abd7fa4b7628fd70 # main
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@cc3e76de337dc59bc1cba8da58d963cd23b873f1 # main
|
||||
secrets:
|
||||
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
and will be closed if no further activity occurs within 7 days.
|
||||
|
||||
If you are the author of this PR, please leave a comment if you want
|
||||
to keep it open. Also, please rebase your PR onto the latest dev
|
||||
to keep it open. Also, please merge the latest dev branch into your
|
||||
branch to ensure that it's up to date with the latest changes.
|
||||
|
||||
Thank you for your contribution!
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# setup-python interpreter so subsequent ``prek`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
|
||||
@@ -10,7 +10,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.16.6
|
||||
rev: v0.16.8
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -431,7 +431,31 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
MyComponent *parent_;
|
||||
};
|
||||
```
|
||||
Register with `@automation.register_action("my_component.do_something", MyAction, schema, synchronous=True)`. Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
|
||||
Register it without writing a builder:
|
||||
```python
|
||||
automation.register_simple_action(
|
||||
"my_component.do_something", MyAction, schema, synchronous=True
|
||||
)
|
||||
```
|
||||
The constructor receives the object named by `config[CONF_ID]`. Use `register_bare_action` for a
|
||||
no-argument constructor, `register_parented_action` for a class deriving from `Parented<T>`, and
|
||||
the `@automation.register_action(...)` decorator only when the builder must also set fields.
|
||||
|
||||
Use `synchronous=True` for actions that run to completion inside `play()` without deferring. Use `synchronous=False` if the action may suspend/defer execution (e.g. `delay`, `wait_until`, `script.wait`) or store trigger arguments for later use.
|
||||
|
||||
**Actions that only forward templatable values to their parent need no C++ class.** Register them
|
||||
with `register_apply_action`; do not write a `TEMPLATABLE_VALUE` class or a builder for this shape.
|
||||
```python
|
||||
automation.register_apply_action(
|
||||
"my_component.set_gains",
|
||||
schema,
|
||||
automation.ApplyField(CONF_KP, "set_kp", cg.float_),
|
||||
automation.ApplyField(CONF_KI, "set_ki", cg.float_),
|
||||
)
|
||||
```
|
||||
The `ApplyField`, `ApplyCall` and `register_apply_action` docstrings in `esphome/automation.py` cover
|
||||
the rest; `cover.control` and `cover.template.publish` are in-tree examples. `TEMPLATABLE_VALUE` with
|
||||
`cg.templatable` stays for actions whose `play()` has real logic beyond forwarding values.
|
||||
|
||||
* **Conditions:**
|
||||
```cpp
|
||||
@@ -443,7 +467,21 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
MyComponent *parent_;
|
||||
};
|
||||
```
|
||||
Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`.
|
||||
Register with `automation.register_simple_condition("my_component.is_active", MyCondition, schema)`;
|
||||
`register_bare_condition`, `register_parented_condition` and the decorator follow the action rules.
|
||||
|
||||
**Conditions that only test their parent need no C++ class either.** Register them with
|
||||
`register_apply_condition`; the expression is applied to the parent, and an `ApplyCall` compares
|
||||
against config values.
|
||||
```python
|
||||
automation.register_apply_condition("my_component.is_active", schema, "is_active()")
|
||||
automation.register_apply_condition(
|
||||
"my_component.state_is",
|
||||
schema,
|
||||
automation.ApplyCall("state == {}", ((CONF_STATE, cg.bool_),)),
|
||||
)
|
||||
```
|
||||
`cover.is_open`, `rtttl.is_playing` and `component.is_idle` are in-tree examples.
|
||||
|
||||
* **Type Hints:** Type-hint all function signatures, including test functions and config validators (e.g. `def validate_x(config: ConfigType) -> ConfigType:`, `def test_x() -> None:`). Import `ConfigType` from `esphome.types`.
|
||||
|
||||
@@ -629,6 +667,9 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
_request_listener_slot()
|
||||
cg.add(hub.register_listener(var))
|
||||
```
|
||||
When several instances each own a list declared at the same size (one per hub of a
|
||||
`MULTI_CONF` component), pass the owning object as the key, `_request_listener_slot(str(hub))`;
|
||||
the define is then the largest count any one key requested instead of the total.
|
||||
```cpp
|
||||
#ifdef MY_COMPONENT_LISTENER_COUNT
|
||||
void register_listener(MyComponentListener *listener);
|
||||
@@ -696,7 +737,9 @@ file does, and it is the authority when they disagree. The most useful starting
|
||||
|
||||
6. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
|
||||
|
||||
7. **Detection:** Look for these patterns in compiler output:
|
||||
7. **Never use `new (std::nothrow)`:** On ESP-IDF exceptions are disabled, so a failed nothrow allocation aborts instead of returning `nullptr`. Use `RAMAllocator` from `esphome/core/helpers.h`; CI rejects `std::nothrow`.
|
||||
|
||||
8. **Detection:** Look for these patterns in compiler output:
|
||||
- Large code sections with STL symbols (vector, map, set)
|
||||
- `alloc`, `realloc`, `dealloc` in symbol names
|
||||
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
|
||||
|
||||
+14
-1
@@ -100,6 +100,7 @@ esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
|
||||
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
|
||||
esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bridge/* @kbx81
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
esphome/components/button/* @esphome/core
|
||||
esphome/components/bytebuffer/* @clydebarrow
|
||||
@@ -111,6 +112,8 @@ esphome/components/captive_portal/* @esphome/core
|
||||
esphome/components/cc1101/* @gabest11 @lygris
|
||||
esphome/components/ccs811/* @habbie
|
||||
esphome/components/cd74hc4067/* @asoehlke
|
||||
esphome/components/cdc_acm_uart/* @kbx81
|
||||
esphome/components/cdc_acm_uart/bridge/* @kbx81
|
||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||
esphome/components/ch423/* @dwmw2
|
||||
esphome/components/chsc6x/* @kkosik20
|
||||
@@ -179,7 +182,6 @@ esphome/components/esp32_camera_web_server/* @ayufan
|
||||
esphome/components/esp32_can/* @Sympatron
|
||||
esphome/components/esp32_hosted/* @swoboda1337
|
||||
esphome/components/esp32_hosted/update/* @swoboda1337
|
||||
esphome/components/esp32_improv/* @jesserockz
|
||||
esphome/components/esp32_rmt/* @jesserockz
|
||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||
esphome/components/esp8266/* @esphome/core
|
||||
@@ -263,8 +265,10 @@ esphome/components/i2s_audio/* @jesserockz
|
||||
esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
esphome/components/icnt86/* @danepowell
|
||||
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
|
||||
esphome/components/improv_base/* @esphome/core
|
||||
esphome/components/improv_ble/* @jesserockz
|
||||
esphome/components/improv_serial/* @esphome/core
|
||||
esphome/components/ina226/* @latonita @Sergio303
|
||||
esphome/components/ina260/* @mreditor97
|
||||
@@ -418,6 +422,7 @@ esphome/components/pn7150_i2c/* @jesserockz @kbx81
|
||||
esphome/components/pn7160/* @jesserockz @kbx81
|
||||
esphome/components/pn7160_i2c/* @jesserockz @kbx81
|
||||
esphome/components/pn7160_spi/* @jesserockz @kbx81
|
||||
esphome/components/pn71xx/* @jesserockz @kbx81
|
||||
esphome/components/power_supply/* @esphome/core
|
||||
esphome/components/preferences/* @esphome/core
|
||||
esphome/components/provisioning/* @esphome/core
|
||||
@@ -425,6 +430,7 @@ esphome/components/psram/* @esphome/core
|
||||
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
|
||||
esphome/components/pvvx_mithermometer/* @pasiz
|
||||
esphome/components/pylontech/* @functionpointer
|
||||
esphome/components/pzem6l24/* @nuttytree
|
||||
esphome/components/qmi8658/* @clydebarrow
|
||||
esphome/components/qmp6988/* @andrewpc
|
||||
esphome/components/qr_code/* @wjtje
|
||||
@@ -453,6 +459,7 @@ esphome/components/rtl87xx/* @kuba2k2
|
||||
esphome/components/rtttl/* @glmnet @ximex
|
||||
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8025t/* @remcom
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
esphome/components/scd4x/* @martgras @sjtrny
|
||||
@@ -474,6 +481,7 @@ esphome/components/sendspin/image/* @kahrendt
|
||||
esphome/components/sendspin/media_player/* @kahrendt
|
||||
esphome/components/sendspin/media_source/* @kahrendt
|
||||
esphome/components/sendspin/sensor/* @kahrendt
|
||||
esphome/components/sendspin/switch/* @kahrendt
|
||||
esphome/components/sendspin/text_sensor/* @kahrendt
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
@@ -542,7 +550,9 @@ esphome/components/sx126x/* @swoboda1337
|
||||
esphome/components/sx127x/* @swoboda1337
|
||||
esphome/components/sy6970/* @linkedupbits
|
||||
esphome/components/syslog/* @clydebarrow
|
||||
esphome/components/systa_bus/* @Mat931
|
||||
esphome/components/t6615/* @tylermenezes
|
||||
esphome/components/tas2780/* @remcom
|
||||
esphome/components/tc74/* @sethgirvan
|
||||
esphome/components/tca9548a/* @andreashergert1984
|
||||
esphome/components/tca9555/* @mobrembski
|
||||
@@ -555,6 +565,7 @@ esphome/components/template/datetime/* @rfdarter
|
||||
esphome/components/template/event/* @nohat
|
||||
esphome/components/template/fan/* @ssieb
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/tfluna/* @candrews
|
||||
esphome/components/thermopro_ble/* @sittner
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @esphome/core
|
||||
@@ -586,6 +597,7 @@ esphome/components/uart/* @esphome/core
|
||||
esphome/components/uart/button/* @ssieb
|
||||
esphome/components/uart/event/* @eoasmxd
|
||||
esphome/components/uart/packet_transport/* @clydebarrow
|
||||
esphome/components/uart_mux/* @kbx81
|
||||
esphome/components/udp/* @clydebarrow
|
||||
esphome/components/ufire_ec/* @pvizeli
|
||||
esphome/components/ufire_ise/* @pvizeli
|
||||
@@ -630,6 +642,7 @@ esphome/components/wts01/* @alepee
|
||||
esphome/components/x9c/* @EtienneMD
|
||||
esphome/components/xdb401/* @RT530
|
||||
esphome/components/xgzp68xx/* @gcormier
|
||||
esphome/components/xiaomi_body_scale/* @dckiller51
|
||||
esphome/components/xiaomi_hhccjcy10/* @fariouche
|
||||
esphome/components/xiaomi_lywsd02mmc/* @juanluss31
|
||||
esphome/components/xiaomi_lywsd03mmc/* @ahpohl
|
||||
|
||||
+1
-4
@@ -22,16 +22,13 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.5
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.16.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
&& platformio settings set check_platformio_interval 1000000 \
|
||||
&& mkdir -p /piolibs
|
||||
|
||||
COPY script/platformio_install_deps.py platformio.ini /
|
||||
RUN /platformio_install_deps.py /platformio.ini --libraries
|
||||
|
||||
ARG BUILD_VERSION
|
||||
|
||||
LABEL \
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
# Home Assistant Add-on: ESPHome
|
||||
# Sends discovery information to Home Assistant.
|
||||
# ==============================================================================
|
||||
declare config
|
||||
declare port
|
||||
|
||||
# We only disable it when disabled explicitly
|
||||
@@ -19,14 +18,17 @@ port=$(bashio::addon.ingress_port)
|
||||
# Wait for the ESPHome Device Builder to become available
|
||||
bashio::net.wait_for "${port}" "127.0.0.1" 300
|
||||
|
||||
config=$(\
|
||||
bashio::var.json \
|
||||
host "127.0.0.1" \
|
||||
port "^${port}" \
|
||||
)
|
||||
# Send one discovery message; the config is a JSON string built with bashio::var.json.
|
||||
send_discovery() {
|
||||
local service=$1
|
||||
local config=$2
|
||||
if bashio::discovery "${service}" "${config}" > /dev/null; then
|
||||
bashio::log.info "Successfully send ${service} discovery information to Home Assistant."
|
||||
else
|
||||
bashio::log.error "${service} discovery message to Home Assistant failed!"
|
||||
fi
|
||||
}
|
||||
|
||||
if bashio::discovery "esphome" "${config}" > /dev/null; then
|
||||
bashio::log.info "Successfully send discovery information to Home Assistant."
|
||||
else
|
||||
bashio::log.error "Discovery message to Home Assistant failed!"
|
||||
fi
|
||||
send_discovery "esphome" "$(bashio::var.json host "127.0.0.1" port "^${port}")"
|
||||
# The Device Builder MCP server, consumed by Home Assistant's mcp integration.
|
||||
send_discovery "mcp" "$(bashio::var.json url "http://127.0.0.1:${port}/api/mcp")"
|
||||
|
||||
@@ -5,3 +5,15 @@ bk72xx:
|
||||
board: generic-bk7231n-qfn32-tuya
|
||||
|
||||
logger:
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
ap:
|
||||
|
||||
# mqtt and captive_portal together pull in AsyncTCP and ESPAsyncWebServer;
|
||||
# a stray ESP32 AsyncTCP copy on the library search path breaks this build
|
||||
captive_portal:
|
||||
|
||||
mqtt:
|
||||
broker: 192.168.178.84
|
||||
|
||||
+64
-31
@@ -16,6 +16,7 @@ from typing import TYPE_CHECKING, Protocol
|
||||
# cause them to be loaded before external components are processed, resulting
|
||||
# in the built-in version being used instead of the external component one.
|
||||
from esphome import const, platform_hooks
|
||||
from esphome.build_helpers.native import analysis_backend, native_backend
|
||||
from esphome.const import (
|
||||
ALLOWED_NAME_CHARS,
|
||||
ARGUMENT_HELP_DEVICE,
|
||||
@@ -817,7 +818,9 @@ def write_cpp_file() -> int:
|
||||
from esphome.build_gen import espidf
|
||||
|
||||
espidf.write_project()
|
||||
else:
|
||||
elif not CORE.using_native_toolchain:
|
||||
# Other native builds generate their project at compile time;
|
||||
# never write a platformio.ini for them
|
||||
from esphome.build_gen import platformio
|
||||
|
||||
platformio.write_project()
|
||||
@@ -859,20 +862,14 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
toolchain.create_factory_bin()
|
||||
toolchain.create_ota_bin()
|
||||
toolchain.create_elf_copy()
|
||||
from esphome.build_helpers.idedata import IDEDATA_BEST_EFFORT_ERRORS
|
||||
from esphome.build_helpers.idedata import warn_if_idedata_missing
|
||||
|
||||
try:
|
||||
if toolchain.get_idedata() is None:
|
||||
_LOGGER.warning("No idedata was generated for this build")
|
||||
except IDEDATA_BEST_EFFORT_ERRORS as err:
|
||||
# The firmware already built; an idedata failure must not fail
|
||||
# a successful build.
|
||||
_LOGGER.warning(
|
||||
"Could not generate idedata: %s (IDE, clang-tidy, and "
|
||||
"memory-analysis data will be unavailable for this build)",
|
||||
err,
|
||||
)
|
||||
_LOGGER.debug("Idedata failure detail", exc_info=True)
|
||||
warn_if_idedata_missing(toolchain.get_idedata)
|
||||
elif CORE.using_native_toolchain:
|
||||
raise EsphomeError(
|
||||
f"Toolchain '{CORE.toolchain.value}' resolved but no platform "
|
||||
"backend claimed the build"
|
||||
)
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
@@ -975,12 +972,15 @@ def upload_using_esptool(
|
||||
|
||||
if file is not None:
|
||||
flash_images = [FlashImage(path=file, offset="0x0")]
|
||||
elif CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
flash_images = [
|
||||
FlashImage(path=toolchain.get_factory_firmware_path(), offset="0x0")
|
||||
]
|
||||
elif (native := native_backend()) is not None:
|
||||
# Every native backend supplies its own 0x0 flash image (bootloader
|
||||
# and partitions included where the target needs them)
|
||||
image = native.get_factory_firmware_path()
|
||||
if not image.is_file():
|
||||
raise EsphomeError(
|
||||
f"{image} does not exist; compile the configuration first"
|
||||
)
|
||||
flash_images = [FlashImage(path=image, offset="0x0")]
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
@@ -1343,8 +1343,12 @@ def _upload_via_native_api(
|
||||
# fall back to a plaintext upload
|
||||
noise_psk = None
|
||||
plaintext_fallback = False
|
||||
allow_plaintext_upload = False
|
||||
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
|
||||
noise_psk = encryption_conf.get(CONF_KEY)
|
||||
allow_plaintext_upload = bool(
|
||||
encryption_conf.get(espota2.CONF_ALLOW_PLAINTEXT_UPLOAD)
|
||||
)
|
||||
if not noise_psk:
|
||||
raise EsphomeError(
|
||||
"OTA encryption is configured but no key was resolved; "
|
||||
@@ -1395,6 +1399,7 @@ def _upload_via_native_api(
|
||||
ota_type,
|
||||
noise_psk,
|
||||
plaintext_fallback=plaintext_fallback,
|
||||
allow_plaintext_upload=allow_plaintext_upload,
|
||||
)
|
||||
|
||||
|
||||
@@ -1965,12 +1970,12 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
import json
|
||||
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
# Native ESP-IDF derives idedata from the build's compile_commands.json,
|
||||
# so the configuration must already be compiled.
|
||||
from esphome.espidf import toolchain as espidf_toolchain
|
||||
native_toolchain = native_backend()
|
||||
|
||||
idedata = espidf_toolchain.get_idedata()
|
||||
if native_toolchain is not None:
|
||||
# Native toolchains derive idedata from the build's
|
||||
# compile_commands.json, so the configuration must already be compiled.
|
||||
idedata = native_toolchain.get_idedata()
|
||||
if idedata is None:
|
||||
_LOGGER.error(
|
||||
"No idedata available; compile the configuration first",
|
||||
@@ -2009,6 +2014,17 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
|
||||
|
||||
# Refuse an unsupported toolchain before paying for a full compile
|
||||
analysis_toolchain = analysis_backend()
|
||||
if analysis_toolchain is None and not CORE.using_toolchain_platformio:
|
||||
_LOGGER.error(
|
||||
"analyze-memory is not supported with the '%s' toolchain on %s; "
|
||||
"re-run with --toolchain platformio",
|
||||
CORE.toolchain.value if CORE.toolchain else "unresolved",
|
||||
CORE.target_platform,
|
||||
)
|
||||
return 1
|
||||
|
||||
# Always compile to ensure fresh data (fast if no changes - just relinks)
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
@@ -2020,13 +2036,30 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
|
||||
# Get idedata for analysis
|
||||
idedata = None
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain
|
||||
if analysis_toolchain is not None:
|
||||
objdump = analysis_toolchain.get_objdump_path()
|
||||
readelf = analysis_toolchain.get_readelf_path()
|
||||
for tool in (objdump, readelf):
|
||||
if not tool.is_file():
|
||||
# The analyzer would silently fall back to host
|
||||
# binutils, which cannot read the target ELF
|
||||
_LOGGER.error(
|
||||
"%s is missing; the toolchain install may be incomplete "
|
||||
"(recompile, or run 'esphome clean-all' if it persists)",
|
||||
tool,
|
||||
)
|
||||
return 1
|
||||
objdump_path = str(objdump)
|
||||
readelf_path = str(readelf)
|
||||
|
||||
objdump_path = str(toolchain.get_objdump_path())
|
||||
readelf_path = str(toolchain.get_readelf_path())
|
||||
|
||||
firmware_elf = toolchain.get_elf_path()
|
||||
firmware_elf = analysis_toolchain.get_elf_path()
|
||||
if not firmware_elf.is_file():
|
||||
# The analyzer swallows tool failures, so a missing ELF would
|
||||
# produce an exit-0 zeroed report
|
||||
_LOGGER.error(
|
||||
"%s is missing; compile the configuration first", firmware_elf
|
||||
)
|
||||
return 1
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
|
||||
@@ -68,12 +68,16 @@ def idedata_candidates(build_path: Path) -> list[Path]:
|
||||
The candidate idedata JSON paths, most specific first
|
||||
"""
|
||||
name = build_path.name
|
||||
data_dir = build_path.parent.parent / "idedata"
|
||||
# Native backends suffix the cache by toolchain (<name>.arduino.json)
|
||||
suffixed = sorted(data_dir.glob(f"{name}.*.json")) if data_dir.is_dir() else []
|
||||
return [
|
||||
# In .pioenvs for test builds
|
||||
build_path / ".pioenvs" / name / "idedata.json",
|
||||
# Both toolchains cache it in the data dir, which holds this build dir:
|
||||
# <data_dir>/idedata/<name>.json next to <data_dir>/build/<name>
|
||||
build_path.parent.parent / "idedata" / f"{name}.json",
|
||||
*suffixed,
|
||||
# Regular builds, invoked from the config dir or from anywhere
|
||||
Path.cwd() / ".esphome" / "idedata" / f"{name}.json",
|
||||
Path.home() / ".esphome" / "idedata" / f"{name}.json",
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
"""Native Arduino ESP8266 build driver (the PlatformIO ``run`` equivalent)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from esphome.build_helpers.ccache import resolve_ccache_path
|
||||
from esphome.const import (
|
||||
CONF_COMPILE_PROCESS_LIMIT,
|
||||
CONF_ESPHOME,
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.helpers import write_file
|
||||
from esphome.types import ConfigType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.arduino8266.framework import InstalledPaths
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# ESP8266 user RAM (matches upload.maximum_ram_size in every board manifest)
|
||||
_MAX_RAM_SIZE = 81920
|
||||
|
||||
|
||||
def _warn_ignored_platformio_options() -> None:
|
||||
"""Warn for component-added platformio options the native build drops."""
|
||||
from esphome.core.config import NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS
|
||||
|
||||
consumed = NATIVE_ARDUINO_CONSUMED_PIO_OPTIONS
|
||||
for key in sorted(CORE.platformio_options or {}):
|
||||
if key not in consumed:
|
||||
_LOGGER.warning(
|
||||
"platformio_options->%s is ignored when building with the "
|
||||
"native 'arduino' toolchain",
|
||||
key,
|
||||
)
|
||||
|
||||
|
||||
_RAM_SECTIONS = (".data", ".rodata", ".bss")
|
||||
_FLASH_SECTIONS = (".irom0.text", ".text", ".text1", ".data", ".rodata")
|
||||
|
||||
|
||||
def get_build_dir() -> Path:
|
||||
return CORE.relative_pioenvs_path(CORE.name)
|
||||
|
||||
|
||||
def get_elf_path() -> Path:
|
||||
return get_build_dir() / "firmware.elf"
|
||||
|
||||
|
||||
def _toolchain_tool(name: str) -> Path:
|
||||
# Imported here, not at module scope: the serial upload/logs fast path
|
||||
# resolves this module for its artifact paths alone, and framework
|
||||
# pulls in the whole package-download stack
|
||||
from esphome.arduino8266 import framework
|
||||
|
||||
return framework.toolchain_tool(framework.get_toolchain_path(), name)
|
||||
|
||||
|
||||
def get_factory_firmware_path() -> Path:
|
||||
"""The image to serial-flash at 0x0 (same bytes as firmware.bin: the
|
||||
8266 factory copy exists for artifact-contract parity, not content)."""
|
||||
return get_build_dir() / "firmware.factory.bin"
|
||||
|
||||
|
||||
def get_addr2line_path() -> Path:
|
||||
return _toolchain_tool("addr2line")
|
||||
|
||||
|
||||
def get_objdump_path() -> Path:
|
||||
return _toolchain_tool("objdump")
|
||||
|
||||
|
||||
def get_readelf_path() -> Path:
|
||||
return _toolchain_tool("readelf")
|
||||
|
||||
|
||||
def run_compile(config: ConfigType, verbose: bool) -> int:
|
||||
from esphome.arduino8266 import framework
|
||||
from esphome.build_gen import arduino8266 as build_gen
|
||||
|
||||
_warn_ignored_platformio_options()
|
||||
paths = framework.check_and_install(CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION])
|
||||
# Resolved once: the probe is not free and three consumers need it
|
||||
ccache = resolve_ccache_path()
|
||||
build_gen.write_project(paths, ccache)
|
||||
|
||||
build_dir = get_build_dir()
|
||||
env = framework.get_build_env(paths.toolchain, ccache)
|
||||
|
||||
# The DB is a pure function of build.ninja; regenerate when it is older
|
||||
compdb = build_dir / "compile_commands.json"
|
||||
ninja_file = build_dir / "build.ninja"
|
||||
if not compdb.is_file() or compdb.stat().st_mtime < ninja_file.stat().st_mtime:
|
||||
_write_compile_commands(paths.ninja, build_dir, env)
|
||||
|
||||
cmd = [str(paths.ninja)]
|
||||
if verbose:
|
||||
cmd.append("-v")
|
||||
if jobs := config[CONF_ESPHOME].get(CONF_COMPILE_PROCESS_LIMIT):
|
||||
cmd += ["-j", str(jobs)]
|
||||
# Explicit targets: a generator defect that drops them fails loudly
|
||||
# instead of a green no-op run leaving stale artifacts in place
|
||||
targets = ["firmware.factory.bin", "firmware.ota.bin"]
|
||||
cmd += targets
|
||||
|
||||
# cwd, not -C: drops ninja's "Entering directory" banner
|
||||
_LOGGER.debug("Running: %s", " ".join(cmd))
|
||||
rc = subprocess.run(
|
||||
cmd, cwd=build_dir, env=env, check=False, close_fds=False
|
||||
).returncode
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
# ninja already refused missing targets; existence covers a rule that
|
||||
# ran but wrote elsewhere
|
||||
build_dir_artifacts = (
|
||||
get_elf_path(),
|
||||
build_dir / "firmware.bin",
|
||||
get_factory_firmware_path(),
|
||||
build_dir / "firmware.ota.bin",
|
||||
)
|
||||
for artifact in build_dir_artifacts:
|
||||
if not artifact.is_file():
|
||||
_LOGGER.error("Build produced no %s", artifact)
|
||||
return 1
|
||||
|
||||
if not _print_size_summary(build_dir, paths):
|
||||
# Cause already warned; name the consequence for CI harnesses
|
||||
_LOGGER.warning("Firmware size summary unavailable for this build")
|
||||
from esphome.build_helpers.idedata import warn_if_idedata_missing
|
||||
|
||||
warn_if_idedata_missing(lambda: get_idedata(ccache))
|
||||
return 0
|
||||
|
||||
|
||||
def _write_compile_commands(
|
||||
ninja_path: Path, build_dir: Path, env: dict[str, str]
|
||||
) -> None:
|
||||
compdb = build_dir / "compile_commands.json"
|
||||
result = subprocess.run(
|
||||
[str(ninja_path), "-C", str(build_dir), "-t", "compdb", "c", "cxx", "asm"],
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
close_fds=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
# Drop any stale database so consumers (IDE integration, clang-tidy,
|
||||
# the memory analyzer) can't silently read outdated data.
|
||||
compdb.unlink(missing_ok=True)
|
||||
raise EsphomeError(f"Could not generate compile_commands.json: {result.stderr}")
|
||||
try:
|
||||
entries = json.loads(result.stdout)
|
||||
except ValueError as err:
|
||||
compdb.unlink(missing_ok=True)
|
||||
raise EsphomeError(
|
||||
f"ninja produced an unparsable compile database: {err} "
|
||||
f"(output starts {result.stdout[:120]!r})"
|
||||
) from err
|
||||
if not entries:
|
||||
# compdb exits 0 with [] for unknown rule names; a renamed compile
|
||||
# rule must fail the build, not silently strand every consumer
|
||||
compdb.unlink(missing_ok=True)
|
||||
raise EsphomeError(
|
||||
"ninja produced an empty compile database; the generator's rule "
|
||||
"names no longer match"
|
||||
)
|
||||
write_file(compdb, result.stdout)
|
||||
|
||||
|
||||
def _parse_app_size(build_dir: Path, paths: InstalledPaths) -> int | None:
|
||||
"""Read the app flash budget (irom0_0_seg length) from the linker script."""
|
||||
from esphome.build_gen.arduino8266 import get_flash_ld_path
|
||||
from esphome.components.esp8266.build_surgery import segment_length
|
||||
|
||||
# Warnings, not debug: without the app size the Flash summary line is
|
||||
# dropped and CI's memory-impact extraction loses its flash metric.
|
||||
ld_path = get_flash_ld_path(build_dir, paths)
|
||||
try:
|
||||
ld_text = ld_path.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError) as err:
|
||||
# A corrupt script degrades the same way, never aborts the build
|
||||
_LOGGER.warning("Cannot read linker script for the Flash summary: %s", err)
|
||||
return None
|
||||
if not (app_size := segment_length(ld_text, "irom0_0_seg")):
|
||||
_LOGGER.warning("No usable irom0_0_seg in %s; skipping Flash summary", ld_path)
|
||||
return None
|
||||
return app_size
|
||||
|
||||
|
||||
def _print_size_summary(build_dir: Path, paths: InstalledPaths) -> bool:
|
||||
"""Print the RAM/Flash lines ``ci_memory_impact_extract.py`` parses;
|
||||
False when skipped."""
|
||||
from esphome.arduino8266.framework import toolchain_tool
|
||||
from esphome.build_helpers.size_summary import print_size_line
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
str(toolchain_tool(paths.toolchain, "size")),
|
||||
"-A",
|
||||
"-d",
|
||||
str(build_dir / "firmware.elf"),
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
close_fds=False,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError) as err:
|
||||
# The summary is a bonus artifact like idedata; a truncated
|
||||
# toolchain extraction must not discard an already-linked build
|
||||
_LOGGER.warning("Could not summarize firmware size: %s", err)
|
||||
return False
|
||||
# -d prints decimal sizes; anything else trips the missing-sections guard
|
||||
sections = {
|
||||
parts[0]: int(parts[1])
|
||||
for line in result.stdout.splitlines()
|
||||
if (parts := line.split())[:1] and parts[0].startswith(".") and len(parts) >= 2
|
||||
if parts[1].isdigit()
|
||||
}
|
||||
if missing := set(_RAM_SECTIONS + _FLASH_SECTIONS) - set(sections):
|
||||
# A defaulted 0 would print a confidently wrong total for CI's metric
|
||||
_LOGGER.warning(
|
||||
"Size output is missing section(s) %s; skipping the size summary",
|
||||
", ".join(sorted(missing)),
|
||||
)
|
||||
return False
|
||||
# Resolve the flash budget before printing: a RAM line without its
|
||||
# Flash line would skew CI's memory-impact extraction
|
||||
app_size = _parse_app_size(build_dir, paths)
|
||||
if not app_size:
|
||||
return False
|
||||
ram = sum(sections[s] for s in _RAM_SECTIONS)
|
||||
flash = sum(sections[s] for s in _FLASH_SECTIONS)
|
||||
print_size_line("RAM", ram, _MAX_RAM_SIZE)
|
||||
print_size_line("Flash", flash, app_size)
|
||||
return True
|
||||
|
||||
|
||||
def get_idedata(ccache: str | None = None) -> dict | None:
|
||||
"""Derive idedata from the build's compile_commands.json (same
|
||||
contract as ``espidf.toolchain.get_idedata``)."""
|
||||
from esphome.build_helpers.idedata import load_or_build_idedata
|
||||
|
||||
# A disabled ccache resolves to None without spawning anything, so
|
||||
# re-resolving here costs nothing when the caller has no answer
|
||||
launcher = ccache or resolve_ccache_path()
|
||||
return load_or_build_idedata(
|
||||
get_build_dir() / "compile_commands.json",
|
||||
get_elf_path(),
|
||||
# Suffixed so a platformio->arduino->platformio round trip on one
|
||||
# config never serves the other toolchain's cache shape
|
||||
CORE.relative_internal_path("idedata", f"{CORE.name}.arduino.json"),
|
||||
# The compile DB's commands carry the same ccache prefix the ninja
|
||||
# rules were generated with
|
||||
launcher=str(launcher) if launcher else None,
|
||||
)
|
||||
+398
-45
@@ -1,5 +1,8 @@
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
import string
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -18,15 +21,17 @@ from esphome.const import (
|
||||
CONF_TYPE_ID,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
)
|
||||
from esphome.core import ID, Lambda
|
||||
from esphome.core import CORE, ID, EsphomeError, Lambda
|
||||
from esphome.cpp_generator import (
|
||||
FlashStringLiteral,
|
||||
LambdaExpression,
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
TemplateArgsType,
|
||||
call_lambda,
|
||||
)
|
||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
|
||||
from esphome.types import ConfigType
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
from esphome.util import Registry
|
||||
|
||||
|
||||
@@ -57,6 +62,7 @@ def maybe_conf(conf, *validators):
|
||||
with cv.remove_prepend_path([conf]):
|
||||
return validator({conf: value})
|
||||
|
||||
validate.inner_schema = validator
|
||||
return validate
|
||||
|
||||
|
||||
@@ -102,6 +108,101 @@ def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schem
|
||||
return CONDITION_REGISTRY.register(name, condition_type, schema)
|
||||
|
||||
|
||||
async def _build_with_parent(
|
||||
config: ConfigType,
|
||||
automation_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(automation_id, template_arg, parent)
|
||||
|
||||
|
||||
async def _build_without_parent(
|
||||
config: ConfigType,
|
||||
automation_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
return cg.new_Pvariable(automation_id, template_arg)
|
||||
|
||||
|
||||
async def _build_parented(
|
||||
config: ConfigType,
|
||||
automation_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(automation_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
def register_simple_action(
|
||||
name: str,
|
||||
action_type: MockObjClass,
|
||||
schema: cv.Schema,
|
||||
*,
|
||||
synchronous: bool,
|
||||
) -> None:
|
||||
"""Register an action whose constructor takes the object named by ``config[CONF_ID]``.
|
||||
|
||||
Use the ``register_action`` decorator instead when the builder must also set fields.
|
||||
"""
|
||||
register_action(name, action_type, schema, synchronous=synchronous)(
|
||||
_build_with_parent
|
||||
)
|
||||
|
||||
|
||||
def register_simple_condition(
|
||||
name: str, condition_type: MockObjClass, schema: cv.Schema
|
||||
) -> None:
|
||||
"""Condition counterpart of ``register_simple_action``."""
|
||||
register_condition(name, condition_type, schema)(_build_with_parent)
|
||||
|
||||
|
||||
def register_bare_action(
|
||||
name: str,
|
||||
action_type: MockObjClass,
|
||||
schema: cv.Schema,
|
||||
*,
|
||||
synchronous: bool,
|
||||
) -> None:
|
||||
"""Register an action whose constructor takes no arguments."""
|
||||
register_action(name, action_type, schema, synchronous=synchronous)(
|
||||
_build_without_parent
|
||||
)
|
||||
|
||||
|
||||
def register_bare_condition(
|
||||
name: str, condition_type: MockObjClass, schema: cv.Schema
|
||||
) -> None:
|
||||
"""Condition counterpart of ``register_bare_action``."""
|
||||
register_condition(name, condition_type, schema)(_build_without_parent)
|
||||
|
||||
|
||||
def register_parented_action(
|
||||
name: str,
|
||||
action_type: MockObjClass,
|
||||
schema: cv.Schema,
|
||||
*,
|
||||
synchronous: bool,
|
||||
) -> None:
|
||||
"""Register an action deriving from ``Parented<T>``.
|
||||
|
||||
The object is constructed without arguments and ``set_parent()`` receives the object
|
||||
named by ``config[CONF_ID]``.
|
||||
"""
|
||||
register_action(name, action_type, schema, synchronous=synchronous)(_build_parented)
|
||||
|
||||
|
||||
def register_parented_condition(
|
||||
name: str, condition_type: MockObjClass, schema: cv.Schema
|
||||
) -> None:
|
||||
"""Condition counterpart of ``register_parented_action``."""
|
||||
register_condition(name, condition_type, schema)(_build_parented)
|
||||
|
||||
|
||||
Action = cg.esphome_ns.class_("Action")
|
||||
Trigger = cg.esphome_ns.class_("Trigger")
|
||||
ACTION_REGISTRY = Registry()
|
||||
@@ -112,6 +213,295 @@ validate_action_list = cv.validate_registry("action", ACTION_REGISTRY)
|
||||
validate_condition = cv.validate_registry_entry("condition", CONDITION_REGISTRY)
|
||||
validate_condition_list = cv.validate_registry("condition", CONDITION_REGISTRY)
|
||||
|
||||
ApplyAction = cg.esphome_ns.class_("ApplyAction", Action)
|
||||
ApplyCondition = cg.esphome_ns.class_("ApplyCondition", Condition)
|
||||
|
||||
|
||||
def flash_string(config: ConfigType, value: str) -> str:
|
||||
"""Default renderer for ``std::string`` constants; copies the literal out of flash on ESP8266."""
|
||||
if CORE.is_esp8266:
|
||||
return f"progmem_string({FlashStringLiteral(value)})"
|
||||
return str(cg.safe_exp(value))
|
||||
|
||||
|
||||
def literal_with_length(config: ConfigType, value: str) -> str:
|
||||
"""Renderer for a ``(const char *, size_t)`` target: a plain literal plus its byte length.
|
||||
|
||||
The target compares or copies the bytes in place, so it needs the RAM literal rather than
|
||||
the PROGMEM rendering on ESP8266, and the length saves a strlen.
|
||||
"""
|
||||
return f"{cg.safe_exp(value)}, {len(value.encode('utf-8'))}"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApplyCall:
|
||||
"""One statement from config keys, e.g. ``"set_range({}, {})"`` with ``((CONF_LOW, cg.float_), ...)``.
|
||||
|
||||
Each arg is ``(conf_key, type_)`` or ``(conf_key, type_, const_fn)``. A ``conf_key`` may be a
|
||||
path into nested sections. A plain ``str`` ``type_`` is raw C++ type text and may use
|
||||
``{parent}``. ``const_fn(config, value)`` renders a constant's argument text; a lambda or an
|
||||
id bypasses it. The statement is skipped when none of its keys is set, always emitted when it
|
||||
has no keys, and a partial set is a config error.
|
||||
"""
|
||||
|
||||
target: str
|
||||
args: tuple[tuple[Any, ...], ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
fields = [
|
||||
f for _, f, _, _ in string.Formatter().parse(self.target) if f is not None
|
||||
]
|
||||
if any(fields):
|
||||
raise ValueError(
|
||||
f"apply target {self.target!r}: only bare {{}} placeholders"
|
||||
)
|
||||
if len(fields) != len(self.args):
|
||||
raise ValueError(
|
||||
f"apply target {self.target!r} has {len(fields)} "
|
||||
f"placeholder(s) for {len(self.args)} config key(s)"
|
||||
)
|
||||
if any(len(arg) not in (2, 3) for arg in self.args):
|
||||
raise ValueError(
|
||||
f"apply target {self.target!r}: each arg is (conf_key, type_[, const_fn])"
|
||||
)
|
||||
|
||||
@property
|
||||
def members(self) -> list[tuple[Any, Any, Any]]:
|
||||
"""Each arg as ``(conf_key, type_, const_fn or None)``."""
|
||||
return [
|
||||
(arg[0], arg[1], arg[2] if len(arg) == 3 else None) for arg in self.args
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ApplyField:
|
||||
"""One config key forwarded as ``target(value)``, or as statement ``target`` when it has ``{}``.
|
||||
|
||||
Double a literal brace in a template. ``conf_key`` may be a path into nested sections.
|
||||
``type_`` may be a C++ type string using ``{parent}`` when the type is only known per
|
||||
instance. ``const_fn(config, value)`` renders a constant's argument text when ``cg.safe_exp``
|
||||
is not the right spelling (unit conversion belongs in the validator); a lambda or an id
|
||||
bypasses it, so the target must also take a plain ``type_``. An absent key emits nothing.
|
||||
"""
|
||||
|
||||
conf_key: str | tuple[str, ...]
|
||||
target: str
|
||||
type_: SafeExpType
|
||||
const_fn: Callable[[ConfigType, Any], str] | None = None
|
||||
|
||||
def call(self) -> ApplyCall:
|
||||
target = self.target if "{}" in self.target else f"{self.target}({{}})"
|
||||
return ApplyCall(target, ((self.conf_key, self.type_, self.const_fn),))
|
||||
|
||||
|
||||
def _config_lookup(config: ConfigType, key: str | tuple[str, ...]) -> Any:
|
||||
if isinstance(key, str):
|
||||
return config.get(key)
|
||||
for part in key:
|
||||
if (config := config.get(part)) is None:
|
||||
return None
|
||||
return config
|
||||
|
||||
|
||||
def _dict_schema(schema: Any) -> Any:
|
||||
"""The dict-backed cv.Schema inside cv.All and maybe_* wrappers, or None; cv.Any is not inspected."""
|
||||
if isinstance(schema, dict):
|
||||
return cv.Schema(schema)
|
||||
if isinstance(getattr(schema, "schema", None), dict):
|
||||
return schema
|
||||
if isinstance(schema, cv.All):
|
||||
inner = schema.validators
|
||||
else:
|
||||
inner = (
|
||||
getattr(schema, "inner_schema", None),
|
||||
) # maybe_conf / maybe_simple_value
|
||||
for candidate in inner:
|
||||
if candidate is not None and (found := _dict_schema(candidate)) is not None:
|
||||
return found
|
||||
return None
|
||||
|
||||
|
||||
def _check_key_in_schema(
|
||||
name: str, schema: Any, conf_key: str | tuple[str, ...]
|
||||
) -> None:
|
||||
"""Reject a key path the schema does not have; a typo would otherwise be a silent no-op.
|
||||
|
||||
Only dict-backed schemas, also inside cv.All and maybe_* wrappers, can be checked.
|
||||
"""
|
||||
for part in (conf_key,) if isinstance(conf_key, str) else conf_key:
|
||||
if (schema := _dict_schema(schema)) is None:
|
||||
return
|
||||
markers = {
|
||||
getattr(marker, "schema", marker): marker for marker in schema.schema
|
||||
}
|
||||
if part not in markers:
|
||||
raise ValueError(f"{name}: config key {part!r} is not in the schema")
|
||||
schema = schema.schema[markers[part]]
|
||||
|
||||
|
||||
async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
|
||||
# Global-scope qualified so a trigger arg named like the id cannot shadow it.
|
||||
return f"::{await cg.get_variable(config[id_key])}"
|
||||
|
||||
|
||||
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
|
||||
# Must match ApplyAction::ApplyFn and ApplyCondition::CheckFn exactly for the function
|
||||
# pointer conversion.
|
||||
return [
|
||||
(cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &"), arg)
|
||||
for t, arg in args
|
||||
]
|
||||
|
||||
|
||||
async def _render_values(
|
||||
name: str,
|
||||
target: str,
|
||||
members: list[tuple[Any, Any, Any]],
|
||||
values: list[Any],
|
||||
config: ConfigType,
|
||||
parent: str,
|
||||
lambda_args: TemplateArgsType,
|
||||
compare: bool = False,
|
||||
) -> list[str]:
|
||||
"""Render the argument text of one statement; every key must be present.
|
||||
|
||||
``compare``: an inlined lambda expression is parenthesized so it binds as a whole
|
||||
beside an operator.
|
||||
"""
|
||||
if any(value is None for value in values):
|
||||
keys = [key for key, _, _ in members]
|
||||
raise EsphomeError(f"{name}: {target!r} needs all of {keys}")
|
||||
exprs: list[str] = []
|
||||
for (_, type_, const_fn), value in zip(members, values, strict=True):
|
||||
if isinstance(value, Lambda):
|
||||
if isinstance(type_, str):
|
||||
type_ = cg.RawExpression(type_.format(parent=parent))
|
||||
inner = await cg.process_lambda(value, lambda_args, return_type=type_)
|
||||
expr = call_lambda(inner)
|
||||
bare = compare and isinstance(expr, cg.RawExpression)
|
||||
exprs.append(f"({expr})" if bare else str(expr))
|
||||
elif isinstance(value, ID):
|
||||
# Qualified like the parent, so a trigger arg named like the id cannot shadow it.
|
||||
exprs.append(f"::{await cg.get_variable(value)}")
|
||||
elif const_fn is not None:
|
||||
exprs.append(const_fn(config, value))
|
||||
else:
|
||||
exprs.append(str(cg.safe_exp(value)))
|
||||
return exprs
|
||||
|
||||
|
||||
def _apply_values(config: ConfigType, members: list[tuple[Any, Any, Any]]) -> list[Any]:
|
||||
return [_config_lookup(config, key) for key, _, _ in members]
|
||||
|
||||
|
||||
def register_apply_action(
|
||||
name: str,
|
||||
schema: cv.Schema,
|
||||
*fields: ApplyField | ApplyCall,
|
||||
call: str | None = None,
|
||||
id_key: str = CONF_ID,
|
||||
) -> None:
|
||||
"""Register an action that only forwards config values to its parent, with no C++ class.
|
||||
|
||||
Generates one stateless function for ``ApplyAction<Ts...>``: the parent (read from
|
||||
``id_key``) and constants are baked in, lambdas are called inline with the trigger args.
|
||||
A constant that is an id (``cv.use_id`` under ``cv.templatable``) is the object it names.
|
||||
With ``call`` every statement targets the call object ``auto apply_call = parent->call()``,
|
||||
and ``apply_call.perform()`` is appended.
|
||||
"""
|
||||
# An action stores the value, so a std::string constant stays in flash on ESP8266.
|
||||
statements_spec = [
|
||||
(
|
||||
c.target,
|
||||
[
|
||||
(key, t, fn or (flash_string if t is cg.std_string else None))
|
||||
for key, t, fn in c.members
|
||||
],
|
||||
)
|
||||
for c in (f if isinstance(f, ApplyCall) else f.call() for f in fields)
|
||||
]
|
||||
_check_key_in_schema(name, schema, id_key)
|
||||
for _, members in statements_spec:
|
||||
for conf_key, _, _ in members:
|
||||
_check_key_in_schema(name, schema, conf_key)
|
||||
|
||||
async def builder(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
parent = await _apply_parent(config, id_key)
|
||||
lambda_args = _apply_lambda_args(args)
|
||||
receiver = "apply_call." if call else f"{parent}->"
|
||||
statements: list[str] = []
|
||||
for target, members in statements_spec:
|
||||
values = _apply_values(config, members)
|
||||
if members and all(value is None for value in values):
|
||||
continue
|
||||
exprs = await _render_values(
|
||||
name, target, members, values, config, parent, lambda_args
|
||||
)
|
||||
statements.append(f"{receiver}{target.format(*exprs)};")
|
||||
if call:
|
||||
statements = [
|
||||
f"auto apply_call = {parent}->{call}();",
|
||||
*statements,
|
||||
"apply_call.perform();",
|
||||
]
|
||||
apply_lambda = LambdaExpression(
|
||||
["\n".join(statements)], lambda_args, capture="", return_type=cg.void
|
||||
)
|
||||
return cg.new_Pvariable(action_id, template_arg, apply_lambda)
|
||||
|
||||
register_action(name, ApplyAction, schema, synchronous=True)(builder)
|
||||
|
||||
|
||||
def register_apply_condition(
|
||||
name: str, schema: cv.Schema, check: str | ApplyCall, id_key: str = CONF_ID
|
||||
) -> None:
|
||||
"""Register a condition that is one expression on its parent, with no C++ class.
|
||||
|
||||
``check`` is applied to the parent: ``"is_playing()"`` becomes ``parent->is_playing()``; an
|
||||
``ApplyCall`` such as ``ApplyCall("state == {}", ((CONF_STATE, cg.bool_),))`` compares
|
||||
against config values, all of which must be present. Write ``== false`` to negate.
|
||||
String constants are plain literals, so compare a ``std::string`` or ``StringRef`` member.
|
||||
Generates one stateless function for ``ApplyCondition<Ts...>``.
|
||||
"""
|
||||
call = check if isinstance(check, ApplyCall) else ApplyCall(check)
|
||||
members = call.members
|
||||
_check_key_in_schema(name, schema, id_key)
|
||||
for conf_key, _, _ in members:
|
||||
_check_key_in_schema(name, schema, conf_key)
|
||||
|
||||
async def builder(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
parent = await _apply_parent(config, id_key)
|
||||
lambda_args = _apply_lambda_args(args)
|
||||
exprs = await _render_values(
|
||||
name,
|
||||
call.target,
|
||||
members,
|
||||
_apply_values(config, members),
|
||||
config,
|
||||
parent,
|
||||
lambda_args,
|
||||
compare=True,
|
||||
)
|
||||
check_lambda = LambdaExpression(
|
||||
[f"return {parent}->{call.target.format(*exprs)};"],
|
||||
lambda_args,
|
||||
capture="",
|
||||
return_type=cg.bool_,
|
||||
)
|
||||
return cg.new_Pvariable(condition_id, template_arg, check_lambda)
|
||||
|
||||
register_condition(name, ApplyCondition, schema)(builder)
|
||||
|
||||
|
||||
def validate_potentially_and_condition(value):
|
||||
if isinstance(value, list):
|
||||
@@ -359,28 +749,15 @@ async def for_condition_to_code(
|
||||
return var
|
||||
|
||||
|
||||
@register_condition(
|
||||
register_apply_condition(
|
||||
"component.is_idle",
|
||||
LambdaCondition,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(cg.Component),
|
||||
}
|
||||
),
|
||||
"is_idle()",
|
||||
)
|
||||
async def component_is_idle_condition_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
lambda_ = await cg.process_lambda(
|
||||
Lambda(f"return {comp}->is_idle();"), args, return_type=bool
|
||||
)
|
||||
return new_lambda_pvariable(
|
||||
condition_id, lambda_, StatelessLambdaCondition, template_arg
|
||||
)
|
||||
|
||||
|
||||
@register_action(
|
||||
@@ -534,44 +911,20 @@ async def lambda_action_to_code(
|
||||
return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
|
||||
|
||||
|
||||
@register_action(
|
||||
register_simple_action(
|
||||
"component.update",
|
||||
UpdateComponentAction,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
||||
}
|
||||
),
|
||||
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
|
||||
synchronous=True,
|
||||
)
|
||||
async def component_update_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, comp)
|
||||
|
||||
|
||||
@register_action(
|
||||
register_simple_action(
|
||||
"component.suspend",
|
||||
SuspendComponentAction,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
||||
}
|
||||
),
|
||||
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(cg.PollingComponent)}),
|
||||
synchronous=True,
|
||||
)
|
||||
async def component_suspend_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
comp = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, comp)
|
||||
|
||||
|
||||
@register_action(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -90,9 +90,10 @@ def get_project_cmakelists(
|
||||
"""
|
||||
idf_target = variant_to_idf_target(get_esp32_variant())
|
||||
|
||||
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
|
||||
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
|
||||
# --format=raw because the legacy mode doesn't support it.
|
||||
# esp_idf_size 2.x (IDF >=6.0) made NG the default and removed --ng;
|
||||
# 1.x (IDF 5.5) needs --ng for --format=json2. 1.x json2 also lacks
|
||||
# total_size, hence the ELF fallback in espidf/size_summary.py; both
|
||||
# go away together when 1.x support is dropped.
|
||||
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
|
||||
|
||||
# Project-wide compile options: -D defines and -W warning flags (skip
|
||||
@@ -211,10 +212,12 @@ include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
|
||||
project({CORE.name})
|
||||
|
||||
# Emit raw JSON size data for ESPHome to read post-build.
|
||||
# Emit per-memory-type JSON size data for ESPHome to read post-build.
|
||||
# json2 stays small; raw dumps every symbol (~2s on a large map) and
|
||||
# this command runs inside the link edge, blocking everything downstream.
|
||||
add_custom_command(
|
||||
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
|
||||
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=json2
|
||||
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
|
||||
${{CMAKE_PROJECT_NAME}}.map
|
||||
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
|
||||
|
||||
@@ -11,6 +11,7 @@ consumers (IDE integration, clang-tidy) expect:
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
@@ -21,6 +22,8 @@ import subprocess
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.helpers import write_file
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Everything idedata generation may raise after a successful link; idedata
|
||||
# is a bonus artifact, so consumers warn instead of failing the build
|
||||
IDEDATA_BEST_EFFORT_ERRORS = (
|
||||
@@ -31,7 +34,21 @@ IDEDATA_BEST_EFFORT_ERRORS = (
|
||||
ValueError,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
def warn_if_idedata_missing(get_idedata: Callable[[], dict | None]) -> None:
|
||||
"""Run an idedata generator, downgrading any failure to a warning:
|
||||
the firmware already built."""
|
||||
try:
|
||||
if get_idedata() is None:
|
||||
_LOGGER.warning("No idedata was generated for this build")
|
||||
except IDEDATA_BEST_EFFORT_ERRORS as err:
|
||||
_LOGGER.warning(
|
||||
"Could not generate idedata: %s (IDE, clang-tidy, and "
|
||||
"memory-analysis data will be unavailable for this build)",
|
||||
err,
|
||||
)
|
||||
_LOGGER.debug("Idedata failure detail", exc_info=True)
|
||||
|
||||
|
||||
# C++ translation-unit suffixes used to identify ESPHome source files.
|
||||
_CXX_SUFFIXES = (".cpp", ".cc")
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Resolution of the native (non-PlatformIO) build backend for a config.
|
||||
|
||||
Kept deliberately light: the serial upload and logs fast path resolves a
|
||||
backend for its artifact paths alone, so importing this must not pull in a
|
||||
platform component package or the backend itself.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from types import ModuleType
|
||||
|
||||
from esphome.const import Toolchain
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
# Native build backend per (target platform, toolchain)
|
||||
NATIVE_TOOLCHAIN_MODULES = {
|
||||
("esp32", Toolchain.ESP_IDF): "esphome.espidf.toolchain",
|
||||
("esp8266", Toolchain.ARDUINO): "esphome.arduino8266.toolchain",
|
||||
}
|
||||
|
||||
|
||||
def native_backend() -> ModuleType | None:
|
||||
"""The native build backend module for the resolved toolchain."""
|
||||
if not CORE.using_native_toolchain:
|
||||
return None
|
||||
key = (CORE.target_platform, CORE.toolchain)
|
||||
if (module_path := NATIVE_TOOLCHAIN_MODULES.get(key)) is None:
|
||||
# Degrading to the PlatformIO path would build with the wrong backend
|
||||
raise EsphomeError(
|
||||
f"Toolchain '{CORE.toolchain.value}' has no native build backend "
|
||||
f"module for platform {CORE.target_platform}"
|
||||
)
|
||||
return importlib.import_module(module_path)
|
||||
|
||||
|
||||
# Binutils and the linked image for memory analysis, for toolchains that build
|
||||
# without PlatformIO but have no native build backend (which supplies them)
|
||||
ANALYSIS_TOOLCHAIN_MODULES = {
|
||||
("nrf52", Toolchain.SDK_NRF): "esphome.components.nrf52.toolchain",
|
||||
}
|
||||
|
||||
|
||||
def analysis_backend() -> ModuleType | None:
|
||||
"""The module giving objdump, readelf and the ELF of a non-PlatformIO build.
|
||||
|
||||
None means PlatformIO's idedata supplies them (or nothing can).
|
||||
"""
|
||||
if (native := native_backend()) is not None:
|
||||
return native
|
||||
module_path = ANALYSIS_TOOLCHAIN_MODULES.get((CORE.target_platform, CORE.toolchain))
|
||||
return importlib.import_module(module_path) if module_path else None
|
||||
@@ -64,7 +64,6 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
Application,
|
||||
Component,
|
||||
ComponentPtr,
|
||||
Controller,
|
||||
EntityBase,
|
||||
EntityCategory,
|
||||
ESPTime,
|
||||
|
||||
@@ -6,5 +6,6 @@ See the component-alias section of esphome/loader.py.
|
||||
|
||||
# alias -> (canonical component, removal version or None)
|
||||
COMPONENT_ALIASES: dict[str, tuple[str, str | None]] = {
|
||||
"esp32_improv": ("improv_ble", "2027.4.0"),
|
||||
"rp2040": ("rp2", "2027.7.0"),
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ class ADCSensor final : public sensor::Sensor, public PollingComponent, public v
|
||||
/// - SamplingMode::MIN: Use the lowest sample value
|
||||
/// - SamplingMode::MAX: Use the highest sample value
|
||||
/// @param sampling_mode The desired sampling mode to use for aggregating ADC samples.
|
||||
void set_sampling_mode(SamplingMode sampling_mode);
|
||||
void set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
|
||||
|
||||
/// Perform a single ADC sampling operation and return the measured value.
|
||||
/// This function handles raw readings, calibration, and averaging as needed.
|
||||
|
||||
@@ -76,6 +76,4 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
|
||||
}
|
||||
}
|
||||
|
||||
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
|
||||
|
||||
} // namespace esphome::adc
|
||||
|
||||
@@ -106,6 +106,20 @@ bool AGS10Component::set_zero_point_with_factory_defaults() { return this->set_z
|
||||
|
||||
bool AGS10Component::set_zero_point_with_current_resistance() { return this->set_zero_point_with(ZP_CURRENT); }
|
||||
|
||||
void AGS10Component::set_zero_point(AGS10SetZeroPointActionMode mode, uint16_t value) {
|
||||
switch (mode) {
|
||||
case FACTORY_DEFAULT:
|
||||
this->set_zero_point_with_factory_defaults();
|
||||
break;
|
||||
case CURRENT_VALUE:
|
||||
this->set_zero_point_with_current_resistance();
|
||||
break;
|
||||
case CUSTOM_VALUE:
|
||||
this->set_zero_point_with(value);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
bool AGS10Component::set_zero_point_with(uint16_t value) {
|
||||
std::array<uint8_t, 5> data{0x00, 0x0C, (uint8_t) ((value >> 8) & 0xFF), (uint8_t) (value & 0xFF), 0};
|
||||
data[4] = crc8(data.data(), 4, 0xFF, 0x31, true);
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::ags10 {
|
||||
|
||||
enum AGS10SetZeroPointActionMode {
|
||||
// Zero-point reset.
|
||||
FACTORY_DEFAULT,
|
||||
// Zero-point calibration with current resistance.
|
||||
CURRENT_VALUE,
|
||||
// Zero-point calibration with custom resistance.
|
||||
CUSTOM_VALUE,
|
||||
};
|
||||
|
||||
class AGS10Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
/**
|
||||
@@ -47,6 +55,11 @@ class AGS10Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
*/
|
||||
bool set_zero_point_with_current_resistance();
|
||||
|
||||
/**
|
||||
* Sets zero-point by mode; the value is only used for CUSTOM_VALUE.
|
||||
*/
|
||||
void set_zero_point(AGS10SetZeroPointActionMode mode, uint16_t value);
|
||||
|
||||
/**
|
||||
* Sets zero-point with the value.
|
||||
*/
|
||||
@@ -100,39 +113,4 @@ class AGS10Component final : public PollingComponent, public i2c::I2CDevice {
|
||||
template<size_t N> optional<std::array<uint8_t, N>> read_and_check_(uint8_t a_register);
|
||||
};
|
||||
|
||||
template<typename... Ts> class AGS10NewI2cAddressAction final : public Action<Ts...>, public Parented<AGS10Component> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, new_address)
|
||||
|
||||
void play(const Ts &...x) override { this->parent_->new_i2c_address(this->new_address_.value(x...)); }
|
||||
};
|
||||
|
||||
enum AGS10SetZeroPointActionMode {
|
||||
// Zero-point reset.
|
||||
FACTORY_DEFAULT,
|
||||
// Zero-point calibration with current resistance.
|
||||
CURRENT_VALUE,
|
||||
// Zero-point calibration with custom resistance.
|
||||
CUSTOM_VALUE,
|
||||
};
|
||||
|
||||
template<typename... Ts> class AGS10SetZeroPointAction final : public Action<Ts...>, public Parented<AGS10Component> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint16_t, value)
|
||||
TEMPLATABLE_VALUE(AGS10SetZeroPointActionMode, mode)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
switch (this->mode_.value(x...)) {
|
||||
case FACTORY_DEFAULT:
|
||||
this->parent_->set_zero_point_with_factory_defaults();
|
||||
break;
|
||||
case CURRENT_VALUE:
|
||||
this->parent_->set_zero_point_with_current_resistance();
|
||||
break;
|
||||
case CUSTOM_VALUE:
|
||||
this->parent_->set_zero_point_with(this->value_.value(x...));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace esphome::ags10
|
||||
|
||||
@@ -17,8 +17,6 @@ from esphome.const import (
|
||||
UNIT_OHM,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_RESISTANCE = "resistance"
|
||||
@@ -28,12 +26,6 @@ DEPENDENCIES = ["i2c"]
|
||||
ags10_ns = cg.esphome_ns.namespace("ags10")
|
||||
AGS10Component = ags10_ns.class_("AGS10Component", cg.PollingComponent, i2c.I2CDevice)
|
||||
|
||||
# Actions
|
||||
AGS10NewI2cAddressAction = ags10_ns.class_(
|
||||
"AGS10NewI2cAddressAction", automation.Action
|
||||
)
|
||||
AGS10SetZeroPointAction = ags10_ns.class_("AGS10SetZeroPointAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -91,24 +83,11 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"ags10.new_i2c_address",
|
||||
AGS10NewI2cAddressAction,
|
||||
AGS10_NEW_I2C_ADDRESS_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_ADDRESS, "new_i2c_address", cg.uint8),
|
||||
)
|
||||
async def ags10newi2caddress_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
|
||||
cg.add(var.set_new_address(address))
|
||||
return var
|
||||
|
||||
|
||||
AGS10SetZeroPointActionMode = ags10_ns.enum("AGS10SetZeroPointActionMode")
|
||||
AGS10_SET_ZERO_POINT_ACTION_MODE = {
|
||||
@@ -128,24 +107,11 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"ags10.set_zero_point",
|
||||
AGS10SetZeroPointAction,
|
||||
AGS10_SET_ZERO_POINT_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyCall(
|
||||
"set_zero_point({}, {})",
|
||||
((CONF_MODE, AGS10SetZeroPointActionMode), (CONF_VALUE, cg.uint16)),
|
||||
),
|
||||
)
|
||||
async def ags10setzeropoint_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
mode = await cg.templatable(
|
||||
config.get(CONF_MODE), args, AGS10SetZeroPointActionMode
|
||||
)
|
||||
cg.add(var.set_mode(mode))
|
||||
value = await cg.templatable(config[CONF_VALUE], args, cg.uint16)
|
||||
cg.add(var.set_value(value))
|
||||
return var
|
||||
|
||||
@@ -4,8 +4,6 @@ from esphome.components import i2c
|
||||
from esphome.components.audio_dac import AudioDac
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MODE
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
@@ -14,7 +12,6 @@ DEPENDENCIES = ["i2c"]
|
||||
aic3204_ns = cg.esphome_ns.namespace("aic3204")
|
||||
AIC3204 = aic3204_ns.class_("AIC3204", AudioDac, cg.Component, i2c.I2CDevice)
|
||||
|
||||
SetAutoMuteAction = aic3204_ns.class_("SetAutoMuteAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
@@ -36,25 +33,11 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"aic3204.set_auto_mute_mode",
|
||||
SetAutoMuteAction,
|
||||
SET_AUTO_MUTE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_MODE, "set_auto_mute_mode", cg.uint8),
|
||||
)
|
||||
async def aic3204_set_volume_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_MODE), args, cg.uint8)
|
||||
cg.add(var.set_auto_mute_mode(template_))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "aic3204.h"
|
||||
|
||||
namespace esphome::aic3204 {
|
||||
|
||||
template<typename... Ts> class SetAutoMuteAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit SetAutoMuteAction(AIC3204 *aic3204) : aic3204_(aic3204) {}
|
||||
|
||||
TEMPLATABLE_VALUE(uint8_t, auto_mute_mode)
|
||||
|
||||
void play(const Ts &...x) override { this->aic3204_->set_auto_mute_mode(this->auto_mute_mode_.value(x...)); }
|
||||
|
||||
protected:
|
||||
AIC3204 *aic3204_;
|
||||
};
|
||||
|
||||
} // namespace esphome::aic3204
|
||||
@@ -41,18 +41,6 @@ StateAnyForwarder = alarm_control_panel_ns.class_("StateAnyForwarder")
|
||||
StateEnterForwarder = alarm_control_panel_ns.class_("StateEnterForwarder")
|
||||
AlarmControlPanelState = alarm_control_panel_ns.enum("AlarmControlPanelState")
|
||||
|
||||
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
|
||||
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
|
||||
ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action)
|
||||
DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action)
|
||||
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
|
||||
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action)
|
||||
ChimeAction = alarm_control_panel_ns.class_("ChimeAction", automation.Action)
|
||||
ReadyAction = alarm_control_panel_ns.class_("ReadyAction", automation.Action)
|
||||
|
||||
AlarmControlPanelCondition = alarm_control_panel_ns.class_(
|
||||
"AlarmControlPanelCondition", automation.Condition
|
||||
)
|
||||
|
||||
_ALARM_CONTROL_PANEL_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
@@ -196,125 +184,38 @@ async def new_alarm_control_panel(config, *args):
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.arm_away",
|
||||
ArmAwayAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if code_config := config.get(CONF_CODE):
|
||||
templatable_ = await cg.templatable(code_config, args, cg.std_string)
|
||||
cg.add(var.set_code(templatable_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.arm_home",
|
||||
ArmHomeAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if code_config := config.get(CONF_CODE):
|
||||
templatable_ = await cg.templatable(code_config, args, cg.std_string)
|
||||
cg.add(var.set_code(templatable_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.arm_night",
|
||||
ArmNightAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if CONF_CODE in config:
|
||||
templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string)
|
||||
cg.add(var.set_code(templatable_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.disarm",
|
||||
DisarmAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
if code_config := config.get(CONF_CODE):
|
||||
templatable_ = await cg.templatable(code_config, args, cg.std_string)
|
||||
cg.add(var.set_code(templatable_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.pending",
|
||||
PendingAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.triggered",
|
||||
TriggeredAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.chime",
|
||||
ChimeAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.ready",
|
||||
ReadyAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_condition(
|
||||
"alarm_control_panel.ready",
|
||||
AlarmControlPanelCondition,
|
||||
ALARM_CONTROL_PANEL_CONDITION_SCHEMA,
|
||||
)
|
||||
async def alarm_action_ready_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"alarm_control_panel.is_armed",
|
||||
AlarmControlPanelCondition,
|
||||
ALARM_CONTROL_PANEL_CONDITION_SCHEMA,
|
||||
)
|
||||
async def alarm_control_panel_is_armed_to_code(
|
||||
config, condition_id, template_arg, args
|
||||
# Mirrors AlarmControlPanel::arm_with_code_: arm first, set the code only when given.
|
||||
for _name, _arm in (
|
||||
("alarm_control_panel.arm_away", "arm_away()"),
|
||||
("alarm_control_panel.arm_home", "arm_home()"),
|
||||
("alarm_control_panel.arm_night", "arm_night()"),
|
||||
("alarm_control_panel.disarm", "disarm()"),
|
||||
):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren)
|
||||
automation.register_apply_action(
|
||||
_name,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
automation.ApplyCall(_arm),
|
||||
automation.ApplyField(CONF_CODE, "set_code", cg.std_string),
|
||||
call="make_call",
|
||||
)
|
||||
|
||||
|
||||
for _name, _call in (
|
||||
("alarm_control_panel.pending", "pending()"),
|
||||
("alarm_control_panel.triggered", "triggered()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
automation.ApplyCall(_call),
|
||||
call="make_call",
|
||||
)
|
||||
|
||||
|
||||
for _name in ("alarm_control_panel.ready", "alarm_control_panel.is_armed"):
|
||||
automation.register_apply_condition(
|
||||
_name, ALARM_CONTROL_PANEL_CONDITION_SCHEMA, "is_armed_pending_or_triggered()"
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -130,6 +130,12 @@ class AlarmControlPanel : public EntityBase {
|
||||
|
||||
// is the state one of the armed states
|
||||
bool is_state_armed(AlarmControlPanelState state);
|
||||
/// Armed, pending (entry delay) or triggered; not ARMING (exit delay). Backs the is_armed and ready
|
||||
/// conditions.
|
||||
bool is_armed_pending_or_triggered() {
|
||||
auto state = this->get_state();
|
||||
return this->is_state_armed(state) || state == ACP_STATE_PENDING || state == ACP_STATE_TRIGGERED;
|
||||
}
|
||||
|
||||
protected:
|
||||
friend AlarmControlPanelCall;
|
||||
@@ -138,11 +144,11 @@ class AlarmControlPanel : public EntityBase {
|
||||
// in order to store last panel state in flash
|
||||
ESPPreferenceObject pref_;
|
||||
// current state
|
||||
AlarmControlPanelState current_state_;
|
||||
AlarmControlPanelState current_state_{ACP_STATE_DISARMED};
|
||||
// the desired (or previous) state
|
||||
AlarmControlPanelState desired_state_;
|
||||
AlarmControlPanelState desired_state_{ACP_STATE_DISARMED};
|
||||
// last time the state was updated
|
||||
uint32_t last_update_;
|
||||
uint32_t last_update_{0};
|
||||
// the call control function
|
||||
virtual void control(const AlarmControlPanelCall &call) = 0;
|
||||
// state callback - passes the new state to listeners
|
||||
|
||||
@@ -27,84 +27,4 @@ static_assert(std::is_trivially_copyable_v<StateAnyForwarder>);
|
||||
static_assert(sizeof(StateEnterForwarder<ACP_STATE_TRIGGERED>) <= sizeof(void *));
|
||||
static_assert(std::is_trivially_copyable_v<StateEnterForwarder<ACP_STATE_TRIGGERED>>);
|
||||
|
||||
template<typename... Ts> class ArmAwayAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, code)
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class ArmHomeAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit ArmHomeAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, code)
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class ArmNightAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit ArmNightAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, code)
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class DisarmAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit DisarmAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, code)
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class PendingAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->make_call().pending().perform(); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class TriggeredAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->make_call().triggered().perform(); }
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AlarmControlPanelCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {}
|
||||
bool check(const Ts &...x) override {
|
||||
return this->parent_->is_state_armed(this->parent_->get_state()) ||
|
||||
this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED;
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::alarm_control_panel
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
#pragma once
|
||||
#include "esphome/components/image/image.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
|
||||
namespace esphome::animation {
|
||||
|
||||
class Animation final : public image::Image {
|
||||
@@ -35,32 +33,4 @@ class Animation final : public image::Image {
|
||||
int loop_current_iteration_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AnimationNextFrameAction final : public Action<Ts...> {
|
||||
public:
|
||||
AnimationNextFrameAction(Animation *parent) : parent_(parent) {}
|
||||
void play(const Ts &...x) override { this->parent_->next_frame(); }
|
||||
|
||||
protected:
|
||||
Animation *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AnimationPrevFrameAction final : public Action<Ts...> {
|
||||
public:
|
||||
AnimationPrevFrameAction(Animation *parent) : parent_(parent) {}
|
||||
void play(const Ts &...x) override { this->parent_->prev_frame(); }
|
||||
|
||||
protected:
|
||||
Animation *parent_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class AnimationSetFrameAction final : public Action<Ts...> {
|
||||
public:
|
||||
AnimationSetFrameAction(Animation *parent) : parent_(parent) {}
|
||||
TEMPLATABLE_VALUE(uint16_t, frame)
|
||||
void play(const Ts &...x) override { this->parent_->set_frame(this->frame_.value(x...)); }
|
||||
|
||||
protected:
|
||||
Animation *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::animation
|
||||
|
||||
@@ -6,8 +6,6 @@ from esphome.components.file.image import image_schema, write_image
|
||||
from esphome.components.image import Image_, validate_settings
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_REPEAT
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@syndlex"]
|
||||
@@ -26,17 +24,6 @@ animation_ns = cg.esphome_ns.namespace("animation")
|
||||
|
||||
Animation_ = animation_ns.class_("Animation", Image_)
|
||||
|
||||
# Actions
|
||||
NextFrameAction = animation_ns.class_(
|
||||
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
PrevFrameAction = animation_ns.class_(
|
||||
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
|
||||
ANIMATION_SCHEMA = image_schema(Animation_).extend(
|
||||
{
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
@@ -72,28 +59,17 @@ SET_FRAME_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
|
||||
automation.register_apply_action(
|
||||
"animation.next_frame", NEXT_FRAME_SCHEMA, automation.ApplyCall("next_frame()")
|
||||
)
|
||||
@automation.register_action(
|
||||
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
|
||||
automation.register_apply_action(
|
||||
"animation.prev_frame", PREV_FRAME_SCHEMA, automation.ApplyCall("prev_frame()")
|
||||
)
|
||||
@automation.register_action(
|
||||
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
|
||||
automation.register_apply_action(
|
||||
"animation.set_frame",
|
||||
SET_FRAME_SCHEMA,
|
||||
automation.ApplyField(CONF_FRAME, "set_frame", cg.uint16),
|
||||
)
|
||||
async def animation_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
if (frame := config.get(CONF_FRAME)) is not None:
|
||||
template_ = await cg.templatable(frame, args, cg.uint16)
|
||||
cg.add(var.set_frame(template_))
|
||||
return var
|
||||
|
||||
|
||||
async def setup_animation(config: ConfigType) -> None:
|
||||
|
||||
@@ -81,7 +81,7 @@ def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||
|
||||
|
||||
api_ns = cg.esphome_ns.namespace("api")
|
||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
||||
APIServer = api_ns.class_("APIServer", cg.Component)
|
||||
HomeAssistantServiceCallAction = api_ns.class_(
|
||||
"HomeAssistantServiceCallAction", automation.Action
|
||||
)
|
||||
@@ -136,6 +136,12 @@ CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
||||
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
||||
|
||||
# Schema defaults that also match the C++ initializers in api_server.h; codegen
|
||||
# skips the setter when the config equals them.
|
||||
DEFAULT_PORT = 6053
|
||||
DEFAULT_REBOOT_TIMEOUT = "15min"
|
||||
DEFAULT_BATCH_DELAY = "100ms"
|
||||
|
||||
|
||||
def _register_provisioning_source(config: ConfigType) -> ConfigType:
|
||||
"""Register the API as a provisioning source when encryption is enabled.
|
||||
@@ -292,7 +298,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(APIServer),
|
||||
cv.Optional(CONF_PORT, default=6053): cv.port,
|
||||
cv.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port,
|
||||
# Removed in 2026.1.0 - kept to provide helpful error message
|
||||
cv.Optional(CONF_PASSWORD): cv.invalid(
|
||||
"The 'password' option has been removed in ESPHome 2026.1.0.\n"
|
||||
@@ -305,14 +311,14 @@ CONFIG_SCHEMA = cv.All(
|
||||
"Or visit https://esphome.io/components/api/#configuration-variables"
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_REBOOT_TIMEOUT, default="15min"
|
||||
CONF_REBOOT_TIMEOUT, default=DEFAULT_REBOOT_TIMEOUT
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.Exclusive(
|
||||
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
|
||||
): ACTIONS_SCHEMA,
|
||||
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
|
||||
cv.Optional(CONF_ENCRYPTION): encryption_schema,
|
||||
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
|
||||
cv.Optional(CONF_BATCH_DELAY, default=DEFAULT_BATCH_DELAY): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||
),
|
||||
@@ -350,10 +356,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
ln882x=5, # Moderate RAM
|
||||
nrf52=4, # ~256KB RAM, BSD sockets, Thread (single HA controller)
|
||||
): cv.int_range(min=1, max=20),
|
||||
# Maximum queued send buffers per connection before dropping connection
|
||||
# Each buffer uses ~8-12 bytes overhead plus actual message size
|
||||
# Max queued messages per connection, and 2 KB of backlog per slot up
|
||||
# to 64 KB (a lone message is exempt), before the connection is dropped
|
||||
# Platform defaults based on available RAM and typical message rates:
|
||||
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
|
||||
cv.SplitDefault(
|
||||
CONF_MAX_SEND_QUEUE,
|
||||
esp8266=4, # Limited RAM, need to fail fast
|
||||
@@ -457,15 +462,20 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
# Track controller registration for StaticVector sizing
|
||||
CORE.register_controller()
|
||||
CORE.register_controller(var)
|
||||
|
||||
# Request a log listener slot for API log streaming
|
||||
request_log_listener()
|
||||
|
||||
cg.add(var.set_port(config[CONF_PORT]))
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
||||
# Skip the setters when the config matches the C++ initializers (DEFAULT_*).
|
||||
if (port := config[CONF_PORT]) != DEFAULT_PORT:
|
||||
cg.add(var.set_port(port))
|
||||
if (reboot_timeout := config[CONF_REBOOT_TIMEOUT]) != cv.time_period(
|
||||
DEFAULT_REBOOT_TIMEOUT
|
||||
):
|
||||
cg.add(var.set_reboot_timeout(reboot_timeout))
|
||||
if (batch_delay := config[CONF_BATCH_DELAY]) != cv.time_period(DEFAULT_BATCH_DELAY):
|
||||
cg.add(var.set_batch_delay(batch_delay))
|
||||
if CONF_LISTEN_BACKLOG in config:
|
||||
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
|
||||
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
|
||||
|
||||
@@ -77,6 +77,7 @@ service APIConnection {
|
||||
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
|
||||
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -802,6 +803,9 @@ message SwitchStateResponse {
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
// If the switch does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
bool missing_state = 4;
|
||||
}
|
||||
message SwitchCommandRequest {
|
||||
option (id) = 33;
|
||||
@@ -1243,6 +1247,9 @@ message ClimateStateResponse {
|
||||
float current_humidity = 14;
|
||||
float target_humidity = 15;
|
||||
uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"];
|
||||
// If the climate device does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
bool missing_state = 17;
|
||||
}
|
||||
message ClimateCommandRequest {
|
||||
option (id) = 48;
|
||||
@@ -1329,6 +1336,9 @@ message WaterHeaterStateResponse {
|
||||
uint32 state = 6;
|
||||
float target_temperature_low = 7;
|
||||
float target_temperature_high = 8;
|
||||
// If the water heater does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
bool missing_state = 9;
|
||||
}
|
||||
|
||||
// Bitmask for WaterHeaterCommandRequest.has_fields
|
||||
@@ -2726,7 +2736,8 @@ enum SerialProxyParity {
|
||||
SERIAL_PROXY_PARITY_ODD = 2;
|
||||
}
|
||||
|
||||
// Configure UART parameters for a serial proxy instance
|
||||
// Configure UART parameters for a serial proxy instance. Only the subscribed client may
|
||||
// configure the port; others are refused with PORT_IN_USE (since API 1.17).
|
||||
message SerialProxyConfigureRequest {
|
||||
option (id) = 138;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2752,7 +2763,8 @@ message SerialProxyDataReceived {
|
||||
bytes data = 2; // Raw data received from the serial device
|
||||
}
|
||||
|
||||
// Write data to a serial device
|
||||
// Write data to a serial device. Only the subscribed client may write; writes from
|
||||
// others are ignored (since API 1.17).
|
||||
message SerialProxyWriteRequest {
|
||||
option (id) = 140;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2763,7 +2775,8 @@ message SerialProxyWriteRequest {
|
||||
bytes data = 2; // Raw data to write to the serial device
|
||||
}
|
||||
|
||||
// Set modem control pin states (RTS and DTR)
|
||||
// Set modem control pin states (RTS and DTR). Only the subscribed client may set them;
|
||||
// others are refused with PORT_IN_USE (since API 1.17).
|
||||
message SerialProxySetModemPinsRequest {
|
||||
option (id) = 141;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2802,6 +2815,7 @@ enum SerialProxyRequestType {
|
||||
// error the device answers with INVALID_ARGUMENT.
|
||||
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODE = 5; // Acknowledges a SerialProxySetModeRequest (since API 1.17)
|
||||
}
|
||||
|
||||
enum SerialProxyStatus {
|
||||
@@ -2814,7 +2828,8 @@ enum SerialProxyStatus {
|
||||
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
|
||||
}
|
||||
|
||||
// Generic request message for simple serial proxy operations
|
||||
// Generic request message for simple serial proxy operations. FLUSH requires an active
|
||||
// subscription; it is refused with PORT_IN_USE otherwise (since API 1.17).
|
||||
message SerialProxyRequest {
|
||||
option (id) = 144;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -2838,6 +2853,29 @@ message SerialProxyRequestResponse {
|
||||
string error_message = 4; // Additional detail on failure (optional)
|
||||
}
|
||||
|
||||
// How a port treats the bytes passing through it. RAW is a plain byte pipe; PROTOCOL
|
||||
// activates the port's protocol-aware tap (if one is configured), letting it observe
|
||||
// traffic and inject protocol bytes such as acknowledgements. Which protocol the tap
|
||||
// speaks is a property of the device configuration, discoverable from the tap
|
||||
// component's own API surface. A client that is about to flash firmware selects RAW
|
||||
// first, which definitively disables that injection.
|
||||
enum SerialProxyMode {
|
||||
SERIAL_PROXY_MODE_RAW = 0;
|
||||
SERIAL_PROXY_MODE_PROTOCOL = 1;
|
||||
}
|
||||
|
||||
// Only the subscribed client may change the mode; any other caller -- including one that
|
||||
// never subscribed -- is refused with PORT_IN_USE. PROTOCOL is refused with NOT_SUPPORTED
|
||||
// when the port has no protocol-aware tap configured.
|
||||
message SerialProxySetModeRequest {
|
||||
option (id) = 152;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1;
|
||||
SerialProxyMode mode = 2;
|
||||
}
|
||||
|
||||
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
||||
message BluetoothSetConnectionParamsRequest {
|
||||
option (id) = 145;
|
||||
|
||||
@@ -1,20 +1,37 @@
|
||||
#include "api_buffer.h"
|
||||
#include <new>
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
#include "esphome/core/log.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
void APIBuffer::debug_check_drop_(size_t drop) const {
|
||||
if (drop > this->size_) {
|
||||
ESP_LOGE("api.buffer", "drop_front: drop=%zu size=%u", drop, this->size_);
|
||||
abort();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool APIBuffer::grow_(size_t n) {
|
||||
// nothrow (no zero-fill) so OOM is reportable; plain new aborts instead
|
||||
// (NEW_OOM_ABORT on ESP8266 Arduino, exception stub on ESP-IDF).
|
||||
// RAMAllocator is no fit here: unique_ptr needs delete[]-compatible memory.
|
||||
std::unique_ptr<uint8_t[]> new_data(new (std::nothrow) uint8_t[n]);
|
||||
if (new_data == nullptr)
|
||||
if (n > MAX_SIZE)
|
||||
return false;
|
||||
if (this->size_)
|
||||
std::memcpy(new_data.get(), this->data_.get(), this->size_);
|
||||
this->data_ = std::move(new_data);
|
||||
// realloc extends in place when it can, avoiding the copy
|
||||
uint8_t *grown = RAMAllocator<uint8_t>().reallocate(this->data_.get(), n);
|
||||
if (grown == nullptr)
|
||||
return false;
|
||||
(void) this->data_.release(); // realloc already freed or reused the old block
|
||||
this->data_.reset(grown);
|
||||
this->capacity_ = n;
|
||||
return true;
|
||||
}
|
||||
|
||||
uint8_t *APIBuffer::append(size_t n) {
|
||||
const size_t old_size = this->size_;
|
||||
if (!this->resize(old_size + n))
|
||||
return nullptr;
|
||||
return this->data_.get() + old_size;
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -25,6 +25,7 @@ namespace esphome::api {
|
||||
/// writes in debug builds.
|
||||
class APIBuffer {
|
||||
public:
|
||||
static constexpr size_t MAX_SIZE = UINT16_MAX; // API frames carry 16 bit lengths
|
||||
void clear() { this->size_ = 0; }
|
||||
/// Returns false if allocation fails; the buffer is left unchanged.
|
||||
[[nodiscard]] inline bool reserve(size_t n) ESPHOME_ALWAYS_INLINE { return n <= this->capacity_ || this->grow_(n); }
|
||||
@@ -36,9 +37,19 @@ class APIBuffer {
|
||||
[[nodiscard]] inline bool reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
|
||||
if (!this->reserve(std::max(reserve_size, new_size)))
|
||||
return false;
|
||||
this->size_ = new_size;
|
||||
this->size_ = static_cast<uint16_t>(new_size);
|
||||
return true;
|
||||
}
|
||||
/// Grow by n bytes; returns the new bytes, or nullptr on allocation failure.
|
||||
[[nodiscard]] uint8_t *append(size_t n);
|
||||
/// Drop the first `drop` bytes, sliding the rest down. Precondition: drop <= size().
|
||||
void drop_front(size_t drop) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
this->debug_check_drop_(drop);
|
||||
#endif
|
||||
this->size_ -= drop;
|
||||
std::memmove(this->data_.get(), this->data_.get() + drop, this->size_);
|
||||
}
|
||||
uint8_t *data() { return this->data_.get(); }
|
||||
const uint8_t *data() const { return this->data_.get(); }
|
||||
size_t size() const { return this->size_; }
|
||||
@@ -55,9 +66,14 @@ class APIBuffer {
|
||||
|
||||
protected:
|
||||
bool grow_(size_t n);
|
||||
std::unique_ptr<uint8_t[]> data_;
|
||||
size_t size_{0};
|
||||
size_t capacity_{0};
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
void debug_check_drop_(size_t drop) const;
|
||||
#endif
|
||||
// RAMAllocator: PSRAM when available, and it reports failure where
|
||||
// new (std::nothrow) still aborts on ESP-IDF without exceptions
|
||||
RAMUniquePtr<uint8_t[]> data_;
|
||||
uint16_t size_{0};
|
||||
uint16_t capacity_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -364,7 +364,10 @@ void APIConnection::check_keepalive_(uint32_t now) {
|
||||
ESP_LOGVV(TAG, "Sending keepalive PING");
|
||||
PingRequest req;
|
||||
this->flags_.sent_ping = this->send_message(req);
|
||||
if (!this->flags_.sent_ping) {
|
||||
if (this->flags_.sent_ping) {
|
||||
// Quiet for a keepalive period and the ping is on its way: a one-off stall's storage can go
|
||||
this->helper_->release_overflow_buffer();
|
||||
} else {
|
||||
// If we can't send the ping request directly (tx_buffer full),
|
||||
// schedule it at the front of the batch so it will be sent with priority
|
||||
ESP_LOGW(TAG, "Buffer full, ping queued");
|
||||
@@ -597,7 +600,7 @@ bool APIConnection::send_light_state(light::LightState *light) {
|
||||
uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *light = static_cast<light::LightState *>(entity);
|
||||
LightStateResponse resp;
|
||||
auto values = light->remote_values;
|
||||
auto values = light->get_reported_values();
|
||||
auto color_mode = values.get_color_mode();
|
||||
resp.state = values.is_on();
|
||||
resp.color_mode = static_cast<enums::ColorMode>(color_mode);
|
||||
@@ -706,6 +709,7 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection
|
||||
auto *a_switch = static_cast<switch_::Switch *>(entity);
|
||||
SwitchStateResponse resp;
|
||||
resp.state = a_switch->state;
|
||||
resp.missing_state = !a_switch->has_state();
|
||||
return fill_and_encode_entity_state(a_switch, resp, conn, remaining_size);
|
||||
}
|
||||
|
||||
@@ -717,12 +721,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
|
||||
}
|
||||
void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
|
||||
ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
|
||||
|
||||
if (msg.state) {
|
||||
a_switch->turn_on();
|
||||
} else {
|
||||
a_switch->turn_off();
|
||||
}
|
||||
a_switch->control(msg.state);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -756,6 +755,7 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
||||
auto traits = climate->get_traits();
|
||||
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
|
||||
resp.action = static_cast<enums::ClimateAction>(climate->action);
|
||||
resp.missing_state = !climate->has_state();
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE))
|
||||
resp.current_temperature = climate->current_temperature;
|
||||
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
@@ -1449,6 +1449,7 @@ uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConne
|
||||
auto *wh = static_cast<water_heater::WaterHeater *>(entity);
|
||||
WaterHeaterStateResponse resp;
|
||||
resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode());
|
||||
resp.missing_state = !wh->has_state();
|
||||
resp.current_temperature = wh->get_current_temperature();
|
||||
resp.target_temperature = wh->get_target_temperature();
|
||||
resp.target_temperature_low = wh->get_target_temperature_low();
|
||||
@@ -1661,6 +1662,7 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
break;
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
|
||||
// Response-only discriminators; never valid in a request
|
||||
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
|
||||
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||
@@ -1673,6 +1675,19 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
send_serial_proxy_ack(this, msg.instance, msg.type, status);
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
|
||||
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
|
||||
return;
|
||||
}
|
||||
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_mode_from_client(this, msg.mode);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE,
|
||||
serial_proxy_result_to_status(result));
|
||||
}
|
||||
|
||||
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
|
||||
if (!this->send_message(msg)) {
|
||||
ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
|
||||
@@ -1799,7 +1814,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 16;
|
||||
resp.api_version_minor = 17;
|
||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||
resp.server_info = ESPHOME_VERSION_REF;
|
||||
resp.name = StringRef(App.get_name());
|
||||
@@ -2255,7 +2270,12 @@ bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type,
|
||||
// Capacity reserved above, cannot fail
|
||||
(void) shared_buf.resize(write_start + payload_size);
|
||||
ProtoWriteBuffer buffer{&shared_buf, write_start};
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
uint8_t *end = encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
proto_check_encode_end(end, shared_buf.data() + shared_buf.size());
|
||||
#else
|
||||
(void) end;
|
||||
#endif
|
||||
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
|
||||
}
|
||||
// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE)
|
||||
|
||||
@@ -244,6 +244,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg);
|
||||
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg);
|
||||
void on_serial_proxy_request(const SerialProxyRequest &msg);
|
||||
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &msg);
|
||||
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
|
||||
#endif
|
||||
|
||||
@@ -345,11 +346,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
/// Returns false as soon as the TCP buffer is full. Marked nodiscard so we
|
||||
/// have no silent failures: every caller must handle (or log) a refusal.
|
||||
template<typename T> [[nodiscard]] bool send_message(const T &msg) {
|
||||
if constexpr (T::ESTIMATED_SIZE == 0) {
|
||||
return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg);
|
||||
} else {
|
||||
return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg);
|
||||
}
|
||||
return this->send_message_(T::calc_size_msg(&msg), T::MESSAGE_TYPE, &T::encode_msg, &msg);
|
||||
}
|
||||
|
||||
/// Clear the shared write buffer and reserve space for the first message.
|
||||
@@ -405,16 +402,6 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void process_state_subscriptions_();
|
||||
#endif
|
||||
|
||||
// Size thunk — converts void* back to concrete type for direct calculate_size() call
|
||||
template<typename T> static uint32_t calc_size(const void *msg) {
|
||||
return static_cast<const T *>(msg)->calculate_size();
|
||||
}
|
||||
|
||||
// Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
|
||||
static uint8_t *encode_msg_noop(const void *, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
|
||||
return buf.get_pos();
|
||||
}
|
||||
|
||||
// Non-template buffer management for send_message
|
||||
bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||
|
||||
@@ -433,11 +420,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Hot paths (state/info) go through fill_and_encode_entity_state/info instead.
|
||||
// batch_message_type_ is already set by dispatch_message_ before reaching here.
|
||||
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
|
||||
if constexpr (T::ESTIMATED_SIZE == 0) {
|
||||
return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size);
|
||||
} else {
|
||||
return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
|
||||
}
|
||||
return encode_to_buffer_slow(T::calc_size_msg(&msg), &T::encode_msg, &msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
// Non-template core — fills state fields and encodes
|
||||
@@ -449,7 +432,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
template<typename T>
|
||||
static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
|
||||
return fill_and_encode_entity_state(entity, msg, &T::calc_size_msg, &T::encode_msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
// Non-template core — fills info fields, allocates buffers, and encodes
|
||||
@@ -461,7 +444,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
template<typename T>
|
||||
static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
|
||||
return fill_and_encode_entity_info(entity, msg, &T::calc_size_msg, &T::encode_msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
// Non-template core — fills device_class, then delegates to fill_and_encode_entity_info
|
||||
@@ -475,8 +458,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg,
|
||||
StringRef &device_class_field, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>,
|
||||
&proto_encode_msg<T>, conn, remaining_size);
|
||||
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &T::calc_size_msg,
|
||||
&T::encode_msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
|
||||
@@ -46,7 +46,13 @@ inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t c
|
||||
return 0;
|
||||
}
|
||||
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
uint8_t *end = encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
// A body that writes fewer bytes than calculate_size() promised would ship stale buffer bytes
|
||||
proto_check_encode_end(end, shared_buf.data() + shared_buf.size());
|
||||
#else
|
||||
(void) end;
|
||||
#endif
|
||||
|
||||
return total_calculated_size;
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin
|
||||
return APIError::OK;
|
||||
|
||||
// Queue unsent data into overflow buffer
|
||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
|
||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, sent)) {
|
||||
HELPER_LOG("Overflow buffer full or out of memory, dropping connection");
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
|
||||
@@ -219,7 +219,10 @@ class APIFrameHelper {
|
||||
if (this->rx_buf_len_ == 0) {
|
||||
this->rx_buf_.release();
|
||||
}
|
||||
this->release_overflow_buffer();
|
||||
}
|
||||
// Free the send backlog storage once it has drained
|
||||
void release_overflow_buffer() { this->overflow_buf_.release(); }
|
||||
|
||||
protected:
|
||||
// Drain backlogged overflow data to the socket and handle errors.
|
||||
|
||||
@@ -5,16 +5,13 @@
|
||||
#include "esphome/components/noise/noise.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
#include <cstring>
|
||||
#include <cinttypes>
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
using noise::noise_err_to_logstr;
|
||||
@@ -26,11 +23,7 @@ static_assert(MAX_HANDSHAKE_SIZE == noise::MAX_HANDSHAKE_SIZE,
|
||||
"api and noise component handshake size limits must match");
|
||||
|
||||
static const char *const TAG = "api.noise";
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
|
||||
#else
|
||||
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
|
||||
#endif
|
||||
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
||||
|
||||
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
|
||||
@@ -67,16 +60,12 @@ APIError APINoiseFrameHelper::init() {
|
||||
}
|
||||
|
||||
// init prologue
|
||||
size_t old_size = prologue_.size();
|
||||
if (!prologue_.resize(old_size + PROLOGUE_INIT_LEN)) [[unlikely]] {
|
||||
uint8_t *dst = prologue_.append(PROLOGUE_INIT_LEN);
|
||||
if (dst == nullptr) [[unlikely]] {
|
||||
state_ = State::FAILED;
|
||||
return APIError::OUT_OF_MEMORY;
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
||||
#else
|
||||
std::memcpy(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
||||
#endif
|
||||
progmem_memcpy(dst, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
||||
|
||||
state_ = State::CLIENT_HELLO;
|
||||
return APIError::OK;
|
||||
@@ -272,17 +261,17 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
|
||||
return handle_handshake_frame_error_(aerr);
|
||||
}
|
||||
// ignore contents, may be used in future for flags
|
||||
// Resize for: existing prologue + 2 size bytes + frame data
|
||||
size_t old_size = this->prologue_.size();
|
||||
// Append 2 size bytes + frame data to the prologue
|
||||
size_t rx_size = this->rx_buf_.size();
|
||||
if (!this->prologue_.resize(old_size + 2 + rx_size)) [[unlikely]] {
|
||||
uint8_t *dst = this->prologue_.append(2 + rx_size);
|
||||
if (dst == nullptr) [[unlikely]] {
|
||||
state_ = State::FAILED;
|
||||
return APIError::OUT_OF_MEMORY;
|
||||
}
|
||||
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
|
||||
this->prologue_[old_size + 1] = (uint8_t) rx_size;
|
||||
dst[0] = (uint8_t) (rx_size >> 8);
|
||||
dst[1] = (uint8_t) rx_size;
|
||||
if (rx_size > 0) {
|
||||
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
|
||||
std::memcpy(dst + 2, this->rx_buf_.data(), rx_size);
|
||||
}
|
||||
|
||||
state_ = State::SERVER_HELLO;
|
||||
|
||||
@@ -1,98 +1,91 @@
|
||||
#include "api_overflow_buffer.h"
|
||||
#ifdef USE_API
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
APIOverflowBuffer::~APIOverflowBuffer() {
|
||||
for (auto *entry : this->queue_) {
|
||||
if (entry != nullptr)
|
||||
Entry::destroy(entry);
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
|
||||
// socket->write() can re-enter this function: a log message emitted from an
|
||||
// lwip callback during the write goes out over the API and lands back in the
|
||||
// frame helper's write/drain path. If a nested drain ran here it would send
|
||||
// and free the entry the outer drain is still holding, causing a double free.
|
||||
// Report "no progress" instead; the outer drain keeps draining, and the
|
||||
// nested send is enqueued behind the existing backlog.
|
||||
// Nested call from inside socket->write(); see draining_
|
||||
if (this->draining_)
|
||||
return 0;
|
||||
|
||||
// RAII so the flag is cleared on every return path
|
||||
struct DrainGuard {
|
||||
explicit DrainGuard(bool &flag) : flag_(flag) { flag_ = true; }
|
||||
~DrainGuard() { this->flag_ = false; }
|
||||
bool &flag_;
|
||||
} guard(this->draining_);
|
||||
APIOverflowBuffer &owner;
|
||||
~DrainGuard() { this->owner.draining_ = false; }
|
||||
} guard{*this};
|
||||
this->draining_ = true;
|
||||
|
||||
while (this->count_ > 0) {
|
||||
Entry *front = this->queue_[this->head_];
|
||||
uint8_t *msg = this->buf_.data() + this->head_;
|
||||
size_t len = msg[0] | (msg[1] << 8);
|
||||
|
||||
ssize_t sent = socket->write(front->current_data(), front->remaining());
|
||||
|
||||
if (sent <= 0) {
|
||||
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
|
||||
// 0 = nothing sent (treat as no progress)
|
||||
ssize_t sent = socket->write(msg + LEN_PREFIX, len);
|
||||
if (sent <= 0)
|
||||
return sent;
|
||||
if (static_cast<size_t>(sent) < len) {
|
||||
// Step past the sent bytes and rewrite the prefix there; it lands on bytes already sent
|
||||
this->head_ += sent;
|
||||
len -= sent;
|
||||
msg += sent;
|
||||
msg[0] = len;
|
||||
msg[1] = len >> 8;
|
||||
return sent;
|
||||
}
|
||||
|
||||
if (static_cast<uint16_t>(sent) < front->remaining()) {
|
||||
// Partially sent, update offset and stop
|
||||
front->offset += static_cast<uint16_t>(sent);
|
||||
return sent;
|
||||
}
|
||||
|
||||
// Entry fully sent — unlink it before freeing so a freed pointer is never
|
||||
// reachable from the queue
|
||||
this->queue_[this->head_] = nullptr;
|
||||
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->head_ += LEN_PREFIX + len;
|
||||
this->count_--;
|
||||
Entry::destroy(front);
|
||||
}
|
||||
|
||||
return 0; // All drained
|
||||
this->head_ = 0;
|
||||
if (this->release_when_drained_) {
|
||||
this->release_when_drained_ = false;
|
||||
this->buf_.release();
|
||||
} else {
|
||||
this->buf_.clear();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
|
||||
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip) {
|
||||
if (this->count_ >= API_MAX_SEND_QUEUE)
|
||||
return false;
|
||||
|
||||
uint16_t buffer_size = total_len - skip;
|
||||
// nothrow: a failed allocation returns nullptr so the connection is dropped
|
||||
// cleanly instead of plain new's crash or abort on OOM
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
|
||||
auto *data = new (std::nothrow) uint8_t[buffer_size];
|
||||
if (data == nullptr)
|
||||
return false;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
|
||||
auto *entry = new (std::nothrow) Entry{data, buffer_size, 0};
|
||||
if (entry == nullptr) {
|
||||
delete[] data;
|
||||
const size_t new_len = total_len - skip;
|
||||
const size_t new_bytes = LEN_PREFIX + new_len;
|
||||
const size_t live = this->buf_.size() - this->head_;
|
||||
// A lone message is only bound by the buffer; refusing it would just drop the connection
|
||||
if (live + new_bytes > (this->count_ > 0 ? MAX_BYTES : MAX_LONE_BYTES))
|
||||
return false;
|
||||
|
||||
if (this->buf_.size() + new_bytes > this->buf_.capacity()) {
|
||||
// Storage would move under an outer drain's write()
|
||||
if (this->draining_)
|
||||
return false;
|
||||
if (this->head_ > 0) {
|
||||
// Reclaim the sent prefix before growing
|
||||
this->buf_.drop_front(this->head_);
|
||||
this->head_ = 0;
|
||||
}
|
||||
if (!this->buf_.reserve(reserve_for(live + new_bytes)))
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t to_skip = skip;
|
||||
uint16_t write_pos = 0;
|
||||
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
if (to_skip >= iov[i].iov_len) {
|
||||
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
|
||||
uint8_t *dst = this->buf_.append(new_bytes);
|
||||
if (dst == nullptr)
|
||||
return false;
|
||||
dst[0] = new_len;
|
||||
dst[1] = new_len >> 8;
|
||||
dst += LEN_PREFIX;
|
||||
for (const struct iovec *end = iov + iovcnt; iov != end; iov++) {
|
||||
if (skip >= iov->iov_len) {
|
||||
skip -= iov->iov_len;
|
||||
} else {
|
||||
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
|
||||
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
|
||||
std::memcpy(entry->data + write_pos, src, len);
|
||||
write_pos += len;
|
||||
to_skip = 0;
|
||||
const size_t len = iov->iov_len - skip;
|
||||
std::memcpy(dst, static_cast<const uint8_t *>(iov->iov_base) + skip, len);
|
||||
dst += len;
|
||||
skip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Publish only after the copy completes so a half-built entry is never reachable
|
||||
this->queue_[this->tail_] = entry;
|
||||
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->count_++;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <sys/types.h>
|
||||
|
||||
@@ -8,71 +9,57 @@
|
||||
|
||||
#include "esphome/components/socket/headers.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "api_buffer.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
|
||||
///
|
||||
/// Under normal operation this buffer is **never used** — data goes straight
|
||||
/// from the frame helper to the socket. It only fills when the LWIP TCP
|
||||
/// send buffer is full (slow client, congested network, heavy logging).
|
||||
/// The queue drains automatically on subsequent write/loop calls once the
|
||||
/// socket becomes writable again.
|
||||
///
|
||||
/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python
|
||||
/// config). If the queue fills completely the connection is marked failed.
|
||||
/// TCP send backlog, only used when the socket send buffer is full.
|
||||
/// One contiguous buffer per connection, allocated on the first stall and
|
||||
/// kept at its high-water mark so a lossy link does not churn the heap.
|
||||
/// Messages are stored as a 2 byte length prefix plus payload.
|
||||
/// API_MAX_SEND_QUEUE bounds queued messages and, at 2 KB per slot, queued
|
||||
/// bytes; exceeding either fails the connection.
|
||||
class APIOverflowBuffer {
|
||||
public:
|
||||
/// A single heap-allocated send-backlog entry.
|
||||
/// Lifetime is manually managed — see destroy().
|
||||
struct Entry {
|
||||
uint8_t *data;
|
||||
uint16_t size; // Total size of the buffer
|
||||
uint16_t offset; // Current send offset within the buffer
|
||||
|
||||
uint16_t remaining() const { return this->size - this->offset; }
|
||||
const uint8_t *current_data() const { return this->data + this->offset; }
|
||||
|
||||
/// Free this entry and its data buffer.
|
||||
static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) {
|
||||
delete[] entry->data;
|
||||
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
};
|
||||
|
||||
~APIOverflowBuffer();
|
||||
|
||||
/// True when no backlogged data is waiting.
|
||||
bool empty() const { return this->count_ == 0; }
|
||||
|
||||
/// True when the queue has no room for another entry.
|
||||
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
|
||||
|
||||
/// Number of entries currently queued.
|
||||
uint8_t count() const { return this->count_; }
|
||||
|
||||
/// Try to drain queued data to the socket.
|
||||
/// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress,
|
||||
/// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors).
|
||||
/// Callers only need to act on -1; 0 and positive values both mean "no error".
|
||||
/// Frees entries as they are fully sent.
|
||||
/// Drain queued messages to the socket.
|
||||
/// Returns bytes written, 0 for a re-entrant call, -1 on error (check errno
|
||||
/// for EWOULDBLOCK); callers only need to act on -1.
|
||||
ssize_t try_drain(socket::Socket *socket);
|
||||
|
||||
/// Enqueue unsent IOV data into the backlog.
|
||||
/// Copies iov data starting at byte offset `skip` into a new entry.
|
||||
/// Returns false if the queue is full or allocation fails (caller should fail the connection).
|
||||
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
|
||||
/// Queue iov data from byte offset `skip` as one message.
|
||||
/// Returns false when a limit is hit, allocation fails, or storage would move
|
||||
/// during a drain; the caller should fail the connection.
|
||||
bool enqueue_iov(const struct iovec *iov, int iovcnt, size_t total_len, size_t skip);
|
||||
|
||||
/// Free the retained storage, now if empty, otherwise once it has drained.
|
||||
void release() {
|
||||
if (this->count_ == 0) {
|
||||
this->buf_.release();
|
||||
} else {
|
||||
this->release_when_drained_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
|
||||
uint8_t head_{0};
|
||||
uint8_t tail_{0};
|
||||
static constexpr size_t LEN_PREFIX = 2;
|
||||
static constexpr size_t BYTES_PER_SLOT = 2048;
|
||||
// Reserve in 256 byte steps so a creeping high-water mark settles quickly
|
||||
static constexpr size_t GROW_QUANTUM = 256;
|
||||
// Lone message ceiling, rounded down so reserve_for() never exceeds the buffer limit
|
||||
static constexpr size_t MAX_LONE_BYTES = APIBuffer::MAX_SIZE & ~(GROW_QUANTUM - 1);
|
||||
static constexpr size_t MAX_BYTES = std::min(API_MAX_SEND_QUEUE * BYTES_PER_SLOT, MAX_LONE_BYTES);
|
||||
static constexpr size_t reserve_for(size_t want) { return (want + GROW_QUANTUM - 1) & ~(GROW_QUANTUM - 1); }
|
||||
|
||||
APIBuffer buf_;
|
||||
uint16_t head_{0}; // offset of the front message's length prefix; bytes before it are sent
|
||||
uint8_t count_{0};
|
||||
// Guards against re-entrant drains: socket->write() can re-enter the API
|
||||
// send path (e.g. a log message emitted from an lwip callback), and a nested
|
||||
// drain would free the entry the outer drain is still holding.
|
||||
bool draining_{false};
|
||||
// socket->write() can re-enter the send path (log from an lwip callback):
|
||||
// a nested drain makes no progress and a nested enqueue never moves storage
|
||||
bool draining_ : 1 {false};
|
||||
bool release_when_drained_ : 1 {false};
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
+2672
-2629
File diff suppressed because it is too large
Load Diff
+901
-354
File diff suppressed because it is too large
Load Diff
@@ -854,6 +854,8 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODE:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODE");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
@@ -878,6 +880,16 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyMode>(enums::SerialProxyMode value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_MODE_RAW:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_MODE_RAW");
|
||||
case enums::SERIAL_PROXY_MODE_PROTOCOL:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_MODE_PROTOCOL");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -1330,6 +1342,7 @@ const char *SwitchStateResponse::dump_to(DumpBuffer &out) const {
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SwitchCommandRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -1672,6 +1685,7 @@ const char *ClimateStateResponse::dump_to(DumpBuffer &out) const {
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ClimateCommandRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -1739,6 +1753,7 @@ const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("state"), this->state);
|
||||
dump_field(out, ESPHOME_PSTR("target_temperature_low"), this->target_temperature_low);
|
||||
dump_field(out, ESPHOME_PSTR("target_temperature_high"), this->target_temperature_high);
|
||||
dump_field(out, ESPHOME_PSTR("missing_state"), this->missing_state);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *WaterHeaterCommandRequest::dump_to(DumpBuffer &out) const {
|
||||
@@ -2805,6 +2820,12 @@ const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
|
||||
dump_field(out, ESPHOME_PSTR("error_message"), this->error_message);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxySetModeRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxySetModeRequest"));
|
||||
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
|
||||
dump_field(out, ESPHOME_PSTR("mode"), static_cast<enums::SerialProxyMode>(this->mode));
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -712,6 +712,17 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
this->on_device_capabilities_request();
|
||||
break;
|
||||
}
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case SerialProxySetModeRequest::MESSAGE_TYPE: {
|
||||
SerialProxySetModeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_serial_proxy_set_mode_request"), msg);
|
||||
#endif
|
||||
this->on_serial_proxy_set_mode_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -235,6 +235,9 @@ class APIServerConnectionBase {
|
||||
void on_serial_proxy_request(const SerialProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void on_serial_proxy_set_mode_request(const SerialProxySetModeRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "api_connection.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -29,6 +28,29 @@ static const char *const TAG = "api";
|
||||
// APIServer
|
||||
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
static constexpr uint32_t NOISE_PSK_PREF_HASH = 88491486UL;
|
||||
#endif
|
||||
|
||||
#if defined(USE_API_NOISE) && defined(USE_OTA_ENCRYPTION_PROVISIONED)
|
||||
bool load_saved_noise_psk(noise::psk_t &out) {
|
||||
SavedNoisePsk saved;
|
||||
#ifdef USE_PREFERENCE_KEY_LOOKUP
|
||||
const bool loaded =
|
||||
global_preferences->load_from_key(NOISE_PSK_PREF_HASH, reinterpret_cast<uint8_t *>(&saved), sizeof(saved));
|
||||
#else
|
||||
// Slot backends need the reservation walk; it only lands on the record when the reservations before
|
||||
// it match a normal boot, otherwise the type checked checksum fails the load
|
||||
const bool loaded = global_preferences->make_preference<SavedNoisePsk>(NOISE_PSK_PREF_HASH, true).load(&saved);
|
||||
#endif
|
||||
// The all-zeros record means no key
|
||||
if (!loaded || noise::NoiseContext::is_all_zeros(saved.psk))
|
||||
return false;
|
||||
out = saved.psk;
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
APIServer::APIServer() { global_api_server = this; }
|
||||
|
||||
void APIServer::socket_failed_(const LogString *msg) {
|
||||
@@ -38,13 +60,10 @@ void APIServer::socket_failed_(const LogString *msg) {
|
||||
}
|
||||
|
||||
void APIServer::setup() {
|
||||
ControllerRegistry::register_controller(this);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
// Always reserve the slot: flash preferences are positional on esp8266, so
|
||||
// a yaml key build must keep the layout of a runtime key build
|
||||
uint32_t hash = 88491486UL;
|
||||
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(hash, true);
|
||||
this->noise_pref_ = global_preferences->make_preference<SavedNoisePsk>(NOISE_PSK_PREF_HASH, true);
|
||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
||||
// A cleared record loads fine but holds no key
|
||||
if (this->load_and_apply_noise_psk_() && this->noise_ctx_.has_psk()) {
|
||||
@@ -433,8 +452,9 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
|
||||
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
|
||||
// fired right at connection time (on_client_connected, on_time_sync, ...) can
|
||||
// arrive before the subscription and are lost - warn instead of failing silently.
|
||||
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s",
|
||||
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(),
|
||||
ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
|
||||
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
|
||||
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.c_str(),
|
||||
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
|
||||
: LOG_STR_LITERAL("no client connected"));
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/controller.h"
|
||||
#include "esphome/core/entity_includes.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#ifdef USE_PROVISIONING
|
||||
@@ -43,9 +43,13 @@ struct SavedNoisePsk {
|
||||
noise::psk_t psk;
|
||||
} PACKED; // NOLINT
|
||||
#endif
|
||||
#if defined(USE_API_NOISE) && defined(USE_OTA_ENCRYPTION_PROVISIONED)
|
||||
/// One-shot read of the provisioned key for a boot without an api server (safe mode); false when
|
||||
/// there is no key
|
||||
bool load_saved_noise_psk(noise::psk_t &out);
|
||||
#endif
|
||||
|
||||
class APIServer final : public Component,
|
||||
public Controller
|
||||
class APIServer final : public Component
|
||||
#ifdef USE_CAMERA
|
||||
,
|
||||
public camera::CameraListener
|
||||
@@ -89,58 +93,58 @@ class APIServer final : public Component,
|
||||
|
||||
void handle_disconnect(APIConnection *conn);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override;
|
||||
void on_binary_sensor_update(binary_sensor::BinarySensor *obj);
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void on_cover_update(cover::Cover *obj) override;
|
||||
void on_cover_update(cover::Cover *obj);
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
void on_fan_update(fan::Fan *obj) override;
|
||||
void on_fan_update(fan::Fan *obj);
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
void on_light_update(light::LightState *obj) override;
|
||||
void on_light_update(light::LightState *obj);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
void on_sensor_update(sensor::Sensor *obj) override;
|
||||
void on_sensor_update(sensor::Sensor *obj);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void on_switch_update(switch_::Switch *obj) override;
|
||||
void on_switch_update(switch_::Switch *obj);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
void on_text_sensor_update(text_sensor::TextSensor *obj) override;
|
||||
void on_text_sensor_update(text_sensor::TextSensor *obj);
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
void on_climate_update(climate::Climate *obj) override;
|
||||
void on_climate_update(climate::Climate *obj);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void on_number_update(number::Number *obj) override;
|
||||
void on_number_update(number::Number *obj);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void on_date_update(datetime::DateEntity *obj) override;
|
||||
void on_date_update(datetime::DateEntity *obj);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void on_time_update(datetime::TimeEntity *obj) override;
|
||||
void on_time_update(datetime::TimeEntity *obj);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void on_datetime_update(datetime::DateTimeEntity *obj) override;
|
||||
void on_datetime_update(datetime::DateTimeEntity *obj);
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void on_text_update(text::Text *obj) override;
|
||||
void on_text_update(text::Text *obj);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_update(select::Select *obj) override;
|
||||
void on_select_update(select::Select *obj);
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_update(lock::Lock *obj) override;
|
||||
void on_lock_update(lock::Lock *obj);
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
void on_valve_update(valve::Valve *obj) override;
|
||||
void on_valve_update(valve::Valve *obj);
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||
void on_media_player_update(media_player::MediaPlayer *obj);
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
void on_water_heater_update(water_heater::WaterHeater *obj) override;
|
||||
void on_water_heater_update(water_heater::WaterHeater *obj);
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void send_homeassistant_action(const HomeassistantActionRequest &call);
|
||||
@@ -183,13 +187,13 @@ class APIServer final : public Component,
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
|
||||
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
void on_event(event::Event *obj) override;
|
||||
void on_event(event::Event *obj);
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void on_update(update::UpdateEntity *obj) override;
|
||||
void on_update(update::UpdateEntity *obj);
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
@@ -314,7 +318,7 @@ class APIServer final : public Component,
|
||||
#endif
|
||||
|
||||
// 4-byte aligned types
|
||||
uint32_t reboot_timeout_{300000};
|
||||
uint32_t reboot_timeout_{900000}; // Keep in sync with DEFAULT_REBOOT_TIMEOUT in __init__.py
|
||||
uint32_t last_connected_{0};
|
||||
|
||||
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
|
||||
@@ -351,8 +355,8 @@ class APIServer final : public Component,
|
||||
#endif
|
||||
|
||||
// Group smaller types together
|
||||
uint16_t port_{6053};
|
||||
uint16_t batch_delay_{100};
|
||||
uint16_t port_{6053}; // Keep in sync with DEFAULT_PORT in __init__.py
|
||||
uint16_t batch_delay_{100}; // Keep in sync with DEFAULT_BATCH_DELAY in __init__.py
|
||||
// Connection limits - these defaults will be overridden by config values
|
||||
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
|
||||
uint8_t listen_backlog_{4};
|
||||
|
||||
@@ -195,6 +195,12 @@ void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *
|
||||
ESP_LOGE(TAG, "Proto encode bounds check failed in %s: need %zu bytes, %td available", caller, bytes, end - pos);
|
||||
abort();
|
||||
}
|
||||
void proto_check_encode_end(const uint8_t *end, const uint8_t *expected) {
|
||||
if (end == expected)
|
||||
return;
|
||||
ESP_LOGE(TAG, "Proto encode ended %td bytes off the calculated size", end - expected);
|
||||
abort();
|
||||
}
|
||||
void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) {
|
||||
if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) {
|
||||
ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes,
|
||||
@@ -210,77 +216,78 @@ void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expe
|
||||
|
||||
#endif
|
||||
|
||||
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
void ProtoDecodableMessage::decode_fields(void *msg, const uint8_t *buffer, size_t length, DecodeFieldFn field) {
|
||||
const uint8_t *ptr = buffer;
|
||||
const uint8_t *end = buffer + length;
|
||||
|
||||
while (ptr < end) {
|
||||
// Parse field header - ptr < end guarantees len >= 1
|
||||
// Single-byte varints dominate, so that case advances the cursor inline.
|
||||
auto read_varint = [&](proto_varint_value_t &value) ESPHOME_ALWAYS_INLINE {
|
||||
if (ptr == end)
|
||||
return false;
|
||||
if (*ptr < 0x80) [[likely]] {
|
||||
value = *ptr++;
|
||||
return true;
|
||||
}
|
||||
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
if (!res.has_value())
|
||||
return false;
|
||||
value = res.value;
|
||||
ptr += res.consumed;
|
||||
return true;
|
||||
};
|
||||
|
||||
while (ptr < end) {
|
||||
proto_varint_value_t tag_value;
|
||||
if (!read_varint(tag_value)) {
|
||||
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t tag = static_cast<uint32_t>(res.value);
|
||||
uint32_t tag = static_cast<uint32_t>(tag_value);
|
||||
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
||||
uint32_t field_id = tag >> 3;
|
||||
ptr += res.consumed;
|
||||
// Length-delimited fields move this past the length prefix
|
||||
const uint8_t *data = ptr;
|
||||
proto_varint_value_t scalar;
|
||||
|
||||
switch (field_type) {
|
||||
case WIRE_TYPE_VARINT: { // VarInt
|
||||
res = ProtoVarInt::parse(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
if (!this->decode_varint(field_id, res.value)) {
|
||||
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
|
||||
static_cast<uint64_t>(res.value));
|
||||
}
|
||||
ptr += res.consumed;
|
||||
break;
|
||||
}
|
||||
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
|
||||
res = ProtoVarInt::parse(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
uint32_t field_length = static_cast<uint32_t>(res.value);
|
||||
ptr += res.consumed;
|
||||
if (field_length > static_cast<size_t>(end - ptr)) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
if (!this->decode_length(field_id, ProtoLengthDelimited(ptr, field_length))) {
|
||||
ESP_LOGV(TAG, "Cannot decode Length Delimited field %" PRIu32 "!", field_id);
|
||||
}
|
||||
ptr += field_length;
|
||||
break;
|
||||
}
|
||||
case WIRE_TYPE_FIXED32: { // 32-bit
|
||||
if (end - ptr < 4) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
uint32_t val;
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
// Protobuf fixed32 is little-endian — direct load on LE platforms
|
||||
memcpy(&val, ptr, 4);
|
||||
#else
|
||||
val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
|
||||
#endif
|
||||
if (!this->decode_32bit(field_id, Proto32Bit(val))) {
|
||||
ESP_LOGV(TAG, "Cannot decode 32-bit field %" PRIu32 " with value %" PRIu32 "!", field_id, val);
|
||||
}
|
||||
ptr += 4;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
|
||||
if (field_type == WIRE_TYPE_VARINT) [[likely]] {
|
||||
if (!read_varint(scalar)) {
|
||||
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
switch (field_type) {
|
||||
case WIRE_TYPE_LENGTH_DELIMITED: {
|
||||
proto_varint_value_t length_value;
|
||||
if (!read_varint(length_value)) {
|
||||
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
uint32_t field_length = static_cast<uint32_t>(length_value);
|
||||
if (field_length > static_cast<size_t>(end - ptr)) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
data = ptr;
|
||||
scalar = field_length;
|
||||
ptr += field_length;
|
||||
break;
|
||||
}
|
||||
case WIRE_TYPE_FIXED32: {
|
||||
if (end - ptr < 4) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
// Byte loads instead of memcpy: ESP-IDF passes -fno-builtin-memcpy, which made this a call
|
||||
scalar = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
|
||||
ptr += 4;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGV(TAG, "Invalid field type %" PRIu32 " at offset %ld", field_type, (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
}
|
||||
field(msg, tag, data, scalar);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+243
-170
@@ -10,6 +10,7 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
|
||||
@@ -170,40 +171,43 @@ class ProtoVarInt {
|
||||
class ProtoMessage;
|
||||
class ProtoSize;
|
||||
|
||||
class ProtoLengthDelimited {
|
||||
/// Case label for decode_field(): the wire tag of a field, so a field that arrives with another wire
|
||||
/// type matches no case.
|
||||
constexpr uint32_t proto_tag(uint32_t field_id, uint32_t wire_type) { return (field_id << 3) | wire_type; }
|
||||
|
||||
/// One decoded field: the payload pointer and a scalar holding the varint or fixed32 value, or the
|
||||
/// length of a length-delimited field. The wire type in the tag says which applies; accessors do not check.
|
||||
class ProtoFieldValue {
|
||||
public:
|
||||
explicit ProtoLengthDelimited(const uint8_t *value, size_t length) : value_(value), length_(length) {}
|
||||
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->value_), this->length_); }
|
||||
ProtoFieldValue(const uint8_t *data, proto_varint_value_t scalar) : data_(data), scalar_(scalar) {}
|
||||
|
||||
// Direct access to raw data without string allocation
|
||||
const uint8_t *data() const { return this->value_; }
|
||||
size_t size() const { return this->length_; }
|
||||
proto_varint_value_t as_varint() const { return this->scalar_; }
|
||||
// A bool is sent as 0 or 1, so the low word is enough and saves a second compare with 64 bit varints
|
||||
bool as_bool() const { return static_cast<uint32_t>(this->scalar_) != 0; }
|
||||
|
||||
/// Decode the length-delimited data into a message instance.
|
||||
// Length-delimited accessors
|
||||
const uint8_t *data() const { return this->data_; }
|
||||
size_t size() const { return static_cast<size_t>(this->scalar_); }
|
||||
std::string as_string() const { return std::string(reinterpret_cast<const char *>(this->data_), this->size()); }
|
||||
/// Decode the length-delimited payload into a message instance.
|
||||
/// Template preserves concrete type so decode() resolves statically.
|
||||
template<typename T> void decode_to_message(T &msg) const;
|
||||
template<typename T> void decode_to_message(T &msg) const { msg.decode(this->data_, this->size()); }
|
||||
|
||||
protected:
|
||||
const uint8_t *const value_;
|
||||
const size_t length_;
|
||||
};
|
||||
|
||||
class Proto32Bit {
|
||||
public:
|
||||
explicit Proto32Bit(uint32_t value) : value_(value) {}
|
||||
uint32_t as_fixed32() const { return this->value_; }
|
||||
int32_t as_sfixed32() const { return static_cast<int32_t>(this->value_); }
|
||||
// Fixed32 accessors
|
||||
uint32_t as_fixed32() const { return static_cast<uint32_t>(this->scalar_); }
|
||||
int32_t as_sfixed32() const { return static_cast<int32_t>(this->as_fixed32()); }
|
||||
float as_float() const {
|
||||
union {
|
||||
uint32_t raw;
|
||||
float value;
|
||||
} s{};
|
||||
s.raw = this->value_;
|
||||
s.raw = this->as_fixed32();
|
||||
return s.value;
|
||||
}
|
||||
|
||||
protected:
|
||||
const uint32_t value_;
|
||||
private:
|
||||
const uint8_t *data_;
|
||||
proto_varint_value_t scalar_;
|
||||
};
|
||||
|
||||
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
|
||||
@@ -221,6 +225,9 @@ class Proto32Bit {
|
||||
proto_check_bounds_failed(pos, n, proto_debug_end_, __builtin_FUNCTION()); \
|
||||
} while (0)
|
||||
void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller);
|
||||
/// Aborts unless an encode body ended exactly where calculate_size() promised. A plain check rather than
|
||||
/// assert(), so NDEBUG cannot switch it off.
|
||||
void proto_check_encode_end(const uint8_t *end, const uint8_t *expected);
|
||||
#else
|
||||
#define PROTO_ENCODE_DEBUG_PARAM
|
||||
#define PROTO_ENCODE_DEBUG_ARG
|
||||
@@ -252,7 +259,7 @@ class ProtoWriteBuffer {
|
||||
*
|
||||
* Following https://protobuf.dev/programming-guides/encoding/#structure
|
||||
*/
|
||||
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); }
|
||||
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw(proto_tag(field_id, type)); }
|
||||
/// Single-pass encode for repeated submessage elements.
|
||||
/// Thin template wrapper; all buffer work is in the non-template core.
|
||||
template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
|
||||
@@ -287,19 +294,31 @@ class ProtoWriteBuffer {
|
||||
uint8_t *pos_;
|
||||
};
|
||||
|
||||
// A four byte unaligned store is a memcpy call on ESP-IDF (-fno-builtin-memcpy) and on ARM cores without
|
||||
// unaligned access (Cortex-M0+, ARM9), so those targets share one outlined byte store helper per fixed32
|
||||
// field. Elsewhere the write inlines to a single store, or on ESP8266 to a few stores that measured
|
||||
// faster than a call, so it stays inline.
|
||||
#if defined(USE_ESP32) || (defined(__arm__) && !defined(__ARM_FEATURE_UNALIGNED))
|
||||
#define PROTO_OUTLINE_FOR_SIZE __attribute__((noinline))
|
||||
#define PROTO_FIXED32_BYTE_STORES true
|
||||
#else
|
||||
#define PROTO_OUTLINE_FOR_SIZE inline
|
||||
#define PROTO_FIXED32_BYTE_STORES false
|
||||
#endif
|
||||
|
||||
// Varint encoding thresholds — used by both proto_encode_* free functions and ProtoSize.
|
||||
constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128
|
||||
constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
|
||||
|
||||
/// Static encode helpers for generated encode() functions.
|
||||
/// Generated code hoists buffer.pos_ into a local uint8_t *__restrict__ pos,
|
||||
/// then calls these methods which take pos by reference. No struct, no overhead.
|
||||
/// For sub-messages, pos is synced back to buffer before the call and reloaded after.
|
||||
/// Static encode helpers for the generated encode bodies. Each takes the write cursor by value and
|
||||
/// returns it advanced, so outlined calls at -Os chain through the return register instead of a
|
||||
/// stack slot. Helpers without a _force suffix skip fields holding the proto3 default.
|
||||
class ProtoEncode {
|
||||
public:
|
||||
/// Write a multi-byte varint directly through a pos pointer.
|
||||
template<typename T>
|
||||
static inline void encode_varint_raw_loop(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, T value) {
|
||||
[[nodiscard]] static inline uint8_t *encode_varint_raw_loop(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
T value) {
|
||||
do {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value | 0x80);
|
||||
@@ -307,48 +326,49 @@ class ProtoEncode {
|
||||
} while (value > 0x7F);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return pos;
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t value) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_varint_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t value) {
|
||||
if (value < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
return pos;
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
/// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t value) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_varint_raw_short(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t value) {
|
||||
if (value < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
return pos;
|
||||
}
|
||||
if (value < VARINT_MAX_2_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 2);
|
||||
*pos++ = static_cast<uint8_t>(value | 0x80);
|
||||
*pos++ = static_cast<uint8_t>(value >> 7);
|
||||
return;
|
||||
return pos;
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint64_t value) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_varint_raw_64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint64_t value) {
|
||||
if (value < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
return pos;
|
||||
}
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
return encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
/// Encode a 48-bit MAC address (stored in a uint64) as varint.
|
||||
/// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the
|
||||
/// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes
|
||||
/// with no per-byte branch. Falls back to the general loop otherwise.
|
||||
/// Caller must guarantee value fits in 48 bits (checked in debug builds).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint64_t value) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_varint_raw_48bit(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint64_t value) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits");
|
||||
#endif
|
||||
@@ -363,38 +383,39 @@ class ProtoEncode {
|
||||
pos[4] = static_cast<uint8_t>((value >> 28) | 0x80);
|
||||
pos[5] = static_cast<uint8_t>((value >> 35) | 0x80);
|
||||
pos[6] = static_cast<uint8_t>(value >> 42);
|
||||
pos += 7;
|
||||
return;
|
||||
return pos + 7;
|
||||
}
|
||||
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
return encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t type) {
|
||||
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type);
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_field_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t type) {
|
||||
return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, proto_tag(field_id, type));
|
||||
}
|
||||
/// Write a single precomputed tag byte. Tag must be < 128.
|
||||
static inline void ESPHOME_ALWAYS_INLINE write_raw_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint8_t b) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
write_raw_byte(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t b) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = b;
|
||||
return pos;
|
||||
}
|
||||
/// Reserve one byte for later backpatch (e.g., sub-message length).
|
||||
/// Advances pos past the reserved byte without writing a value.
|
||||
static inline void ESPHOME_ALWAYS_INLINE reserve_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
reserve_byte(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
pos++;
|
||||
return pos + 1;
|
||||
}
|
||||
/// Write raw bytes to the buffer (no tag, no length prefix).
|
||||
static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
const void *data, size_t len) {
|
||||
[[nodiscard]] static inline uint8_t *ESPHOME_ALWAYS_INLINE
|
||||
encode_raw(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, const void *data, size_t len) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
|
||||
std::memcpy(pos, data, len);
|
||||
pos += len;
|
||||
return pos + len;
|
||||
}
|
||||
/// Encode tag + 1-byte length + raw string data. For strings with max_data_length < 128.
|
||||
/// Tag must be a single-byte varint (< 128). Always encodes (no zero check).
|
||||
static inline void encode_short_string_force(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag,
|
||||
const StringRef &ref) {
|
||||
[[nodiscard]] static inline uint8_t *encode_short_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint8_t tag, const StringRef &ref) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128");
|
||||
#endif
|
||||
@@ -402,137 +423,191 @@ class ProtoEncode {
|
||||
pos[0] = tag;
|
||||
pos[1] = static_cast<uint8_t>(ref.size());
|
||||
std::memcpy(pos + 2, ref.c_str(), ref.size());
|
||||
pos += 2 + ref.size();
|
||||
return pos + 2 + ref.size();
|
||||
}
|
||||
/// Write a precomputed tag byte + 32-bit value in one operation.
|
||||
static inline void ESPHOME_ALWAYS_INLINE write_tag_and_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint8_t tag, uint32_t value) {
|
||||
/// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field.
|
||||
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *write_tag_and_fixed32(
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, uint32_t value) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 5);
|
||||
pos[0] = tag;
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
std::memcpy(pos + 1, &value, 4);
|
||||
#else
|
||||
pos[1] = static_cast<uint8_t>(value & 0xFF);
|
||||
pos[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
|
||||
pos[3] = static_cast<uint8_t>((value >> 16) & 0xFF);
|
||||
pos[4] = static_cast<uint8_t>((value >> 24) & 0xFF);
|
||||
#endif
|
||||
pos += 5;
|
||||
write_fixed32_le(pos + 1, value);
|
||||
return pos + 5;
|
||||
}
|
||||
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
const char *string, size_t len, bool force = false) {
|
||||
if (len == 0 && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
|
||||
[[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const char *string, size_t len) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
|
||||
// NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) -- false positive on [[likely]] attribute
|
||||
if (len < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
|
||||
*pos++ = static_cast<uint8_t>(len);
|
||||
} else {
|
||||
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
|
||||
pos = encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
|
||||
}
|
||||
std::memcpy(pos, string, len);
|
||||
pos += len;
|
||||
return pos + len;
|
||||
}
|
||||
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
const std::string &value, bool force = false) {
|
||||
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size(), force);
|
||||
[[nodiscard]] static inline uint8_t *encode_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const char *string, size_t len) {
|
||||
if (len == 0)
|
||||
return pos;
|
||||
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, string, len);
|
||||
}
|
||||
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
const StringRef &ref, bool force = false) {
|
||||
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size(), force);
|
||||
[[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const std::string &value) {
|
||||
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size());
|
||||
}
|
||||
static inline void encode_bytes(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
const uint8_t *data, size_t len, bool force = false) {
|
||||
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len, force);
|
||||
[[nodiscard]] static inline uint8_t *encode_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const StringRef &ref) {
|
||||
return encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size());
|
||||
}
|
||||
static inline void encode_uint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
[[nodiscard]] static inline uint8_t *encode_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const StringRef &ref) {
|
||||
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size());
|
||||
}
|
||||
static inline void encode_uint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
uint64_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
[[nodiscard]] static inline uint8_t *encode_bytes(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const uint8_t *data, size_t len) {
|
||||
return encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len);
|
||||
}
|
||||
static inline void encode_bool(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, bool value,
|
||||
bool force = false) {
|
||||
if (!value && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
[[nodiscard]] static inline uint8_t *encode_bytes_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, const uint8_t *data, size_t len) {
|
||||
return encode_string_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_uint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t value) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
return encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_uint32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t value) {
|
||||
if (value == 0)
|
||||
return pos;
|
||||
return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_uint64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint64_t value) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
return encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_uint64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint64_t value) {
|
||||
if (value == 0)
|
||||
return pos;
|
||||
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_bool_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, bool value) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = value ? 0x01 : 0x00;
|
||||
return pos;
|
||||
}
|
||||
static inline void encode_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5);
|
||||
[[nodiscard]] static inline uint8_t *encode_bool(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, bool value) {
|
||||
if (!value)
|
||||
return pos;
|
||||
return encode_bool_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
|
||||
}
|
||||
/// Tag + fixed32 for multi-byte tags; single-byte tags use write_tag_and_fixed32.
|
||||
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *encode_fixed32_force(
|
||||
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, uint32_t value) {
|
||||
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 4);
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
std::memcpy(pos, &value, 4);
|
||||
pos += 4;
|
||||
#else
|
||||
*pos++ = (value >> 0) & 0xFF;
|
||||
*pos++ = (value >> 8) & 0xFF;
|
||||
*pos++ = (value >> 16) & 0xFF;
|
||||
*pos++ = (value >> 24) & 0xFF;
|
||||
#endif
|
||||
write_fixed32_le(pos, value);
|
||||
return pos + 4;
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_fixed32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, uint32_t value) {
|
||||
if (value == 0)
|
||||
return pos;
|
||||
return encode_fixed32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
|
||||
}
|
||||
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
|
||||
// not supported to reduce overhead on embedded systems. All ESPHome devices are
|
||||
// 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support
|
||||
// is needed in the future, the necessary encoding/decoding functions must be added.
|
||||
static inline void encode_float(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, float value,
|
||||
bool force = false) {
|
||||
uint32_t raw = float_to_raw(value);
|
||||
if (raw == 0 && !force)
|
||||
return;
|
||||
encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, raw);
|
||||
[[nodiscard]] static inline uint8_t *encode_float(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, float value) {
|
||||
return encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, float_to_raw(value));
|
||||
}
|
||||
static inline void encode_int32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int32_t value,
|
||||
bool force = false) {
|
||||
[[nodiscard]] static inline uint8_t *encode_float_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, float value) {
|
||||
return encode_fixed32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, float_to_raw(value));
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_int32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int32_t value) {
|
||||
if (value < 0) {
|
||||
// negative int32 is always 10 byte long
|
||||
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force);
|
||||
return;
|
||||
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
|
||||
}
|
||||
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value), force);
|
||||
return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value));
|
||||
}
|
||||
static inline void encode_int64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int64_t value,
|
||||
bool force = false) {
|
||||
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force);
|
||||
[[nodiscard]] static inline uint8_t *encode_int32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int32_t value) {
|
||||
if (value == 0)
|
||||
return pos;
|
||||
return encode_int32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, value);
|
||||
}
|
||||
static inline void encode_sint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
int32_t value, bool force = false) {
|
||||
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value), force);
|
||||
[[nodiscard]] static inline uint8_t *encode_int64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int64_t value) {
|
||||
return encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
|
||||
}
|
||||
static inline void encode_sint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
|
||||
int64_t value, bool force = false) {
|
||||
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value), force);
|
||||
[[nodiscard]] static inline uint8_t *encode_int64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int64_t value) {
|
||||
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value));
|
||||
}
|
||||
/// Sub-message encoding: sync pos to buffer, delegate, get pos from return value.
|
||||
[[nodiscard]] static inline uint8_t *encode_sint32(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int32_t value) {
|
||||
return encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value));
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_sint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int32_t value) {
|
||||
return encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value));
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_sint64(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int64_t value) {
|
||||
return encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value));
|
||||
}
|
||||
[[nodiscard]] static inline uint8_t *encode_sint64_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
uint32_t field_id, int64_t value) {
|
||||
return encode_uint64_force(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value));
|
||||
}
|
||||
/// Sub-message encoding: sync pos to buffer, delegate, read the cursor back.
|
||||
template<typename T>
|
||||
static inline void encode_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, ProtoWriteBuffer &buffer,
|
||||
uint32_t field_id, const T &value) {
|
||||
[[nodiscard]] static inline uint8_t *encode_sub_message(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) {
|
||||
buffer.set_pos(pos);
|
||||
buffer.encode_sub_message(field_id, value);
|
||||
pos = buffer.get_pos();
|
||||
return buffer.get_pos();
|
||||
}
|
||||
template<typename T>
|
||||
static inline void encode_optional_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) {
|
||||
[[nodiscard]] static inline uint8_t *encode_optional_sub_message(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
|
||||
ProtoWriteBuffer &buffer, uint32_t field_id,
|
||||
const T &value) {
|
||||
buffer.set_pos(pos);
|
||||
buffer.encode_optional_sub_message(field_id, value);
|
||||
pos = buffer.get_pos();
|
||||
return buffer.get_pos();
|
||||
}
|
||||
|
||||
private:
|
||||
/// Unaligned little endian store of four bytes: byte stores where the outlined helper lives (ESP-IDF, ARM
|
||||
/// without unaligned access), otherwise a memcpy the compiler folds into one store. Callers bounds check
|
||||
/// and advance the cursor themselves.
|
||||
static inline void ESPHOME_ALWAYS_INLINE write_fixed32_le(uint8_t *__restrict__ pos, uint32_t value) {
|
||||
if constexpr (PROTO_FIXED32_BYTE_STORES) {
|
||||
// Spelled out so the outlined helper does not itself become a memcpy call
|
||||
pos[0] = static_cast<uint8_t>(value);
|
||||
pos[1] = static_cast<uint8_t>(value >> 8);
|
||||
pos[2] = static_cast<uint8_t>(value >> 16);
|
||||
pos[3] = static_cast<uint8_t>(value >> 24);
|
||||
} else {
|
||||
const uint32_t le = convert_little_endian(value);
|
||||
__builtin_memcpy(pos, &le, 4);
|
||||
}
|
||||
}
|
||||
};
|
||||
#undef PROTO_OUTLINE_FOR_SIZE
|
||||
#undef PROTO_FIXED32_BYTE_STORES
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
/**
|
||||
@@ -624,11 +699,12 @@ class DumpBuffer {
|
||||
|
||||
class ProtoMessage {
|
||||
public:
|
||||
// Non-virtual defaults for messages with no fields.
|
||||
// Concrete message classes hide these with their own implementations.
|
||||
// All call sites use templates to preserve the concrete type, so virtual
|
||||
// dispatch is not needed. This eliminates per-message vtable entries for
|
||||
// encode/calculate_size, saving ~1.3 KB of flash across all message types.
|
||||
// Non-virtual defaults for messages with no fields; generated classes hide all four. The
|
||||
// static encode_msg/calc_size_msg take const void * so &T::encode_msg needs no thunk.
|
||||
static uint8_t *encode_msg(const void *self, ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) {
|
||||
return buffer.get_pos();
|
||||
}
|
||||
static uint32_t calc_size_msg(const void *self) { return 0; }
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); }
|
||||
uint32_t calculate_size() const { return 0; }
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -648,7 +724,13 @@ class ProtoMessage {
|
||||
// Base class for messages that support decoding
|
||||
class ProtoDecodableMessage : public ProtoMessage {
|
||||
public:
|
||||
void decode(const uint8_t *buffer, size_t length);
|
||||
/// Stores one decoded field into \p msg; generated per message type. \p scalar is the varint or
|
||||
/// fixed32 value, or the length of the length-delimited payload at \p data. An unknown field or
|
||||
/// wrong wire type matches no case and is skipped.
|
||||
using DecodeFieldFn = void (*)(void *msg, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar);
|
||||
/// Walk \p buffer and hand every field to \p field. The generated decode() passes the message's
|
||||
/// own decode_field, so decodable messages carry no vtable.
|
||||
static void decode_fields(void *msg, const uint8_t *buffer, size_t length, DecodeFieldFn field);
|
||||
|
||||
/**
|
||||
* Count occurrences of a repeated field in a protobuf buffer.
|
||||
@@ -660,14 +742,15 @@ class ProtoDecodableMessage : public ProtoMessage {
|
||||
* @return Number of times the field appears in the buffer
|
||||
*/
|
||||
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
|
||||
|
||||
protected:
|
||||
~ProtoDecodableMessage() = default;
|
||||
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; }
|
||||
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
|
||||
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
|
||||
// NOTE: decode_64bit removed - wire type 1 not supported
|
||||
// The destructor stays accessible on purpose: the generated messages are aggregates that brace
|
||||
// initialise sub message members, which copies a base temporary. That trades away the compile time
|
||||
// guard against deleting through this type; messages are stack locals and never owned through a base
|
||||
// pointer. ProtoMessage keeps its guard for the dump builds.
|
||||
};
|
||||
#ifndef HAS_PROTO_MESSAGE_DUMP
|
||||
// decode() passes decode_field explicitly, so nothing here may add a vtable
|
||||
static_assert(!std::is_polymorphic_v<ProtoDecodableMessage>, "decodable messages carry no vtable");
|
||||
#endif
|
||||
|
||||
class ProtoSize {
|
||||
public:
|
||||
@@ -792,7 +875,7 @@ class ProtoSize {
|
||||
* @return The number of bytes needed to encode the field ID and wire type
|
||||
*/
|
||||
static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
|
||||
uint32_t tag = (field_id << 3) | (type & WIRE_TYPE_MASK);
|
||||
uint32_t tag = proto_tag(field_id, type & WIRE_TYPE_MASK);
|
||||
return varint(tag);
|
||||
}
|
||||
|
||||
@@ -876,24 +959,14 @@ class ProtoSize {
|
||||
|
||||
// Implementation of methods that depend on ProtoSize being fully defined
|
||||
|
||||
// Encode thunk — converts void* back to concrete type for direct encode() call
|
||||
template<typename T> uint8_t *proto_encode_msg(const void *msg, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
|
||||
return static_cast<const T *>(msg)->encode(buf PROTO_ENCODE_DEBUG_ARG);
|
||||
}
|
||||
|
||||
// Thin template wrapper; delegates to non-template core in proto.cpp.
|
||||
template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
|
||||
this->encode_sub_message(field_id, &value, &proto_encode_msg<T>);
|
||||
this->encode_sub_message(field_id, &value, &T::encode_msg);
|
||||
}
|
||||
|
||||
// Thin template wrapper; delegates to non-template core.
|
||||
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
|
||||
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>);
|
||||
}
|
||||
|
||||
// Template decode_to_message - preserves concrete type so decode() resolves statically
|
||||
template<typename T> void ProtoLengthDelimited::decode_to_message(T &msg) const {
|
||||
msg.decode(this->value_, this->length_);
|
||||
this->encode_optional_sub_message(field_id, T::calc_size_msg(&value), &value, &T::encode_msg);
|
||||
}
|
||||
|
||||
template<typename T> const char *proto_enum_to_string(T value);
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#ifdef USE_API
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/component_iterator.h"
|
||||
#include "esphome/core/controller.h"
|
||||
namespace esphome::api {
|
||||
|
||||
class APIConnection;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.components.const import UNIT_COUNTS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_CLEAR,
|
||||
@@ -33,8 +34,6 @@ CONF_F7 = "f7"
|
||||
CONF_F8 = "f8"
|
||||
CONF_NIR = "nir"
|
||||
|
||||
UNIT_COUNTS = "#"
|
||||
|
||||
AS7341_GAIN = as7341_ns.enum("AS7341Gain")
|
||||
GAIN_OPTIONS = {
|
||||
"X0.5": AS7341_GAIN.AS7341_GAIN_0_5X,
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cerrno>
|
||||
#include <sys/select.h>
|
||||
|
||||
namespace esphome::async_tcp {
|
||||
|
||||
@@ -42,7 +41,15 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
|
||||
return false;
|
||||
}
|
||||
|
||||
socket_->setblocking(false);
|
||||
if (socket_->setblocking(false) != 0) {
|
||||
// Capture before the log and close() clobber errno
|
||||
const int saved_errno = errno;
|
||||
ESP_LOGE(TAG, "Failed to set nonblocking: errno %d", saved_errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, saved_errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
|
||||
if (err == 0) {
|
||||
@@ -97,45 +104,22 @@ void AsyncClient::loop() {
|
||||
return;
|
||||
|
||||
if (connecting_) {
|
||||
// For connecting, we need to check writability, not readability
|
||||
// The Application's select() only monitors read FDs, so we do our own check here
|
||||
// For ESP platforms lwip_select() might be faster, but this code isn't used
|
||||
// on those platforms anyway. If it was, we'd fix the Application select()
|
||||
// to report writability instead of doing it this way.
|
||||
int fd = socket_->get_fd();
|
||||
if (fd < 0) {
|
||||
ESP_LOGW(TAG, "Invalid socket fd");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(fd, &writefds);
|
||||
|
||||
struct timeval tv = {0, 0};
|
||||
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
|
||||
if (ret > 0 && FD_ISSET(fd, &writefds)) {
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
|
||||
int err = 0;
|
||||
switch (socket::poll_connect(*socket_, err)) {
|
||||
case socket::ConnectPollResult::CONNECT_POLL_RESULT_PENDING:
|
||||
break;
|
||||
case socket::ConnectPollResult::CONNECT_POLL_RESULT_CONNECTED:
|
||||
connecting_ = false;
|
||||
connected_ = true;
|
||||
if (connect_cb_)
|
||||
connect_cb_(connect_arg_, this);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connection failed: %d", error);
|
||||
break;
|
||||
case socket::ConnectPollResult::CONNECT_POLL_RESULT_ERROR:
|
||||
ESP_LOGW(TAG, "Connection failed: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, error);
|
||||
}
|
||||
} else if (ret < 0) {
|
||||
const int err = errno;
|
||||
ESP_LOGE(TAG, "Select error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, err);
|
||||
error_cb_(error_arg_, this, err);
|
||||
break;
|
||||
}
|
||||
} else if (connected_) {
|
||||
// For connected sockets, use the Application's select() results
|
||||
|
||||
@@ -4,8 +4,6 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@X-Ryl669"]
|
||||
@@ -80,30 +78,21 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
|
||||
# Actions
|
||||
AT581XResetAction = at581x_ns.class_("AT581XResetAction", automation.Action)
|
||||
AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Action)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"at581x.reset",
|
||||
AT581XResetAction,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(AT581XComponent)}),
|
||||
automation.ApplyCall("reset_hardware_frontend()"),
|
||||
)
|
||||
async def at581x_reset_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
return var
|
||||
|
||||
def _megahertz(value: float) -> int:
|
||||
return int(value / 1000000)
|
||||
|
||||
|
||||
def _microamps(value: float) -> int:
|
||||
return int(value * 1000000)
|
||||
|
||||
|
||||
RADAR_SETTINGS_SCHEMA = cv.Schema(
|
||||
@@ -111,7 +100,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
|
||||
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
|
||||
cv.Optional(CONF_HW_FRONTEND_RESET): cv.templatable(cv.boolean),
|
||||
cv.Optional(CONF_FREQUENCY, default="5800MHz"): cv.templatable(
|
||||
cv.All(cv.frequency, cv.one_of(*RADAR_ALLOWED_FREQ))
|
||||
cv.All(cv.frequency, cv.one_of(*RADAR_ALLOWED_FREQ), _megahertz)
|
||||
),
|
||||
cv.Optional(CONF_SENSING_DISTANCE, default=823): cv.templatable(
|
||||
cv.int_range(min=0, max=1023)
|
||||
@@ -123,7 +112,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
|
||||
)
|
||||
),
|
||||
cv.Optional(CONF_POWER_CONSUMPTION, default="70uA"): cv.templatable(
|
||||
cv.All(cv.current, cv.one_of(*RADAR_ALLOWED_CUR_CONSUMPTION))
|
||||
cv.All(cv.current, cv.one_of(*RADAR_ALLOWED_CUR_CONSUMPTION), _microamps)
|
||||
),
|
||||
cv.Optional(CONF_PROTECT_TIME, default="1000ms"): cv.templatable(
|
||||
cv.All(
|
||||
@@ -165,60 +154,22 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
# i2c_write_config() must follow the setters: it flushes the staged values.
|
||||
automation.register_apply_action(
|
||||
"at581x.settings",
|
||||
AT581XSettingsAction,
|
||||
RADAR_SETTINGS_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_FREQUENCY, "set_frequency", cg.int_),
|
||||
automation.ApplyField(CONF_SENSING_DISTANCE, "set_sensing_distance", cg.int_),
|
||||
automation.ApplyField(
|
||||
CONF_POWERON_SELFCHECK_TIME, "set_poweron_selfcheck_time", cg.int_
|
||||
),
|
||||
automation.ApplyField(CONF_POWER_CONSUMPTION, "set_power_consumption", cg.int_),
|
||||
automation.ApplyField(CONF_PROTECT_TIME, "set_protect_time", cg.int_),
|
||||
automation.ApplyField(CONF_TRIGGER_BASE, "set_trigger_base", cg.int_),
|
||||
automation.ApplyField(CONF_TRIGGER_KEEP, "set_trigger_keep", cg.int_),
|
||||
automation.ApplyField(CONF_STAGE_GAIN, "set_stage_gain", cg.int_),
|
||||
automation.ApplyCall("i2c_write_config()"),
|
||||
automation.ApplyField(
|
||||
CONF_HW_FRONTEND_RESET, "reset_hardware_frontend_if", cg.bool_
|
||||
),
|
||||
)
|
||||
async def at581x_settings_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
# Radar configuration
|
||||
if frontend_reset := config.get(CONF_HW_FRONTEND_RESET):
|
||||
template_ = await cg.templatable(frontend_reset, args, cg.int8)
|
||||
cg.add(var.set_hw_frontend_reset(template_))
|
||||
|
||||
if freq := config.get(CONF_FREQUENCY):
|
||||
if not cg.is_template(freq):
|
||||
freq = int(freq / 1000000)
|
||||
template_ = await cg.templatable(freq, args, cg.int_)
|
||||
cg.add(var.set_frequency(template_))
|
||||
|
||||
if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None:
|
||||
template_ = await cg.templatable(sens_dist, args, cg.int_)
|
||||
cg.add(var.set_sensing_distance(template_))
|
||||
|
||||
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
|
||||
template_ = await cg.templatable(selfcheck, args, cg.int_)
|
||||
cg.add(var.set_poweron_selfcheck_time(template_))
|
||||
|
||||
if protect := config.get(CONF_PROTECT_TIME):
|
||||
template_ = await cg.templatable(protect, args, cg.int_)
|
||||
cg.add(var.set_protect_time(template_))
|
||||
|
||||
if trig_base := config.get(CONF_TRIGGER_BASE):
|
||||
template_ = await cg.templatable(trig_base, args, cg.int_)
|
||||
cg.add(var.set_trigger_base(template_))
|
||||
|
||||
if trig_keep := config.get(CONF_TRIGGER_KEEP):
|
||||
template_ = await cg.templatable(trig_keep, args, cg.int_)
|
||||
cg.add(var.set_trigger_keep(template_))
|
||||
|
||||
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
|
||||
template_ = await cg.templatable(stage_gain, args, cg.int_)
|
||||
cg.add(var.set_stage_gain(template_))
|
||||
|
||||
if power := config.get(CONF_POWER_CONSUMPTION):
|
||||
if not cg.is_template(power):
|
||||
power = int(power * 1000000)
|
||||
template_ = await cg.templatable(power, args, cg.int_)
|
||||
cg.add(var.set_power_consumption(template_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -38,6 +38,10 @@ class AT581XComponent final : public Component, public i2c::I2CDevice {
|
||||
|
||||
bool i2c_write_config();
|
||||
bool reset_hardware_frontend();
|
||||
void reset_hardware_frontend_if(bool reset) {
|
||||
if (reset)
|
||||
this->reset_hardware_frontend();
|
||||
}
|
||||
bool i2c_write_reg(uint8_t addr, uint8_t data);
|
||||
bool i2c_write_reg(uint8_t addr, uint32_t data);
|
||||
bool i2c_write_reg(uint8_t addr, uint16_t data);
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "at581x.h"
|
||||
|
||||
namespace esphome::at581x {
|
||||
|
||||
template<typename... Ts> class AT581XResetAction final : public Action<Ts...>, public Parented<AT581XComponent> {
|
||||
public:
|
||||
void play(const Ts &...x) { this->parent_->reset_hardware_frontend(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class AT581XSettingsAction final : public Action<Ts...>, public Parented<AT581XComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(int8_t, hw_frontend_reset)
|
||||
TEMPLATABLE_VALUE(int, frequency)
|
||||
TEMPLATABLE_VALUE(int, sensing_distance)
|
||||
TEMPLATABLE_VALUE(int, poweron_selfcheck_time)
|
||||
TEMPLATABLE_VALUE(int, power_consumption)
|
||||
TEMPLATABLE_VALUE(int, protect_time)
|
||||
TEMPLATABLE_VALUE(int, trigger_base)
|
||||
TEMPLATABLE_VALUE(int, trigger_keep)
|
||||
TEMPLATABLE_VALUE(int, stage_gain)
|
||||
|
||||
void play(const Ts &...x) {
|
||||
if (this->frequency_.has_value()) {
|
||||
int v = this->frequency_.value(x...);
|
||||
this->parent_->set_frequency(v);
|
||||
}
|
||||
if (this->sensing_distance_.has_value()) {
|
||||
int v = this->sensing_distance_.value(x...);
|
||||
this->parent_->set_sensing_distance(v);
|
||||
}
|
||||
if (this->poweron_selfcheck_time_.has_value()) {
|
||||
int v = this->poweron_selfcheck_time_.value(x...);
|
||||
this->parent_->set_poweron_selfcheck_time(v);
|
||||
}
|
||||
if (this->power_consumption_.has_value()) {
|
||||
int v = this->power_consumption_.value(x...);
|
||||
this->parent_->set_power_consumption(v);
|
||||
}
|
||||
if (this->protect_time_.has_value()) {
|
||||
int v = this->protect_time_.value(x...);
|
||||
this->parent_->set_protect_time(v);
|
||||
}
|
||||
if (this->trigger_base_.has_value()) {
|
||||
int v = this->trigger_base_.value(x...);
|
||||
this->parent_->set_trigger_base(v);
|
||||
}
|
||||
if (this->trigger_keep_.has_value()) {
|
||||
int v = this->trigger_keep_.value(x...);
|
||||
this->parent_->set_trigger_keep(v);
|
||||
}
|
||||
if (this->stage_gain_.has_value()) {
|
||||
int v = this->stage_gain_.value(x...);
|
||||
this->parent_->set_stage_gain(v);
|
||||
}
|
||||
|
||||
// This actually perform all the modification on the system
|
||||
this->parent_->i2c_write_config();
|
||||
|
||||
if (this->hw_frontend_reset_.has_value() && this->hw_frontend_reset_.value(x...) == true) {
|
||||
this->parent_->reset_hardware_frontend();
|
||||
}
|
||||
}
|
||||
};
|
||||
} // namespace esphome::at581x
|
||||
@@ -339,10 +339,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# HTTPS streams verify the server against the root certificate bundle
|
||||
require_certificate_bundle()
|
||||
|
||||
add_idf_component(
|
||||
name="esphome/esp-audio-libs",
|
||||
ref="3.2.1",
|
||||
)
|
||||
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
|
||||
|
||||
data = _get_data()
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ esp_err_t AudioReader::add_sink(const std::weak_ptr<ring_buffer::RingBuffer> &ou
|
||||
if (current_audio_file_ != nullptr) {
|
||||
// A transfer buffer isn't ncessary for a local file
|
||||
this->file_ring_buffer_ = output_ring_buffer.lock();
|
||||
if (this->file_ring_buffer_ == nullptr) {
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -51,14 +51,14 @@ void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_le
|
||||
|
||||
void AudioTransferBuffer::clear_buffered_data() {
|
||||
this->buffer_length_ = 0;
|
||||
if (this->ring_buffer_.use_count() > 0) {
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
this->ring_buffer_->reset();
|
||||
}
|
||||
}
|
||||
|
||||
void AudioSinkTransferBuffer::clear_buffered_data() {
|
||||
this->buffer_length_ = 0;
|
||||
if (this->ring_buffer_.use_count() > 0) {
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
this->ring_buffer_->reset();
|
||||
}
|
||||
#ifdef USE_SPEAKER
|
||||
@@ -69,7 +69,7 @@ void AudioSinkTransferBuffer::clear_buffered_data() {
|
||||
}
|
||||
|
||||
bool AudioTransferBuffer::has_buffered_data() const {
|
||||
if (this->ring_buffer_.use_count() > 0) {
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
|
||||
}
|
||||
return (this->available() > 0);
|
||||
@@ -144,7 +144,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_
|
||||
size_t bytes_to_read = AudioTransferBuffer::free();
|
||||
size_t bytes_read = 0;
|
||||
if (bytes_to_read > 0) {
|
||||
if (this->ring_buffer_.use_count() > 0) {
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
bytes_read = this->ring_buffer_->read((void *) this->get_buffer_end(), bytes_to_read, ticks_to_wait);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait,
|
||||
bytes_written = this->speaker_->play(this->data_start_, this->available(), ticks_to_wait);
|
||||
} else
|
||||
#endif
|
||||
if (this->ring_buffer_.use_count() > 0) {
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
bytes_written =
|
||||
this->ring_buffer_->write_without_replacement((void *) this->data_start_, this->available(), ticks_to_wait);
|
||||
} else if (this->sink_callback_ != nullptr) {
|
||||
@@ -186,7 +186,7 @@ bool AudioSinkTransferBuffer::has_buffered_data() const {
|
||||
return (this->speaker_->has_buffered_data() || (this->available() > 0));
|
||||
}
|
||||
#endif
|
||||
if (this->ring_buffer_.use_count() > 0) {
|
||||
if (this->ring_buffer_ != nullptr) {
|
||||
return ((this->ring_buffer_->available() > 0) || (this->available() > 0));
|
||||
}
|
||||
return (this->available() > 0);
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MIC_GAIN
|
||||
from esphome.core import ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.const import CONF_MIC_GAIN
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
@@ -12,8 +11,6 @@ IS_PLATFORM_COMPONENT = True
|
||||
audio_adc_ns = cg.esphome_ns.namespace("audio_adc")
|
||||
AudioAdc = audio_adc_ns.class_("AudioAdc")
|
||||
|
||||
SetMicGainAction = audio_adc_ns.class_("SetMicGainAction", automation.Action)
|
||||
|
||||
|
||||
SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
{
|
||||
@@ -24,25 +21,11 @@ SET_MIC_GAIN_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"audio_adc.set_mic_gain",
|
||||
SetMicGainAction,
|
||||
SET_MIC_GAIN_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_MIC_GAIN, "set_mic_gain", cg.float_),
|
||||
)
|
||||
async def audio_adc_set_mic_gain_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, cg.float_)
|
||||
cg.add(var.set_mic_gain(template_))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "audio_adc.h"
|
||||
|
||||
namespace esphome::audio_adc {
|
||||
|
||||
template<typename... Ts> class SetMicGainAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit SetMicGainAction(AudioAdc *audio_adc) : audio_adc_(audio_adc) {}
|
||||
|
||||
TEMPLATABLE_VALUE(float, mic_gain)
|
||||
|
||||
void play(const Ts &...x) override { this->audio_adc_->set_mic_gain(this->mic_gain_.value(x...)); }
|
||||
|
||||
protected:
|
||||
AudioAdc *audio_adc_;
|
||||
};
|
||||
|
||||
} // namespace esphome::audio_adc
|
||||
@@ -2,9 +2,8 @@ from esphome import automation
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_VOLUME
|
||||
from esphome.core import ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.const import CONF_VOLUME
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
@@ -13,10 +12,6 @@ IS_PLATFORM_COMPONENT = True
|
||||
audio_dac_ns = cg.esphome_ns.namespace("audio_dac")
|
||||
AudioDac = audio_dac_ns.class_("AudioDac")
|
||||
|
||||
MuteOffAction = audio_dac_ns.class_("MuteOffAction", automation.Action)
|
||||
MuteOnAction = audio_dac_ns.class_("MuteOnAction", automation.Action)
|
||||
SetVolumeAction = audio_dac_ns.class_("SetVolumeAction", automation.Action)
|
||||
|
||||
|
||||
MUTE_ACTION_SCHEMA = maybe_simple_id(
|
||||
{
|
||||
@@ -33,41 +28,19 @@ SET_VOLUME_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"audio_dac.mute_off", MuteOffAction, MUTE_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"audio_dac.mute_on", MuteOnAction, MUTE_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def audio_dac_mute_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
for _name, _call in (
|
||||
("audio_dac.mute_off", "set_mute_off()"),
|
||||
("audio_dac.mute_on", "set_mute_on()"),
|
||||
):
|
||||
automation.register_apply_action(
|
||||
_name, MUTE_ACTION_SCHEMA, automation.ApplyCall(_call)
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"audio_dac.set_volume",
|
||||
SetVolumeAction,
|
||||
SET_VOLUME_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_VOLUME, "set_volume", cg.float_),
|
||||
)
|
||||
async def audio_dac_set_volume_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_VOLUME), args, cg.float_)
|
||||
cg.add(var.set_volume(template_))
|
||||
|
||||
return var
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "audio_dac.h"
|
||||
|
||||
namespace esphome::audio_dac {
|
||||
|
||||
template<typename... Ts> class MuteOffAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit MuteOffAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
|
||||
|
||||
void play(const Ts &...x) override { this->audio_dac_->set_mute_off(); }
|
||||
|
||||
protected:
|
||||
AudioDac *audio_dac_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class MuteOnAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit MuteOnAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
|
||||
|
||||
void play(const Ts &...x) override { this->audio_dac_->set_mute_on(); }
|
||||
|
||||
protected:
|
||||
AudioDac *audio_dac_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetVolumeAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit SetVolumeAction(AudioDac *audio_dac) : audio_dac_(audio_dac) {}
|
||||
|
||||
TEMPLATABLE_VALUE(float, volume)
|
||||
|
||||
void play(const Ts &...x) override { this->audio_dac_->set_volume(this->volume_.value(x...)); }
|
||||
|
||||
protected:
|
||||
AudioDac *audio_dac_;
|
||||
};
|
||||
|
||||
} // namespace esphome::audio_dac
|
||||
@@ -203,16 +203,10 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
|
||||
this->away_config_ = away_config;
|
||||
}
|
||||
|
||||
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; }
|
||||
|
||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
|
||||
void BangBangClimate::dump_config() {
|
||||
LOG_CLIMATE("", "Bang Bang Climate", this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
|
||||
@@ -22,10 +22,10 @@ class BangBangClimate final : public climate::Climate, public Component {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_sensor(sensor::Sensor *sensor);
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor);
|
||||
void set_supports_cool(bool supports_cool);
|
||||
void set_supports_heat(bool supports_heat);
|
||||
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
||||
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
||||
|
||||
|
||||
@@ -17,11 +17,7 @@ class BinaryLightOutput final : public light::LightOutput {
|
||||
void write_state(light::LightState *state) override {
|
||||
bool binary;
|
||||
state->current_values_as_binary(&binary);
|
||||
if (binary) {
|
||||
this->output_->turn_on();
|
||||
} else {
|
||||
this->output_->turn_off();
|
||||
}
|
||||
this->output_->set_state(binary);
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from logging import getLogger
|
||||
|
||||
from esphome import automation, core
|
||||
from esphome.automation import Condition, maybe_simple_id
|
||||
from esphome.automation import maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server, zigbee
|
||||
from esphome.components.const import CONF_ON_STATE_CHANGE
|
||||
@@ -39,6 +39,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_GARAGE_DOOR,
|
||||
DEVICE_CLASS_GAS,
|
||||
DEVICE_CLASS_GLASS_BREAK,
|
||||
DEVICE_CLASS_HEAT,
|
||||
DEVICE_CLASS_LIGHT,
|
||||
DEVICE_CLASS_LOCK,
|
||||
@@ -81,6 +82,7 @@ DEVICE_CLASSES = [
|
||||
DEVICE_CLASS_EMPTY,
|
||||
DEVICE_CLASS_GARAGE_DOOR,
|
||||
DEVICE_CLASS_GAS,
|
||||
DEVICE_CLASS_GLASS_BREAK,
|
||||
DEVICE_CLASS_HEAT,
|
||||
DEVICE_CLASS_LIGHT,
|
||||
DEVICE_CLASS_LOCK,
|
||||
@@ -132,15 +134,6 @@ MultiClickTriggerBase = binary_sensor_ns.class_(
|
||||
MultiClickTrigger = binary_sensor_ns.class_("MultiClickTrigger", MultiClickTriggerBase)
|
||||
MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
|
||||
|
||||
BinarySensorPublishAction = binary_sensor_ns.class_(
|
||||
"BinarySensorPublishAction", automation.Action
|
||||
)
|
||||
BinarySensorInvalidateAction = binary_sensor_ns.class_(
|
||||
"BinarySensorInvalidateAction", automation.Action
|
||||
)
|
||||
|
||||
# Condition
|
||||
BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Condition)
|
||||
|
||||
# Filters
|
||||
Filter = binary_sensor_ns.class_("Filter")
|
||||
@@ -452,7 +445,9 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
cv.Optional(CONF_FILTERS): validate_filters,
|
||||
cv.Optional(
|
||||
CONF_FILTERS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_filters,
|
||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_CLICK): cv.All(
|
||||
@@ -644,20 +639,12 @@ BINARY_SENSOR_CONDITION_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"binary_sensor.is_on", BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA
|
||||
automation.register_apply_condition(
|
||||
"binary_sensor.is_on", BINARY_SENSOR_CONDITION_SCHEMA, "state"
|
||||
)
|
||||
async def binary_sensor_is_on_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, True)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"binary_sensor.is_off", BinarySensorCondition, BINARY_SENSOR_CONDITION_SCHEMA
|
||||
automation.register_apply_condition(
|
||||
"binary_sensor.is_off", BINARY_SENSOR_CONDITION_SCHEMA, "state == false"
|
||||
)
|
||||
async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, False)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
@@ -665,20 +652,16 @@ async def to_code(config):
|
||||
cg.add_global(binary_sensor_ns.using)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"binary_sensor.invalidate_state",
|
||||
BinarySensorInvalidateAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(BinarySensor),
|
||||
},
|
||||
key=CONF_ID,
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyCall("invalidate_state()"),
|
||||
)
|
||||
async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
# automation.cpp only implements the click/double_click/multi_click triggers
|
||||
|
||||
@@ -18,26 +18,6 @@ struct MultiClickTriggerEvent {
|
||||
uint32_t max_length;
|
||||
};
|
||||
|
||||
class PressTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit PressTrigger(BinarySensor *parent) {
|
||||
parent->add_on_state_callback([this](bool state) {
|
||||
if (state)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
class ReleaseTrigger final : public Trigger<> {
|
||||
public:
|
||||
explicit ReleaseTrigger(BinarySensor *parent) {
|
||||
parent->add_on_state_callback([this](bool state) {
|
||||
if (!state)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length);
|
||||
|
||||
class ClickTrigger final : public Trigger<> {
|
||||
@@ -140,53 +120,4 @@ template<size_t N> class MultiClickTrigger final : public MultiClickTriggerBase
|
||||
std::array<MultiClickTriggerEvent, N> timing_storage_{};
|
||||
};
|
||||
|
||||
class StateTrigger final : public Trigger<bool> {
|
||||
public:
|
||||
explicit StateTrigger(BinarySensor *parent) {
|
||||
parent->add_on_state_callback([this](bool state) { this->trigger(state); });
|
||||
}
|
||||
};
|
||||
|
||||
class StateChangeTrigger final : public Trigger<optional<bool>, optional<bool> > {
|
||||
public:
|
||||
explicit StateChangeTrigger(BinarySensor *parent) {
|
||||
parent->add_full_state_callback(
|
||||
[this](optional<bool> old_state, optional<bool> state) { this->trigger(old_state, state); });
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class BinarySensorCondition final : public Condition<Ts...> {
|
||||
public:
|
||||
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
|
||||
bool check(const Ts &...x) override { return this->parent_->state == this->state_; }
|
||||
|
||||
protected:
|
||||
BinarySensor *parent_;
|
||||
bool state_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class BinarySensorPublishAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit BinarySensorPublishAction(BinarySensor *sensor) : sensor_(sensor) {}
|
||||
TEMPLATABLE_VALUE(bool, state)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto val = this->state_.value(x...);
|
||||
this->sensor_->publish_state(val);
|
||||
}
|
||||
|
||||
protected:
|
||||
BinarySensor *sensor_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class BinarySensorInvalidateAction final : public Action<Ts...> {
|
||||
public:
|
||||
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
|
||||
|
||||
void play(const Ts &...x) override { this->sensor_->invalidate_state(); }
|
||||
|
||||
protected:
|
||||
BinarySensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -32,7 +32,8 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
|
||||
*/
|
||||
class BinarySensor : public StatefulEntityBase<bool> {
|
||||
public:
|
||||
explicit BinarySensor() = default;
|
||||
// User provided, not "= default": `new(p) BinarySensor()` would zero-fill .bss that is already zero.
|
||||
explicit BinarySensor() {}
|
||||
|
||||
const bool &get_state() const override { return this->state; }
|
||||
void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; }
|
||||
|
||||
@@ -53,6 +53,9 @@ class DelayedOnOffFilter final : public Filter {
|
||||
|
||||
class DelayedOnFilter : public Filter {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) DelayedOnFilter()` would zero-fill .bss that is already zero.
|
||||
DelayedOnFilter() {}
|
||||
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
@@ -63,6 +66,9 @@ class DelayedOnFilter : public Filter {
|
||||
|
||||
class DelayedOffFilter : public Filter {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) DelayedOffFilter()` would zero-fill .bss that is already zero.
|
||||
DelayedOffFilter() {}
|
||||
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
@@ -143,6 +149,8 @@ class StatelessLambdaFilter : public Filter {
|
||||
|
||||
class SettleFilter : public Filter {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) SettleFilter()` would zero-fill .bss that is already zero.
|
||||
SettleFilter() {}
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
@@ -23,7 +23,7 @@ public ble_api.h.
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import libretiny
|
||||
from esphome.components import libretiny, wifi
|
||||
from esphome.components.libretiny.const import (
|
||||
FAMILY_BK7231N,
|
||||
FAMILY_BK7231Q,
|
||||
@@ -84,6 +84,15 @@ def _final_validate(config: ConfigType) -> None:
|
||||
# which run on a BLE 4.2 board. The hard error is raised at codegen.
|
||||
if msg := _unsupported_family_message(libretiny.get_libretiny_family()):
|
||||
_LOGGER.warning("%s (this configuration cannot compile)", msg)
|
||||
# Any wifi power_save_mode other than NONE also arms the Beken SDK's MCU
|
||||
# sleep. With the BLE controller running, that sleep never wakes up once the
|
||||
# station is stopped (adapter restart after failed roams, wifi.disable): the
|
||||
# device is dead until a power cycle (esphome#18592). Keep power save off
|
||||
# until LibreTiny ships the SDK-side fix (libretiny-eu/libretiny#414).
|
||||
wifi.force_power_save_off(
|
||||
"with BLE running, the Beken SDK's MCU sleep halts the device once the "
|
||||
"station is stopped (https://github.com/esphome/esphome/issues/18592)"
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
@@ -53,7 +53,6 @@ BK72xxBLETracker = bk72xx_ble_tracker_ns.class_(
|
||||
)
|
||||
|
||||
StartScanAction = bk72xx_ble_tracker_ns.class_("StartScanAction", automation.Action)
|
||||
StopScanAction = bk72xx_ble_tracker_ns.class_("StopScanAction", automation.Action)
|
||||
|
||||
ESPBTAdvertiseTrigger = ble_automation.ESPBTAdvertiseTrigger
|
||||
BLEServiceDataAdvertiseTrigger = ble_automation.BLEServiceDataAdvertiseTrigger
|
||||
@@ -121,9 +120,8 @@ async def start_scan_action_to_code(
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"bk72xx_ble_tracker.stop_scan",
|
||||
StopScanAction,
|
||||
automation.maybe_simple_id(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -131,17 +129,8 @@ async def start_scan_action_to_code(
|
||||
}
|
||||
)
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyCall("stop_scan()"),
|
||||
)
|
||||
async def stop_scan_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: list,
|
||||
) -> cg.MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
|
||||
@@ -38,11 +38,6 @@ template<typename... Ts> class StartScanAction final : public Action<Ts...>, pub
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class StopScanAction final : public Action<Ts...>, public Parented<BK72xxBLETracker> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->stop_scan(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::bk72xx_ble_tracker
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/datatypes.h"
|
||||
|
||||
@@ -47,8 +46,6 @@ struct sbe24_t { // NOLINT(readability-identifier-naming,altera-struct-pack-ali
|
||||
int8_t h{0};
|
||||
} __attribute__((packed));
|
||||
|
||||
template<typename... Ts> class ResetEnergyAction;
|
||||
|
||||
class BL0906;
|
||||
|
||||
using ActionCallbackFuncPtr = void (BL0906::*)();
|
||||
@@ -85,9 +82,10 @@ class BL0906 final : public PollingComponent, public uart::UARTDevice {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
template<typename... Ts> friend class ResetEnergyAction;
|
||||
/// Queue an energy counter reset for the next poll
|
||||
void reset_energy() { this->enqueue_action_(&BL0906::reset_energy_); }
|
||||
|
||||
protected:
|
||||
void reset_energy_();
|
||||
|
||||
void read_data_(uint8_t address, float reference, sensor::Sensor *sensor);
|
||||
@@ -103,9 +101,4 @@ class BL0906 final : public PollingComponent, public uart::UARTDevice {
|
||||
std::vector<ActionCallbackFuncPtr> action_queue_{};
|
||||
};
|
||||
|
||||
template<typename... Ts> class ResetEnergyAction final : public Action<Ts...>, public Parented<BL0906> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->enqueue_action_(&BL0906::reset_energy_); }
|
||||
};
|
||||
|
||||
} // namespace esphome::bl0906
|
||||
|
||||
@@ -32,8 +32,6 @@ from esphome.const import (
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
# Import ICONS not included in esphome's const.py, from the local components const.py
|
||||
@@ -45,7 +43,6 @@ CONF_TOTAL_ENERGY = "total_energy"
|
||||
|
||||
bl0906_ns = cg.esphome_ns.namespace("bl0906")
|
||||
BL0906 = bl0906_ns.class_("BL0906", cg.PollingComponent, uart.UARTDevice)
|
||||
ResetEnergyAction = bl0906_ns.class_("ResetEnergyAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
@@ -138,25 +135,15 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"bl0906.reset_energy",
|
||||
ResetEnergyAction,
|
||||
maybe_simple_id(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.use_id(BL0906),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyCall("reset_energy()"),
|
||||
)
|
||||
async def reset_energy_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
|
||||
@@ -207,26 +207,20 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_simple_action(
|
||||
"ble_client.disconnect",
|
||||
BLEDisconnectAction,
|
||||
BLE_CONNECT_ACTION_SCHEMA,
|
||||
synchronous=False,
|
||||
)
|
||||
async def ble_disconnect_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_simple_action(
|
||||
"ble_client.connect",
|
||||
BLEConnectAction,
|
||||
BLE_CONNECT_ACTION_SCHEMA,
|
||||
synchronous=False,
|
||||
)
|
||||
async def ble_connect_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -328,15 +322,12 @@ async def passkey_reply_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_simple_action(
|
||||
"ble_client.remove_bond",
|
||||
BLERemoveBondAction,
|
||||
BLE_REMOVE_BOND_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def remove_bond_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -45,15 +45,7 @@ void BluedroidGattClient::setup() {
|
||||
|
||||
void BluedroidGattClient::loop() {
|
||||
if (!esp32_ble::global_ble->is_active()) {
|
||||
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
|
||||
// frees its slot, then re-register the app on the next enable.
|
||||
auto down_st = this->state();
|
||||
if (down_st != ClientState::IDLE && down_st != ClientState::INIT) {
|
||||
this->release_services();
|
||||
this->set_idle_();
|
||||
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
|
||||
}
|
||||
this->set_state(ClientState::INIT);
|
||||
// ble_before_disabled_event_handler() settles the slot.
|
||||
return;
|
||||
}
|
||||
auto st = this->state();
|
||||
@@ -65,7 +57,7 @@ void BluedroidGattClient::loop() {
|
||||
ESP_LOGE(TAG, "gattc app register failed: app_id=%d code=%d", this->app_id, ret);
|
||||
this->mark_failed();
|
||||
}
|
||||
// Do not wait for REG_EVT; a dropped event must not wedge the slot.
|
||||
// Do not wait for REG_EVT; connect() rejects until it lands.
|
||||
this->set_idle_();
|
||||
} else if (st == ClientState::DISCONNECTING || this->disconnect_pending()) {
|
||||
// The one teardown safety net: a lost CLOSE_EVT, or a scheduled
|
||||
@@ -78,8 +70,8 @@ void BluedroidGattClient::loop() {
|
||||
this->listener_->on_connection_state(false, 0, ESP_GATT_CONN_TIMEOUT);
|
||||
}
|
||||
} else {
|
||||
// The loop stays on while a link exists (stack-down watch, pre-started
|
||||
// search flush); it settles only back at IDLE.
|
||||
// The loop stays on while a link exists (pre-started search flush); it
|
||||
// settles only back at IDLE.
|
||||
this->deliver_pending_search_();
|
||||
if (this->state() == ClientState::IDLE) {
|
||||
this->disable_loop();
|
||||
@@ -87,6 +79,22 @@ void BluedroidGattClient::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Stack down: no CLOSE_EVT will come. Settle a live link so the consumer
|
||||
// frees its slot, then register the app again on the next enable.
|
||||
void BluedroidGattClient::ble_before_disabled_event_handler() {
|
||||
auto st = this->state();
|
||||
if (st != ClientState::IDLE && st != ClientState::INIT) {
|
||||
this->release_services();
|
||||
this->set_idle_();
|
||||
this->listener_->on_connection_state(false, 0, ble_device_base::GATT_ERR_NOT_CONNECTED);
|
||||
}
|
||||
// The interface belongs to the torn-down stack.
|
||||
this->gattc_if_ = ESP_GATT_IF_NONE;
|
||||
this->set_state(ClientState::INIT);
|
||||
// An idle slot runs no loop; the INIT branch must run to register again.
|
||||
this->enable_loop();
|
||||
}
|
||||
|
||||
void BluedroidGattClient::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Bluedroid GATT client %d", this->connection_index_);
|
||||
if (this->is_failed()) {
|
||||
@@ -97,6 +105,11 @@ void BluedroidGattClient::dump_config() {
|
||||
// ---- contract ops ----
|
||||
|
||||
int BluedroidGattClient::connect(uint64_t address, uint8_t addr_type) {
|
||||
if (this->gattc_if_ == ESP_GATT_IF_NONE) {
|
||||
// Bluedroid drops an open on an unknown interface without any event.
|
||||
ESP_LOGW(TAG, "[%d] Connect rejected, GATT app not registered", this->connection_index_);
|
||||
return ble_device_base::GATT_ERR_NOT_CONNECTED;
|
||||
}
|
||||
// Only from idle: clobbering DISCONNECTING would open a new link the
|
||||
// stale CLOSE_EVT then tears down.
|
||||
if (this->state() != ClientState::IDLE) {
|
||||
|
||||
@@ -31,6 +31,9 @@ class BluetoothConnection;
|
||||
// void disconnect() cannot overload with an int-returning twin.
|
||||
class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public Component {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) BluedroidGattClient()` would zero-fill .bss that is already zero.
|
||||
BluedroidGattClient() {}
|
||||
|
||||
static constexpr uint16_t UNSET_CONN_ID = 0xFFFF;
|
||||
|
||||
// Lifecycle of one connection attempt's service search.
|
||||
@@ -56,6 +59,7 @@ class BluedroidGattClient final : public esp32_ble_tracker::ESPBTClient, public
|
||||
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
|
||||
void connect() override;
|
||||
void disconnect() override;
|
||||
void ble_before_disabled_event_handler() override;
|
||||
bool wants_parsed_advertisements() override { return false; }
|
||||
void on_scan_end() override {}
|
||||
bool parse_device(const ble_device_base::ESPBTDevice &device) override { return false; }
|
||||
|
||||
@@ -37,6 +37,9 @@ enum class PendingAck : uint8_t {
|
||||
|
||||
class BluetoothConnection final : public ble_device_base::GattClientListener {
|
||||
public:
|
||||
// User provided, not "= default": `new(p) BluetoothConnection()` would zero-fill .bss that is already zero.
|
||||
BluetoothConnection() {}
|
||||
|
||||
/// Wire the platform backend. Called from codegen before setup.
|
||||
void set_backend(ble_device_base::BLEGattConnection *backend) {
|
||||
this->backend_ = backend;
|
||||
|
||||
@@ -626,7 +626,7 @@ void RP2GattClient::handle_connected_(uint8_t status, uint16_t con_handle) {
|
||||
// explicit kick the MTU would only be exchanged on the first GATT query,
|
||||
// which never happens on a V3_WITH_CACHE connection.
|
||||
// Both registration calls above return void (BTstack 075a078, arduino-pico
|
||||
// 6.0.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
|
||||
// 6.1.0); failures surface as a missing GATT_EVENT_MTU and are reclaimed by
|
||||
// the connect timeout in loop().
|
||||
gatt_client_send_mtu_negotiation(&RP2GattClient::gatt_packet_handler, this->con_handle_);
|
||||
}
|
||||
|
||||
@@ -395,6 +395,17 @@ async def _to_code_ble_hub(config: ConfigType) -> None:
|
||||
await _connections_to_code(var, config)
|
||||
|
||||
|
||||
def enable_advertisement_filter() -> None:
|
||||
"""Compile the advertisement filter hook into bluetooth_proxy.
|
||||
|
||||
Called by external filtering components from to_code(). The define behind
|
||||
this is an implementation detail; do not emit it directly.
|
||||
|
||||
Public API for external components. Do not remove.
|
||||
"""
|
||||
cg.add_define("USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER")
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if CORE.is_esp32:
|
||||
await _to_code_esp32(config)
|
||||
|
||||
@@ -94,6 +94,15 @@ void BluetoothProxy::on_raw_advertisement_(const ble_device_base::RawAdvertiseme
|
||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||||
return;
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
// Ask the filter before the packet is queued, so a dropped advertisement never
|
||||
// reaches the batch or the network.
|
||||
if (this->advertisement_filter_.is_set() && !this->advertisement_filter_.should_forward(raw)) {
|
||||
ESP_LOGVV(TAG, "Filtered packet from %012" PRIX64, raw.address);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
auto &adv = this->response_.advertisements[this->response_.advertisements_len];
|
||||
adv.address = raw.address;
|
||||
adv.rssi = raw.rssi;
|
||||
@@ -184,6 +193,9 @@ void BluetoothProxy::dump_config() {
|
||||
" Adapter MAC: %s",
|
||||
scan_mode, mac_out);
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
ESP_LOGCONFIG(TAG, " Advertisement filter: %s", YESNO(this->advertisement_filter_.is_set()));
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
|
||||
@@ -97,6 +97,29 @@ static_assert(pending_reply_round_trips(0xABCD112233445566ULL, 0x000011223344556
|
||||
static_assert(PendingReply{}.empty());
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
/// Predicate slot letting an external component drop advertisements before they
|
||||
/// are queued for the API. Same shape as
|
||||
/// ble_device_base::RawAdvertisementCallback. Runs on the advertisement hot
|
||||
/// path, so it must be cheap and must not block.
|
||||
///
|
||||
/// Usage:
|
||||
/// proxy->set_advertisement_filter({this, [](void *self, const ble_device_base::RawAdvertisement &adv) {
|
||||
/// return static_cast<MyFilter *>(self)->should_forward(adv);
|
||||
/// }});
|
||||
///
|
||||
/// Returning false drops the advertisement. Not called at all while the API is
|
||||
/// disconnected, which matters to a stateful filter. Compiled in only when an
|
||||
/// external component calls bluetooth_proxy.enable_advertisement_filter().
|
||||
struct AdvertisementFilter {
|
||||
void *instance{nullptr};
|
||||
bool (*fn)(void *instance, const ble_device_base::RawAdvertisement &adv){nullptr};
|
||||
/// A default-constructed slot is "no filter"; the proxy guards on this.
|
||||
bool is_set() const { return this->fn != nullptr; }
|
||||
bool should_forward(const ble_device_base::RawAdvertisement &adv) const { return this->fn(this->instance, adv); }
|
||||
};
|
||||
#endif // USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
|
||||
class BluetoothProxy final : public Component {
|
||||
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
|
||||
// Allow the connection to update connections_free_response_
|
||||
@@ -162,6 +185,11 @@ class BluetoothProxy final : public Component {
|
||||
void set_active(bool active) { this->active_ = active; }
|
||||
bool has_active() { return this->active_; }
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
/// One subscriber; a later call replaces an earlier one.
|
||||
void set_advertisement_filter(AdvertisementFilter filter) { this->advertisement_filter_ = filter; }
|
||||
#endif
|
||||
|
||||
uint32_t get_legacy_version() const {
|
||||
if (!this->active_) {
|
||||
return LEGACY_PASSIVE_ONLY_VERSION;
|
||||
@@ -330,6 +358,10 @@ class BluetoothProxy final : public Component {
|
||||
// start on an even word, closing two alignment holes.
|
||||
uint32_t last_advertisement_flush_time_{0};
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY_ADVERTISEMENT_FILTER
|
||||
AdvertisementFilter advertisement_filter_{};
|
||||
#endif
|
||||
|
||||
// BLE advertisement batching
|
||||
api::BluetoothLERawAdvertisementsResponse response_;
|
||||
|
||||
|
||||
@@ -34,24 +34,4 @@ class BM8563 final : public time::RealTimeClock, public i2c::I2CDevice {
|
||||
uint8_t byte_to_bcd2_(uint8_t value);
|
||||
};
|
||||
|
||||
template<typename... Ts> class WriteAction final : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->write_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ReadAction final : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->read_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class TimerAction final : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint32_t, duration)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto duration = this->duration_.value(x...);
|
||||
this->parent_->start_timer(duration);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::bm8563
|
||||
|
||||
@@ -3,8 +3,6 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DURATION, CONF_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -13,9 +11,6 @@ I2C_ADDR = 0x51
|
||||
|
||||
bm8563_ns = cg.esphome_ns.namespace("bm8563")
|
||||
BM8563 = bm8563_ns.class_("BM8563", time.RealTimeClock, i2c.I2CDevice)
|
||||
WriteAction = bm8563_ns.class_("WriteAction", automation.Action)
|
||||
ReadAction = bm8563_ns.class_("ReadAction", automation.Action)
|
||||
TimerAction = bm8563_ns.class_("TimerAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
time.TIME_SCHEMA.extend(
|
||||
@@ -28,70 +23,30 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bm8563.write_time",
|
||||
WriteAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
BM8563_ACTION_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
}
|
||||
)
|
||||
async def bm8563_write_time_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
automation.register_apply_action(
|
||||
"bm8563.write_time", BM8563_ACTION_SCHEMA, automation.ApplyCall("write_time()")
|
||||
)
|
||||
|
||||
@automation.register_action(
|
||||
automation.register_apply_action(
|
||||
"bm8563.read_time", BM8563_ACTION_SCHEMA, automation.ApplyCall("read_time()")
|
||||
)
|
||||
|
||||
automation.register_apply_action(
|
||||
"bm8563.start_timer",
|
||||
TimerAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
automation.ApplyField(CONF_DURATION, "start_timer", cg.uint32),
|
||||
)
|
||||
async def bm8563_start_timer_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32)
|
||||
cg.add(var.set_duration(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bm8563.read_time",
|
||||
ReadAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def bm8563_read_time_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user