mirror of
https://github.com/esphome/esphome.git
synced 2026-08-29 01:03:29 +00:00
Compare commits
689
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e9499d87d | ||
|
|
145373ed13 | ||
|
|
8749c3d18d | ||
|
|
3426eb2733 | ||
|
|
688e833413 | ||
|
|
94b47496c8 | ||
|
|
19b434f99c | ||
|
|
0b78800e3d | ||
|
|
515aa96844 | ||
|
|
6a86ef0ee0 | ||
|
|
4eba9ee41a | ||
|
|
b579751bdf | ||
|
|
bc8d0840eb | ||
|
|
3f615f87cf | ||
|
|
c6d423159c | ||
|
|
791590c8a3 | ||
|
|
036e5cda7e | ||
|
|
44860ff512 | ||
|
|
90927ba788 | ||
|
|
169c055011 | ||
|
|
8623b6836d | ||
|
|
802a14f90a | ||
|
|
69a3ab6fb1 | ||
|
|
2cd5db4854 | ||
|
|
767b60a914 | ||
|
|
7ca1b79544 | ||
|
|
a25509bda0 | ||
|
|
04825d3a58 | ||
|
|
e714848fb9 | ||
|
|
d7ee824228 | ||
|
|
862b7f7c3e | ||
|
|
77c41d2579 | ||
|
|
4d92741e69 | ||
|
|
b2ac49d8a8 | ||
|
|
8fb6496583 | ||
|
|
5f3d73720e | ||
|
|
cdd8a16cb9 | ||
|
|
b3d9aa2d84 | ||
|
|
acd3c4f156 | ||
|
|
37b78ad46a | ||
|
|
a15fae6a1e | ||
|
|
c866add320 | ||
|
|
2a05372a51 | ||
|
|
4efc3ecb70 | ||
|
|
4457158e3b | ||
|
|
19866c60ee | ||
|
|
ad437cbd15 | ||
|
|
7e236bc762 | ||
|
|
6eed2e5d2b | ||
|
|
3f0ec0b157 | ||
|
|
7c5105b16e | ||
|
|
b97c877992 | ||
|
|
ca96365143 | ||
|
|
3b6c6e3dad | ||
|
|
454e380057 | ||
|
|
cd1e5aad67 | ||
|
|
a8c4018536 | ||
|
|
ad508d79af | ||
|
|
32a0a5c55c | ||
|
|
b103df1bb1 | ||
|
|
c2a153a946 | ||
|
|
200ee0c9c3 | ||
|
|
81c375af0b | ||
|
|
3ca65ffb0d | ||
|
|
68b474fd00 | ||
|
|
efa7fae845 | ||
|
|
29c290166b | ||
|
|
49fc4be861 | ||
|
|
d348284d9e | ||
|
|
b8408e14b9 | ||
|
|
b11a34af5c | ||
|
|
5ed20805ce | ||
|
|
7674477dd0 | ||
|
|
2b11fbe164 | ||
|
|
3a248d526d | ||
|
|
2f49405740 | ||
|
|
d418472a8f | ||
|
|
5f6a910e2d | ||
|
|
7a715ee583 | ||
|
|
d8dc1dc70e | ||
|
|
5479752da6 | ||
|
|
b0e4711cc3 | ||
|
|
d744a98f5b | ||
|
|
a03945bbfb | ||
|
|
bc2b4bd381 | ||
|
|
f8acaab6c1 | ||
|
|
5f2b8f7251 | ||
|
|
d80d2c7034 | ||
|
|
1f5152892c | ||
|
|
89671f9e0d | ||
|
|
c6f9153e63 | ||
|
|
aa3268a6d8 | ||
|
|
879cbf6264 | ||
|
|
4da8868510 | ||
|
|
df76fc6fba | ||
|
|
0d75f597b3 | ||
|
|
9974ad97fa | ||
|
|
c8ebe461d1 | ||
|
|
936bc708af | ||
|
|
13dcf54a43 | ||
|
|
5bccc41ca3 | ||
|
|
5ba31dcccb | ||
|
|
3fe4e255d8 | ||
|
|
de1361ac0e | ||
|
|
d877fb021c | ||
|
|
3c56b9e66e | ||
|
|
67608f9386 | ||
|
|
1ceaf699c2 | ||
|
|
5aaf5e8150 | ||
|
|
5ce8c68df5 | ||
|
|
412b7a8b99 | ||
|
|
e594888176 | ||
|
|
6bdd8b1163 | ||
|
|
d75f85b089 | ||
|
|
34c1a009b6 | ||
|
|
06e3525f82 | ||
|
|
476ec16a3d | ||
|
|
c9e91be35f | ||
|
|
d9b3536e9f | ||
|
|
0ed990e311 | ||
|
|
69d997521f | ||
|
|
31afd94a30 | ||
|
|
6f7b9a1482 | ||
|
|
4affd574ac | ||
|
|
4840740032 | ||
|
|
6c23ad661d | ||
|
|
ae5e5b3011 | ||
|
|
3ab464c345 | ||
|
|
b74de2dfa2 | ||
|
|
41171e61d1 | ||
|
|
7ea8892e0a | ||
|
|
dce979245d | ||
|
|
6e65634408 | ||
|
|
6300e9b72e | ||
|
|
bdf8dec028 | ||
|
|
a872626fba | ||
|
|
00bdbf8ed1 | ||
|
|
cb4dd32e19 | ||
|
|
6381ed97b9 | ||
|
|
786e9ef278 | ||
|
|
25fb3983cb | ||
|
|
4510f10491 | ||
|
|
0ecc7045fa | ||
|
|
5cc1f001fa | ||
|
|
24ce2195da | ||
|
|
d5fe45db66 | ||
|
|
a027d06231 | ||
|
|
76caadc594 | ||
|
|
7d34efecf2 | ||
|
|
c4096d44d8 | ||
|
|
8f7534f39a | ||
|
|
6bac1ed950 | ||
|
|
57241e40b8 | ||
|
|
76ddbe5ebd | ||
|
|
81d9b943aa | ||
|
|
28f0a7b155 | ||
|
|
531bd8ec62 | ||
|
|
40e6e406d7 | ||
|
|
6cfbe5b166 | ||
|
|
b738b36a39 | ||
|
|
ac66e3a96c | ||
|
|
e7fc3937bf | ||
|
|
70f4555ec8 | ||
|
|
5f20ddb630 | ||
|
|
6cfb915aeb | ||
|
|
04a44d6e63 | ||
|
|
cb10904fa3 | ||
|
|
89e5ba00de | ||
|
|
81b1678498 | ||
|
|
ba0d9e77db | ||
|
|
330f070921 | ||
|
|
f6ae5e4892 | ||
|
|
b0b174f3e2 | ||
|
|
d8857080f0 | ||
|
|
c285af3857 | ||
|
|
d7d236c6bc | ||
|
|
de00530c0e | ||
|
|
acf5ce718c | ||
|
|
5904928f0c | ||
|
|
dceeee69d3 | ||
|
|
68f3a6b9a5 | ||
|
|
662bf7d7f0 | ||
|
|
0d71ab8efb | ||
|
|
4ce4768ebd | ||
|
|
fb13327922 | ||
|
|
4c62420f1b | ||
|
|
ca3f31643f | ||
|
|
d770004e0e | ||
|
|
b28efcd545 | ||
|
|
6b6d27f905 | ||
|
|
603c3539a3 | ||
|
|
f248a85b51 | ||
|
|
5a3d7e3292 | ||
|
|
c6d329db64 | ||
|
|
bdb203d742 | ||
|
|
f509516d60 | ||
|
|
4c856949c2 | ||
|
|
c90a5f4cff | ||
|
|
964fc1ef3f | ||
|
|
03c7d3b343 | ||
|
|
d405db1268 | ||
|
|
8cac10d319 | ||
|
|
030a3b7ab3 | ||
|
|
dfc26e9f09 | ||
|
|
98f138f6b2 | ||
|
|
4f9941decb | ||
|
|
385681121f | ||
|
|
c395861fa3 | ||
|
|
30b2bbfe3a | ||
|
|
3af17d5c92 | ||
|
|
72d6864938 | ||
|
|
89ffacaffb | ||
|
|
6eff668dba | ||
|
|
350202efa6 | ||
|
|
14388c859a | ||
|
|
7061bd7e70 | ||
|
|
fe274653bd | ||
|
|
7beb0078c2 | ||
|
|
e26dbb6d75 | ||
|
|
7aa0f30ceb | ||
|
|
438a4ddc14 | ||
|
|
5a749c4b5d | ||
|
|
3de58dae99 | ||
|
|
fd2971bd70 | ||
|
|
8a9bb7741a | ||
|
|
393dabadf8 | ||
|
|
56dc9124ab | ||
|
|
92b655c5a0 | ||
|
|
c474421399 | ||
|
|
ca611a3159 | ||
|
|
6dbef0d0bb | ||
|
|
832c6feecc | ||
|
|
41b0fe6747 | ||
|
|
e580e267ce | ||
|
|
d3590ffd7c | ||
|
|
9179fe26ce | ||
|
|
54429bcdfb | ||
|
|
e6c49e2bd9 | ||
|
|
8018a0d930 | ||
|
|
20920e0971 | ||
|
|
81be569a85 | ||
|
|
641c80b193 | ||
|
|
8170cd3d96 | ||
|
|
e2eae002e6 | ||
|
|
065867f815 | ||
|
|
5aebd7cb41 | ||
|
|
d84293931b | ||
|
|
6497ca1496 | ||
|
|
759ddfc91d | ||
|
|
062c542c36 | ||
|
|
2c32ac2221 | ||
|
|
d23e9a205a | ||
|
|
03a443c2b7 | ||
|
|
0c97e2b354 | ||
|
|
4803a92554 | ||
|
|
3a35c75ec1 | ||
|
|
8536d54998 | ||
|
|
10bccf3fc0 | ||
|
|
b070d1631c | ||
|
|
abcfaa1810 | ||
|
|
c822b80c95 | ||
|
|
ae8aa3fee9 | ||
|
|
ce0a0da42a | ||
|
|
2199fcc851 | ||
|
|
554e19ccdb | ||
|
|
9662fea8b9 | ||
|
|
b4031dc53a | ||
|
|
eb958bd0a0 | ||
|
|
2da0319eaa | ||
|
|
c376d32db4 | ||
|
|
59fc108aa7 | ||
|
|
a272ca9e17 | ||
|
|
e40537cc34 | ||
|
|
9d761102e5 | ||
|
|
1372ec8ad2 | ||
|
|
dde6906f98 | ||
|
|
c4ddad0237 | ||
|
|
6d4d9aa1ce | ||
|
|
caff71c17a | ||
|
|
8e4c9c31e6 | ||
|
|
a478ae4a46 | ||
|
|
bd69eab7fb | ||
|
|
5adaee5408 | ||
|
|
b903c884fd | ||
|
|
9731c9527a | ||
|
|
f595a32434 | ||
|
|
1959913347 | ||
|
|
035555b24c | ||
|
|
f8de6fe0d1 | ||
|
|
5137c3f16b | ||
|
|
ea3ff0e372 | ||
|
|
1157189b36 | ||
|
|
dd7a25b302 | ||
|
|
7c0e03f309 | ||
|
|
4efd308345 | ||
|
|
f741c274d5 | ||
|
|
4e172a5774 | ||
|
|
e4a54b27ed | ||
|
|
7b3330d5fe | ||
|
|
e681c441d9 | ||
|
|
8cbb83a06d | ||
|
|
bac6530203 | ||
|
|
355d4788b8 | ||
|
|
b8dac2dc89 | ||
|
|
585d818e7a | ||
|
|
80b492f638 | ||
|
|
692bb2a810 | ||
|
|
4a874a374a | ||
|
|
54cd17591d | ||
|
|
98b595c95d | ||
|
|
e16cfded5d | ||
|
|
746b294ed7 | ||
|
|
cf31c08a5c | ||
|
|
e7574a574b | ||
|
|
33484108a9 | ||
|
|
e697a40fda | ||
|
|
ec6123e0ef | ||
|
|
da3c6dec7f | ||
|
|
cfac36c647 | ||
|
|
d179f43e7c | ||
|
|
327630523c | ||
|
|
0e4f4237fd | ||
|
|
96a74cd217 | ||
|
|
dc64c93f39 | ||
|
|
1b109518f7 | ||
|
|
5793a5f08a | ||
|
|
9d0d1b42d5 | ||
|
|
8230c16f12 | ||
|
|
905c806009 | ||
|
|
42f59f94c9 | ||
|
|
35bb8388a2 | ||
|
|
6fda5b9a07 | ||
|
|
5ac6e2b3c6 | ||
|
|
b08026e413 | ||
|
|
f6c6708f24 | ||
|
|
48c6948ae8 | ||
|
|
e714ef4dfc | ||
|
|
d1f065671e | ||
|
|
e9466ff738 | ||
|
|
4d6300b400 | ||
|
|
030e79fa5a | ||
|
|
02da5c6484 | ||
|
|
b2440cb655 | ||
|
|
160d8b8f0c | ||
|
|
8899713ef9 | ||
|
|
f3cdefce21 | ||
|
|
b115813fbe | ||
|
|
ab45ab316a | ||
|
|
da9c0bdd14 | ||
|
|
5b3a6c05bf | ||
|
|
a1e6370e8b | ||
|
|
dada0f2c2b | ||
|
|
cd53681787 | ||
|
|
a22c2a9768 | ||
|
|
18206967a1 | ||
|
|
791e2b8204 | ||
|
|
d9e18ee982 | ||
|
|
d26d3fdc33 | ||
|
|
227f5c67e2 | ||
|
|
044d593f09 | ||
|
|
d6d80ee323 | ||
|
|
f0651e5c9b | ||
|
|
f24b731f95 | ||
|
|
01ad424d12 | ||
|
|
435d522683 | ||
|
|
a63c3bc0c7 | ||
|
|
b83ce91528 | ||
|
|
a30238aab6 | ||
|
|
47156c9a5b | ||
|
|
78240c9a46 | ||
|
|
5a9f06e584 | ||
|
|
ecb007da70 | ||
|
|
ad1a4fca36 | ||
|
|
832a738588 | ||
|
|
fedb3ac5c1 | ||
|
|
cb4e55e444 | ||
|
|
3ef5a8e6a4 | ||
|
|
0e915e9b8b | ||
|
|
dba3b287dd | ||
|
|
ce019f508d | ||
|
|
5c2286cc4a | ||
|
|
efc0a94112 | ||
|
|
c60062c418 | ||
|
|
763a1d9371 | ||
|
|
4db1666024 | ||
|
|
0dc69aab1e | ||
|
|
a282cb095e | ||
|
|
259e7182a3 | ||
|
|
74bdf275d2 | ||
|
|
14499223fd | ||
|
|
6aab523dd9 | ||
|
|
1f31e51446 | ||
|
|
c062d0c717 | ||
|
|
149ca8c954 | ||
|
|
2acba9b364 | ||
|
|
36b0b62a8f | ||
|
|
f4bc7dda35 | ||
|
|
cbbc0a8fa9 | ||
|
|
7d246893be | ||
|
|
dcabaedff1 | ||
|
|
27aef88077 | ||
|
|
bff8bea83e | ||
|
|
bc98ba3b46 | ||
|
|
49d060fdfd | ||
|
|
2acde317d2 | ||
|
|
f2ff1cc986 | ||
|
|
4e6a8e297e | ||
|
|
86ac8f661b | ||
|
|
689861ffc9 | ||
|
|
eec01ea793 | ||
|
|
b3f058613c | ||
|
|
1eda5a8534 | ||
|
|
520bf3f63b | ||
|
|
1cf5df3d59 | ||
|
|
91836a8c69 | ||
|
|
f4482275cb | ||
|
|
f599e37375 | ||
|
|
fd7e340e81 | ||
|
|
84a8ac3772 | ||
|
|
910147139a | ||
|
|
4a459e3d0d | ||
|
|
35a1136bea | ||
|
|
3b091676bf | ||
|
|
d7af5f37b6 | ||
|
|
121795fa8d | ||
|
|
a2267eb686 | ||
|
|
dbd6f323c0 | ||
|
|
3a9a6edc0e | ||
|
|
11875ec040 | ||
|
|
a97b836012 | ||
|
|
afeb1a5ea1 | ||
|
|
803eef7de6 | ||
|
|
fb06b4d631 | ||
|
|
a6ca775a48 | ||
|
|
65609bb94b | ||
|
|
60cf44de3b | ||
|
|
f71867ed2d | ||
|
|
f08e06f574 | ||
|
|
27f4db8458 | ||
|
|
e99e0cc8b8 | ||
|
|
d119ad6c60 | ||
|
|
74fc2e367a | ||
|
|
ea10f94376 | ||
|
|
ef1d77885d | ||
|
|
dccf55eadc | ||
|
|
65704e881f | ||
|
|
a30e82459f | ||
|
|
5a300e92f1 | ||
|
|
8e9fb0f93c | ||
|
|
4d63564cdd | ||
|
|
11ea819bc7 | ||
|
|
abc9098bd8 | ||
|
|
00cffa09a2 | ||
|
|
aa944456e0 | ||
|
|
409d74a48d | ||
|
|
7957808f00 | ||
|
|
d47dc0d992 | ||
|
|
9624abcd57 | ||
|
|
2c229debe8 | ||
|
|
6a0ce046fd | ||
|
|
6b6344b454 | ||
|
|
09fd3fb53b | ||
|
|
73ab04330e | ||
|
|
b8bb4453e0 | ||
|
|
91eed262c8 | ||
|
|
3063b9067a | ||
|
|
62c27826ee | ||
|
|
c95e0b90f5 | ||
|
|
84a3c39562 | ||
|
|
5cf2135038 | ||
|
|
7ad97697b2 | ||
|
|
5bd328e6c4 | ||
|
|
5cec72020f | ||
|
|
ef2f16713a | ||
|
|
12f8b14418 | ||
|
|
790c6d6353 | ||
|
|
77fa411f04 | ||
|
|
bc6b997350 | ||
|
|
52bfc0efb1 | ||
|
|
e20c43520b | ||
|
|
9b12934307 | ||
|
|
f93d46e684 | ||
|
|
d33a097152 | ||
|
|
46f90d0c54 | ||
|
|
5177972c04 | ||
|
|
8008eda57f | ||
|
|
466f890e1f | ||
|
|
ece90ee97b | ||
|
|
57b7f03e42 | ||
|
|
4ab2a7ba98 | ||
|
|
65f5c302c5 | ||
|
|
a0f0ad8729 | ||
|
|
51af18cdfb | ||
|
|
57a9c71da8 | ||
|
|
3d8a3c6527 | ||
|
|
1483b7c10b | ||
|
|
37de984df5 | ||
|
|
7fe0847e5c | ||
|
|
80815f1dc7 | ||
|
|
8f5f349e1b | ||
|
|
83b4da1777 | ||
|
|
12c1b15578 | ||
|
|
6a84d67786 | ||
|
|
7a3f700206 | ||
|
|
7d9c18a330 | ||
|
|
4cfa4893ef | ||
|
|
5a4337c240 | ||
|
|
edd4a86d14 | ||
|
|
44dcd82d78 | ||
|
|
12da2140cf | ||
|
|
c006e9804a | ||
|
|
6343c11873 | ||
|
|
375cf95240 | ||
|
|
18e3eac419 | ||
|
|
0620ce81c3 | ||
|
|
2564ad0deb | ||
|
|
e3936f500f | ||
|
|
cea86989c9 | ||
|
|
e82453e80e | ||
|
|
3eec5085da | ||
|
|
57c8648210 | ||
|
|
46fa42fa33 | ||
|
|
9bd548ba92 | ||
|
|
72da2b1ce2 | ||
|
|
ab58f1080a | ||
|
|
47942fb166 | ||
|
|
f326c3f04d | ||
|
|
3b51ec2ae7 | ||
|
|
2ab09e1a77 | ||
|
|
a3ea77c2f1 | ||
|
|
7fe4399b94 | ||
|
|
545f762568 | ||
|
|
e83439eaae | ||
|
|
b6a9761dae | ||
|
|
fbe4b39a16 | ||
|
|
ecca240eef | ||
|
|
347a6155f8 | ||
|
|
a8e721abeb | ||
|
|
2d62ea78d2 | ||
|
|
8ef0f38f4e | ||
|
|
3c47ab42d6 | ||
|
|
aafeca5859 | ||
|
|
132f494195 | ||
|
|
5e9de7c94b | ||
|
|
b7d0b676fc | ||
|
|
55b45fc6fb | ||
|
|
6f8dbb6fbc | ||
|
|
ca97c86d65 | ||
|
|
828eac90f3 | ||
|
|
d9359a70c1 | ||
|
|
29404a782c | ||
|
|
e75a7a61fa | ||
|
|
185f12266a | ||
|
|
c455991962 | ||
|
|
f735dcadc0 | ||
|
|
78a65eabdc | ||
|
|
b3fda9973e | ||
|
|
4a85c98285 | ||
|
|
2c92a2498e | ||
|
|
74e22b5ad7 | ||
|
|
e9e77d02a0 | ||
|
|
7418fcce8d | ||
|
|
b768e2a1ce | ||
|
|
6084314cc9 | ||
|
|
2df953f3d7 | ||
|
|
10e592fa3a | ||
|
|
a99a8f364e | ||
|
|
200a1644a5 | ||
|
|
9daae377fc | ||
|
|
28384673f6 | ||
|
|
0b1065feee | ||
|
|
47da743d11 | ||
|
|
b361342bde | ||
|
|
d34eed6d2d | ||
|
|
12238eec3b | ||
|
|
fe53ff6b6b | ||
|
|
470226ca03 | ||
|
|
7b7107556f | ||
|
|
d0f38d7644 | ||
|
|
f90b776071 | ||
|
|
e26237e57d | ||
|
|
d2c3f749ab | ||
|
|
b7cc271219 | ||
|
|
17eed7055b | ||
|
|
b7121940c8 | ||
|
|
07fa16e2e7 | ||
|
|
8aa7db15e5 | ||
|
|
8b637b339b | ||
|
|
4f866c563b | ||
|
|
7a999f9a48 | ||
|
|
0a88c81d95 | ||
|
|
804e8fb856 | ||
|
|
43939a9199 | ||
|
|
f414a07bcd | ||
|
|
5c9d050ebe | ||
|
|
285a508e09 | ||
|
|
92f55f721f | ||
|
|
476d540065 | ||
|
|
ae730d6357 | ||
|
|
9823205ef3 | ||
|
|
d1391c2b10 | ||
|
|
8b888f31e0 | ||
|
|
6a247dfe91 | ||
|
|
482869fbbe | ||
|
|
4dea147386 | ||
|
|
1fd6337254 | ||
|
|
4ce6d59484 | ||
|
|
014cc19902 | ||
|
|
b9041566ea | ||
|
|
3a403c40d5 | ||
|
|
096e71bd67 | ||
|
|
fffa902a1a | ||
|
|
463e3833da | ||
|
|
4416aacebb | ||
|
|
15a626bcf3 | ||
|
|
a347a2e879 | ||
|
|
7be4566b41 | ||
|
|
96e26c6a5f | ||
|
|
7362c01c67 | ||
|
|
e45b4e4938 | ||
|
|
e32329cc11 | ||
|
|
199e368fe2 | ||
|
|
55726120db | ||
|
|
346ba7e831 | ||
|
|
9518006724 | ||
|
|
3d5f6f692f | ||
|
|
4712c15c75 | ||
|
|
9bc72529a6 | ||
|
|
6d20943a9c | ||
|
|
a3af82867b | ||
|
|
27483a4101 | ||
|
|
6d20ebc66b | ||
|
|
031a038b49 | ||
|
|
f6c7434b2a | ||
|
|
3d9fecb562 | ||
|
|
ebe93e2c68 | ||
|
|
1f4fcead38 | ||
|
|
1f000ba668 | ||
|
|
e0d28d7f5c | ||
|
|
47a58dd799 | ||
|
|
37bea1c153 | ||
|
|
d1a7b8df8b | ||
|
|
c01f24553c | ||
|
|
07e8b303b9 | ||
|
|
ebb0923362 | ||
|
|
e1c279718f | ||
|
|
cf764740cf | ||
|
|
58d549ed4c | ||
|
|
6b22d8068d | ||
|
|
443d8f1f28 | ||
|
|
1ec21a2245 | ||
|
|
bb7d4c3630 | ||
|
|
f42fe9af29 | ||
|
|
0bc2d71370 | ||
|
|
594c12b3d9 | ||
|
|
bca72e9b6d | ||
|
|
ce09504c92 | ||
|
|
dda4566b9e | ||
|
|
46a5665a66 | ||
|
|
9161f74bb1 | ||
|
|
3f01f9895f | ||
|
|
801a1817b5 | ||
|
|
32c76ae828 | ||
|
|
c664f5fc95 | ||
|
|
2bc4681fd6 | ||
|
|
646501b0ef | ||
|
|
de3e657d8b | ||
|
|
1add726892 | ||
|
|
5a000cf5e4 | ||
|
|
6ed676fe32 | ||
|
|
7cceddb8a3 | ||
|
|
039b897e7b | ||
|
|
b178f74e5d | ||
|
|
e5224e22ae | ||
|
|
be66e8b99c | ||
|
|
4b91c8aff4 | ||
|
|
9d7997f55a | ||
|
|
4db47de556 | ||
|
|
c8de632764 | ||
|
|
b794b7b1d1 | ||
|
|
add18d4e35 | ||
|
|
02c1810c3a | ||
|
|
4f3153375a | ||
|
|
1c3a67b5e8 | ||
|
|
236ff33a09 | ||
|
|
7c07fb48c5 | ||
|
|
d72bab79d7 | ||
|
|
617e2ec1e0 | ||
|
|
1afac0312d |
@@ -32,7 +32,7 @@ runs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||
# downstream jobs rely on is preserved.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -49,7 +49,7 @@ runs:
|
||||
python -m venv venv
|
||||
source venv/bin/activate
|
||||
python --version
|
||||
uv pip install -r requirements.txt -r requirements_test.txt
|
||||
uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt
|
||||
uv pip install -e .
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
|
||||
@@ -58,5 +58,5 @@ runs:
|
||||
python -m venv venv
|
||||
source ./venv/Scripts/activate
|
||||
python --version
|
||||
uv pip install -r requirements.txt -r requirements_test.txt
|
||||
uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt
|
||||
uv pip install -e .
|
||||
|
||||
@@ -70,6 +70,7 @@ async function isStackedPr(github, context) {
|
||||
async function detectMergeBranch(github, context) {
|
||||
const labels = new Set();
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
const defaultBranch = context.payload.repository.default_branch;
|
||||
|
||||
if (baseRef === 'release') {
|
||||
labels.add('merging-to-release');
|
||||
@@ -78,7 +79,7 @@ async function detectMergeBranch(github, context) {
|
||||
} else if (await isStackedPr(github, context)) {
|
||||
// GitHub manages the merge order for a stack, so these are not blocked.
|
||||
labels.add('stacked-pr');
|
||||
} else if (baseRef !== 'dev') {
|
||||
} else if (baseRef !== defaultBranch) {
|
||||
// A chain built by hand: it must not merge until its base branch does.
|
||||
labels.add('chained-pr');
|
||||
}
|
||||
|
||||
@@ -43,14 +43,14 @@ const WITHOUT_SCHEMA = 'CODEOWNERS = ["@esphome/core"]';
|
||||
|
||||
// Builds a fresh context for detectMergeBranch tests instead of mutating the
|
||||
// shared CONTEXT fixture above (which other describe blocks rely on).
|
||||
function makeMergeContext(baseRef, { stack } = {}) {
|
||||
function makeMergeContext(baseRef, { stack, defaultBranch = 'dev' } = {}) {
|
||||
const pull_request = { number: 1, base: { ref: baseRef } };
|
||||
if (stack !== undefined) {
|
||||
pull_request.stack = stack;
|
||||
}
|
||||
return {
|
||||
repo: { owner: 'esphome', repo: 'esphome' },
|
||||
payload: { pull_request }
|
||||
payload: { pull_request, repository: { default_branch: defaultBranch } }
|
||||
};
|
||||
}
|
||||
|
||||
@@ -136,6 +136,21 @@ describe('detectMergeBranch', () => {
|
||||
assert.deepEqual(Array.from(labels).sort(), ['chained-pr']);
|
||||
assert.equal(state.calls, 1);
|
||||
});
|
||||
|
||||
it('base ref matches default branch adds no labels', async () => {
|
||||
const { github } = makeStackGithub({ stack: null });
|
||||
const context = makeMergeContext('other', { defaultBranch: 'other' });
|
||||
const labels = await detectMergeBranch(github, context);
|
||||
assert.deepEqual(Array.from(labels).sort(), []);
|
||||
});
|
||||
|
||||
it('base ref dev when the default branch is main adds chained-pr', async () => {
|
||||
const { github } = makeStackGithub({ stack: null });
|
||||
const context = makeMergeContext('dev', { defaultBranch: 'main' });
|
||||
const labels = await detectMergeBranch(github, context);
|
||||
assert.deepEqual(Array.from(labels).sort(), ['chained-pr']);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -29,7 +29,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull-request-only workflow: a save could never be shared and
|
||||
@@ -41,10 +41,32 @@ jobs:
|
||||
version: "0.11.15"
|
||||
|
||||
- name: Install apt dependencies
|
||||
# PR-only workflow, so nothing on dev could seed a shared apt cache
|
||||
# entry; the cached apt action would save one copy per PR. Plain apt
|
||||
# with every call bounded: the apt.conf.d timeouts make a dead
|
||||
# mirror fail over in seconds, and timeout runs under sudo so it can
|
||||
# kill apt-get itself. Install without update first: image lists are
|
||||
# fresh, and the index refresh is what a congested mirror makes slow.
|
||||
timeout-minutes: 15
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt-cache show protobuf-compiler
|
||||
sudo apt install -y protobuf-compiler
|
||||
sudo tee /etc/apt/apt.conf.d/99ci-acquire-timeouts >/dev/null <<'EOF'
|
||||
Acquire::Retries "1";
|
||||
Acquire::http::Timeout "15";
|
||||
Acquire::https::Timeout "15";
|
||||
EOF
|
||||
# Common path: the image's package lists are fresh enough.
|
||||
if sudo DEBIAN_FRONTEND=noninteractive timeout -k 15 90 \
|
||||
apt-get install -y protobuf-compiler; then
|
||||
protoc --version
|
||||
exit 0
|
||||
fi
|
||||
# Rescue path: refresh the lists once with a generous bound; the
|
||||
# apt config already fails a stalled mirror over quickly.
|
||||
sudo DEBIAN_FRONTEND=noninteractive timeout -k 10 30 \
|
||||
dpkg --configure -a || true
|
||||
sudo timeout -k 15 300 apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive timeout -k 15 300 \
|
||||
apt-get install -y protobuf-compiler
|
||||
protoc --version
|
||||
- name: Install python dependencies
|
||||
run: uv pip install --system aioesphomeapi -c requirements.txt -r requirements_dev.txt
|
||||
|
||||
@@ -67,7 +67,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Determine tag and whether to push
|
||||
id: tag
|
||||
@@ -153,7 +153,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.12"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
|
||||
+125
-45
@@ -49,7 +49,7 @@ jobs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||
# that ``. venv/bin/activate`` see an identical layout.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -68,6 +68,22 @@ jobs:
|
||||
uv pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt
|
||||
uv pip install -e .
|
||||
|
||||
seed-apt-cache:
|
||||
name: Seed apt package cache
|
||||
runs-on: ubuntu-24.04
|
||||
# PR-branch cache saves are invisible to other PRs, so dev/beta/release
|
||||
# pushes seed the one shared entry PR jobs restore. The key is derived
|
||||
# only from the package list and version; keep both identical in every
|
||||
# step that restores it. In ci-status needs so a broken seed fails dev.
|
||||
if: github.event_name == 'push'
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Install apt packages (cached)
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
|
||||
determine-jobs:
|
||||
name: Determine which jobs to run
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -96,6 +112,12 @@ jobs:
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
validate-only-components: ${{ steps.determine.outputs.validate-only-components }}
|
||||
benchmarks: ${{ steps.determine.outputs.benchmarks }}
|
||||
# "true" when this run is a pull request into one of the release
|
||||
# branches. Those pull requests are batches of changes already tested on
|
||||
# their original dev pull requests, so several jobs below trade coverage
|
||||
# for turnaround time on them. Matched exactly, not by prefix, so an
|
||||
# ordinary branch named e.g. "release-notes" is not caught by it.
|
||||
release-pr: ${{ github.base_ref == 'beta' || github.base_ref == 'release' }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -214,7 +236,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- determine-jobs
|
||||
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release') && needs.determine-jobs.outputs.core-ci == 'true'
|
||||
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.release-pr == 'false' && needs.determine-jobs.outputs.core-ci == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -323,7 +345,8 @@ jobs:
|
||||
|
||||
integration-tests:
|
||||
name: Run integration tests (${{ matrix.bucket.name }})
|
||||
runs-on: ubuntu-latest
|
||||
# Must match seed-apt-cache's image: the apt cache key has no OS in it.
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
@@ -335,24 +358,16 @@ jobs:
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
- name: Install ccache
|
||||
# Speeds up the host compiles: tests in a bucket compile overlapping
|
||||
# component sets, so later tests reuse earlier tests' objects.
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends ccache
|
||||
- name: Restore ccache (restore-only)
|
||||
# esphome stores the PlatformIO ccache under the machine-global cache
|
||||
# dir (see _ccache_env() in esphome/platformio/toolchain.py). The
|
||||
# bucket-name prefix prefers a same-bucket seed; the bare prefix falls
|
||||
# back to any seed when the bucket layout differs from dev.
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
- name: Install apt packages (cached)
|
||||
# ccache speeds up the host compiles. A cache hit never touches apt
|
||||
# (mirror outages cannot hang the job); the timeout bounds the cold
|
||||
# path. Packages and version must match seed-apt-cache exactly;
|
||||
# libsdl2-dev is unused here and carried only for cache-key parity.
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
path: ~/.cache/esphome/platformio-ccache
|
||||
key: integration-ccache-${{ matrix.bucket.name }}-${{ github.sha }}
|
||||
restore-keys: |
|
||||
integration-ccache-${{ matrix.bucket.name }}-
|
||||
integration-ccache-
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
@@ -367,7 +382,7 @@ jobs:
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -401,14 +416,6 @@ jobs:
|
||||
# esphome stores the PlatformIO ccache under the machine-global cache
|
||||
# dir (see _ccache_env() in esphome/platformio/toolchain.py).
|
||||
run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s
|
||||
- name: Save ccache
|
||||
# Pull request saves land in per-PR scopes nothing else can reuse;
|
||||
# dev pushes seed the shared copy instead.
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.cache/esphome/platformio-ccache
|
||||
key: integration-ccache-${{ matrix.bucket.name }}-${{ github.sha }}
|
||||
|
||||
import-time:
|
||||
name: Check import esphome.__main__ time
|
||||
@@ -441,12 +448,28 @@ jobs:
|
||||
benchmarks:
|
||||
name: Run CodSpeed benchmarks
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 30
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: >-
|
||||
(github.event_name == 'push' && github.ref_name == 'dev') ||
|
||||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
|
||||
github.repository == 'esphome/esphome' && (
|
||||
(github.event_name == 'push' && github.ref_name == 'dev') ||
|
||||
(
|
||||
github.event_name == 'pull_request' &&
|
||||
needs.determine-jobs.outputs.release-pr == 'false' &&
|
||||
needs.determine-jobs.outputs.benchmarks == 'true'
|
||||
)
|
||||
)
|
||||
# CodSpeed benchmarks require a CodSpeed account linked to the repository to run
|
||||
# (https://codspeed.io) -- disabled on forks that aren't esphome/esphome itself.
|
||||
#
|
||||
# Pull requests into beta and release are skipped as well. CodSpeed compares a
|
||||
# pull request against the newest commit of its base branch that has a benchmark
|
||||
# run of its own, and only dev is benchmarked. A release pull request therefore
|
||||
# falls back to dev's latest run, so every speed-up merged into dev since the
|
||||
# release branched is reported as a regression in the release. The changes there
|
||||
# have already been benchmarked on their original dev pull requests.
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -460,12 +483,58 @@ jobs:
|
||||
- name: Build benchmarks
|
||||
id: build
|
||||
run: |
|
||||
# pipefail: without it a failed build is masked by the grep/cut
|
||||
# pipeline below, leaving BINARY empty and silently dropping every
|
||||
# C++ benchmark from the run while the job still reports success.
|
||||
set -o pipefail
|
||||
. venv/bin/activate
|
||||
export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py)
|
||||
# --build-only prints BUILD_BINARY=<path> to stdout
|
||||
BINARY=$(script/cpp_benchmark.py --all --build-only | grep '^BUILD_BINARY=' | tail -1 | cut -d= -f2-)
|
||||
BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py)
|
||||
export BENCHMARK_LIB_CONFIG
|
||||
# --build-only prints BUILD_BINARY=<path> to stdout; the grep is
|
||||
# non-fatal so a missing marker reaches the check below instead of
|
||||
# tripping errexit at this assignment
|
||||
BINARY=$(script/cpp_benchmark.py --all --build-only | { grep '^BUILD_BINARY=' || true; } | tail -1 | cut -d= -f2-)
|
||||
if [ -z "$BINARY" ]; then
|
||||
echo "::error::Benchmark build did not report a binary path"
|
||||
exit 1
|
||||
fi
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Bound apt fetches and pre-install libc6-dbg
|
||||
# The CodSpeed runner installs valgrind + libc6-dbg via its own
|
||||
# unbounded apt-get update; per-invocation apt options cannot reach
|
||||
# it. The apt.conf.d timeouts below bound every later apt call in
|
||||
# this job, the runner's included. Pre-installing libc6-dbg lets the
|
||||
# runner skip apt once its valgrind cache is restored (it checks
|
||||
# ``dpkg -s libc6-dbg``, so the cache action's unregistered restores
|
||||
# would not count). Install without update first: image lists are
|
||||
# fresh, and the index refresh is what a congested mirror makes
|
||||
# slow. Best effort; the job timeout is the last backstop.
|
||||
timeout-minutes: 15
|
||||
continue-on-error: true
|
||||
run: |
|
||||
sudo tee /etc/apt/apt.conf.d/99ci-acquire-timeouts >/dev/null <<'EOF'
|
||||
Acquire::Retries "1";
|
||||
Acquire::http::Timeout "15";
|
||||
Acquire::https::Timeout "15";
|
||||
EOF
|
||||
if dpkg -s libc6-dbg >/dev/null 2>&1; then
|
||||
echo "libc6-dbg already installed"
|
||||
exit 0
|
||||
fi
|
||||
# Common path: the image's package lists are fresh enough.
|
||||
if sudo DEBIAN_FRONTEND=noninteractive timeout -k 15 90 \
|
||||
apt-get install -y libc6-dbg; then
|
||||
exit 0
|
||||
fi
|
||||
# Rescue path: refresh the lists once with a generous bound; the
|
||||
# apt config already fails a stalled mirror over quickly.
|
||||
sudo DEBIAN_FRONTEND=noninteractive timeout -k 10 30 \
|
||||
dpkg --configure -a || true
|
||||
sudo timeout -k 15 300 apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive timeout -k 15 300 \
|
||||
apt-get install -y libc6-dbg
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3
|
||||
with:
|
||||
@@ -550,24 +619,29 @@ jobs:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
# Key on the exact Python version as well: LibreTiny creates a venv under
|
||||
# ~/.platformio/penv whose interpreter is a symlink into the runner's
|
||||
# hosted toolcache, so a cache saved on an older runner image breaks once
|
||||
# a new image ships a newer patch release and drops the old interpreter.
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ steps.restore-python.outputs.python-version }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev' && matrix.pio_cache_key
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ steps.restore-python.outputs.python-version }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache ESP-IDF install
|
||||
if: matrix.cache_idf
|
||||
@@ -872,7 +946,7 @@ jobs:
|
||||
ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 32 || 16 }}
|
||||
max-parallel: ${{ needs.determine-jobs.outputs.release-pr == 'true' && 32 || 16 }}
|
||||
matrix:
|
||||
batch: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
|
||||
steps:
|
||||
@@ -884,12 +958,17 @@ jobs:
|
||||
- name: List components
|
||||
run: echo ${{ matrix.batch.components }}
|
||||
|
||||
- name: Install apt packages
|
||||
# Not cached: this job is pull-request-only, so a cache save could
|
||||
# never be shared and would only consume quota.
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends libsdl2-dev ccache
|
||||
- name: Install apt packages (cached)
|
||||
# A cache hit (seeded on dev by seed-apt-cache) never touches apt,
|
||||
# so mirror outages cannot hang this PR-only job; the timeout bounds
|
||||
# the cold path. Packages and version must match seed-apt-cache
|
||||
# exactly. The action has no --no-install-recommends; same package
|
||||
# set this job used before #17463.
|
||||
timeout-minutes: 10
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -954,7 +1033,7 @@ jobs:
|
||||
# - This catches pin conflicts and other issues in directly changed code
|
||||
# - Grouped tests use --testing-mode to allow config merging (disables some checks)
|
||||
# - Dependencies are safe to group since they weren't modified in this PR
|
||||
if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
|
||||
if [[ "${{ needs.determine-jobs.outputs.release-pr }}" == "true" ]]; then
|
||||
directly_changed_csv=""
|
||||
echo "Testing components: $components_csv"
|
||||
echo "Target branch: ${{ github.base_ref }} - grouping all components"
|
||||
@@ -1091,7 +1170,7 @@ jobs:
|
||||
# install step (order-of-magnitude faster on cold boots,
|
||||
# with its own wheel cache). actions/setup-python still
|
||||
# provides the interpreter.
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
@@ -1424,6 +1503,7 @@ jobs:
|
||||
# this check.
|
||||
needs:
|
||||
- common
|
||||
- seed-apt-cache
|
||||
- determine-jobs
|
||||
- ci-custom
|
||||
- pylint
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -14,4 +14,4 @@ jobs:
|
||||
permissions:
|
||||
issues: write # issues.lock on closed issues
|
||||
pull-requests: write # issues.lock on closed pull requests
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@9f6577fd37b5cf773ab1b9be929714a0dcd15661 # 2026.7.0
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@0fdd5e311b7e744069166696072a1a9cbc5fbeb6 # 2026.8.1
|
||||
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
||||
@@ -202,7 +202,7 @@ jobs:
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4.2.0
|
||||
uses: docker/setup-buildx-action@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
|
||||
# GitHub App token so the labels, comments and closures come from
|
||||
# esphome[bot] instead of github-actions[bot].
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@61fd37a044cad4e9aa4303027b2a61b6a34da855 # main
|
||||
uses: esphome/workflows/.github/workflows/stale.yml@a1c1485ab46ef41a84a6a9d8abd7fa4b7628fd70 # main
|
||||
secrets:
|
||||
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
with:
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# setup-python interpreter so subsequent ``prek`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.16.0
|
||||
rev: v0.16.3
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -763,3 +763,13 @@ The project uses English for non-code content. When drafting documentation, code
|
||||
PR descriptions, and similar text, avoid technical jargon. Instead, express concepts in plain English,
|
||||
using standard technical terms only when required. Ensure the text is readily comprehensible to a wide
|
||||
audience, including non-native English speakers.
|
||||
|
||||
## 10. Code Comments
|
||||
|
||||
Code comments on individual lines should be used only where necessary to flag issues that may not be obvious
|
||||
on a simple reading of the code. Keep them short (e.g. 1 or 2 lines).
|
||||
|
||||
Function and method comment blocks may include more detail as required to make
|
||||
calling contracts clear and document parameter usage, but should still be kept concise.
|
||||
|
||||
Avoid redundancy and repetition; comments should never simply restate what the code already says.
|
||||
|
||||
@@ -381,6 +381,7 @@ esphome/components/nextion/switch/* @senexcrenshaw
|
||||
esphome/components/nextion/text_sensor/* @senexcrenshaw
|
||||
esphome/components/nfc/* @jesserockz @kbx81
|
||||
esphome/components/noblex/* @AGalfra
|
||||
esphome/components/noise/* @esphome/core
|
||||
esphome/components/npi19/* @bakerkj
|
||||
esphome/components/nrf52/* @tomaszduda23
|
||||
esphome/components/number/* @esphome/core
|
||||
@@ -475,6 +476,7 @@ esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/serial_proxy/* @kbx81
|
||||
esphome/components/sfa30/* @ghsensdev
|
||||
esphome/components/sfa40/* @NoQuarrel
|
||||
esphome/components/sgp40/* @SenexCrenshaw
|
||||
esphome/components/sgp4x/* @martgras @SenexCrenshaw
|
||||
esphome/components/sha256/* @esphome/core
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.9.6
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.0
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
+32
-17
@@ -762,9 +762,11 @@ def _wrap_to_code(name, comp, yaml_util):
|
||||
async def wrapped(conf):
|
||||
cg.add(cg.LineComment(f"{name}:"))
|
||||
if comp.config_schema is not None:
|
||||
conf_str = yaml_util.dump(conf)
|
||||
# sort_keys: voluptuous fills defaults in set order, so an
|
||||
# unsorted dump would churn main.cpp and relink every run
|
||||
conf_str = yaml_util.dump(conf, sort_keys=True)
|
||||
conf_str = conf_str.replace("//", "")
|
||||
# remove tailing \ to avoid multi-line comment warning
|
||||
# remove trailing \ to avoid multi-line comment warning
|
||||
conf_str = conf_str.replace("\\\n", "\n")
|
||||
cg.add(cg.LineComment(indent(conf_str)))
|
||||
await coro(conf)
|
||||
@@ -855,7 +857,20 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
toolchain.create_factory_bin()
|
||||
toolchain.create_ota_bin()
|
||||
toolchain.create_elf_copy()
|
||||
toolchain.get_idedata()
|
||||
from esphome.build_helpers.idedata import IDEDATA_BEST_EFFORT_ERRORS
|
||||
|
||||
try:
|
||||
if toolchain.get_idedata() is None:
|
||||
_LOGGER.warning("No idedata was generated for this build")
|
||||
except IDEDATA_BEST_EFFORT_ERRORS as err:
|
||||
# The firmware already built; an idedata failure must not fail
|
||||
# a successful build.
|
||||
_LOGGER.warning(
|
||||
"Could not generate idedata: %s (IDE, clang-tidy, and "
|
||||
"memory-analysis data will be unavailable for this build)",
|
||||
err,
|
||||
)
|
||||
_LOGGER.debug("Idedata failure detail", exc_info=True)
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
@@ -2732,7 +2747,8 @@ def run_esphome(argv):
|
||||
conf_path.name,
|
||||
)
|
||||
|
||||
if config is None:
|
||||
cache_missed = config is None
|
||||
if cache_missed:
|
||||
from esphome.config import read_config
|
||||
|
||||
config = read_config(
|
||||
@@ -2741,26 +2757,25 @@ def run_esphome(argv):
|
||||
# Snapshot only needed by `esphome config --no-defaults`.
|
||||
snapshot_user_config=getattr(args, "no_defaults", False),
|
||||
)
|
||||
# Refresh the cache so the next upload/logs hits the fast path
|
||||
# instead of re-running read_config. Skip when the storage
|
||||
# sidecar is absent (no compile has run): the cache would
|
||||
# never be loaded back, so writing secrets to disk is wasted.
|
||||
if cache_eligible and config is not None:
|
||||
from esphome.compiled_config import save_compiled_config
|
||||
from esphome.storage_json import ext_storage_path
|
||||
|
||||
if ext_storage_path(conf_path.name).exists():
|
||||
save_compiled_config(config)
|
||||
if config is None:
|
||||
return 2
|
||||
if config is None:
|
||||
return 2
|
||||
CORE.config = config
|
||||
|
||||
# Fallback for platforms whose validators didn't set the toolchain
|
||||
# (only the esp32 component reads esp32.framework.toolchain). All
|
||||
# other platforms only support PlatformIO today.
|
||||
# other platforms only support PlatformIO today. Must run before the
|
||||
# cache refresh below so its sidecar records the same toolchain a
|
||||
# compile would.
|
||||
if CORE.toolchain is None:
|
||||
CORE.toolchain = Toolchain.PLATFORMIO
|
||||
|
||||
# Refresh the cache so the next upload/logs hits the fast path
|
||||
# instead of re-running read_config.
|
||||
if cache_eligible and cache_missed:
|
||||
from esphome.compiled_config import save_compiled_config_and_sidecar
|
||||
|
||||
save_compiled_config_and_sidecar(config)
|
||||
|
||||
if args.command not in POST_CONFIG_ACTIONS:
|
||||
safe_print(f"Unknown command {args.command}")
|
||||
return 1
|
||||
|
||||
+80
-34
@@ -1,11 +1,18 @@
|
||||
"""ESP-IDF direct build generator for ESPHome."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.components.esp32 import get_esp32_variant, idf_version
|
||||
from esphome.components.esp32 import (
|
||||
get_esp32_variant,
|
||||
get_excluded_builtin_components,
|
||||
get_managed_component_require_names,
|
||||
idf_version,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE
|
||||
from esphome.espidf import variant_to_idf_target
|
||||
from esphome.framework_helpers import (
|
||||
get_project_compile_flags,
|
||||
get_project_cxx_compile_flags,
|
||||
@@ -13,6 +20,8 @@ from esphome.framework_helpers import (
|
||||
)
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Replaces the IDF default C++ standard (-std=gnu++2b appended to
|
||||
# CXX_COMPILE_OPTIONS by project.cmake's __build_init) with the one set via
|
||||
# cg.set_cpp_standard(). Emitted between include(project.cmake) and project(),
|
||||
@@ -26,11 +35,12 @@ idf_build_set_property(CXX_COMPILE_OPTIONS "${{esphome_cxx_compile_options}}")""
|
||||
|
||||
|
||||
def get_available_components() -> list[str] | None:
|
||||
"""Get list of built-in ESP-IDF components from project_description.json.
|
||||
"""List the built-in ESP-IDF components from ``project_description.json``.
|
||||
|
||||
Excludes ``src``, IDF-managed components (``managed_components/``), and
|
||||
converted PIO libs (``pio_components/``). Returns ``None`` if the build
|
||||
dir or ``project_description.json`` isn't ready yet.
|
||||
Only components below its ``idf_path/components`` count, which leaves out
|
||||
``src``, IDF-managed components, converted PIO libs and project local
|
||||
ones such as the Arduino ``component_stubs``. Returns ``None`` if the
|
||||
build dir or ``project_description.json`` isn't ready yet.
|
||||
"""
|
||||
if CORE.build_path is None:
|
||||
return None
|
||||
@@ -41,41 +51,44 @@ def get_available_components() -> list[str] | None:
|
||||
try:
|
||||
with project_desc.open(encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
component_info = data.get("build_component_info", {})
|
||||
|
||||
result = []
|
||||
for name, info in component_info.items():
|
||||
# Exclude our own src component
|
||||
if name == "src":
|
||||
continue
|
||||
|
||||
# Exclude IDF-managed and converted-PIO components (external).
|
||||
comp_dir = info.get("dir", "")
|
||||
if "managed_components" in comp_dir or "pio_components" in comp_dir:
|
||||
continue
|
||||
|
||||
result.append(name)
|
||||
|
||||
return result
|
||||
except (json.JSONDecodeError, OSError):
|
||||
root = (Path(data["idf_path"]) / "components").resolve()
|
||||
result = [
|
||||
name
|
||||
for name, info in data.get("build_component_info", {}).items()
|
||||
if (comp_dir := info.get("dir"))
|
||||
and Path(comp_dir).resolve().is_relative_to(root)
|
||||
]
|
||||
except (json.JSONDecodeError, KeyError, OSError) as err:
|
||||
_LOGGER.debug("Could not read %s: %s", project_desc, err)
|
||||
return None
|
||||
if not result:
|
||||
_LOGGER.warning("No ESP-IDF components found under %s", root)
|
||||
return result
|
||||
|
||||
|
||||
def has_discovered_components() -> bool:
|
||||
"""Check if we have discovered components from a previous configure."""
|
||||
return get_available_components() is not None
|
||||
"""Check if a previous configure discovered any built-in components."""
|
||||
return bool(get_available_components())
|
||||
|
||||
|
||||
def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
def _cmake_quote(value: str) -> str:
|
||||
"""Quote a cmake arg value for a set() line. add_cmake_arg rejects
|
||||
whitespace, quotes, and '$', so only backslashes need escaping."""
|
||||
escaped = value.replace("\\", "\\\\")
|
||||
return f'"{escaped}"'
|
||||
|
||||
|
||||
def get_project_cmakelists(
|
||||
minimal: bool = False, builtin_components: list[str] | None = None
|
||||
) -> str:
|
||||
"""Generate the top-level CMakeLists.txt for ESP-IDF project.
|
||||
|
||||
When ``minimal`` is true, omit ``ESPHOME_PROJECT_BUILTIN_COMPONENTS``
|
||||
since ``project_description.json`` may be stale on the first write.
|
||||
``builtin_components`` supplies the discovered list (from the cache)
|
||||
instead of reading it from ``project_description.json``.
|
||||
"""
|
||||
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
|
||||
variant = get_esp32_variant()
|
||||
idf_target = variant.lower().replace("-", "")
|
||||
idf_target = variant_to_idf_target(get_esp32_variant())
|
||||
|
||||
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
|
||||
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
|
||||
@@ -109,6 +122,15 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
else ""
|
||||
)
|
||||
|
||||
# CMake variables registered via cg.add_cmake_arg(). Emitted before
|
||||
# include(project.cmake) so values like EXCLUDE_COMPONENTS are already
|
||||
# set when project.cmake seeds the component list, and on minimal
|
||||
# (discovery) writes too so excluded components never register.
|
||||
cmake_args = "\n".join(
|
||||
f"set({name} {_cmake_quote(value)})"
|
||||
for name, value in sorted(CORE.cmake_args.items())
|
||||
)
|
||||
|
||||
# Per-project list exposed as a CMake variable so converted PIO libs
|
||||
# can reference ${ESPHOME_PROJECT_MANAGED_COMPONENTS} without baking
|
||||
# project-specific names into their cached CMakeLists.
|
||||
@@ -119,8 +141,6 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
# runs as a separate CMake script invocation that doesn't load the
|
||||
# project's top-level CMakeLists; without this, ${ESPHOME_PROJECT_
|
||||
# MANAGED_COMPONENTS} in a converted-lib REQUIRES expands to empty).
|
||||
from esphome.components.esp32 import get_managed_component_require_names
|
||||
|
||||
managed_components_property = "\n".join(
|
||||
f"idf_build_set_property(ESPHOME_PROJECT_MANAGED_COMPONENTS {name} APPEND)"
|
||||
for name in get_managed_component_require_names()
|
||||
@@ -131,12 +151,24 @@ def get_project_cmakelists(minimal: bool = False) -> str:
|
||||
# component's REQUIRES including real IDF components). Referenced by
|
||||
# src/CMakeLists and by each converted PIO lib's CMakeLists. Skipped
|
||||
# on minimal writes because project_description.json may be stale.
|
||||
# Excluded components are dropped here as well: a stale
|
||||
# project_description.json from a build without exclusions may still
|
||||
# list them, and requiring an excluded component pulls it back into
|
||||
# the build (IDF requirement expansion overrides EXCLUDE_COMPONENTS).
|
||||
# Derived from the EXCLUDE_COMPONENTS cmake arg emitted above so the
|
||||
# two can never disagree within one generated file.
|
||||
builtin_components_property = (
|
||||
""
|
||||
if minimal
|
||||
else "\n".join(
|
||||
f"idf_build_set_property(ESPHOME_PROJECT_BUILTIN_COMPONENTS {name} APPEND)"
|
||||
for name in sorted(get_available_components() or [])
|
||||
for name in sorted(
|
||||
set(
|
||||
builtin_components
|
||||
if builtin_components is not None
|
||||
else get_available_components() or []
|
||||
).difference(CORE.cmake_args.get("EXCLUDE_COMPONENTS", "").split(";"))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -163,6 +195,8 @@ set(CMAKE_NINJA_FORCE_RESPONSE_FILE 1)
|
||||
set(IDF_TARGET {idf_target})
|
||||
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
||||
|
||||
{cmake_args}
|
||||
|
||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
|
||||
{cpp_standard_options}
|
||||
@@ -248,7 +282,9 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||
"""
|
||||
|
||||
|
||||
def write_project(minimal: bool = False) -> None:
|
||||
def write_project(
|
||||
minimal: bool = False, builtin_components: list[str] | None = None
|
||||
) -> None:
|
||||
"""Write ESP-IDF project files."""
|
||||
mkdir_p(CORE.build_path)
|
||||
mkdir_p(CORE.relative_src_path())
|
||||
@@ -256,7 +292,7 @@ def write_project(minimal: bool = False) -> None:
|
||||
# Write top-level CMakeLists.txt
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("CMakeLists.txt"),
|
||||
get_project_cmakelists(minimal=minimal),
|
||||
get_project_cmakelists(minimal=minimal, builtin_components=builtin_components),
|
||||
)
|
||||
|
||||
# Write component CMakeLists.txt in src/
|
||||
@@ -264,3 +300,13 @@ def write_project(minimal: bool = False) -> None:
|
||||
CORE.relative_src_path("CMakeLists.txt"),
|
||||
get_component_cmakelists(),
|
||||
)
|
||||
|
||||
# Snapshot the exclusion set so has_outdated_files() can trigger a
|
||||
# discovery reconfigure when it changes. Excluded components never
|
||||
# register in project_description.json, so re-including one (e.g. a
|
||||
# config gains mqtt) requires a fresh discovery pass before the
|
||||
# ESPHOME_PROJECT_BUILTIN_COMPONENTS property can list it.
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("exclude_components.esphomeinternal"),
|
||||
";".join(get_excluded_builtin_components()),
|
||||
)
|
||||
|
||||
@@ -63,6 +63,17 @@ def get_ini_content():
|
||||
# Add extra script for C++ flags
|
||||
CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"])
|
||||
|
||||
# Add CMake args. A user-supplied value (str or list) is deliberately
|
||||
# replaced; this option was always overwritten at FINAL priority.
|
||||
if CORE.cmake_args:
|
||||
CORE.add_platformio_option(
|
||||
"board_build.cmake_extra_args",
|
||||
" ".join(
|
||||
f"-D{name}={value}" for name, value in sorted(CORE.cmake_args.items())
|
||||
),
|
||||
replace=True,
|
||||
)
|
||||
|
||||
content = "[platformio]\n"
|
||||
content += f"description = ESPHome {__version__}\n"
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Build helpers shared by the native (non-PlatformIO) toolchains."""
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Derive idedata from an ESP-IDF native-toolchain ``compile_commands.json``.
|
||||
"""Derive idedata from a native (non-PlatformIO) build's ``compile_commands.json``.
|
||||
|
||||
PlatformIO exposes a curated ``pio run -t idedata`` JSON; the native ESP-IDF
|
||||
toolchain has no such command, but its CMake build emits
|
||||
``build/compile_commands.json`` (CMAKE_EXPORT_COMPILE_COMMANDS). This module
|
||||
turns that file into the same fields consumers (IDE integration, clang-tidy)
|
||||
expect:
|
||||
PlatformIO exposes a curated ``pio run -t idedata`` JSON; the native
|
||||
toolchains have no such command, but each build produces a
|
||||
``compile_commands.json`` (CMAKE_EXPORT_COMPILE_COMMANDS for ESP-IDF, ninja's
|
||||
compdb tool otherwise). This module turns that file into the same fields
|
||||
consumers (IDE integration, clang-tidy) expect:
|
||||
|
||||
{cc_path, cxx_path, cxx_flags, defines, includes: {build, toolchain}}
|
||||
"""
|
||||
@@ -18,6 +18,19 @@ from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.helpers import write_file
|
||||
|
||||
# Everything idedata generation may raise after a successful link; idedata
|
||||
# is a bonus artifact, so consumers warn instead of failing the build
|
||||
IDEDATA_BEST_EFFORT_ERRORS = (
|
||||
EsphomeError,
|
||||
LookupError,
|
||||
OSError,
|
||||
RuntimeError,
|
||||
ValueError,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# C++ translation-unit suffixes used to identify ESPHome source files.
|
||||
@@ -30,12 +43,8 @@ _ESPHOME_SRC_MARKER = "/src/esphome/"
|
||||
|
||||
|
||||
def _is_esphome_src(file: str) -> bool:
|
||||
"""Whether ``file`` is an ESPHome C++ translation unit.
|
||||
|
||||
``compile_commands.json`` ``file`` paths use the OS-native separator, so on
|
||||
Windows they contain backslashes; normalize to ``/`` before testing the
|
||||
marker, otherwise no source matches and the build-include union is empty.
|
||||
"""
|
||||
"""Whether ``file`` is an ESPHome C++ translation unit; normalized to
|
||||
``/`` first since Windows compile DBs use backslashes."""
|
||||
return _ESPHOME_SRC_MARKER in file.replace("\\", "/") and file.endswith(
|
||||
_CXX_SUFFIXES
|
||||
)
|
||||
@@ -106,11 +115,8 @@ def _expand_response_files(tokens: list[str], directory: Path) -> list[str]:
|
||||
|
||||
|
||||
def _pick_entry(entries: list[dict]) -> dict:
|
||||
"""Pick a representative ESPHome C++ translation unit.
|
||||
|
||||
All ESPHome sources share the same component flags/defines, so any one of
|
||||
them yields the cxx_path / cxx_flags / defines we need.
|
||||
"""
|
||||
"""Pick a representative ESPHome C++ TU; all share the same component
|
||||
flags/defines."""
|
||||
for entry in entries:
|
||||
if _is_esphome_src(entry["file"]):
|
||||
return entry
|
||||
@@ -120,25 +126,46 @@ def _pick_entry(entries: list[dict]) -> dict:
|
||||
raise ValueError("no C++ translation unit found in compile_commands.json")
|
||||
|
||||
|
||||
def _parse_entry(entry: dict) -> tuple[str, list[str], list[str], list[str]]:
|
||||
# Compiler launchers that may prefix a compile command; a closed launcher
|
||||
# denylist beats enumerating compiler names, an open set.
|
||||
_LAUNCHER_STEMS = frozenset({"ccache", "sccache", "distcc", "icecc", "buildcache"})
|
||||
|
||||
|
||||
def _is_launcher(token: str) -> bool:
|
||||
return Path(token).stem.lower() in _LAUNCHER_STEMS
|
||||
|
||||
|
||||
def parse_entry(
|
||||
entry: dict, launcher: str | None = None
|
||||
) -> tuple[str, list[str], list[str], list[str]]:
|
||||
"""Parse one compile_commands entry -> (cxx_path, defines, includes, cxx_flags)."""
|
||||
directory = Path(entry["directory"])
|
||||
tokens = _expand_response_files(_split_command(entry["command"]), directory)
|
||||
|
||||
def _include(raw: str) -> str:
|
||||
# Include paths in compile_commands are interpreted relative to the
|
||||
# entry's ``directory`` (e.g. build-local ``-Iconfig``); resolve them
|
||||
# so the cached idedata is usable regardless of the consumer's cwd.
|
||||
# Emit forward slashes (``normpath`` yields ``\`` on Windows) so the
|
||||
# paths match the absolute, already-forward-slash entries in the JSON.
|
||||
# Resolve against the entry's ``directory`` so cached idedata works
|
||||
# from any cwd; emit forward slashes to match the JSON's own entries
|
||||
raw = raw.strip()
|
||||
if raw and not Path(raw).is_absolute():
|
||||
raw = os.path.normpath(directory / raw)
|
||||
return raw.replace("\\", "/")
|
||||
|
||||
# A launcher-wrapped command ("ccache g++ ...") names the compiler second
|
||||
if launcher is not None and tokens[:1] == [launcher]:
|
||||
tokens = tokens[1:]
|
||||
if not tokens:
|
||||
# An empty command, or one that was only the launcher; fail by name
|
||||
raise ValueError(f"empty compile command for {entry.get('file')}")
|
||||
if _is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
|
||||
# Stale DB built with a launcher this run no longer configures; the
|
||||
# real compiler is the next token
|
||||
_LOGGER.warning("Stripping unconfigured launcher %s", tokens[0])
|
||||
tokens = tokens[1:]
|
||||
# token0 is the compiler path; the rest of the command already uses forward
|
||||
# slashes on Windows, so normalize it too for a consistent idedata file.
|
||||
cxx_path = tokens[0].replace("\\", "/")
|
||||
# Enforced here so no caller can record ccache as the compiler
|
||||
reject_launcher_compiler(cxx_path)
|
||||
defines: list[str] = []
|
||||
includes: list[str] = []
|
||||
cxx_flags: list[str] = []
|
||||
@@ -168,7 +195,7 @@ def _parse_entry(entry: dict) -> tuple[str, list[str], list[str], list[str]]:
|
||||
return cxx_path, defines, includes, cxx_flags
|
||||
|
||||
|
||||
def _get_toolchain_includes(cxx_path: str) -> list[str]:
|
||||
def get_toolchain_includes(cxx_path: str) -> list[str]:
|
||||
"""Query the compiler for its builtin ``#include <...>`` search dirs."""
|
||||
result = subprocess.run(
|
||||
[cxx_path, "-E", "-x", "c++", "-", "-v"],
|
||||
@@ -219,26 +246,128 @@ def _cc_path_from_cxx(cxx_path: str) -> str:
|
||||
return f"{stem}{suffix}"
|
||||
|
||||
|
||||
def idedata_from_build(compile_commands: Path) -> dict:
|
||||
def _cache_usable(cached: object) -> bool:
|
||||
"""Check a cached idedata dict against the guarantees of the write path.
|
||||
|
||||
Caches written by older versions predate the launcher rejection and the
|
||||
include-union shape; serving one would bypass both. The dict check also
|
||||
keeps "in" from substring-matching a bare JSON string.
|
||||
"""
|
||||
if not isinstance(cached, dict) or "cc_path" not in cached:
|
||||
return False
|
||||
cxx_path = cached.get("cxx_path")
|
||||
if not isinstance(cxx_path, str) or _is_launcher(cxx_path):
|
||||
return False
|
||||
includes = cached.get("includes")
|
||||
return isinstance(includes, dict) and isinstance(includes.get("build"), list)
|
||||
|
||||
|
||||
def load_or_build_idedata(
|
||||
compile_commands: Path,
|
||||
elf_path: Path,
|
||||
cache: Path,
|
||||
launcher: str | None = None,
|
||||
) -> dict | None:
|
||||
"""Return idedata for a compile_commands.json build, cached on mtime.
|
||||
|
||||
Shared by the native ESP-IDF and ESP8266 Arduino toolchains. Returns None
|
||||
when the compile DB doesn't exist yet (nothing was built). ``launcher``
|
||||
is the compiler-launcher path (ccache) the build was generated with, if
|
||||
any; commands in the compile DB are prefixed with it.
|
||||
"""
|
||||
if not compile_commands.is_file():
|
||||
_LOGGER.debug("No %s yet; skipping idedata generation", compile_commands)
|
||||
return None
|
||||
|
||||
if cache.is_file() and cache.stat().st_mtime >= compile_commands.stat().st_mtime:
|
||||
try:
|
||||
cached = json.loads(cache.read_text(encoding="utf-8"))
|
||||
except (ValueError, OSError) as err:
|
||||
# A recurring cause (interrupted write, disk full) would otherwise
|
||||
# look like unexplained slow builds
|
||||
_LOGGER.warning("Discarding unreadable idedata cache %s: %s", cache, err)
|
||||
else:
|
||||
if _cache_usable(cached):
|
||||
# Re-stamp so a relocated build dir cannot serve a stale ELF path
|
||||
cached["prog_path"] = str(elf_path)
|
||||
return cached
|
||||
_LOGGER.debug("Regenerating idedata: cache %s fails validation", cache)
|
||||
|
||||
data = idedata_from_build(compile_commands, launcher)
|
||||
data["prog_path"] = str(elf_path)
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Atomic so a crash mid-write cannot leave a truncated cache
|
||||
write_file(cache, json.dumps(data, indent=2) + "\n")
|
||||
return data
|
||||
|
||||
|
||||
def reject_launcher_compiler(cxx_path: str) -> None:
|
||||
"""Reject a compile DB naming a launcher (ccache) as the compiler; it
|
||||
must never be probed, cached, or consumed."""
|
||||
if _is_launcher(cxx_path):
|
||||
raise EsphomeError(
|
||||
f"compile_commands.json names the launcher {cxx_path} as the "
|
||||
"compiler; the compile database is unusable"
|
||||
)
|
||||
|
||||
|
||||
def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> dict:
|
||||
"""Parse compile_commands.json into the idedata fields consumers expect.
|
||||
|
||||
A single ESP-IDF compile entry only carries its own component's REQUIRES
|
||||
include set, but consumers (clang-tidy) analyze ESPHome headers that
|
||||
transitively pull in other components. So take cxx_path / cxx_flags /
|
||||
defines from a representative ESPHome TU, but union the include dirs across
|
||||
all ESPHome TUs to get a project-wide superset (as PlatformIO's idedata
|
||||
provides).
|
||||
A single compile entry only carries the include set its own translation
|
||||
unit was built with (per-component under ESP-IDF), but consumers
|
||||
(clang-tidy) analyze ESPHome headers that transitively pull in other
|
||||
components. So take cxx_path / cxx_flags / defines from a representative
|
||||
ESPHome TU, but union the include dirs across all ESPHome TUs to get a
|
||||
project-wide superset (as PlatformIO's idedata provides).
|
||||
"""
|
||||
entries = json.loads(Path(compile_commands).read_text(encoding="utf-8"))
|
||||
cxx_path, defines, _, cxx_flags = _parse_entry(_pick_entry(entries))
|
||||
if not isinstance(entries, list) or not all(isinstance(e, dict) for e in entries):
|
||||
# A TypeError here would escape IDEDATA_BEST_EFFORT_ERRORS
|
||||
raise EsphomeError(f"{compile_commands} is not a compile-command list")
|
||||
|
||||
build_includes: dict[str, None] = {}
|
||||
representative = _pick_entry(entries)
|
||||
cxx_path, defines, rep_includes, cxx_flags = parse_entry(representative, launcher)
|
||||
|
||||
# Seed with the representative's includes so it is not parsed twice
|
||||
has_esphome_tu = _is_esphome_src(representative["file"])
|
||||
build_includes: dict[str, None] = dict.fromkeys(
|
||||
rep_includes if has_esphome_tu else ()
|
||||
)
|
||||
|
||||
def _shape(entry: dict) -> str:
|
||||
# directory + command minus TU-specific paths: same shape means the
|
||||
# same include set, so tokenize once per shape. Response-file
|
||||
# commands never dedupe (the .rsp contents differ per object)
|
||||
command = entry["command"]
|
||||
directory = entry.get("directory", "")
|
||||
if "@" in command:
|
||||
return f"unique:{directory}|{entry.get('output') or command}"
|
||||
stripped = command.replace(entry.get("file", ""), "").replace(
|
||||
entry.get("output", ""), ""
|
||||
)
|
||||
return f"{directory}|{stripped}"
|
||||
|
||||
seen_shapes = {_shape(representative)}
|
||||
for entry in entries:
|
||||
if not _is_esphome_src(entry["file"]):
|
||||
if entry is representative or not _is_esphome_src(entry["file"]):
|
||||
continue
|
||||
for inc in _parse_entry(entry)[2]:
|
||||
has_esphome_tu = True
|
||||
if (shape := _shape(entry)) in seen_shapes:
|
||||
_LOGGER.debug("Include union: %s shares a command shape", entry["file"])
|
||||
continue
|
||||
seen_shapes.add(shape)
|
||||
for inc in parse_entry(entry, launcher)[2]:
|
||||
build_includes.setdefault(inc, None)
|
||||
|
||||
if not has_esphome_tu:
|
||||
# An arbitrary fallback TU breaks clang-tidy/IDE consumers, and a
|
||||
# warning would be cached into permanence; call sites downgrade this
|
||||
raise EsphomeError(
|
||||
f"No ESPHome translation unit found in {compile_commands}; "
|
||||
"refusing to cache unusable idedata"
|
||||
)
|
||||
|
||||
return {
|
||||
"cc_path": _cc_path_from_cxx(cxx_path),
|
||||
"cxx_path": cxx_path,
|
||||
@@ -246,6 +375,6 @@ def idedata_from_build(compile_commands: Path) -> dict:
|
||||
"defines": defines,
|
||||
"includes": {
|
||||
"build": list(build_includes),
|
||||
"toolchain": _get_toolchain_includes(cxx_path),
|
||||
"toolchain": get_toolchain_includes(cxx_path),
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
"""The PlatformIO-format size bar shared by the native toolchains."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def format_bar(used: int, total: int) -> str:
|
||||
"""Match PlatformIO's ``_format_availale_bytes`` (sic, pioupload.py) exactly."""
|
||||
pct_raw = used / total if total else 0
|
||||
blocks = 10
|
||||
filled = min(int(round(blocks * pct_raw)), blocks)
|
||||
progress = "=" * filled
|
||||
return (
|
||||
f"[{progress:<{blocks}}] {pct_raw: 6.1%} "
|
||||
f"(used {used:d} bytes from {total:d} bytes)"
|
||||
)
|
||||
|
||||
|
||||
def print_size_line(label: str, used: int, total: int) -> None:
|
||||
"""One PlatformIO-format summary line (``RAM``/``Flash``).
|
||||
|
||||
The label padding is part of the format: ``script/ci_memory_impact_extract.py``
|
||||
matches these lines verbatim.
|
||||
"""
|
||||
print(f"{label + ':':<7}{format_bar(used, total)}")
|
||||
@@ -25,6 +25,7 @@ from esphome.cpp_generator import ( # noqa: F401
|
||||
add,
|
||||
add_build_flag,
|
||||
add_build_unflag,
|
||||
add_cmake_arg,
|
||||
add_cxx_build_flag,
|
||||
add_define,
|
||||
add_global,
|
||||
|
||||
@@ -18,9 +18,9 @@ from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from esphome.const import __version__ as ESPHOME_VERSION
|
||||
from esphome.core import CORE, Lambda
|
||||
from esphome.core import CORE, EsphomeError, Lambda
|
||||
from esphome.helpers import write_file
|
||||
from esphome.storage_json import StorageJSON, ext_storage_path
|
||||
from esphome.storage_json import StorageJSON, ext_storage_path, storage_path
|
||||
from esphome.types import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -65,7 +65,71 @@ def save_compiled_config(config: ConfigType) -> None:
|
||||
# non-basic dict key), so every upload/logs pays the slow path.
|
||||
_LOGGER.warning("Cannot cache the validated config: %s", err)
|
||||
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
|
||||
_LOGGER.debug("Skipping compiled config cache write: %s", err)
|
||||
# Likely persistent (permissions, full disk): every upload/logs
|
||||
# pays the slow path until it clears, so surface it.
|
||||
_LOGGER.warning("Skipping compiled config cache write: %s", err)
|
||||
|
||||
|
||||
def save_compiled_config_and_sidecar(config: ConfigType) -> None:
|
||||
"""Refresh the cache from the upload/logs fallback (CORE.config must be set).
|
||||
|
||||
The cache is only written when a complete sidecar is on disk:
|
||||
load_compiled_config can't use it otherwise, and it holds resolved
|
||||
secrets.
|
||||
"""
|
||||
if _refresh_sidecar():
|
||||
save_compiled_config(config)
|
||||
|
||||
|
||||
def _refresh_sidecar() -> bool:
|
||||
"""Ensure a complete sidecar is on disk; True when one is.
|
||||
|
||||
Writes one (without claiming a build) when missing or wizard-only.
|
||||
Failures are non-fatal; the next upload/logs pays the slow path again.
|
||||
"""
|
||||
try:
|
||||
path = storage_path()
|
||||
try:
|
||||
old = StorageJSON.load_strict(path)
|
||||
except Exception as err: # noqa: BLE001 # pylint: disable=broad-except
|
||||
# Present but unreadable: it may hold a real build's metadata,
|
||||
# and a fresh rewrite would also stop the next compile from
|
||||
# cleaning a possibly incoherent build tree.
|
||||
_LOGGER.warning(
|
||||
"Not caching: storage sidecar %s is unreadable (%s)", path, err
|
||||
)
|
||||
return False
|
||||
if old is not None and old.can_apply_to_core():
|
||||
# Compile-written; nothing to refresh.
|
||||
return True
|
||||
if CORE.build_path is not None and CORE.build_path.exists():
|
||||
# An unvalidated build tree: its absent or mismatched sidecar
|
||||
# is what makes the next compile wipe it, so don't vouch for
|
||||
# a build this run never saw.
|
||||
_LOGGER.warning(
|
||||
"Not caching: build tree %s has no matching sidecar; "
|
||||
"'esphome compile' will settle it",
|
||||
CORE.build_path,
|
||||
)
|
||||
return False
|
||||
new = StorageJSON.from_esphome_core(CORE, old, claim_build=False)
|
||||
if not new.can_apply_to_core():
|
||||
_LOGGER.warning("Not caching: rebuilt storage sidecar is still incomplete")
|
||||
return False
|
||||
new.save(path)
|
||||
return True
|
||||
except (OSError, EsphomeError) as err:
|
||||
# write_file wraps OSError into EsphomeError. Persistent
|
||||
# (unwritable storage dir), so surface that every upload/logs
|
||||
# pays the slow path.
|
||||
_LOGGER.warning("Could not refresh the storage sidecar: %s", err)
|
||||
except Exception: # noqa: BLE001 # pylint: disable=broad-except
|
||||
# A structural bug; keep the traceback so it isn't mistaken
|
||||
# for the I/O failure above.
|
||||
_LOGGER.warning(
|
||||
"Unexpected error refreshing the storage sidecar", exc_info=True
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
def load_compiled_config(conf_path: Path) -> ConfigType | None:
|
||||
@@ -98,11 +162,8 @@ def load_compiled_config(conf_path: Path) -> ConfigType | None:
|
||||
return None
|
||||
|
||||
storage = StorageJSON.load(ext_storage_path(conf_path.name))
|
||||
if storage is None:
|
||||
return None
|
||||
# apply_to_core assumes a real compile wrote the sidecar; wizard-only
|
||||
# sidecars leave both of these unset and can't drive upload/logs.
|
||||
if not storage.core_platform and not storage.target_platform:
|
||||
if storage is None or not storage.can_apply_to_core():
|
||||
_LOGGER.debug("Ignoring compiled config cache: sidecar missing or incomplete")
|
||||
return None
|
||||
storage.apply_to_core()
|
||||
return config
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_METER,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@MrSuicideParrot"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
@@ -35,7 +36,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
@@ -6,6 +6,7 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_MILLIMETER,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@TH-Braemer"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
@@ -35,7 +36,7 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import stepper
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DIR_PIN, CONF_ID, CONF_SLEEP_PIN, CONF_STEP_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
a4988_ns = cg.esphome_ns.namespace("a4988")
|
||||
A4988 = a4988_ns.class_("A4988", stepper.Stepper, cg.Component)
|
||||
@@ -17,7 +18,7 @@ CONFIG_SCHEMA = stepper.STEPPER_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await stepper.register_stepper(var, config)
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_GRAMS_PER_CUBIC_METER,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
absolute_humidity_ns = cg.esphome_ns.namespace("absolute_humidity")
|
||||
AbsoluteHumidityComponent = absolute_humidity_ns.class_(
|
||||
@@ -43,7 +44,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@glmnet"]
|
||||
|
||||
@@ -48,7 +49,13 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if CORE.is_esp32:
|
||||
from esphome.components.esp32 import include_builtin_idf_component
|
||||
|
||||
# Re-enable the gptimer driver (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_driver_gptimer")
|
||||
|
||||
if CORE.is_esp8266:
|
||||
# ac_dimmer uses setTimer1Callback which requires the waveform generator
|
||||
from esphome.components.esp8266.const import require_waveform
|
||||
|
||||
@@ -4,6 +4,9 @@ from esphome.components.light.effects import register_addressable_effect
|
||||
from esphome.components.light.types import AddressableLightEffect
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_NAME, CONF_UART_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
@@ -21,7 +24,7 @@ CONFIG_SCHEMA = cv.Schema({})
|
||||
"Adalight",
|
||||
{cv.GenerateID(CONF_UART_ID): cv.use_id(uart.UARTComponent)},
|
||||
)
|
||||
async def adalight_light_effect_to_code(config, effect_id):
|
||||
async def adalight_light_effect_to_code(config: ConfigType, effect_id: ID) -> MockObj:
|
||||
effect = cg.new_Pvariable(effect_id, config[CONF_NAME])
|
||||
await uart.register_uart_device(effect, config)
|
||||
return effect
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
@@ -16,6 +18,7 @@ from esphome.components.esp32 import (
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
@@ -225,7 +228,7 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
}
|
||||
|
||||
|
||||
def validate_adc_pin(value):
|
||||
def validate_adc_pin(value: Any) -> ConfigType | str:
|
||||
if str(value).upper() == "VCC":
|
||||
if CORE.is_rp2:
|
||||
return pins.internal_gpio_input_pin_schema(29)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
namespace esphome::adc {
|
||||
|
||||
static const char *const TAG = "adc.common";
|
||||
static const char *const TAG = "adc";
|
||||
|
||||
const LogString *sampling_mode_to_str(SamplingMode mode) {
|
||||
switch (mode) {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
namespace esphome::adc {
|
||||
|
||||
static const char *const TAG = "adc.esp32";
|
||||
static const char *const TAG = "adc";
|
||||
|
||||
adc_oneshot_unit_handle_t ADCSensor::shared_adc_handles[2] = {nullptr, nullptr};
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ ADC_MODE(ADC_VCC)
|
||||
|
||||
namespace esphome::adc {
|
||||
|
||||
static const char *const TAG = "adc.esp8266";
|
||||
static const char *const TAG = "adc";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
#ifndef USE_ADC_SENSOR_VCC
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace esphome::adc {
|
||||
|
||||
static const char *const TAG = "adc.libretiny";
|
||||
static const char *const TAG = "adc";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
#ifndef USE_ADC_SENSOR_VCC
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
namespace esphome::adc {
|
||||
|
||||
static const char *const TAG = "adc.rp2";
|
||||
static const char *const TAG = "adc";
|
||||
|
||||
// The on-die temperature sensor sits on the last ADC channel: input 4 on RP2040
|
||||
// and RP2350A, but input 8 on RP2350B, which has eight external channels rather
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace esphome::adc {
|
||||
|
||||
static const char *const TAG = "adc.zephyr";
|
||||
static const char *const TAG = "adc";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
if (!adc_is_ready_dt(this->channel_)) {
|
||||
|
||||
@@ -52,7 +52,7 @@ _attenuation = cv.enum(ATTENUATION_MODES, lower=True)
|
||||
_sampling_mode = cv.enum(SAMPLING_MODES, lower=True)
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
|
||||
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
|
||||
|
||||
@@ -120,7 +120,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
|
||||
def _overlay_io_channels():
|
||||
def _overlay_io_channels() -> str:
|
||||
channel_count = CORE.data[CONF_ADC_CHANNEL_ID]
|
||||
entries = ", ".join(f"<&adc {channel_id}>" for channel_id in range(channel_count))
|
||||
return f"""
|
||||
@@ -132,7 +132,7 @@ def _overlay_io_channels():
|
||||
"""
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
MULTI_CONF = True
|
||||
@@ -17,7 +18,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
).extend(spi.spi_device_schema(cs_pin_required=True))
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import ADC128S102, adc128s102_ns
|
||||
|
||||
@@ -28,7 +29,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
config[CONF_CHANNEL],
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_WIDTH,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@justfalter"]
|
||||
|
||||
@@ -38,7 +39,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
wrapped_light = await cg.get_variable(config[CONF_ADDRESSABLE_LIGHT_ID])
|
||||
cg.add(var.set_width(config[CONF_WIDTH]))
|
||||
|
||||
@@ -36,6 +36,7 @@ from esphome.const import (
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -243,7 +244,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def neutral_channel(config):
|
||||
async def neutral_channel(config: ConfigType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
current = config[CONF_CURRENT]
|
||||
@@ -257,7 +258,7 @@ async def neutral_channel(config):
|
||||
return var
|
||||
|
||||
|
||||
async def power_channel(config):
|
||||
async def power_channel(config: ConfigType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
|
||||
for sensor_type in POWER_SENSOR_TYPES:
|
||||
@@ -280,7 +281,7 @@ async def power_channel(config):
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -23,6 +23,8 @@ from esphome.const import (
|
||||
UNIT_VOLT_AMPS_REACTIVE,
|
||||
UNIT_WATT,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@angelnu"]
|
||||
|
||||
@@ -163,7 +165,7 @@ ADE7953_CONFIG_SCHEMA = cv.Schema(
|
||||
).extend(cv.polling_component_schema("60s"))
|
||||
|
||||
|
||||
async def register_ade7953(var, config):
|
||||
async def register_ade7953(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if irq_pin_config := config.get(CONF_IRQ_PIN):
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import ade7953_base, i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["ade7953_base"]
|
||||
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await i2c.register_i2c_device(var, config)
|
||||
await ade7953_base.register_ade7953(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import ade7953_base, spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["spi"]
|
||||
AUTO_LOAD = ["ade7953_base"]
|
||||
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await spi.register_spi_device(var, config)
|
||||
await ade7953_base.register_ade7953(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
@@ -24,7 +25,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_ADS1115_ID, ADS1115Component, ads1115_ns
|
||||
|
||||
@@ -86,7 +87,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await sensor.register_sensor(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@solomondg1"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
@@ -23,7 +24,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
UNIT_CELSIUS,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import ADS1118, CONF_ADS1118_ID, ads1118_ns
|
||||
|
||||
@@ -86,7 +87,7 @@ CONFIG_SCHEMA = cv.typed_schema(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await cg.register_parented(var, config[CONF_ADS1118_ID])
|
||||
|
||||
@@ -17,6 +17,9 @@ from esphome.const import (
|
||||
UNIT_OHM,
|
||||
UNIT_PARTS_PER_BILLION,
|
||||
)
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_RESISTANCE = "resistance"
|
||||
|
||||
@@ -62,7 +65,7 @@ CONFIG_SCHEMA = (
|
||||
FINAL_VALIDATE_SCHEMA = i2c.final_validate_device_schema("ags10", max_frequency="15khz")
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
@@ -94,7 +97,12 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
|
||||
AGS10_NEW_I2C_ADDRESS_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
|
||||
async def ags10newi2caddress_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
address = await cg.templatable(config[CONF_ADDRESS], args, cg.uint8)
|
||||
@@ -126,7 +134,12 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
AGS10_SET_ZERO_POINT_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
|
||||
async def ags10setzeropoint_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
mode = await cg.templatable(
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
@@ -50,7 +51,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -4,6 +4,9 @@ from esphome.components import i2c
|
||||
from esphome.components.audio_dac import AudioDac
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MODE
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@kbx81"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -39,7 +42,12 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
SET_AUTO_MUTE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
|
||||
async def aic3204_set_volume_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -49,7 +57,7 @@ async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import ble_device_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["ble_device_base"]
|
||||
CODEOWNERS = ["@jeromelaban"]
|
||||
@@ -21,6 +22,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await ble_device_base.register_ble_device(var, config)
|
||||
|
||||
@@ -20,6 +20,8 @@ from esphome.const import (
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@ncareau", "@jeromelaban"]
|
||||
|
||||
@@ -78,7 +80,7 @@ BASE_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def wave_base_to_code(var, config):
|
||||
async def wave_base_to_code(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import airthings_wave_base
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = airthings_wave_base.DEPENDENCIES
|
||||
|
||||
@@ -20,6 +21,6 @@ CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await airthings_wave_base.wave_base_to_code(var, config)
|
||||
|
||||
@@ -83,7 +83,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await airthings_wave_base.wave_base_to_code(var, config)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ from esphome.const import (
|
||||
UNIT_VOLT,
|
||||
UNIT_WATT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
alpha3_ns = cg.esphome_ns.namespace("alpha3")
|
||||
Alpha3 = alpha3_ns.class_("Alpha3", ble_client.BLEClientNode, cg.PollingComponent)
|
||||
@@ -68,7 +69,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
@@ -40,7 +41,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
@@ -42,7 +43,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import ble_client, cover
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@buxtronix"]
|
||||
DEPENDENCIES = ["ble_client"]
|
||||
@@ -27,7 +28,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await cover.new_cover(config)
|
||||
cg.add(var.set_pin(config[CONF_PIN]))
|
||||
cg.add(var.set_invert_position(config[CONF_INVERT_POSITION]))
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["am43"]
|
||||
CODEOWNERS = ["@buxtronix"]
|
||||
@@ -42,7 +43,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_SENSOR_ID, CONF_THRESHOLD
|
||||
from esphome.types import ConfigType
|
||||
|
||||
analog_threshold_ns = cg.esphome_ns.namespace("analog_threshold")
|
||||
|
||||
@@ -32,7 +33,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ from esphome.components.file.image import image_schema, write_image
|
||||
from esphome.components.image import Image_, validate_settings
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_REPEAT
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@syndlex"]
|
||||
@@ -79,7 +81,12 @@ SET_FRAME_SCHEMA = cv.Schema(
|
||||
@automation.register_action(
|
||||
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
async def animation_action_to_code(config, action_id, template_arg, args):
|
||||
async def animation_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import ble_client, climate
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_UNIT_OF_MEASUREMENT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
UNITS = {
|
||||
"f": "f",
|
||||
@@ -28,7 +29,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate.new_climate(config)
|
||||
await cg.register_component(var, config)
|
||||
await ble_client.register_ble_node(var, config)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Based on this datasheet:
|
||||
# https://www.mouser.ca/datasheet/2/678/AVGO_S_A0002854364_1-2574547.pdf
|
||||
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
@@ -11,6 +13,8 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_LUX,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
@@ -55,7 +59,7 @@ AMBIENT_LIGHT_GAINS = {
|
||||
}
|
||||
|
||||
|
||||
def _validate_measurement_rate(value):
|
||||
def _validate_measurement_rate(value: Any) -> MockObj:
|
||||
value = cv.positive_time_period_milliseconds(value)
|
||||
return cv.enum(MEASUREMENT_RATES, int=True)(value.total_milliseconds)
|
||||
|
||||
@@ -85,7 +89,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
@@ -57,7 +58,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DIRECTION, DEVICE_CLASS_MOVING
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import APDS9960, CONF_APDS9960_ID
|
||||
|
||||
@@ -19,7 +20,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_APDS9960_ID])
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
func = getattr(hub, f"set_{config[CONF_DIRECTION]}_direction_binary_sensor")
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import APDS9960, CONF_APDS9960_ID
|
||||
|
||||
@@ -27,7 +28,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_APDS9960_ID])
|
||||
var = await sensor.new_sensor(config)
|
||||
func = getattr(hub, f"set_{config[CONF_TYPE]}_sensor")
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import base64
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.logger import request_log_listener
|
||||
from esphome.config_helpers import get_logger_level
|
||||
|
||||
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
|
||||
# components and downstream consumers that import them from api
|
||||
from esphome.components.noise import ( # noqa: F401
|
||||
ENCRYPTION_SCHEMA,
|
||||
decode_encryption_key,
|
||||
encryption_schema,
|
||||
validate_encryption_key,
|
||||
)
|
||||
from esphome.config_helpers import filter_source_files_from_defines, get_logger_level
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACTION,
|
||||
@@ -37,6 +46,10 @@ from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_pr
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigFragmentType, ConfigType
|
||||
|
||||
# Compat alias: downstream consumers (e.g. device-builder) referenced the
|
||||
# schema by its old private name before it moved to the noise component
|
||||
_encryption_schema = encryption_schema
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "api"
|
||||
@@ -45,9 +58,15 @@ CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
|
||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||
"""Conditionally auto-load json only when capture_response is used."""
|
||||
"""Conditionally auto-load noise (encryption) and json (capture_response)."""
|
||||
base = ["socket"]
|
||||
|
||||
# A falsy config is a tooling probe for the maximal set (None from
|
||||
# dependency resolution, {} from the components-graph platform probe);
|
||||
# a validated config always carries defaults, never empty
|
||||
if not config or CONF_ENCRYPTION in config:
|
||||
base = base + ["noise"]
|
||||
|
||||
# Check if any homeassistant.action/homeassistant.service has capture_response: true
|
||||
# This flag is set during config validation in _validate_response_config
|
||||
if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
|
||||
@@ -129,20 +148,6 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
value = cv.string_strict(value)
|
||||
try:
|
||||
decoded = base64.b64decode(value, validate=True)
|
||||
except ValueError as err:
|
||||
raise cv.Invalid("Invalid key format, please check it's using base64") from err
|
||||
|
||||
if len(decoded) != 32:
|
||||
raise cv.Invalid("Encryption key must be base64 and 32 bytes long")
|
||||
|
||||
# Return original data for roundtrip conversion
|
||||
return value
|
||||
|
||||
|
||||
CONF_SUPPORTS_RESPONSE = "supports_response"
|
||||
|
||||
# Enum values in api::enums namespace
|
||||
@@ -217,7 +222,7 @@ def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _validate_supports_response(value):
|
||||
def _validate_supports_response(value: Any) -> str:
|
||||
"""Validate supports_response after auto-detection has set the value."""
|
||||
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
|
||||
|
||||
@@ -249,18 +254,6 @@ ACTIONS_SCHEMA = automation.validate_automation(
|
||||
),
|
||||
)
|
||||
|
||||
ENCRYPTION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_KEY): cv.sensitive(validate_encryption_key),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _encryption_schema(config):
|
||||
if config is None:
|
||||
config = {}
|
||||
return ENCRYPTION_SCHEMA(config)
|
||||
|
||||
|
||||
def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for API component."""
|
||||
@@ -296,7 +289,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
CONF_SERVICES, group_of_exclusion=CONF_ACTIONS
|
||||
): ACTIONS_SCHEMA,
|
||||
cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA,
|
||||
cv.Optional(CONF_ENCRYPTION): _encryption_schema,
|
||||
cv.Optional(CONF_ENCRYPTION): encryption_schema,
|
||||
cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All(
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||
@@ -393,7 +386,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if actions := config.get(CONF_ACTIONS, []):
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.Pvariable] = []
|
||||
triggers: list[cg.MockObj] = []
|
||||
for conf in actions:
|
||||
func_args: list[tuple[MockObj, str]] = []
|
||||
service_template_args: list[MockObj] = [] # User service argument types
|
||||
@@ -483,7 +476,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if (encryption_config := config.get(CONF_ENCRYPTION, None)) is not None:
|
||||
if key := encryption_config.get(CONF_KEY):
|
||||
decoded = base64.b64decode(key)
|
||||
decoded = decode_encryption_key(key)
|
||||
cg.add(var.set_noise_psk(list(decoded)))
|
||||
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
|
||||
else:
|
||||
@@ -497,10 +490,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
# and plaintext disabled. Only a factory reset can remove it.
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
cg.add_define("USE_API_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.11")
|
||||
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
|
||||
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
|
||||
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
|
||||
else:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
|
||||
@@ -581,7 +570,7 @@ async def homeassistant_service_to_code(
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
):
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||
@@ -647,7 +636,7 @@ async def homeassistant_service_to_code(
|
||||
return var
|
||||
|
||||
|
||||
def validate_homeassistant_event(value):
|
||||
def validate_homeassistant_event(value: Any) -> str:
|
||||
value = cv.string(value)
|
||||
if not value.startswith("esphome."):
|
||||
raise cv.Invalid(
|
||||
@@ -676,7 +665,12 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
|
||||
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
||||
async def homeassistant_event_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
@@ -724,7 +718,12 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
|
||||
async def homeassistant_tag_scanned_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
@@ -740,7 +739,7 @@ CONF_SUCCESS = "success"
|
||||
CONF_ERROR_MESSAGE = "error_message"
|
||||
|
||||
|
||||
def _validate_api_respond_data(config):
|
||||
def _validate_api_respond_data(config: ConfigType) -> ConfigType:
|
||||
"""Set flag during validation so AUTO_LOAD can include json component."""
|
||||
if CONF_DATA in config:
|
||||
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
|
||||
@@ -824,18 +823,32 @@ API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
|
||||
@automation.register_condition(
|
||||
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
|
||||
)
|
||||
async def api_connected_to_code(config, condition_id, template_arg, args):
|
||||
async def api_connected_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(condition_id, template_arg)
|
||||
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
|
||||
cg.add(var.set_state_subscription_only(templ))
|
||||
return var
|
||||
|
||||
|
||||
# user_services.cpp is only needed when user defined actions exist; the
|
||||
# frame helpers are fully #ifdef'd on the protocol defines set in to_code
|
||||
# (both are set when encryption is configured without a key).
|
||||
_define_filter = filter_source_files_from_defines(
|
||||
{
|
||||
"user_services.cpp": "USE_API_USER_DEFINED_ACTIONS",
|
||||
"api_frame_helper_noise.cpp": "USE_API_NOISE",
|
||||
"api_frame_helper_plaintext.cpp": "USE_API_PLAINTEXT",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
"""Filter out api_pb2_dump.cpp when proto message dumping is not enabled,
|
||||
user_services.cpp when no services are defined, and protocol-specific
|
||||
implementations based on encryption configuration."""
|
||||
files_to_filter: list[str] = []
|
||||
files_to_filter = _define_filter()
|
||||
|
||||
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
|
||||
# This is a particularly large file that still needs to be opened and read
|
||||
@@ -846,21 +859,4 @@ def FILTER_SOURCE_FILES() -> list[str]:
|
||||
if get_logger_level() != "VERY_VERBOSE":
|
||||
files_to_filter.append("api_pb2_dump.cpp")
|
||||
|
||||
# user_services.cpp is only needed when services are defined
|
||||
config = CORE.config.get(DOMAIN, {})
|
||||
if config and not config.get(CONF_ACTIONS) and not config[CONF_CUSTOM_SERVICES]:
|
||||
files_to_filter.append("user_services.cpp")
|
||||
|
||||
# Filter protocol-specific implementations based on encryption configuration
|
||||
encryption_config = config.get(CONF_ENCRYPTION) if config else None
|
||||
|
||||
# If encryption is not configured at all, we only need plaintext
|
||||
if encryption_config is None:
|
||||
files_to_filter.append("api_frame_helper_noise.cpp")
|
||||
# If encryption is configured with a key, we only need noise
|
||||
elif encryption_config.get(CONF_KEY):
|
||||
files_to_filter.append("api_frame_helper_plaintext.cpp")
|
||||
# If encryption is configured but no key is provided, we need both
|
||||
# (this allows a plaintext client to provide a noise key)
|
||||
|
||||
return files_to_filter
|
||||
|
||||
@@ -232,6 +232,7 @@ enum SerialProxyPortType {
|
||||
message SerialProxyInfo {
|
||||
string name = 1; // Human-readable port name
|
||||
SerialProxyPortType port_type = 2; // Port type (RS232, RS485)
|
||||
uint32 configured_line_states = 3; // Bitmask of SerialProxyLineStateFlags this instance can drive
|
||||
}
|
||||
|
||||
// DeviceInfoResponse max_data_length values:
|
||||
@@ -1002,8 +1003,12 @@ message GetTimeResponse {
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 epoch_seconds = 1;
|
||||
string timezone = 2;
|
||||
ParsedTimezone parsed_timezone = 3;
|
||||
// Deprecated in 2026.9.0: clients still send this string for older firmware,
|
||||
// but new firmware only reads parsed_timezone. Clients older than Home
|
||||
// Assistant 2026.3.0 that send only the string leave the device on its
|
||||
// codegen-configured timezone (or UTC).
|
||||
string timezone = 2 [deprecated = true];
|
||||
ParsedTimezone parsed_timezone = 3 [(track_presence) = true];
|
||||
}
|
||||
|
||||
// ==================== USER-DEFINES SERVICES ====================
|
||||
@@ -2622,6 +2627,22 @@ message ZWaveProxyRequest {
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
enum ZWaveProxyStatus {
|
||||
ZWAVE_PROXY_STATUS_OK = 0; // Request completed successfully
|
||||
ZWAVE_PROXY_STATUS_IN_USE = 1; // Denied: another client is already subscribed
|
||||
ZWAVE_PROXY_STATUS_NOT_SUPPORTED = 2; // Request type not supported
|
||||
}
|
||||
|
||||
// Acknowledges a ZWaveProxyRequest (subscribe/unsubscribe). Sent since API 1.16.
|
||||
message ZWaveProxyRequestResponse {
|
||||
option (id) = 151;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_ZWAVE_PROXY";
|
||||
|
||||
ZWaveProxyRequestType type = 1; // Which request type this responds to
|
||||
ZWaveProxyStatus status = 2; // Result status
|
||||
}
|
||||
|
||||
// ==================== INFRARED ====================
|
||||
// Note: Feature and capability flag enums are defined in
|
||||
// esphome/components/infrared/infrared.h
|
||||
@@ -2765,12 +2786,18 @@ message SerialProxyGetModemPinsResponse {
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
|
||||
SerialProxyStatus status = 3; // INVALID_ARGUMENT if the instance index is out of range (since API 1.16)
|
||||
}
|
||||
|
||||
enum SerialProxyRequestType {
|
||||
SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance
|
||||
SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance
|
||||
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent)
|
||||
// Values below are only valid in SerialProxyRequestResponse.type, identifying which
|
||||
// operation is being acknowledged. Sending them in SerialProxyRequest.type is an
|
||||
// error the device answers with INVALID_ARGUMENT.
|
||||
SERIAL_PROXY_REQUEST_TYPE_CONFIGURE = 3; // Acknowledges a SerialProxyConfigureRequest
|
||||
SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS = 4; // Acknowledges a SerialProxySetModemPinsRequest
|
||||
}
|
||||
|
||||
enum SerialProxyStatus {
|
||||
@@ -2779,6 +2806,8 @@ enum SerialProxyStatus {
|
||||
SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error
|
||||
SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed
|
||||
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance
|
||||
SERIAL_PROXY_STATUS_PORT_IN_USE = 5; // Denied: another client holds the port
|
||||
SERIAL_PROXY_STATUS_INVALID_ARGUMENT = 6; // Invalid instance index or parameter value
|
||||
}
|
||||
|
||||
// Generic request message for simple serial proxy operations
|
||||
@@ -2791,7 +2820,9 @@ message SerialProxyRequest {
|
||||
SerialProxyRequestType type = 2; // Request type
|
||||
}
|
||||
|
||||
// Response to a SerialProxyRequest (e.g. flush completion or failure)
|
||||
// Acknowledges a serial proxy operation; the type field identifies which
|
||||
// operation is being acknowledged. Flush has been acknowledged since the
|
||||
// message was introduced; all other acknowledgements are sent since API 1.16.
|
||||
message SerialProxyRequestResponse {
|
||||
option (id) = 147;
|
||||
option (source) = SOURCE_SERVER;
|
||||
|
||||
@@ -160,11 +160,6 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
|
||||
#else
|
||||
#error "No frame helper defined"
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
if (camera::Camera::instance() != nullptr) {
|
||||
this->image_reader_ = std::unique_ptr<camera::CameraImageReader>{camera::Camera::instance()->create_image_reader()};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::start() {
|
||||
@@ -417,15 +412,15 @@ void APIConnection::finalize_iterator_sync_() {
|
||||
}
|
||||
|
||||
void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
size_t max_batch = MAX_INITIAL_PER_BATCH;
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||
iterator.advance();
|
||||
}
|
||||
// Budget by remaining batch capacity so a pass cannot overfill the batch;
|
||||
// stops early on a refused send and resumes next loop pass
|
||||
size_t batch_size = this->deferred_batch_.size();
|
||||
if (batch_size < MAX_INITIAL_BATCH_SIZE)
|
||||
iterator.try_advance(MAX_INITIAL_BATCH_SIZE - batch_size);
|
||||
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= max_batch) {
|
||||
// Flush immediately once enough is queued (not guaranteed every pass);
|
||||
// partial batches go out via the batch timer or finalize_iterator_sync_()
|
||||
if (this->deferred_batch_.size() >= MAX_INITIAL_BATCH_SIZE) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
@@ -1140,6 +1135,7 @@ void APIConnection::try_send_camera_image_() {
|
||||
if (!this->image_reader_)
|
||||
return;
|
||||
|
||||
const auto *cam = camera::Camera::instance();
|
||||
// Send as many chunks as possible without blocking
|
||||
while (this->image_reader_->available()) {
|
||||
if (!this->helper_->can_write_without_blocking())
|
||||
@@ -1149,11 +1145,11 @@ void APIConnection::try_send_camera_image_() {
|
||||
bool done = this->image_reader_->available() == to_send;
|
||||
|
||||
CameraImageResponse msg;
|
||||
msg.key = camera::Camera::instance()->get_object_id_hash();
|
||||
msg.key = cam->get_object_id_hash();
|
||||
msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
|
||||
msg.done = done;
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = camera::Camera::instance()->get_device_id();
|
||||
msg.device_id = cam->get_device_id();
|
||||
#endif
|
||||
|
||||
if (!this->send_message(msg)) {
|
||||
@@ -1169,15 +1165,19 @@ void APIConnection::try_send_camera_image_() {
|
||||
void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) {
|
||||
if (!this->flags_.state_subscription)
|
||||
return;
|
||||
if (!this->image_reader_)
|
||||
if (this->image_reader_ && this->image_reader_->available())
|
||||
return;
|
||||
if (this->image_reader_->available())
|
||||
if (!image->was_requested_by(esphome::camera::API_REQUESTER) && !image->was_requested_by(esphome::camera::IDLE))
|
||||
return;
|
||||
if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) {
|
||||
this->image_reader_->set_image(std::move(image));
|
||||
// Try to send immediately to reduce latency
|
||||
this->try_send_camera_image_();
|
||||
if (!this->image_reader_) {
|
||||
// Created on the first image this connection will send, so connections
|
||||
// that never receive one never pay for a reader. Only a registered
|
||||
// camera's listener can reach this, so instance() is non-null here.
|
||||
this->image_reader_ = std::unique_ptr<camera::CameraImageReader>{camera::Camera::instance()->create_image_reader()};
|
||||
}
|
||||
this->image_reader_->set_image(std::move(image));
|
||||
// Try to send immediately to reduce latency
|
||||
this->try_send_camera_image_();
|
||||
}
|
||||
uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
|
||||
auto *camera = static_cast<camera::Camera *>(entity);
|
||||
@@ -1204,31 +1204,28 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||
#if defined(USE_HOMEASSISTANT_TIMEZONE) && defined(USE_TIME_TIMEZONE)
|
||||
if (!value.timezone.empty()) {
|
||||
// Check if the sender provided pre-parsed timezone data.
|
||||
// If std_offset is non-zero or DST rules are present, the parsed data was populated.
|
||||
// For UTC (all zeros), string parsing produces the same result, so the fallback is equivalent.
|
||||
// Apply only if the sender provided pre-parsed timezone data (Home Assistant 2026.3.0
|
||||
// and newer); field presence distinguishes a genuine all-zero UTC timezone from an
|
||||
// absent field. Older clients send only the deprecated timezone string, which is no
|
||||
// longer decoded; for them the device keeps its codegen-configured timezone.
|
||||
if (value.has_parsed_timezone) {
|
||||
const auto &pt = value.parsed_timezone;
|
||||
if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) {
|
||||
time::ParsedTimezone tz{};
|
||||
tz.std_offset_seconds = pt.std_offset_seconds;
|
||||
tz.dst_offset_seconds = pt.dst_offset_seconds;
|
||||
tz.dst_start.time_seconds = pt.dst_start.time_seconds;
|
||||
tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
|
||||
tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
|
||||
tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
|
||||
tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
|
||||
tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
|
||||
tz.dst_end.time_seconds = pt.dst_end.time_seconds;
|
||||
tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
|
||||
tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
|
||||
tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
|
||||
tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
|
||||
tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
|
||||
time::set_global_tz(tz);
|
||||
} else {
|
||||
homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size());
|
||||
}
|
||||
time::ParsedTimezone tz{};
|
||||
tz.std_offset_seconds = pt.std_offset_seconds;
|
||||
tz.dst_offset_seconds = pt.dst_offset_seconds;
|
||||
tz.dst_start.time_seconds = pt.dst_start.time_seconds;
|
||||
tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
|
||||
tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
|
||||
tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
|
||||
tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
|
||||
tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
|
||||
tz.dst_end.time_seconds = pt.dst_end.time_seconds;
|
||||
tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
|
||||
tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
|
||||
tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
|
||||
tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
|
||||
tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
|
||||
time::set_global_tz(tz);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1384,7 +1381,12 @@ void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
|
||||
}
|
||||
|
||||
void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
|
||||
ZWaveProxyRequestResponse resp{};
|
||||
resp.type = msg.type;
|
||||
resp.status = zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, msg.type);
|
||||
if (!this->send_message(resp)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "Z-Wave proxy response");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1553,15 +1555,50 @@ void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
static enums::SerialProxyStatus serial_proxy_result_to_status(serial_proxy::SerialProxyResult result) {
|
||||
switch (result) {
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_OK:
|
||||
return enums::SERIAL_PROXY_STATUS_OK;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_ASSUMED_SUCCESS:
|
||||
return enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_PORT_IN_USE:
|
||||
return enums::SERIAL_PROXY_STATUS_PORT_IN_USE;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_INVALID_ARGUMENT:
|
||||
return enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_TIMEOUT:
|
||||
return enums::SERIAL_PROXY_STATUS_TIMEOUT;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_NOT_SUPPORTED:
|
||||
return enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
|
||||
case serial_proxy::SerialProxyResult::SERIAL_PROXY_RESULT_ERROR:
|
||||
return enums::SERIAL_PROXY_STATUS_ERROR;
|
||||
}
|
||||
return enums::SERIAL_PROXY_STATUS_ERROR; // Unreachable; all enum values handled above
|
||||
}
|
||||
|
||||
static void send_serial_proxy_ack(APIConnection *conn, uint32_t instance, enums::SerialProxyRequestType type,
|
||||
enums::SerialProxyStatus status) {
|
||||
SerialProxyRequestResponse resp{};
|
||||
resp.instance = instance;
|
||||
resp.type = type;
|
||||
resp.status = status;
|
||||
if (!conn->send_message(resp)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
|
||||
}
|
||||
}
|
||||
|
||||
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 %" PRIu32 " out of range (max %" PRIu32 ")", msg.instance,
|
||||
static_cast<uint32_t>(proxies.size()));
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE,
|
||||
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
|
||||
return;
|
||||
}
|
||||
proxies[msg.instance]->configure(this, msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity),
|
||||
msg.stop_bits, msg.data_size);
|
||||
serial_proxy::SerialProxyResult result = proxies[msg.instance]->configure(
|
||||
this, msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity), msg.stop_bits, msg.data_size);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE,
|
||||
serial_proxy_result_to_status(result));
|
||||
}
|
||||
|
||||
void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) {
|
||||
@@ -1577,20 +1614,30 @@ void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetM
|
||||
auto &proxies = App.get_serial_proxies();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS,
|
||||
enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
|
||||
return;
|
||||
}
|
||||
proxies[msg.instance]->set_modem_pins(this, msg.line_states);
|
||||
serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_modem_pins(this, msg.line_states);
|
||||
send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS,
|
||||
serial_proxy_result_to_status(result));
|
||||
}
|
||||
|
||||
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 %" PRIu32 " out of range", msg.instance);
|
||||
return;
|
||||
}
|
||||
SerialProxyGetModemPinsResponse resp{};
|
||||
resp.instance = msg.instance;
|
||||
resp.line_states = proxies[msg.instance]->get_modem_pins();
|
||||
if (msg.instance >= proxies.size()) {
|
||||
ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
|
||||
// Pre-1.16 clients do not read the status field and would take this error
|
||||
// for a successful "both pins deasserted" answer; let them time out as before
|
||||
if (!this->client_supports_api_version(1, 16)) {
|
||||
return;
|
||||
}
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||
} else {
|
||||
resp.line_states = proxies[msg.instance]->get_modem_pins();
|
||||
}
|
||||
if (!this->send_message(resp)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
|
||||
}
|
||||
@@ -1600,40 +1647,31 @@ 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 %" PRIu32 " out of range", msg.instance);
|
||||
send_serial_proxy_ack(this, msg.instance, msg.type, enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
|
||||
return;
|
||||
}
|
||||
auto *proxy = proxies[msg.instance];
|
||||
enums::SerialProxyStatus status;
|
||||
switch (msg.type) {
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
proxies[msg.instance]->serial_proxy_request(this, msg.type);
|
||||
status = serial_proxy_result_to_status(proxy->serial_proxy_request(this, msg.type));
|
||||
break;
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: {
|
||||
SerialProxyRequestResponse resp{};
|
||||
resp.instance = msg.instance;
|
||||
resp.type = enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH;
|
||||
switch (proxies[msg.instance]->flush_port()) {
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_SUCCESS:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_OK;
|
||||
break;
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_ASSUMED_SUCCESS:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
|
||||
break;
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_TIMEOUT:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_TIMEOUT;
|
||||
break;
|
||||
case uart::UARTFlushResult::UART_FLUSH_RESULT_FAILED:
|
||||
resp.status = enums::SERIAL_PROXY_STATUS_ERROR;
|
||||
break;
|
||||
}
|
||||
if (!this->send_message(resp)) {
|
||||
API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
|
||||
}
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH:
|
||||
status = serial_proxy_result_to_status(proxy->flush_port(this));
|
||||
break;
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||
// Response-only discriminators; never valid in a request
|
||||
ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
|
||||
status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
|
||||
status = enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
|
||||
break;
|
||||
}
|
||||
send_serial_proxy_ack(this, msg.instance, msg.type, status);
|
||||
}
|
||||
|
||||
void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
|
||||
@@ -1752,15 +1790,17 @@ void APIConnection::complete_authentication_() {
|
||||
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
// Copy client name with truncation if needed (set_client_name handles truncation)
|
||||
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
||||
this->client_api_version_major_ = msg.api_version_major;
|
||||
this->client_api_version_minor_ = msg.api_version_minor;
|
||||
this->client_api_version_major_ =
|
||||
static_cast<uint8_t>(std::min<uint32_t>(msg.api_version_major, std::numeric_limits<uint8_t>::max()));
|
||||
this->client_api_version_minor_ =
|
||||
static_cast<uint8_t>(std::min<uint32_t>(msg.api_version_minor, std::numeric_limits<uint8_t>::max()));
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %u.%u", this->helper_->get_client_name(),
|
||||
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 15;
|
||||
resp.api_version_minor = 16;
|
||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||
resp.server_info = ESPHOME_VERSION_REF;
|
||||
resp.name = StringRef(App.get_name());
|
||||
@@ -1894,6 +1934,7 @@ bool APIConnection::send_device_info_response_() {
|
||||
auto &info = resp.serial_proxies[serial_proxy_index++];
|
||||
info.name = StringRef(proxy->get_name());
|
||||
info.port_type = proxy->get_port_type();
|
||||
info.configured_line_states = proxy->get_configured_modem_pins();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -1954,6 +1995,7 @@ bool APIConnection::send_device_capabilities_response_() {
|
||||
auto &info = resp.serial_proxies[serial_proxy_index++];
|
||||
info.name = StringRef(proxy->get_name());
|
||||
info.port_type = proxy->get_port_type();
|
||||
info.configured_line_states = proxy->get_configured_modem_pins();
|
||||
}
|
||||
#endif
|
||||
return this->send_message(resp);
|
||||
@@ -2130,7 +2172,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
||||
}
|
||||
#endif
|
||||
|
||||
psk_t psk{};
|
||||
noise::psk_t psk{};
|
||||
if (msg.key_len == 0) {
|
||||
if (this->parent_->clear_noise_psk(true)) {
|
||||
resp.success = true;
|
||||
@@ -2139,7 +2181,7 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
||||
}
|
||||
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
} else if (APINoiseContext::is_all_zeros(psk)) {
|
||||
} else if (noise::NoiseContext::is_all_zeros(psk)) {
|
||||
// Accepting the reserved provisioning PSK would report success without
|
||||
// enabling encryption (or silently clear an existing key)
|
||||
ESP_LOGW(TAG, "Rejecting all-zero encryption key");
|
||||
@@ -2184,7 +2226,7 @@ bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn,
|
||||
bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn,
|
||||
const void *msg) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise)
|
||||
@@ -2213,7 +2255,7 @@ uint16_t APIConnection::encode_to_buffer_slow(uint32_t calculated_size, MessageE
|
||||
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) {
|
||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) {
|
||||
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
|
||||
|
||||
if (!this->try_to_clear_buffer(!is_log_message)) {
|
||||
@@ -2243,12 +2285,12 @@ void APIConnection::on_fatal_error() {
|
||||
this->flags_.remove = true;
|
||||
}
|
||||
|
||||
bool APIConnection::schedule_message_front_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
|
||||
bool APIConnection::schedule_message_front_(EntityBase *entity, uint16_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,
|
||||
bool APIConnection::send_message_smart_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index) {
|
||||
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
||||
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
|
||||
@@ -53,11 +53,11 @@ void log_dropped_message(const char *tag, int line, const LogString *what);
|
||||
|
||||
// Keepalive timeout in milliseconds
|
||||
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
|
||||
// Maximum number of entities to process in a single batch during initial state/info sending
|
||||
static constexpr size_t MAX_INITIAL_PER_BATCH = 34;
|
||||
// Deferred batch size cap during initial state/info sync
|
||||
static constexpr size_t MAX_INITIAL_BATCH_SIZE = 34;
|
||||
// Verify MAX_MESSAGES_PER_BATCH (defined in api_frame_helper.h) can hold the initial batch
|
||||
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
|
||||
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
|
||||
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_BATCH_SIZE,
|
||||
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_BATCH_SIZE");
|
||||
|
||||
#ifdef USE_BENCHMARK
|
||||
class APIConnection;
|
||||
@@ -326,8 +326,10 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
bool is_marked_for_removal() const { return this->flags_.remove; }
|
||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||
|
||||
// Get client API version for feature detection
|
||||
bool client_supports_api_version(uint16_t major, uint16_t minor) const {
|
||||
// Get client API version for feature detection.
|
||||
// Stored versions saturate at 255 (see send_hello_response_), so requesting
|
||||
// a minimum above that can never match.
|
||||
bool client_supports_api_version(uint8_t major, uint8_t minor) const {
|
||||
return this->client_api_version_major_ > major ||
|
||||
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
|
||||
}
|
||||
@@ -374,7 +376,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);
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint16_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
|
||||
@@ -423,7 +425,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
}
|
||||
|
||||
// Non-template buffer management for send_message
|
||||
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||
bool send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||
|
||||
// Core batch encoding logic. ALWAYS_INLINE so encode_fn devirtualizes at hot call sites.
|
||||
// Defined in api_connection_buffer.h (needs APIServer complete).
|
||||
@@ -664,10 +666,9 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
struct BatchItem {
|
||||
EntityBase *entity; // 4 bytes - Entity pointer
|
||||
uint8_t message_type; // 1 byte - Message type for protocol and dispatch
|
||||
uint16_t message_type; // 2 bytes - Message type for protocol and dispatch
|
||||
uint8_t estimated_size; // 1 byte - Estimated message size (max 255 bytes)
|
||||
uint8_t aux_data_index{AUX_DATA_UNUSED}; // 1 byte - For events: index into entity's event_types
|
||||
// 1 byte padding
|
||||
};
|
||||
|
||||
std::vector<BatchItem> items;
|
||||
@@ -677,7 +678,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// connections that do, buffers are released after initial sync anyway
|
||||
|
||||
// Add item to the batch (with deduplication)
|
||||
void add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
void add_item(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||
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
|
||||
@@ -693,7 +694,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
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) {
|
||||
void add_item_front(EntityBase *entity, uint16_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) {
|
||||
@@ -758,13 +759,15 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif
|
||||
} flags_{}; // 2 bytes total
|
||||
|
||||
// 2-byte types immediately after flags_ (no padding between them)
|
||||
uint16_t client_api_version_major_{0};
|
||||
uint16_t client_api_version_minor_{0};
|
||||
// 2-byte type immediately after flags_ (no padding between them)
|
||||
uint16_t batch_message_type_{0}; // Current message type during batch encoding
|
||||
// 1-byte types to fill remaining space before next 4-byte boundary
|
||||
// Client API versions are clamped to 255 on receive (see send_hello_response_)
|
||||
uint8_t client_api_version_major_{0};
|
||||
uint8_t client_api_version_minor_{0};
|
||||
ActiveIterator active_iterator_{ActiveIterator::NONE};
|
||||
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
|
||||
// Total: 2 (flags) + 2 + 1 + 1 + 1 + 1 (batch_header_size_ below) = 8 bytes,
|
||||
// aligned to 4-byte boundary
|
||||
|
||||
// Actual header size used by encode_to_buffer for the current message.
|
||||
// Read by process_batch_multi_ to pass into MessageInfo.
|
||||
@@ -813,7 +816,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// 2. It's an EventResponse (events are edge-triggered - every occurrence matters)
|
||||
// 3. OR: User has opted into immediate sending (should_try_send_immediately = true
|
||||
// AND batch_delay = 0)
|
||||
inline bool should_send_immediately_(uint8_t message_type) const {
|
||||
inline bool should_send_immediately_(uint16_t message_type) const {
|
||||
return (
|
||||
#ifdef USE_UPDATE
|
||||
message_type == UpdateStateResponse::MESSAGE_TYPE ||
|
||||
@@ -827,11 +830,11 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Helper method to send a message either immediately or via batching
|
||||
// Tries immediate send if should_send_immediately_() returns true and buffer has space
|
||||
// Falls back to batching if immediate send fails or isn't applicable
|
||||
bool send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
bool send_message_smart_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED);
|
||||
|
||||
// Helper function to schedule a deferred message with known message type
|
||||
bool schedule_message_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
|
||||
bool schedule_message_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) {
|
||||
this->deferred_batch_.add_item(entity, message_type, estimated_size, aux_data_index);
|
||||
return this->schedule_batch_();
|
||||
@@ -839,7 +842,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
// Helper function to schedule a high priority message at the front of the 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);
|
||||
bool schedule_message_front_(EntityBase *entity, uint16_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);
|
||||
|
||||
@@ -36,7 +36,7 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and oth
|
||||
static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
|
||||
|
||||
// Maximum number of messages to batch in a single write operation
|
||||
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
|
||||
// Must be >= MAX_INITIAL_BATCH_SIZE in api_connection.h (enforced by static_assert there)
|
||||
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
|
||||
|
||||
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
|
||||
@@ -49,16 +49,16 @@ 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.
|
||||
// message_type matches the wire formats: noise carries a fixed 16-bit type
|
||||
// field, plaintext a type varint. The proto codegen caps message IDs at 16383
|
||||
// so the plaintext type varint fits the 2 bytes budgeted in HEADER_PADDING.
|
||||
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)
|
||||
uint16_t message_type; // Message type (0-16383)
|
||||
uint8_t header_size; // Actual header size used (avoids recomputation in write path)
|
||||
|
||||
MessageInfo(uint8_t type, uint16_t off, uint16_t size, uint8_t hdr)
|
||||
MessageInfo(uint16_t type, uint16_t off, uint16_t size, uint8_t hdr)
|
||||
: offset(off), payload_size(size), message_type(type), header_size(hdr) {}
|
||||
};
|
||||
|
||||
@@ -173,7 +173,7 @@ class APIFrameHelper {
|
||||
}
|
||||
// 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;
|
||||
virtual APIError write_protobuf_packet(uint16_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.
|
||||
@@ -187,15 +187,15 @@ class APIFrameHelper {
|
||||
// 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 {
|
||||
uint8_t frame_header_size(uint16_t payload_size, uint16_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));
|
||||
: static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint16(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));
|
||||
return static_cast<uint8_t>(1 + ProtoSize::varint16(payload_size) + ProtoSize::varint16(message_type));
|
||||
#endif
|
||||
}
|
||||
// Get the frame footer size required by this protocol
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_NOISE
|
||||
#include "api_connection.h" // For ClientInfo struct
|
||||
#include "esphome/components/noise/noise.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
@@ -17,6 +17,14 @@
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
using noise::noise_err_to_logstr;
|
||||
|
||||
// api_frame_helper.h keeps its own MAX_HANDSHAKE_SIZE because that header is
|
||||
// also compiled in plaintext-only builds without the noise component; keep
|
||||
// the two definitions from drifting apart.
|
||||
static_assert(MAX_HANDSHAKE_SIZE == noise::MAX_HANDSHAKE_SIZE,
|
||||
"api and noise component handshake size limits must match");
|
||||
|
||||
static const char *const TAG = "api.noise";
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
|
||||
@@ -51,45 +59,6 @@ static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#endif
|
||||
|
||||
/// Convert a noise error code to a readable error
|
||||
const LogString *noise_err_to_logstr(int err) {
|
||||
if (err == NOISE_ERROR_NO_MEMORY)
|
||||
return LOG_STR("NO_MEMORY");
|
||||
if (err == NOISE_ERROR_UNKNOWN_ID)
|
||||
return LOG_STR("UNKNOWN_ID");
|
||||
if (err == NOISE_ERROR_UNKNOWN_NAME)
|
||||
return LOG_STR("UNKNOWN_NAME");
|
||||
if (err == NOISE_ERROR_MAC_FAILURE)
|
||||
return LOG_STR("MAC_FAILURE");
|
||||
if (err == NOISE_ERROR_NOT_APPLICABLE)
|
||||
return LOG_STR("NOT_APPLICABLE");
|
||||
if (err == NOISE_ERROR_SYSTEM)
|
||||
return LOG_STR("SYSTEM");
|
||||
if (err == NOISE_ERROR_REMOTE_KEY_REQUIRED)
|
||||
return LOG_STR("REMOTE_KEY_REQUIRED");
|
||||
if (err == NOISE_ERROR_LOCAL_KEY_REQUIRED)
|
||||
return LOG_STR("LOCAL_KEY_REQUIRED");
|
||||
if (err == NOISE_ERROR_PSK_REQUIRED)
|
||||
return LOG_STR("PSK_REQUIRED");
|
||||
if (err == NOISE_ERROR_INVALID_LENGTH)
|
||||
return LOG_STR("INVALID_LENGTH");
|
||||
if (err == NOISE_ERROR_INVALID_PARAM)
|
||||
return LOG_STR("INVALID_PARAM");
|
||||
if (err == NOISE_ERROR_INVALID_STATE)
|
||||
return LOG_STR("INVALID_STATE");
|
||||
if (err == NOISE_ERROR_INVALID_NONCE)
|
||||
return LOG_STR("INVALID_NONCE");
|
||||
if (err == NOISE_ERROR_INVALID_PRIVATE_KEY)
|
||||
return LOG_STR("INVALID_PRIVATE_KEY");
|
||||
if (err == NOISE_ERROR_INVALID_PUBLIC_KEY)
|
||||
return LOG_STR("INVALID_PUBLIC_KEY");
|
||||
if (err == NOISE_ERROR_INVALID_FORMAT)
|
||||
return LOG_STR("INVALID_FORMAT");
|
||||
if (err == NOISE_ERROR_INVALID_SIGNATURE)
|
||||
return LOG_STR("INVALID_SIGNATURE");
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
/// Initialize the frame helper, returns OK if successful.
|
||||
APIError APINoiseFrameHelper::init() {
|
||||
APIError err = init_common_();
|
||||
@@ -194,9 +163,9 @@ APIError APINoiseFrameHelper::loop() {
|
||||
*/
|
||||
APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
// read header
|
||||
if (rx_header_buf_len_ < 3) {
|
||||
if (rx_header_buf_len_ < noise::FRAME_HEADER_SIZE) {
|
||||
// no header information yet
|
||||
uint8_t to_read = 3 - rx_header_buf_len_;
|
||||
uint8_t to_read = static_cast<uint8_t>(noise::FRAME_HEADER_SIZE) - rx_header_buf_len_;
|
||||
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
|
||||
APIError err = handle_socket_read_result_(received);
|
||||
if (err != APIError::OK) {
|
||||
@@ -208,7 +177,7 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
if (rx_header_buf_[0] != 0x01) {
|
||||
if (rx_header_buf_[0] != noise::FRAME_INDICATOR) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
|
||||
return APIError::BAD_INDICATOR;
|
||||
@@ -348,15 +317,15 @@ APIError APINoiseFrameHelper::state_action_server_hello_() {
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_() {
|
||||
int action = noise_handshakestate_get_action(this->handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE) {
|
||||
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
|
||||
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ) {
|
||||
return this->state_action_handshake_read_();
|
||||
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
|
||||
} else if (action == noise::NoiseResponderHandshake::Action::ACTION_WRITE) {
|
||||
return this->state_action_handshake_write_();
|
||||
}
|
||||
// bad state for action
|
||||
this->state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
HELPER_LOG("Bad action for handshake: %d", (int) action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
@@ -368,20 +337,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
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) {
|
||||
} else if (this->rx_buf_[0] != noise::HANDSHAKE_STATUS_OK) {
|
||||
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);
|
||||
int err = this->handshake_.read_message(this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
|
||||
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"));
|
||||
this->send_explicit_handshake_reject_(noise::reject_reason_for(err));
|
||||
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
|
||||
APIError::HANDSHAKESTATE_READ_FAILED);
|
||||
}
|
||||
@@ -390,18 +355,16 @@ APIError APINoiseFrameHelper::state_action_handshake_read_() {
|
||||
}
|
||||
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);
|
||||
size_t msg_len = 0;
|
||||
|
||||
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
|
||||
int err = this->handshake_.write_message(buffer + 1, sizeof(buffer) - 1, msg_len);
|
||||
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
|
||||
buffer[0] = noise::HANDSHAKE_STATUS_OK;
|
||||
|
||||
aerr = this->write_frame_(buffer, mbuf.size + 1);
|
||||
aerr = this->write_frame_(buffer, msg_len + 1);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return this->check_handshake_finished_();
|
||||
@@ -409,33 +372,22 @@ APIError APINoiseFrameHelper::state_action_handshake_write_() {
|
||||
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];
|
||||
data[0] = 0x01; // failure
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||
reason_len = std::min(reason_len, sizeof(data) - 1);
|
||||
if (reason_len > 0) {
|
||||
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||
}
|
||||
#else
|
||||
// Normal memory access
|
||||
const char *reason_str = LOG_STR_ARG(reason);
|
||||
size_t reason_len = strlen(reason_str);
|
||||
reason_len = std::min(reason_len, sizeof(data) - 1);
|
||||
if (reason_len > 0) {
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
|
||||
std::memcpy(data + 1, reason_str, reason_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
size_t data_size = reason_len + 1;
|
||||
static_assert(sizeof(data) >= noise::MAC_FAILURE_PAYLOAD_SIZE,
|
||||
"reject buffer must fit the MAC failure wire contract");
|
||||
size_t data_size = noise::format_reject_payload(data, sizeof(data), reason);
|
||||
|
||||
// temporarily remove failed state
|
||||
auto orig_state = state_;
|
||||
state_ = State::EXPLICIT_REJECT;
|
||||
write_frame_(data, data_size);
|
||||
state_ = orig_state;
|
||||
APIError aerr = write_frame_(data, data_size);
|
||||
if (aerr != APIError::OK) {
|
||||
// Best effort; the reject reason is a diagnosis aid, not a protocol step
|
||||
ESP_LOGW(TAG, "Sending handshake reject failed: %d", (int) aerr);
|
||||
}
|
||||
if (state_ == State::EXPLICIT_REJECT) {
|
||||
// write_frame_ may have moved the state to FAILED; keep that decision
|
||||
state_ = orig_state;
|
||||
}
|
||||
}
|
||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
@@ -490,14 +442,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
}
|
||||
// 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,
|
||||
APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint16_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
|
||||
// The noise frame header is written after encryption, when the size is known
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
constexpr uint8_t msg_offset = 3;
|
||||
constexpr uint8_t msg_offset = noise::FRAME_HEADER_SIZE;
|
||||
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
|
||||
@@ -515,15 +465,14 @@ APIError APINoiseFrameHelper::encrypt_noise_message_(uint8_t *buf_start, uint16_
|
||||
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);
|
||||
// Fill in the frame header now that the encrypted size is known
|
||||
noise::write_frame_header(buf_start, static_cast<uint16_t>(mbuf.size));
|
||||
|
||||
encrypted_len_out = static_cast<uint16_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||
encrypted_len_out = static_cast<uint16_t>(noise::FRAME_HEADER_SIZE + mbuf.size);
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
@@ -568,21 +517,19 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
uint8_t header[3];
|
||||
header[0] = 0x01; // indicator
|
||||
header[1] = (uint8_t) (len >> 8);
|
||||
header[2] = (uint8_t) len;
|
||||
uint8_t header[noise::FRAME_HEADER_SIZE];
|
||||
noise::write_frame_header(header, len);
|
||||
|
||||
if (len == 0) {
|
||||
return this->write_raw_buf_(header, 3);
|
||||
return this->write_raw_buf_(header, noise::FRAME_HEADER_SIZE);
|
||||
}
|
||||
struct iovec iov[2];
|
||||
iov[0].iov_base = header;
|
||||
iov[0].iov_len = 3;
|
||||
iov[0].iov_len = noise::FRAME_HEADER_SIZE;
|
||||
iov[1].iov_base = const_cast<uint8_t *>(data);
|
||||
iov[1].iov_len = len;
|
||||
|
||||
return this->write_raw_iov_(iov, 2, 3 + len);
|
||||
return this->write_raw_iov_(iov, 2, noise::FRAME_HEADER_SIZE + len);
|
||||
}
|
||||
|
||||
/** Initiate the data structures for the handshake.
|
||||
@@ -590,42 +537,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
* @return 0 on success, -1 on error (check errno)
|
||||
*/
|
||||
APIError APINoiseFrameHelper::init_handshake_() {
|
||||
int err;
|
||||
memset(&nid_, 0, sizeof(nid_));
|
||||
// const char *proto = "Noise_NNpsk0_25519_ChaChaPoly_SHA256";
|
||||
// err = noise_protocol_name_to_id(&nid_, proto, strlen(proto));
|
||||
nid_.pattern_id = NOISE_PATTERN_NN;
|
||||
nid_.cipher_id = NOISE_CIPHER_CHACHAPOLY;
|
||||
nid_.dh_id = NOISE_DH_CURVE25519;
|
||||
nid_.prefix_id = NOISE_PREFIX_STANDARD;
|
||||
nid_.hybrid_id = NOISE_DH_NONE;
|
||||
nid_.hash_id = NOISE_HASH_SHA256;
|
||||
nid_.modifier_ids[0] = NOISE_MODIFIER_PSK0;
|
||||
|
||||
err = noise_handshakestate_new_by_id(&handshake_, &nid_, NOISE_ROLE_RESPONDER);
|
||||
APIError aerr =
|
||||
handle_noise_error_(err, LOG_STR("noise_handshakestate_new_by_id"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
int err = this->handshake_.init(this->ctx_.get_psk(), prologue_.data(), prologue_.size());
|
||||
APIError aerr = handle_noise_error_(err, LOG_STR("noise_handshake_init"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
const auto &psk = this->ctx_.get_psk();
|
||||
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
|
||||
APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
err = noise_handshakestate_set_prologue(handshake_, prologue_.data(), prologue_.size());
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_prologue"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
// set_prologue copies it into handshakestate, so we can get rid of it now
|
||||
// init copies the prologue into the handshakestate, so we can get rid of it now
|
||||
prologue_.release();
|
||||
|
||||
err = noise_handshakestate_start(handshake_);
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
@@ -634,15 +551,17 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
assert(state_ == State::HANDSHAKE);
|
||||
#endif
|
||||
|
||||
int action = noise_handshakestate_get_action(handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
|
||||
noise::NoiseResponderHandshake::Action action = this->handshake_.action();
|
||||
if (action == noise::NoiseResponderHandshake::Action::ACTION_READ ||
|
||||
action == noise::NoiseResponderHandshake::Action::ACTION_WRITE)
|
||||
return APIError::OK;
|
||||
if (action != NOISE_ACTION_SPLIT) {
|
||||
if (action != noise::NoiseResponderHandshake::Action::ACTION_SPLIT) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad action for handshake: %d", action);
|
||||
HELPER_LOG("Bad action for handshake: %d", (int) action);
|
||||
return APIError::HANDSHAKESTATE_BAD_STATE;
|
||||
}
|
||||
int err = noise_handshakestate_split(handshake_, &send_cipher_, &recv_cipher_);
|
||||
// split() also frees the handshake state
|
||||
int err = this->handshake_.split(send_cipher_, recv_cipher_);
|
||||
APIError aerr =
|
||||
handle_noise_error_(err, LOG_STR("noise_handshakestate_split"), APIError::HANDSHAKESTATE_SPLIT_FAILED);
|
||||
if (aerr != APIError::OK)
|
||||
@@ -651,17 +570,11 @@ APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
this->frame_footer_size_ = noise_cipherstate_get_mac_length(send_cipher_);
|
||||
|
||||
HELPER_LOG("Handshake complete!");
|
||||
noise_handshakestate_free(handshake_);
|
||||
handshake_ = nullptr;
|
||||
state_ = State::DATA;
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
APINoiseFrameHelper::~APINoiseFrameHelper() {
|
||||
if (handshake_ != nullptr) {
|
||||
noise_handshakestate_free(handshake_);
|
||||
handshake_ = nullptr;
|
||||
}
|
||||
if (send_cipher_ != nullptr) {
|
||||
noise_cipherstate_free(send_cipher_);
|
||||
send_cipher_ = nullptr;
|
||||
@@ -672,16 +585,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
|
||||
void noise_rand_bytes(void *output, size_t len) {
|
||||
if (!esphome::random_bytes(reinterpret_cast<uint8_t *>(output), len)) {
|
||||
ESP_LOGE(TAG, "Acquiring random bytes failed; rebooting");
|
||||
arch_restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_NOISE
|
||||
#endif // USE_API
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_NOISE
|
||||
#include "noise/protocol.h"
|
||||
#include "api_noise_context.h"
|
||||
#include "esphome/components/noise/noise_handshake.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -14,9 +14,9 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
// 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
|
||||
static constexpr uint8_t HEADER_PADDING = noise::FRAME_HEADER_SIZE + 2 + 2; // frame header + type + data_len
|
||||
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, noise::NoiseContext &ctx)
|
||||
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
|
||||
frame_header_padding_ = HEADER_PADDING;
|
||||
}
|
||||
@@ -31,7 +31,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
#endif
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
@@ -44,7 +44,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
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,
|
||||
APIError encrypt_noise_message_(uint8_t *buf_start, uint16_t payload_size, uint16_t message_type,
|
||||
uint16_t &encrypted_len_out);
|
||||
APIError init_handshake_();
|
||||
APIError check_handshake_finished_();
|
||||
@@ -52,25 +52,22 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError handle_handshake_frame_error_(APIError aerr);
|
||||
APIError handle_noise_error_(int err, const LogString *func_name, APIError api_err);
|
||||
|
||||
// Pointers first (4 bytes each)
|
||||
NoiseHandshakeState *handshake_{nullptr};
|
||||
// Pointers first (4 bytes each; the handshake wrapper holds one pointer)
|
||||
noise::NoiseResponderHandshake handshake_;
|
||||
NoiseCipherState *send_cipher_{nullptr};
|
||||
NoiseCipherState *recv_cipher_{nullptr};
|
||||
|
||||
// Reference to noise context (4 bytes on 32-bit)
|
||||
APINoiseContext &ctx_;
|
||||
noise::NoiseContext &ctx_;
|
||||
|
||||
// Buffer for noise handshake prologue (released after handshake)
|
||||
APIBuffer prologue_;
|
||||
|
||||
// NoiseProtocolId (size depends on implementation)
|
||||
NoiseProtocolId nid_;
|
||||
|
||||
// Group small types together
|
||||
// Fixed-size header buffer for noise protocol:
|
||||
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
|
||||
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
|
||||
uint8_t rx_header_buf_[3];
|
||||
uint8_t rx_header_buf_[noise::FRAME_HEADER_SIZE];
|
||||
uint8_t rx_header_buf_len_ = 0;
|
||||
// 4 bytes total, no padding
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "api_pb2.h"
|
||||
#include "proto.h"
|
||||
#include <cstring>
|
||||
#include <cinttypes>
|
||||
@@ -252,24 +253,21 @@ ESPHOME_ALWAYS_INLINE static inline void encode_varint_16(uint16_t value, uint8_
|
||||
*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;
|
||||
}
|
||||
}
|
||||
// The generator rejects message IDs above MAX_MESSAGE_TYPE, so the type varint
|
||||
// can never outgrow the 2 bytes HEADER_PADDING budgets for it. Without this
|
||||
// bound, write_plaintext_header's header_offset would underflow for the first
|
||||
// message in a batch and the header write would land outside the buffer.
|
||||
static_assert(1 + 3 + ProtoSize::varint16(MAX_MESSAGE_TYPE) <= APIPlaintextFrameHelper::HEADER_PADDING,
|
||||
"HEADER_PADDING cannot fit the type varint of the largest message ID");
|
||||
|
||||
// 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) {
|
||||
uint16_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 type_varint_len = ProtoSize::varint16(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.
|
||||
@@ -292,12 +290,12 @@ ESPHOME_ALWAYS_INLINE static inline uint8_t write_plaintext_header(uint8_t *buf_
|
||||
|
||||
// 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);
|
||||
encode_varint_16(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) {
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,8 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
// 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 4-5: message type varint (up to 2 bytes; covers message IDs up to
|
||||
// 16383, enforced by the proto codegen)
|
||||
// Pos 6+: actual payload data
|
||||
static constexpr uint8_t HEADER_PADDING = 1 + 3 + 2; // indicator + size varint + type varint
|
||||
|
||||
@@ -21,7 +22,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
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_packet(uint16_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
#ifdef USE_API_NOISE
|
||||
// After try_read_frame_ returned PROTOCOL_SWITCH_TO_NOISE: copy out the
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
using psk_t = std::array<uint8_t, 32>;
|
||||
|
||||
class APINoiseContext {
|
||||
public:
|
||||
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
|
||||
// doubles as the well-known provisioning PSK that unprovisioned devices
|
||||
// accept for Noise handshakes (passive-sniffing protection only, no
|
||||
// authentication). It is never a valid real key.
|
||||
static bool is_all_zeros(const psk_t &psk) {
|
||||
uint8_t acc = 0;
|
||||
for (uint8_t b : psk) {
|
||||
acc |= b;
|
||||
}
|
||||
return acc == 0;
|
||||
}
|
||||
void set_psk(psk_t psk) {
|
||||
this->psk_ = psk;
|
||||
this->has_psk_ = !is_all_zeros(psk);
|
||||
}
|
||||
const psk_t &get_psk() const { return this->psk_; }
|
||||
bool has_psk() const { return this->has_psk_; }
|
||||
|
||||
protected:
|
||||
psk_t psk_{};
|
||||
bool has_psk_{false};
|
||||
};
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
} // namespace esphome::api
|
||||
@@ -116,4 +116,10 @@ extend google.protobuf.FieldOptions {
|
||||
// the per-byte loop when the upper bits are non-zero (the common case
|
||||
// for real MAC addresses, since OUIs occupy the top 24 bits).
|
||||
optional bool mac_address = 50019 [default=false];
|
||||
|
||||
// track_presence: Track whether this message-typed field was present on the wire.
|
||||
// Generates a `bool has_<field>{false};` member on the decoding side that is set
|
||||
// to true when the field arrives, so an all-default submessage can be told apart
|
||||
// from an absent one (e.g. a UTC ParsedTimezone, which is all zeros).
|
||||
optional bool track_presence = 50020 [default=false];
|
||||
}
|
||||
|
||||
@@ -102,12 +102,14 @@ uint8_t *SerialProxyInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PAR
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->port_type));
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, this->configured_line_states);
|
||||
return pos;
|
||||
}
|
||||
uint32_t SerialProxyInfo::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_length(1, this->name.size());
|
||||
size += this->port_type ? 2 : 0;
|
||||
size += ProtoSize::calc_uint32(1, this->configured_line_states);
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
@@ -1249,12 +1251,9 @@ bool ParsedTimezone::decode_length(uint32_t field_id, ProtoLengthDelimited value
|
||||
}
|
||||
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->timezone = StringRef(reinterpret_cast<const char *>(value.data()), value.size());
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
value.decode_to_message(this->parsed_timezone);
|
||||
this->has_parsed_timezone = true;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
@@ -3945,6 +3944,18 @@ uint32_t ZWaveProxyRequest::calculate_size() const {
|
||||
size += ProtoSize::calc_length(1, this->data_len);
|
||||
return size;
|
||||
}
|
||||
uint8_t *ZWaveProxyRequestResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->type));
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->status));
|
||||
return pos;
|
||||
}
|
||||
uint32_t ZWaveProxyRequestResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += this->type ? 2 : 0;
|
||||
size += this->status ? 2 : 0;
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
uint8_t *ListEntitiesInfraredResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -4187,12 +4198,14 @@ uint8_t *SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer PROTO_
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, this->instance);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, this->line_states);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, static_cast<uint32_t>(this->status));
|
||||
return pos;
|
||||
}
|
||||
uint32_t SerialProxyGetModemPinsResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, this->instance);
|
||||
size += ProtoSize::calc_uint32(1, this->line_states);
|
||||
size += this->status ? 2 : 0;
|
||||
return size;
|
||||
}
|
||||
bool SerialProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
|
||||
+174
-142
File diff suppressed because it is too large
Load Diff
@@ -816,6 +816,18 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::ZWaveProxyStatus>(enums::ZWaveProxyStatus value) {
|
||||
switch (value) {
|
||||
case enums::ZWAVE_PROXY_STATUS_OK:
|
||||
return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_OK");
|
||||
case enums::ZWAVE_PROXY_STATUS_IN_USE:
|
||||
return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_IN_USE");
|
||||
case enums::ZWAVE_PROXY_STATUS_NOT_SUPPORTED:
|
||||
return ESPHOME_PSTR("ZWAVE_PROXY_STATUS_NOT_SUPPORTED");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) {
|
||||
@@ -838,6 +850,10 @@ template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_FLUSH");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_CONFIGURE");
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
@@ -854,6 +870,10 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_TIMEOUT");
|
||||
case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_NOT_SUPPORTED");
|
||||
case enums::SERIAL_PROXY_STATUS_PORT_IN_USE:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_PORT_IN_USE");
|
||||
case enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT:
|
||||
return ESPHOME_PSTR("SERIAL_PROXY_STATUS_INVALID_ARGUMENT");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
@@ -914,6 +934,7 @@ const char *SerialProxyInfo::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyInfo"));
|
||||
dump_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("port_type"), static_cast<enums::SerialProxyPortType>(this->port_type));
|
||||
dump_field(out, ESPHOME_PSTR("configured_line_states"), this->configured_line_states);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
@@ -1468,7 +1489,7 @@ const char *ParsedTimezone::dump_to(DumpBuffer &out) const {
|
||||
const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("GetTimeResponse"));
|
||||
dump_field(out, ESPHOME_PSTR("epoch_seconds"), this->epoch_seconds);
|
||||
dump_field(out, ESPHOME_PSTR("timezone"), this->timezone);
|
||||
dump_field(out, ESPHOME_PSTR("has_parsed_timezone"), this->has_parsed_timezone);
|
||||
out.append(2, ' ').append_p(ESPHOME_PSTR("parsed_timezone")).append(": ");
|
||||
this->parsed_timezone.dump_to(out);
|
||||
out.append("\n");
|
||||
@@ -2644,6 +2665,12 @@ const char *ZWaveProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
dump_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ZWaveProxyRequestResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ZWaveProxyRequestResponse"));
|
||||
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ZWaveProxyRequestType>(this->type));
|
||||
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::ZWaveProxyStatus>(this->status));
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const {
|
||||
@@ -2753,6 +2780,7 @@ const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyGetModemPinsResponse"));
|
||||
dump_field(out, ESPHOME_PSTR("instance"), this->instance);
|
||||
dump_field(out, ESPHOME_PSTR("line_states"), this->line_states);
|
||||
dump_field(out, ESPHOME_PSTR("status"), static_cast<enums::SerialProxyStatus>(this->status));
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
|
||||
@@ -423,12 +423,6 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
|
||||
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
|
||||
#endif
|
||||
|
||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
bool has_subscriber = false;
|
||||
@@ -553,10 +547,6 @@ const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_sta
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t APIServer::get_port() const { return this->port_; }
|
||||
|
||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
|
||||
const LogString *fail_log_msg, bool make_active) {
|
||||
@@ -598,7 +588,7 @@ bool APIServer::load_and_apply_noise_psk_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
bool APIServer::save_noise_psk(noise::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
|
||||
// but if it is, reject the change
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
#include "api_buffer.h"
|
||||
// Must precede clients_ so APIConnection is complete for default_delete (libc++).
|
||||
#include "api_connection.h"
|
||||
#include "api_noise_context.h"
|
||||
#ifdef USE_API_NOISE
|
||||
// Only present in the build when the noise component is loaded
|
||||
#include "esphome/components/noise/noise.h"
|
||||
#endif
|
||||
#include "api_pb2.h"
|
||||
#include "api_pb2_service.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
@@ -37,7 +40,7 @@ class UserServiceDescriptor;
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
struct SavedNoisePsk {
|
||||
psk_t psk;
|
||||
noise::psk_t psk;
|
||||
} PACKED; // NOLINT
|
||||
#endif
|
||||
|
||||
@@ -51,8 +54,8 @@ class APIServer final : public Component,
|
||||
public:
|
||||
APIServer();
|
||||
void setup() override;
|
||||
uint16_t get_port() const;
|
||||
float get_setup_priority() const override;
|
||||
uint16_t get_port() const { return this->port_; }
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
@@ -63,9 +66,9 @@ class APIServer final : public Component,
|
||||
#ifdef USE_CAMERA
|
||||
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
|
||||
#endif
|
||||
void set_port(uint16_t port);
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
void set_batch_delay(uint16_t batch_delay);
|
||||
void set_port(uint16_t port) { this->port_ = port; }
|
||||
void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
void set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
uint16_t get_batch_delay() const { return batch_delay_; }
|
||||
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
|
||||
|
||||
@@ -73,10 +76,10 @@ class APIServer final : public Component,
|
||||
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||
bool save_noise_psk(noise::psk_t psk, bool make_active = true);
|
||||
bool clear_noise_psk(bool make_active = true);
|
||||
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
||||
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
noise::NoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
void handle_disconnect(APIConnection *conn);
|
||||
@@ -354,7 +357,7 @@ class APIServer final : public Component,
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
APINoiseContext noise_ctx_;
|
||||
noise::NoiseContext noise_ctx_;
|
||||
ESPPreferenceObject noise_pref_;
|
||||
#endif // USE_API_NOISE
|
||||
};
|
||||
|
||||
@@ -95,9 +95,17 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
|
||||
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
// Yield after every Nth service; bounds direct (non-batched) writes per loop pass
|
||||
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
|
||||
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
return this->client_->send_message(resp);
|
||||
if (!this->client_->send_message(resp))
|
||||
return false;
|
||||
// at_ is this service's index
|
||||
if ((this->at_ + 1) % SERVICE_YIELD_INTERVAL == 0)
|
||||
this->yield_after_step_();
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -684,11 +684,6 @@ class ProtoSize {
|
||||
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
|
||||
*
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.const import (
|
||||
DEVICE_CLASS_AQI,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, CONF_EXTENDED_RANGE, aqi_ns
|
||||
|
||||
@@ -16,7 +17,7 @@ DEPENDENCIES = ["sensor"]
|
||||
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
|
||||
|
||||
|
||||
def _validate_extended_range(config):
|
||||
def _validate_extended_range(config: ConfigType) -> ConfigType:
|
||||
if CONF_EXTENDED_RANGE in config and config[CONF_CALCULATION_TYPE] == "CAQI":
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_EXTENDED_RANGE}' is not supported with 'calculation_type: CAQI'. "
|
||||
@@ -48,7 +49,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ from esphome.const import (
|
||||
CONF_TUNE_ANTENNA,
|
||||
CONF_WATCHDOG_THRESHOLD,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
MULTI_CONF = True
|
||||
|
||||
@@ -42,7 +44,7 @@ AS3935_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def setup_as3935(var, config):
|
||||
async def setup_as3935(var: MockObj, config: ConfigType) -> None:
|
||||
await cg.register_component(var, config)
|
||||
|
||||
irq_pin = await cg.gpio_pin_expression(config[CONF_IRQ_PIN])
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import AS3935, CONF_AS3935_ID
|
||||
|
||||
@@ -13,7 +14,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema().extend(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_AS3935_ID])
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
cg.add(hub.set_thunder_alert_binary_sensor(var))
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_KILOMETER,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import AS3935, CONF_AS3935_ID
|
||||
|
||||
@@ -31,7 +32,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
hub = await cg.get_variable(config[CONF_AS3935_ID])
|
||||
|
||||
if distance_config := config.get(CONF_DISTANCE):
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import as3935, i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["as3935"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await as3935.setup_as3935(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import as3935, spi
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["as3935"]
|
||||
DEPENDENCIES = ["spi"]
|
||||
@@ -20,7 +21,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await as3935.setup_as3935(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
@@ -11,6 +14,7 @@ from esphome.const import (
|
||||
CONF_RANGE,
|
||||
CONF_WATCHDOG,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@ammmze"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -72,13 +76,13 @@ POSITION_TO_ANGLE = 360 / RESOLUTION
|
||||
MIN_RANGE = round(18 * ANGLE_TO_POSITION)
|
||||
|
||||
|
||||
def angle(min=-360, max=360):
|
||||
def angle(min: float = -360, max: float = 360) -> Callable[[Any], Any]:
|
||||
return cv.All(
|
||||
cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max)
|
||||
)
|
||||
|
||||
|
||||
def angle_to_position(value, min=-360, max=360):
|
||||
def angle_to_position(value: Any, min: float = -360, max: float = 360) -> int:
|
||||
try:
|
||||
value = angle(min=min, max=max)(value)
|
||||
return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
|
||||
@@ -86,17 +90,17 @@ def angle_to_position(value, min=-360, max=360):
|
||||
raise cv.Invalid(f"When using angle, {e.error_message}") from e
|
||||
|
||||
|
||||
def percent_to_position(value):
|
||||
def percent_to_position(value: Any) -> int:
|
||||
value = cv.possibly_negative_percentage(value)
|
||||
return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION
|
||||
|
||||
|
||||
def position(min=-MAX_POSITION, max=MAX_POSITION):
|
||||
def position(min: int = -MAX_POSITION, max: int = MAX_POSITION) -> Callable[[Any], Any]:
|
||||
"""Validate that the config option is a position.
|
||||
Accepts integers, degrees, or percentage (of 360 degrees).
|
||||
"""
|
||||
|
||||
def validator(value):
|
||||
def validator(value: Any) -> int:
|
||||
if isinstance(value, str) and value.endswith("%"):
|
||||
value = percent_to_position(value)
|
||||
|
||||
@@ -112,7 +116,7 @@ def position(min=-MAX_POSITION, max=MAX_POSITION):
|
||||
return validator
|
||||
|
||||
|
||||
def position_range():
|
||||
def position_range() -> Callable[[Any], Any]:
|
||||
"""Validate that value given is a valid range for the device.
|
||||
A valid range is one of the following:
|
||||
- a value of 0 (meaning full range)
|
||||
@@ -129,7 +133,7 @@ def position_range():
|
||||
zero_validator,
|
||||
)
|
||||
|
||||
def validator(value):
|
||||
def validator(value: Any) -> Any:
|
||||
is_negative_str = isinstance(value, str) and value.startswith("-")
|
||||
is_negative_num = isinstance(value, (float, int)) and value < 0
|
||||
if is_negative_str or is_negative_num:
|
||||
@@ -139,13 +143,13 @@ def position_range():
|
||||
return validator
|
||||
|
||||
|
||||
def has_valid_range_config():
|
||||
def has_valid_range_config() -> Callable[[ConfigType], ConfigType]:
|
||||
"""Validate that that the config start + end position results in a valid
|
||||
positional range, which must be >= 18degrees
|
||||
"""
|
||||
range_validator = position_range()
|
||||
|
||||
def validator(config):
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
# if we don't have an end position, then there is nothing to do
|
||||
if CONF_END_POSITION not in config:
|
||||
return config
|
||||
@@ -203,7 +207,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
ICON_ROTATE_RIGHT,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import AS5600Component, as5600_ns
|
||||
|
||||
@@ -77,7 +78,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_parented(var, config[CONF_AS5600_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome.const import (
|
||||
ICON_BRIGHTNESS_5,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@mrgnr"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -96,7 +97,7 @@ SENSORS = {
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
@@ -25,7 +26,7 @@ CONFIG_SCHEMA = cv.Schema({})
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if CORE.is_esp32:
|
||||
# https://github.com/ESP32Async/AsyncTCP
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
@@ -4,6 +4,9 @@ import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_FREQUENCY, CONF_ID
|
||||
from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@X-Ryl669"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
@@ -70,7 +73,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
@@ -91,7 +94,12 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def at581x_reset_to_code(config, action_id, template_arg, args):
|
||||
async def at581x_reset_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
@@ -163,7 +171,12 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
|
||||
RADAR_SETTINGS_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def at581x_settings_to_code(config, action_id, template_arg, args):
|
||||
async def at581x_settings_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import DEVICE_CLASS_SWITCH, ICON_WIFI
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import CONF_AT581X_ID, AT581XComponent, at581x_ns
|
||||
|
||||
@@ -22,7 +23,7 @@ CONFIG_SCHEMA = switch.switch_schema(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
at581x_component = await cg.get_variable(config[CONF_AT581X_ID])
|
||||
s = await switch.new_switch(config)
|
||||
await cg.register_parented(s, config[CONF_AT581X_ID])
|
||||
|
||||
@@ -21,6 +21,7 @@ from esphome.const import (
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@ahpohl"]
|
||||
|
||||
@@ -77,7 +78,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await ble_device_base.register_ble_device(var, config)
|
||||
|
||||
@@ -30,6 +30,7 @@ from esphome.const import (
|
||||
UNIT_WATT,
|
||||
UNIT_WATT_HOURS,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_METER_CONSTANT = "meter_constant"
|
||||
CONF_PL_CONST = "pl_const"
|
||||
@@ -123,7 +124,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await spi.register_spi_device(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import button
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import atm90e32_ns
|
||||
from ..sensor import ATM90E32Component
|
||||
@@ -67,7 +68,7 @@ CONFIG_SCHEMA = {
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
if run_gain := config.get(CONF_RUN_GAIN_CALIBRATION):
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user