mirror of
https://github.com/esphome/esphome.git
synced 2026-06-25 08:38:24 +00:00
Compare commits
725 Commits
copilot/lv
...
config-ver
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fbd571df7 | ||
|
|
bb81c91d0c | ||
|
|
78f1467be4 | ||
|
|
da44d43981 | ||
|
|
9cebce1b6e | ||
|
|
b20fedd806 | ||
|
|
ee91ad8f06 | ||
|
|
7560112144 | ||
|
|
43c6b839cd | ||
|
|
0c9d443a5c | ||
|
|
14defb69b6 | ||
|
|
3a6f3dfb94 | ||
|
|
7bd36e0c8d | ||
|
|
e4f413adad | ||
|
|
1504ac3d19 | ||
|
|
947c714f89 | ||
|
|
e4d5886383 | ||
|
|
f504099485 | ||
|
|
cb56f9a9bf | ||
|
|
26a656af29 | ||
|
|
a8bd035b62 | ||
|
|
f05fa45747 | ||
|
|
78875abee4 | ||
|
|
37608c2656 | ||
|
|
a5b1f3eece | ||
|
|
0d3a3552da | ||
|
|
0a0176d600 | ||
|
|
4cb7ea2584 | ||
|
|
a43ee15b56 | ||
|
|
213ab312d2 | ||
|
|
94f30d5950 | ||
|
|
6af341bb5b | ||
|
|
82656cb0cf | ||
|
|
b72f5447c3 | ||
|
|
73b8e8ac09 | ||
|
|
9459f0426d | ||
|
|
0dae41aa22 | ||
|
|
7321e6e52f | ||
|
|
f0c21520aa | ||
|
|
b0c133201f | ||
|
|
572fb83015 | ||
|
|
0d3db2b670 | ||
|
|
e5f6a734ba | ||
|
|
bab9cd3e7a | ||
|
|
36812591eb | ||
|
|
1862c6115f | ||
|
|
ef780886c3 | ||
|
|
602305b20d | ||
|
|
78701debec | ||
|
|
08ac61ae94 | ||
|
|
6d5340f253 | ||
|
|
e2dfef5ddc | ||
|
|
1d88027618 | ||
|
|
9841deec31 | ||
|
|
ed5852c2d6 | ||
|
|
b26601a3dc | ||
|
|
f5806818cd | ||
|
|
c3e739eba9 | ||
|
|
b167b64f06 | ||
|
|
722cfae04c | ||
|
|
9cb2b562b9 | ||
|
|
81fb6712fe | ||
|
|
227dfa3730 | ||
|
|
aa80bdbbc6 | ||
|
|
914ed10bcc | ||
|
|
92c99a7d41 | ||
|
|
af1aaba547 | ||
|
|
5a2b7546f6 | ||
|
|
4047d5af5f | ||
|
|
6857e1ceb4 | ||
|
|
4479212008 | ||
|
|
cb90ac45c3 | ||
|
|
1847666e75 | ||
|
|
aad1318b4a | ||
|
|
7a23a339e9 | ||
|
|
38d894dfe7 | ||
|
|
b293be23b0 | ||
|
|
ccb53e34ca | ||
|
|
ec9d59f3dc | ||
|
|
df72aa26c0 | ||
|
|
d3691c7ca5 | ||
|
|
562ce541a0 | ||
|
|
6ebe1e92eb | ||
|
|
1bf455cfbb | ||
|
|
290e213cd0 | ||
|
|
b1b0005574 | ||
|
|
70ea527161 | ||
|
|
34c35c84d5 | ||
|
|
bcbfc843ae | ||
|
|
d4fe46bb24 | ||
|
|
523c6f2376 | ||
|
|
b018ac67bc | ||
|
|
1a529a62aa | ||
|
|
6a46437a5f | ||
|
|
cfe8c0eeee | ||
|
|
b232fc91ab | ||
|
|
ac50f33388 | ||
|
|
ff52bb3029 | ||
|
|
627e440bd6 | ||
|
|
6bb90a1268 | ||
|
|
7d8add70a7 | ||
|
|
9094392870 | ||
|
|
c6ad23fbc0 | ||
|
|
6af7a9ed8f | ||
|
|
0b051289f5 | ||
|
|
d8329dba22 | ||
|
|
ee70a4aa72 | ||
|
|
04a58159d0 | ||
|
|
4c758fa1da | ||
|
|
c8e21802db | ||
|
|
b40ffacb8d | ||
|
|
e0118dd8eb | ||
|
|
e7194dce75 | ||
|
|
01b5bef37f | ||
|
|
403a9f7b7e | ||
|
|
10f52f2056 | ||
|
|
274c01ca74 | ||
|
|
3b82c6e38b | ||
|
|
f59a1011df | ||
|
|
82c0cb8929 | ||
|
|
2bdd9f6217 | ||
|
|
193e7d476d | ||
|
|
1b3e7d5ec4 | ||
|
|
767a8c49b0 | ||
|
|
4c43f7e9d0 | ||
|
|
3ef140e25d | ||
|
|
0a568a3e1e | ||
|
|
ef44491c69 | ||
|
|
089a2c99e2 | ||
|
|
311812c8cc | ||
|
|
a77ab59436 | ||
|
|
89fbfc6f71 | ||
|
|
28f3bcdba3 | ||
|
|
445715b9fd | ||
|
|
8843c36ec6 | ||
|
|
bd63f63b36 | ||
|
|
033e144e06 | ||
|
|
20d49f9a7c | ||
|
|
3b2caa1f5b | ||
|
|
c3769e4fce | ||
|
|
6d894dd6ee | ||
|
|
2db2b89eb1 | ||
|
|
e48c7165c5 | ||
|
|
506edaadd5 | ||
|
|
3f82a3a519 | ||
|
|
79cee864cb | ||
|
|
9f5ed938e5 | ||
|
|
4729efbd04 | ||
|
|
da9fbb8044 | ||
|
|
cf01163c8c | ||
|
|
5ba8c644e4 | ||
|
|
c833ff4a84 | ||
|
|
2a530a4bf4 | ||
|
|
6b4b653462 | ||
|
|
edb16a27d3 | ||
|
|
21df5d9bf6 | ||
|
|
73c972a604 | ||
|
|
8cdffef82a | ||
|
|
4034809281 | ||
|
|
ce6bffb65c | ||
|
|
e8bc4bedb4 | ||
|
|
b85a7ef317 | ||
|
|
9f7e310526 | ||
|
|
af7cb1d81e | ||
|
|
53ce2a2f7f | ||
|
|
fb0283e0ee | ||
|
|
5d0cfc31fa | ||
|
|
f30f0a0edc | ||
|
|
6aa538a61d | ||
|
|
7918a93a7f | ||
|
|
fe6ecb24b4 | ||
|
|
6db787d5e4 | ||
|
|
5b4385a084 | ||
|
|
4f69c3b850 | ||
|
|
c62a75ee17 | ||
|
|
d4e9c62d92 | ||
|
|
ac8a2467a5 | ||
|
|
dc1dd9ebb7 | ||
|
|
0c06d78a4f | ||
|
|
41c9ed28cd | ||
|
|
5608aa10a5 | ||
|
|
daa68a2a60 | ||
|
|
a408b5a4fe | ||
|
|
e264c97454 | ||
|
|
8790dec137 | ||
|
|
6480868e6e | ||
|
|
0578e43352 | ||
|
|
2a89d4835f | ||
|
|
5084c61016 | ||
|
|
b45f94d511 | ||
|
|
66a4752e13 | ||
|
|
4d4f78de81 | ||
|
|
0faa641c8a | ||
|
|
0f16d27a72 | ||
|
|
835ee456a5 | ||
|
|
17f3b7dbd5 | ||
|
|
171a429526 | ||
|
|
e4ee2b7c04 | ||
|
|
c85a062e23 | ||
|
|
873378fa1f | ||
|
|
4f00ad409e | ||
|
|
20b516ff11 | ||
|
|
8754bbfa89 | ||
|
|
6d92cc3d2b | ||
|
|
2f684bf4f3 | ||
|
|
45af21bf38 | ||
|
|
e6318a2d16 | ||
|
|
bef4c8a86c | ||
|
|
6e67864510 | ||
|
|
c2af4874f9 | ||
|
|
2001b91280 | ||
|
|
5460ee7edd | ||
|
|
40081e5ae7 | ||
|
|
a7c5b0ab46 | ||
|
|
e1a813e11f | ||
|
|
1dfeef0265 | ||
|
|
395610c117 | ||
|
|
ae96f82b82 | ||
|
|
2c610abcd0 | ||
|
|
d3591c8d9e | ||
|
|
ec420d5792 | ||
|
|
17209df7b5 | ||
|
|
9cf9b02ba2 | ||
|
|
c90fa2378a | ||
|
|
c04dfa922e | ||
|
|
668007707d | ||
|
|
ab71f5276f | ||
|
|
d062f62656 | ||
|
|
03db32d045 | ||
|
|
8f6d489a9a | ||
|
|
dd07fba943 | ||
|
|
6f5d642a31 | ||
|
|
2721f08bcc | ||
|
|
eafc5df3f2 | ||
|
|
46d0c29be5 | ||
|
|
abdbbf4dd2 | ||
|
|
4dc0599a7d | ||
|
|
ded0936b2a | ||
|
|
52c35ec09c | ||
|
|
76490e45bc | ||
|
|
0a8130858c | ||
|
|
ff5ba99d16 | ||
|
|
14ec82084b | ||
|
|
8e02d0a20e | ||
|
|
faa05031a7 | ||
|
|
d4cce142c5 | ||
|
|
576d89a82a | ||
|
|
4a18ef87d7 | ||
|
|
2cd92a311b | ||
|
|
94f1e48d95 | ||
|
|
19c8f0ac7a | ||
|
|
312dea7ddb | ||
|
|
fb0033947c | ||
|
|
4b8f99ed10 | ||
|
|
4a764ae1e3 | ||
|
|
5b840c1662 | ||
|
|
62d84db5a4 | ||
|
|
019d415bbd | ||
|
|
7de060ed55 | ||
|
|
cfa41b3467 | ||
|
|
0a42a11f1c | ||
|
|
063a8ce666 | ||
|
|
a2bd83382b | ||
|
|
869cace2f3 | ||
|
|
b83edf6c17 | ||
|
|
e1aa92b983 | ||
|
|
a72609e640 | ||
|
|
a8b7c7a4ac | ||
|
|
9bf53e0ab8 | ||
|
|
51f3f5c774 | ||
|
|
313b9fd5bf | ||
|
|
e658a8559e | ||
|
|
4db82877af | ||
|
|
2e3ff4e215 | ||
|
|
8ffe0f5e31 | ||
|
|
c7513b9262 | ||
|
|
de7f081799 | ||
|
|
88f4067dd6 | ||
|
|
d20d613c1d | ||
|
|
801f3fadaa | ||
|
|
b307c7c74c | ||
|
|
aad898503d | ||
|
|
14bcdfe700 | ||
|
|
0d7f2f05b9 | ||
|
|
ee7b38504b | ||
|
|
5d31f4aeba | ||
|
|
9fe4d5c63d | ||
|
|
97ad5ab35f | ||
|
|
e7ddc6f6d3 | ||
|
|
cbcf80081b | ||
|
|
3073f3ec5c | ||
|
|
5a52936f72 | ||
|
|
3ca3cdc5e2 | ||
|
|
4ebfe71b8f | ||
|
|
2fe6cb392b | ||
|
|
d354747da0 | ||
|
|
17ec5389d8 | ||
|
|
687753b0be | ||
|
|
186525e77d | ||
|
|
9d396cea5a | ||
|
|
ac14b9e558 | ||
|
|
ef6c65c7ec | ||
|
|
c6c743e2bb | ||
|
|
6460f3a757 | ||
|
|
0d809a7481 | ||
|
|
674d030cbb | ||
|
|
7ab7538220 | ||
|
|
488a6a1c40 | ||
|
|
f94e1dfab6 | ||
|
|
e49384cd57 | ||
|
|
10b38e1588 | ||
|
|
b6ef1a58fb | ||
|
|
9894bdc0f1 | ||
|
|
99ee405f4e | ||
|
|
517d0390d0 | ||
|
|
96c3986481 | ||
|
|
e62c78ad46 | ||
|
|
e428cb5092 | ||
|
|
b8b8d1bb15 | ||
|
|
82dc80a413 | ||
|
|
d15fa84f4f | ||
|
|
4fa3e48d33 | ||
|
|
094e0440c6 | ||
|
|
b155c13117 | ||
|
|
0816579fa9 | ||
|
|
c6e683cc33 | ||
|
|
14bcd9db59 | ||
|
|
d9da91efbe | ||
|
|
017af24c22 | ||
|
|
496c395f1a | ||
|
|
29ca7bc8f9 | ||
|
|
62d0c25a2b | ||
|
|
1c67e4ce4c | ||
|
|
162c8810db | ||
|
|
9036c29c8a | ||
|
|
9bd936112d | ||
|
|
c98bb9060f | ||
|
|
ce0d360790 | ||
|
|
2b5ee69eb2 | ||
|
|
5a14d6a4ad | ||
|
|
6f62b2f18c | ||
|
|
c78fb964a2 | ||
|
|
8650c5b013 | ||
|
|
5051891813 | ||
|
|
95e2b0a8b0 | ||
|
|
ab45591507 | ||
|
|
62b4b250c7 | ||
|
|
a7963bee98 | ||
|
|
e86978f0da | ||
|
|
6044f41db5 | ||
|
|
a64f09a43f | ||
|
|
dbd4e77d61 | ||
|
|
02185fb4f4 | ||
|
|
2f2b7e42ba | ||
|
|
1c97954b47 | ||
|
|
859ea23bde | ||
|
|
7644f17cf6 | ||
|
|
1de94c1a84 | ||
|
|
10f08e0802 | ||
|
|
aac74f4c94 | ||
|
|
07f6be679f | ||
|
|
ea0ce710a8 | ||
|
|
155657f1cc | ||
|
|
0f2d8656ad | ||
|
|
30d1230a17 | ||
|
|
83a4edbea1 | ||
|
|
f193bab60b | ||
|
|
f01762ea44 | ||
|
|
f23843130e | ||
|
|
c7a163441e | ||
|
|
ae9068a4c4 | ||
|
|
dae8ea1b04 | ||
|
|
2d7eb116f2 | ||
|
|
9ea27e68ee | ||
|
|
4d2062282e | ||
|
|
2d9a42e4ba | ||
|
|
830517a98f | ||
|
|
1a1725f958 | ||
|
|
297f9c134f | ||
|
|
f51871fa6b | ||
|
|
9ee5089891 | ||
|
|
b0d39aedd3 | ||
|
|
89de00e7ce | ||
|
|
53b6528cc5 | ||
|
|
16ae753317 | ||
|
|
2337767c38 | ||
|
|
4f2290d548 | ||
|
|
7ab26a4fe0 | ||
|
|
533eeabf1d | ||
|
|
c6bb1fe141 | ||
|
|
f8f65c1a7b | ||
|
|
d90e2a6a9a | ||
|
|
4969fd6e99 | ||
|
|
95683b7416 | ||
|
|
38f4dc3217 | ||
|
|
f2a0d9943d | ||
|
|
ea0227a206 | ||
|
|
5a23669747 | ||
|
|
2a5933e4f7 | ||
|
|
6fecd72049 | ||
|
|
8360502a94 | ||
|
|
5548a32771 | ||
|
|
6f05e3d204 | ||
|
|
bcd8ddeabe | ||
|
|
af662da90d | ||
|
|
710186998b | ||
|
|
be3e0c27bf | ||
|
|
4d0d3cc271 | ||
|
|
4134763f34 | ||
|
|
1e72f0ee5a | ||
|
|
63710a4cb7 | ||
|
|
c82166e5f3 | ||
|
|
90624e6eca | ||
|
|
6b89998b60 | ||
|
|
dde472b0cf | ||
|
|
f7222a0e6c | ||
|
|
0262d20bbe | ||
|
|
37b33f62de | ||
|
|
2f405fd96f | ||
|
|
67ee727e38 | ||
|
|
12a0f5959f | ||
|
|
5dcae1a133 | ||
|
|
0343121e9b | ||
|
|
da09e1e1ce | ||
|
|
e7e590b36f | ||
|
|
da8d9d9c2d | ||
|
|
b8a9d327f0 | ||
|
|
a359ecaaf4 | ||
|
|
c21c7dd292 | ||
|
|
34295fbd69 | ||
|
|
3fbf0f0c01 | ||
|
|
1436d034bf | ||
|
|
08c7b3afbd | ||
|
|
f36d78e09c | ||
|
|
be56be5201 | ||
|
|
bcc7b8f490 | ||
|
|
27c662e73f | ||
|
|
eefbb42be4 | ||
|
|
b5c4449a16 | ||
|
|
5cdbbd4887 | ||
|
|
bdce47e764 | ||
|
|
813b142b72 | ||
|
|
b7dabe236e | ||
|
|
2e3ea2152d | ||
|
|
ea609d3552 | ||
|
|
f33fd047ee | ||
|
|
cc88896280 | ||
|
|
fbfb5d401f | ||
|
|
212b3e1688 | ||
|
|
31a70ab299 | ||
|
|
8f2cf8b8a7 | ||
|
|
600ca01fd3 | ||
|
|
65051153ac | ||
|
|
514c0c8331 | ||
|
|
dc634b8c7b | ||
|
|
66a4acafd0 | ||
|
|
3bf45d8fe0 | ||
|
|
9cd7c5e700 | ||
|
|
d79cf1d718 | ||
|
|
3d8a3a91f2 | ||
|
|
3fd3dcc7e5 | ||
|
|
7b5a4b466a | ||
|
|
92642df419 | ||
|
|
f5f99071fb | ||
|
|
cb15e98765 | ||
|
|
2f2c7ac393 | ||
|
|
d9788aaefc | ||
|
|
f7b410fd0c | ||
|
|
e261b5de65 | ||
|
|
954227b203 | ||
|
|
4a23ba7d8a | ||
|
|
b71c406e70 | ||
|
|
15bcd62f22 | ||
|
|
23dcc5389d | ||
|
|
9dca7e0daf | ||
|
|
66b6d36a26 | ||
|
|
2064eef273 | ||
|
|
64e836f9c8 | ||
|
|
2cb987095d | ||
|
|
da6c4e20fe | ||
|
|
26b426bbff | ||
|
|
2449aa75af | ||
|
|
2c9a3051d6 | ||
|
|
9b97e95cf3 | ||
|
|
c64bc24960 | ||
|
|
ceb3cb2ae7 | ||
|
|
a3913b98ba | ||
|
|
ef65e47bc5 | ||
|
|
53b2a03c80 | ||
|
|
58df755d8b | ||
|
|
c5eb0eb984 | ||
|
|
f25fa71235 | ||
|
|
8561a8c495 | ||
|
|
8688ef7125 | ||
|
|
46ea61666e | ||
|
|
8969eb76e9 | ||
|
|
ffee4c22b3 | ||
|
|
ad3f6ae313 | ||
|
|
b579758c46 | ||
|
|
45e6d49d36 | ||
|
|
ddb188e8f0 | ||
|
|
1a86e88373 | ||
|
|
31574a427b | ||
|
|
1bc6a8d956 | ||
|
|
d420e7bc23 | ||
|
|
cd3c2ae77e | ||
|
|
95b0e60617 | ||
|
|
ffbbe5eab3 | ||
|
|
18168ad7fd | ||
|
|
17afbeb87b | ||
|
|
d51b047f63 | ||
|
|
508ec295a4 | ||
|
|
66754fa376 | ||
|
|
4da7f5ecc2 | ||
|
|
29419d9d97 | ||
|
|
3520ef7480 | ||
|
|
d6475eaeed | ||
|
|
a9aaf29d83 | ||
|
|
38fa8925da | ||
|
|
c2b8ea3361 | ||
|
|
584807b039 | ||
|
|
5da3253f4b | ||
|
|
2a97eca00b | ||
|
|
1f3fd60d29 | ||
|
|
8a802ca666 | ||
|
|
a91e6d92f6 | ||
|
|
d9adb078aa | ||
|
|
7a7c33fdb1 | ||
|
|
b6abfec82e | ||
|
|
47774fb644 | ||
|
|
34410e92b7 | ||
|
|
a99f051e19 | ||
|
|
f6c63c62e4 | ||
|
|
76d75850a3 | ||
|
|
68d9f657ad | ||
|
|
24b8a95340 | ||
|
|
d245b9f123 | ||
|
|
a2dee21e8e | ||
|
|
3016cd3636 | ||
|
|
7532e1f957 | ||
|
|
f0db0c1054 | ||
|
|
05c15f4241 | ||
|
|
951ad91cb2 | ||
|
|
53bd57f3c2 | ||
|
|
4b9467cd0c | ||
|
|
0a607b9c93 | ||
|
|
810c046cc6 | ||
|
|
5a8d6931a8 | ||
|
|
0d67f91fac | ||
|
|
f9d41bd36a | ||
|
|
39509265bc | ||
|
|
2f3c21c7c1 | ||
|
|
d77bf23c76 | ||
|
|
f5cd1e5e76 | ||
|
|
a73c67e476 | ||
|
|
a95f9f41fb | ||
|
|
6ffb5af60c | ||
|
|
a5416df615 | ||
|
|
985477f2cf | ||
|
|
a4a8fa3027 | ||
|
|
623408bbfe | ||
|
|
514df6c99a | ||
|
|
54283a2599 | ||
|
|
4493d2efb6 | ||
|
|
83b3187126 | ||
|
|
a2d452684a | ||
|
|
2e42547d32 | ||
|
|
dea8fdd906 | ||
|
|
b41634e19a | ||
|
|
b0f6a94df5 | ||
|
|
1e65165e48 | ||
|
|
73e939ffb5 | ||
|
|
2d9922496c | ||
|
|
6feb2d04df | ||
|
|
90dafa3fa4 | ||
|
|
e77cdb5971 | ||
|
|
90e6c0d7c7 | ||
|
|
240e53afce | ||
|
|
fa8a609bcc | ||
|
|
6aafb521c1 | ||
|
|
81f0aa1168 | ||
|
|
3152642571 | ||
|
|
1e2c410abf | ||
|
|
a008c27fcf | ||
|
|
1edf952dda | ||
|
|
d9ada4536c | ||
|
|
bf89a191f0 | ||
|
|
c2456409bd | ||
|
|
02e23eb386 | ||
|
|
6898284361 | ||
|
|
f3a31be6d0 | ||
|
|
9260401747 | ||
|
|
8a6b009173 | ||
|
|
676ac9d8b8 | ||
|
|
29e263ad7d | ||
|
|
a075f63b59 | ||
|
|
ec60da893f | ||
|
|
d8fbce365a | ||
|
|
f6c5767a83 | ||
|
|
19615f2eae | ||
|
|
c42c6745b9 | ||
|
|
65d0a91fcc | ||
|
|
a22d47c719 | ||
|
|
010516aef2 | ||
|
|
a15389318f | ||
|
|
5d67868ac6 | ||
|
|
e0d8000007 | ||
|
|
b66ff374a2 | ||
|
|
6c981e83db | ||
|
|
2355fcb44e | ||
|
|
f5bbff0b05 | ||
|
|
c45c9da771 | ||
|
|
7a40759567 | ||
|
|
af5b98c635 | ||
|
|
690dc324c9 | ||
|
|
26e78c840c | ||
|
|
9c9ae190ee | ||
|
|
238adbe008 | ||
|
|
f457b995f7 | ||
|
|
b6aec4fa25 | ||
|
|
9fb5b6aa15 | ||
|
|
752fe30332 | ||
|
|
4ff85e2a1e | ||
|
|
13baf26050 | ||
|
|
8751f348c8 | ||
|
|
22bc47da23 | ||
|
|
55df21db51 | ||
|
|
3cd50f0495 | ||
|
|
b3390d40fb | ||
|
|
7eddf429ea | ||
|
|
793813790a | ||
|
|
fe2c4e47bf | ||
|
|
df4318505f | ||
|
|
69911c3db1 | ||
|
|
8ad8f89e50 | ||
|
|
a3d9854704 | ||
|
|
13d3968d9b | ||
|
|
382de7ca90 | ||
|
|
a0d0516b22 | ||
|
|
0fb31726f6 | ||
|
|
e6a73cab8f | ||
|
|
bf6000ef3d | ||
|
|
332118db56 | ||
|
|
6956bf7e53 | ||
|
|
11b829dda1 | ||
|
|
1e16b30380 | ||
|
|
4c1363b104 | ||
|
|
9da0c5bc85 | ||
|
|
4b0c711f77 | ||
|
|
9385f16128 | ||
|
|
36d2e58b11 | ||
|
|
03d6b36fe0 | ||
|
|
3b5b51b4f0 | ||
|
|
e8c5dfca3e | ||
|
|
5a984b54cf | ||
|
|
43879964bd | ||
|
|
5560c9eef7 | ||
|
|
f4097d5a95 | ||
|
|
225330413a | ||
|
|
e67b5a78d0 | ||
|
|
baf365404c | ||
|
|
0de2c758aa | ||
|
|
597bb18543 | ||
|
|
ebdf20adc0 | ||
|
|
7ecdf6db2e | ||
|
|
8a3b5a8def | ||
|
|
98d9fd76b3 | ||
|
|
6992219e34 | ||
|
|
fbe3e7d99c | ||
|
|
9cdc17566a | ||
|
|
cd05462e9f | ||
|
|
83d02c602a | ||
|
|
e85065b1c4 | ||
|
|
d0e705d948 | ||
|
|
2c06464f7b | ||
|
|
84727b1f71 | ||
|
|
aef987dccf | ||
|
|
b2b61bea6a | ||
|
|
30f66be1da | ||
|
|
6caa9ee227 | ||
|
|
9152f77cdd | ||
|
|
4d09eb2cec | ||
|
|
5cc4f6e85a | ||
|
|
6d16c57747 | ||
|
|
27f3a5f5f4 | ||
|
|
45c0e6ef7f | ||
|
|
593dbc9e67 | ||
|
|
daafa8faa3 | ||
|
|
320474b62d | ||
|
|
a3c483edf3 | ||
|
|
036be63f7b | ||
|
|
bbfe324dd6 | ||
|
|
de3292c828 | ||
|
|
67ab2e143c | ||
|
|
9abc112f76 | ||
|
|
b5880df93c | ||
|
|
2352c732de | ||
|
|
77264de3f6 | ||
|
|
42da281854 | ||
|
|
06cc5a29a7 | ||
|
|
98b4e1ea15 | ||
|
|
0bf6e1e839 | ||
|
|
3fe84eadef | ||
|
|
12eed0d384 | ||
|
|
28e8250b69 | ||
|
|
0297260a57 | ||
|
|
d4f7cb984c | ||
|
|
08187a01b1 | ||
|
|
daf3502e15 | ||
|
|
08cab43548 | ||
|
|
5cbe936256 | ||
|
|
729d3d4bc2 | ||
|
|
8af0991590 | ||
|
|
99d968f80a | ||
|
|
705d548435 | ||
|
|
2b6d63fd09 | ||
|
|
c917b8ce06 | ||
|
|
12b10d8b89 | ||
|
|
6a77b8b1f4 | ||
|
|
ba4be2a904 | ||
|
|
ca0523b86c | ||
|
|
5e68282519 | ||
|
|
a0d5525312 | ||
|
|
c48fd0738b | ||
|
|
8224da3460 |
@@ -124,6 +124,28 @@ This document provides essential context for AI models interacting with this pro
|
||||
* **Indentation:** Use spaces (two per indentation level), not tabs
|
||||
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
|
||||
* **Line length:** Wrap lines at no more than 120 characters
|
||||
* **Constructor parameters vs setters:** Component properties that are both **required** and **invariant**
|
||||
(never change after construction) should be constructor parameters rather than set via setter methods.
|
||||
This makes the dependency explicit and prevents use of the object in an incompletely-initialized state.
|
||||
In code generation, when calling `cg.new_Pvariable()` or the relevant helper function to create the component, pass these as arguments.
|
||||
```cpp
|
||||
// Good - required invariant dependency as constructor parameter
|
||||
class SourceTextSensor : public text_sensor::TextSensor, public Component {
|
||||
public:
|
||||
explicit SourceTextSensor(text::Text *source) : source_(source) {}
|
||||
protected:
|
||||
text::Text *source_;
|
||||
};
|
||||
```
|
||||
```cpp
|
||||
// Bad - required invariant dependency as setter
|
||||
class SourceTextSensor : public text_sensor::TextSensor, public Component {
|
||||
public:
|
||||
void set_source(text::Text *source) { this->source_ = source; }
|
||||
protected:
|
||||
text::Text *source_{nullptr};
|
||||
};
|
||||
```
|
||||
|
||||
* **Component Structure:**
|
||||
* **Standard Files:**
|
||||
@@ -217,6 +239,123 @@ This document provides essential context for AI models interacting with this pro
|
||||
var = await switch.new_switch(config)
|
||||
```
|
||||
|
||||
* **Automations (Triggers, Actions, Conditions):**
|
||||
|
||||
Automations have three building blocks: **Triggers** (fire when something happens), **Actions** (do something), and **Conditions** (check if something is true).
|
||||
|
||||
* **Triggers -- Callback method (preferred):**
|
||||
|
||||
Use `build_callback_automation()` for simple triggers. This eliminates the need for a C++ Trigger class by using a lightweight pointer-sized forwarder struct registered directly as a callback. No `CONF_TRIGGER_ID` in the schema.
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
from esphome import automation
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({
|
||||
cv.GenerateID(): cv.declare_id(MyComponent),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
}).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_state_callback", [(bool, "x")], conf
|
||||
)
|
||||
```
|
||||
|
||||
`build_callback_automation` arguments: `parent`, `callback_method` (C++ method name), `args` (template args as `[(type, name)]` tuples), `config`, and optional `forwarder` (defaults to `TriggerForwarder<Ts...>`).
|
||||
|
||||
For boolean filtering (e.g. `on_press`/`on_release`), use built-in forwarders with `args=[]`:
|
||||
```python
|
||||
for conf_key, forwarder in (
|
||||
(CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
|
||||
(CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
|
||||
):
|
||||
for conf in config.get(conf_key, []):
|
||||
await automation.build_callback_automation(
|
||||
var, "add_on_state_callback", [], conf, forwarder=forwarder
|
||||
)
|
||||
```
|
||||
|
||||
**C++ -- no trigger class needed.** The callback registration method must be templatized to accept both `std::function` and lightweight forwarder structs (which avoid heap allocation):
|
||||
```cpp
|
||||
class MyComponent : public Component {
|
||||
public:
|
||||
// Must be a template -- accepts both std::function and pointer-sized forwarder structs
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
protected:
|
||||
// Use CallbackManager when callbacks are always registered (e.g. core components)
|
||||
CallbackManager<void(bool)> state_callback_;
|
||||
// Use LazyCallbackManager when callbacks are often not registered -- saves 8 bytes
|
||||
// (nullptr vs empty std::vector) per instance when no callbacks are added
|
||||
// LazyCallbackManager<void(bool)> state_callback_;
|
||||
};
|
||||
```
|
||||
|
||||
* **Triggers -- Trigger class method:**
|
||||
|
||||
Use `build_automation()` with a `Trigger<Ts...>` subclass only when the forwarder needs **mutable state beyond a single `Automation*` pointer** (e.g. edge detection tracking previous state, timing logic).
|
||||
|
||||
**Python:**
|
||||
```python
|
||||
TurnOnTrigger = my_ns.class_("TurnOnTrigger", automation.Trigger.template())
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema({
|
||||
cv.Optional(CONF_ON_TURN_ON): automation.validate_automation(
|
||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TurnOnTrigger)}
|
||||
),
|
||||
})
|
||||
|
||||
async def to_code(config):
|
||||
for conf in config.get(CONF_ON_TURN_ON, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
```
|
||||
|
||||
**C++:**
|
||||
```cpp
|
||||
class TurnOnTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit TurnOnTrigger(MyComponent *parent) : last_on_{false} {
|
||||
parent->add_on_state_callback([this](bool state) {
|
||||
if (state && !this->last_on_)
|
||||
this->trigger();
|
||||
this->last_on_ = state;
|
||||
});
|
||||
}
|
||||
protected:
|
||||
bool last_on_;
|
||||
};
|
||||
```
|
||||
|
||||
* **Actions:**
|
||||
```cpp
|
||||
template<typename... Ts> class MyAction : public Action<Ts...> {
|
||||
public:
|
||||
explicit MyAction(MyComponent *parent) : parent_(parent) {}
|
||||
void play(const Ts &...) override { this->parent_->do_something(); }
|
||||
protected:
|
||||
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.
|
||||
|
||||
* **Conditions:**
|
||||
```cpp
|
||||
template<typename... Ts> class MyCondition : public Condition<Ts...> {
|
||||
public:
|
||||
explicit MyCondition(MyComponent *parent) : parent_(parent) {}
|
||||
bool check(const Ts &...) override { return this->parent_->is_active(); }
|
||||
protected:
|
||||
MyComponent *parent_;
|
||||
};
|
||||
```
|
||||
Register with `@automation.register_condition("my_component.is_active", MyCondition, schema)`.
|
||||
|
||||
* **Configuration Validation:**
|
||||
* **Common Validators:** `cv.int_`, `cv.float_`, `cv.string`, `cv.boolean`, `cv.int_range(min=0, max=100)`, `cv.positive_int`, `cv.percentage`.
|
||||
* **Complex Validation:** `cv.All(cv.string, cv.Length(min=1, max=50))`, `cv.Any(cv.int_, cv.string)`.
|
||||
@@ -252,10 +391,39 @@ This document provides essential context for AI models interacting with this pro
|
||||
* **Component Tests:** YAML-based compilation tests are located in `tests/`. The structure is as follows:
|
||||
```
|
||||
tests/
|
||||
├── test_build_components/ # Base test configurations
|
||||
└── components/[component]/ # Component-specific tests
|
||||
├── test_build_components/
|
||||
│ └── common/ # Shared bus packages (uart, i2c, spi, etc.)
|
||||
│ ├── uart/ # UART at default baud rate
|
||||
│ ├── uart_115200/ # UART at 115200 baud
|
||||
│ ├── i2c/ # I2C bus
|
||||
│ └── spi/ # SPI bus
|
||||
└── components/[component]/
|
||||
├── common.yaml # Component-only config (no bus definitions)
|
||||
├── test.esp32-idf.yaml
|
||||
├── test.esp8266-ard.yaml
|
||||
└── test.rp2040-ard.yaml
|
||||
```
|
||||
Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
|
||||
|
||||
* **Test Grouping with Packages:** Components that use shared bus packages can be grouped together in CI to reduce build count. **Never define buses (uart, i2c, spi, modbus) directly in test YAML files** — always use packages from `test_build_components/common/`:
|
||||
```yaml
|
||||
# test.esp32-idf.yaml — use packages for buses
|
||||
packages:
|
||||
uart: !include ../../test_build_components/common/uart_115200/esp32-idf.yaml
|
||||
|
||||
<<: !include common.yaml
|
||||
```
|
||||
```yaml
|
||||
# common.yaml — component config only, NO bus definitions
|
||||
my_component:
|
||||
id: my_instance
|
||||
|
||||
sensor:
|
||||
- platform: my_component
|
||||
name: My Sensor
|
||||
```
|
||||
Components that define buses directly are flagged as "NEEDS MIGRATION" and cannot be grouped, increasing CI build time.
|
||||
|
||||
* **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
|
||||
```bash
|
||||
./script/test_component_grouping.py -e config --all
|
||||
@@ -395,6 +563,30 @@ This document provides essential context for AI models interacting with this pro
|
||||
|
||||
Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`).
|
||||
|
||||
**Callback Managers:**
|
||||
|
||||
ESPHome provides two callback manager types in `esphome/core/helpers.h` for the observer pattern. Both support `std::function`, lambdas, and lightweight forwarder structs via their templatized `add()` method.
|
||||
|
||||
| Type | Idle overhead (32-bit) | When to use |
|
||||
|------|----------------------|-------------|
|
||||
| `CallbackManager<void(Ts...)>` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered |
|
||||
| `LazyCallbackManager<void(Ts...)>` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) |
|
||||
|
||||
`LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first callback is added. Prefer it for entity-level callbacks where most instances have no subscribers.
|
||||
|
||||
**Important:** Registration methods that add to a callback manager **must always be templatized** to accept both `std::function` and pointer-sized forwarder structs (used by `build_callback_automation`). Never use `std::function` in the method signature:
|
||||
```cpp
|
||||
// Bad -- forces heap allocation for forwarder structs
|
||||
void add_on_state_callback(std::function<void(bool)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
// Good -- accepts any callable without forcing std::function wrapping
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
```
|
||||
|
||||
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
|
||||
|
||||
**Bad Pattern (Module-Level Globals):**
|
||||
|
||||
@@ -1 +1 @@
|
||||
9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6
|
||||
c65f1a0804a7765462d570c50891ac719260592df2c9cdfe88233fc346ac59e9
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"--privileged",
|
||||
"-e",
|
||||
"GIT_EDITOR=code --wait"
|
||||
// uncomment and edit the path in order to pass though local USB serial to the conatiner
|
||||
// uncomment and edit the path in order to pass through local USB serial to the container
|
||||
// , "--device=/dev/ttyACM0"
|
||||
],
|
||||
"appPort": 6052,
|
||||
|
||||
4
.github/actions/build-image/action.yaml
vendored
4
.github/actions/build-image/action.yaml
vendored
@@ -47,7 +47,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@@ -73,7 +73,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
1
.github/scripts/auto-label-pr/constants.js
vendored
1
.github/scripts/auto-label-pr/constants.js
vendored
@@ -4,6 +4,7 @@ module.exports = {
|
||||
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
|
||||
TOO_BIG_MARKER: '<!-- too-big-request -->',
|
||||
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
|
||||
ORG_FORK_MARKER: '<!-- maintainer-access-warning -->',
|
||||
|
||||
MANAGED_LABELS: [
|
||||
'new-component',
|
||||
|
||||
28
.github/scripts/auto-label-pr/detectors.js
vendored
28
.github/scripts/auto-label-pr/detectors.js
vendored
@@ -235,19 +235,20 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||
}
|
||||
}
|
||||
|
||||
// Get PR head to fetch files from the PR branch
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
// Get base branch ref to check if deprecation already exists for the component
|
||||
// This prevents flagging a PR that simply adds deprecation
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
|
||||
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
|
||||
for (const component of components) {
|
||||
const initFile = `esphome/components/${component}/__init__.py`;
|
||||
try {
|
||||
// Fetch file content from PR head using GitHub API
|
||||
// Fetch file content from base branch using GitHub API
|
||||
const { data: fileData } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: initFile,
|
||||
ref: `refs/pull/${prNumber}/head`
|
||||
ref: baseRef
|
||||
});
|
||||
|
||||
// Decode base64 content
|
||||
@@ -280,6 +281,24 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||
return { labels, deprecatedInfo };
|
||||
}
|
||||
|
||||
// Strategy: Detect when maintainers cannot modify the PR branch
|
||||
function detectMaintainerAccess(context) {
|
||||
const pr = context.payload.pull_request;
|
||||
|
||||
// Only relevant for cross-repo PRs (forks)
|
||||
if (!pr.head.repo || pr.head.repo.full_name === pr.base.repo.full_name) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (pr.maintainer_can_modify) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const isOrgFork = pr.head.repo.owner.type === 'Organization';
|
||||
console.log(`Maintainer cannot modify PR branch (${isOrgFork ? 'org fork: ' + pr.head.repo.owner.login : 'user disabled'})`);
|
||||
return { isOrgFork, orgName: pr.head.repo.owner.login };
|
||||
}
|
||||
|
||||
// Strategy: Requirements detection
|
||||
async function detectRequirements(allLabels, prFiles, context) {
|
||||
const labels = new Set();
|
||||
@@ -328,5 +347,6 @@ module.exports = {
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectMaintainerAccess,
|
||||
detectRequirements
|
||||
};
|
||||
|
||||
16
.github/scripts/auto-label-pr/index.js
vendored
16
.github/scripts/auto-label-pr/index.js
vendored
@@ -12,9 +12,10 @@ const {
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectMaintainerAccess,
|
||||
detectRequirements
|
||||
} = require('./detectors');
|
||||
const { handleReviews } = require('./reviews');
|
||||
const { handleReviews, handleMaintainerAccessComment } = require('./reviews');
|
||||
const { applyLabels, removeOldLabels } = require('./labels');
|
||||
|
||||
// Fetch API data
|
||||
@@ -114,7 +115,8 @@ module.exports = async ({ github, context }) => {
|
||||
codeOwnerLabels,
|
||||
testLabels,
|
||||
checkboxLabels,
|
||||
deprecatedResult
|
||||
deprecatedResult,
|
||||
maintainerAccess
|
||||
] = await Promise.all([
|
||||
detectMergeBranch(context),
|
||||
detectComponentPlatforms(changedFiles, apiData),
|
||||
@@ -127,7 +129,8 @@ module.exports = async ({ github, context }) => {
|
||||
detectCodeOwner(github, context, changedFiles),
|
||||
detectTests(changedFiles),
|
||||
detectPRTemplateCheckboxes(context),
|
||||
detectDeprecatedComponents(github, context, changedFiles)
|
||||
detectDeprecatedComponents(github, context, changedFiles),
|
||||
detectMaintainerAccess(context)
|
||||
]);
|
||||
|
||||
// Extract deprecated component info
|
||||
@@ -177,8 +180,11 @@ module.exports = async ({ github, context }) => {
|
||||
|
||||
console.log('Computed labels:', finalLabels.join(', '));
|
||||
|
||||
// Handle reviews
|
||||
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||
// Handle reviews and org fork comment
|
||||
await Promise.all([
|
||||
handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD),
|
||||
handleMaintainerAccessComment(github, context, maintainerAccess)
|
||||
]);
|
||||
|
||||
// Apply labels
|
||||
await applyLabels(github, context, finalLabels);
|
||||
|
||||
62
.github/scripts/auto-label-pr/reviews.js
vendored
62
.github/scripts/auto-label-pr/reviews.js
vendored
@@ -2,7 +2,8 @@ const {
|
||||
BOT_COMMENT_MARKER,
|
||||
CODEOWNERS_MARKER,
|
||||
TOO_BIG_MARKER,
|
||||
DEPRECATED_COMPONENT_MARKER
|
||||
DEPRECATED_COMPONENT_MARKER,
|
||||
ORG_FORK_MARKER
|
||||
} = require('./constants');
|
||||
|
||||
// Generate review messages
|
||||
@@ -136,6 +137,63 @@ async function handleReviews(github, context, finalLabels, originalLabelCount, d
|
||||
}
|
||||
}
|
||||
|
||||
// Handle maintainer access warning comment
|
||||
async function handleMaintainerAccessComment(github, context, maintainerAccess) {
|
||||
if (!maintainerAccess) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
// Check if we already posted the warning (iterate pages to exit early)
|
||||
let existingComment;
|
||||
for await (const { data: comments } of github.paginate.iterator(
|
||||
github.rest.issues.listComments,
|
||||
{ owner, repo, issue_number: pr_number }
|
||||
)) {
|
||||
existingComment = comments.find(comment =>
|
||||
comment.user.type === 'Bot' &&
|
||||
comment.body && comment.body.includes(ORG_FORK_MARKER)
|
||||
);
|
||||
if (existingComment) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (existingComment) {
|
||||
console.log('Maintainer access warning comment already exists, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
let body;
|
||||
if (maintainerAccess.isOrgFork) {
|
||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Organization Fork Detected\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`It looks like this PR was submitted from a fork owned by the **${maintainerAccess.orgName}** organization. ` +
|
||||
`GitHub does not allow maintainers to push changes to pull request branches when the fork is owned by an organization. ` +
|
||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
||||
`To allow maintainer collaboration, please re-submit this PR from a personal fork instead.\n\n` +
|
||||
`See: [Setting up the local repository](https://developers.esphome.io/contributing/development-environment/?h=org#set-up-the-local-repository) for more details.`;
|
||||
} else {
|
||||
body = `${ORG_FORK_MARKER}\n### ⚠️ Maintainer Access Disabled\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`It looks like this PR does not have the "Allow edits from maintainers" option enabled. ` +
|
||||
`This means we won't be able to make small adjustments or fixups to your PR directly.\n\n` +
|
||||
`Please enable this option in the PR sidebar to allow maintainer collaboration.`;
|
||||
}
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
body
|
||||
});
|
||||
console.log('Created maintainer access warning comment');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleReviews
|
||||
handleReviews,
|
||||
handleMaintainerAccessComment
|
||||
};
|
||||
|
||||
6
.github/workflows/auto-label-pr.yml
vendored
6
.github/workflows/auto-label-pr.yml
vendored
@@ -20,20 +20,20 @@ env:
|
||||
jobs:
|
||||
label:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Auto Label PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
||||
6
.github/workflows/ci-api-proto.yml
vendored
6
.github/workflows/ci-api-proto.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
fi
|
||||
- if: failure()
|
||||
name: Review PR
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
esphome/components/api/api_pb2_service.*
|
||||
- if: success()
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
|
||||
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -42,7 +42,7 @@ jobs:
|
||||
|
||||
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Request changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
await github.rest.pulls.createReview({
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
|
||||
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
let reviews = await github.rest.pulls.listReviews({
|
||||
|
||||
46
.github/workflows/ci.yml
vendored
46
.github/workflows/ci.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -154,12 +154,12 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -198,7 +198,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -231,7 +231,7 @@ jobs:
|
||||
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -253,7 +253,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4
|
||||
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
|
||||
with:
|
||||
run: ${{ steps.build.outputs.binary }}
|
||||
mode: simulation
|
||||
@@ -387,14 +387,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -466,14 +466,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -555,14 +555,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -723,7 +723,7 @@ jobs:
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
||||
env:
|
||||
SKIP: pylint,clang-tidy-hash
|
||||
SKIP: pylint,clang-tidy-hash,ci-custom
|
||||
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
||||
if: always()
|
||||
|
||||
@@ -817,7 +817,7 @@ jobs:
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -841,7 +841,7 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -868,7 +868,8 @@ jobs:
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
-t "$platform" \
|
||||
--base-only 2>&1 | \
|
||||
tee /dev/stderr | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
@@ -882,7 +883,7 @@ jobs:
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -903,7 +904,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -929,7 +930,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -954,7 +955,8 @@ jobs:
|
||||
python script/test_build_components.py \
|
||||
-e compile \
|
||||
-c "$component_list" \
|
||||
-t "$platform" 2>&1 | \
|
||||
-t "$platform" \
|
||||
--base-only 2>&1 | \
|
||||
tee /dev/stderr | \
|
||||
python script/ci_memory_impact_extract.py \
|
||||
--output-env \
|
||||
@@ -967,7 +969,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
CODEOWNERS
|
||||
|
||||
- name: Check codeowner approval and update label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
with:
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Request reviews from component codeowners
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
2
.github/workflows/external-component-bot.yml
vendored
2
.github/workflows/external-component-bot.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Add external component comment
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
|
||||
2
.github/workflows/issue-codeowner-notify.yml
vendored
2
.github/workflows/issue-codeowner-notify.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify codeowners for component issues
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const owner = context.repo.owner;
|
||||
|
||||
2
.github/workflows/lock.yml
vendored
2
.github/workflows/lock.yml
vendored
@@ -8,4 +8,4 @@ on:
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@main
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0
|
||||
|
||||
5
.github/workflows/pr-title-check.yml
vendored
5
.github/workflows/pr-title-check.yml
vendored
@@ -3,6 +3,9 @@ name: PR Title Check
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
branches-ignore:
|
||||
- release
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
@@ -15,7 +18,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const {
|
||||
|
||||
24
.github/workflows/release.yml
vendored
24
.github/workflows/release.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
pip3 install build
|
||||
python3 -m build
|
||||
- name: Publish
|
||||
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
|
||||
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
|
||||
with:
|
||||
skip-existing: true
|
||||
|
||||
@@ -102,12 +102,12 @@ jobs:
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -182,13 +182,13 @@ jobs:
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: matrix.registry == 'ghcr'
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -229,7 +229,7 @@ jobs:
|
||||
repositories: home-assistant-addon
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -264,7 +264,7 @@ jobs:
|
||||
repositories: esphome-schema
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -295,7 +295,7 @@ jobs:
|
||||
repositories: version-notifier
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
|
||||
27
.github/workflows/status-check-labels.yml
vendored
27
.github/workflows/status-check-labels.yml
vendored
@@ -2,30 +2,29 @@ name: Status check labels
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled, unlabeled]
|
||||
types: [opened, reopened, labeled, unlabeled, synchronize]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Check ${{ matrix.label }}
|
||||
name: Check blocking labels
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
label:
|
||||
- needs-docs
|
||||
- merge-after-release
|
||||
- chained-pr
|
||||
steps:
|
||||
- name: Check for ${{ matrix.label }} label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
- name: Check for blocking labels
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||
with:
|
||||
script: |
|
||||
const blockingLabels = ['needs-docs', 'merge-after-release', 'chained-pr'];
|
||||
const { data: labels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number
|
||||
});
|
||||
const hasLabel = labels.find(label => label.name === '${{ matrix.label }}');
|
||||
if (hasLabel) {
|
||||
core.setFailed('Pull request cannot be merged, it is labeled as ${{ matrix.label }}');
|
||||
const labelNames = labels.map(l => l.name);
|
||||
const found = blockingLabels.filter(bl => labelNames.includes(bl));
|
||||
if (found.length > 0) {
|
||||
core.setFailed(`Pull request cannot be merged, it has blocking label(s): ${found.join(', ')}`);
|
||||
}
|
||||
|
||||
2
.github/workflows/sync-device-classes.yml
vendored
2
.github/workflows/sync-device-classes.yml
vendored
@@ -41,7 +41,7 @@ jobs:
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@openhomefoundation.org>
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.6
|
||||
rev: v0.15.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -58,6 +58,7 @@ repos:
|
||||
entry: python3 script/run-in-env.py pylint
|
||||
language: system
|
||||
types: [python]
|
||||
files: ^esphome/.+\.py$
|
||||
- id: clang-tidy-hash
|
||||
name: Update clang-tidy hash
|
||||
entry: python script/clang_tidy_hash.py --update-if-changed
|
||||
@@ -65,3 +66,7 @@ repos:
|
||||
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
|
||||
pass_filenames: false
|
||||
additional_dependencies: []
|
||||
- id: ci-custom
|
||||
name: ci-custom
|
||||
entry: python3 script/run-in-env.py script/ci-custom.py
|
||||
language: system
|
||||
|
||||
@@ -92,6 +92,7 @@ esphome/components/bmp3xx_i2c/* @latonita
|
||||
esphome/components/bmp3xx_spi/* @latonita
|
||||
esphome/components/bmp581_base/* @danielkent-net @kahrendt
|
||||
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
|
||||
esphome/components/bmp581_spi/* @danielkent-net @kahrendt
|
||||
esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
@@ -141,12 +142,13 @@ esphome/components/dlms_meter/* @SimonFischer04
|
||||
esphome/components/dps310/* @kbx81
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/ds2484/* @mrk-its
|
||||
esphome/components/dsmr/* @glmnet @PolarGoose @zuidwijk
|
||||
esphome/components/dsmr/* @glmnet @PolarGoose
|
||||
esphome/components/duty_time/* @dudanov
|
||||
esphome/components/ee895/* @Stock-M
|
||||
esphome/components/ektf2232/touchscreen/* @jesserockz
|
||||
esphome/components/emc2101/* @ellull
|
||||
esphome/components/emmeti/* @E440QF
|
||||
esphome/components/emontx/* @FredM67 @glynhudson @TrystanLea
|
||||
esphome/components/ens160/* @latonita
|
||||
esphome/components/ens160_base/* @latonita @vincentscode
|
||||
esphome/components/ens160_i2c/* @latonita
|
||||
@@ -216,6 +218,7 @@ esphome/components/hbridge/light/* @DotNetDann
|
||||
esphome/components/hbridge/switch/* @dwmw2
|
||||
esphome/components/hc8/* @omartijn
|
||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/hdc2080/* @G-Pereira @jesserockz
|
||||
esphome/components/hdc302x/* @joshuasing
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
@@ -329,6 +332,7 @@ esphome/components/mipi_dsi/* @clydebarrow
|
||||
esphome/components/mipi_rgb/* @clydebarrow
|
||||
esphome/components/mipi_spi/* @clydebarrow
|
||||
esphome/components/mitsubishi/* @RubyBailey
|
||||
esphome/components/mitsubishi_cn105/* @crnjan
|
||||
esphome/components/mixer/speaker/* @kahrendt
|
||||
esphome/components/mlx90393/* @functionpointer
|
||||
esphome/components/mlx90614/* @jesserockz
|
||||
@@ -459,6 +463,7 @@ esphome/components/sonoff_d1/* @anatoly-savchenkov
|
||||
esphome/components/sound_level/* @kahrendt
|
||||
esphome/components/spa06_base/* @danielkent-net
|
||||
esphome/components/spa06_i2c/* @danielkent-net
|
||||
esphome/components/spa06_spi/* @danielkent-net
|
||||
esphome/components/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
|
||||
esphome/components/speaker_source/* @kahrendt
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.4.0-dev
|
||||
PROJECT_NUMBER = 2026.5.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -750,8 +750,15 @@ def upload_using_esptool(
|
||||
platformio_api.FlashImage(
|
||||
path=idedata.firmware_bin_path, offset=firmware_offset
|
||||
),
|
||||
*idedata.extra_flash_images,
|
||||
]
|
||||
for image in idedata.extra_flash_images:
|
||||
if not image.path.is_file():
|
||||
_LOGGER.warning(
|
||||
"Skipping missing flash image declared by platform: %s",
|
||||
image.path,
|
||||
)
|
||||
continue
|
||||
flash_images.append(image)
|
||||
|
||||
mcu = "esp8266"
|
||||
if CORE.is_esp32:
|
||||
@@ -1046,7 +1053,11 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
|
||||
):
|
||||
from esphome.components.api.client import run_logs
|
||||
|
||||
return run_logs(config, network_devices)
|
||||
return run_logs(
|
||||
config,
|
||||
network_devices,
|
||||
subscribe_states=not getattr(args, "no_states", False),
|
||||
)
|
||||
|
||||
if port_type in (PortType.NETWORK, PortType.MQTT) and has_mqtt_logging():
|
||||
from esphome import mqtt
|
||||
@@ -1079,7 +1090,7 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# add the console decoration so the front-end can hide the secrets
|
||||
if not args.show_secrets:
|
||||
output = re.sub(
|
||||
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output
|
||||
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[8m\2\\033[28m", output
|
||||
)
|
||||
if not CORE.quiet:
|
||||
safe_print(output)
|
||||
@@ -1238,6 +1249,38 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
return 0
|
||||
|
||||
|
||||
def command_bundle(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome.bundle import BUNDLE_EXTENSION, ConfigBundleCreator
|
||||
|
||||
creator = ConfigBundleCreator(config)
|
||||
|
||||
if args.list_only:
|
||||
files = creator.discover_files()
|
||||
for bf in sorted(files, key=lambda f: f.path):
|
||||
safe_print(f" {bf.path}")
|
||||
_LOGGER.info("Found %d files", len(files))
|
||||
return 0
|
||||
|
||||
result = creator.create_bundle()
|
||||
|
||||
if args.output:
|
||||
output_path = Path(args.output)
|
||||
else:
|
||||
stem = CORE.config_path.stem
|
||||
output_path = CORE.config_dir / f"{stem}{BUNDLE_EXTENSION}"
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
output_path.write_bytes(result.data)
|
||||
|
||||
_LOGGER.info(
|
||||
"Bundle created: %s (%d files, %.1f KB)",
|
||||
output_path,
|
||||
len(result.files),
|
||||
len(result.data) / 1024,
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
def command_dashboard(args: ArgsProtocol) -> int | None:
|
||||
from esphome.dashboard import dashboard
|
||||
|
||||
@@ -1513,6 +1556,7 @@ POST_CONFIG_ACTIONS = {
|
||||
"rename": command_rename,
|
||||
"discover": command_discover,
|
||||
"analyze-memory": command_analyze_memory,
|
||||
"bundle": command_bundle,
|
||||
}
|
||||
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
@@ -1664,6 +1708,11 @@ def parse_args(argv):
|
||||
help="Reset the device before starting serial logs.",
|
||||
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
|
||||
)
|
||||
parser_logs.add_argument(
|
||||
"--no-states",
|
||||
action="store_true",
|
||||
help="Do not show entity state changes in log output.",
|
||||
)
|
||||
|
||||
parser_discover = subparsers.add_parser(
|
||||
"discover",
|
||||
@@ -1809,6 +1858,24 @@ def parse_args(argv):
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
|
||||
parser_bundle = subparsers.add_parser(
|
||||
"bundle",
|
||||
help="Create a self-contained config bundle for remote compilation.",
|
||||
)
|
||||
parser_bundle.add_argument(
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
parser_bundle.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
help="Output path for the bundle archive.",
|
||||
)
|
||||
parser_bundle.add_argument(
|
||||
"--list-only",
|
||||
help="List discovered files without creating the archive.",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
# Keep backward compatibility with the old command line format of
|
||||
# esphome <config> <command>.
|
||||
#
|
||||
@@ -1887,6 +1954,16 @@ def run_esphome(argv):
|
||||
_LOGGER.warning("Skipping secrets file %s", conf_path)
|
||||
return 0
|
||||
|
||||
# Bundle support: if the configuration is a .esphomebundle, extract it
|
||||
# and rewrite conf_path to the extracted YAML config.
|
||||
from esphome.bundle import is_bundle_path, prepare_bundle_for_compile
|
||||
|
||||
if is_bundle_path(conf_path):
|
||||
_LOGGER.info("Extracting config bundle %s...", conf_path)
|
||||
conf_path = prepare_bundle_for_compile(conf_path)
|
||||
# Update the argument so downstream code sees the extracted path
|
||||
args.configuration[0] = str(conf_path)
|
||||
|
||||
CORE.config_path = conf_path
|
||||
CORE.dashboard = args.dashboard
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ _COMPONENT_PREFIX_LIB = "[lib]"
|
||||
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
|
||||
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
|
||||
|
||||
# Placement new storage suffix (generated by codegen Pvariable)
|
||||
_PSTORAGE_SUFFIX = "__pstorage"
|
||||
|
||||
|
||||
# C++ namespace prefixes
|
||||
_NAMESPACE_ESPHOME = "esphome::"
|
||||
_NAMESPACE_STD = "std::"
|
||||
@@ -332,6 +336,13 @@ class MemoryAnalyzer:
|
||||
# Demangle C++ names if needed
|
||||
demangled = self._demangle_symbol(symbol_name)
|
||||
|
||||
# Check for placement new storage symbols (generated by codegen)
|
||||
# Format: {component}__{id}__pstorage
|
||||
if demangled.endswith(_PSTORAGE_SUFFIX) and (
|
||||
component := self._match_pstorage_component(demangled)
|
||||
):
|
||||
return component
|
||||
|
||||
# Check for special component classes first (before namespace pattern)
|
||||
# This handles cases like esphome::ESPHomeOTAComponent which should map to ota
|
||||
if _NAMESPACE_ESPHOME in demangled:
|
||||
@@ -399,6 +410,24 @@ class MemoryAnalyzer:
|
||||
# Track uncategorized symbols for analysis
|
||||
return "other"
|
||||
|
||||
def _match_pstorage_component(self, symbol_name: str) -> str | None:
|
||||
"""Match a __pstorage symbol to its ESPHome component.
|
||||
|
||||
Symbol format: {component}__{id}__pstorage
|
||||
The component namespace is embedded by codegen before the double underscore.
|
||||
"""
|
||||
prefix = symbol_name[: -len(_PSTORAGE_SUFFIX)]
|
||||
# Extract component namespace before the first double underscore
|
||||
dunder_pos = prefix.find("__")
|
||||
if dunder_pos == -1:
|
||||
return None
|
||||
component_name = prefix[:dunder_pos]
|
||||
if component_name in get_esphome_components():
|
||||
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
|
||||
if component_name in self.external_components:
|
||||
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
|
||||
return None
|
||||
|
||||
def _batch_demangle_symbols(self, symbols: list[str]) -> None:
|
||||
"""Batch demangle C++ symbol names for efficiency."""
|
||||
if not symbols:
|
||||
|
||||
@@ -15,6 +15,7 @@ from . import (
|
||||
_COMPONENT_PREFIX_ESPHOME,
|
||||
_COMPONENT_PREFIX_EXTERNAL,
|
||||
_COMPONENT_PREFIX_LIB,
|
||||
_PSTORAGE_SUFFIX,
|
||||
RAM_SECTIONS,
|
||||
MemoryAnalyzer,
|
||||
)
|
||||
@@ -23,6 +24,17 @@ if TYPE_CHECKING:
|
||||
from . import ComponentMemory
|
||||
|
||||
|
||||
def _format_pstorage_name(name: str) -> str:
|
||||
"""Format a __pstorage symbol as 'storage for {id}'."""
|
||||
if not name.endswith(_PSTORAGE_SUFFIX):
|
||||
return name
|
||||
prefix = name[: -len(_PSTORAGE_SUFFIX)]
|
||||
# Strip component namespace prefix: {component}__{id} -> {id}
|
||||
dunder_pos = prefix.find("__")
|
||||
var_id = prefix[dunder_pos + 2 :] if dunder_pos != -1 else prefix
|
||||
return f"storage for {var_id}"
|
||||
|
||||
|
||||
class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
"""Memory analyzer with CLI-specific report generation."""
|
||||
|
||||
@@ -148,11 +160,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
If section is one of the RAM sections (.data or .bss), a label like
|
||||
" [data]" or " [bss]" is appended. For non-RAM sections or when
|
||||
section is None, no section label is added.
|
||||
|
||||
Placement new storage symbols are formatted as "storage for {id}".
|
||||
"""
|
||||
display_name = _format_pstorage_name(demangled)
|
||||
section_label = ""
|
||||
if section in RAM_SECTIONS:
|
||||
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
|
||||
return f"{demangled} ({size:,} B){section_label}"
|
||||
return f"{display_name} ({size:,} B){section_label}"
|
||||
|
||||
def _add_top_symbols(self, lines: list[str]) -> None:
|
||||
"""Add a section showing the top largest symbols in the binary."""
|
||||
@@ -175,11 +190,13 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
for i, (_, demangled, size, section, component) in enumerate(top_symbols):
|
||||
# Format section label
|
||||
section_label = f"[{section[1:]}]" if section else ""
|
||||
# Truncate demangled name if too long
|
||||
# Format storage symbols readably
|
||||
display_name = _format_pstorage_name(demangled)
|
||||
# Truncate if too long
|
||||
demangled_display = (
|
||||
f"{demangled[:truncate_limit]}..."
|
||||
if len(demangled) > self.COL_TOP_SYMBOL_NAME
|
||||
else demangled
|
||||
f"{display_name[:truncate_limit]}..."
|
||||
if len(display_name) > self.COL_TOP_SYMBOL_NAME
|
||||
else display_name
|
||||
)
|
||||
lines.append(
|
||||
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
|
||||
@@ -573,15 +590,16 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(f"Total size: {comp_mem.flash_total:,} B")
|
||||
lines.append("")
|
||||
|
||||
# Show all symbols above threshold for better visibility
|
||||
# Show symbols above threshold, always include storage symbols
|
||||
large_symbols = [
|
||||
(sym, dem, size, sec)
|
||||
for sym, dem, size, sec in sorted_symbols
|
||||
if size > self.SYMBOL_SIZE_THRESHOLD
|
||||
or dem.endswith(_PSTORAGE_SUFFIX)
|
||||
]
|
||||
|
||||
lines.append(
|
||||
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):"
|
||||
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B & storage ({len(large_symbols)} symbols):"
|
||||
)
|
||||
for i, (symbol, demangled, size, section) in enumerate(large_symbols):
|
||||
lines.append(
|
||||
@@ -604,7 +622,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
# Sort by size descending
|
||||
sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True)
|
||||
large_ram_syms = [
|
||||
s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD
|
||||
s
|
||||
for s in sorted_ram_syms
|
||||
if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD
|
||||
or s[1].endswith(_PSTORAGE_SUFFIX)
|
||||
]
|
||||
|
||||
lines.append(f"{name} ({mem.ram_total:,} B total RAM):")
|
||||
@@ -622,13 +643,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
for symbol, demangled, size, section in large_ram_syms[:10]:
|
||||
# Format section label consistently by stripping leading dot
|
||||
section_label = section.lstrip(".") if section else ""
|
||||
display_name = _format_pstorage_name(demangled)
|
||||
# Add ellipsis if name is truncated
|
||||
demangled_display = (
|
||||
f"{demangled[:70]}..." if len(demangled) > 70 else demangled
|
||||
)
|
||||
lines.append(
|
||||
f" {size:>6,} B [{section_label}] {demangled_display}"
|
||||
display_name = (
|
||||
f"{display_name[:70]}..."
|
||||
if len(display_name) > 70
|
||||
else display_name
|
||||
)
|
||||
lines.append(f" {size:>6,} B [{section_label}] {display_name}")
|
||||
if len(large_ram_syms) > 10:
|
||||
lines.append(f" ... and {len(large_ram_syms) - 10} more")
|
||||
lines.append("")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
@@ -137,6 +138,9 @@ UpdateComponentAction = cg.esphome_ns.class_("UpdateComponentAction", Action)
|
||||
SuspendComponentAction = cg.esphome_ns.class_("SuspendComponentAction", Action)
|
||||
ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
|
||||
Automation = cg.esphome_ns.class_("Automation")
|
||||
TriggerForwarder = cg.esphome_ns.class_("TriggerForwarder")
|
||||
TriggerOnTrueForwarder = cg.esphome_ns.class_("TriggerOnTrueForwarder")
|
||||
TriggerOnFalseForwarder = cg.esphome_ns.class_("TriggerOnFalseForwarder")
|
||||
|
||||
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
|
||||
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
|
||||
@@ -195,11 +199,10 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
|
||||
return cv.Schema([schema])(value)
|
||||
except cv.Invalid as err2:
|
||||
if "extra keys not allowed" in str(err2) and len(err2.path) == 2:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise err
|
||||
raise err from None
|
||||
if "Unable to find action" in str(err):
|
||||
raise err2
|
||||
raise cv.MultipleInvalid([err, err2])
|
||||
raise err2 from None
|
||||
raise cv.MultipleInvalid([err, err2]) from None
|
||||
elif isinstance(value, dict):
|
||||
if CONF_THEN in value:
|
||||
return [schema(value)]
|
||||
@@ -247,7 +250,9 @@ async def and_condition_to_code(
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("or", OrCondition, validate_condition_list)
|
||||
@@ -258,7 +263,9 @@ async def or_condition_to_code(
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("all", AndCondition, validate_condition_list)
|
||||
@@ -269,7 +276,9 @@ async def all_condition_to_code(
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("any", OrCondition, validate_condition_list)
|
||||
@@ -280,7 +289,9 @@ async def any_condition_to_code(
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("not", NotCondition, validate_potentially_and_condition)
|
||||
@@ -302,7 +313,9 @@ async def xor_condition_to_code(
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
conditions = await build_condition_list(config, template_arg, args)
|
||||
return cg.new_Pvariable(condition_id, template_arg, conditions)
|
||||
return cg.new_Pvariable(
|
||||
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
|
||||
)
|
||||
|
||||
|
||||
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
|
||||
@@ -413,13 +426,16 @@ async def if_action_to_code(
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
has_else = CONF_ELSE in config
|
||||
# Prepend HasElse bool to template arguments: IfAction<HasElse, Ts...>
|
||||
if_template_arg = cg.TemplateArguments(has_else, *template_arg)
|
||||
cond_conf = next(el for el in config if el in (CONF_ANY, CONF_ALL, CONF_CONDITION))
|
||||
condition = await build_condition(config[cond_conf], template_arg, args)
|
||||
var = cg.new_Pvariable(action_id, template_arg, condition)
|
||||
var = cg.new_Pvariable(action_id, if_template_arg, condition)
|
||||
if CONF_THEN in config:
|
||||
actions = await build_action_list(config[CONF_THEN], template_arg, args)
|
||||
cg.add(var.add_then(actions))
|
||||
if CONF_ELSE in config:
|
||||
if has_else:
|
||||
actions = await build_action_list(config[CONF_ELSE], template_arg, args)
|
||||
cg.add(var.add_else(actions))
|
||||
return var
|
||||
@@ -658,3 +674,76 @@ async def build_automation(
|
||||
actions = await build_action_list(config[CONF_THEN], templ, args)
|
||||
cg.add(obj.add_actions(actions))
|
||||
return obj
|
||||
|
||||
|
||||
async def build_callback_automation(
|
||||
parent: MockObj,
|
||||
callback_method: str,
|
||||
args: TemplateArgsType,
|
||||
config: ConfigType,
|
||||
forwarder: MockObj | MockObjClass | None = None,
|
||||
) -> None:
|
||||
"""Build an Automation and register it as a callback on the parent.
|
||||
|
||||
Eliminates the need for a Trigger wrapper object by registering the
|
||||
automation's trigger() directly as a callback on the parent component.
|
||||
|
||||
Uses template forwarder structs so the compiler deduplicates the operator()
|
||||
body across all call sites with the same signature. The forwarder must be
|
||||
pointer-sized (single Automation* field) to fit inline in Callback::ctx_
|
||||
and avoid heap allocation.
|
||||
|
||||
:param parent: The component object (e.g., button, sensor).
|
||||
:param callback_method: Name of the callback method (e.g., "add_on_press_callback").
|
||||
:param args: Automation template args as list of (type, name) tuples.
|
||||
:param config: The automation config dict.
|
||||
:param forwarder: Optional forwarder type to use instead of the default
|
||||
TriggerForwarder<Ts...>. Pass any struct type whose aggregate init takes
|
||||
a single Automation pointer (e.g., TriggerOnTrueForwarder).
|
||||
"""
|
||||
arg_types = [arg[0] for arg in args]
|
||||
templ = cg.TemplateArguments(*arg_types)
|
||||
obj = cg.new_Pvariable(config[CONF_AUTOMATION_ID], templ)
|
||||
actions = await build_action_list(config[CONF_THEN], templ, args)
|
||||
cg.add(obj.add_actions(actions))
|
||||
# Use template forwarder structs for deduplication. The compiler generates
|
||||
# one operator() per forwarder type; different automation pointers are just
|
||||
# data in the struct.
|
||||
if forwarder is None:
|
||||
forwarder = TriggerForwarder.template(templ)
|
||||
# RawExpression for aggregate init — both forwarder and obj are codegen
|
||||
# MockObjs (not user input), and there's no Expression type for positional
|
||||
# aggregate initialization (StructInitializer uses named fields).
|
||||
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class CallbackAutomation:
|
||||
"""A single callback automation entry for build_callback_automations."""
|
||||
|
||||
conf_key: str
|
||||
callback_method: str
|
||||
args: TemplateArgsType = field(default_factory=list)
|
||||
forwarder: MockObj | MockObjClass | None = None
|
||||
|
||||
|
||||
async def build_callback_automations(
|
||||
parent: MockObj,
|
||||
config: ConfigType,
|
||||
entries: tuple[CallbackAutomation, ...],
|
||||
) -> None:
|
||||
"""Build multiple callback automations from a tuple of entries.
|
||||
|
||||
:param parent: The component object (e.g., button, sensor).
|
||||
:param config: The full component config dict.
|
||||
:param entries: Tuple of CallbackAutomation entries to process.
|
||||
"""
|
||||
for entry in entries:
|
||||
for conf in config.get(entry.conf_key, []):
|
||||
await build_callback_automation(
|
||||
parent,
|
||||
entry.callback_method,
|
||||
entry.args,
|
||||
conf,
|
||||
forwarder=entry.forwarder,
|
||||
)
|
||||
|
||||
@@ -53,6 +53,13 @@ def get_project_cmakelists() -> str:
|
||||
variant = get_esp32_variant()
|
||||
idf_target = variant.lower().replace("-", "")
|
||||
|
||||
# Extract compile definitions from build flags (-DXXX -> XXX)
|
||||
compile_defs = [flag for flag in CORE.build_flags if flag.startswith("-D")]
|
||||
extra_compile_options = "\n".join(
|
||||
f'idf_build_set_property(COMPILE_OPTIONS "{compile_def}" APPEND)'
|
||||
for compile_def in compile_defs
|
||||
)
|
||||
|
||||
return f"""\
|
||||
# Auto-generated by ESPHome
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
@@ -61,6 +68,9 @@ set(IDF_TARGET {idf_target})
|
||||
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
||||
|
||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
|
||||
{extra_compile_options}
|
||||
|
||||
project({CORE.name})
|
||||
"""
|
||||
|
||||
@@ -70,10 +80,6 @@ def get_component_cmakelists(minimal: bool = False) -> str:
|
||||
idf_requires = [] if minimal else (get_available_components() or [])
|
||||
requires_str = " ".join(idf_requires)
|
||||
|
||||
# Extract compile definitions from build flags (-DXXX -> XXX)
|
||||
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
|
||||
compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else ""
|
||||
|
||||
# Extract compile options (-W flags, excluding linker flags)
|
||||
compile_opts = [
|
||||
flag
|
||||
@@ -104,11 +110,6 @@ idf_component_register(
|
||||
# Apply C++ standard
|
||||
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
|
||||
|
||||
# ESPHome compile definitions
|
||||
target_compile_definitions(${{COMPONENT_LIB}} PUBLIC
|
||||
{compile_defs_str}
|
||||
)
|
||||
|
||||
# ESPHome compile options
|
||||
target_compile_options(${{COMPONENT_LIB}} PUBLIC
|
||||
{compile_opts_str}
|
||||
|
||||
765
esphome/bundle.py
Normal file
765
esphome/bundle.py
Normal file
@@ -0,0 +1,765 @@
|
||||
"""Config bundle creator and extractor for ESPHome.
|
||||
|
||||
A bundle is a self-contained .tar.gz archive containing a YAML config
|
||||
and every local file it depends on. Bundles can be created from a config
|
||||
and compiled directly: ``esphome compile my_device.esphomebundle.tar.gz``
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import tarfile
|
||||
from typing import Any
|
||||
|
||||
from esphome import const, yaml_util
|
||||
from esphome.const import (
|
||||
CONF_ESPHOME,
|
||||
CONF_EXTERNAL_COMPONENTS,
|
||||
CONF_INCLUDES,
|
||||
CONF_INCLUDES_C,
|
||||
CONF_PATH,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
BUNDLE_EXTENSION = ".esphomebundle.tar.gz"
|
||||
MANIFEST_FILENAME = "manifest.json"
|
||||
CURRENT_MANIFEST_VERSION = 1
|
||||
MAX_DECOMPRESSED_SIZE = 500 * 1024 * 1024 # 500 MB
|
||||
MAX_MANIFEST_SIZE = 1024 * 1024 # 1 MB
|
||||
|
||||
# Directories preserved across bundle extractions (build caches)
|
||||
_PRESERVE_DIRS = (".esphome", ".pioenvs", ".pio")
|
||||
_BUNDLE_STAGING_DIR = ".bundle_staging"
|
||||
|
||||
|
||||
class ManifestKey(StrEnum):
|
||||
"""Keys used in bundle manifest.json."""
|
||||
|
||||
MANIFEST_VERSION = "manifest_version"
|
||||
ESPHOME_VERSION = "esphome_version"
|
||||
CONFIG_FILENAME = "config_filename"
|
||||
FILES = "files"
|
||||
HAS_SECRETS = "has_secrets"
|
||||
|
||||
|
||||
# String prefixes that are never local file paths
|
||||
_NON_PATH_PREFIXES = ("http://", "https://", "ftp://", "mdi:", "<")
|
||||
|
||||
# File extensions recognized when resolving relative path strings.
|
||||
# A relative string with one of these extensions is resolved against the
|
||||
# config directory and included if the file exists.
|
||||
_KNOWN_FILE_EXTENSIONS = frozenset(
|
||||
{
|
||||
# Fonts
|
||||
".ttf",
|
||||
".otf",
|
||||
".woff",
|
||||
".woff2",
|
||||
".pcf",
|
||||
".bdf",
|
||||
# Images
|
||||
".png",
|
||||
".jpg",
|
||||
".jpeg",
|
||||
".bmp",
|
||||
".gif",
|
||||
".svg",
|
||||
".ico",
|
||||
".webp",
|
||||
# Certificates
|
||||
".pem",
|
||||
".crt",
|
||||
".key",
|
||||
".der",
|
||||
".p12",
|
||||
".pfx",
|
||||
# C/C++ includes
|
||||
".h",
|
||||
".hpp",
|
||||
".c",
|
||||
".cpp",
|
||||
".ino",
|
||||
# Web assets
|
||||
".css",
|
||||
".js",
|
||||
".html",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Matches !secret references in YAML text. This is intentionally a simple
|
||||
# regex scan rather than a YAML parse — it may match inside comments or
|
||||
# multi-line strings, which is the conservative direction (include more
|
||||
# secrets rather than fewer).
|
||||
_SECRET_RE = re.compile(r"!secret\s+(\S+)")
|
||||
|
||||
|
||||
def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]:
|
||||
"""Scan YAML files for ``!secret <key>`` references."""
|
||||
keys: set[str] = set()
|
||||
for fpath in yaml_files:
|
||||
try:
|
||||
text = fpath.read_text(encoding="utf-8")
|
||||
except (OSError, UnicodeDecodeError):
|
||||
continue
|
||||
for match in _SECRET_RE.finditer(text):
|
||||
keys.add(match.group(1))
|
||||
return keys
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleFile:
|
||||
"""A file to include in the bundle."""
|
||||
|
||||
path: str # Relative path inside the archive
|
||||
source: Path # Absolute path on disk
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleResult:
|
||||
"""Result of creating a bundle."""
|
||||
|
||||
data: bytes
|
||||
manifest: dict[str, Any]
|
||||
files: list[BundleFile]
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleManifest:
|
||||
"""Parsed and validated bundle manifest."""
|
||||
|
||||
manifest_version: int
|
||||
esphome_version: str
|
||||
config_filename: str
|
||||
files: list[str]
|
||||
has_secrets: bool
|
||||
|
||||
|
||||
class ConfigBundleCreator:
|
||||
"""Creates a self-contained bundle from an ESPHome config."""
|
||||
|
||||
def __init__(self, config: dict[str, Any]) -> None:
|
||||
self._config = config
|
||||
self._config_dir = Path(CORE.config_dir).resolve()
|
||||
self._config_path = Path(CORE.config_path).resolve()
|
||||
self._files: list[BundleFile] = []
|
||||
self._seen_paths: set[Path] = set()
|
||||
self._secrets_paths: set[Path] = set()
|
||||
|
||||
def discover_files(self) -> list[BundleFile]:
|
||||
"""Discover all files needed for the bundle."""
|
||||
self._files = []
|
||||
self._seen_paths = set()
|
||||
self._secrets_paths = set()
|
||||
|
||||
# The main config file
|
||||
self._add_file(self._config_path)
|
||||
|
||||
# Phase 1: YAML includes (tracked during config loading)
|
||||
self._discover_yaml_includes()
|
||||
|
||||
# Phase 2: Component-referenced files from validated config
|
||||
self._discover_component_files()
|
||||
|
||||
return list(self._files)
|
||||
|
||||
def create_bundle(self) -> BundleResult:
|
||||
"""Create the bundle archive."""
|
||||
files = self.discover_files()
|
||||
|
||||
# Determine which secret keys are actually referenced by the
|
||||
# bundled YAML files so we only ship those, not the entire
|
||||
# secrets.yaml which may contain secrets for other devices.
|
||||
yaml_sources = [
|
||||
bf.source for bf in files if bf.source.suffix in (".yaml", ".yml")
|
||||
]
|
||||
used_secret_keys = _find_used_secret_keys(yaml_sources)
|
||||
filtered_secrets = self._build_filtered_secrets(used_secret_keys)
|
||||
|
||||
has_secrets = bool(filtered_secrets)
|
||||
if has_secrets:
|
||||
_LOGGER.warning(
|
||||
"Bundle contains secrets (e.g. Wi-Fi passwords). "
|
||||
"Do not share it with untrusted parties."
|
||||
)
|
||||
|
||||
manifest = self._build_manifest(files, has_secrets=has_secrets)
|
||||
|
||||
buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
|
||||
# Add manifest first
|
||||
manifest_data = json.dumps(manifest, indent=2).encode("utf-8")
|
||||
_add_bytes_to_tar(tar, MANIFEST_FILENAME, manifest_data)
|
||||
|
||||
# Add filtered secrets files
|
||||
for rel_path, data in sorted(filtered_secrets.items()):
|
||||
_add_bytes_to_tar(tar, rel_path, data)
|
||||
|
||||
# Add files in sorted order for determinism, skipping secrets
|
||||
# files which were already added above with filtered content
|
||||
for bf in sorted(files, key=lambda f: f.path):
|
||||
if bf.source in self._secrets_paths:
|
||||
continue
|
||||
self._add_to_tar(tar, bf)
|
||||
|
||||
return BundleResult(data=buf.getvalue(), manifest=manifest, files=files)
|
||||
|
||||
def _add_file(self, abs_path: Path) -> bool:
|
||||
"""Add a file to the bundle. Returns False if already added."""
|
||||
abs_path = abs_path.resolve()
|
||||
if abs_path in self._seen_paths:
|
||||
return False
|
||||
if not abs_path.is_file():
|
||||
_LOGGER.warning("Bundle: skipping missing file %s", abs_path)
|
||||
return False
|
||||
|
||||
rel_path = self._relative_to_config_dir(abs_path)
|
||||
if rel_path is None:
|
||||
_LOGGER.warning(
|
||||
"Bundle: skipping file outside config directory: %s", abs_path
|
||||
)
|
||||
return False
|
||||
|
||||
self._seen_paths.add(abs_path)
|
||||
self._files.append(BundleFile(path=rel_path, source=abs_path))
|
||||
return True
|
||||
|
||||
def _add_directory(self, abs_path: Path) -> None:
|
||||
"""Recursively add all files in a directory."""
|
||||
abs_path = abs_path.resolve()
|
||||
if not abs_path.is_dir():
|
||||
_LOGGER.warning("Bundle: skipping missing directory %s", abs_path)
|
||||
return
|
||||
for child in sorted(abs_path.rglob("*")):
|
||||
if child.is_file() and "__pycache__" not in child.parts:
|
||||
self._add_file(child)
|
||||
|
||||
def _relative_to_config_dir(self, abs_path: Path) -> str | None:
|
||||
"""Get a path relative to the config directory. Returns None if outside.
|
||||
|
||||
Always uses forward slashes for consistency in tar archives.
|
||||
"""
|
||||
try:
|
||||
return abs_path.relative_to(self._config_dir).as_posix()
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _discover_yaml_includes(self) -> None:
|
||||
"""Discover YAML files loaded during config parsing.
|
||||
|
||||
Deliberately uses a fresh re-parse and force-loads every deferred
|
||||
``IncludeFile`` to include *all* potentially-reachable includes,
|
||||
even branches not selected by the local substitutions. Bundles are
|
||||
meant to be compiled on another system where command-line
|
||||
substitution overrides may choose a different branch — e.g.
|
||||
``!include network/${eth_model}/config.yaml`` must ship every
|
||||
candidate so the remote build can pick any one.
|
||||
|
||||
Entries with unresolved substitution variables in the filename
|
||||
path are skipped with a warning (they cannot be resolved without
|
||||
the substitution pass).
|
||||
|
||||
Secrets files are tracked separately so we can filter them to
|
||||
only include the keys this config actually references.
|
||||
"""
|
||||
# Must be a fresh parse: IncludeFile.load() caches its result in
|
||||
# _content, and we discover files by listening for loader calls. On
|
||||
# an already-parsed tree the cache is populated, .load() returns
|
||||
# without calling the loader, the listener never fires, and the
|
||||
# referenced files would be silently dropped from the bundle.
|
||||
with yaml_util.track_yaml_loads() as loaded_files:
|
||||
try:
|
||||
data = yaml_util.load_yaml(self._config_path)
|
||||
except EsphomeError:
|
||||
_LOGGER.debug(
|
||||
"Bundle: re-loading YAML for include discovery failed, "
|
||||
"proceeding with partial file list"
|
||||
)
|
||||
else:
|
||||
_force_load_include_files(data)
|
||||
|
||||
for fpath in loaded_files:
|
||||
if fpath == self._config_path.resolve():
|
||||
continue # Already added as config
|
||||
if fpath.name in const.SECRETS_FILES:
|
||||
self._secrets_paths.add(fpath)
|
||||
self._add_file(fpath)
|
||||
|
||||
def _discover_component_files(self) -> None:
|
||||
"""Walk the validated config for file references.
|
||||
|
||||
Uses a generic recursive walk to find file paths instead of
|
||||
hardcoding per-component knowledge about config dict formats.
|
||||
After validation, components typically resolve paths to absolute
|
||||
using CORE.relative_config_path() or cv.file_(). Relative paths
|
||||
with known file extensions are also resolved and checked.
|
||||
|
||||
Core ESPHome concepts that use relative paths or directories
|
||||
are handled explicitly.
|
||||
"""
|
||||
config = self._config
|
||||
|
||||
# Generic walk: find all file paths in the validated config
|
||||
self._walk_config_for_files(config)
|
||||
|
||||
# --- Core ESPHome concepts needing explicit handling ---
|
||||
|
||||
# esphome.includes / includes_c - can be relative paths and directories
|
||||
esphome_conf = config.get(CONF_ESPHOME, {})
|
||||
for include_path in esphome_conf.get(CONF_INCLUDES, []):
|
||||
resolved = _resolve_include_path(include_path)
|
||||
if resolved is None:
|
||||
continue
|
||||
if resolved.is_dir():
|
||||
self._add_directory(resolved)
|
||||
else:
|
||||
self._add_file(resolved)
|
||||
for include_path in esphome_conf.get(CONF_INCLUDES_C, []):
|
||||
resolved = _resolve_include_path(include_path)
|
||||
if resolved is not None:
|
||||
self._add_file(resolved)
|
||||
|
||||
# external_components with source: local - directories
|
||||
for ext_conf in config.get(CONF_EXTERNAL_COMPONENTS, []):
|
||||
source = ext_conf.get(CONF_SOURCE, {})
|
||||
if not isinstance(source, dict):
|
||||
continue
|
||||
if source.get(CONF_TYPE) != "local":
|
||||
continue
|
||||
path = source.get(CONF_PATH)
|
||||
if not path:
|
||||
continue
|
||||
p = Path(path)
|
||||
if not p.is_absolute():
|
||||
p = CORE.relative_config_path(p)
|
||||
self._add_directory(p)
|
||||
|
||||
def _walk_config_for_files(self, obj: Any) -> None:
|
||||
"""Recursively walk the config dict looking for file path references."""
|
||||
if isinstance(obj, dict):
|
||||
for value in obj.values():
|
||||
self._walk_config_for_files(value)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
for item in obj:
|
||||
self._walk_config_for_files(item)
|
||||
elif isinstance(obj, Path):
|
||||
if obj.is_absolute() and obj.is_file():
|
||||
self._add_file(obj)
|
||||
elif isinstance(obj, str):
|
||||
self._check_string_path(obj)
|
||||
|
||||
def _check_string_path(self, value: str) -> None:
|
||||
"""Check if a string value is a local file reference."""
|
||||
# Fast exits for strings that cannot be file paths
|
||||
if len(value) < 2 or "\n" in value:
|
||||
return
|
||||
if value.startswith(_NON_PATH_PREFIXES):
|
||||
return
|
||||
# File paths must contain a path separator or a dot (for extension)
|
||||
if "/" not in value and "\\" not in value and "." not in value:
|
||||
return
|
||||
|
||||
p = Path(value)
|
||||
|
||||
# Absolute path - check if it points to an existing file
|
||||
if p.is_absolute():
|
||||
if p.is_file():
|
||||
self._add_file(p)
|
||||
return
|
||||
|
||||
# Relative path with a known file extension - likely a component
|
||||
# validator that forgot to resolve to absolute via cv.file_() or
|
||||
# CORE.relative_config_path(). Warn and try to resolve.
|
||||
if p.suffix.lower() in _KNOWN_FILE_EXTENSIONS:
|
||||
_LOGGER.warning(
|
||||
"Bundle: non-absolute path in validated config: %s "
|
||||
"(component validator should return absolute paths)",
|
||||
value,
|
||||
)
|
||||
resolved = CORE.relative_config_path(p)
|
||||
if resolved.is_file():
|
||||
self._add_file(resolved)
|
||||
|
||||
def _build_filtered_secrets(self, used_keys: set[str]) -> dict[str, bytes]:
|
||||
"""Build filtered secrets files containing only the referenced keys.
|
||||
|
||||
Returns a dict mapping relative archive path to YAML bytes.
|
||||
"""
|
||||
if not used_keys or not self._secrets_paths:
|
||||
return {}
|
||||
|
||||
result: dict[str, bytes] = {}
|
||||
for secrets_path in self._secrets_paths:
|
||||
rel_path = self._relative_to_config_dir(secrets_path)
|
||||
if rel_path is None:
|
||||
continue
|
||||
try:
|
||||
all_secrets = yaml_util.load_yaml(secrets_path, clear_secrets=False)
|
||||
except EsphomeError:
|
||||
_LOGGER.warning("Bundle: failed to load secrets file %s", secrets_path)
|
||||
continue
|
||||
if not isinstance(all_secrets, dict):
|
||||
continue
|
||||
filtered = {k: v for k, v in all_secrets.items() if k in used_keys}
|
||||
if filtered:
|
||||
data = yaml_util.dump(filtered, show_secrets=True).encode("utf-8")
|
||||
result[rel_path] = data
|
||||
return result
|
||||
|
||||
def _build_manifest(
|
||||
self, files: list[BundleFile], *, has_secrets: bool
|
||||
) -> dict[str, Any]:
|
||||
"""Build the manifest.json content."""
|
||||
return {
|
||||
ManifestKey.MANIFEST_VERSION: CURRENT_MANIFEST_VERSION,
|
||||
ManifestKey.ESPHOME_VERSION: const.__version__,
|
||||
ManifestKey.CONFIG_FILENAME: self._config_path.name,
|
||||
ManifestKey.FILES: [f.path for f in files],
|
||||
ManifestKey.HAS_SECRETS: has_secrets,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
|
||||
"""Add a BundleFile to the tar archive with deterministic metadata."""
|
||||
with open(bf.source, "rb") as f:
|
||||
_add_bytes_to_tar(tar, bf.path, f.read())
|
||||
|
||||
|
||||
def extract_bundle(
|
||||
bundle_path: Path,
|
||||
target_dir: Path | None = None,
|
||||
) -> Path:
|
||||
"""Extract a bundle archive and return the path to the config YAML.
|
||||
|
||||
Sanity checks reject path traversal, symlinks, absolute paths, and
|
||||
oversized archives to prevent accidental file overwrites or extraction
|
||||
outside the target directory. These are **not** a security boundary —
|
||||
bundles are assumed to come from the user's own machine or a trusted
|
||||
build pipeline.
|
||||
|
||||
Args:
|
||||
bundle_path: Path to the .tar.gz bundle file.
|
||||
target_dir: Directory to extract into. If None, extracts next to
|
||||
the bundle file in a directory named after it.
|
||||
|
||||
Returns:
|
||||
Absolute path to the extracted config YAML file.
|
||||
|
||||
Raises:
|
||||
EsphomeError: If the bundle is invalid or extraction fails.
|
||||
"""
|
||||
|
||||
bundle_path = bundle_path.resolve()
|
||||
if not bundle_path.is_file():
|
||||
raise EsphomeError(f"Bundle file not found: {bundle_path}")
|
||||
|
||||
if target_dir is None:
|
||||
target_dir = _default_target_dir(bundle_path)
|
||||
|
||||
target_dir = target_dir.resolve()
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Read and validate the archive
|
||||
try:
|
||||
with tarfile.open(bundle_path, "r:gz") as tar:
|
||||
manifest = _read_manifest_from_tar(tar)
|
||||
_validate_tar_members(tar, target_dir)
|
||||
tar.extractall(path=target_dir, filter="data")
|
||||
except tarfile.TarError as err:
|
||||
raise EsphomeError(f"Failed to extract bundle: {err}") from err
|
||||
|
||||
config_filename = manifest[ManifestKey.CONFIG_FILENAME]
|
||||
config_path = target_dir / config_filename
|
||||
if not config_path.is_file():
|
||||
raise EsphomeError(
|
||||
f"Bundle manifest references config '{config_filename}' "
|
||||
f"but it was not found in the archive"
|
||||
)
|
||||
|
||||
return config_path
|
||||
|
||||
|
||||
def read_bundle_manifest(bundle_path: Path) -> BundleManifest:
|
||||
"""Read and validate the manifest from a bundle without full extraction.
|
||||
|
||||
Args:
|
||||
bundle_path: Path to the .tar.gz bundle file.
|
||||
|
||||
Returns:
|
||||
Parsed BundleManifest.
|
||||
|
||||
Raises:
|
||||
EsphomeError: If the manifest is missing, invalid, or version unsupported.
|
||||
"""
|
||||
|
||||
try:
|
||||
with tarfile.open(bundle_path, "r:gz") as tar:
|
||||
manifest = _read_manifest_from_tar(tar)
|
||||
except tarfile.TarError as err:
|
||||
raise EsphomeError(f"Failed to read bundle: {err}") from err
|
||||
|
||||
return BundleManifest(
|
||||
manifest_version=manifest[ManifestKey.MANIFEST_VERSION],
|
||||
esphome_version=manifest.get(ManifestKey.ESPHOME_VERSION, "unknown"),
|
||||
config_filename=manifest[ManifestKey.CONFIG_FILENAME],
|
||||
files=manifest.get(ManifestKey.FILES, []),
|
||||
has_secrets=manifest.get(ManifestKey.HAS_SECRETS, False),
|
||||
)
|
||||
|
||||
|
||||
def _read_manifest_from_tar(tar: tarfile.TarFile) -> dict[str, Any]:
|
||||
"""Read and validate manifest.json from an open tar archive."""
|
||||
|
||||
try:
|
||||
member = tar.getmember(MANIFEST_FILENAME)
|
||||
except KeyError:
|
||||
raise EsphomeError("Invalid bundle: missing manifest.json") from None
|
||||
|
||||
f = tar.extractfile(member)
|
||||
if f is None:
|
||||
raise EsphomeError("Invalid bundle: manifest.json is not a regular file")
|
||||
|
||||
if member.size > MAX_MANIFEST_SIZE:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: manifest.json too large "
|
||||
f"({member.size} bytes, max {MAX_MANIFEST_SIZE})"
|
||||
)
|
||||
|
||||
try:
|
||||
manifest = json.loads(f.read())
|
||||
except (json.JSONDecodeError, UnicodeDecodeError) as err:
|
||||
raise EsphomeError(f"Invalid bundle: malformed manifest.json: {err}") from err
|
||||
|
||||
# Version check
|
||||
version = manifest.get(ManifestKey.MANIFEST_VERSION)
|
||||
if version is None:
|
||||
raise EsphomeError("Invalid bundle: manifest.json missing 'manifest_version'")
|
||||
if not isinstance(version, int) or version < 1:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: manifest_version must be a positive integer, got {version!r}"
|
||||
)
|
||||
if version > CURRENT_MANIFEST_VERSION:
|
||||
raise EsphomeError(
|
||||
f"Bundle manifest version {version} is newer than this ESPHome "
|
||||
f"version supports (max {CURRENT_MANIFEST_VERSION}). "
|
||||
f"Please upgrade ESPHome to compile this bundle."
|
||||
)
|
||||
|
||||
# Required fields
|
||||
if ManifestKey.CONFIG_FILENAME not in manifest:
|
||||
raise EsphomeError("Invalid bundle: manifest.json missing 'config_filename'")
|
||||
|
||||
return manifest
|
||||
|
||||
|
||||
def _validate_tar_members(tar: tarfile.TarFile, target_dir: Path) -> None:
|
||||
"""Sanity-check tar members to prevent mistakes and accidental overwrites.
|
||||
|
||||
This is not a security boundary — bundles are created locally or come
|
||||
from a trusted build pipeline. The checks catch malformed archives
|
||||
and common mistakes (stray absolute paths, ``..`` components) that
|
||||
could silently overwrite unrelated files.
|
||||
"""
|
||||
|
||||
total_size = 0
|
||||
for member in tar.getmembers():
|
||||
# Reject absolute paths (Unix and Windows)
|
||||
if member.name.startswith(("/", "\\")):
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: absolute path in archive: {member.name}"
|
||||
)
|
||||
|
||||
# Reject path traversal (split on both / and \ for cross-platform)
|
||||
parts = re.split(r"[/\\]", member.name)
|
||||
if ".." in parts:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: path traversal in archive: {member.name}"
|
||||
)
|
||||
|
||||
# Reject symlinks
|
||||
if member.issym() or member.islnk():
|
||||
raise EsphomeError(f"Invalid bundle: symlink in archive: {member.name}")
|
||||
|
||||
# Ensure extraction stays within target_dir
|
||||
target_path = (target_dir / member.name).resolve()
|
||||
if not target_path.is_relative_to(target_dir):
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: file would extract outside target: {member.name}"
|
||||
)
|
||||
|
||||
# Track total decompressed size
|
||||
total_size += member.size
|
||||
if total_size > MAX_DECOMPRESSED_SIZE:
|
||||
raise EsphomeError(
|
||||
f"Invalid bundle: decompressed size exceeds "
|
||||
f"{MAX_DECOMPRESSED_SIZE // (1024 * 1024)}MB limit"
|
||||
)
|
||||
|
||||
|
||||
def is_bundle_path(path: Path) -> bool:
|
||||
"""Check if a path looks like a bundle file."""
|
||||
return path.name.lower().endswith(BUNDLE_EXTENSION)
|
||||
|
||||
|
||||
def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
|
||||
"""Add in-memory bytes to a tar archive with deterministic metadata."""
|
||||
info = tarfile.TarInfo(name=name)
|
||||
info.size = len(data)
|
||||
info.mtime = 0
|
||||
info.uid = 0
|
||||
info.gid = 0
|
||||
info.mode = 0o644
|
||||
tar.addfile(info, io.BytesIO(data))
|
||||
|
||||
|
||||
def _force_load_include_files(obj: Any, _seen: set[int] | None = None) -> None:
|
||||
"""Recursively resolve any ``IncludeFile`` instances in a YAML tree.
|
||||
|
||||
Nested ``!include`` returns a deferred ``IncludeFile`` that is only
|
||||
resolved during the substitution pass. During bundle discovery we need
|
||||
the referenced files to actually load so the ``track_yaml_loads``
|
||||
listener fires for them.
|
||||
|
||||
``IncludeFile`` instances with unresolved substitution variables in the
|
||||
filename cannot be loaded — we skip and warn about those.
|
||||
"""
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
|
||||
if isinstance(obj, yaml_util.IncludeFile):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
if obj.has_unresolved_expressions():
|
||||
_LOGGER.warning(
|
||||
"Bundle: cannot resolve !include %s (referenced from %s) "
|
||||
"with substitutions in path",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
)
|
||||
return
|
||||
try:
|
||||
loaded = obj.load()
|
||||
except EsphomeError as err:
|
||||
_LOGGER.warning(
|
||||
"Bundle: failed to load !include %s (referenced from %s): %s",
|
||||
obj.file,
|
||||
obj.parent_file,
|
||||
err,
|
||||
)
|
||||
return
|
||||
_force_load_include_files(loaded, _seen)
|
||||
elif isinstance(obj, dict):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for value in obj.values():
|
||||
_force_load_include_files(value, _seen)
|
||||
elif isinstance(obj, (list, tuple)):
|
||||
if id(obj) in _seen:
|
||||
return
|
||||
_seen.add(id(obj))
|
||||
for item in obj:
|
||||
_force_load_include_files(item, _seen)
|
||||
|
||||
|
||||
def _resolve_include_path(include_path: Any) -> Path | None:
|
||||
"""Resolve an include path to absolute, skipping system includes."""
|
||||
if isinstance(include_path, str) and include_path.startswith("<"):
|
||||
return None # System include, not a local file
|
||||
p = Path(include_path)
|
||||
if not p.is_absolute():
|
||||
p = CORE.relative_config_path(p)
|
||||
return p
|
||||
|
||||
|
||||
def _default_target_dir(bundle_path: Path) -> Path:
|
||||
"""Compute the default extraction directory for a bundle."""
|
||||
name = bundle_path.name
|
||||
if name.lower().endswith(BUNDLE_EXTENSION):
|
||||
name = name[: -len(BUNDLE_EXTENSION)]
|
||||
return bundle_path.parent / name
|
||||
|
||||
|
||||
def _restore_preserved_dirs(preserved: dict[str, Path], target_dir: Path) -> None:
|
||||
"""Move preserved build cache directories back into target_dir.
|
||||
|
||||
If the bundle contained entries under a preserved directory name,
|
||||
the extracted copy is removed so the original cache always wins.
|
||||
"""
|
||||
for dirname, src in preserved.items():
|
||||
dst = target_dir / dirname
|
||||
if dst.exists():
|
||||
shutil.rmtree(dst)
|
||||
shutil.move(str(src), str(dst))
|
||||
|
||||
|
||||
def prepare_bundle_for_compile(
|
||||
bundle_path: Path,
|
||||
target_dir: Path | None = None,
|
||||
) -> Path:
|
||||
"""Extract a bundle for compilation, preserving build caches.
|
||||
|
||||
Unlike extract_bundle(), this preserves .esphome/ and .pioenvs/
|
||||
directories in the target if they already exist (for incremental builds).
|
||||
|
||||
Args:
|
||||
bundle_path: Path to the .tar.gz bundle file.
|
||||
target_dir: Directory to extract into. Must be specified for
|
||||
build server use.
|
||||
|
||||
Returns:
|
||||
Absolute path to the extracted config YAML file.
|
||||
"""
|
||||
|
||||
bundle_path = bundle_path.resolve()
|
||||
if not bundle_path.is_file():
|
||||
raise EsphomeError(f"Bundle file not found: {bundle_path}")
|
||||
|
||||
if target_dir is None:
|
||||
target_dir = _default_target_dir(bundle_path)
|
||||
|
||||
target_dir = target_dir.resolve()
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
preserved: dict[str, Path] = {}
|
||||
|
||||
# Temporarily move preserved dirs out of the way
|
||||
staging = target_dir / _BUNDLE_STAGING_DIR
|
||||
for dirname in _PRESERVE_DIRS:
|
||||
src = target_dir / dirname
|
||||
if src.is_dir():
|
||||
dst = staging / dirname
|
||||
dst.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.move(str(src), str(dst))
|
||||
preserved[dirname] = dst
|
||||
|
||||
try:
|
||||
# Clean non-preserved content and extract fresh
|
||||
for item in target_dir.iterdir():
|
||||
if item.name == _BUNDLE_STAGING_DIR:
|
||||
continue
|
||||
if item.is_dir():
|
||||
shutil.rmtree(item)
|
||||
else:
|
||||
item.unlink()
|
||||
|
||||
config_path = extract_bundle(bundle_path, target_dir)
|
||||
finally:
|
||||
# Restore preserved dirs (idempotent) and clean staging
|
||||
_restore_preserved_dirs(preserved, target_dir)
|
||||
if staging.is_dir():
|
||||
shutil.rmtree(staging)
|
||||
|
||||
return config_path
|
||||
@@ -79,6 +79,7 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
float_,
|
||||
global_ns,
|
||||
gpio_Flags,
|
||||
int8,
|
||||
int16,
|
||||
int32,
|
||||
int64,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "adc_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
@@ -346,7 +347,8 @@ float ADCSensor::sample_autorange_() {
|
||||
ESP_LOGVV(TAG, "Autorange summary:");
|
||||
ESP_LOGVV(TAG, " Raw readings: 12db=%d, 6db=%d, 2.5db=%d, 0db=%d", raw12, raw6, raw2, raw0);
|
||||
ESP_LOGVV(TAG, " Voltages: 12db=%.6f, 6db=%.6f, 2.5db=%.6f, 0db=%.6f", mv12, mv6, mv2, mv0);
|
||||
ESP_LOGVV(TAG, " Coefficients: c12=%u, c6=%u, c2=%u, c0=%u, sum=%u", c12, c6, c2, c0, csum);
|
||||
ESP_LOGVV(TAG, " Coefficients: c12=%" PRIu32 ", c6=%" PRIu32 ", c2=%" PRIu32 ", c0=%" PRIu32 ", sum=%" PRIu32, c12,
|
||||
c6, c2, c0, csum);
|
||||
|
||||
if (csum == 0) {
|
||||
ESP_LOGE(TAG, "Invalid weight sum in autorange calculation");
|
||||
@@ -354,8 +356,10 @@ float ADCSensor::sample_autorange_() {
|
||||
}
|
||||
|
||||
const float final_result = (mv12 * c12 + mv6 * c6 + mv2 * c2 + mv0 * c0) / csum;
|
||||
ESP_LOGV(TAG, "Autorange final: (%.6f*%u + %.6f*%u + %.6f*%u + %.6f*%u)/%u = %.6fV", mv12, c12, mv6, c6, mv2, c2, mv0,
|
||||
c0, csum, final_result);
|
||||
ESP_LOGV(TAG,
|
||||
"Autorange final: (%.6f*%" PRIu32 " + %.6f*%" PRIu32 " + %.6f*%" PRIu32 " + %.6f*%" PRIu32 ")/%" PRIu32
|
||||
" = %.6fV",
|
||||
mv12, c12, mv6, c6, mv2, c2, mv0, c0, csum, final_result);
|
||||
|
||||
return final_result;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,11 @@ import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
|
||||
from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
require_adc_oneshot_iram,
|
||||
)
|
||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
@@ -24,6 +28,7 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import (
|
||||
ATTENUATION_MODES,
|
||||
@@ -65,6 +70,13 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def _require_adc_iram(config: ConfigType) -> ConfigType:
|
||||
"""Register ADC oneshot IRAM requirement during config validation."""
|
||||
if CORE.is_esp32:
|
||||
require_adc_oneshot_iram()
|
||||
return config
|
||||
|
||||
|
||||
ADCSensor = adc_ns.class_(
|
||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
||||
)
|
||||
@@ -95,6 +107,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s")),
|
||||
validate_config,
|
||||
_require_adc_iram,
|
||||
)
|
||||
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
@@ -8,6 +8,9 @@ namespace ade7953_base {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
constexpr uint16_t CONFIG_DEFAULT = 0x8004u;
|
||||
constexpr uint16_t CONFIG_LOCK_BIT = 0x8000u;
|
||||
|
||||
static const float ADE_POWER_FACTOR = 154.0f;
|
||||
static const float ADE_WATTSEC_POWER_FACTOR = ADE_POWER_FACTOR * ADE_POWER_FACTOR / 3600;
|
||||
|
||||
@@ -18,7 +21,12 @@ void ADE7953::setup() {
|
||||
|
||||
// The chip might take up to 100ms to initialise
|
||||
this->set_timeout(100, [this]() {
|
||||
// this->ade_write_8(0x0010, 0x04);
|
||||
// Lock communication interface (SPI or I2C)
|
||||
uint16_t config_v = CONFIG_DEFAULT;
|
||||
this->ade_read_16(CONFIG_16, &config_v);
|
||||
config_v &= static_cast<uint16_t>(~CONFIG_LOCK_BIT); // Clear the lock bit
|
||||
this->ade_write_16(CONFIG_16, config_v);
|
||||
// Configure optimum settings according to datasheet
|
||||
this->ade_write_8(0x00FE, 0xAD);
|
||||
this->ade_write_16(0x0120, 0x0030);
|
||||
// Set gains
|
||||
|
||||
@@ -9,31 +9,35 @@
|
||||
namespace esphome {
|
||||
namespace ade7953_base {
|
||||
|
||||
static const uint8_t PGA_V_8 =
|
||||
static constexpr uint8_t PGA_V_8 =
|
||||
0x007; // PGA_V, (R/W) Default: 0x00, Unsigned, Voltage channel gain configuration (Bits[2:0])
|
||||
static const uint8_t PGA_IA_8 =
|
||||
static constexpr uint8_t PGA_IA_8 =
|
||||
0x008; // PGA_IA, (R/W) Default: 0x00, Unsigned, Current Channel A gain configuration (Bits[2:0])
|
||||
static const uint8_t PGA_IB_8 =
|
||||
static constexpr uint8_t PGA_IB_8 =
|
||||
0x009; // PGA_IB, (R/W) Default: 0x00, Unsigned, Current Channel B gain configuration (Bits[2:0])
|
||||
|
||||
static const uint32_t AIGAIN_32 =
|
||||
static constexpr uint16_t CONFIG_16 = 0x102; // CONFIG, (R/W) Default: 0x8004, Unsigned, Configuration register
|
||||
|
||||
static constexpr uint16_t AIGAIN_32 =
|
||||
0x380; // AIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVGAIN_32 = 0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static const uint32_t AWGAIN_32 =
|
||||
static constexpr uint16_t AVGAIN_32 =
|
||||
0x381; // AVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static constexpr uint16_t AWGAIN_32 =
|
||||
0x382; // AWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVARGAIN_32 =
|
||||
static constexpr uint16_t AVARGAIN_32 =
|
||||
0x383; // AVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel A)(32 bit)
|
||||
static const uint32_t AVAGAIN_32 =
|
||||
static constexpr uint16_t AVAGAIN_32 =
|
||||
0x384; // AVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel A)(32 bit)
|
||||
|
||||
static const uint32_t BIGAIN_32 =
|
||||
static constexpr uint16_t BIGAIN_32 =
|
||||
0x38C; // BIGAIN, (R/W) Default: 0x400000, Unsigned,Current channel gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVGAIN_32 = 0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static const uint32_t BWGAIN_32 =
|
||||
static constexpr uint16_t BVGAIN_32 =
|
||||
0x38D; // BVGAIN, (R/W) Default: 0x400000, Unsigned,Voltage channel gain(32 bit)
|
||||
static constexpr uint16_t BWGAIN_32 =
|
||||
0x38E; // BWGAIN, (R/W) Default: 0x400000, Unsigned,Active power gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVARGAIN_32 =
|
||||
static constexpr uint16_t BVARGAIN_32 =
|
||||
0x38F; // BVARGAIN, (R/W) Default: 0x400000, Unsigned, Reactive power gain (Current Channel B)(32 bit)
|
||||
static const uint32_t BVAGAIN_32 =
|
||||
static constexpr uint16_t BVAGAIN_32 =
|
||||
0x390; // BVAGAIN, (R/W) Default: 0x400000, Unsigned,Apparent power gain (Current Channel B)(32 bit)
|
||||
|
||||
class ADE7953 : public PollingComponent, public sensor::Sensor {
|
||||
|
||||
@@ -7,6 +7,9 @@ namespace ade7953_spi {
|
||||
|
||||
static const char *const TAG = "ade7953";
|
||||
|
||||
// Datasheet requires at least 1.2µs after clearing CONFIG LOCK_BIT before raising CS
|
||||
constexpr uint8_t CONFIG_LOCK_SETTLE_US = 2;
|
||||
|
||||
void AdE7953Spi::setup() {
|
||||
this->spi_setup();
|
||||
ade7953_base::ADE7953::setup();
|
||||
@@ -32,6 +35,9 @@ bool AdE7953Spi::ade_write_16(uint16_t reg, uint16_t value) {
|
||||
this->write_byte16(reg);
|
||||
this->transfer_byte(0);
|
||||
this->write_byte16(value);
|
||||
if (reg == ade7953_base::CONFIG_16) {
|
||||
delayMicroseconds(CONFIG_LOCK_SETTLE_US);
|
||||
}
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ namespace esphome {
|
||||
namespace ade7953_spi {
|
||||
|
||||
class AdE7953Spi : public ade7953_base::ADE7953,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_LEADING,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH, spi::CLOCK_PHASE_TRAILING,
|
||||
spi::DATA_RATE_1MHZ> {
|
||||
public:
|
||||
void setup() override;
|
||||
|
||||
@@ -12,11 +12,15 @@ CONF_ADS1118_ID = "ads1118_id"
|
||||
ads1118_ns = cg.esphome_ns.namespace("ads1118")
|
||||
ADS1118 = ads1118_ns.class_("ADS1118", cg.Component, spi.SPIDevice)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ADS1118),
|
||||
}
|
||||
).extend(spi.spi_device_schema(cs_pin_required=True))
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ADS1118),
|
||||
}
|
||||
)
|
||||
.extend(spi.spi_device_schema(cs_pin_required=True))
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -35,7 +35,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(AGS10Component),
|
||||
cv.Optional(CONF_TVOC): sensor.sensor_schema(
|
||||
cv.Required(CONF_TVOC): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PARTS_PER_BILLION,
|
||||
icon=ICON_RADIATOR,
|
||||
accuracy_decimals=0,
|
||||
@@ -97,7 +97,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
|
||||
async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
address = await cg.templatable(config[CONF_ADDRESS], args, int)
|
||||
address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
|
||||
cg.add(var.set_new_address(address))
|
||||
return var
|
||||
|
||||
@@ -112,7 +112,9 @@ AGS10_SET_ZERO_POINT_ACTION_MODE = {
|
||||
AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(AGS10Component),
|
||||
cv.Required(CONF_MODE): cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True),
|
||||
cv.Required(CONF_MODE): cv.templatable(
|
||||
cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True)
|
||||
),
|
||||
cv.Optional(CONF_VALUE, default=0xFFFF): cv.templatable(cv.uint16_t),
|
||||
},
|
||||
)
|
||||
@@ -127,8 +129,10 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
|
||||
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, enumerate)
|
||||
mode = await cg.templatable(
|
||||
config.get(CONF_MODE), args, AGS10SetZeroPointActionMode
|
||||
)
|
||||
cg.add(var.set_mode(mode))
|
||||
value = await cg.templatable(config[CONF_VALUE], args, int)
|
||||
value = await cg.templatable(config[CONF_VALUE], args, cg.uint16)
|
||||
cg.add(var.set_value(value))
|
||||
return var
|
||||
|
||||
@@ -43,7 +43,7 @@ async def aic3204_set_volume_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)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_MODE), args, int)
|
||||
template_ = await cg.templatable(config.get(CONF_MODE), args, cg.uint8)
|
||||
cg.add(var.set_auto_mute_mode(template_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -10,7 +10,6 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MQTT_ID,
|
||||
CONF_ON_STATE,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
@@ -34,39 +33,9 @@ CONF_ON_READY = "on_ready"
|
||||
alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel")
|
||||
AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase)
|
||||
|
||||
StateTrigger = alarm_control_panel_ns.class_(
|
||||
"StateTrigger", automation.Trigger.template()
|
||||
)
|
||||
TriggeredTrigger = alarm_control_panel_ns.class_(
|
||||
"TriggeredTrigger", automation.Trigger.template()
|
||||
)
|
||||
ClearedTrigger = alarm_control_panel_ns.class_(
|
||||
"ClearedTrigger", automation.Trigger.template()
|
||||
)
|
||||
ArmingTrigger = alarm_control_panel_ns.class_(
|
||||
"ArmingTrigger", automation.Trigger.template()
|
||||
)
|
||||
PendingTrigger = alarm_control_panel_ns.class_(
|
||||
"PendingTrigger", automation.Trigger.template()
|
||||
)
|
||||
ArmedHomeTrigger = alarm_control_panel_ns.class_(
|
||||
"ArmedHomeTrigger", automation.Trigger.template()
|
||||
)
|
||||
ArmedNightTrigger = alarm_control_panel_ns.class_(
|
||||
"ArmedNightTrigger", automation.Trigger.template()
|
||||
)
|
||||
ArmedAwayTrigger = alarm_control_panel_ns.class_(
|
||||
"ArmedAwayTrigger", automation.Trigger.template()
|
||||
)
|
||||
DisarmedTrigger = alarm_control_panel_ns.class_(
|
||||
"DisarmedTrigger", automation.Trigger.template()
|
||||
)
|
||||
ChimeTrigger = alarm_control_panel_ns.class_(
|
||||
"ChimeTrigger", automation.Trigger.template()
|
||||
)
|
||||
ReadyTrigger = alarm_control_panel_ns.class_(
|
||||
"ReadyTrigger", automation.Trigger.template()
|
||||
)
|
||||
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)
|
||||
@@ -89,61 +58,17 @@ _ALARM_CONTROL_PANEL_SCHEMA = (
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
|
||||
mqtt.MQTTAlarmControlPanelComponent
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmingTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PENDING): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PendingTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedHomeTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedNightTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ArmedAwayTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_DISARMED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(DisarmedTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_CLEARED): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_CHIME): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_READY): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_ARMING): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_PENDING): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_ARMED_HOME): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_ARMED_NIGHT): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_ARMED_AWAY): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_DISARMED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_CLEARED): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_CHIME): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_READY): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -186,41 +111,66 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_STATE, "add_on_state_callback", forwarder=StateAnyForwarder
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_TRIGGERED,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_TRIGGERED
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ARMING,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(AlarmControlPanelState.ACP_STATE_ARMING),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PENDING,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_PENDING
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ARMED_HOME,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_ARMED_HOME
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ARMED_NIGHT,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_ARMED_NIGHT
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_ARMED_AWAY,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_ARMED_AWAY
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_DISARMED,
|
||||
"add_on_state_callback",
|
||||
forwarder=StateEnterForwarder.template(
|
||||
AlarmControlPanelState.ACP_STATE_DISARMED
|
||||
),
|
||||
),
|
||||
automation.CallbackAutomation(CONF_ON_CLEARED, "add_on_cleared_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_CHIME, "add_on_chime_callback"),
|
||||
automation.CallbackAutomation(CONF_ON_READY, "add_on_ready_callback"),
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("alarm_control_panel")
|
||||
async def setup_alarm_control_panel_core_(var, config):
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_TRIGGERED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_ARMING, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_PENDING, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_ARMED_HOME, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_ARMED_NIGHT, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_ARMED_AWAY, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_DISARMED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_CLEARED, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_CHIME, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
for conf in config.get(CONF_ON_READY, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
|
||||
@@ -31,12 +31,12 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
|
||||
this->last_update_ = millis();
|
||||
if (state != this->current_state_) {
|
||||
auto prev_state = this->current_state_;
|
||||
ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(),
|
||||
ESP_LOGV(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(),
|
||||
LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
|
||||
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
|
||||
this->current_state_ = state;
|
||||
// Single state callback - triggers check get_state() for specific states
|
||||
this->state_callback_.call();
|
||||
// Single state callback - listeners receive the new state as an argument
|
||||
this->state_callback_.call(state);
|
||||
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_alarm_control_panel_update(this);
|
||||
#endif
|
||||
|
||||
@@ -145,8 +145,8 @@ class AlarmControlPanel : public EntityBase {
|
||||
uint32_t last_update_;
|
||||
// the call control function
|
||||
virtual void control(const AlarmControlPanelCall &call) = 0;
|
||||
// state callback - triggers check get_state() for specific state
|
||||
LazyCallbackManager<void()> state_callback_{};
|
||||
// state callback - passes the new state to listeners
|
||||
LazyCallbackManager<void(AlarmControlPanelState)> state_callback_{};
|
||||
// clear callback - fires when leaving TRIGGERED state
|
||||
LazyCallbackManager<void()> cleared_callback_{};
|
||||
// chime callback
|
||||
|
||||
@@ -5,60 +5,27 @@
|
||||
|
||||
namespace esphome::alarm_control_panel {
|
||||
|
||||
/// Trigger on any state change
|
||||
class StateTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_state_callback([this]() { this->trigger(); });
|
||||
/// Callback forwarder that triggers an Automation<> on any state change.
|
||||
/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_.
|
||||
struct StateAnyForwarder {
|
||||
Automation<> *automation;
|
||||
void operator()(AlarmControlPanelState /*state*/) const { this->automation->trigger(); }
|
||||
};
|
||||
|
||||
/// Callback forwarder that triggers an Automation<> only when the alarm enters a specific state.
|
||||
/// Pointer-sized (single Automation* field) to fit inline in Callback::ctx_.
|
||||
template<AlarmControlPanelState State> struct StateEnterForwarder {
|
||||
Automation<> *automation;
|
||||
void operator()(AlarmControlPanelState state) const {
|
||||
if (state == State)
|
||||
this->automation->trigger();
|
||||
}
|
||||
};
|
||||
|
||||
/// Template trigger that fires when entering a specific state
|
||||
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {
|
||||
alarm_control_panel->add_on_state_callback([this]() {
|
||||
if (this->alarm_control_panel_->get_state() == State)
|
||||
this->trigger();
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
};
|
||||
|
||||
// Type aliases for state-specific triggers
|
||||
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>;
|
||||
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>;
|
||||
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>;
|
||||
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>;
|
||||
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>;
|
||||
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
|
||||
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>;
|
||||
|
||||
/// Trigger when leaving TRIGGERED state (alarm cleared)
|
||||
class ClearedTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_cleared_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
/// Trigger on chime event (zone opened while disarmed)
|
||||
class ChimeTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
|
||||
/// Trigger on ready state change
|
||||
class ReadyTrigger : public Trigger<> {
|
||||
public:
|
||||
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
|
||||
alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); });
|
||||
}
|
||||
};
|
||||
static_assert(sizeof(StateAnyForwarder) <= sizeof(void *));
|
||||
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 : public Action<Ts...> {
|
||||
public:
|
||||
|
||||
@@ -9,6 +9,10 @@ from esphome.const import (
|
||||
CONF_POWER,
|
||||
CONF_SPEED,
|
||||
CONF_VOLTAGE,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_AMPERE,
|
||||
UNIT_CUBIC_METER_PER_HOUR,
|
||||
UNIT_METER,
|
||||
@@ -27,26 +31,35 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_FLOW): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HEAD): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_METER,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_POWER): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_WATT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_AMPERE,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_CURRENT,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_SPEED): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
|
||||
accuracy_decimals=2,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -8,6 +8,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_BATTERY,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_BRIGHTNESS_5,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
|
||||
@@ -26,11 +27,13 @@ CONFIG_SCHEMA = (
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
accuracy_decimals=0,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
icon=ICON_BRIGHTNESS_5,
|
||||
accuracy_decimals=0,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -19,8 +19,8 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
|
||||
|
||||
protected:
|
||||
sensor::Sensor *sensor_{nullptr};
|
||||
TemplatableValue<float> upper_threshold_{};
|
||||
TemplatableValue<float> lower_threshold_{};
|
||||
TemplatableFn<float> upper_threshold_{};
|
||||
TemplatableFn<float> lower_threshold_{};
|
||||
bool raw_state_{false}; // Pre-filter state for hysteresis logic
|
||||
};
|
||||
|
||||
|
||||
@@ -40,10 +40,10 @@ async def to_code(config):
|
||||
cg.add(var.set_sensor(sens))
|
||||
|
||||
if isinstance(config[CONF_THRESHOLD], dict):
|
||||
lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], float)
|
||||
upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], float)
|
||||
lower = await cg.templatable(config[CONF_THRESHOLD][CONF_LOWER], [], cg.float_)
|
||||
upper = await cg.templatable(config[CONF_THRESHOLD][CONF_UPPER], [], cg.float_)
|
||||
else:
|
||||
lower = await cg.templatable(config[CONF_THRESHOLD], [], float)
|
||||
lower = await cg.templatable(config[CONF_THRESHOLD], [], cg.float_)
|
||||
upper = lower
|
||||
cg.add(var.set_upper_threshold(upper))
|
||||
cg.add(var.set_lower_threshold(lower))
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/alloc_helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace anova {
|
||||
|
||||
@@ -105,14 +107,14 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) {
|
||||
}
|
||||
case READ_TARGET_TEMPERATURE:
|
||||
case SET_TARGET_TEMPERATURE: {
|
||||
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
|
||||
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
|
||||
if (this->fahrenheit_)
|
||||
this->target_temp_ = ftoc(this->target_temp_);
|
||||
this->has_target_temp_ = true;
|
||||
break;
|
||||
}
|
||||
case READ_CURRENT_TEMPERATURE: {
|
||||
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
|
||||
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
|
||||
if (this->fahrenheit_)
|
||||
this->current_temp_ = ftoc(this->current_temp_);
|
||||
this->has_current_temp_ = true;
|
||||
|
||||
@@ -291,12 +291,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.SplitDefault(
|
||||
CONF_MAX_CONNECTIONS,
|
||||
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
|
||||
esp32=8, # 520KB RAM available
|
||||
esp32=5, # 520KB RAM available
|
||||
rp2040=4, # 264KB RAM but LWIP constraints
|
||||
bk72xx=8, # Moderate RAM
|
||||
rtl87xx=8, # Moderate RAM
|
||||
bk72xx=5, # Moderate RAM
|
||||
rtl87xx=5, # Moderate RAM
|
||||
host=8, # Abundant resources
|
||||
ln882x=8, # Moderate RAM
|
||||
ln882x=5, # Moderate RAM
|
||||
): 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
|
||||
@@ -336,8 +336,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
||||
if CONF_LISTEN_BACKLOG in config:
|
||||
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
|
||||
if CONF_MAX_CONNECTIONS in config:
|
||||
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
|
||||
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
|
||||
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
|
||||
|
||||
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
|
||||
|
||||
@@ -129,11 +129,12 @@ message HelloResponse {
|
||||
|
||||
// A string identifying the server (ESP); like client info this may be empty
|
||||
// and only exists for debugging/logging purposes.
|
||||
// For example "ESPHome v1.10.0 on ESP8266"
|
||||
string server_info = 3;
|
||||
// Currently set to ESPHOME_VERSION string literal.
|
||||
string server_info = 3 [(max_data_length) = 32, (force) = true];
|
||||
|
||||
// The name of the server (App.get_name())
|
||||
string name = 4;
|
||||
// The name of the server (App.get_name() - device hostname)
|
||||
// max_data_length matches ESPHOME_DEVICE_NAME_MAX_LEN (validated by validate_hostname)
|
||||
string name = 4 [(max_data_length) = 31, (force) = true];
|
||||
}
|
||||
|
||||
// DEPRECATED in ESPHome 2026.1.0 - Password authentication is no longer supported.
|
||||
@@ -196,12 +197,14 @@ message DeviceInfoRequest {
|
||||
|
||||
message AreaInfo {
|
||||
uint32 area_id = 1;
|
||||
string name = 2;
|
||||
// max_data_length matches core/config.FRIENDLY_NAME_MAX_LEN via AREA_SCHEMA
|
||||
string name = 2 [(max_data_length) = 120, (force) = true];
|
||||
}
|
||||
|
||||
message DeviceInfo {
|
||||
uint32 device_id = 1;
|
||||
string name = 2;
|
||||
// max_data_length matches core/config.FRIENDLY_NAME_MAX_LEN via DEVICE_SCHEMA
|
||||
string name = 2 [(max_data_length) = 120, (force) = true];
|
||||
uint32 area_id = 3;
|
||||
}
|
||||
|
||||
@@ -216,6 +219,16 @@ message SerialProxyInfo {
|
||||
SerialProxyPortType port_type = 2; // Port type (RS232, RS485)
|
||||
}
|
||||
|
||||
// DeviceInfoResponse max_data_length values:
|
||||
// name = 31 (ESPHOME_DEVICE_NAME_MAX_LEN, validated by validate_hostname)
|
||||
// friendly_name = 120 (core/config.FRIENDLY_NAME_MAX_LEN)
|
||||
// mac_address/bluetooth_mac_address = 17 (MAC_ADDRESS_PRETTY_BUFFER_SIZE - 1, constexpr)
|
||||
// esphome_version = 32 (ESPHOME_VERSION string literal)
|
||||
// compilation_time = 25 (Application::BUILD_TIME_STR_SIZE - 1, constexpr)
|
||||
// manufacturer = 20 (longest hardcoded literal: "Nordic Semiconductor")
|
||||
// model = 127 (core/config.BOARD_MAX_LENGTH, validated in platform schemas)
|
||||
// project_name/project_version = 127 (core/config.PROJECT_MAX_LENGTH)
|
||||
// suggested_area = 120 (core/config.FRIENDLY_NAME_MAX_LEN via AREA_SCHEMA)
|
||||
message DeviceInfoResponse {
|
||||
option (id) = 10;
|
||||
option (source) = SOURCE_SERVER;
|
||||
@@ -224,28 +237,30 @@ message DeviceInfoResponse {
|
||||
// with older ESPHome versions that still send this field.
|
||||
bool uses_password = 1 [deprecated = true];
|
||||
|
||||
// The name of the node, given by "App.set_name()"
|
||||
string name = 2;
|
||||
// The name of the node, given by "App.set_name()" - device hostname
|
||||
string name = 2 [(max_data_length) = 31, (force) = true];
|
||||
|
||||
// The mac address of the device. For example "AC:BC:32:89:0E:A9"
|
||||
string mac_address = 3;
|
||||
string mac_address = 3 [(max_data_length) = 17, (force) = true];
|
||||
|
||||
// A string describing the ESPHome version. For example "1.10.0"
|
||||
string esphome_version = 4;
|
||||
string esphome_version = 4 [(max_data_length) = 32, (force) = true];
|
||||
|
||||
// A string describing the date of compilation, this is generated by the compiler
|
||||
// and therefore may not be in the same format all the time.
|
||||
// If the user isn't using ESPHome, this will also not be set.
|
||||
string compilation_time = 5;
|
||||
string compilation_time = 5 [(max_data_length) = 25, (force) = true];
|
||||
|
||||
// The model of the board. For example NodeMCU
|
||||
string model = 6;
|
||||
// max_data_length matches core/config.BOARD_MAX_LENGTH (validated in platform schemas)
|
||||
string model = 6 [(max_data_length) = 127, (force) = true];
|
||||
|
||||
bool has_deep_sleep = 7 [(field_ifdef) = "USE_DEEP_SLEEP"];
|
||||
|
||||
// The esphome project details if set
|
||||
string project_name = 8 [(field_ifdef) = "ESPHOME_PROJECT_NAME"];
|
||||
string project_version = 9 [(field_ifdef) = "ESPHOME_PROJECT_NAME"];
|
||||
// max_data_length matches core/config.PROJECT_MAX_LENGTH
|
||||
string project_name = 8 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME"];
|
||||
string project_version = 9 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME"];
|
||||
|
||||
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
|
||||
|
||||
@@ -253,18 +268,18 @@ message DeviceInfoResponse {
|
||||
uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
|
||||
string manufacturer = 12;
|
||||
string manufacturer = 12 [(max_data_length) = 20, (force) = true];
|
||||
|
||||
string friendly_name = 13;
|
||||
string friendly_name = 13 [(max_data_length) = 120, (force) = true];
|
||||
|
||||
// Deprecated in API version 1.10
|
||||
uint32 legacy_voice_assistant_version = 14 [deprecated=true, (field_ifdef) = "USE_VOICE_ASSISTANT"];
|
||||
uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
|
||||
|
||||
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
|
||||
string suggested_area = 16 [(max_data_length) = 120, (force) = true, (field_ifdef) = "USE_AREAS"];
|
||||
|
||||
// The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA"
|
||||
string bluetooth_mac_address = 18 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
string bluetooth_mac_address = 18 [(max_data_length) = 17, (force) = true, (field_ifdef) = "USE_BLUETOOTH_PROXY"];
|
||||
|
||||
// Supports receiving and saving api encryption key
|
||||
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
|
||||
@@ -308,6 +323,12 @@ enum EntityCategory {
|
||||
ENTITY_CATEGORY_DIAGNOSTIC = 2;
|
||||
}
|
||||
|
||||
// Entity field max_data_length values match Python validation constants:
|
||||
// name/object_id = 120 (config_validation.NAME_MAX_LENGTH)
|
||||
// icon = 63 (core/config.ICON_MAX_LENGTH)
|
||||
// device_class = 47 (core/config.DEVICE_CLASS_MAX_LENGTH)
|
||||
// unit_of_measurement = 63 (core/config.UNIT_OF_MEASUREMENT_MAX_LENGTH)
|
||||
|
||||
// ==================== BINARY SENSOR ====================
|
||||
message ListEntitiesBinarySensorResponse {
|
||||
option (id) = 12;
|
||||
@@ -315,15 +336,15 @@ message ListEntitiesBinarySensorResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_BINARY_SENSOR";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string device_class = 5;
|
||||
string device_class = 5 [(max_data_length) = 47];
|
||||
bool is_status_binary_sensor = 6;
|
||||
bool disabled_by_default = 7;
|
||||
string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
EntityCategory entity_category = 9;
|
||||
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -334,7 +355,7 @@ message BinarySensorStateResponse {
|
||||
option (ifdef) = "USE_BINARY_SENSOR";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool state = 2;
|
||||
// If the binary sensor does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
@@ -349,17 +370,17 @@ message ListEntitiesCoverResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_COVER";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
bool assumed_state = 5;
|
||||
bool supports_position = 6;
|
||||
bool supports_tilt = 7;
|
||||
string device_class = 8;
|
||||
string device_class = 8 [(max_data_length) = 47];
|
||||
bool disabled_by_default = 9;
|
||||
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
EntityCategory entity_category = 11;
|
||||
bool supports_stop = 12;
|
||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -383,7 +404,7 @@ message CoverStateResponse {
|
||||
option (ifdef) = "USE_COVER";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
// legacy: state has been removed in 1.13
|
||||
// clients/servers must still send/accept it until the next protocol change
|
||||
// Deprecated in API version 1.1
|
||||
@@ -409,7 +430,7 @@ message CoverCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
|
||||
// legacy: command has been removed in 1.13
|
||||
// clients/servers must still send/accept it until the next protocol change
|
||||
@@ -433,9 +454,9 @@ message ListEntitiesFanResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_FAN";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
bool supports_oscillation = 5;
|
||||
@@ -443,7 +464,7 @@ message ListEntitiesFanResponse {
|
||||
bool supports_direction = 7;
|
||||
int32 supported_speed_count = 8;
|
||||
bool disabled_by_default = 9;
|
||||
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
EntityCategory entity_category = 11;
|
||||
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -466,7 +487,7 @@ message FanStateResponse {
|
||||
option (ifdef) = "USE_FAN";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool state = 2;
|
||||
bool oscillating = 3;
|
||||
// Deprecated in API version 1.6
|
||||
@@ -483,7 +504,7 @@ message FanCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool has_state = 2;
|
||||
bool state = 3;
|
||||
// Deprecated in API version 1.6
|
||||
@@ -521,9 +542,9 @@ message ListEntitiesLightResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_LIGHT";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
|
||||
@@ -540,7 +561,7 @@ message ListEntitiesLightResponse {
|
||||
float max_mireds = 10;
|
||||
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
bool disabled_by_default = 13;
|
||||
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
EntityCategory entity_category = 15;
|
||||
uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -551,7 +572,7 @@ message LightStateResponse {
|
||||
option (ifdef) = "USE_LIGHT";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool state = 2;
|
||||
float brightness = 3;
|
||||
ColorMode color_mode = 11;
|
||||
@@ -573,7 +594,7 @@ message LightCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool has_state = 2;
|
||||
bool state = 3;
|
||||
bool has_brightness = 4;
|
||||
@@ -626,16 +647,16 @@ message ListEntitiesSensorResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SENSOR";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string unit_of_measurement = 6;
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
string unit_of_measurement = 6 [(max_data_length) = 63];
|
||||
int32 accuracy_decimals = 7;
|
||||
bool force_update = 8;
|
||||
string device_class = 9;
|
||||
string device_class = 9 [(max_data_length) = 47];
|
||||
SensorStateClass state_class = 10;
|
||||
// Last reset type removed in 2021.9.0
|
||||
// Deprecated in API version 1.5
|
||||
@@ -650,8 +671,9 @@ message SensorStateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SENSOR";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
float state = 2;
|
||||
// If the sensor does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
@@ -666,16 +688,16 @@ message ListEntitiesSwitchResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SWITCH";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool assumed_state = 6;
|
||||
bool disabled_by_default = 7;
|
||||
EntityCategory entity_category = 8;
|
||||
string device_class = 9;
|
||||
string device_class = 9 [(max_data_length) = 47];
|
||||
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
message SwitchStateResponse {
|
||||
@@ -685,7 +707,7 @@ message SwitchStateResponse {
|
||||
option (ifdef) = "USE_SWITCH";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -696,7 +718,7 @@ message SwitchCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -708,15 +730,15 @@ message ListEntitiesTextSensorResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_TEXT_SENSOR";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
string device_class = 8;
|
||||
string device_class = 8 [(max_data_length) = 47];
|
||||
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
message TextSensorStateResponse {
|
||||
@@ -726,7 +748,7 @@ message TextSensorStateResponse {
|
||||
option (ifdef) = "USE_TEXT_SENSOR";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
string state = 2;
|
||||
// If the text sensor does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
@@ -756,9 +778,10 @@ message SubscribeLogsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (log) = false;
|
||||
option (no_delay) = false;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
LogLevel level = 1;
|
||||
bytes message = 3;
|
||||
LogLevel level = 1 [(force) = true];
|
||||
bytes message = 3 [(force) = true];
|
||||
}
|
||||
|
||||
// ==================== NOISE ENCRYPTION ====================
|
||||
@@ -922,7 +945,7 @@ message ListEntitiesServicesResponse {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
|
||||
string name = 1;
|
||||
fixed32 key = 2;
|
||||
fixed32 key = 2 [(force) = true];
|
||||
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
||||
SupportsResponseType supports_response = 4;
|
||||
}
|
||||
@@ -945,7 +968,7 @@ message ExecuteServiceRequest {
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
|
||||
uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
|
||||
bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
|
||||
@@ -971,12 +994,12 @@ message ListEntitiesCameraResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_CAMERA";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
bool disabled_by_default = 5;
|
||||
string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
EntityCategory entity_category = 7;
|
||||
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -987,7 +1010,7 @@ message CameraImageResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_CAMERA";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bytes data = 2;
|
||||
bool done = 3;
|
||||
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -1056,9 +1079,9 @@ message ListEntitiesClimateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_CLIMATE";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
bool supports_current_temperature = 5; // Deprecated: use feature_flags
|
||||
@@ -1078,7 +1101,7 @@ message ListEntitiesClimateResponse {
|
||||
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
|
||||
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"];
|
||||
bool disabled_by_default = 18;
|
||||
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
EntityCategory entity_category = 20;
|
||||
float visual_current_temperature_step = 21;
|
||||
bool supports_current_humidity = 22; // Deprecated: use feature_flags
|
||||
@@ -1095,7 +1118,7 @@ message ClimateStateResponse {
|
||||
option (ifdef) = "USE_CLIMATE";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
ClimateMode mode = 2;
|
||||
float current_temperature = 3;
|
||||
float target_temperature = 4;
|
||||
@@ -1121,7 +1144,7 @@ message ClimateCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool has_mode = 2;
|
||||
ClimateMode mode = 3;
|
||||
bool has_target_temperature = 4;
|
||||
@@ -1167,10 +1190,10 @@ message ListEntitiesWaterHeaterResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_WATER_HEATER";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 5;
|
||||
EntityCategory entity_category = 6;
|
||||
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -1189,7 +1212,7 @@ message WaterHeaterStateResponse {
|
||||
option (ifdef) = "USE_WATER_HEATER";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
float current_temperature = 2;
|
||||
float target_temperature = 3;
|
||||
WaterHeaterMode mode = 4;
|
||||
@@ -1219,7 +1242,7 @@ message WaterHeaterCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
// Bitmask of which fields are set (see WaterHeaterCommandHasField)
|
||||
uint32 has_fields = 2;
|
||||
WaterHeaterMode mode = 3;
|
||||
@@ -1243,20 +1266,20 @@ message ListEntitiesNumberResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_NUMBER";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
float min_value = 6;
|
||||
float max_value = 7;
|
||||
float step = 8;
|
||||
bool disabled_by_default = 9;
|
||||
EntityCategory entity_category = 10;
|
||||
string unit_of_measurement = 11;
|
||||
string unit_of_measurement = 11 [(max_data_length) = 63];
|
||||
NumberMode mode = 12;
|
||||
string device_class = 13;
|
||||
string device_class = 13 [(max_data_length) = 47];
|
||||
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
message NumberStateResponse {
|
||||
@@ -1266,7 +1289,7 @@ message NumberStateResponse {
|
||||
option (ifdef) = "USE_NUMBER";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
float state = 2;
|
||||
// If the number does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
@@ -1280,7 +1303,7 @@ message NumberCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
float state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -1292,12 +1315,12 @@ message ListEntitiesSelectResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SELECT";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
bool disabled_by_default = 7;
|
||||
EntityCategory entity_category = 8;
|
||||
@@ -1310,7 +1333,7 @@ message SelectStateResponse {
|
||||
option (ifdef) = "USE_SELECT";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
string state = 2;
|
||||
// If the select does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
@@ -1324,7 +1347,7 @@ message SelectCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
string state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -1336,12 +1359,12 @@ message ListEntitiesSirenResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SIREN";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
repeated string tones = 7 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
bool supports_duration = 8;
|
||||
@@ -1356,7 +1379,7 @@ message SirenStateResponse {
|
||||
option (ifdef) = "USE_SIREN";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -1367,7 +1390,7 @@ message SirenCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool has_state = 2;
|
||||
bool state = 3;
|
||||
bool has_tone = 4;
|
||||
@@ -1399,12 +1422,12 @@ message ListEntitiesLockResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_LOCK";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
bool assumed_state = 8;
|
||||
@@ -1422,7 +1445,7 @@ message LockStateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_LOCK";
|
||||
option (no_delay) = true;
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
LockState state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -1432,7 +1455,7 @@ message LockCommandRequest {
|
||||
option (ifdef) = "USE_LOCK";
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
LockCommand command = 2;
|
||||
|
||||
// Not yet implemented:
|
||||
@@ -1448,15 +1471,15 @@ message ListEntitiesButtonResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_BUTTON";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
string device_class = 8;
|
||||
string device_class = 8 [(max_data_length) = 47];
|
||||
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
message ButtonCommandRequest {
|
||||
@@ -1466,7 +1489,7 @@ message ButtonCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
uint32 device_id = 2 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
@@ -1515,12 +1538,12 @@ message ListEntitiesMediaPlayerResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_MEDIA_PLAYER";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
|
||||
@@ -1538,7 +1561,7 @@ message MediaPlayerStateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_MEDIA_PLAYER";
|
||||
option (no_delay) = true;
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
MediaPlayerState state = 2;
|
||||
float volume = 3;
|
||||
bool muted = 4;
|
||||
@@ -1551,7 +1574,7 @@ message MediaPlayerCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
|
||||
bool has_command = 2;
|
||||
MediaPlayerCommand command = 3;
|
||||
@@ -1604,9 +1627,10 @@ message BluetoothLEAdvertisementResponse {
|
||||
}
|
||||
|
||||
message BluetoothLERawAdvertisement {
|
||||
option (inline_encode) = true;
|
||||
uint64 address = 1 [(force) = true];
|
||||
sint32 rssi = 2 [(force) = true];
|
||||
uint32 address_type = 3;
|
||||
uint32 address_type = 3 [(max_value) = 4];
|
||||
|
||||
bytes data = 4 [(fixed_array_size) = 62, (force) = true];
|
||||
}
|
||||
@@ -1616,6 +1640,7 @@ message BluetoothLERawAdvertisementsResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
option (no_delay) = true;
|
||||
option (speed_optimized) = true;
|
||||
|
||||
repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"];
|
||||
}
|
||||
@@ -2103,11 +2128,11 @@ message ListEntitiesAlarmControlPanelResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
uint32 supported_features = 8;
|
||||
@@ -2122,7 +2147,7 @@ message AlarmControlPanelStateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
|
||||
option (no_delay) = true;
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
AlarmControlPanelState state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -2133,7 +2158,7 @@ message AlarmControlPanelCommandRequest {
|
||||
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
AlarmControlPanelStateCommand command = 2;
|
||||
string code = 3;
|
||||
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -2150,11 +2175,11 @@ message ListEntitiesTextResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_TEXT";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
|
||||
@@ -2171,7 +2196,7 @@ message TextStateResponse {
|
||||
option (ifdef) = "USE_TEXT";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
string state = 2;
|
||||
// If the Text does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
@@ -2185,7 +2210,7 @@ message TextCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
string state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -2198,12 +2223,12 @@ message ListEntitiesDateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_DATETIME_DATE";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -2215,7 +2240,7 @@ message DateStateResponse {
|
||||
option (ifdef) = "USE_DATETIME_DATE";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
// If the date does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
bool missing_state = 2;
|
||||
@@ -2231,7 +2256,7 @@ message DateCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
uint32 year = 2;
|
||||
uint32 month = 3;
|
||||
uint32 day = 4;
|
||||
@@ -2245,12 +2270,12 @@ message ListEntitiesTimeResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_DATETIME_TIME";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -2262,7 +2287,7 @@ message TimeStateResponse {
|
||||
option (ifdef) = "USE_DATETIME_TIME";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
// If the time does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
bool missing_state = 2;
|
||||
@@ -2278,7 +2303,7 @@ message TimeCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
uint32 hour = 2;
|
||||
uint32 minute = 3;
|
||||
uint32 second = 4;
|
||||
@@ -2292,15 +2317,15 @@ message ListEntitiesEventResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_EVENT";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
string device_class = 8;
|
||||
string device_class = 8 [(max_data_length) = 47];
|
||||
|
||||
repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -2311,7 +2336,7 @@ message EventResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_EVENT";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
string event_type = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -2323,15 +2348,15 @@ message ListEntitiesValveResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_VALVE";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
string device_class = 8;
|
||||
string device_class = 8 [(max_data_length) = 47];
|
||||
|
||||
bool assumed_state = 9;
|
||||
bool supports_position = 10;
|
||||
@@ -2351,7 +2376,7 @@ message ValveStateResponse {
|
||||
option (ifdef) = "USE_VALVE";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
float position = 2;
|
||||
ValveOperation current_operation = 3;
|
||||
uint32 device_id = 4 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -2364,7 +2389,7 @@ message ValveCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool has_position = 2;
|
||||
float position = 3;
|
||||
bool stop = 4;
|
||||
@@ -2378,12 +2403,12 @@ message ListEntitiesDateTimeResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_DATETIME_DATETIME";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -2395,7 +2420,7 @@ message DateTimeStateResponse {
|
||||
option (ifdef) = "USE_DATETIME_DATETIME";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
// If the datetime does not have a valid state yet.
|
||||
// Equivalent to `!obj->has_state()` - inverse logic to make state packets smaller
|
||||
bool missing_state = 2;
|
||||
@@ -2409,7 +2434,7 @@ message DateTimeCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
fixed32 epoch_seconds = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -2421,15 +2446,15 @@ message ListEntitiesUpdateResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_UPDATE";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
string device_class = 8;
|
||||
string device_class = 8 [(max_data_length) = 47];
|
||||
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
message UpdateStateResponse {
|
||||
@@ -2439,7 +2464,7 @@ message UpdateStateResponse {
|
||||
option (ifdef) = "USE_UPDATE";
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
bool missing_state = 2;
|
||||
bool in_progress = 3;
|
||||
bool has_progress = 4;
|
||||
@@ -2463,7 +2488,7 @@ message UpdateCommandRequest {
|
||||
option (no_delay) = true;
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
fixed32 key = 1 [(force) = true];
|
||||
UpdateCommand command = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
@@ -2504,14 +2529,15 @@ message ListEntitiesInfraredResponse {
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_INFRARED";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
string object_id = 1 [(max_data_length) = 120, (force) = true];
|
||||
fixed32 key = 2 [(force) = true];
|
||||
string name = 3 [(max_data_length) = 120, (force) = true];
|
||||
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
|
||||
bool disabled_by_default = 5;
|
||||
EntityCategory entity_category = 6;
|
||||
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
|
||||
uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags
|
||||
uint32 receiver_frequency = 9; // Demodulation frequency of the IR receiver in Hz (0 = unspecified)
|
||||
}
|
||||
|
||||
// Command to transmit infrared/RF data using raw timings
|
||||
@@ -2521,7 +2547,7 @@ message InfraredRFTransmitRawTimingsRequest {
|
||||
option (ifdef) = "USE_IR_RF";
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
fixed32 key = 2; // Key identifying the transmitter instance
|
||||
fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance
|
||||
uint32 carrier_frequency = 3; // Carrier frequency in Hz
|
||||
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
|
||||
repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off)
|
||||
@@ -2535,7 +2561,7 @@ message InfraredRFReceiveEvent {
|
||||
option (no_delay) = true;
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
fixed32 key = 2; // Key identifying the receiver instance
|
||||
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
|
||||
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
|
||||
}
|
||||
|
||||
|
||||
@@ -52,11 +52,11 @@
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// Read a maximum of 5 messages per loop iteration to prevent starving other components.
|
||||
// Maximum messages to read per loop iteration to prevent starving other components.
|
||||
// This is a balance between API responsiveness and allowing other components to run.
|
||||
// Since each message could contain multiple protobuf messages when using packet batching,
|
||||
// this limits the number of messages processed, not the number of TCP packets.
|
||||
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5;
|
||||
static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 10;
|
||||
static constexpr uint8_t MAX_PING_RETRIES = 60;
|
||||
static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
|
||||
static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
|
||||
@@ -72,6 +72,14 @@ static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000;
|
||||
|
||||
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
|
||||
|
||||
// Cross-validate C++ constants against proto max_data_length annotations in api.proto
|
||||
static_assert(MAC_ADDRESS_PRETTY_BUFFER_SIZE - 1 == 17,
|
||||
"Update max_data_length for mac_address/bluetooth_mac_address in api.proto");
|
||||
static_assert(Application::BUILD_TIME_STR_SIZE - 1 == 25, "Update max_data_length for compilation_time in api.proto");
|
||||
static_assert(sizeof(ESPHOME_VERSION) - 1 <= 32, "Update max_data_length for esphome_version in api.proto");
|
||||
static_assert(ESPHOME_DEVICE_NAME_MAX_LEN <= 31, "Update max_data_length for name in api.proto");
|
||||
static_assert(ESPHOME_FRIENDLY_NAME_MAX_LEN <= 120, "Update max_data_length for friendly_name in api.proto");
|
||||
|
||||
static const char *const TAG = "api.connection";
|
||||
#ifdef USE_CAMERA
|
||||
static const int CAMERA_STOP_STREAM = 5000;
|
||||
@@ -132,8 +140,6 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
|
||||
|
||||
void APIConnection::start() {
|
||||
this->last_traffic_ = App.get_loop_component_start_time();
|
||||
|
||||
@@ -214,10 +220,17 @@ void APIConnection::loop() {
|
||||
}
|
||||
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
// Check if socket has data ready before attempting to read
|
||||
if (this->helper_->is_socket_ready()) {
|
||||
// Check if socket has data ready before attempting to read.
|
||||
// Also try reading if we hit the message limit last time — LWIP's rcvevent
|
||||
// (used by is_socket_ready) tracks pbuf dequeues, not bytes. When multiple
|
||||
// messages share a TCP segment, the last message's data stays in LWIP's
|
||||
// lastdata cache after rcvevent hits 0, making is_socket_ready() return false
|
||||
// even though data remains.
|
||||
if (this->helper_->is_socket_ready() || this->flags_.may_have_remaining_data) {
|
||||
this->flags_.may_have_remaining_data = false;
|
||||
// Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
|
||||
for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
|
||||
uint8_t message_count = 0;
|
||||
for (; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
|
||||
ReadPacketBuffer buffer;
|
||||
err = this->helper_->read_packet(&buffer);
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
@@ -234,11 +247,16 @@ void APIConnection::loop() {
|
||||
this->last_traffic_ = now;
|
||||
}
|
||||
// read a packet
|
||||
this->read_message(buffer.data_len, buffer.type, buffer.data);
|
||||
this->read_message_(buffer.data_len, buffer.type, buffer.data);
|
||||
if (this->flags_.remove)
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If we hit the limit, there may be more data remaining in LWIP's
|
||||
// lastdata cache that rcvevent doesn't account for.
|
||||
if (message_count == MAX_MESSAGES_PER_LOOP) {
|
||||
this->flags_.may_have_remaining_data = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Process deferred batch if scheduled and timer has expired
|
||||
@@ -309,6 +327,8 @@ void APIConnection::process_active_iterator_() {
|
||||
this->destroy_active_iterator_();
|
||||
if (this->flags_.state_subscription) {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
} else {
|
||||
this->finalize_iterator_sync_();
|
||||
}
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.list_entities);
|
||||
@@ -316,21 +336,27 @@ void APIConnection::process_active_iterator_() {
|
||||
} else { // INITIAL_STATE
|
||||
if (this->iterator_storage_.initial_state.completed()) {
|
||||
this->destroy_active_iterator_();
|
||||
// Process any remaining batched messages immediately
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
this->finalize_iterator_sync_();
|
||||
} else {
|
||||
this->process_iterator_batch_(this->iterator_storage_.initial_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void APIConnection::finalize_iterator_sync_() {
|
||||
// Flush any remaining batched messages immediately so clients
|
||||
// receive completion responses (e.g. ListEntitiesDoneResponse)
|
||||
// without waiting for the batch timer.
|
||||
if (!this->deferred_batch_.empty()) {
|
||||
this->process_batch_();
|
||||
}
|
||||
// Enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
}
|
||||
|
||||
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
size_t max_batch = this->get_max_batch_size_();
|
||||
@@ -400,7 +426,7 @@ uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResp
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = entity->get_device_id();
|
||||
#endif
|
||||
return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
|
||||
return encode_to_buffer_slow(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
|
||||
}
|
||||
|
||||
uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
|
||||
@@ -1465,7 +1491,7 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent
|
||||
void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %u out of range (max %u)", msg.instance,
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range (max %" PRIu32 ")", msg.instance,
|
||||
static_cast<uint32_t>(proxies.size()));
|
||||
return;
|
||||
}
|
||||
@@ -1476,7 +1502,7 @@ void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigure
|
||||
void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
return;
|
||||
}
|
||||
proxies[msg.instance]->write_from_client(msg.data, msg.data_len);
|
||||
@@ -1485,7 +1511,7 @@ void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest
|
||||
void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
return;
|
||||
}
|
||||
proxies[msg.instance]->set_modem_pins(msg.line_states);
|
||||
@@ -1494,7 +1520,7 @@ void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetM
|
||||
void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
return;
|
||||
}
|
||||
SerialProxyGetModemPinsResponse resp{};
|
||||
@@ -1506,7 +1532,7 @@ void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetM
|
||||
void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %u out of range", msg.instance);
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
return;
|
||||
}
|
||||
switch (msg.type) {
|
||||
@@ -1519,16 +1545,16 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
resp.instance = msg.instance;
|
||||
resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH;
|
||||
switch (proxies[msg.instance]->flush_port()) {
|
||||
case uart::FlushResult::SUCCESS:
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_OK;
|
||||
break;
|
||||
case uart::FlushResult::ASSUMED_SUCCESS:
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
|
||||
break;
|
||||
case uart::FlushResult::TIMEOUT:
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT;
|
||||
break;
|
||||
case uart::FlushResult::FAILED:
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
@@ -1536,7 +1562,7 @@ void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown serial proxy request type: %u", static_cast<uint32_t>(msg.type));
|
||||
ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -1549,6 +1575,7 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection
|
||||
auto *infrared = static_cast<infrared::Infrared *>(entity);
|
||||
ListEntitiesInfraredResponse msg;
|
||||
msg.capabilities = infrared->get_capability_flags();
|
||||
msg.receiver_frequency = infrared->get_traits().get_receiver_frequency_hz();
|
||||
return fill_and_encode_entity_info(infrared, msg, conn, remaining_size);
|
||||
}
|
||||
#endif
|
||||
@@ -1717,6 +1744,7 @@ bool APIConnection::send_device_info_response_() {
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
|
||||
resp.manufacturer = MANUFACTURER;
|
||||
#endif
|
||||
static_assert(sizeof(ESPHOME_MANUFACTURER) - 1 <= 20, "Update max_data_length for manufacturer in api.proto");
|
||||
#undef ESPHOME_MANUFACTURER
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
@@ -1994,52 +2022,15 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M
|
||||
size_t write_start = shared_buf.size();
|
||||
shared_buf.resize(write_start + payload_size);
|
||||
ProtoWriteBuffer buffer{&shared_buf, write_start};
|
||||
encode_fn(msg, buffer);
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
|
||||
}
|
||||
// Encodes a message to the buffer and returns the total number of bytes used,
|
||||
// including header and footer overhead. Returns 0 if the message doesn't fit.
|
||||
uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
|
||||
APIConnection *conn, uint32_t remaining_size) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
if (conn->flags_.log_only_mode) {
|
||||
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
|
||||
DumpBuffer dump_buf;
|
||||
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
// Cache frame sizes to avoid repeated virtual calls
|
||||
const uint8_t header_padding = conn->helper_->frame_header_padding();
|
||||
const uint8_t footer_size = conn->helper_->frame_footer_size();
|
||||
// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE)
|
||||
|
||||
// Calculate total size with padding for buffer allocation
|
||||
size_t total_calculated_size = calculated_size + header_padding + footer_size;
|
||||
|
||||
// Check if it fits
|
||||
if (total_calculated_size > remaining_size)
|
||||
return 0; // Doesn't fit
|
||||
|
||||
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
|
||||
|
||||
if (conn->flags_.batch_first_message) {
|
||||
// First message - buffer already prepared by caller, just clear flag
|
||||
conn->flags_.batch_first_message = false;
|
||||
} else {
|
||||
// Batch message second or later
|
||||
// Add padding for previous message footer + this message header
|
||||
size_t current_size = shared_buf.size();
|
||||
shared_buf.reserve_and_resize(current_size + total_calculated_size, current_size + footer_size + header_padding);
|
||||
}
|
||||
|
||||
// Pre-resize buffer to include payload, then encode through raw pointer
|
||||
size_t write_start = shared_buf.size();
|
||||
shared_buf.resize(write_start + calculated_size);
|
||||
ProtoWriteBuffer buffer{&shared_buf, write_start};
|
||||
encode_fn(msg, buffer);
|
||||
|
||||
// Return total size (header + payload + footer)
|
||||
return static_cast<uint16_t>(header_padding + calculated_size + footer_size);
|
||||
// Noinline version for cold paths — single shared copy
|
||||
uint16_t APIConnection::encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
|
||||
APIConnection *conn, uint32_t remaining_size) {
|
||||
return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size);
|
||||
}
|
||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
|
||||
@@ -2071,37 +2062,9 @@ void APIConnection::on_fatal_error() {
|
||||
this->flags_.remove = true;
|
||||
}
|
||||
|
||||
void __attribute__((flatten)) APIConnection::DeferredBatch::push_item(const BatchItem &item) { items.push_back(item); }
|
||||
|
||||
void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index) {
|
||||
// Check if we already have a message of this type for this entity
|
||||
// This provides deduplication per entity/message_type combination
|
||||
// O(n) but optimized for RAM and not performance.
|
||||
// Skip deduplication for events - they are edge-triggered, every occurrence matters
|
||||
#ifdef USE_EVENT
|
||||
if (message_type != EventResponse::MESSAGE_TYPE)
|
||||
#endif
|
||||
{
|
||||
for (const auto &item : items) {
|
||||
if (item.entity == entity && item.message_type == message_type)
|
||||
return; // Already queued
|
||||
}
|
||||
}
|
||||
// No existing item found (or event), add new one
|
||||
this->push_item({entity, message_type, estimated_size, aux_data_index});
|
||||
}
|
||||
|
||||
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
||||
// Add high priority message and swap to front
|
||||
// This avoids expensive vector::insert which shifts all elements
|
||||
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
|
||||
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
|
||||
this->push_item({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
||||
if (items.size() > 1) {
|
||||
// Swap the new high-priority item to the front
|
||||
std::swap(items.front(), items.back());
|
||||
}
|
||||
bool APIConnection::schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
||||
this->deferred_batch_.add_item_front(entity, message_type, estimated_size);
|
||||
return this->schedule_batch_();
|
||||
}
|
||||
|
||||
bool APIConnection::send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
@@ -2135,6 +2098,13 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure TCP_NODELAY is on before draining overflow and writing batch data.
|
||||
// Log messages enable Nagle (NODELAY off) to coalesce small packets.
|
||||
// If Nagle is still on when we try to drain, LWIP holds data in the
|
||||
// Nagle buffer, the TCP send buffer stays full, and the overflow
|
||||
// buffer can never drain — blocking the batch write indefinitely.
|
||||
this->helper_->set_nodelay_for_message(false);
|
||||
|
||||
// Try to clear buffer first
|
||||
if (!this->try_to_clear_buffer(true)) {
|
||||
// Can't write now, we'll try again later
|
||||
@@ -2194,17 +2164,15 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
|
||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||
|
||||
const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||
const uint8_t frame_overhead = header_padding + footer_size;
|
||||
|
||||
// Stack-allocated array for message info
|
||||
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
|
||||
MessageInfo *message_info = reinterpret_cast<MessageInfo *>(message_info_storage);
|
||||
size_t items_processed = 0;
|
||||
uint16_t remaining_size = std::numeric_limits<uint16_t>::max();
|
||||
// Track where each message's header padding begins in the buffer
|
||||
// For plaintext: this is where the 6-byte header padding starts
|
||||
// For noise: this is where the 7-byte header padding starts
|
||||
// The actual message data follows after the header padding
|
||||
// Track where each message's header begins in the buffer
|
||||
// First message: offset 0 (max padding, may have unused leading bytes)
|
||||
// Subsequent messages: offset points to exact header start (no gaps)
|
||||
uint32_t current_offset = 0;
|
||||
|
||||
// Process items and encode directly to buffer (up to our limit)
|
||||
@@ -2220,13 +2188,14 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
|
||||
}
|
||||
|
||||
// Message was encoded successfully
|
||||
// payload_size is header_padding + actual payload size + footer_size
|
||||
uint16_t proto_payload_size = payload_size - frame_overhead;
|
||||
// payload_size = header_size + proto_payload_size + footer_size
|
||||
uint16_t proto_payload_size = payload_size - this->batch_header_size_ - footer_size;
|
||||
// Use placement new to construct MessageInfo in pre-allocated stack array
|
||||
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
|
||||
// Explicit destruction is not needed because MessageInfo is trivially destructible,
|
||||
// as ensured by the static_assert in its definition.
|
||||
new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size);
|
||||
new (&message_info[items_processed++])
|
||||
MessageInfo(item.message_type, current_offset, proto_payload_size, this->batch_header_size_);
|
||||
// After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation
|
||||
if (items_processed == 1) {
|
||||
remaining_size = MAX_BATCH_PACKET_SIZE;
|
||||
@@ -2276,6 +2245,7 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
|
||||
uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size,
|
||||
bool batch_first) {
|
||||
this->flags_.batch_first_message = batch_first;
|
||||
this->batch_message_type_ = item.message_type;
|
||||
#ifdef USE_EVENT
|
||||
// Events need aux_data_index to look up event type from entity
|
||||
if (item.message_type == EventResponse::MESSAGE_TYPE) {
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
#include "esphome/components/rp2040/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
#include "esphome/components/esp8266/crash_handler.h"
|
||||
#endif
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
@@ -44,16 +47,46 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP
|
||||
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
|
||||
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
|
||||
|
||||
#ifdef USE_BENCHMARK
|
||||
class APIConnection;
|
||||
void bench_enable_immediate_send(APIConnection *conn);
|
||||
void bench_clear_batch(APIConnection *conn);
|
||||
void bench_process_batch(APIConnection *conn);
|
||||
#endif
|
||||
|
||||
class APIConnection final : public APIServerConnectionBase {
|
||||
public:
|
||||
friend class APIServer;
|
||||
friend class ListEntitiesIterator;
|
||||
#ifdef USE_BENCHMARK
|
||||
friend void bench_enable_immediate_send(APIConnection *conn);
|
||||
friend void bench_clear_batch(APIConnection *conn);
|
||||
friend void bench_process_batch(APIConnection *conn);
|
||||
#endif
|
||||
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
|
||||
virtual ~APIConnection();
|
||||
~APIConnection();
|
||||
|
||||
void start();
|
||||
void loop();
|
||||
|
||||
protected:
|
||||
// read_message_ is defined here (instead of in APIServerConnectionBase) so the
|
||||
// compiler can devirtualize and inline on_* handler calls within this final class.
|
||||
void read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data);
|
||||
|
||||
// Auth helpers defined here (not in ProtoService) so the compiler can
|
||||
// devirtualize is_connection_setup()/on_no_setup_connection() calls
|
||||
// within this final class.
|
||||
inline bool check_connection_setup_() {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
inline bool check_authenticated_() { return this->check_connection_setup_(); }
|
||||
|
||||
public:
|
||||
bool send_list_info_done() {
|
||||
return this->schedule_message_(nullptr, ListEntitiesDoneResponse::MESSAGE_TYPE,
|
||||
ListEntitiesDoneResponse::ESTIMATED_SIZE);
|
||||
@@ -63,72 +96,72 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
bool send_cover_state(cover::Cover *cover);
|
||||
void on_cover_command_request(const CoverCommandRequest &msg) override;
|
||||
void on_cover_command_request(const CoverCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
bool send_fan_state(fan::Fan *fan);
|
||||
void on_fan_command_request(const FanCommandRequest &msg) override;
|
||||
void on_fan_command_request(const FanCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
bool send_light_state(light::LightState *light);
|
||||
void on_light_command_request(const LightCommandRequest &msg) override;
|
||||
void on_light_command_request(const LightCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
bool send_sensor_state(sensor::Sensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool send_switch_state(switch_::Switch *a_switch);
|
||||
void on_switch_command_request(const SwitchCommandRequest &msg) override;
|
||||
void on_switch_command_request(const SwitchCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
|
||||
void on_camera_image_request(const CameraImageRequest &msg) override;
|
||||
void on_camera_image_request(const CameraImageRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
bool send_climate_state(climate::Climate *climate);
|
||||
void on_climate_command_request(const ClimateCommandRequest &msg) override;
|
||||
void on_climate_command_request(const ClimateCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
bool send_number_state(number::Number *number);
|
||||
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||
void on_number_command_request(const NumberCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool send_date_state(datetime::DateEntity *date);
|
||||
void on_date_command_request(const DateCommandRequest &msg) override;
|
||||
void on_date_command_request(const DateCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool send_time_state(datetime::TimeEntity *time);
|
||||
void on_time_command_request(const TimeCommandRequest &msg) override;
|
||||
void on_time_command_request(const TimeCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool send_datetime_state(datetime::DateTimeEntity *datetime);
|
||||
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
|
||||
void on_date_time_command_request(const DateTimeCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool send_text_state(text::Text *text);
|
||||
void on_text_command_request(const TextCommandRequest &msg) override;
|
||||
void on_text_command_request(const TextCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool send_select_state(select::Select *select);
|
||||
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||
void on_select_command_request(const SelectCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void on_button_command_request(const ButtonCommandRequest &msg) override;
|
||||
void on_button_command_request(const ButtonCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool send_lock_state(lock::Lock *a_lock);
|
||||
void on_lock_command_request(const LockCommandRequest &msg) override;
|
||||
void on_lock_command_request(const LockCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
bool send_valve_state(valve::Valve *valve);
|
||||
void on_valve_command_request(const ValveCommandRequest &msg) override;
|
||||
void on_valve_command_request(const ValveCommandRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool send_media_player_state(media_player::MediaPlayer *media_player);
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &msg);
|
||||
#endif
|
||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
@@ -138,23 +171,23 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->send_message(call);
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
|
||||
void on_homeassistant_action_response(const HomeassistantActionResponse &msg);
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
void on_unsubscribe_bluetooth_le_advertisements_request() override;
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg);
|
||||
void on_unsubscribe_bluetooth_le_advertisements_request();
|
||||
|
||||
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
|
||||
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
|
||||
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
|
||||
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
|
||||
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
|
||||
void on_subscribe_bluetooth_connections_free_request() override;
|
||||
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override;
|
||||
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg);
|
||||
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg);
|
||||
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg);
|
||||
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg);
|
||||
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg);
|
||||
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg);
|
||||
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg);
|
||||
void on_subscribe_bluetooth_connections_free_request();
|
||||
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg);
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg);
|
||||
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
@@ -165,42 +198,42 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
|
||||
void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
|
||||
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
|
||||
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
|
||||
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
|
||||
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
|
||||
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
|
||||
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg);
|
||||
void on_voice_assistant_response(const VoiceAssistantResponse &msg);
|
||||
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg);
|
||||
void on_voice_assistant_audio(const VoiceAssistantAudio &msg);
|
||||
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg);
|
||||
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg);
|
||||
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg);
|
||||
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg);
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg);
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override;
|
||||
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override;
|
||||
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override;
|
||||
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override;
|
||||
void on_serial_proxy_request(const SerialProxyRequest &msg) override;
|
||||
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg);
|
||||
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg);
|
||||
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 send_serial_proxy_data(const SerialProxyDataReceived &msg);
|
||||
#endif
|
||||
|
||||
@@ -210,26 +243,26 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool send_update_state(update::UpdateEntity *update);
|
||||
void on_update_command_request(const UpdateCommandRequest &msg) override;
|
||||
void on_update_command_request(const UpdateCommandRequest &msg);
|
||||
#endif
|
||||
|
||||
void on_disconnect_response() override;
|
||||
void on_ping_response() override {
|
||||
void on_disconnect_response();
|
||||
void on_ping_response() {
|
||||
// we initiated ping
|
||||
this->flags_.sent_ping = false;
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
|
||||
void on_home_assistant_state_response(const HomeAssistantStateResponse &msg);
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void on_get_time_response(const GetTimeResponse &value) override;
|
||||
void on_get_time_response(const GetTimeResponse &value);
|
||||
#endif
|
||||
void on_hello_request(const HelloRequest &msg) override;
|
||||
void on_disconnect_request() override;
|
||||
void on_ping_request() override;
|
||||
void on_device_info_request() override;
|
||||
void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
||||
void on_subscribe_states_request() override {
|
||||
void on_hello_request(const HelloRequest &msg);
|
||||
void on_disconnect_request();
|
||||
void on_ping_request();
|
||||
void on_device_info_request();
|
||||
void on_list_entities_request() { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
||||
void on_subscribe_states_request() {
|
||||
this->flags_.state_subscription = true;
|
||||
// Start initial state iterator only if no iterator is active
|
||||
// If list_entities is running, we'll start initial_state when it completes
|
||||
@@ -237,25 +270,29 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override {
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
esp32::crash_handler_log();
|
||||
esp32::crash_handler_clear();
|
||||
#endif
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
rp2040::crash_handler_log();
|
||||
#endif
|
||||
#ifdef USE_ESP8266_CRASH_HANDLER
|
||||
esp8266::crash_handler_log();
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
|
||||
void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; }
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_subscribe_home_assistant_states_request() override;
|
||||
void on_subscribe_home_assistant_states_request();
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
@@ -265,13 +302,13 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg);
|
||||
#endif
|
||||
|
||||
bool is_authenticated() override {
|
||||
bool is_authenticated() {
|
||||
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::AUTHENTICATED;
|
||||
}
|
||||
bool is_connection_setup() override {
|
||||
bool is_connection_setup() {
|
||||
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
|
||||
this->is_authenticated();
|
||||
}
|
||||
@@ -284,11 +321,11 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
|
||||
}
|
||||
|
||||
void on_fatal_error() override;
|
||||
void on_no_setup_connection() override;
|
||||
void on_fatal_error();
|
||||
void on_no_setup_connection();
|
||||
|
||||
// Function pointer type for type-erased message encoding
|
||||
using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &);
|
||||
using MessageEncodeFn = uint8_t *(*) (const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM);
|
||||
// Function pointer type for type-erased size calculation
|
||||
using CalculateSizeFn = uint32_t (*)(const void *);
|
||||
|
||||
@@ -324,7 +361,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
return true;
|
||||
return this->try_to_clear_buffer_slow_(log_out_of_space);
|
||||
}
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type);
|
||||
|
||||
const char *get_name() const { return this->helper_->get_client_name(); }
|
||||
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
|
||||
@@ -367,21 +404,66 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
}
|
||||
|
||||
// Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
|
||||
static void encode_msg_noop(const void *, ProtoWriteBuffer &) {}
|
||||
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, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||
|
||||
// Non-template buffer management for batch encoding
|
||||
static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
|
||||
APIConnection *conn, uint32_t remaining_size);
|
||||
// Core batch encoding logic. Computes header size, checks fit, resizes buffer, encodes.
|
||||
// ALWAYS_INLINE so the compiler can devirtualize encode_fn at hot call sites.
|
||||
static inline uint16_t ESPHOME_ALWAYS_INLINE encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn,
|
||||
const void *msg, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
if (conn->flags_.log_only_mode) {
|
||||
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
|
||||
DumpBuffer dump_buf;
|
||||
conn->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
|
||||
return 1;
|
||||
}
|
||||
#endif
|
||||
const uint8_t footer_size = conn->helper_->frame_footer_size();
|
||||
|
||||
// Thin template wrapper — computes size, delegates buffer work to non-template helper
|
||||
// First message uses max padding (already in buffer), subsequent use exact header size
|
||||
size_t to_add;
|
||||
if (conn->flags_.batch_first_message) {
|
||||
conn->flags_.batch_first_message = false;
|
||||
conn->batch_header_size_ = conn->helper_->frame_header_padding();
|
||||
to_add = calculated_size;
|
||||
} else {
|
||||
conn->batch_header_size_ = conn->helper_->frame_header_size(calculated_size, conn->batch_message_type_);
|
||||
to_add = calculated_size + conn->batch_header_size_ + footer_size;
|
||||
}
|
||||
|
||||
// Check if it fits (using actual header size, not max padding)
|
||||
uint16_t total_calculated_size = calculated_size + conn->batch_header_size_ + footer_size;
|
||||
if (total_calculated_size > remaining_size)
|
||||
return 0;
|
||||
|
||||
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
|
||||
shared_buf.resize(shared_buf.size() + to_add);
|
||||
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
|
||||
return total_calculated_size;
|
||||
}
|
||||
|
||||
// Noinline version of encode_to_buffer for cold paths (entity info, zero-payload messages).
|
||||
// All cold callers share this single copy instead of each getting an ALWAYS_INLINE expansion.
|
||||
static uint16_t encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
|
||||
APIConnection *conn, uint32_t remaining_size);
|
||||
|
||||
// Thin template wrapper — uses noinline encode_to_buffer_slow since
|
||||
// encode_message_to_buffer callers are cold paths (zero-payload control messages).
|
||||
// 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(0, &encode_msg_noop, &msg, conn, remaining_size);
|
||||
return encode_to_buffer_slow(0, &encode_msg_noop, &msg, conn, remaining_size);
|
||||
} else {
|
||||
return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
|
||||
return encode_to_buffer_slow(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -580,6 +662,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Helper methods for iterator lifecycle management
|
||||
void destroy_active_iterator_();
|
||||
void begin_iterator_(ActiveIterator type);
|
||||
void finalize_iterator_sync_();
|
||||
#ifdef USE_CAMERA
|
||||
std::unique_ptr<camera::CameraImageReader> image_reader_;
|
||||
#endif
|
||||
@@ -614,11 +697,28 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
// Add item to the batch (with deduplication)
|
||||
void add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index = AUX_DATA_UNUSED);
|
||||
uint8_t aux_data_index = AUX_DATA_UNUSED) {
|
||||
// Dedup: O(n) scan but optimized for RAM over performance
|
||||
// Skip deduplication for events - they are edge-triggered, every occurrence matters
|
||||
#ifdef USE_EVENT
|
||||
if (message_type != EventResponse::MESSAGE_TYPE)
|
||||
#endif
|
||||
{
|
||||
for (const auto &item : this->items) {
|
||||
if (item.entity == entity && item.message_type == message_type)
|
||||
return; // Already queued
|
||||
}
|
||||
}
|
||||
this->items.push_back({entity, message_type, estimated_size, aux_data_index});
|
||||
}
|
||||
// Add item to the front of the batch (for high priority messages like ping)
|
||||
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
|
||||
// Single push_back site to avoid duplicate _M_realloc_insert instantiation
|
||||
void push_item(const BatchItem &item);
|
||||
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
||||
// Swap to front avoids expensive vector::insert which shifts all elements
|
||||
this->items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
|
||||
if (this->items.size() > 1) {
|
||||
std::swap(this->items.front(), this->items.back());
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all items
|
||||
void clear() {
|
||||
@@ -671,6 +771,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
uint8_t batch_scheduled : 1;
|
||||
uint8_t batch_first_message : 1; // For batch buffer allocation
|
||||
uint8_t should_try_send_immediately : 1; // True after initial states are sent
|
||||
uint8_t may_have_remaining_data : 1; // Read loop hit limit, retry without ready check
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
uint8_t log_only_mode : 1;
|
||||
#endif
|
||||
@@ -679,11 +780,16 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// 2-byte types immediately after flags_ (no padding between them)
|
||||
uint16_t client_api_version_major_{0};
|
||||
uint16_t client_api_version_minor_{0};
|
||||
// 1-byte type to fill padding
|
||||
// 1-byte types to fill remaining space before next 4-byte boundary
|
||||
ActiveIterator active_iterator_{ActiveIterator::NONE};
|
||||
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
|
||||
uint8_t batch_message_type_{0}; // Current message type during batch encoding
|
||||
// Total: 2 (flags) + 2 + 2 + 1 + 1 = 8 bytes, aligned to 4-byte boundary
|
||||
|
||||
uint32_t get_batch_delay_ms_() const;
|
||||
// Actual header size used by encode_to_buffer for the current message.
|
||||
// Read by process_batch_multi_ to pass into MessageInfo.
|
||||
uint8_t batch_header_size_{0};
|
||||
|
||||
uint32_t get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
|
||||
// Message will use 8 more bytes than the minimum size, and typical
|
||||
// MTU is 1500. Sometimes users will see as low as 1460 MTU.
|
||||
// If its IPv6 the header is 40 bytes, and if its IPv4
|
||||
@@ -750,10 +856,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
}
|
||||
|
||||
// Helper function to schedule a high priority message at the front of the batch
|
||||
bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
||||
this->deferred_batch_.add_item_front(entity, message_type, estimated_size);
|
||||
return this->schedule_batch_();
|
||||
}
|
||||
// Out-of-line: callers (on_shutdown, check_keepalive_) are cold paths
|
||||
bool schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
|
||||
|
||||
// Helper function to log client messages with name and peername
|
||||
void log_client_(int level, const LogString *message);
|
||||
|
||||
@@ -100,10 +100,17 @@ const LogString *api_error_to_logstr(APIError err) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
void APIFrameHelper::log_packet_sending_(const void *data, uint16_t len) {
|
||||
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
|
||||
}
|
||||
#endif
|
||||
|
||||
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
|
||||
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
|
||||
int err = errno;
|
||||
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
|
||||
if (err != EWOULDBLOCK && err != EAGAIN) {
|
||||
this->state_ = State::FAILED;
|
||||
HELPER_LOG("Socket write failed with errno %d", err);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
@@ -111,45 +118,58 @@ APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
|
||||
// Returns OK if all data was sent or successfully queued.
|
||||
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
|
||||
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
|
||||
// Single-buffer write path: wraps in iovec and delegates.
|
||||
APIError APIFrameHelper::write_raw_buf_(const void *data, uint16_t len, ssize_t sent) {
|
||||
struct iovec iov = {const_cast<void *>(data), len};
|
||||
APIError err = this->write_raw_iov_(&iov, 1, len, sent);
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
|
||||
}
|
||||
// Log after write/enqueue so re-entrant log sends can't corrupt data before it's sent
|
||||
if (err == APIError::OK)
|
||||
LOG_PACKET_SENDING(reinterpret_cast<const uint8_t *>(data), len);
|
||||
#endif
|
||||
return err;
|
||||
}
|
||||
|
||||
uint16_t skip = 0;
|
||||
|
||||
// Drain any existing backlog first
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
APIError err = this->drain_overflow_and_handle_errors_();
|
||||
if (err != APIError::OK)
|
||||
return err;
|
||||
}
|
||||
|
||||
// If backlog is clear, try direct send
|
||||
if (this->overflow_buf_.empty()) [[likely]] {
|
||||
ssize_t sent =
|
||||
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
|
||||
if (sent == -1) [[unlikely]] {
|
||||
// Handles partial writes, errors, and overflow buffering.
|
||||
// Called when the inline fast path couldn't complete the write,
|
||||
// or directly from cold paths (handshake, error handling).
|
||||
APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, ssize_t sent) {
|
||||
if (sent <= 0) {
|
||||
if (sent == WRITE_NOT_ATTEMPTED) {
|
||||
// Cold path: no write attempted yet, drain overflow and try
|
||||
if (!this->overflow_buf_.empty()) {
|
||||
APIError err = this->drain_overflow_and_handle_errors_();
|
||||
if (err != APIError::OK)
|
||||
return err;
|
||||
}
|
||||
if (this->overflow_buf_.empty()) {
|
||||
sent = this->write_iov_to_socket_(iov, iovcnt);
|
||||
if (sent == static_cast<ssize_t>(total_write_len))
|
||||
return APIError::OK;
|
||||
// Partial write or -1: fall through to error check / enqueue below
|
||||
} else {
|
||||
// Overflow backlog remains after drain; skip socket write, enqueue everything
|
||||
sent = 0;
|
||||
}
|
||||
}
|
||||
// WRITE_FAILED (-1): fast path or retry write returned -1, check errno
|
||||
if (sent == WRITE_FAILED) {
|
||||
int err = errno;
|
||||
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
|
||||
if (err != EWOULDBLOCK && err != EAGAIN) {
|
||||
this->state_ = State::FAILED;
|
||||
HELPER_LOG("Socket write failed with errno %d", err);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
|
||||
return APIError::OK;
|
||||
} else {
|
||||
skip = static_cast<uint16_t>(sent);
|
||||
sent = 0; // Treat WOULD_BLOCK as zero bytes sent
|
||||
}
|
||||
}
|
||||
|
||||
// Full write completed (possible when called directly, not via write_raw_fast_buf_)
|
||||
if (sent == static_cast<ssize_t>(total_write_len))
|
||||
return APIError::OK;
|
||||
|
||||
// Queue unsent data into overflow buffer
|
||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
|
||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
|
||||
HELPER_LOG("Overflow buffer full, dropping connection");
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
|
||||
@@ -49,12 +49,17 @@ struct ReadPacketBuffer {
|
||||
};
|
||||
|
||||
// Packed message info structure to minimize memory usage
|
||||
// Note: message_type is uint8_t — all current protobuf message types fit in 8 bits.
|
||||
// The noise wire format encodes types as 16-bit, but the high byte is always 0.
|
||||
// If message types ever exceed 255, this and encrypt_noise_message_ must be updated.
|
||||
struct MessageInfo {
|
||||
uint16_t offset; // Offset in buffer where message starts
|
||||
uint16_t payload_size; // Size of the message payload
|
||||
uint8_t message_type; // Message type (0-255)
|
||||
uint8_t header_size; // Actual header size used (avoids recomputation in write path)
|
||||
|
||||
MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
|
||||
MessageInfo(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr)
|
||||
: offset(off), payload_size(size), message_type(type), header_size(hdr) {}
|
||||
};
|
||||
|
||||
enum class APIError : uint16_t {
|
||||
@@ -161,23 +166,39 @@ class APIFrameHelper {
|
||||
this->nodelay_counter_ = 0;
|
||||
}
|
||||
}
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
// Resize buffer to include footer space if needed (e.g. Noise MAC)
|
||||
if (frame_footer_size_)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
||||
MessageInfo msg{type, 0,
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
// Write multiple protobuf messages in a single operation
|
||||
// messages contains (message_type, offset, length) for each message in the buffer
|
||||
// The buffer contains all messages with appropriate padding before each
|
||||
// Write a single protobuf message - the hot path (87-100% of all writes).
|
||||
// Caller must ensure state is DATA before calling.
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf messages in a single batched operation.
|
||||
// Caller must ensure state is DATA and messages is not empty.
|
||||
// messages contains (message_type, offset, length) for each message in the buffer.
|
||||
// The buffer contains all messages with appropriate padding before each.
|
||||
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
|
||||
// Get the frame header padding required by this protocol
|
||||
// Get the maximum frame header padding required by this protocol (worst case)
|
||||
uint8_t frame_header_padding() const { return frame_header_padding_; }
|
||||
// Get the actual frame header size for a specific message.
|
||||
// For noise: always returns frame_header_padding_ (fixed 7-byte header).
|
||||
// For plaintext: computes actual size from varint lengths (3-6 bytes).
|
||||
// Distinguishes protocols via frame_footer_size_ (noise always has a non-zero MAC
|
||||
// footer, plaintext has footer=0). If a protocol with a plaintext footer is ever
|
||||
// added, this should become a virtual method.
|
||||
uint8_t frame_header_size(uint16_t payload_size, uint8_t message_type) const {
|
||||
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
||||
return this->frame_footer_size_
|
||||
? this->frame_header_padding_
|
||||
: static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
|
||||
#elif defined(USE_API_NOISE)
|
||||
return this->frame_header_padding_;
|
||||
#else // USE_API_PLAINTEXT only
|
||||
return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint8(message_type));
|
||||
#endif
|
||||
}
|
||||
// Get the frame footer size required by this protocol
|
||||
uint8_t frame_footer_size() const { return frame_footer_size_; }
|
||||
// Check if socket has data ready to read
|
||||
// Check if socket has buffered data ready to read.
|
||||
// Contract: callers must read until it would block (EAGAIN/EWOULDBLOCK)
|
||||
// or track that they stopped early and retry without this check.
|
||||
// See Socket::ready() for details.
|
||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||
// Release excess memory from internal buffers after initial sync
|
||||
void release_buffers() {
|
||||
@@ -196,18 +217,41 @@ class APIFrameHelper {
|
||||
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
|
||||
APIError drain_overflow_and_handle_errors_();
|
||||
|
||||
// Common implementation for writing raw data to socket
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
|
||||
// Sentinel values for the sent parameter in write_raw_ methods
|
||||
static constexpr ssize_t WRITE_FAILED = -1; // Fast path: write()/writev() returned -1
|
||||
static constexpr ssize_t WRITE_NOT_ATTEMPTED = -2; // Cold path: no write attempted yet
|
||||
|
||||
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
|
||||
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
|
||||
APIError check_socket_write_err_(int err) {
|
||||
if (err == EWOULDBLOCK || err == EAGAIN)
|
||||
return APIError::WOULD_BLOCK;
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
// Dispatch to write() or writev() based on iovec count
|
||||
inline ssize_t ESPHOME_ALWAYS_INLINE write_iov_to_socket_(const struct iovec *iov, int iovcnt) {
|
||||
return (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
}
|
||||
|
||||
// Inlined write methods — used by hot paths (write_protobuf_packet, write_protobuf_messages)
|
||||
// These inline the fast path (overflow empty + full write) and tail-call the out-of-line
|
||||
// slow path only on failure/partial write.
|
||||
inline APIError ESPHOME_ALWAYS_INLINE write_raw_fast_buf_(const void *data, uint16_t len) {
|
||||
if (this->overflow_buf_.empty()) [[likely]] {
|
||||
ssize_t sent = this->socket_->write(data, len);
|
||||
if (sent == static_cast<ssize_t>(len)) [[likely]] {
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
this->log_packet_sending_(data, len);
|
||||
#endif
|
||||
return APIError::OK;
|
||||
}
|
||||
// sent is -1 (WRITE_FAILED) or partial write count
|
||||
return this->write_raw_buf_(data, len, sent);
|
||||
}
|
||||
return this->write_raw_buf_(data, len, WRITE_NOT_ATTEMPTED);
|
||||
}
|
||||
// Out-of-line write paths: handle partial writes, errors, overflow buffering
|
||||
// sent: WRITE_NOT_ATTEMPTED (cold path), WRITE_FAILED (fast path write returned -1), or bytes sent (partial write)
|
||||
APIError write_raw_buf_(const void *data, uint16_t len, ssize_t sent = WRITE_NOT_ATTEMPTED);
|
||||
APIError write_raw_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
|
||||
ssize_t sent = WRITE_NOT_ATTEMPTED);
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
void log_packet_sending_(const void *data, uint16_t len);
|
||||
#endif
|
||||
|
||||
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
|
||||
|
||||
@@ -47,15 +47,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
||||
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#define LOG_PACKET_SENDING(data, len) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Sending raw: %s", \
|
||||
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
#endif
|
||||
|
||||
/// Convert a noise error code to a readable error
|
||||
@@ -244,132 +237,144 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
* If an error occurred, returns that error. Only returns OK if the transport is ready for data
|
||||
* traffic.
|
||||
*/
|
||||
// Split into per-state methods so the compiler doesn't allocate stack space
|
||||
// for all branches simultaneously. On RP2040 the core0 stack lives in a 4KB
|
||||
// scratch RAM bank; the Noise crypto path (curve25519) needs ~2KB+ of stack,
|
||||
// so every byte saved in the caller matters.
|
||||
APIError APINoiseFrameHelper::state_action_() {
|
||||
int err;
|
||||
APIError aerr;
|
||||
if (state_ == State::INITIALIZE) {
|
||||
HELPER_LOG("Bad state for method: %d", (int) state_);
|
||||
return APIError::BAD_STATE;
|
||||
switch (this->state_) {
|
||||
case State::INITIALIZE:
|
||||
HELPER_LOG("Bad state for method: %d", (int) this->state_);
|
||||
return APIError::BAD_STATE;
|
||||
case State::CLIENT_HELLO:
|
||||
return this->state_action_client_hello_();
|
||||
case State::SERVER_HELLO:
|
||||
return this->state_action_server_hello_();
|
||||
case State::HANDSHAKE:
|
||||
return this->state_action_handshake_();
|
||||
case State::CLOSED:
|
||||
case State::FAILED:
|
||||
return APIError::BAD_STATE;
|
||||
default:
|
||||
return APIError::OK;
|
||||
}
|
||||
if (state_ == State::CLIENT_HELLO) {
|
||||
// waiting for client hello
|
||||
aerr = this->try_read_frame_();
|
||||
if (aerr != APIError::OK) {
|
||||
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();
|
||||
size_t rx_size = this->rx_buf_.size();
|
||||
this->prologue_.resize(old_size + 2 + rx_size);
|
||||
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
|
||||
this->prologue_[old_size + 1] = (uint8_t) rx_size;
|
||||
if (rx_size > 0) {
|
||||
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
|
||||
}
|
||||
|
||||
state_ = State::SERVER_HELLO;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_client_hello_() {
|
||||
// waiting for client hello
|
||||
APIError aerr = this->try_read_frame_();
|
||||
if (aerr != APIError::OK) {
|
||||
return handle_handshake_frame_error_(aerr);
|
||||
}
|
||||
if (state_ == State::SERVER_HELLO) {
|
||||
// send server hello
|
||||
const auto &name = App.get_name();
|
||||
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||
get_mac_address_into_buffer(mac);
|
||||
|
||||
// Calculate positions and sizes
|
||||
size_t name_len = name.size() + 1; // including null terminator
|
||||
size_t name_offset = 1;
|
||||
size_t mac_offset = name_offset + name_len;
|
||||
size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE;
|
||||
|
||||
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
|
||||
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
|
||||
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
|
||||
uint8_t msg[max_msg_size];
|
||||
|
||||
// chosen proto
|
||||
msg[0] = 0x01;
|
||||
|
||||
// node name, terminated by null byte
|
||||
std::memcpy(msg + name_offset, name.c_str(), name_len);
|
||||
// node mac, terminated by null byte
|
||||
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
|
||||
|
||||
aerr = write_frame_(msg, total_size);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// start handshake
|
||||
aerr = init_handshake_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
state_ = State::HANDSHAKE;
|
||||
// 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();
|
||||
size_t rx_size = this->rx_buf_.size();
|
||||
this->prologue_.resize(old_size + 2 + rx_size);
|
||||
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
|
||||
this->prologue_[old_size + 1] = (uint8_t) rx_size;
|
||||
if (rx_size > 0) {
|
||||
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
|
||||
}
|
||||
if (state_ == State::HANDSHAKE) {
|
||||
int action = noise_handshakestate_get_action(handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||
// waiting for handshake msg
|
||||
aerr = this->try_read_frame_();
|
||||
if (aerr != APIError::OK) {
|
||||
return handle_handshake_frame_error_(aerr);
|
||||
}
|
||||
|
||||
if (this->rx_buf_.empty()) {
|
||||
send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
} else if (this->rx_buf_[0] != 0x00) {
|
||||
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
|
||||
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
}
|
||||
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
||||
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
|
||||
if (err != 0) {
|
||||
// Special handling for MAC failure
|
||||
send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
|
||||
: LOG_STR("Handshake error"));
|
||||
return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
|
||||
APIError::HANDSHAKESTATE_READ_FAILED);
|
||||
}
|
||||
|
||||
aerr = check_handshake_finished_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
|
||||
uint8_t buffer[65];
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
|
||||
|
||||
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
|
||||
APIError aerr_write = handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
|
||||
APIError::HANDSHAKESTATE_WRITE_FAILED);
|
||||
if (aerr_write != APIError::OK)
|
||||
return aerr_write;
|
||||
buffer[0] = 0x00; // success
|
||||
|
||||
aerr = write_frame_(buffer, mbuf.size + 1);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
aerr = check_handshake_finished_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
} else {
|
||||
// bad state for action
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
}
|
||||
if (state_ == State::CLOSED || state_ == State::FAILED) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
state_ = State::SERVER_HELLO;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_server_hello_() {
|
||||
// send server hello
|
||||
const auto &name = App.get_name();
|
||||
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||
get_mac_address_into_buffer(mac);
|
||||
|
||||
// Calculate positions and sizes
|
||||
size_t name_len = name.size() + 1; // including null terminator
|
||||
size_t name_offset = 1;
|
||||
size_t mac_offset = name_offset + name_len;
|
||||
size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE;
|
||||
|
||||
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
|
||||
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
|
||||
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
|
||||
uint8_t msg[max_msg_size];
|
||||
|
||||
// chosen proto
|
||||
msg[0] = 0x01;
|
||||
|
||||
// node name, terminated by null byte
|
||||
std::memcpy(msg + name_offset, name.c_str(), name_len);
|
||||
// node mac, terminated by null byte
|
||||
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
|
||||
|
||||
APIError aerr = write_frame_(msg, total_size);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// start handshake
|
||||
aerr = init_handshake_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
state_ = State::HANDSHAKE;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_() {
|
||||
int action = noise_handshakestate_get_action(this->handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||
return this->state_action_handshake_read_();
|
||||
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
|
||||
return this->state_action_handshake_write_();
|
||||
}
|
||||
// bad state for action
|
||||
this->state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
APIError aerr = this->try_read_frame_();
|
||||
if (aerr != APIError::OK) {
|
||||
return this->handle_handshake_frame_error_(aerr);
|
||||
}
|
||||
|
||||
if (this->rx_buf_.empty()) {
|
||||
this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
} else if (this->rx_buf_[0] != 0x00) {
|
||||
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
|
||||
this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
|
||||
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
|
||||
}
|
||||
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
||||
int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
|
||||
if (err != 0) {
|
||||
// Special handling for MAC failure
|
||||
this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
|
||||
: LOG_STR("Handshake error"));
|
||||
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
|
||||
APIError::HANDSHAKESTATE_READ_FAILED);
|
||||
}
|
||||
|
||||
return this->check_handshake_finished_();
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_write_() {
|
||||
uint8_t buffer[65];
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
|
||||
|
||||
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
|
||||
APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
|
||||
APIError::HANDSHAKESTATE_WRITE_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
buffer[0] = 0x00; // success
|
||||
|
||||
aerr = this->write_frame_(buffer, mbuf.size + 1);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return this->check_handshake_finished_();
|
||||
}
|
||||
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
|
||||
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
|
||||
uint8_t data[32];
|
||||
@@ -452,65 +457,83 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = type;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
// Encrypt a single noise message in place and return the encrypted frame length.
|
||||
// Returns APIError::OK on success.
|
||||
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
|
||||
uint16_t &encrypted_len_out) {
|
||||
// Write noise header
|
||||
buf_start[0] = 0x01; // indicator
|
||||
// buf_start[1], buf_start[2] to be set after encryption
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
constexpr uint8_t msg_offset = 3;
|
||||
buf_start[msg_offset] = static_cast<uint8_t>(message_type >> 8); // type high byte
|
||||
buf_start[msg_offset + 1] = static_cast<uint8_t>(message_type); // type low byte
|
||||
buf_start[msg_offset + 2] = static_cast<uint8_t>(payload_size >> 8); // data_len high byte
|
||||
buf_start[msg_offset + 3] = static_cast<uint8_t>(payload_size); // data_len low byte
|
||||
// payload data is already in the buffer starting at offset + 7
|
||||
|
||||
// Encrypt the message in place
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + payload_size, 4 + payload_size + this->frame_footer_size_);
|
||||
|
||||
int err = noise_cipherstate_encrypt(this->send_cipher_, &mbuf);
|
||||
APIError aerr =
|
||||
this->handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
if (messages.empty()) {
|
||||
return APIError::OK;
|
||||
}
|
||||
// Fill in the encrypted size
|
||||
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
||||
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
||||
|
||||
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
|
||||
// Resize buffer to include footer space for Noise MAC
|
||||
if (this->frame_footer_size_)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + this->frame_footer_size_);
|
||||
|
||||
uint16_t payload_size =
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING - this->frame_footer_size_);
|
||||
uint8_t *buf_start = buffer.get_buffer()->data();
|
||||
uint16_t encrypted_len;
|
||||
APIError aerr = this->encrypt_noise_message_(buf_start, payload_size, type, encrypted_len);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return this->write_raw_fast_buf_(buf_start, encrypted_len);
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
assert(!messages.empty());
|
||||
#endif
|
||||
|
||||
// Noise messages are already contiguous in the buffer:
|
||||
// HEADER_PADDING (7) exactly matches the fixed header size, and
|
||||
// footer space (16) is consumed by the encryption MAC.
|
||||
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||
|
||||
// Stack-allocated iovec array - no heap allocation
|
||||
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
|
||||
uint8_t *write_start = buffer_data + messages[0].offset;
|
||||
uint16_t total_write_len = 0;
|
||||
|
||||
// We need to encrypt each message in place
|
||||
for (const auto &msg : messages) {
|
||||
// The buffer already has padding at offset
|
||||
uint8_t *buf_start = buffer_data + msg.offset;
|
||||
|
||||
// Write noise header
|
||||
buf_start[0] = 0x01; // indicator
|
||||
// buf_start[1], buf_start[2] to be set after encryption
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
constexpr uint8_t msg_offset = 3;
|
||||
buf_start[msg_offset] = static_cast<uint8_t>(msg.message_type >> 8); // type high byte
|
||||
buf_start[msg_offset + 1] = static_cast<uint8_t>(msg.message_type); // type low byte
|
||||
buf_start[msg_offset + 2] = static_cast<uint8_t>(msg.payload_size >> 8); // data_len high byte
|
||||
buf_start[msg_offset + 3] = static_cast<uint8_t>(msg.payload_size); // data_len low byte
|
||||
// payload data is already in the buffer starting at offset + 7
|
||||
|
||||
// Make sure we have space for MAC
|
||||
// The buffer should already have been sized appropriately
|
||||
|
||||
// Encrypt the message in place
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + msg.payload_size,
|
||||
4 + msg.payload_size + frame_footer_size_);
|
||||
|
||||
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
|
||||
APIError aerr =
|
||||
handle_noise_error_(err, LOG_STR("noise_cipherstate_encrypt"), APIError::CIPHERSTATE_ENCRYPT_FAILED);
|
||||
uint16_t encrypted_len;
|
||||
APIError aerr = this->encrypt_noise_message_(buf_start, msg.payload_size, msg.message_type, encrypted_len);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
// Fill in the encrypted size
|
||||
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
||||
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
||||
|
||||
// Add iovec for this encrypted message
|
||||
size_t msg_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||
iovs.push_back({buf_start, msg_len});
|
||||
total_write_len += msg_len;
|
||||
total_write_len += encrypted_len;
|
||||
}
|
||||
|
||||
// Send all encrypted messages in one writev call
|
||||
return this->write_raw_(iovs.data(), iovs.size(), total_write_len);
|
||||
return this->write_raw_fast_buf_(write_start, total_write_len);
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
@@ -519,16 +542,16 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
header[1] = (uint8_t) (len >> 8);
|
||||
header[2] = (uint8_t) len;
|
||||
|
||||
if (len == 0) {
|
||||
return this->write_raw_buf_(header, 3);
|
||||
}
|
||||
struct iovec iov[2];
|
||||
iov[0].iov_base = header;
|
||||
iov[0].iov_len = 3;
|
||||
if (len == 0) {
|
||||
return this->write_raw_(iov, 1, 3); // Just header
|
||||
}
|
||||
iov[1].iov_base = const_cast<uint8_t *>(data);
|
||||
iov[1].iov_len = len;
|
||||
|
||||
return this->write_raw_(iov, 2, 3 + len); // Header + data
|
||||
return this->write_raw_iov_(iov, 2, 3 + len);
|
||||
}
|
||||
|
||||
/** Initiate the data structures for the handshake.
|
||||
@@ -594,7 +617,7 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
|
||||
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
|
||||
|
||||
HELPER_LOG("Handshake complete!");
|
||||
noise_handshakestate_free(handshake_);
|
||||
|
||||
@@ -9,25 +9,35 @@ namespace esphome::api {
|
||||
|
||||
class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
// Noise header structure:
|
||||
// Pos 0: indicator (0x01)
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
|
||||
// Pos 7+: actual payload data
|
||||
static constexpr uint8_t HEADER_PADDING = 1 + 2 + 2 + 2; // indicator + size + type + data_len
|
||||
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
|
||||
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
|
||||
// Noise header structure:
|
||||
// Pos 0: indicator (0x01)
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
// Pos 3-6: encrypted type (16-bit) + data_len (16-bit)
|
||||
// Pos 7+: actual payload data
|
||||
frame_header_padding_ = 7;
|
||||
frame_header_padding_ = HEADER_PADDING;
|
||||
}
|
||||
~APINoiseFrameHelper() override;
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
APIError state_action_();
|
||||
APIError state_action_client_hello_();
|
||||
APIError state_action_server_hello_();
|
||||
APIError state_action_handshake_();
|
||||
APIError state_action_handshake_read_();
|
||||
APIError state_action_handshake_write_();
|
||||
APIError try_read_frame_();
|
||||
APIError write_frame_(const uint8_t *data, uint16_t len);
|
||||
APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint8_t message_type,
|
||||
uint16_t &encrypted_len_out);
|
||||
APIError init_handshake_();
|
||||
APIError check_handshake_finished_();
|
||||
void send_explicit_handshake_reject_(const LogString *reason);
|
||||
|
||||
@@ -39,15 +39,8 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
||||
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#define LOG_PACKET_SENDING(data, len) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Sending raw: %s", \
|
||||
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
#endif
|
||||
|
||||
/// Initialize the frame helper, returns OK if successful.
|
||||
@@ -205,7 +198,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
// Make sure to tell the remote that we don't
|
||||
// understand the indicator byte so it knows
|
||||
// we do not support it.
|
||||
struct iovec iov[1];
|
||||
// The \x00 first byte is the marker for plaintext.
|
||||
//
|
||||
// The remote will know how to handle the indicator byte,
|
||||
@@ -220,14 +212,12 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
"Bad indicator byte";
|
||||
char msg[INDICATOR_MSG_SIZE];
|
||||
memcpy_P(msg, MSG_PROGMEM, INDICATOR_MSG_SIZE);
|
||||
iov[0].iov_base = (void *) msg;
|
||||
this->write_raw_buf_(msg, INDICATOR_MSG_SIZE);
|
||||
#else
|
||||
static const char MSG[] = "\x00"
|
||||
"Bad indicator byte";
|
||||
iov[0].iov_base = (void *) MSG;
|
||||
this->write_raw_buf_(MSG, INDICATOR_MSG_SIZE);
|
||||
#endif
|
||||
iov[0].iov_len = INDICATOR_MSG_SIZE;
|
||||
this->write_raw_(iov, 1, INDICATOR_MSG_SIZE);
|
||||
}
|
||||
return aerr;
|
||||
}
|
||||
@@ -237,73 +227,101 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = this->rx_header_parsed_type_;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
// Encode a 16-bit varint (1-3 bytes) using pre-computed length.
|
||||
ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_t varint_len, uint8_t *p) {
|
||||
if (varint_len >= 2) {
|
||||
*p++ = static_cast<uint8_t>(value | 0x80);
|
||||
value >>= 7;
|
||||
if (varint_len == 3) {
|
||||
*p++ = static_cast<uint8_t>(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
}
|
||||
*p = static_cast<uint8_t>(value);
|
||||
}
|
||||
|
||||
// Encode an 8-bit varint (1-2 bytes) using pre-computed length.
|
||||
ESPHOME_ALWAYS_INLINE static inline void encode_varint_8(uint8_t value, uint8_t varint_len, uint8_t *p) {
|
||||
if (varint_len == 2) {
|
||||
*p++ = static_cast<uint8_t>(value | 0x80);
|
||||
*p = static_cast<uint8_t>(value >> 7);
|
||||
} else {
|
||||
*p = value;
|
||||
}
|
||||
}
|
||||
|
||||
// Write plaintext header into pre-allocated padding before payload.
|
||||
// padding_size: bytes reserved before payload (HEADER_PADDING for first/single msg,
|
||||
// actual header size for contiguous batch messages).
|
||||
// Returns the total header length (indicator + varints).
|
||||
ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_start, uint16_t payload_size,
|
||||
uint8_t message_type, uint8_t padding_size) {
|
||||
uint8_t size_varint_len = ProtoSize::varint16(payload_size);
|
||||
uint8_t type_varint_len = ProtoSize::varint8(message_type);
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// The header is right-justified within the padding so it sits immediately before payload.
|
||||
//
|
||||
// Single/first message (padding_size = HEADER_PADDING = 6):
|
||||
// Example (small, header=3): [0-2] unused | [3] 0x00 | [4] size | [5] type | [6...] payload
|
||||
// Example (medium, header=4): [0-1] unused | [2] 0x00 | [3-4] size | [5] type | [6...] payload
|
||||
// Example (large, header=6): [0] 0x00 | [1-3] size | [4-5] type | [6...] payload
|
||||
//
|
||||
// Batch messages 2+ (padding_size = actual header size, no unused bytes):
|
||||
// Example (small, header=3): [0] 0x00 | [1] size | [2] type | [3...] payload
|
||||
// Example (medium, header=4): [0] 0x00 | [1-2] size | [3] type | [4...] payload
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(padding_size >= total_header_len);
|
||||
#endif
|
||||
uint32_t header_offset = padding_size - total_header_len;
|
||||
|
||||
// Write the plaintext header
|
||||
buf_start[header_offset] = 0x00; // indicator
|
||||
|
||||
// Encode varints directly into buffer using pre-computed lengths
|
||||
encode_varint_16(payload_size, size_varint_len, buf_start + header_offset + 1);
|
||||
encode_varint_8(message_type, type_varint_len, buf_start + header_offset + 1 + size_varint_len);
|
||||
|
||||
return total_header_len;
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
|
||||
uint16_t payload_size = static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING);
|
||||
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||
uint8_t header_len = write_plaintext_header(buffer_data, payload_size, type, HEADER_PADDING);
|
||||
return this->write_raw_fast_buf_(buffer_data + HEADER_PADDING - header_len,
|
||||
static_cast<uint16_t>(header_len + payload_size));
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
|
||||
std::span<const MessageInfo> messages) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
if (messages.empty()) {
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
assert(!messages.empty());
|
||||
#endif
|
||||
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||
|
||||
// Stack-allocated iovec array - no heap allocation
|
||||
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
|
||||
uint16_t total_write_len = 0;
|
||||
// First message has max padding (header_size = HEADER_PADDING), may have unused leading bytes.
|
||||
// Subsequent messages were encoded with exact header sizes (header_size = actual header len).
|
||||
// write_plaintext_header right-justifies the header within header_size bytes of padding.
|
||||
const auto &first = messages[0];
|
||||
uint8_t *first_start = buffer_data + first.offset;
|
||||
uint8_t header_len = write_plaintext_header(first_start, first.payload_size, first.message_type, HEADER_PADDING);
|
||||
uint8_t *write_start = first_start + HEADER_PADDING - header_len;
|
||||
uint16_t total_len = header_len + first.payload_size;
|
||||
|
||||
for (const auto &msg : messages) {
|
||||
// Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead
|
||||
uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE
|
||||
? 1
|
||||
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
|
||||
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// Calculate where to start writing the header
|
||||
// The header starts at the latest possible position to minimize unused padding
|
||||
//
|
||||
// Example 1 (small values): total_header_len = 3, header_offset = 6 - 3 = 3
|
||||
// [0-2] - Unused padding
|
||||
// [3] - 0x00 indicator byte
|
||||
// [4] - Payload size varint (1 byte, for sizes 0-127)
|
||||
// [5] - Message type varint (1 byte, for types 0-127)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// Example 2 (medium values): total_header_len = 4, header_offset = 6 - 4 = 2
|
||||
// [0-1] - Unused padding
|
||||
// [2] - 0x00 indicator byte
|
||||
// [3-4] - Payload size varint (2 bytes, for sizes 128-16383)
|
||||
// [5] - Message type varint (1 byte, for types 0-127)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
|
||||
// [0] - 0x00 indicator byte
|
||||
// [1-3] - Payload size varint (3 bytes, for sizes 16384-65535)
|
||||
// [4-5] - Message type varint (2 bytes, for types 128-16383)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// The message starts at offset + frame_header_padding_
|
||||
// So we write the header starting at offset + frame_header_padding_ - total_header_len
|
||||
uint8_t *buf_start = buffer_data + msg.offset;
|
||||
uint32_t header_offset = frame_header_padding_ - total_header_len;
|
||||
|
||||
// Write the plaintext header
|
||||
buf_start[header_offset] = 0x00; // indicator
|
||||
|
||||
// Encode varints directly into buffer
|
||||
encode_varint_to_buffer(msg.payload_size, buf_start + header_offset + 1);
|
||||
encode_varint_to_buffer(msg.message_type, buf_start + header_offset + 1 + size_varint_len);
|
||||
|
||||
// Add iovec for this message (header + payload)
|
||||
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
|
||||
iovs.push_back({buf_start + header_offset, msg_len});
|
||||
total_write_len += msg_len;
|
||||
for (size_t i = 1; i < messages.size(); i++) {
|
||||
const auto &msg = messages[i];
|
||||
header_len = write_plaintext_header(buffer_data + msg.offset, msg.payload_size, msg.message_type, msg.header_size);
|
||||
total_len += header_len + msg.payload_size;
|
||||
}
|
||||
|
||||
// Send all messages in one writev call
|
||||
return write_raw_(iovs.data(), iovs.size(), total_write_len);
|
||||
return this->write_raw_fast_buf_(write_start, total_len);
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -7,18 +7,21 @@ namespace esphome::api {
|
||||
|
||||
class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
// Plaintext header structure (worst case):
|
||||
// Pos 0: indicator (0x00)
|
||||
// Pos 1-3: payload size varint (up to 3 bytes)
|
||||
// Pos 4-5: message type varint (up to 2 bytes)
|
||||
// Pos 6+: actual payload data
|
||||
static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint
|
||||
|
||||
explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
|
||||
// Plaintext header structure (worst case):
|
||||
// Pos 0: indicator (0x00)
|
||||
// Pos 1-3: payload size varint (up to 3 bytes)
|
||||
// Pos 4-5: message type varint (up to 2 bytes)
|
||||
// Pos 6+: actual payload data
|
||||
frame_header_padding_ = 6;
|
||||
frame_header_padding_ = HEADER_PADDING;
|
||||
}
|
||||
~APIPlaintextFrameHelper() override = default;
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -22,6 +22,8 @@ extend google.protobuf.MessageOptions {
|
||||
optional bool log = 1039 [default=true];
|
||||
optional bool no_delay = 1040 [default=false];
|
||||
optional string base_class = 1041;
|
||||
optional bool inline_encode = 1042 [default=false];
|
||||
optional bool speed_optimized = 1043 [default=false];
|
||||
}
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
@@ -96,4 +98,16 @@ extend google.protobuf.FieldOptions {
|
||||
// variant of the calc_ method. Use on fields that are almost always non-default
|
||||
// to eliminate dead branches on hot paths.
|
||||
optional bool force = 50016 [default=false];
|
||||
|
||||
// max_value: Maximum value a field can have.
|
||||
// When max_value < 128, the code generator emits constant-size calculations
|
||||
// and direct byte writes instead of varint branching, since the encoded varint
|
||||
// is guaranteed to be 1 byte.
|
||||
optional uint32 max_value = 50017;
|
||||
|
||||
// max_data_length: Maximum length of a string or bytes field.
|
||||
// When max_data_length < 128, the code generator emits constant-size
|
||||
// length varint calculations and direct byte writes, since the length
|
||||
// varint is guaranteed to be 1 byte.
|
||||
optional uint32 max_data_length = 50018;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
// This file was automatically generated with a tool.
|
||||
// See script/api_protobuf/api_protobuf.py
|
||||
#include "api_pb2_service.h"
|
||||
#include "api_connection.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::api {
|
||||
@@ -8,8 +9,8 @@ namespace esphome::api {
|
||||
static const char *const TAG = "api.service";
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void APIServerConnectionBase::log_send_message_(const char *name, const char *dump) {
|
||||
ESP_LOGVV(TAG, "send_message %s: %s", name, dump);
|
||||
void APIServerConnectionBase::log_send_message_(const LogString *name, const char *dump) {
|
||||
ESP_LOGVV(TAG, "send_message %s: %s", LOG_STR_ARG(name), dump);
|
||||
}
|
||||
void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) {
|
||||
DumpBuffer dump_buf;
|
||||
@@ -20,7 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||
|
||||
@@ -8,238 +8,234 @@
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
class APIServerConnectionBase : public ProtoService {
|
||||
class APIServerConnectionBase {
|
||||
public:
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
protected:
|
||||
void log_send_message_(const char *name, const char *dump);
|
||||
void log_send_message_(const LogString *name, const char *dump);
|
||||
void log_receive_message_(const LogString *name, const ProtoMessage &msg);
|
||||
void log_receive_message_(const LogString *name);
|
||||
|
||||
public:
|
||||
#endif
|
||||
|
||||
virtual void on_hello_request(const HelloRequest &value){};
|
||||
void on_hello_request(const HelloRequest &value){};
|
||||
|
||||
virtual void on_disconnect_request(){};
|
||||
virtual void on_disconnect_response(){};
|
||||
virtual void on_ping_request(){};
|
||||
virtual void on_ping_response(){};
|
||||
virtual void on_device_info_request(){};
|
||||
void on_disconnect_request(){};
|
||||
void on_disconnect_response(){};
|
||||
void on_ping_request(){};
|
||||
void on_ping_response(){};
|
||||
void on_device_info_request(){};
|
||||
|
||||
virtual void on_list_entities_request(){};
|
||||
void on_list_entities_request(){};
|
||||
|
||||
virtual void on_subscribe_states_request(){};
|
||||
void on_subscribe_states_request(){};
|
||||
|
||||
#ifdef USE_COVER
|
||||
virtual void on_cover_command_request(const CoverCommandRequest &value){};
|
||||
void on_cover_command_request(const CoverCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_FAN
|
||||
virtual void on_fan_command_request(const FanCommandRequest &value){};
|
||||
void on_fan_command_request(const FanCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
virtual void on_light_command_request(const LightCommandRequest &value){};
|
||||
void on_light_command_request(const LightCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SWITCH
|
||||
virtual void on_switch_command_request(const SwitchCommandRequest &value){};
|
||||
void on_switch_command_request(const SwitchCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
virtual void on_subscribe_logs_request(const SubscribeLogsRequest &value){};
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &value){};
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
virtual void on_subscribe_homeassistant_services_request(){};
|
||||
void on_subscribe_homeassistant_services_request(){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
|
||||
void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void on_subscribe_home_assistant_states_request(){};
|
||||
void on_subscribe_home_assistant_states_request(){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
|
||||
void on_home_assistant_state_response(const HomeAssistantStateResponse &value){};
|
||||
#endif
|
||||
|
||||
virtual void on_get_time_response(const GetTimeResponse &value){};
|
||||
void on_get_time_response(const GetTimeResponse &value){};
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
|
||||
void on_execute_service_request(const ExecuteServiceRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
virtual void on_camera_image_request(const CameraImageRequest &value){};
|
||||
void on_camera_image_request(const CameraImageRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
virtual void on_climate_command_request(const ClimateCommandRequest &value){};
|
||||
void on_climate_command_request(const ClimateCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_NUMBER
|
||||
virtual void on_number_command_request(const NumberCommandRequest &value){};
|
||||
void on_number_command_request(const NumberCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
virtual void on_select_command_request(const SelectCommandRequest &value){};
|
||||
void on_select_command_request(const SelectCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SIREN
|
||||
virtual void on_siren_command_request(const SirenCommandRequest &value){};
|
||||
void on_siren_command_request(const SirenCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_LOCK
|
||||
virtual void on_lock_command_request(const LockCommandRequest &value){};
|
||||
void on_lock_command_request(const LockCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BUTTON
|
||||
virtual void on_button_command_request(const ButtonCommandRequest &value){};
|
||||
void on_button_command_request(const ButtonCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
virtual void on_media_player_command_request(const MediaPlayerCommandRequest &value){};
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_subscribe_bluetooth_le_advertisements_request(
|
||||
const SubscribeBluetoothLEAdvertisementsRequest &value){};
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){};
|
||||
void on_bluetooth_device_request(const BluetoothDeviceRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){};
|
||||
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){};
|
||||
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){};
|
||||
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){};
|
||||
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){};
|
||||
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){};
|
||||
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_subscribe_bluetooth_connections_free_request(){};
|
||||
void on_subscribe_bluetooth_connections_free_request(){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_unsubscribe_bluetooth_le_advertisements_request(){};
|
||||
void on_unsubscribe_bluetooth_le_advertisements_request(){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){};
|
||||
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){};
|
||||
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void on_voice_assistant_response(const VoiceAssistantResponse &value){};
|
||||
void on_voice_assistant_response(const VoiceAssistantResponse &value){};
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){};
|
||||
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){};
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void on_voice_assistant_audio(const VoiceAssistantAudio &value){};
|
||||
void on_voice_assistant_audio(const VoiceAssistantAudio &value){};
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){};
|
||||
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &value){};
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){};
|
||||
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){};
|
||||
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){};
|
||||
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){};
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_TEXT
|
||||
virtual void on_text_command_request(const TextCommandRequest &value){};
|
||||
void on_text_command_request(const TextCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATE
|
||||
virtual void on_date_command_request(const DateCommandRequest &value){};
|
||||
void on_date_command_request(const DateCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_TIME
|
||||
virtual void on_time_command_request(const TimeCommandRequest &value){};
|
||||
void on_time_command_request(const TimeCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_VALVE
|
||||
virtual void on_valve_command_request(const ValveCommandRequest &value){};
|
||||
void on_valve_command_request(const ValveCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
virtual void on_date_time_command_request(const DateTimeCommandRequest &value){};
|
||||
void on_date_time_command_request(const DateTimeCommandRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
virtual void on_update_command_request(const UpdateCommandRequest &value){};
|
||||
void on_update_command_request(const UpdateCommandRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){};
|
||||
void on_z_wave_proxy_frame(const ZWaveProxyFrame &value){};
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){};
|
||||
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){};
|
||||
void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){};
|
||||
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){};
|
||||
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_request(const SerialProxyRequest &value){};
|
||||
void on_serial_proxy_request(const SerialProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -46,10 +46,8 @@ void APIServer::setup() {
|
||||
|
||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
||||
// Only load saved PSK if not set from YAML
|
||||
SavedNoisePsk noise_pref_saved{};
|
||||
if (this->noise_pref_.load(&noise_pref_saved)) {
|
||||
if (this->load_and_apply_noise_psk_()) {
|
||||
ESP_LOGD(TAG, "Loaded saved Noise PSK");
|
||||
this->set_noise_psk(noise_pref_saved.psk);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
@@ -110,7 +108,7 @@ void APIServer::setup() {
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
// Set warning status if reboot timeout is enabled
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
this->status_set_warning(LOG_STR("waiting for client connection"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +118,7 @@ void APIServer::loop() {
|
||||
this->accept_new_connections_();
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
if (this->api_connection_count_ == 0) {
|
||||
// Check reboot timeout - done in loop to avoid scheduler heap churn
|
||||
// (cancelled scheduler items sit in heap memory until their scheduled time)
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
@@ -137,15 +135,15 @@ void APIServer::loop() {
|
||||
// Check network connectivity once for all clients
|
||||
if (!network::is_connected()) {
|
||||
// Network is down - disconnect all clients
|
||||
for (auto &client : this->clients_) {
|
||||
for (auto &client : this->active_clients()) {
|
||||
client->on_fatal_error();
|
||||
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
|
||||
}
|
||||
// Continue to process and clean up the clients below
|
||||
}
|
||||
|
||||
size_t client_index = 0;
|
||||
while (client_index < this->clients_.size()) {
|
||||
uint8_t client_index = 0;
|
||||
while (client_index < this->api_connection_count_) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
// Common case: process active client
|
||||
@@ -163,7 +161,7 @@ void APIServer::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::remove_client_(size_t client_index) {
|
||||
void APIServer::remove_client_(uint8_t client_index) {
|
||||
auto &client = this->clients_[client_index];
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
@@ -181,15 +179,18 @@ void APIServer::remove_client_(size_t client_index) {
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
// Swap-and-reset: move the removed client to the trailing slot and null it out so slots
|
||||
// [api_connection_count_, N) remain nullptr.
|
||||
const uint8_t last_index = this->api_connection_count_ - 1;
|
||||
if (client_index < last_index) {
|
||||
std::swap(this->clients_[client_index], this->clients_[last_index]);
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
this->clients_[last_index].reset();
|
||||
this->api_connection_count_--;
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning(LOG_STR("waiting for client connection"));
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
@@ -212,8 +213,8 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
@@ -222,11 +223,11 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
this->clients_[this->api_connection_count_++].reset(conn);
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
@@ -239,7 +240,7 @@ void APIServer::dump_config() {
|
||||
" Address: %s:%u\n"
|
||||
" Listen backlog: %u\n"
|
||||
" Max connections: %u",
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
|
||||
#ifdef USE_API_NOISE
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
|
||||
if (!this->noise_ctx_.has_psk()) {
|
||||
@@ -257,7 +258,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
if (obj->is_internal()) \
|
||||
return; \
|
||||
for (auto &c : this->clients_) { \
|
||||
for (auto &c : this->active_clients()) { \
|
||||
if (c->flags_.state_subscription) \
|
||||
c->send_##entity_name##_state(obj); \
|
||||
} \
|
||||
@@ -339,7 +340,7 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
|
||||
void APIServer::on_event(event::Event *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (c->flags_.state_subscription)
|
||||
c->send_event(obj);
|
||||
}
|
||||
@@ -351,7 +352,7 @@ void APIServer::on_event(event::Event *obj) {
|
||||
void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (c->flags_.state_subscription)
|
||||
c->send_update_state(obj);
|
||||
}
|
||||
@@ -362,7 +363,7 @@ void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
// We could add code to manage a second subscription type, but, since this message type is
|
||||
// very infrequent and small, we simply send it to all clients
|
||||
for (auto &c : this->clients_)
|
||||
for (auto &c : this->active_clients())
|
||||
c->send_message(msg);
|
||||
}
|
||||
#endif
|
||||
@@ -377,7 +378,7 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
|
||||
resp.key = key;
|
||||
resp.timings = timings;
|
||||
|
||||
for (auto &c : this->clients_)
|
||||
for (auto &c : this->active_clients())
|
||||
c->send_infrared_rf_receive_event(resp);
|
||||
}
|
||||
#endif
|
||||
@@ -394,7 +395,7 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
for (auto &client : this->clients_) {
|
||||
for (auto &client : this->active_clients()) {
|
||||
client->send_homeassistant_action(call);
|
||||
}
|
||||
}
|
||||
@@ -514,7 +515,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
|
||||
const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) {
|
||||
const LogString *fail_log_msg, bool make_active) {
|
||||
if (!this->noise_pref_.save(&new_psk)) {
|
||||
ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
|
||||
return false;
|
||||
@@ -526,10 +527,15 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
|
||||
if (make_active) {
|
||||
this->set_timeout(100, [this, active_psk]() {
|
||||
this->set_timeout(100, [this]() {
|
||||
// Re-read the PSK from preferences rather than capturing the 32-byte array
|
||||
// in the lambda (which would exceed std::function SBO and heap-allocate).
|
||||
if (!this->load_and_apply_noise_psk_()) {
|
||||
ESP_LOGW(TAG, "Failed to load saved PSK for activation");
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
|
||||
this->set_noise_psk(active_psk);
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
DisconnectRequest req;
|
||||
c->send_message(req);
|
||||
}
|
||||
@@ -538,6 +544,14 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
|
||||
return true;
|
||||
}
|
||||
|
||||
bool APIServer::load_and_apply_noise_psk_() {
|
||||
SavedNoisePsk saved{};
|
||||
if (!this->noise_pref_.load(&saved))
|
||||
return false;
|
||||
this->set_noise_psk(saved.psk);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
|
||||
// When PSK is set from YAML, this function should never be called
|
||||
@@ -552,7 +566,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
}
|
||||
|
||||
SavedNoisePsk new_saved_psk{psk};
|
||||
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk,
|
||||
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
|
||||
make_active);
|
||||
#endif
|
||||
}
|
||||
@@ -564,8 +578,7 @@ bool APIServer::clear_noise_psk(bool make_active) {
|
||||
return false;
|
||||
#else
|
||||
SavedNoisePsk empty_psk{};
|
||||
psk_t empty{};
|
||||
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty,
|
||||
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
|
||||
make_active);
|
||||
#endif
|
||||
}
|
||||
@@ -573,7 +586,7 @@ bool APIServer::clear_noise_psk(bool make_active) {
|
||||
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void APIServer::request_time() {
|
||||
for (auto &client : this->clients_) {
|
||||
for (auto &client : this->active_clients()) {
|
||||
if (!client->flags_.remove && client->is_authenticated()) {
|
||||
client->send_time_request();
|
||||
return; // Only request from one client to avoid clock conflicts
|
||||
@@ -583,8 +596,8 @@ void APIServer::request_time() {
|
||||
#endif
|
||||
|
||||
bool APIServer::is_connected_with_state_subscription() const {
|
||||
for (const auto &client : this->clients_) {
|
||||
if (client->flags_.state_subscription) {
|
||||
for (uint8_t i = 0; i < this->api_connection_count_; i++) {
|
||||
if (this->clients_[i]->flags_.state_subscription) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -599,7 +612,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
|
||||
// we would be filling a buffer we are trying to clear
|
||||
return;
|
||||
}
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
|
||||
c->try_send_log_message(level, tag, message, message_len);
|
||||
}
|
||||
@@ -608,7 +621,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
if (!c->flags_.remove)
|
||||
c->set_camera_state(image);
|
||||
}
|
||||
@@ -625,7 +638,7 @@ void APIServer::on_shutdown() {
|
||||
this->batch_delay_ = 5;
|
||||
|
||||
// Send disconnect requests to all connected clients
|
||||
for (auto &c : this->clients_) {
|
||||
for (auto &c : this->active_clients()) {
|
||||
DisconnectRequest req;
|
||||
if (!c->send_message(req)) {
|
||||
// If we can't send the disconnect request directly (tx_buffer full),
|
||||
@@ -643,7 +656,7 @@ bool APIServer::teardown() {
|
||||
this->loop();
|
||||
|
||||
// Return true only when all clients have been torn down
|
||||
return this->clients_.empty();
|
||||
return this->api_connection_count_ == 0;
|
||||
}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include "esphome/components/camera/camera.h"
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::api {
|
||||
@@ -63,7 +65,6 @@ class APIServer final : public Component,
|
||||
void set_batch_delay(uint16_t batch_delay);
|
||||
uint16_t get_batch_delay() const { return batch_delay_; }
|
||||
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
|
||||
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
|
||||
|
||||
// Get reference to shared buffer for API connections
|
||||
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||
@@ -186,9 +187,26 @@ class APIServer final : public Component,
|
||||
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
|
||||
#endif
|
||||
|
||||
bool is_connected() const { return !this->clients_.empty(); }
|
||||
bool is_connected() const { return this->api_connection_count_ != 0; }
|
||||
bool is_connected_with_state_subscription() const;
|
||||
|
||||
// Range-for view over the populated slice [0, api_connection_count_). Read-only with respect
|
||||
// to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the
|
||||
// APIConnection but cannot reset/move the slot and break the count invariant.
|
||||
using APIConnectionPtr = std::unique_ptr<APIConnection>;
|
||||
class ActiveClientsView {
|
||||
const APIConnectionPtr *begin_;
|
||||
const APIConnectionPtr *end_;
|
||||
|
||||
public:
|
||||
ActiveClientsView(const APIConnectionPtr *b, const APIConnectionPtr *e) : begin_(b), end_(e) {}
|
||||
const APIConnectionPtr *begin() const { return this->begin_; }
|
||||
const APIConnectionPtr *end() const { return this->end_; }
|
||||
};
|
||||
ActiveClientsView active_clients() const {
|
||||
return {this->clients_.data(), this->clients_.data() + this->api_connection_count_};
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
struct HomeAssistantStateSubscription {
|
||||
const char *entity_id; // Pointer to flash (internal) or heap (external)
|
||||
@@ -234,12 +252,14 @@ class APIServer final : public Component,
|
||||
protected:
|
||||
// Accept incoming socket connections. Only called when socket has pending connections.
|
||||
void __attribute__((noinline)) accept_new_connections_();
|
||||
// Remove a disconnected client by index. Swaps with last element and pops.
|
||||
void __attribute__((noinline)) remove_client_(size_t client_index);
|
||||
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
|
||||
void __attribute__((noinline)) remove_client_(uint8_t client_index);
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
bool make_active);
|
||||
// Load saved PSK from preferences and apply it. Returns true on success.
|
||||
bool load_and_apply_noise_psk_();
|
||||
#endif // USE_API_NOISE
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
// Helper methods to reduce code duplication
|
||||
@@ -271,8 +291,9 @@ class APIServer final : public Component,
|
||||
uint32_t reboot_timeout_{300000};
|
||||
uint32_t last_connected_{0};
|
||||
|
||||
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
|
||||
std::array<std::unique_ptr<APIConnection>, MAX_API_CONNECTIONS> clients_{};
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
std::vector<std::unique_ptr<APIConnection>> clients_;
|
||||
// Shared proto write buffer for all connections.
|
||||
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
|
||||
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
|
||||
@@ -307,10 +328,10 @@ class APIServer final : public Component,
|
||||
uint16_t port_{6053};
|
||||
uint16_t batch_delay_{100};
|
||||
// Connection limits - these defaults will be overridden by config values
|
||||
// from cv.SplitDefault in __init__.py which sets platform-specific defaults
|
||||
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
|
||||
uint8_t listen_backlog_{4};
|
||||
uint8_t max_connections_{8};
|
||||
bool shutting_down_ = false;
|
||||
uint8_t api_connection_count_{0};
|
||||
// 7 bytes used, 1 byte padding
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
|
||||
@@ -32,7 +32,11 @@ if TYPE_CHECKING:
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
async def async_run_logs(
|
||||
config: dict[str, Any],
|
||||
addresses: list[str],
|
||||
subscribe_states: bool = True,
|
||||
) -> None:
|
||||
"""Run the logs command in the event loop."""
|
||||
conf = config["api"]
|
||||
name = config["esphome"]["name"]
|
||||
@@ -89,14 +93,20 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
config, raw_line, backtrace_state=backtrace_state
|
||||
)
|
||||
|
||||
stop = await async_run(cli, on_log, name=name)
|
||||
stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states)
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
finally:
|
||||
await stop()
|
||||
|
||||
|
||||
def run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
def run_logs(
|
||||
config: dict[str, Any],
|
||||
addresses: list[str],
|
||||
subscribe_states: bool = True,
|
||||
) -> None:
|
||||
"""Run the logs command."""
|
||||
with contextlib.suppress(KeyboardInterrupt):
|
||||
asyncio.run(async_run_logs(config, addresses))
|
||||
asyncio.run(
|
||||
async_run_logs(config, addresses, subscribe_states=subscribe_states)
|
||||
)
|
||||
|
||||
@@ -145,14 +145,15 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
|
||||
// [tag][v1][v2][body ..... body]
|
||||
// ^-- pos_ = element end, within buffer
|
||||
void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value,
|
||||
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
|
||||
uint8_t *(*encode_fn)(const void *,
|
||||
ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)) {
|
||||
this->encode_field_raw(field_id, 2);
|
||||
// Reserve 1 byte for length varint (optimistic: submessage < 128 bytes)
|
||||
uint8_t *len_pos = this->pos_;
|
||||
this->debug_check_bounds_(1);
|
||||
this->pos_++;
|
||||
uint8_t *body_start = this->pos_;
|
||||
encode_fn(value, *this);
|
||||
this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_));
|
||||
uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start);
|
||||
if (body_size < 128) [[likely]] {
|
||||
// Common case: 1-byte varint, just backpatch
|
||||
@@ -173,22 +174,27 @@ void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value,
|
||||
|
||||
// Non-template core for encode_optional_sub_message.
|
||||
void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
|
||||
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
|
||||
uint8_t *(*encode_fn)(const void *,
|
||||
ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)) {
|
||||
if (nested_size == 0)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 2);
|
||||
this->encode_varint_raw(nested_size);
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
uint8_t *start = this->pos_;
|
||||
encode_fn(value, *this);
|
||||
this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_));
|
||||
if (static_cast<uint32_t>(this->pos_ - start) != nested_size)
|
||||
this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start);
|
||||
#else
|
||||
encode_fn(value, *this);
|
||||
this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_));
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller) {
|
||||
ESP_LOGE(TAG, "Proto encode bounds check failed in %s: need %zu bytes, %td available", caller, bytes, end - pos);
|
||||
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,
|
||||
@@ -201,6 +207,7 @@ void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expe
|
||||
expected, actual);
|
||||
abort();
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
@@ -257,7 +264,13 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Fixed32-bit at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
uint32_t val = encode_uint32(ptr[3], ptr[2], ptr[1], ptr[0]);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#include <cassert>
|
||||
@@ -24,6 +25,19 @@ constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded me
|
||||
constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float
|
||||
constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag
|
||||
|
||||
// Reinterpret float bits as uint32_t without floating-point comparison.
|
||||
// Used by both encode_float() and calc_float() to ensure identical zero checks.
|
||||
// Uses union type-punning which is a GCC/Clang extension (not standard C++),
|
||||
// but bit_cast/memcpy don't optimize to a no-op on xtensa-gcc (ESP8266).
|
||||
inline uint32_t float_to_raw(float value) {
|
||||
union {
|
||||
float f;
|
||||
uint32_t u;
|
||||
} v;
|
||||
v.f = value;
|
||||
return v.u;
|
||||
}
|
||||
|
||||
// Helper functions for ZigZag encoding/decoding
|
||||
inline constexpr uint32_t encode_zigzag32(int32_t value) {
|
||||
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||
@@ -152,8 +166,7 @@ class ProtoVarInt {
|
||||
#endif
|
||||
};
|
||||
|
||||
// Forward declarations for decode_to_message and related encoding helpers
|
||||
class ProtoDecodableMessage;
|
||||
// Forward declarations for encoding helpers
|
||||
class ProtoMessage;
|
||||
class ProtoSize;
|
||||
|
||||
@@ -166,16 +179,9 @@ class ProtoLengthDelimited {
|
||||
const uint8_t *data() const { return this->value_; }
|
||||
size_t size() const { return this->length_; }
|
||||
|
||||
/**
|
||||
* Decode the length-delimited data into an existing ProtoDecodableMessage instance.
|
||||
*
|
||||
* This method allows decoding without templates, enabling use in contexts
|
||||
* where the message type is not known at compile time. The ProtoDecodableMessage's
|
||||
* decode() method will be called with the raw data and length.
|
||||
*
|
||||
* @param msg The ProtoDecodableMessage instance to decode into
|
||||
*/
|
||||
void decode_to_message(ProtoDecodableMessage &msg) const;
|
||||
/// Decode the length-delimited data into a message instance.
|
||||
/// Template preserves concrete type so decode() resolves statically.
|
||||
template<typename T> void decode_to_message(T &msg) const;
|
||||
|
||||
protected:
|
||||
const uint8_t *const value_;
|
||||
@@ -202,6 +208,26 @@ class Proto32Bit {
|
||||
|
||||
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
|
||||
|
||||
// Debug bounds checking for proto encode functions.
|
||||
// In debug mode (ESPHOME_DEBUG_API), an extra end-of-buffer pointer is threaded
|
||||
// through the entire encode chain. In production, these expand to nothing.
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
#define PROTO_ENCODE_DEBUG_PARAM , uint8_t *proto_debug_end_
|
||||
#define PROTO_ENCODE_DEBUG_ARG , proto_debug_end_
|
||||
#define PROTO_ENCODE_DEBUG_INIT(buf) , (buf)->data() + (buf)->size()
|
||||
#define PROTO_ENCODE_CHECK_BOUNDS(pos, n) \
|
||||
do { \
|
||||
if ((pos) + (n) > proto_debug_end_) \
|
||||
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);
|
||||
#else
|
||||
#define PROTO_ENCODE_DEBUG_PARAM
|
||||
#define PROTO_ENCODE_DEBUG_ARG
|
||||
#define PROTO_ENCODE_DEBUG_INIT(buf)
|
||||
#define PROTO_ENCODE_CHECK_BOUNDS(pos, n)
|
||||
#endif
|
||||
|
||||
class ProtoWriteBuffer {
|
||||
public:
|
||||
ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
|
||||
@@ -214,15 +240,6 @@ class ProtoWriteBuffer {
|
||||
}
|
||||
this->encode_varint_raw_slow_(value);
|
||||
}
|
||||
void encode_varint_raw_64(uint64_t value) {
|
||||
while (value > 0x7F) {
|
||||
this->debug_check_bounds_(1);
|
||||
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
this->debug_check_bounds_(1);
|
||||
*this->pos_++ = static_cast<uint8_t>(value);
|
||||
}
|
||||
/**
|
||||
* Encode a field key (tag/wire type combination).
|
||||
*
|
||||
@@ -236,98 +253,6 @@ 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_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
|
||||
if (len == 0 && !force)
|
||||
return;
|
||||
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
|
||||
this->encode_varint_raw(len);
|
||||
// Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks
|
||||
// and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings.
|
||||
this->debug_check_bounds_(len);
|
||||
std::memcpy(this->pos_, string, len);
|
||||
this->pos_ += len;
|
||||
}
|
||||
void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
|
||||
this->encode_string(field_id, value.data(), value.size(), force);
|
||||
}
|
||||
void encode_string(uint32_t field_id, const StringRef &ref, bool force = false) {
|
||||
this->encode_string(field_id, ref.c_str(), ref.size(), force);
|
||||
}
|
||||
void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) {
|
||||
this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);
|
||||
}
|
||||
void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - uint32
|
||||
this->encode_varint_raw(value);
|
||||
}
|
||||
void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
|
||||
this->encode_varint_raw_64(value);
|
||||
}
|
||||
void encode_bool(uint32_t field_id, bool value, bool force = false) {
|
||||
if (!value && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
|
||||
this->debug_check_bounds_(1);
|
||||
*this->pos_++ = value ? 0x01 : 0x00;
|
||||
}
|
||||
// noinline: 51 call sites; inlining causes net code growth vs a single out-of-line copy
|
||||
__attribute__((noinline)) void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
|
||||
this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32
|
||||
this->debug_check_bounds_(4);
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
// Protobuf fixed32 is little-endian, so direct copy works
|
||||
std::memcpy(this->pos_, &value, 4);
|
||||
this->pos_ += 4;
|
||||
#else
|
||||
*this->pos_++ = (value >> 0) & 0xFF;
|
||||
*this->pos_++ = (value >> 8) & 0xFF;
|
||||
*this->pos_++ = (value >> 16) & 0xFF;
|
||||
*this->pos_++ = (value >> 24) & 0xFF;
|
||||
#endif
|
||||
}
|
||||
// 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.
|
||||
void encode_float(uint32_t field_id, float value, bool force = false) {
|
||||
if (value == 0.0f && !force)
|
||||
return;
|
||||
|
||||
union {
|
||||
float value;
|
||||
uint32_t raw;
|
||||
} val{};
|
||||
val.value = value;
|
||||
this->encode_fixed32(field_id, val.raw);
|
||||
}
|
||||
void encode_int32(uint32_t field_id, int32_t value, bool force = false) {
|
||||
if (value < 0) {
|
||||
// negative int32 is always 10 byte long
|
||||
this->encode_int64(field_id, value, force);
|
||||
return;
|
||||
}
|
||||
this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
|
||||
}
|
||||
void encode_int64(uint32_t field_id, int64_t value, bool force = false) {
|
||||
this->encode_uint64(field_id, static_cast<uint64_t>(value), force);
|
||||
}
|
||||
void encode_sint32(uint32_t field_id, int32_t value, bool force = false) {
|
||||
this->encode_uint32(field_id, encode_zigzag32(value), force);
|
||||
}
|
||||
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
|
||||
this->encode_uint64(field_id, encode_zigzag64(value), force);
|
||||
}
|
||||
/// Encode a packed repeated sint32 field (zero-copy from vector)
|
||||
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
|
||||
/// 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);
|
||||
@@ -335,12 +260,17 @@ class ProtoWriteBuffer {
|
||||
/// Thin template wrapper; all buffer work is in the non-template core.
|
||||
template<typename T> void encode_optional_sub_message(uint32_t field_id, const T &value);
|
||||
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
// Non-template core for encode_sub_message — backpatch approach.
|
||||
void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &));
|
||||
void encode_sub_message(uint32_t field_id, const void *value,
|
||||
uint8_t *(*encode_fn)(const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM));
|
||||
// Non-template core for encode_optional_sub_message.
|
||||
void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
|
||||
void (*encode_fn)(const void *, ProtoWriteBuffer &));
|
||||
uint8_t *(*encode_fn)(const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM));
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
APIBuffer *get_buffer() const { return buffer_; }
|
||||
uint8_t *get_pos() const { return pos_; }
|
||||
void set_pos(uint8_t *pos) { pos_ = pos; }
|
||||
|
||||
protected:
|
||||
// Slow path for encode_varint_raw values >= 128, outlined to keep fast path small
|
||||
@@ -357,6 +287,226 @@ class ProtoWriteBuffer {
|
||||
uint8_t *pos_;
|
||||
};
|
||||
|
||||
// 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.
|
||||
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) {
|
||||
do {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value | 0x80);
|
||||
value >>= 7;
|
||||
} while (value > 0x7F);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
}
|
||||
static inline void 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;
|
||||
}
|
||||
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) {
|
||||
if (value < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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) {
|
||||
if (value < VARINT_MAX_1_BYTE) [[likely]] {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
}
|
||||
encode_varint_raw_loop(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);
|
||||
}
|
||||
/// 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) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = b;
|
||||
}
|
||||
/// 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) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
pos++;
|
||||
}
|
||||
/// 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) {
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
|
||||
std::memcpy(pos, data, len);
|
||||
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) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128");
|
||||
#endif
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size());
|
||||
pos[0] = tag;
|
||||
pos[1] = static_cast<uint8_t>(ref.size());
|
||||
std::memcpy(pos + 2, ref.c_str(), ref.size());
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
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
|
||||
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);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
|
||||
}
|
||||
std::memcpy(pos, string, len);
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
|
||||
*pos++ = value ? 0x01 : 0x00;
|
||||
}
|
||||
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);
|
||||
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
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
static inline void encode_int32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int32_t value,
|
||||
bool force = false) {
|
||||
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;
|
||||
}
|
||||
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value), force);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
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);
|
||||
}
|
||||
/// Sub-message encoding: sync pos to buffer, delegate, get pos from return value.
|
||||
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) {
|
||||
buffer.set_pos(pos);
|
||||
buffer.encode_sub_message(field_id, value);
|
||||
pos = 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) {
|
||||
buffer.set_pos(pos);
|
||||
buffer.encode_optional_sub_message(field_id, value);
|
||||
pos = buffer.get_pos();
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
/**
|
||||
* Fixed-size buffer for message dumps - avoids heap allocation.
|
||||
@@ -394,6 +544,23 @@ class DumpBuffer {
|
||||
return *this;
|
||||
}
|
||||
|
||||
/// Append a PROGMEM string (flash-safe on ESP8266, regular append on other platforms)
|
||||
DumpBuffer &append_p(const char *str) {
|
||||
if (str) {
|
||||
#ifdef USE_ESP8266
|
||||
append_p_esp8266(str);
|
||||
#else
|
||||
append_impl_(str, strlen(str));
|
||||
#endif
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
/// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site
|
||||
void append_p_esp8266(const char *str);
|
||||
#endif
|
||||
|
||||
const char *c_str() const { return buf_; }
|
||||
size_t size() const { return pos_; }
|
||||
|
||||
@@ -435,11 +602,11 @@ class ProtoMessage {
|
||||
// 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.
|
||||
void encode(ProtoWriteBuffer &buffer) const {}
|
||||
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
|
||||
virtual const char *dump_to(DumpBuffer &out) const = 0;
|
||||
virtual const char *message_name() const { return "unknown"; }
|
||||
virtual const LogString *message_name() const { return LOG_STR("unknown"); }
|
||||
#endif
|
||||
|
||||
#ifndef USE_HOST
|
||||
@@ -454,7 +621,7 @@ class ProtoMessage {
|
||||
// Base class for messages that support decoding
|
||||
class ProtoDecodableMessage : public ProtoMessage {
|
||||
public:
|
||||
virtual void decode(const uint8_t *buffer, size_t length);
|
||||
void decode(const uint8_t *buffer, size_t length);
|
||||
|
||||
/**
|
||||
* Count occurrences of a repeated field in a protobuf buffer.
|
||||
@@ -477,12 +644,24 @@ class ProtoDecodableMessage : public ProtoMessage {
|
||||
|
||||
class ProtoSize {
|
||||
public:
|
||||
// Varint encoding thresholds: values below each threshold fit in N bytes
|
||||
static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128
|
||||
static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384
|
||||
// Varint encoding thresholds — use namespace-level constants for 1/2 byte,
|
||||
// class-level for 3/4 byte (only used within ProtoSize).
|
||||
static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = VARINT_MAX_1_BYTE;
|
||||
static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = VARINT_MAX_2_BYTE;
|
||||
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
|
||||
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
|
||||
|
||||
// Varint encoded length for a 16-bit value (1, 2, or 3 bytes).
|
||||
// Fully inline — no slow path call for values >= 128.
|
||||
static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint16(uint16_t value) {
|
||||
return value < VARINT_THRESHOLD_1_BYTE ? 1 : (value < VARINT_THRESHOLD_2_BYTE ? 2 : 3);
|
||||
}
|
||||
|
||||
// Varint encoded length for an 8-bit value (1 or 2 bytes).
|
||||
static constexpr inline uint8_t ESPHOME_ALWAYS_INLINE varint8(uint8_t value) {
|
||||
return value < VARINT_THRESHOLD_1_BYTE ? 1 : 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
*
|
||||
@@ -496,6 +675,17 @@ class ProtoSize {
|
||||
return varint_wide(value);
|
||||
return varint_slow(value);
|
||||
}
|
||||
/// Size of a varint expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
|
||||
/// Inlines both checks; falls back to slow path for 3+ bytes.
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_short(uint32_t value) {
|
||||
if (value < VARINT_THRESHOLD_1_BYTE) [[likely]]
|
||||
return 1;
|
||||
if (value < VARINT_THRESHOLD_2_BYTE) [[likely]]
|
||||
return 2;
|
||||
if (__builtin_is_constant_evaluated())
|
||||
return varint_wide(value);
|
||||
return varint_slow(value);
|
||||
}
|
||||
|
||||
private:
|
||||
// Slow path for varint >= 128, outlined to keep fast path small
|
||||
@@ -600,8 +790,8 @@ class ProtoSize {
|
||||
}
|
||||
static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; }
|
||||
static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; }
|
||||
static constexpr uint32_t calc_float(uint32_t field_id_size, float value) {
|
||||
return value != 0.0f ? field_id_size + 4 : 0;
|
||||
static uint32_t calc_float(uint32_t field_id_size, float value) {
|
||||
return float_to_raw(value) != 0 ? field_id_size + 4 : 0;
|
||||
}
|
||||
static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) {
|
||||
return value ? field_id_size + 4 : 0;
|
||||
@@ -610,10 +800,10 @@ class ProtoSize {
|
||||
return value ? field_id_size + 4 : 0;
|
||||
}
|
||||
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
|
||||
return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
|
||||
return value ? field_id_size + varint_short(encode_zigzag32(value)) : 0;
|
||||
}
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) {
|
||||
return field_id_size + varint(encode_zigzag32(value));
|
||||
return field_id_size + varint_short(encode_zigzag32(value));
|
||||
}
|
||||
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
|
||||
return value ? field_id_size + varint(value) : 0;
|
||||
@@ -656,28 +846,9 @@ class ProtoSize {
|
||||
|
||||
// Implementation of methods that depend on ProtoSize being fully defined
|
||||
|
||||
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined
|
||||
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
|
||||
if (values.empty())
|
||||
return;
|
||||
|
||||
// Calculate packed size
|
||||
size_t packed_size = 0;
|
||||
for (int value : values) {
|
||||
packed_size += ProtoSize::varint(encode_zigzag32(value));
|
||||
}
|
||||
|
||||
// Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values
|
||||
this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED);
|
||||
this->encode_varint_raw(packed_size);
|
||||
for (int value : values) {
|
||||
this->encode_varint_raw(encode_zigzag32(value));
|
||||
}
|
||||
}
|
||||
|
||||
// Encode thunk — converts void* back to concrete type for direct encode() call
|
||||
template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) {
|
||||
static_cast<const T *>(msg)->encode(buf);
|
||||
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.
|
||||
@@ -690,33 +861,14 @@ template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(u
|
||||
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>);
|
||||
}
|
||||
|
||||
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
|
||||
inline void ProtoLengthDelimited::decode_to_message(ProtoDecodableMessage &msg) const {
|
||||
// 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_);
|
||||
}
|
||||
|
||||
template<typename T> const char *proto_enum_to_string(T value);
|
||||
|
||||
class ProtoService {
|
||||
public:
|
||||
protected:
|
||||
virtual bool is_authenticated() = 0;
|
||||
virtual bool is_connection_setup() = 0;
|
||||
virtual void on_fatal_error() = 0;
|
||||
virtual void on_no_setup_connection() = 0;
|
||||
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
|
||||
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0;
|
||||
|
||||
// Authentication helper methods
|
||||
inline bool check_connection_setup_() {
|
||||
if (!this->is_connection_setup()) {
|
||||
this->on_no_setup_connection();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool check_authenticated_() { return this->check_connection_setup_(); }
|
||||
};
|
||||
// ProtoService removed — its methods were inlined into APIConnection.
|
||||
// APIConnection is the concrete server-side implementation; the extra virtual layer was unnecessary.
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -275,7 +275,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
|
||||
protected:
|
||||
APIServer *parent_;
|
||||
TemplatableValue<bool, Ts...> success_{true};
|
||||
TemplatableFn<bool, Ts...> success_{[](Ts...) -> bool { return true; }};
|
||||
TemplatableValue<std::string, Ts...> error_message_{""};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
std::function<void(Ts..., JsonObject)> json_builder_;
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.const import (
|
||||
CONF_LIGHTNING_ENERGY,
|
||||
ICON_FLASH,
|
||||
ICON_SIGNAL_DISTANCE_VARIANT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_KILOMETER,
|
||||
)
|
||||
|
||||
@@ -20,13 +21,14 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
unit_of_measurement=UNIT_KILOMETER,
|
||||
icon=ICON_SIGNAL_DISTANCE_VARIANT,
|
||||
accuracy_decimals=1,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema(
|
||||
icon=ICON_FLASH,
|
||||
accuracy_decimals=1,
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
||||
@@ -83,7 +83,7 @@ def angle_to_position(value, min=-360, max=360):
|
||||
value = angle(min=min, max=max)(value)
|
||||
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(f"When using angle, {e.error_message}")
|
||||
raise cv.Invalid(f"When using angle, {e.error_message}") from e
|
||||
|
||||
|
||||
def percent_to_position(value):
|
||||
@@ -164,7 +164,7 @@ def has_valid_range_config():
|
||||
except cv.Invalid as e:
|
||||
raise cv.Invalid(
|
||||
f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
|
||||
)
|
||||
) from e
|
||||
|
||||
return validator
|
||||
|
||||
|
||||
@@ -2,11 +2,9 @@ import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ANGLE,
|
||||
CONF_GAIN,
|
||||
CONF_ID,
|
||||
CONF_MAGNITUDE,
|
||||
CONF_POSITION,
|
||||
CONF_STATUS,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
ICON_MAGNET,
|
||||
@@ -21,7 +19,6 @@ DEPENDENCIES = ["as5600"]
|
||||
|
||||
AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent)
|
||||
|
||||
CONF_RAW_ANGLE = "raw_angle"
|
||||
CONF_RAW_POSITION = "raw_position"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
@@ -89,18 +86,6 @@ async def to_code(config):
|
||||
if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE):
|
||||
cg.add(var.set_out_of_range_mode(out_of_range_mode_config))
|
||||
|
||||
if angle_config := config.get(CONF_ANGLE):
|
||||
sens = await sensor.new_sensor(angle_config)
|
||||
cg.add(var.set_angle_sensor(sens))
|
||||
|
||||
if raw_angle_config := config.get(CONF_RAW_ANGLE):
|
||||
sens = await sensor.new_sensor(raw_angle_config)
|
||||
cg.add(var.set_raw_angle_sensor(sens))
|
||||
|
||||
if position_config := config.get(CONF_POSITION):
|
||||
sens = await sensor.new_sensor(position_config)
|
||||
cg.add(var.set_position_sensor(sens))
|
||||
|
||||
if raw_position_config := config.get(CONF_RAW_POSITION):
|
||||
sens = await sensor.new_sensor(raw_position_config)
|
||||
cg.add(var.set_raw_position_sensor(sens))
|
||||
|
||||
@@ -25,27 +25,10 @@ static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
|
||||
void AS5600Sensor::dump_config() {
|
||||
LOG_SENSOR("", "AS5600 Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_);
|
||||
if (this->angle_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Angle Sensor", this->angle_sensor_);
|
||||
}
|
||||
if (this->raw_angle_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Raw Angle Sensor", this->raw_angle_sensor_);
|
||||
}
|
||||
if (this->position_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Position Sensor", this->position_sensor_);
|
||||
}
|
||||
if (this->raw_position_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_);
|
||||
}
|
||||
if (this->gain_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_);
|
||||
}
|
||||
if (this->magnitude_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_);
|
||||
}
|
||||
if (this->status_sensor_ != nullptr) {
|
||||
LOG_SENSOR(" ", "Status Sensor", this->status_sensor_);
|
||||
}
|
||||
LOG_SENSOR(" ", "Raw Position Sensor", this->raw_position_sensor_);
|
||||
LOG_SENSOR(" ", "Gain Sensor", this->gain_sensor_);
|
||||
LOG_SENSOR(" ", "Magnitude Sensor", this->magnitude_sensor_);
|
||||
LOG_SENSOR(" ", "Status Sensor", this->status_sensor_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,6 @@ class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>,
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; }
|
||||
void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; }
|
||||
void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; }
|
||||
void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) {
|
||||
this->raw_position_sensor_ = raw_position_sensor;
|
||||
}
|
||||
@@ -28,9 +25,6 @@ class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>,
|
||||
OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; }
|
||||
|
||||
protected:
|
||||
sensor::Sensor *angle_sensor_{nullptr};
|
||||
sensor::Sensor *raw_angle_sensor_{nullptr};
|
||||
sensor::Sensor *position_sensor_{nullptr};
|
||||
sensor::Sensor *raw_position_sensor_{nullptr};
|
||||
sensor::Sensor *gain_sensor_{nullptr};
|
||||
sensor::Sensor *magnitude_sensor_{nullptr};
|
||||
|
||||
@@ -169,53 +169,43 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
|
||||
|
||||
# Radar configuration
|
||||
if frontend_reset := config.get(CONF_HW_FRONTEND_RESET):
|
||||
template_ = await cg.templatable(frontend_reset, args, int)
|
||||
template_ = await cg.templatable(frontend_reset, args, cg.int8)
|
||||
cg.add(var.set_hw_frontend_reset(template_))
|
||||
|
||||
if freq := config.get(CONF_FREQUENCY):
|
||||
template_ = await cg.templatable(freq, args, float)
|
||||
template_ = int(template_ / 1000000)
|
||||
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):
|
||||
template_ = await cg.templatable(sens_dist, args, int)
|
||||
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, float)
|
||||
if isinstance(template_, cv.TimePeriod):
|
||||
template_ = template_.total_milliseconds
|
||||
template_ = int(template_)
|
||||
template_ = await cg.templatable(selfcheck, args, cg.int32)
|
||||
cg.add(var.set_poweron_selfcheck_time(template_))
|
||||
|
||||
if protect := config.get(CONF_PROTECT_TIME):
|
||||
template_ = await cg.templatable(protect, args, float)
|
||||
if isinstance(template_, cv.TimePeriod):
|
||||
template_ = template_.total_milliseconds
|
||||
template_ = int(template_)
|
||||
template_ = await cg.templatable(protect, args, cg.int32)
|
||||
cg.add(var.set_protect_time(template_))
|
||||
|
||||
if trig_base := config.get(CONF_TRIGGER_BASE):
|
||||
template_ = await cg.templatable(trig_base, args, float)
|
||||
if isinstance(template_, cv.TimePeriod):
|
||||
template_ = template_.total_milliseconds
|
||||
template_ = int(template_)
|
||||
template_ = await cg.templatable(trig_base, args, cg.int32)
|
||||
cg.add(var.set_trigger_base(template_))
|
||||
|
||||
if trig_keep := config.get(CONF_TRIGGER_KEEP):
|
||||
template_ = await cg.templatable(trig_keep, args, float)
|
||||
if isinstance(template_, cv.TimePeriod):
|
||||
template_ = template_.total_milliseconds
|
||||
template_ = int(template_)
|
||||
template_ = await cg.templatable(trig_keep, args, cg.int32)
|
||||
cg.add(var.set_trigger_keep(template_))
|
||||
|
||||
if stage_gain := config.get(CONF_STAGE_GAIN):
|
||||
template_ = await cg.templatable(stage_gain, args, int)
|
||||
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):
|
||||
template_ = await cg.templatable(power, args, float)
|
||||
template_ = int(template_ * 1000000)
|
||||
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
|
||||
|
||||
@@ -14,6 +14,7 @@ from esphome.const import (
|
||||
CONF_VOLTAGE,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_FREQUENCY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_REACTIVE_POWER,
|
||||
@@ -103,6 +104,7 @@ CONFIG_SCHEMA = (
|
||||
unit_of_measurement=UNIT_HERTZ,
|
||||
icon=ICON_CURRENT_AC,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_FREQUENCY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True),
|
||||
|
||||
@@ -550,8 +550,8 @@ float ATM90E32Component::get_phase_harmonic_active_power_(uint8_t phase) {
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_phase_angle_(uint8_t phase) {
|
||||
uint16_t val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0;
|
||||
return (val > 180) ? (float) (val - 360.0f) : (float) val;
|
||||
float val = this->read16_(ATM90E32_REGISTER_PANGLE + phase) / 10.0f;
|
||||
return (val > 180.0f) ? val - 360.0f : val;
|
||||
}
|
||||
|
||||
float ATM90E32Component::get_phase_peak_current_(uint8_t phase) {
|
||||
|
||||
@@ -111,14 +111,14 @@ class ATM90E32Component : public PollingComponent,
|
||||
#endif
|
||||
float get_reference_voltage(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase >= 0 && phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
return (phase < 3 && ref_voltages_[phase]) ? ref_voltages_[phase]->state : 120.0; // Default voltage
|
||||
#else
|
||||
return 120.0; // Default voltage
|
||||
#endif
|
||||
}
|
||||
float get_reference_current(uint8_t phase) {
|
||||
#ifdef USE_NUMBER
|
||||
return (phase >= 0 && phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
return (phase < 3 && ref_currents_[phase]) ? ref_currents_[phase]->state : 5.0f; // Default current
|
||||
#else
|
||||
return 5.0f; // Default current
|
||||
#endif
|
||||
@@ -134,7 +134,6 @@ class ATM90E32Component : public PollingComponent,
|
||||
void set_freq_status_text_sensor(text_sensor::TextSensor *sensor) { this->freq_status_text_sensor_ = sensor; }
|
||||
#endif
|
||||
uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier);
|
||||
int32_t last_periodic_millis = millis();
|
||||
|
||||
protected:
|
||||
#ifdef USE_NUMBER
|
||||
|
||||
@@ -20,6 +20,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_APPARENT_POWER,
|
||||
DEVICE_CLASS_CURRENT,
|
||||
DEVICE_CLASS_ENERGY,
|
||||
DEVICE_CLASS_FREQUENCY,
|
||||
DEVICE_CLASS_POWER,
|
||||
DEVICE_CLASS_POWER_FACTOR,
|
||||
DEVICE_CLASS_REACTIVE_POWER,
|
||||
@@ -131,7 +132,6 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_PHASE_ANGLE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_DEGREES,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_POWER,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HARMONIC_POWER): sensor.sensor_schema(
|
||||
@@ -166,6 +166,7 @@ CONFIG_SCHEMA = (
|
||||
unit_of_measurement=UNIT_HERTZ,
|
||||
icon=ICON_CURRENT_AC,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_FREQUENCY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema(
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_component,
|
||||
add_idf_sdkconfig_option,
|
||||
include_builtin_idf_component,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||
from esphome.core import CORE
|
||||
@@ -27,6 +31,7 @@ class AudioData:
|
||||
flac_support: bool = False
|
||||
mp3_support: bool = False
|
||||
opus_support: bool = False
|
||||
micro_decoder_support: bool = False
|
||||
|
||||
|
||||
def _get_data() -> AudioData:
|
||||
@@ -50,6 +55,11 @@ def request_opus_support() -> None:
|
||||
_get_data().opus_support = True
|
||||
|
||||
|
||||
def request_micro_decoder_support() -> None:
|
||||
"""Request micro-decoder library support for audio decoding."""
|
||||
_get_data().micro_decoder_support = True
|
||||
|
||||
|
||||
CONF_MIN_BITS_PER_SAMPLE = "min_bits_per_sample"
|
||||
CONF_MAX_BITS_PER_SAMPLE = "max_bits_per_sample"
|
||||
CONF_MIN_CHANNELS = "min_channels"
|
||||
@@ -204,14 +214,28 @@ async def to_code(config):
|
||||
|
||||
add_idf_component(
|
||||
name="esphome/esp-audio-libs",
|
||||
ref="2.0.3",
|
||||
ref="2.0.4",
|
||||
)
|
||||
|
||||
data = _get_data()
|
||||
|
||||
if data.micro_decoder_support:
|
||||
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
|
||||
|
||||
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
|
||||
if not data.flac_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_FLAC", False)
|
||||
if not data.mp3_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False)
|
||||
if not data.opus_support:
|
||||
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False)
|
||||
|
||||
# Legacy audio_decoder.cpp support defines and components
|
||||
if data.flac_support:
|
||||
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-flac", ref="0.1.1")
|
||||
if data.mp3_support:
|
||||
cg.add_define("USE_AUDIO_MP3_SUPPORT")
|
||||
if data.opus_support:
|
||||
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.3.5")
|
||||
add_idf_component(name="esphome/micro-opus", ref="0.3.6")
|
||||
|
||||
@@ -84,13 +84,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
|
||||
switch (this->audio_file_type_) {
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
case AudioFileType::FLAC:
|
||||
this->flac_decoder_ = make_unique<esp_audio_libs::flac::FLACDecoder>();
|
||||
// CRC check slows down decoding by 15-20% on an ESP32-S3. FLAC sources in ESPHome are either from an http source
|
||||
// or built into the firmware, so the data integrity is already verified by the time it gets to the decoder,
|
||||
// making the CRC check unnecessary.
|
||||
this->flac_decoder_->set_crc_check_enabled(false);
|
||||
this->flac_decoder_ = make_unique<micro_flac::FLACDecoder>();
|
||||
this->free_buffer_required_ =
|
||||
this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header
|
||||
this->decoder_buffers_internally_ = true;
|
||||
break;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
@@ -268,59 +265,45 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
|
||||
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
FileDecoderState AudioDecoder::decode_flac_() {
|
||||
if (!this->audio_stream_info_.has_value()) {
|
||||
// Header hasn't been read
|
||||
auto result = this->flac_decoder_->read_header(this->input_buffer_->data(), this->input_buffer_->available());
|
||||
size_t bytes_consumed, samples_decoded;
|
||||
|
||||
if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
|
||||
// Serrious error reading FLAC header, there is no recovery
|
||||
return FileDecoderState::FAILED;
|
||||
micro_flac::FLACDecoderResult result = this->flac_decoder_->decode(
|
||||
this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(),
|
||||
this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded);
|
||||
|
||||
if (result == micro_flac::FLAC_DECODER_SUCCESS) {
|
||||
if (samples_decoded > 0 && this->audio_stream_info_.has_value()) {
|
||||
this->output_transfer_buffer_->increase_buffer_length(
|
||||
this->audio_stream_info_.value().samples_to_bytes(samples_decoded));
|
||||
}
|
||||
|
||||
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
|
||||
this->input_buffer_->consume(bytes_consumed);
|
||||
} else if (result == micro_flac::FLAC_DECODER_HEADER_READY) {
|
||||
// Header just parsed, stream info now available
|
||||
const auto &info = this->flac_decoder_->get_stream_info();
|
||||
this->audio_stream_info_ = audio::AudioStreamInfo(info.bits_per_sample(), info.num_channels(), info.sample_rate());
|
||||
|
||||
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
|
||||
return FileDecoderState::MORE_TO_PROCESS;
|
||||
}
|
||||
|
||||
// Reallocate the output transfer buffer to the smallest necessary size
|
||||
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
|
||||
// Reallocate the output transfer buffer to the required size
|
||||
this->free_buffer_required_ = this->flac_decoder_->get_output_buffer_size_samples() * info.bytes_per_sample();
|
||||
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
|
||||
// Couldn't reallocate output buffer
|
||||
return FileDecoderState::FAILED;
|
||||
}
|
||||
|
||||
this->audio_stream_info_ =
|
||||
audio::AudioStreamInfo(this->flac_decoder_->get_sample_depth(), this->flac_decoder_->get_num_channels(),
|
||||
this->flac_decoder_->get_sample_rate());
|
||||
|
||||
return FileDecoderState::MORE_TO_PROCESS;
|
||||
}
|
||||
|
||||
uint32_t output_samples = 0;
|
||||
auto result = this->flac_decoder_->decode_frame(this->input_buffer_->data(), this->input_buffer_->available(),
|
||||
this->output_transfer_buffer_->get_buffer_end(), &output_samples);
|
||||
|
||||
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
|
||||
// Not an issue, just needs more data that we'll get next time.
|
||||
return FileDecoderState::POTENTIALLY_FAILED;
|
||||
}
|
||||
|
||||
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
|
||||
this->input_buffer_->consume(bytes_consumed);
|
||||
|
||||
if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
|
||||
// Corrupted frame, don't retry with current buffer content, wait for new sync
|
||||
return FileDecoderState::POTENTIALLY_FAILED;
|
||||
}
|
||||
|
||||
// We have successfully decoded some input data and have new output data
|
||||
this->output_transfer_buffer_->increase_buffer_length(
|
||||
this->audio_stream_info_.value().samples_to_bytes(output_samples));
|
||||
|
||||
if (result == esp_audio_libs::flac::FLAC_DECODER_NO_MORE_FRAMES) {
|
||||
this->input_buffer_->consume(bytes_consumed);
|
||||
} else if (result == micro_flac::FLAC_DECODER_END_OF_STREAM) {
|
||||
this->input_buffer_->consume(bytes_consumed);
|
||||
return FileDecoderState::END_OF_FILE;
|
||||
} else if (result == micro_flac::FLAC_DECODER_NEED_MORE_DATA) {
|
||||
this->input_buffer_->consume(bytes_consumed);
|
||||
return FileDecoderState::MORE_TO_PROCESS;
|
||||
} else if (result == micro_flac::FLAC_DECODER_ERROR_OUTPUT_TOO_SMALL) {
|
||||
// Reallocate to decode the frame on the next call
|
||||
const auto &info = this->flac_decoder_->get_stream_info();
|
||||
this->free_buffer_required_ = this->flac_decoder_->get_output_buffer_size_samples() * info.bytes_per_sample();
|
||||
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
|
||||
return FileDecoderState::FAILED;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGE(TAG, "FLAC decoder failed: %d", static_cast<int>(result));
|
||||
return FileDecoderState::POTENTIALLY_FAILED;
|
||||
}
|
||||
|
||||
return FileDecoderState::MORE_TO_PROCESS;
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
#include "esp_err.h"
|
||||
|
||||
// esp-audio-libs
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
#include <flac_decoder.h>
|
||||
#endif
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
#include <mp3_decoder.h>
|
||||
#endif
|
||||
#include <wav_decoder.h>
|
||||
|
||||
// micro-flac
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
#include <micro_flac/flac_decoder.h>
|
||||
#endif
|
||||
|
||||
// micro-opus
|
||||
#ifdef USE_AUDIO_OPUS_SUPPORT
|
||||
#include <micro_opus/ogg_opus_decoder.h>
|
||||
@@ -119,7 +121,7 @@ class AudioDecoder {
|
||||
std::unique_ptr<esp_audio_libs::wav_decoder::WAVDecoder> wav_decoder_;
|
||||
#ifdef USE_AUDIO_FLAC_SUPPORT
|
||||
FileDecoderState decode_flac_();
|
||||
std::unique_ptr<esp_audio_libs::flac::FLACDecoder> flac_decoder_;
|
||||
std::unique_ptr<micro_flac::FLACDecoder> flac_decoder_;
|
||||
#endif
|
||||
#ifdef USE_AUDIO_MP3_SUPPORT
|
||||
FileDecoderState decode_mp3_();
|
||||
|
||||
@@ -32,7 +32,7 @@ async def audio_adc_set_mic_gain_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)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, float)
|
||||
template_ = await cg.templatable(config.get(CONF_MIC_GAIN), args, cg.float_)
|
||||
cg.add(var.set_mic_gain(template_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -52,7 +52,7 @@ async def audio_dac_set_volume_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)
|
||||
|
||||
template_ = await cg.templatable(config.get(CONF_VOLUME), args, float)
|
||||
template_ = await cg.templatable(config.get(CONF_VOLUME), args, cg.float_)
|
||||
cg.add(var.set_volume(template_))
|
||||
|
||||
return var
|
||||
|
||||
@@ -116,7 +116,7 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
|
||||
raise cv.Invalid(
|
||||
f"Unable to determine audio file type of '{path}'. "
|
||||
f"Try re-encoding the file into a supported format. Details: {e}"
|
||||
)
|
||||
) from e
|
||||
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
|
||||
if file_type == "wav":
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "esphome/components/audio/audio_decoder.h"
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::audio_file {
|
||||
@@ -249,7 +250,7 @@ void AudioFileMediaSource::decode_task(void *params) {
|
||||
|
||||
audio::AudioStreamInfo stream_info = decoder->get_audio_stream_info().value();
|
||||
|
||||
ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %d", stream_info.get_bits_per_sample(),
|
||||
ESP_LOGD(TAG, "Bits per sample: %d, Channels: %d, Sample rate: %" PRIu32, stream_info.get_bits_per_sample(),
|
||||
stream_info.get_channels(), stream_info.get_sample_rate());
|
||||
|
||||
if (stream_info.get_bits_per_sample() != 16 || stream_info.get_channels() > 2) {
|
||||
|
||||
@@ -61,6 +61,15 @@ void BedJetClimate::dump_config() {
|
||||
}
|
||||
|
||||
void BedJetClimate::setup() {
|
||||
// Set custom modes once during setup — stored on Climate base class, wired via get_traits()
|
||||
this->set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
|
||||
this->set_supported_custom_presets({
|
||||
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
|
||||
"M1",
|
||||
"M2",
|
||||
"M3",
|
||||
});
|
||||
|
||||
// restore set points
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value()) {
|
||||
|
||||
@@ -42,21 +42,14 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
|
||||
climate::CLIMATE_MODE_DRY,
|
||||
});
|
||||
|
||||
// It would be better if we had a slider for the fan modes.
|
||||
traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
|
||||
traits.set_supported_presets({
|
||||
// If we support NONE, then have to decide what happens if the user switches to it (turn off?)
|
||||
// climate::CLIMATE_PRESET_NONE,
|
||||
// Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead.
|
||||
climate::CLIMATE_PRESET_BOOST,
|
||||
});
|
||||
// String literals are stored in rodata and valid for program lifetime
|
||||
traits.set_supported_custom_presets({
|
||||
this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT",
|
||||
"M1",
|
||||
"M2",
|
||||
"M3",
|
||||
});
|
||||
// Custom fan modes and presets are set once in setup(), stored on Climate base class,
|
||||
// and wired automatically via get_traits()
|
||||
traits.set_visual_min_temperature(19.0);
|
||||
traits.set_visual_max_temperature(43.0);
|
||||
traits.set_visual_temperature_step(1.0);
|
||||
|
||||
@@ -12,7 +12,7 @@ CODEOWNERS = ["@B48D81EFCC"]
|
||||
|
||||
sensor_ns = cg.esphome_ns.namespace("bh1900nux")
|
||||
BH1900NUXSensor = sensor_ns.class_(
|
||||
"BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice
|
||||
"BH1900NUXSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
@@ -120,25 +120,15 @@ BinarySensorInitiallyOff = binary_sensor_ns.class_(
|
||||
BinarySensorPtr = BinarySensor.operator("ptr")
|
||||
|
||||
# Triggers
|
||||
PressTrigger = binary_sensor_ns.class_("PressTrigger", automation.Trigger.template())
|
||||
ReleaseTrigger = binary_sensor_ns.class_(
|
||||
"ReleaseTrigger", automation.Trigger.template()
|
||||
)
|
||||
ClickTrigger = binary_sensor_ns.class_("ClickTrigger", automation.Trigger.template())
|
||||
DoubleClickTrigger = binary_sensor_ns.class_(
|
||||
"DoubleClickTrigger", automation.Trigger.template()
|
||||
)
|
||||
MultiClickTrigger = binary_sensor_ns.class_(
|
||||
"MultiClickTrigger", automation.Trigger.template(), cg.Component
|
||||
MultiClickTriggerBase = binary_sensor_ns.class_(
|
||||
"MultiClickTriggerBase", automation.Trigger.template(), cg.Component
|
||||
)
|
||||
MultiClickTrigger = binary_sensor_ns.class_("MultiClickTrigger", MultiClickTriggerBase)
|
||||
MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
|
||||
StateTrigger = binary_sensor_ns.class_(
|
||||
"StateTrigger", automation.Trigger.template(bool)
|
||||
)
|
||||
StateChangeTrigger = binary_sensor_ns.class_(
|
||||
"StateChangeTrigger",
|
||||
automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)),
|
||||
)
|
||||
|
||||
BinarySensorPublishAction = binary_sensor_ns.class_(
|
||||
"BinarySensorPublishAction", automation.Action
|
||||
@@ -266,6 +256,7 @@ async def delayed_off_filter_to_code(config, filter_id):
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
cv.Length(max=254),
|
||||
),
|
||||
)
|
||||
async def autorepeat_filter_to_code(config, filter_id):
|
||||
@@ -294,7 +285,7 @@ async def autorepeat_filter_to_code(config, filter_id):
|
||||
),
|
||||
)
|
||||
]
|
||||
var = cg.new_Pvariable(filter_id, timings)
|
||||
var = cg.new_Pvariable(filter_id, cg.TemplateArguments(len(timings)), timings)
|
||||
await cg.register_component(var, {})
|
||||
return var
|
||||
|
||||
@@ -341,8 +332,9 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
state = cv.boolean(parts[0])
|
||||
except cv.Invalid:
|
||||
# pylint: disable=raise-missing-from
|
||||
raise cv.Invalid(f"First word must either be ON or OFF, not {parts[0]}")
|
||||
raise cv.Invalid(
|
||||
f"First word must either be ON or OFF, not {parts[0]}"
|
||||
) from None
|
||||
|
||||
if parts[1] != "for":
|
||||
raise cv.Invalid(f"Second word must be 'for', got {parts[1]}")
|
||||
@@ -359,7 +351,9 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
length = cv.positive_time_period_milliseconds(parts[4])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing length failed: {err}"
|
||||
) from err
|
||||
return {CONF_STATE: state, key: str(length)}
|
||||
|
||||
if parts[3] != "to":
|
||||
@@ -368,12 +362,16 @@ def parse_multi_click_timing_str(value):
|
||||
try:
|
||||
min_length = cv.positive_time_period_milliseconds(parts[2])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing minimum length failed: {err}"
|
||||
) from err
|
||||
|
||||
try:
|
||||
max_length = cv.positive_time_period_milliseconds(parts[4])
|
||||
except cv.Invalid as err:
|
||||
raise cv.Invalid(f"Multi Click Grammar Parsing minimum length failed: {err}")
|
||||
raise cv.Invalid(
|
||||
f"Multi Click Grammar Parsing maximum length failed: {err}"
|
||||
) from err
|
||||
|
||||
return {
|
||||
CONF_STATE: state,
|
||||
@@ -399,7 +397,7 @@ def validate_multi_click_timing(value):
|
||||
new_state = v_.get(CONF_STATE, not state)
|
||||
if new_state == state:
|
||||
raise cv.Invalid(
|
||||
f"Timings must have alternating state. Indices {i} and {i + 1} have the same state {state}"
|
||||
f"Timings must have alternating state. Indices {i - 1} and {i} have the same state {state}"
|
||||
)
|
||||
if max_length is not None and max_length < min_length:
|
||||
raise cv.Invalid(
|
||||
@@ -458,16 +456,8 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(CONF_FILTERS): validate_filters,
|
||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(PressTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_RELEASE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReleaseTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_CLICK): cv.All(
|
||||
automation.validate_automation(
|
||||
{
|
||||
@@ -502,23 +492,17 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(MultiClickTrigger),
|
||||
cv.Required(CONF_TIMING): cv.All(
|
||||
[parse_multi_click_timing_str], validate_multi_click_timing
|
||||
[parse_multi_click_timing_str],
|
||||
validate_multi_click_timing,
|
||||
cv.Length(min=1, max=255),
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_INVALID_COOLDOWN, default="1s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -554,15 +538,31 @@ def binary_sensor_schema(
|
||||
return _BINARY_SENSOR_SCHEMA.extend(schema)
|
||||
|
||||
|
||||
_CALLBACK_AUTOMATIONS = (
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_PRESS,
|
||||
"add_on_state_callback",
|
||||
forwarder=automation.TriggerOnTrueForwarder,
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_RELEASE,
|
||||
"add_on_state_callback",
|
||||
forwarder=automation.TriggerOnFalseForwarder,
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_STATE, "add_on_state_callback", [(bool, "x")]
|
||||
),
|
||||
automation.CallbackAutomation(
|
||||
CONF_ON_STATE_CHANGE,
|
||||
"add_full_state_callback",
|
||||
[(cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x")],
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.AUTOMATION)
|
||||
async def _build_binary_sensor_automations(var, config):
|
||||
for conf in config.get(CONF_ON_PRESS, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_RELEASE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
for conf in config.get(CONF_ON_CLICK, []):
|
||||
trigger = cg.new_Pvariable(
|
||||
@@ -586,27 +586,14 @@ async def _build_binary_sensor_automations(var, config):
|
||||
)
|
||||
for tim in conf[CONF_TIMING]
|
||||
]
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var, timings)
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID], cg.TemplateArguments(len(timings)), var, timings
|
||||
)
|
||||
if CONF_INVALID_COOLDOWN in conf:
|
||||
cg.add(trigger.set_invalid_cooldown(conf[CONF_INVALID_COOLDOWN]))
|
||||
await cg.register_component(trigger, conf)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [(bool, "x")], conf)
|
||||
|
||||
for conf in config.get(CONF_ON_STATE_CHANGE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[
|
||||
(cg.optional.template(bool), "x_previous"),
|
||||
(cg.optional.template(bool), "x"),
|
||||
],
|
||||
conf,
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("binary_sensor")
|
||||
async def setup_binary_sensor_core_(var, config):
|
||||
|
||||
@@ -13,7 +13,7 @@ constexpr uint32_t MULTICLICK_COOLDOWN_ID = 1;
|
||||
constexpr uint32_t MULTICLICK_IS_VALID_ID = 2;
|
||||
constexpr uint32_t MULTICLICK_IS_NOT_VALID_ID = 3;
|
||||
|
||||
void MultiClickTrigger::on_state_(bool state) {
|
||||
void MultiClickTriggerBase::on_state_(bool state) {
|
||||
// Handle duplicate events
|
||||
if (state == this->last_state_) {
|
||||
return;
|
||||
@@ -32,7 +32,7 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length);
|
||||
ESP_LOGV(TAG, "Multi Click: Starting multi click action!");
|
||||
this->at_index_ = 1;
|
||||
if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) {
|
||||
if (this->timing_count_ == 1 && evt.max_length == 4294967294UL) {
|
||||
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
|
||||
} else {
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
@@ -50,7 +50,7 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (*this->at_index_ == this->timing_.size()) {
|
||||
if (*this->at_index_ == this->timing_count_) {
|
||||
this->trigger_();
|
||||
return;
|
||||
}
|
||||
@@ -61,7 +61,7 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
this->schedule_is_not_valid_(evt.max_length);
|
||||
} else if (*this->at_index_ + 1 != this->timing_.size()) {
|
||||
} else if (*this->at_index_ + 1 != this->timing_count_) {
|
||||
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
this->schedule_is_valid_(evt.min_length);
|
||||
@@ -74,7 +74,7 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
}
|
||||
void MultiClickTrigger::schedule_cooldown_() {
|
||||
void MultiClickTriggerBase::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
this->is_in_cooldown_ = true;
|
||||
this->set_timeout(MULTICLICK_COOLDOWN_ID, this->invalid_cooldown_, [this]() {
|
||||
@@ -86,7 +86,7 @@ void MultiClickTrigger::schedule_cooldown_() {
|
||||
this->cancel_timeout(MULTICLICK_IS_VALID_ID);
|
||||
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) {
|
||||
if (min_length == 0) {
|
||||
this->is_valid_ = true;
|
||||
return;
|
||||
@@ -97,19 +97,19 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
this->is_valid_ = true;
|
||||
});
|
||||
}
|
||||
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
});
|
||||
}
|
||||
void MultiClickTrigger::cancel() {
|
||||
void MultiClickTriggerBase::cancel() {
|
||||
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
}
|
||||
void MultiClickTrigger::trigger_() {
|
||||
void MultiClickTriggerBase::trigger_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout(MULTICLICK_TRIGGER_ID);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cinttypes>
|
||||
#include <utility>
|
||||
|
||||
@@ -89,10 +90,10 @@ class DoubleClickTrigger : public Trigger<> {
|
||||
uint32_t max_length_; /// Maximum length of click. 0 means no maximum.
|
||||
};
|
||||
|
||||
class MultiClickTrigger : public Trigger<>, public Component {
|
||||
/// Non-template base for MultiClickTrigger (keeps large method bodies out of the header).
|
||||
class MultiClickTriggerBase : public Trigger<>, public Component {
|
||||
public:
|
||||
explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing)
|
||||
: parent_(parent), timing_(timing) {}
|
||||
explicit MultiClickTriggerBase(BinarySensor *parent) : parent_(parent) {}
|
||||
|
||||
void setup() override {
|
||||
this->last_state_ = this->parent_->get_state_default(false);
|
||||
@@ -104,6 +105,8 @@ class MultiClickTrigger : public Trigger<>, public Component {
|
||||
void set_invalid_cooldown(uint32_t invalid_cooldown) { this->invalid_cooldown_ = invalid_cooldown; }
|
||||
|
||||
void cancel();
|
||||
MultiClickTriggerBase(const MultiClickTriggerBase &) = delete;
|
||||
MultiClickTriggerBase &operator=(const MultiClickTriggerBase &) = delete;
|
||||
|
||||
protected:
|
||||
void on_state_(bool state);
|
||||
@@ -113,14 +116,30 @@ class MultiClickTrigger : public Trigger<>, public Component {
|
||||
void trigger_();
|
||||
|
||||
BinarySensor *parent_;
|
||||
FixedVector<MultiClickTriggerEvent> timing_;
|
||||
const MultiClickTriggerEvent *timing_{nullptr};
|
||||
uint32_t invalid_cooldown_{1000};
|
||||
optional<size_t> at_index_{};
|
||||
uint8_t timing_count_{0};
|
||||
bool last_state_{false};
|
||||
bool is_in_cooldown_{false};
|
||||
bool is_valid_{false};
|
||||
};
|
||||
|
||||
/// Template wrapper that provides inline std::array storage for timing events.
|
||||
/// N is set by code generation to match the exact number of timing events configured in YAML.
|
||||
template<size_t N> class MultiClickTrigger : public MultiClickTriggerBase {
|
||||
public:
|
||||
MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing)
|
||||
: MultiClickTriggerBase(parent) {
|
||||
init_array_from(this->timing_storage_, timing);
|
||||
this->timing_ = this->timing_storage_.data();
|
||||
this->timing_count_ = N;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::array<MultiClickTriggerEvent, N> timing_storage_{};
|
||||
};
|
||||
|
||||
class StateTrigger : public Trigger<bool> {
|
||||
public:
|
||||
explicit StateTrigger(BinarySensor *parent) {
|
||||
|
||||
@@ -32,20 +32,13 @@ void BinarySensor::publish_initial_state(bool new_state) {
|
||||
this->invalidate_state();
|
||||
this->publish_state(new_state);
|
||||
}
|
||||
void BinarySensor::send_state_internal(bool new_state) {
|
||||
// copy the new state to the visible property for backwards compatibility, before any callbacks
|
||||
this->state = new_state;
|
||||
// Note that set_new_state_ de-dups and will only trigger callbacks if the state has actually changed
|
||||
this->set_new_state(new_state);
|
||||
}
|
||||
|
||||
bool BinarySensor::set_new_state(const optional<bool> &new_state) {
|
||||
if (StatefulEntityBase::set_new_state(new_state)) {
|
||||
// weirdly, this file could be compiled even without USE_BINARY_SENSOR defined
|
||||
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_binary_sensor_update(this);
|
||||
#endif
|
||||
ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
|
||||
ESP_LOGV(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -32,7 +32,10 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
|
||||
*/
|
||||
class BinarySensor : public StatefulEntityBase<bool> {
|
||||
public:
|
||||
explicit BinarySensor(){};
|
||||
explicit BinarySensor() = default;
|
||||
|
||||
const bool &get_state() const override { return this->state; }
|
||||
void set_trigger_on_initial_state(bool value) { this->trigger_on_initial_state_ = value; }
|
||||
|
||||
/** Publish a new state to the front-end.
|
||||
*
|
||||
@@ -54,16 +57,24 @@ class BinarySensor : public StatefulEntityBase<bool> {
|
||||
|
||||
// ========== INTERNAL METHODS ==========
|
||||
// (In most use cases you won't need these)
|
||||
void send_state_internal(bool new_state);
|
||||
void send_state_internal(bool new_state) {
|
||||
// Fast path: skip virtual dispatch when state hasn't changed
|
||||
if (this->flags_.has_state && this->state == new_state)
|
||||
return;
|
||||
this->set_new_state(new_state);
|
||||
}
|
||||
|
||||
/// Return whether this binary sensor has outputted a state.
|
||||
virtual bool is_status_binary_sensor() const;
|
||||
|
||||
// For backward compatibility, provide an accessible property
|
||||
|
||||
/// The current state of this binary sensor. Also used as the backing storage for StatefulEntityBase.
|
||||
bool state{};
|
||||
|
||||
protected:
|
||||
bool get_trigger_on_initial_state() const override { return this->trigger_on_initial_state_; }
|
||||
void set_state_value(const bool &value) override { this->state = value; }
|
||||
|
||||
bool trigger_on_initial_state_{true};
|
||||
#ifdef USE_BINARY_SENSOR_FILTER
|
||||
Filter *filter_list_{nullptr};
|
||||
#endif
|
||||
@@ -73,7 +84,7 @@ class BinarySensor : public StatefulEntityBase<bool> {
|
||||
|
||||
class BinarySensorInitiallyOff : public BinarySensor {
|
||||
public:
|
||||
bool has_state() const override { return true; }
|
||||
BinarySensorInitiallyOff() { this->set_has_state(true); }
|
||||
};
|
||||
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -76,14 +76,11 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
|
||||
|
||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
|
||||
|
||||
AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {}
|
||||
|
||||
optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||
// AutorepeatFilterBase
|
||||
optional<bool> AutorepeatFilterBase::new_value(bool value) {
|
||||
if (value) {
|
||||
// Ignore if already running
|
||||
if (this->active_timing_ != 0)
|
||||
return {};
|
||||
|
||||
this->next_timing_();
|
||||
return true;
|
||||
} else {
|
||||
@@ -94,34 +91,26 @@ optional<bool> AutorepeatFilter::new_value(bool value) {
|
||||
}
|
||||
}
|
||||
|
||||
void AutorepeatFilter::next_timing_() {
|
||||
// Entering this method
|
||||
// 1st time: starts waiting the first delay
|
||||
// 2nd time: starts waiting the second delay and starts toggling with the first time_off / _on
|
||||
// last time: no delay to start but have to bump the index to reflect the last
|
||||
if (this->active_timing_ < this->timings_.size()) {
|
||||
void AutorepeatFilterBase::next_timing_() {
|
||||
if (this->active_timing_ < this->timings_count_) {
|
||||
this->set_timeout(AUTOREPEAT_TIMING_ID, this->timings_[this->active_timing_].delay,
|
||||
[this]() { this->next_timing_(); });
|
||||
}
|
||||
|
||||
if (this->active_timing_ <= this->timings_.size()) {
|
||||
if (this->active_timing_ <= this->timings_count_) {
|
||||
this->active_timing_++;
|
||||
}
|
||||
|
||||
if (this->active_timing_ == 2)
|
||||
this->next_value_(false);
|
||||
|
||||
// Leaving this method: if the toggling is started, it has to use [active_timing_ - 2] for the intervals
|
||||
}
|
||||
|
||||
void AutorepeatFilter::next_value_(bool val) {
|
||||
void AutorepeatFilterBase::next_value_(bool val) {
|
||||
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
|
||||
this->output(val); // This is at least the second one so not initial
|
||||
this->output(val);
|
||||
this->set_timeout(AUTOREPEAT_ON_OFF_ID, val ? timing.time_on : timing.time_off,
|
||||
[this, val]() { this->next_value_(!val); });
|
||||
}
|
||||
|
||||
float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
float AutorepeatFilterBase::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BINARY_SENSOR_FILTER
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -34,7 +36,7 @@ class TimeoutFilter : public Filter, public Component {
|
||||
template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; }
|
||||
|
||||
protected:
|
||||
TemplatableValue<uint32_t> timeout_delay_{};
|
||||
TemplatableFn<uint32_t> timeout_delay_{};
|
||||
};
|
||||
|
||||
class DelayedOnOffFilter final : public Filter, public Component {
|
||||
@@ -47,8 +49,8 @@ class DelayedOnOffFilter final : public Filter, public Component {
|
||||
template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; }
|
||||
|
||||
protected:
|
||||
TemplatableValue<uint32_t> on_delay_{};
|
||||
TemplatableValue<uint32_t> off_delay_{};
|
||||
TemplatableFn<uint32_t> on_delay_{};
|
||||
TemplatableFn<uint32_t> off_delay_{};
|
||||
};
|
||||
|
||||
class DelayedOnFilter : public Filter, public Component {
|
||||
@@ -60,7 +62,7 @@ class DelayedOnFilter : public Filter, public Component {
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
protected:
|
||||
TemplatableValue<uint32_t> delay_{};
|
||||
TemplatableFn<uint32_t> delay_{};
|
||||
};
|
||||
|
||||
class DelayedOffFilter : public Filter, public Component {
|
||||
@@ -72,7 +74,7 @@ class DelayedOffFilter : public Filter, public Component {
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
protected:
|
||||
TemplatableValue<uint32_t> delay_{};
|
||||
TemplatableFn<uint32_t> delay_{};
|
||||
};
|
||||
|
||||
class InvertFilter : public Filter {
|
||||
@@ -86,22 +88,39 @@ struct AutorepeatFilterTiming {
|
||||
uint32_t time_on;
|
||||
};
|
||||
|
||||
class AutorepeatFilter : public Filter, public Component {
|
||||
/// Non-template base for AutorepeatFilter — all methods in filter.cpp.
|
||||
/// Lambdas capture this base pointer, so set_timeout/cancel_timeout are instantiated once.
|
||||
class AutorepeatFilterBase : public Filter, public Component {
|
||||
public:
|
||||
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings);
|
||||
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
AutorepeatFilterBase(const AutorepeatFilterBase &) = delete;
|
||||
AutorepeatFilterBase &operator=(const AutorepeatFilterBase &) = delete;
|
||||
|
||||
protected:
|
||||
AutorepeatFilterBase() = default;
|
||||
void next_timing_();
|
||||
void next_value_(bool val);
|
||||
|
||||
FixedVector<AutorepeatFilterTiming> timings_;
|
||||
const AutorepeatFilterTiming *timings_{nullptr};
|
||||
uint8_t timings_count_{0};
|
||||
uint8_t active_timing_{0};
|
||||
};
|
||||
|
||||
/// Template wrapper that provides inline std::array storage for timings.
|
||||
/// N is set by code generation to match the exact number of timings configured in YAML.
|
||||
template<size_t N> class AutorepeatFilter : public AutorepeatFilterBase {
|
||||
public:
|
||||
explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) {
|
||||
init_array_from(this->timings_storage_, timings);
|
||||
this->timings_ = this->timings_storage_.data();
|
||||
this->timings_count_ = N;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::array<AutorepeatFilterTiming, N> timings_storage_{};
|
||||
};
|
||||
|
||||
class LambdaFilter : public Filter {
|
||||
public:
|
||||
explicit LambdaFilter(std::function<optional<bool>(bool)> f);
|
||||
@@ -136,7 +155,7 @@ class SettleFilter : public Filter, public Component {
|
||||
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
|
||||
|
||||
protected:
|
||||
TemplatableValue<uint32_t> delay_{};
|
||||
TemplatableFn<uint32_t> delay_{};
|
||||
bool steady_{true};
|
||||
};
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user