mirror of
https://github.com/esphome/esphome.git
synced 2026-02-09 09:11:52 +00:00
Compare commits
991 Commits
integratio
...
integratio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6469863aea | ||
|
|
8f032d5213 | ||
|
|
4efaaedb05 | ||
|
|
12c369cb09 | ||
|
|
e56d1a6ee9 | ||
|
|
b728ebd5fb | ||
|
|
5caa54761b | ||
|
|
4efdfda8fb | ||
|
|
6099dd3065 | ||
|
|
30558262d8 | ||
|
|
6dd7f45cc9 | ||
|
|
17664750b7 | ||
|
|
95e92716d9 | ||
|
|
0f1ece6bdc | ||
|
|
43d7b1027b | ||
|
|
2eac6b06b7 | ||
|
|
140629a323 | ||
|
|
2376c020e8 | ||
|
|
d28c3e2641 | ||
|
|
4cf1d7babf | ||
|
|
cac82280b3 | ||
|
|
df7e837e3e | ||
|
|
25b1ce4268 | ||
|
|
aa38ec379a | ||
|
|
639a704411 | ||
|
|
c641256396 | ||
|
|
e4dd75f0b5 | ||
|
|
a7d1686cfd | ||
|
|
d71f7df367 | ||
|
|
a56415ca3f | ||
|
|
5cb3af9e0e | ||
|
|
11774df7f6 | ||
|
|
41fedaedb3 | ||
|
|
380a669377 | ||
|
|
9299d83eeb | ||
|
|
381f4e9f34 | ||
|
|
99b7996759 | ||
|
|
31ca8984ed | ||
|
|
02687615b3 | ||
|
|
b283f1ae75 | ||
|
|
2ceb6ee95b | ||
|
|
4cdd73904f | ||
|
|
b8cad678b1 | ||
|
|
5c5bf50e49 | ||
|
|
f2e1c2c650 | ||
|
|
401d3c2056 | ||
|
|
30662bc11b | ||
|
|
b650d2df31 | ||
|
|
bb2d7c9742 | ||
|
|
4795971f1c | ||
|
|
ea99593575 | ||
|
|
bf7ede1d43 | ||
|
|
43448d55f1 | ||
|
|
e362e6fe2f | ||
|
|
82d9616f1b | ||
|
|
999774889d | ||
|
|
c90ca4df87 | ||
|
|
a167332518 | ||
|
|
1b7efdd051 | ||
|
|
caff93d7b8 | ||
|
|
806a86a6ad | ||
|
|
42126bae72 | ||
|
|
803b9a7a18 | ||
|
|
a8fd6c132e | ||
|
|
1dcffdc872 | ||
|
|
a40c87eeed | ||
|
|
2829f7b485 | ||
|
|
79a205eee2 | ||
|
|
e039676422 | ||
|
|
9616596146 | ||
|
|
ad546edbb7 | ||
|
|
a07b429510 | ||
|
|
94d6ca22b2 | ||
|
|
78f98fa08f | ||
|
|
eb3bad823b | ||
|
|
5374252470 | ||
|
|
f7630075ff | ||
|
|
8677f3db03 | ||
|
|
9add30b900 | ||
|
|
2e50651400 | ||
|
|
bfee24421a | ||
|
|
86fee6e4af | ||
|
|
48ea97139e | ||
|
|
67fbd31e46 | ||
|
|
7b40e8afcb | ||
|
|
d73384ae46 | ||
|
|
456e0f2679 | ||
|
|
852efb06a8 | ||
|
|
1b1efc5d49 | ||
|
|
67bfbee567 | ||
|
|
3deea03ff5 | ||
|
|
c139aff8d9 | ||
|
|
98f900183d | ||
|
|
059087ed21 | ||
|
|
d3778af3e8 | ||
|
|
663151821f | ||
|
|
8c4a732eb7 | ||
|
|
2d0b1db3dd | ||
|
|
4e3ccb4fc5 | ||
|
|
66ab62b3fb | ||
|
|
2a6e20dd32 | ||
|
|
7516e418f2 | ||
|
|
3864f06a15 | ||
|
|
98dcea6e7d | ||
|
|
9ee51b06fa | ||
|
|
4efca40457 | ||
|
|
a43e3e5948 | ||
|
|
f64f71b9ac | ||
|
|
60298f67b8 | ||
|
|
4d2354da2e | ||
|
|
6a3da67a1e | ||
|
|
6ebafa8a9e | ||
|
|
3ba7e48615 | ||
|
|
9de91539e6 | ||
|
|
51b0661d9d | ||
|
|
3c85ff4744 | ||
|
|
6a383a62b8 | ||
|
|
0fa7050b1c | ||
|
|
fa1554cac0 | ||
|
|
14071086bb | ||
|
|
30f9bfaf83 | ||
|
|
daebc2cc39 | ||
|
|
6b089a611c | ||
|
|
8a2c5407d8 | ||
|
|
52a039585d | ||
|
|
fd6bd7fb67 | ||
|
|
b544cf2ffe | ||
|
|
6d1281301f | ||
|
|
901192cca1 | ||
|
|
3478c68af7 | ||
|
|
67e7ba4812 | ||
|
|
981c132cf4 | ||
|
|
572376091e | ||
|
|
803e73fdec | ||
|
|
e7c9808b87 | ||
|
|
82eb8e3492 | ||
|
|
21a5c2891e | ||
|
|
96289775f2 | ||
|
|
3e4269d32a | ||
|
|
bd6d43de52 | ||
|
|
8da986d41a | ||
|
|
eb7aa3420f | ||
|
|
4fb851954b | ||
|
|
d1399d9145 | ||
|
|
ba40fbc53d | ||
|
|
c8d4d870e8 | ||
|
|
fcb458e449 | ||
|
|
48a8f753f5 | ||
|
|
6293f3110c | ||
|
|
ce34b37e02 | ||
|
|
b454cac1dc | ||
|
|
a9a943b858 | ||
|
|
85c1660edf | ||
|
|
664baac09e | ||
|
|
b247e4a213 | ||
|
|
86f91eed2f | ||
|
|
ea7aeb2507 | ||
|
|
61b2416a7c | ||
|
|
41cecbfb0f | ||
|
|
e3b547b1b0 | ||
|
|
93dd8d0e4f | ||
|
|
12b3e95ed3 | ||
|
|
a94c0f745f | ||
|
|
7fc3b1ddfe | ||
|
|
ca33d4111b | ||
|
|
9315da79bc | ||
|
|
4325c86b0a | ||
|
|
e3b9c9362d | ||
|
|
11882bc6fa | ||
|
|
155447f541 | ||
|
|
b908d6ad04 | ||
|
|
238e40966f | ||
|
|
29e73ef317 | ||
|
|
f9192b5f75 | ||
|
|
2917057da8 | ||
|
|
6464fcbc14 | ||
|
|
305e0172f3 | ||
|
|
378f608aad | ||
|
|
9712c1062f | ||
|
|
c7c9ffe7e1 | ||
|
|
368ef5687b | ||
|
|
b7dc975331 | ||
|
|
44f308502e | ||
|
|
ec477801ca | ||
|
|
16ef68ea4d | ||
|
|
21c4fb3880 | ||
|
|
82587fe854 | ||
|
|
1d627b47b7 | ||
|
|
f71d6c615a | ||
|
|
9272967096 | ||
|
|
9914293777 | ||
|
|
0e3e060fce | ||
|
|
c892174378 | ||
|
|
bbfa4c69af | ||
|
|
6d563be2b4 | ||
|
|
14ce07b9cb | ||
|
|
783273cdab | ||
|
|
a3e17acc65 | ||
|
|
e130727ed6 | ||
|
|
c3622ef7fb | ||
|
|
7018a3b3ba | ||
|
|
8cd4d07fe7 | ||
|
|
3f4f438445 | ||
|
|
10821ec078 | ||
|
|
e4ad2082bc | ||
|
|
b1d2eaa54b | ||
|
|
7afd0eb1aa | ||
|
|
16b4aba3cf | ||
|
|
89330aa157 | ||
|
|
de84169701 | ||
|
|
d6466bdbc7 | ||
|
|
548b434f49 | ||
|
|
112a2c5d92 | ||
|
|
fef5d3f88f | ||
|
|
8e461db301 | ||
|
|
6decdfad26 | ||
|
|
1bf04d22dc | ||
|
|
c4a4a86cff | ||
|
|
c7729cb019 | ||
|
|
8d8bd21cda | ||
|
|
c80c06cabd | ||
|
|
7a7cc66141 | ||
|
|
107e470410 | ||
|
|
3173283166 | ||
|
|
2a74dd27e1 | ||
|
|
2573863d82 | ||
|
|
8fa94dbdf3 | ||
|
|
28364970de | ||
|
|
ed4f00d4a3 | ||
|
|
1f4c56f6a1 | ||
|
|
55ef8393af | ||
|
|
c3b823552f | ||
|
|
081f953dc3 | ||
|
|
f4e410f47f | ||
|
|
da4fc73c06 | ||
|
|
3859f48ddd | ||
|
|
11bdc66a35 | ||
|
|
857c8be1c9 | ||
|
|
bbdb202e2c | ||
|
|
9d43a9326e | ||
|
|
35dc54f242 | ||
|
|
d405af6db7 | ||
|
|
282b475532 | ||
|
|
8300e9ca87 | ||
|
|
ef6f93a40c | ||
|
|
f75db5106f | ||
|
|
0d09366608 | ||
|
|
9ea8461440 | ||
|
|
55116a7462 | ||
|
|
161f5eb731 | ||
|
|
85995975d8 | ||
|
|
ed8c0dc99d | ||
|
|
92fcf2f78c | ||
|
|
155d3f8cd8 | ||
|
|
b9bb444bf0 | ||
|
|
4337a4cd0d | ||
|
|
e5bd6865ca | ||
|
|
87dc930dcf | ||
|
|
55fb382445 | ||
|
|
aa20b23203 | ||
|
|
940029c844 | ||
|
|
3b85680ad7 | ||
|
|
7babd079ea | ||
|
|
be44d4801f | ||
|
|
7223a1bac8 | ||
|
|
caf0fa84af | ||
|
|
1e2cdf3feb | ||
|
|
7bd8b08e16 | ||
|
|
c27870b15d | ||
|
|
ed3fb5a1b5 | ||
|
|
08506dcee8 | ||
|
|
25c0073b2d | ||
|
|
a556824875 | ||
|
|
89fc5ebc97 | ||
|
|
67dfa5e2bc | ||
|
|
13ddf267bb | ||
|
|
7366259c64 | ||
|
|
1b90ccde27 | ||
|
|
43d9d6fe64 | ||
|
|
4a579700a0 | ||
|
|
c1b412d5f3 | ||
|
|
f3f9911b62 | ||
|
|
2c874167a1 | ||
|
|
becb6559f1 | ||
|
|
92cd779c19 | ||
|
|
36f2654fa6 | ||
|
|
ba18a8b3e3 | ||
|
|
ab8ac72c4f | ||
|
|
1b3c9aa98e | ||
|
|
bafbd4235a | ||
|
|
900aab45f1 | ||
|
|
bc7dba2bb6 | ||
|
|
bc41d25657 | ||
|
|
094d64f872 | ||
|
|
b085585461 | ||
|
|
49ef4e00df | ||
|
|
8314ad9ca0 | ||
|
|
5544f0d346 | ||
|
|
2cc89c8888 | ||
|
|
4a45fe9849 | ||
|
|
e26996be93 | ||
|
|
ef3fcf6635 | ||
|
|
4704de51be | ||
|
|
b633444e11 | ||
|
|
dc11bb8709 | ||
|
|
5dc8bfe95e | ||
|
|
4d05cd3059 | ||
|
|
c7dfdfc6d7 | ||
|
|
2dc2465b13 | ||
|
|
f2fdc476b3 | ||
|
|
0714c5b7b8 | ||
|
|
21815ddfa0 | ||
|
|
6eefff4e29 | ||
|
|
2541ec1565 | ||
|
|
e114050222 | ||
|
|
43b6e7962d | ||
|
|
f5f5e2bdae | ||
|
|
c05f0589fc | ||
|
|
6b91ba5353 | ||
|
|
5ef8a90aa0 | ||
|
|
c9dfaa36b4 | ||
|
|
a74940f1c0 | ||
|
|
4da67712a9 | ||
|
|
4d9b7c47f8 | ||
|
|
3d43b740fd | ||
|
|
eeb24b67e1 | ||
|
|
4bf9cc6546 | ||
|
|
2de3b6aed7 | ||
|
|
efd17f78bf | ||
|
|
54154e7c68 | ||
|
|
935d496c70 | ||
|
|
c4994d4f62 | ||
|
|
b39b2fbe23 | ||
|
|
d9261ae66d | ||
|
|
7f17b90fb9 | ||
|
|
d2e9e8ebd6 | ||
|
|
9758b15508 | ||
|
|
b2cc98e083 | ||
|
|
f27c80cbe4 | ||
|
|
4cc2c39a19 | ||
|
|
159e2bed69 | ||
|
|
54c62428ae | ||
|
|
70debb1c98 | ||
|
|
f0199d5de9 | ||
|
|
cf26ca6043 | ||
|
|
a5869c2a9c | ||
|
|
95f39149d7 | ||
|
|
be1542cc8a | ||
|
|
0a5abac71b | ||
|
|
1428853e5e | ||
|
|
e6bae1a97e | ||
|
|
068fc3476b | ||
|
|
f11b8615da | ||
|
|
5d4bde98dc | ||
|
|
b8b072cf86 | ||
|
|
18f7e0e6b3 | ||
|
|
a9f3d6a2d9 | ||
|
|
7755014a7d | ||
|
|
8d0ce49eb4 | ||
|
|
c93a470e96 | ||
|
|
527129f17c | ||
|
|
e23c75d31e | ||
|
|
f021df399e | ||
|
|
ad7ecfa9cb | ||
|
|
39472ccb41 | ||
|
|
2f77d50e0f | ||
|
|
21bd0ff6aa | ||
|
|
d0017ded5b | ||
|
|
2cc4b551e7 | ||
|
|
77428b79c0 | ||
|
|
f4d7d06c41 | ||
|
|
b65f6ab7b4 | ||
|
|
280297cb97 | ||
|
|
26419931e2 | ||
|
|
c027d9116f | ||
|
|
79832e60d0 | ||
|
|
de32116ded | ||
|
|
181adb081e | ||
|
|
d9bf8b7343 | ||
|
|
2394ac276c | ||
|
|
b3e09e5c68 | ||
|
|
9fad6d0b7e | ||
|
|
9ee1a51f1a | ||
|
|
cf691a43b2 | ||
|
|
d4110bf650 | ||
|
|
4b6f6f21d1 | ||
|
|
ff6f7d3248 | ||
|
|
0a1fa05c8f | ||
|
|
cb9fbf8970 | ||
|
|
a430b3a426 | ||
|
|
fbeb0e8e54 | ||
|
|
9d63642bdb | ||
|
|
8cb701e412 | ||
|
|
d41c84d624 | ||
|
|
61ccdc2d9e | ||
|
|
9f1a427ce2 | ||
|
|
e962bdd06f | ||
|
|
32f0fa5a96 | ||
|
|
9b69516ac7 | ||
|
|
8752ffef93 | ||
|
|
ed1a9fd1e2 | ||
|
|
edf9730c24 | ||
|
|
97a6d87c2f | ||
|
|
c3220b04a0 | ||
|
|
08f651fd38 | ||
|
|
ae71f07abb | ||
|
|
5a2774876a | ||
|
|
6de59fa246 | ||
|
|
ccf5c1f7e9 | ||
|
|
f786b4cd95 | ||
|
|
72add75eea | ||
|
|
fc7328197d | ||
|
|
efecea9450 | ||
|
|
26e4cda610 | ||
|
|
a6543d32bd | ||
|
|
aa6650c86d | ||
|
|
14e379792c | ||
|
|
da947d060f | ||
|
|
ede2f205d3 | ||
|
|
8cf29c40a9 | ||
|
|
0332cbfdd4 | ||
|
|
1119003eb5 | ||
|
|
c089d9aeac | ||
|
|
4f0894e970 | ||
|
|
848c237159 | ||
|
|
6892805094 | ||
|
|
bd3b7aa50a | ||
|
|
bce4a9c9ab | ||
|
|
ae0dc8d21f | ||
|
|
9ba295d334 | ||
|
|
aad3ed3411 | ||
|
|
dfcf611a67 | ||
|
|
aa8ccfc32b | ||
|
|
18991686ab | ||
|
|
1501db38b1 | ||
|
|
bc6d88fabe | ||
|
|
62f34bea83 | ||
|
|
6114005952 | ||
|
|
c0e5ae4298 | ||
|
|
420de987bc | ||
|
|
61e33217cd | ||
|
|
b5b9a89561 | ||
|
|
bc9fc66225 | ||
|
|
6727fe9040 | ||
|
|
7e740208aa | ||
|
|
e1c8a3e679 | ||
|
|
81317b2108 | ||
|
|
b94b98ad90 | ||
|
|
56110d4495 | ||
|
|
1362ff6cba | ||
|
|
dbd7401721 | ||
|
|
f1f9c14e93 | ||
|
|
f0801ecac0 | ||
|
|
89cc20c423 | ||
|
|
1b1d74d679 | ||
|
|
8141d9838b | ||
|
|
16e645d8d6 | ||
|
|
379652f631 | ||
|
|
69d9085944 | ||
|
|
7cc83400a4 | ||
|
|
440de782e8 | ||
|
|
333ac18f53 | ||
|
|
37389a9709 | ||
|
|
b1dff2b2d8 | ||
|
|
6cfce56f98 | ||
|
|
3f90fe0623 | ||
|
|
d95d4afdae | ||
|
|
dbfec4e53a | ||
|
|
b59c100235 | ||
|
|
7a5352453e | ||
|
|
45f6321ba1 | ||
|
|
18c152723c | ||
|
|
3fb794206d | ||
|
|
e764483f9a | ||
|
|
7fcbb06a9e | ||
|
|
09b76d5e4a | ||
|
|
8791c24072 | ||
|
|
652c02b9ab | ||
|
|
4ab552d750 | ||
|
|
e420964b93 | ||
|
|
7d717a78dc | ||
|
|
2f0abd5c3f | ||
|
|
d49d8095df | ||
|
|
8a8c1290db | ||
|
|
01ffeba2c2 | ||
|
|
78ed898f0b | ||
|
|
75ee9a718a | ||
|
|
bfeb447178 | ||
|
|
29f8d70b35 | ||
|
|
abbca13abf | ||
|
|
54aee071ec | ||
|
|
c8d7b94270 | ||
|
|
9c641dae45 | ||
|
|
dc32535a9b | ||
|
|
d910386359 | ||
|
|
8c22fcf8df | ||
|
|
719971cbbf | ||
|
|
60b1f6a5b3 | ||
|
|
dcc80d5d31 | ||
|
|
03e5eb1a1e | ||
|
|
787dde2a29 | ||
|
|
5e80b98947 | ||
|
|
2b3c05e4f1 | ||
|
|
ae6e4bb7a9 | ||
|
|
57a16c483d | ||
|
|
fb990698d1 | ||
|
|
2342551402 | ||
|
|
bee957120d | ||
|
|
57e51f60ef | ||
|
|
00506984f0 | ||
|
|
1462647c4d | ||
|
|
9ee5722618 | ||
|
|
4081eb68f8 | ||
|
|
023a702fb9 | ||
|
|
355da3ad24 | ||
|
|
79467eee12 | ||
|
|
4beed62df1 | ||
|
|
6229ad8c9c | ||
|
|
b628f0bf29 | ||
|
|
1e96573213 | ||
|
|
cf33a61e33 | ||
|
|
cc1cb28856 | ||
|
|
89bd9b610e | ||
|
|
9dbcf1447b | ||
|
|
6c853cae57 | ||
|
|
48e6efb6aa | ||
|
|
cfc3b3336f | ||
|
|
9ca394d1e5 | ||
|
|
634449ff4f | ||
|
|
89a7f425de | ||
|
|
7385150178 | ||
|
|
abce9bb380 | ||
|
|
49c75296cf | ||
|
|
d94ac0e2f8 | ||
|
|
e6ff6f57e7 | ||
|
|
ad955f02ea | ||
|
|
b2692016f5 | ||
|
|
5891a00132 | ||
|
|
e62a87afe1 | ||
|
|
5b68d9b589 | ||
|
|
231586e537 | ||
|
|
8c8ae8b9c6 | ||
|
|
52d7d3c637 | ||
|
|
93ee2ce826 | ||
|
|
083c14f70d | ||
|
|
2cfb1ab2e6 | ||
|
|
9afe0824d9 | ||
|
|
65c46e39e3 | ||
|
|
1ff2f3b6a3 | ||
|
|
891382a32e | ||
|
|
7c27835c03 | ||
|
|
fb31561f1c | ||
|
|
a5253d45b0 | ||
|
|
385750d8c3 | ||
|
|
a8852e9d7d | ||
|
|
c1b78e0c14 | ||
|
|
3ebe6a38b1 | ||
|
|
0fd50b2381 | ||
|
|
7c017f4075 | ||
|
|
8b9d2f5a47 | ||
|
|
f3ce739d26 | ||
|
|
7d576a24a4 | ||
|
|
b2f5dbc77b | ||
|
|
7f4c954ff2 | ||
|
|
8506b9b330 | ||
|
|
93c7d16871 | ||
|
|
f78b6dd8c3 | ||
|
|
bd29e870ce | ||
|
|
da2b8aecf1 | ||
|
|
d00bf3f49d | ||
|
|
534584ab31 | ||
|
|
4a415dcbd1 | ||
|
|
c69e6e4363 | ||
|
|
1f4be6512f | ||
|
|
dcc8f50750 | ||
|
|
b298837276 | ||
|
|
726c5daa74 | ||
|
|
8b3f020dba | ||
|
|
49b652ed89 | ||
|
|
a60dea1d83 | ||
|
|
8556ae7209 | ||
|
|
69be581346 | ||
|
|
e23d295e8b | ||
|
|
3b5c4c2416 | ||
|
|
2d1fbe0736 | ||
|
|
1b6ae4348b | ||
|
|
67febb13c0 | ||
|
|
e46de0c40a | ||
|
|
16d40be33c | ||
|
|
53b6be6a49 | ||
|
|
ca99f1bda4 | ||
|
|
e68b302bba | ||
|
|
3e11a9d8a5 | ||
|
|
3f93633404 | ||
|
|
11a7d462fe | ||
|
|
9dcb469460 | ||
|
|
fe6357c2f4 | ||
|
|
6752a50417 | ||
|
|
7409886a2d | ||
|
|
5377943439 | ||
|
|
f8b964554a | ||
|
|
c95cecd697 | ||
|
|
0c83c0a386 | ||
|
|
9b3a9bc3ef | ||
|
|
d0cc602979 | ||
|
|
5e3561d60b | ||
|
|
60028036ed | ||
|
|
c6a7616de0 | ||
|
|
ca9ed369f9 | ||
|
|
b3d5961ae4 | ||
|
|
30c94c2c11 | ||
|
|
f36f171647 | ||
|
|
cb91215e03 | ||
|
|
4e96b20b46 | ||
|
|
a1a60c44da | ||
|
|
2c99652f35 | ||
|
|
898c8a5836 | ||
|
|
849df4b2a8 | ||
|
|
5f7582ffdb | ||
|
|
dcd0f53027 | ||
|
|
b5e073bf7f | ||
|
|
cde2199b64 | ||
|
|
a1eef9870c | ||
|
|
19e9ab253e | ||
|
|
e3a99f12e4 | ||
|
|
d31a860bf2 | ||
|
|
cfea3472bd | ||
|
|
31859a3eb5 | ||
|
|
9f3e5f990f | ||
|
|
f317f58545 | ||
|
|
01c23eace3 | ||
|
|
9b8556c2b2 | ||
|
|
9628c213b5 | ||
|
|
07a71c412d | ||
|
|
0d736e4143 | ||
|
|
a93e3b6fa0 | ||
|
|
22ab20ba4c | ||
|
|
6ee51b0159 | ||
|
|
e2b3186731 | ||
|
|
31aa58c45d | ||
|
|
a757cb3c91 | ||
|
|
91ad54d864 | ||
|
|
3703755e03 | ||
|
|
c1d380dee4 | ||
|
|
b2120609b9 | ||
|
|
9e6e8a7ecb | ||
|
|
de06b36544 | ||
|
|
695df9b979 | ||
|
|
aa91cdd984 | ||
|
|
284a9cdab6 | ||
|
|
77ebfc8687 | ||
|
|
899f2bbac5 | ||
|
|
bb35e7b4b5 | ||
|
|
64e4edd70f | ||
|
|
300b7169ad | ||
|
|
1353dbc31e | ||
|
|
300eea034b | ||
|
|
90a06b5249 | ||
|
|
1b7b307d08 | ||
|
|
a946aefbed | ||
|
|
8708f96de4 | ||
|
|
bd056b3b9e | ||
|
|
5d49c81e2d | ||
|
|
bec7d6d223 | ||
|
|
973105f2e5 | ||
|
|
53fb876738 | ||
|
|
d2bc168f39 | ||
|
|
34ec72ad49 | ||
|
|
85c814b712 | ||
|
|
fc951baebc | ||
|
|
a1cdfe71de | ||
|
|
c1971955a3 | ||
|
|
e1df75fc9b | ||
|
|
ea83330ab9 | ||
|
|
4cdf0224ba | ||
|
|
20edd11ca7 | ||
|
|
47f029b713 | ||
|
|
f4be547d41 | ||
|
|
9a8c71a58b | ||
|
|
d45a20af83 | ||
|
|
1a7435250e | ||
|
|
3c91d72403 | ||
|
|
d37c37ef62 | ||
|
|
aad3764806 | ||
|
|
0a63fc6f05 | ||
|
|
50e739ee8e | ||
|
|
6c84f20491 | ||
|
|
a68506f924 | ||
|
|
a20d42ca0b | ||
|
|
4ec8846198 | ||
|
|
40ea65b1c0 | ||
|
|
f7937ef952 | ||
|
|
d6bf137026 | ||
|
|
ed9a672f44 | ||
|
|
6e2f7a196f | ||
|
|
e2182b6227 | ||
|
|
cfe121b38b | ||
|
|
77fa58541f | ||
|
|
5fbd9d5b14 | ||
|
|
2b1783ce61 | ||
|
|
904072ce79 | ||
|
|
0a4b98d74a | ||
|
|
49840ed4fa | ||
|
|
823b5ac1ab | ||
|
|
b8017de724 | ||
|
|
ca96604582 | ||
|
|
d18d378f06 | ||
|
|
83e3752544 | ||
|
|
0490b2d450 | ||
|
|
55ff740e4e | ||
|
|
aba8a83cba | ||
|
|
a23809d5db | ||
|
|
32fc3ea6f5 | ||
|
|
deb8ffd348 | ||
|
|
41f7c5f15f | ||
|
|
fe787c03f9 | ||
|
|
d627fa2068 | ||
|
|
1d297f990c | ||
|
|
6de2049076 | ||
|
|
cd43f8474e | ||
|
|
c2ff653d1b | ||
|
|
13c0140375 | ||
|
|
ff128ecc7e | ||
|
|
cea87b4190 | ||
|
|
9c398b1ad4 | ||
|
|
21da776b71 | ||
|
|
cfe7ad538d | ||
|
|
79805f07b3 | ||
|
|
025c492dd3 | ||
|
|
ecc0b366b3 | ||
|
|
2839d1179e | ||
|
|
d4465dd63f | ||
|
|
f0e04169f7 | ||
|
|
bb3179f26d | ||
|
|
9c59302278 | ||
|
|
11ef339d58 | ||
|
|
6a48665ed0 | ||
|
|
d833c78c5b | ||
|
|
ad7f62a368 | ||
|
|
9f0ddcff54 | ||
|
|
04442579ca | ||
|
|
eed8357948 | ||
|
|
044fa4d72a | ||
|
|
6a17db8857 | ||
|
|
9a8510ed9a | ||
|
|
0843ec6ae8 | ||
|
|
14be8253ab | ||
|
|
74c84c8747 | ||
|
|
3e9a6c582e | ||
|
|
6ca1b90752 | ||
|
|
d91ebd2113 | ||
|
|
084113926c | ||
|
|
a10cc04e3b | ||
|
|
06ae11e002 | ||
|
|
fe1aa7e9ba | ||
|
|
a5f60750c2 | ||
|
|
183081cfbc | ||
|
|
a382383d83 | ||
|
|
cd96c1fe18 | ||
|
|
fd564352c8 | ||
|
|
52d0f1cc68 | ||
|
|
e0c772a243 | ||
|
|
a883b3fdec | ||
|
|
3f9d6e39a9 | ||
|
|
03cfd87b16 | ||
|
|
6d8294c2d3 | ||
|
|
6a3205f4db | ||
|
|
6f22509883 | ||
|
|
455ade0dca | ||
|
|
87fcfc9d76 | ||
|
|
393f1007e3 | ||
|
|
4e67898073 | ||
|
|
d86048cc2d | ||
|
|
0c868cbcc5 | ||
|
|
e8ea90cb13 | ||
|
|
93c6414c8b | ||
|
|
b6023023ee | ||
|
|
2b5d212644 | ||
|
|
e1355de4cb | ||
|
|
7385c4cf3d | ||
|
|
a3c2248b44 | ||
|
|
3bd6ec4ec7 | ||
|
|
b203b8cee7 | ||
|
|
258e57ff17 | ||
|
|
051604f284 | ||
|
|
3744186c3d | ||
|
|
10dfd95ff2 | ||
|
|
ba6e050c91 | ||
|
|
07746d1724 | ||
|
|
9d25e385d9 | ||
|
|
04019c7eca | ||
|
|
22e0a8ce2e | ||
|
|
4696d7eb6a | ||
|
|
b4f63fd992 | ||
|
|
ded835ab63 | ||
|
|
73a249c075 | ||
|
|
3b4c3a5046 | ||
|
|
3841c8e651 | ||
|
|
d3ec76b55c | ||
|
|
fe6f27c526 | ||
|
|
911f9bfa26 | ||
|
|
3d789f9ccc | ||
|
|
cd80ceacfc | ||
|
|
f73c539ea7 | ||
|
|
0b7d2699f3 | ||
|
|
f2092df3bc | ||
|
|
f87aa384d0 | ||
|
|
f9687a2a31 | ||
|
|
8d8a728bd9 | ||
|
|
8c59b45a3c | ||
|
|
7458f64f15 | ||
|
|
429cc11948 | ||
|
|
4598205c88 | ||
|
|
856f54667c | ||
|
|
3e1a5a06f5 | ||
|
|
bc2e8e33ee | ||
|
|
f084d320fc | ||
|
|
f93382445e | ||
|
|
6e2887dacb | ||
|
|
463363a08d | ||
|
|
a0790f926e | ||
|
|
d569f577a1 | ||
|
|
ca59ab8f37 | ||
|
|
a5b389547b | ||
|
|
e5f70d1677 | ||
|
|
e2cd8a6004 | ||
|
|
8dc2a7d9d7 | ||
|
|
157e978af4 | ||
|
|
83a77118bd | ||
|
|
b2474c6de9 | ||
|
|
3aaf10b6a8 | ||
|
|
33f545a8e3 | ||
|
|
d056e1040b | ||
|
|
75a78b2bf3 | ||
|
|
cd6314dc96 | ||
|
|
f91bffff9a | ||
|
|
5cbe9af485 | ||
|
|
b445d46888 | ||
|
|
a7fbecb25c | ||
|
|
bf92d94863 | ||
|
|
9c3817f544 | ||
|
|
ee9e3315b6 | ||
|
|
67dea1e538 | ||
|
|
003b9c6c3f | ||
|
|
2f1a345905 | ||
|
|
7ef933abec | ||
|
|
4ddd40bcfb | ||
|
|
8ae901b3f1 | ||
|
|
bc49174920 | ||
|
|
123ee02d39 | ||
|
|
0cc8055757 | ||
|
|
8a86105aa6 | ||
|
|
370191a0f0 | ||
|
|
11783e9060 | ||
|
|
27a212c14d | ||
|
|
65dc182526 | ||
|
|
dd91039ff1 | ||
|
|
99524a83cd | ||
|
|
d602a2e5e4 | ||
|
|
1c851bc32e | ||
|
|
40557c579c | ||
|
|
5981f3a35a | ||
|
|
c15bfd243a | ||
|
|
3e994a3ded | ||
|
|
106f74f43b | ||
|
|
53227ec12f | ||
|
|
3930f21fad | ||
|
|
365d325feb | ||
|
|
bcc7e68b4c | ||
|
|
6da4f95258 | ||
|
|
0207e6e8b5 | ||
|
|
ee6e12913c | ||
|
|
d95ef154aa | ||
|
|
8730581ef2 | ||
|
|
3cad649434 | ||
|
|
d06d1229e4 | ||
|
|
b6a062445a | ||
|
|
5cdd012d26 | ||
|
|
97babdcc94 | ||
|
|
b2544d1e7b | ||
|
|
d75254309f | ||
|
|
5d1acb0cb8 | ||
|
|
1c9a9c7536 | ||
|
|
c0ffb13620 | ||
|
|
dcab12adae | ||
|
|
fb714636e3 | ||
|
|
05a431ea54 | ||
|
|
804a0c6d05 | ||
|
|
177fa272b0 | ||
|
|
5976199fcc | ||
|
|
1a34b4e7d7 | ||
|
|
d455a544a9 | ||
|
|
f3448e24be | ||
|
|
cafc7651c2 | ||
|
|
4099e944d6 | ||
|
|
5ad989a13a | ||
|
|
0fe2d68c59 | ||
|
|
b82149c291 | ||
|
|
ffe2ecdd3c | ||
|
|
a7223a2cd7 | ||
|
|
3bc1ea45ce | ||
|
|
c4d27c6af7 | ||
|
|
d692ac281c | ||
|
|
011407ea8b | ||
|
|
1141e83a7c | ||
|
|
27efbe919b | ||
|
|
929af941f8 | ||
|
|
214ce95cf3 | ||
|
|
3a7b83ba93 | ||
|
|
cc2f3d85dc | ||
|
|
723f67d5e2 | ||
|
|
70e45706d9 | ||
|
|
56a2a2269f | ||
|
|
d6841ba33a | ||
|
|
10cbd0164a | ||
|
|
d285706b41 | ||
|
|
ef469c20df | ||
|
|
6870d3dc50 | ||
|
|
9cc39621a6 | ||
|
|
c4f7d09553 | ||
|
|
ab1661ef22 | ||
|
|
ccbf17d5ab | ||
|
|
bac96086be | ||
|
|
c32e4bc65b | ||
|
|
ca6f8a7a73 | ||
|
|
51bf568b8f | ||
|
|
8a0d99285c | ||
|
|
7e456265a4 | ||
|
|
6954a69ed2 | ||
|
|
0fbeb3ace6 | ||
|
|
46b1e30c10 | ||
|
|
9538ec6f9b | ||
|
|
9855555978 | ||
|
|
a2ff099e92 | ||
|
|
b8d21a00ba | ||
|
|
993765d732 | ||
|
|
2baebe92af | ||
|
|
1c4997d51d | ||
|
|
3853e3a4dc | ||
|
|
36302338e6 | ||
|
|
8d84fe0113 | ||
|
|
5e76680396 | ||
|
|
58746b737f | ||
|
|
f93e843972 | ||
|
|
1e2d9f6ecb | ||
|
|
8188caaff1 | ||
|
|
42698eedee | ||
|
|
8e68f95a8a | ||
|
|
97e71756e0 | ||
|
|
371dd65a50 | ||
|
|
ae57e3e52f | ||
|
|
70f95cb6a8 | ||
|
|
9a978273b1 | ||
|
|
ea61ad2764 | ||
|
|
9c735b7e95 | ||
|
|
fdbf457afd | ||
|
|
195301f9fc | ||
|
|
dd98b6a3a7 | ||
|
|
139042acbe | ||
|
|
b23f1b203f | ||
|
|
2f58cb89b6 | ||
|
|
d558291403 | ||
|
|
60968d311b | ||
|
|
66a21d3059 | ||
|
|
30584e2e96 | ||
|
|
468ae39a9e | ||
|
|
beb9c8d328 | ||
|
|
cdda3fb7cc | ||
|
|
bba00a3906 | ||
|
|
42e50ca178 | ||
|
|
165e362a1b | ||
|
|
e4763f8e71 | ||
|
|
9fddd0659e | ||
|
|
faea546a0e | ||
|
|
069db2e128 | ||
|
|
5f2203b915 | ||
|
|
5c67e04fef | ||
|
|
0cdcacc7fc | ||
|
|
4c0e0b8d76 | ||
|
|
48b80858a7 | ||
|
|
081081f69a | ||
|
|
8abb783b64 | ||
|
|
bccfe9eead | ||
|
|
35fb44da36 | ||
|
|
1dfb8926d3 | ||
|
|
0d63c755b7 | ||
|
|
6f2ca4c2a7 | ||
|
|
8d51e2f580 | ||
|
|
11fb46ad11 | ||
|
|
9245c691d0 | ||
|
|
971a1a3e00 |
@@ -1 +1 @@
|
||||
15dc295268b2dcf75942f42759b3ddec64eba89f75525698eb39c95a7f4b14ce
|
||||
0f2b9a65dce7c59289d3aeb40936360a62a7be937b585147b45bb1509eaafb36
|
||||
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
38
.github/scripts/auto-label-pr/constants.js
vendored
Normal file
38
.github/scripts/auto-label-pr/constants.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
// Constants and markers for PR auto-labeling
|
||||
module.exports = {
|
||||
BOT_COMMENT_MARKER: '<!-- auto-label-pr-bot -->',
|
||||
CODEOWNERS_MARKER: '<!-- codeowners-request -->',
|
||||
TOO_BIG_MARKER: '<!-- too-big-request -->',
|
||||
DEPRECATED_COMPONENT_MARKER: '<!-- deprecated-component-request -->',
|
||||
|
||||
MANAGED_LABELS: [
|
||||
'new-component',
|
||||
'new-platform',
|
||||
'new-target-platform',
|
||||
'merging-to-release',
|
||||
'merging-to-beta',
|
||||
'chained-pr',
|
||||
'core',
|
||||
'small-pr',
|
||||
'dashboard',
|
||||
'github-actions',
|
||||
'by-code-owner',
|
||||
'has-tests',
|
||||
'needs-tests',
|
||||
'needs-docs',
|
||||
'needs-codeowners',
|
||||
'too-big',
|
||||
'labeller-recheck',
|
||||
'bugfix',
|
||||
'new-feature',
|
||||
'breaking-change',
|
||||
'developer-breaking-change',
|
||||
'code-quality',
|
||||
'deprecated-component'
|
||||
],
|
||||
|
||||
DOCS_PR_PATTERNS: [
|
||||
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
||||
/esphome\/esphome-docs#\d+/
|
||||
]
|
||||
};
|
||||
373
.github/scripts/auto-label-pr/detectors.js
vendored
Normal file
373
.github/scripts/auto-label-pr/detectors.js
vendored
Normal file
@@ -0,0 +1,373 @@
|
||||
const fs = require('fs');
|
||||
const { DOCS_PR_PATTERNS } = require('./constants');
|
||||
|
||||
// Strategy: Merge branch detection
|
||||
async function detectMergeBranch(context) {
|
||||
const labels = new Set();
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
|
||||
if (baseRef === 'release') {
|
||||
labels.add('merging-to-release');
|
||||
} else if (baseRef === 'beta') {
|
||||
labels.add('merging-to-beta');
|
||||
} else if (baseRef !== 'dev') {
|
||||
labels.add('chained-pr');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Component and platform labeling
|
||||
async function detectComponentPlatforms(changedFiles, apiData) {
|
||||
const labels = new Set();
|
||||
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
|
||||
|
||||
for (const file of changedFiles) {
|
||||
const componentMatch = file.match(componentRegex);
|
||||
if (componentMatch) {
|
||||
labels.add(`component: ${componentMatch[1]}`);
|
||||
}
|
||||
|
||||
const platformMatch = file.match(targetPlatformRegex);
|
||||
if (platformMatch) {
|
||||
labels.add(`platform: ${platformMatch[1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: New component detection
|
||||
async function detectNewComponents(prFiles) {
|
||||
const labels = new Set();
|
||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||
|
||||
for (const file of addedFiles) {
|
||||
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
|
||||
if (componentMatch) {
|
||||
try {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
if (content.includes('IS_TARGET_PLATFORM = True')) {
|
||||
labels.add('new-target-platform');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Failed to read content of ${file}:`, error.message);
|
||||
}
|
||||
labels.add('new-component');
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: New platform detection
|
||||
async function detectNewPlatforms(prFiles, apiData) {
|
||||
const labels = new Set();
|
||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||
|
||||
for (const file of addedFiles) {
|
||||
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
|
||||
if (platformFileMatch) {
|
||||
const [, component, platform] = platformFileMatch;
|
||||
if (apiData.platformComponents.includes(platform)) {
|
||||
labels.add('new-platform');
|
||||
}
|
||||
}
|
||||
|
||||
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
|
||||
if (platformDirMatch) {
|
||||
const [, component, platform] = platformDirMatch;
|
||||
if (apiData.platformComponents.includes(platform)) {
|
||||
labels.add('new-platform');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Core files detection
|
||||
async function detectCoreChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const coreFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
|
||||
if (coreFiles.length > 0) {
|
||||
labels.add('core');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: PR size detection
|
||||
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
|
||||
const labels = new Set();
|
||||
|
||||
if (totalChanges <= SMALL_PR_THRESHOLD) {
|
||||
labels.add('small-pr');
|
||||
return labels;
|
||||
}
|
||||
|
||||
const testAdditions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||
const testDeletions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||
|
||||
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||
|
||||
// Don't add too-big if mega-pr label is already present
|
||||
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
|
||||
labels.add('too-big');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Dashboard changes
|
||||
async function detectDashboardChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const dashboardFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
|
||||
if (dashboardFiles.length > 0) {
|
||||
labels.add('dashboard');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: GitHub Actions changes
|
||||
async function detectGitHubActionsChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const githubActionsFiles = changedFiles.filter(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
|
||||
if (githubActionsFiles.length > 0) {
|
||||
labels.add('github-actions');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Code owner detection
|
||||
async function detectCodeOwner(github, context, changedFiles) {
|
||||
const labels = new Set();
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
try {
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS',
|
||||
});
|
||||
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
const codeownersRegexes = codeownersLines.map(line => {
|
||||
const parts = line.split(/\s+/);
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
let regex;
|
||||
if (pattern.endsWith('*')) {
|
||||
const dir = pattern.slice(0, -1);
|
||||
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
} else if (pattern.includes('*')) {
|
||||
// First escape all regex special chars except *, then replace * with .*
|
||||
const regexPattern = pattern
|
||||
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*');
|
||||
regex = new RegExp(`^${regexPattern}$`);
|
||||
} else {
|
||||
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
}
|
||||
|
||||
return { regex, owners };
|
||||
});
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { regex, owners } of codeownersRegexes) {
|
||||
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
|
||||
labels.add('by-code-owner');
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to read or parse CODEOWNERS file:', error.message);
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Test detection
|
||||
async function detectTests(changedFiles) {
|
||||
const labels = new Set();
|
||||
const testFiles = changedFiles.filter(file => file.startsWith('tests/'));
|
||||
|
||||
if (testFiles.length > 0) {
|
||||
labels.add('has-tests');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: PR Template Checkbox detection
|
||||
async function detectPRTemplateCheckboxes(context) {
|
||||
const labels = new Set();
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
|
||||
console.log('Checking PR template checkboxes...');
|
||||
|
||||
// Check for checked checkboxes in the "Types of changes" section
|
||||
const checkboxPatterns = [
|
||||
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
|
||||
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
||||
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
||||
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
|
||||
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||
];
|
||||
|
||||
for (const { pattern, label } of checkboxPatterns) {
|
||||
if (pattern.test(prBody)) {
|
||||
console.log(`Found checked checkbox for: ${label}`);
|
||||
labels.add(label);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Deprecated component detection
|
||||
async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||
const labels = new Set();
|
||||
const deprecatedInfo = [];
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
// Compile regex once for better performance
|
||||
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
|
||||
// Get files that are modified or added in components directory
|
||||
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
|
||||
|
||||
if (componentFiles.length === 0) {
|
||||
return { labels, deprecatedInfo };
|
||||
}
|
||||
|
||||
// Extract unique component names using the same regex
|
||||
const components = new Set();
|
||||
for (const file of componentFiles) {
|
||||
const match = file.match(componentFileRegex);
|
||||
if (match) {
|
||||
components.add(match[1]);
|
||||
}
|
||||
}
|
||||
|
||||
// Get PR head to fetch files from the PR branch
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
|
||||
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
|
||||
for (const component of components) {
|
||||
const initFile = `esphome/components/${component}/__init__.py`;
|
||||
try {
|
||||
// Fetch file content from PR head using GitHub API
|
||||
const { data: fileData } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: initFile,
|
||||
ref: `refs/pull/${prNumber}/head`
|
||||
});
|
||||
|
||||
// Decode base64 content
|
||||
const content = Buffer.from(fileData.content, 'base64').toString('utf8');
|
||||
|
||||
// Look for DEPRECATED_COMPONENT = "message" or DEPRECATED_COMPONENT = 'message'
|
||||
// Support single quotes, double quotes, and triple quotes (for multiline)
|
||||
const doubleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*"""([\s\S]*?)"""/s) ||
|
||||
content.match(/DEPRECATED_COMPONENT\s*=\s*"((?:[^"\\]|\\.)*)"/);
|
||||
const singleQuoteMatch = content.match(/DEPRECATED_COMPONENT\s*=\s*'''([\s\S]*?)'''/s) ||
|
||||
content.match(/DEPRECATED_COMPONENT\s*=\s*'((?:[^'\\]|\\.)*)'/);
|
||||
const deprecatedMatch = doubleQuoteMatch || singleQuoteMatch;
|
||||
|
||||
if (deprecatedMatch) {
|
||||
labels.add('deprecated-component');
|
||||
deprecatedInfo.push({
|
||||
component: component,
|
||||
message: deprecatedMatch[1].trim()
|
||||
});
|
||||
console.log(`Found deprecated component: ${component}`);
|
||||
}
|
||||
} catch (error) {
|
||||
// Only log if it's not a simple "file not found" error (404)
|
||||
if (error.status !== 404) {
|
||||
console.log(`Error reading ${initFile}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { labels, deprecatedInfo };
|
||||
}
|
||||
|
||||
// Strategy: Requirements detection
|
||||
async function detectRequirements(allLabels, prFiles, context) {
|
||||
const labels = new Set();
|
||||
|
||||
// Check for missing tests
|
||||
if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) {
|
||||
labels.add('needs-tests');
|
||||
}
|
||||
|
||||
// Check for missing docs
|
||||
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
|
||||
|
||||
if (!hasDocsLink) {
|
||||
labels.add('needs-docs');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing CODEOWNERS
|
||||
if (allLabels.has('new-component')) {
|
||||
const codeownersModified = prFiles.some(file =>
|
||||
file.filename === 'CODEOWNERS' &&
|
||||
(file.status === 'modified' || file.status === 'added') &&
|
||||
(file.additions || 0) > 0
|
||||
);
|
||||
|
||||
if (!codeownersModified) {
|
||||
labels.add('needs-codeowners');
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
detectMergeBranch,
|
||||
detectComponentPlatforms,
|
||||
detectNewComponents,
|
||||
detectNewPlatforms,
|
||||
detectCoreChanges,
|
||||
detectPRSize,
|
||||
detectDashboardChanges,
|
||||
detectGitHubActionsChanges,
|
||||
detectCodeOwner,
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectRequirements
|
||||
};
|
||||
187
.github/scripts/auto-label-pr/index.js
vendored
Normal file
187
.github/scripts/auto-label-pr/index.js
vendored
Normal file
@@ -0,0 +1,187 @@
|
||||
const { MANAGED_LABELS } = require('./constants');
|
||||
const {
|
||||
detectMergeBranch,
|
||||
detectComponentPlatforms,
|
||||
detectNewComponents,
|
||||
detectNewPlatforms,
|
||||
detectCoreChanges,
|
||||
detectPRSize,
|
||||
detectDashboardChanges,
|
||||
detectGitHubActionsChanges,
|
||||
detectCodeOwner,
|
||||
detectTests,
|
||||
detectPRTemplateCheckboxes,
|
||||
detectDeprecatedComponents,
|
||||
detectRequirements
|
||||
} = require('./detectors');
|
||||
const { handleReviews } = require('./reviews');
|
||||
const { applyLabels, removeOldLabels } = require('./labels');
|
||||
|
||||
// Fetch API data
|
||||
async function fetchApiData() {
|
||||
try {
|
||||
const response = await fetch('https://data.esphome.io/components.json');
|
||||
const componentsData = await response.json();
|
||||
return {
|
||||
targetPlatforms: componentsData.target_platforms || [],
|
||||
platformComponents: componentsData.platform_components || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('Failed to fetch components data from API:', error.message);
|
||||
return { targetPlatforms: [], platformComponents: [] };
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = async ({ github, context }) => {
|
||||
// Environment variables
|
||||
const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD);
|
||||
const MAX_LABELS = parseInt(process.env.MAX_LABELS);
|
||||
const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD);
|
||||
const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD);
|
||||
|
||||
// Global state
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
|
||||
// Get current labels and PR data
|
||||
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number
|
||||
});
|
||||
const currentLabels = currentLabelsData.map(label => label.name);
|
||||
const managedLabels = currentLabels.filter(label =>
|
||||
label.startsWith('component: ') || MANAGED_LABELS.includes(label)
|
||||
);
|
||||
|
||||
// Check for mega-PR early - if present, skip most automatic labeling
|
||||
const isMegaPR = currentLabels.includes('mega-pr');
|
||||
|
||||
// Get all PR files with automatic pagination
|
||||
const prFiles = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
}
|
||||
);
|
||||
|
||||
// Calculate data from PR files
|
||||
const changedFiles = prFiles.map(file => file.filename);
|
||||
const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||
const totalChanges = totalAdditions + totalDeletions;
|
||||
|
||||
console.log('Current labels:', currentLabels.join(', '));
|
||||
console.log('Changed files:', changedFiles.length);
|
||||
console.log('Total changes:', totalChanges);
|
||||
if (isMegaPR) {
|
||||
console.log('Mega-PR detected - applying limited labeling logic');
|
||||
}
|
||||
|
||||
// Fetch API data
|
||||
const apiData = await fetchApiData();
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
|
||||
// Early exit for release and beta branches only
|
||||
if (baseRef === 'release' || baseRef === 'beta') {
|
||||
const branchLabels = await detectMergeBranch(context);
|
||||
const finalLabels = Array.from(branchLabels);
|
||||
|
||||
console.log('Computed labels (merge branch only):', finalLabels.join(', '));
|
||||
|
||||
// Apply labels
|
||||
await applyLabels(github, context, finalLabels);
|
||||
|
||||
// Remove old managed labels
|
||||
await removeOldLabels(github, context, managedLabels, finalLabels);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Run all strategies
|
||||
const [
|
||||
branchLabels,
|
||||
componentLabels,
|
||||
newComponentLabels,
|
||||
newPlatformLabels,
|
||||
coreLabels,
|
||||
sizeLabels,
|
||||
dashboardLabels,
|
||||
actionsLabels,
|
||||
codeOwnerLabels,
|
||||
testLabels,
|
||||
checkboxLabels,
|
||||
deprecatedResult
|
||||
] = await Promise.all([
|
||||
detectMergeBranch(context),
|
||||
detectComponentPlatforms(changedFiles, apiData),
|
||||
detectNewComponents(prFiles),
|
||||
detectNewPlatforms(prFiles, apiData),
|
||||
detectCoreChanges(changedFiles),
|
||||
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD),
|
||||
detectDashboardChanges(changedFiles),
|
||||
detectGitHubActionsChanges(changedFiles),
|
||||
detectCodeOwner(github, context, changedFiles),
|
||||
detectTests(changedFiles),
|
||||
detectPRTemplateCheckboxes(context),
|
||||
detectDeprecatedComponents(github, context, changedFiles)
|
||||
]);
|
||||
|
||||
// Extract deprecated component info
|
||||
const deprecatedLabels = deprecatedResult.labels;
|
||||
const deprecatedInfo = deprecatedResult.deprecatedInfo;
|
||||
|
||||
// Combine all labels
|
||||
const allLabels = new Set([
|
||||
...branchLabels,
|
||||
...componentLabels,
|
||||
...newComponentLabels,
|
||||
...newPlatformLabels,
|
||||
...coreLabels,
|
||||
...sizeLabels,
|
||||
...dashboardLabels,
|
||||
...actionsLabels,
|
||||
...codeOwnerLabels,
|
||||
...testLabels,
|
||||
...checkboxLabels,
|
||||
...deprecatedLabels
|
||||
]);
|
||||
|
||||
// Detect requirements based on all other labels
|
||||
const requirementLabels = await detectRequirements(allLabels, prFiles, context);
|
||||
for (const label of requirementLabels) {
|
||||
allLabels.add(label);
|
||||
}
|
||||
|
||||
let finalLabels = Array.from(allLabels);
|
||||
|
||||
// For mega-PRs, exclude component labels if there are too many
|
||||
if (isMegaPR) {
|
||||
const componentLabels = finalLabels.filter(label => label.startsWith('component: '));
|
||||
if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) {
|
||||
finalLabels = finalLabels.filter(label => !label.startsWith('component: '));
|
||||
console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle too many labels (only for non-mega PRs)
|
||||
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
||||
const originalLabelCount = finalLabels.length;
|
||||
|
||||
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
|
||||
finalLabels = ['too-big'];
|
||||
}
|
||||
|
||||
console.log('Computed labels:', finalLabels.join(', '));
|
||||
|
||||
// Handle reviews
|
||||
await handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||
|
||||
// Apply labels
|
||||
await applyLabels(github, context, finalLabels);
|
||||
|
||||
// Remove old managed labels
|
||||
await removeOldLabels(github, context, managedLabels, finalLabels);
|
||||
};
|
||||
41
.github/scripts/auto-label-pr/labels.js
vendored
Normal file
41
.github/scripts/auto-label-pr/labels.js
vendored
Normal file
@@ -0,0 +1,41 @@
|
||||
// Apply labels to PR
|
||||
async function applyLabels(github, context, finalLabels) {
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
|
||||
if (finalLabels.length > 0) {
|
||||
console.log(`Adding labels: ${finalLabels.join(', ')}`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
labels: finalLabels
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Remove old managed labels
|
||||
async function removeOldLabels(github, context, managedLabels, finalLabels) {
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
|
||||
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
|
||||
for (const label of labelsToRemove) {
|
||||
console.log(`Removing label: ${label}`);
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
name: label
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Failed to remove label ${label}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
applyLabels,
|
||||
removeOldLabels
|
||||
};
|
||||
141
.github/scripts/auto-label-pr/reviews.js
vendored
Normal file
141
.github/scripts/auto-label-pr/reviews.js
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
const {
|
||||
BOT_COMMENT_MARKER,
|
||||
CODEOWNERS_MARKER,
|
||||
TOO_BIG_MARKER,
|
||||
DEPRECATED_COMPONENT_MARKER
|
||||
} = require('./constants');
|
||||
|
||||
// Generate review messages
|
||||
function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD) {
|
||||
const messages = [];
|
||||
|
||||
// Deprecated component message
|
||||
if (finalLabels.includes('deprecated-component') && deprecatedInfo && deprecatedInfo.length > 0) {
|
||||
let message = `${DEPRECATED_COMPONENT_MARKER}\n### ⚠️ Deprecated Component\n\n`;
|
||||
message += `Hey there @${prAuthor},\n`;
|
||||
message += `This PR modifies one or more deprecated components. Please be aware:\n\n`;
|
||||
|
||||
for (const info of deprecatedInfo) {
|
||||
message += `#### Component: \`${info.component}\`\n`;
|
||||
message += `${info.message}\n\n`;
|
||||
}
|
||||
|
||||
message += `Consider migrating to the recommended alternative if applicable.`;
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
// Too big message
|
||||
if (finalLabels.includes('too-big')) {
|
||||
const testAdditions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||
const testDeletions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||
|
||||
const tooManyLabels = originalLabelCount > MAX_LABELS;
|
||||
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
|
||||
|
||||
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
||||
|
||||
if (tooManyLabels && tooManyChanges) {
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
|
||||
} else if (tooManyLabels) {
|
||||
message += `This PR affects ${originalLabelCount} different components/areas.`;
|
||||
} else {
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
|
||||
}
|
||||
|
||||
message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`;
|
||||
message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`;
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
// CODEOWNERS message
|
||||
if (finalLabels.includes('needs-codeowners')) {
|
||||
const message = `${CODEOWNERS_MARKER}\n### 👥 Code Ownership\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`Thanks for submitting this pull request! Can you add yourself as a codeowner for this integration? ` +
|
||||
`This way we can notify you if a bug report for this integration is reported.\n\n` +
|
||||
`In \`__init__.py\` of the integration, please add:\n\n` +
|
||||
`\`\`\`python\nCODEOWNERS = ["@${prAuthor}"]\n\`\`\`\n\n` +
|
||||
`And run \`script/build_codeowners.py\``;
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
// Handle reviews
|
||||
async function handleReviews(github, context, finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, MAX_LABELS, TOO_BIG_THRESHOLD) {
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo, prFiles, totalAdditions, totalDeletions, prAuthor, MAX_LABELS, TOO_BIG_THRESHOLD);
|
||||
const hasReviewableLabels = finalLabels.some(label =>
|
||||
['too-big', 'needs-codeowners', 'deprecated-component'].includes(label)
|
||||
);
|
||||
|
||||
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
|
||||
const botReviews = reviews.filter(review =>
|
||||
review.user.type === 'Bot' &&
|
||||
review.state === 'CHANGES_REQUESTED' &&
|
||||
review.body && review.body.includes(BOT_COMMENT_MARKER)
|
||||
);
|
||||
|
||||
if (hasReviewableLabels) {
|
||||
const reviewBody = `${BOT_COMMENT_MARKER}\n\n${reviewMessages.join('\n\n---\n\n')}`;
|
||||
|
||||
if (botReviews.length > 0) {
|
||||
// Update existing review
|
||||
await github.rest.pulls.updateReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
review_id: botReviews[0].id,
|
||||
body: reviewBody
|
||||
});
|
||||
console.log('Updated existing bot review');
|
||||
} else {
|
||||
// Create new review
|
||||
await github.rest.pulls.createReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
body: reviewBody,
|
||||
event: 'REQUEST_CHANGES'
|
||||
});
|
||||
console.log('Created new bot review');
|
||||
}
|
||||
} else if (botReviews.length > 0) {
|
||||
// Dismiss existing reviews
|
||||
for (const review of botReviews) {
|
||||
try {
|
||||
await github.rest.pulls.dismissReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
review_id: review.id,
|
||||
message: 'Review dismissed: All requirements have been met'
|
||||
});
|
||||
console.log(`Dismissed bot review ${review.id}`);
|
||||
} catch (error) {
|
||||
console.log(`Failed to dismiss review ${review.id}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
handleReviews
|
||||
};
|
||||
632
.github/workflows/auto-label-pr.yml
vendored
632
.github/workflows/auto-label-pr.yml
vendored
@@ -36,633 +36,5 @@ jobs:
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
|
||||
// Constants
|
||||
const SMALL_PR_THRESHOLD = parseInt('${{ env.SMALL_PR_THRESHOLD }}');
|
||||
const MAX_LABELS = parseInt('${{ env.MAX_LABELS }}');
|
||||
const TOO_BIG_THRESHOLD = parseInt('${{ env.TOO_BIG_THRESHOLD }}');
|
||||
const COMPONENT_LABEL_THRESHOLD = parseInt('${{ env.COMPONENT_LABEL_THRESHOLD }}');
|
||||
const BOT_COMMENT_MARKER = '<!-- auto-label-pr-bot -->';
|
||||
const CODEOWNERS_MARKER = '<!-- codeowners-request -->';
|
||||
const TOO_BIG_MARKER = '<!-- too-big-request -->';
|
||||
|
||||
const MANAGED_LABELS = [
|
||||
'new-component',
|
||||
'new-platform',
|
||||
'new-target-platform',
|
||||
'merging-to-release',
|
||||
'merging-to-beta',
|
||||
'chained-pr',
|
||||
'core',
|
||||
'small-pr',
|
||||
'dashboard',
|
||||
'github-actions',
|
||||
'by-code-owner',
|
||||
'has-tests',
|
||||
'needs-tests',
|
||||
'needs-docs',
|
||||
'needs-codeowners',
|
||||
'too-big',
|
||||
'labeller-recheck',
|
||||
'bugfix',
|
||||
'new-feature',
|
||||
'breaking-change',
|
||||
'developer-breaking-change',
|
||||
'code-quality'
|
||||
];
|
||||
|
||||
const DOCS_PR_PATTERNS = [
|
||||
/https:\/\/github\.com\/esphome\/esphome-docs\/pull\/\d+/,
|
||||
/esphome\/esphome-docs#\d+/
|
||||
];
|
||||
|
||||
// Global state
|
||||
const { owner, repo } = context.repo;
|
||||
const pr_number = context.issue.number;
|
||||
|
||||
// Get current labels and PR data
|
||||
const { data: currentLabelsData } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number
|
||||
});
|
||||
const currentLabels = currentLabelsData.map(label => label.name);
|
||||
const managedLabels = currentLabels.filter(label =>
|
||||
label.startsWith('component: ') || MANAGED_LABELS.includes(label)
|
||||
);
|
||||
|
||||
// Check for mega-PR early - if present, skip most automatic labeling
|
||||
const isMegaPR = currentLabels.includes('mega-pr');
|
||||
|
||||
// Get all PR files with automatic pagination
|
||||
const prFiles = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
}
|
||||
);
|
||||
|
||||
// Calculate data from PR files
|
||||
const changedFiles = prFiles.map(file => file.filename);
|
||||
const totalAdditions = prFiles.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||
const totalDeletions = prFiles.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||
const totalChanges = totalAdditions + totalDeletions;
|
||||
|
||||
console.log('Current labels:', currentLabels.join(', '));
|
||||
console.log('Changed files:', changedFiles.length);
|
||||
console.log('Total changes:', totalChanges);
|
||||
if (isMegaPR) {
|
||||
console.log('Mega-PR detected - applying limited labeling logic');
|
||||
}
|
||||
|
||||
// Fetch API data
|
||||
async function fetchApiData() {
|
||||
try {
|
||||
const response = await fetch('https://data.esphome.io/components.json');
|
||||
const componentsData = await response.json();
|
||||
return {
|
||||
targetPlatforms: componentsData.target_platforms || [],
|
||||
platformComponents: componentsData.platform_components || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.log('Failed to fetch components data from API:', error.message);
|
||||
return { targetPlatforms: [], platformComponents: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy: Merge branch detection
|
||||
async function detectMergeBranch() {
|
||||
const labels = new Set();
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
|
||||
if (baseRef === 'release') {
|
||||
labels.add('merging-to-release');
|
||||
} else if (baseRef === 'beta') {
|
||||
labels.add('merging-to-beta');
|
||||
} else if (baseRef !== 'dev') {
|
||||
labels.add('chained-pr');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Component and platform labeling
|
||||
async function detectComponentPlatforms(apiData) {
|
||||
const labels = new Set();
|
||||
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
|
||||
|
||||
for (const file of changedFiles) {
|
||||
const componentMatch = file.match(componentRegex);
|
||||
if (componentMatch) {
|
||||
labels.add(`component: ${componentMatch[1]}`);
|
||||
}
|
||||
|
||||
const platformMatch = file.match(targetPlatformRegex);
|
||||
if (platformMatch) {
|
||||
labels.add(`platform: ${platformMatch[1]}`);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: New component detection
|
||||
async function detectNewComponents() {
|
||||
const labels = new Set();
|
||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||
|
||||
for (const file of addedFiles) {
|
||||
const componentMatch = file.match(/^esphome\/components\/([^\/]+)\/__init__\.py$/);
|
||||
if (componentMatch) {
|
||||
try {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
if (content.includes('IS_TARGET_PLATFORM = True')) {
|
||||
labels.add('new-target-platform');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(`Failed to read content of ${file}:`, error.message);
|
||||
}
|
||||
labels.add('new-component');
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: New platform detection
|
||||
async function detectNewPlatforms(apiData) {
|
||||
const labels = new Set();
|
||||
const addedFiles = prFiles.filter(file => file.status === 'added').map(file => file.filename);
|
||||
|
||||
for (const file of addedFiles) {
|
||||
const platformFileMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\.py$/);
|
||||
if (platformFileMatch) {
|
||||
const [, component, platform] = platformFileMatch;
|
||||
if (apiData.platformComponents.includes(platform)) {
|
||||
labels.add('new-platform');
|
||||
}
|
||||
}
|
||||
|
||||
const platformDirMatch = file.match(/^esphome\/components\/([^\/]+)\/([^\/]+)\/__init__\.py$/);
|
||||
if (platformDirMatch) {
|
||||
const [, component, platform] = platformDirMatch;
|
||||
if (apiData.platformComponents.includes(platform)) {
|
||||
labels.add('new-platform');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Core files detection
|
||||
async function detectCoreChanges() {
|
||||
const labels = new Set();
|
||||
const coreFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
|
||||
if (coreFiles.length > 0) {
|
||||
labels.add('core');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: PR size detection
|
||||
async function detectPRSize() {
|
||||
const labels = new Set();
|
||||
|
||||
if (totalChanges <= SMALL_PR_THRESHOLD) {
|
||||
labels.add('small-pr');
|
||||
return labels;
|
||||
}
|
||||
|
||||
const testAdditions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||
const testDeletions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||
|
||||
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||
|
||||
// Don't add too-big if mega-pr label is already present
|
||||
if (nonTestChanges > TOO_BIG_THRESHOLD && !isMegaPR) {
|
||||
labels.add('too-big');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Dashboard changes
|
||||
async function detectDashboardChanges() {
|
||||
const labels = new Set();
|
||||
const dashboardFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
|
||||
if (dashboardFiles.length > 0) {
|
||||
labels.add('dashboard');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: GitHub Actions changes
|
||||
async function detectGitHubActionsChanges() {
|
||||
const labels = new Set();
|
||||
const githubActionsFiles = changedFiles.filter(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
|
||||
if (githubActionsFiles.length > 0) {
|
||||
labels.add('github-actions');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Code owner detection
|
||||
async function detectCodeOwner() {
|
||||
const labels = new Set();
|
||||
|
||||
try {
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS',
|
||||
});
|
||||
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
const codeownersRegexes = codeownersLines.map(line => {
|
||||
const parts = line.split(/\s+/);
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
let regex;
|
||||
if (pattern.endsWith('*')) {
|
||||
const dir = pattern.slice(0, -1);
|
||||
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
} else if (pattern.includes('*')) {
|
||||
// First escape all regex special chars except *, then replace * with .*
|
||||
const regexPattern = pattern
|
||||
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*');
|
||||
regex = new RegExp(`^${regexPattern}$`);
|
||||
} else {
|
||||
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
}
|
||||
|
||||
return { regex, owners };
|
||||
});
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { regex, owners } of codeownersRegexes) {
|
||||
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
|
||||
labels.add('by-code-owner');
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to read or parse CODEOWNERS file:', error.message);
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Test detection
|
||||
async function detectTests() {
|
||||
const labels = new Set();
|
||||
const testFiles = changedFiles.filter(file => file.startsWith('tests/'));
|
||||
|
||||
if (testFiles.length > 0) {
|
||||
labels.add('has-tests');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: PR Template Checkbox detection
|
||||
async function detectPRTemplateCheckboxes() {
|
||||
const labels = new Set();
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
|
||||
console.log('Checking PR template checkboxes...');
|
||||
|
||||
// Check for checked checkboxes in the "Types of changes" section
|
||||
const checkboxPatterns = [
|
||||
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
|
||||
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
||||
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
||||
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
|
||||
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||
];
|
||||
|
||||
for (const { pattern, label } of checkboxPatterns) {
|
||||
if (pattern.test(prBody)) {
|
||||
console.log(`Found checked checkbox for: ${label}`);
|
||||
labels.add(label);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Requirements detection
|
||||
async function detectRequirements(allLabels) {
|
||||
const labels = new Set();
|
||||
|
||||
// Check for missing tests
|
||||
if ((allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) && !allLabels.has('has-tests')) {
|
||||
labels.add('needs-tests');
|
||||
}
|
||||
|
||||
// Check for missing docs
|
||||
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
|
||||
|
||||
if (!hasDocsLink) {
|
||||
labels.add('needs-docs');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for missing CODEOWNERS
|
||||
if (allLabels.has('new-component')) {
|
||||
const codeownersModified = prFiles.some(file =>
|
||||
file.filename === 'CODEOWNERS' &&
|
||||
(file.status === 'modified' || file.status === 'added') &&
|
||||
(file.additions || 0) > 0
|
||||
);
|
||||
|
||||
if (!codeownersModified) {
|
||||
labels.add('needs-codeowners');
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Generate review messages
|
||||
function generateReviewMessages(finalLabels, originalLabelCount) {
|
||||
const messages = [];
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
// Too big message
|
||||
if (finalLabels.includes('too-big')) {
|
||||
const testAdditions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||
const testDeletions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.deletions || 0), 0);
|
||||
const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
|
||||
|
||||
const tooManyLabels = originalLabelCount > MAX_LABELS;
|
||||
const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
|
||||
|
||||
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
|
||||
|
||||
if (tooManyLabels && tooManyChanges) {
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
|
||||
} else if (tooManyLabels) {
|
||||
message += `This PR affects ${originalLabelCount} different components/areas.`;
|
||||
} else {
|
||||
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
|
||||
}
|
||||
|
||||
message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`;
|
||||
message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`;
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
// CODEOWNERS message
|
||||
if (finalLabels.includes('needs-codeowners')) {
|
||||
const message = `${CODEOWNERS_MARKER}\n### 👥 Code Ownership\n\n` +
|
||||
`Hey there @${prAuthor},\n` +
|
||||
`Thanks for submitting this pull request! Can you add yourself as a codeowner for this integration? ` +
|
||||
`This way we can notify you if a bug report for this integration is reported.\n\n` +
|
||||
`In \`__init__.py\` of the integration, please add:\n\n` +
|
||||
`\`\`\`python\nCODEOWNERS = ["@${prAuthor}"]\n\`\`\`\n\n` +
|
||||
`And run \`script/build_codeowners.py\``;
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
// Handle reviews
|
||||
async function handleReviews(finalLabels, originalLabelCount) {
|
||||
const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
|
||||
const hasReviewableLabels = finalLabels.some(label =>
|
||||
['too-big', 'needs-codeowners'].includes(label)
|
||||
);
|
||||
|
||||
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
|
||||
const botReviews = reviews.filter(review =>
|
||||
review.user.type === 'Bot' &&
|
||||
review.state === 'CHANGES_REQUESTED' &&
|
||||
review.body && review.body.includes(BOT_COMMENT_MARKER)
|
||||
);
|
||||
|
||||
if (hasReviewableLabels) {
|
||||
const reviewBody = `${BOT_COMMENT_MARKER}\n\n${reviewMessages.join('\n\n---\n\n')}`;
|
||||
|
||||
if (botReviews.length > 0) {
|
||||
// Update existing review
|
||||
await github.rest.pulls.updateReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
review_id: botReviews[0].id,
|
||||
body: reviewBody
|
||||
});
|
||||
console.log('Updated existing bot review');
|
||||
} else {
|
||||
// Create new review
|
||||
await github.rest.pulls.createReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
body: reviewBody,
|
||||
event: 'REQUEST_CHANGES'
|
||||
});
|
||||
console.log('Created new bot review');
|
||||
}
|
||||
} else if (botReviews.length > 0) {
|
||||
// Dismiss existing reviews
|
||||
for (const review of botReviews) {
|
||||
try {
|
||||
await github.rest.pulls.dismissReview({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number,
|
||||
review_id: review.id,
|
||||
message: 'Review dismissed: All requirements have been met'
|
||||
});
|
||||
console.log(`Dismissed bot review ${review.id}`);
|
||||
} catch (error) {
|
||||
console.log(`Failed to dismiss review ${review.id}:`, error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main execution
|
||||
const apiData = await fetchApiData();
|
||||
const baseRef = context.payload.pull_request.base.ref;
|
||||
|
||||
// Early exit for release and beta branches only
|
||||
if (baseRef === 'release' || baseRef === 'beta') {
|
||||
const branchLabels = await detectMergeBranch();
|
||||
const finalLabels = Array.from(branchLabels);
|
||||
|
||||
console.log('Computed labels (merge branch only):', finalLabels.join(', '));
|
||||
|
||||
// Apply labels
|
||||
if (finalLabels.length > 0) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
labels: finalLabels
|
||||
});
|
||||
}
|
||||
|
||||
// Remove old managed labels
|
||||
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
|
||||
for (const label of labelsToRemove) {
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
name: label
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Failed to remove label ${label}:`, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Run all strategies
|
||||
const [
|
||||
branchLabels,
|
||||
componentLabels,
|
||||
newComponentLabels,
|
||||
newPlatformLabels,
|
||||
coreLabels,
|
||||
sizeLabels,
|
||||
dashboardLabels,
|
||||
actionsLabels,
|
||||
codeOwnerLabels,
|
||||
testLabels,
|
||||
checkboxLabels
|
||||
] = await Promise.all([
|
||||
detectMergeBranch(),
|
||||
detectComponentPlatforms(apiData),
|
||||
detectNewComponents(),
|
||||
detectNewPlatforms(apiData),
|
||||
detectCoreChanges(),
|
||||
detectPRSize(),
|
||||
detectDashboardChanges(),
|
||||
detectGitHubActionsChanges(),
|
||||
detectCodeOwner(),
|
||||
detectTests(),
|
||||
detectPRTemplateCheckboxes()
|
||||
]);
|
||||
|
||||
// Combine all labels
|
||||
const allLabels = new Set([
|
||||
...branchLabels,
|
||||
...componentLabels,
|
||||
...newComponentLabels,
|
||||
...newPlatformLabels,
|
||||
...coreLabels,
|
||||
...sizeLabels,
|
||||
...dashboardLabels,
|
||||
...actionsLabels,
|
||||
...codeOwnerLabels,
|
||||
...testLabels,
|
||||
...checkboxLabels
|
||||
]);
|
||||
|
||||
// Detect requirements based on all other labels
|
||||
const requirementLabels = await detectRequirements(allLabels);
|
||||
for (const label of requirementLabels) {
|
||||
allLabels.add(label);
|
||||
}
|
||||
|
||||
let finalLabels = Array.from(allLabels);
|
||||
|
||||
// For mega-PRs, exclude component labels if there are too many
|
||||
if (isMegaPR) {
|
||||
const componentLabels = finalLabels.filter(label => label.startsWith('component: '));
|
||||
if (componentLabels.length > COMPONENT_LABEL_THRESHOLD) {
|
||||
finalLabels = finalLabels.filter(label => !label.startsWith('component: '));
|
||||
console.log(`Mega-PR detected - excluding ${componentLabels.length} component labels (threshold: ${COMPONENT_LABEL_THRESHOLD})`);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle too many labels (only for non-mega PRs)
|
||||
const tooManyLabels = finalLabels.length > MAX_LABELS;
|
||||
const originalLabelCount = finalLabels.length;
|
||||
|
||||
if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
|
||||
finalLabels = ['too-big'];
|
||||
}
|
||||
|
||||
console.log('Computed labels:', finalLabels.join(', '));
|
||||
|
||||
// Handle reviews
|
||||
await handleReviews(finalLabels, originalLabelCount);
|
||||
|
||||
// Apply labels
|
||||
if (finalLabels.length > 0) {
|
||||
console.log(`Adding labels: ${finalLabels.join(', ')}`);
|
||||
await github.rest.issues.addLabels({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
labels: finalLabels
|
||||
});
|
||||
}
|
||||
|
||||
// Remove old managed labels
|
||||
const labelsToRemove = managedLabels.filter(label => !finalLabels.includes(label));
|
||||
for (const label of labelsToRemove) {
|
||||
console.log(`Removing label: ${label}`);
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner,
|
||||
repo,
|
||||
issue_number: pr_number,
|
||||
name: label
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`Failed to remove label ${label}:`, error.message);
|
||||
}
|
||||
}
|
||||
const script = require('./.github/scripts/auto-label-pr/index.js');
|
||||
await script({ github, context });
|
||||
|
||||
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -157,7 +157,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -193,7 +193,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -223,7 +223,7 @@ jobs:
|
||||
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -245,7 +245,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -334,14 +334,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -413,14 +413,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -502,14 +502,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -735,7 +735,7 @@ jobs:
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -759,7 +759,7 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -800,7 +800,7 @@ jobs:
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -847,7 +847,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
8
.github/workflows/release.yml
vendored
8
.github/workflows/release.yml
vendored
@@ -102,12 +102,12 @@ jobs:
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -182,13 +182,13 @@ jobs:
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: matrix.registry == 'ghcr'
|
||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.13
|
||||
rev: v0.15.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -88,7 +88,8 @@ esphome/components/bmp3xx/* @latonita
|
||||
esphome/components/bmp3xx_base/* @latonita @martgras
|
||||
esphome/components/bmp3xx_i2c/* @latonita
|
||||
esphome/components/bmp3xx_spi/* @latonita
|
||||
esphome/components/bmp581/* @kahrendt
|
||||
esphome/components/bmp581_base/* @danielkent-net @kahrendt
|
||||
esphome/components/bmp581_i2c/* @danielkent-net @kahrendt
|
||||
esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
@@ -103,6 +104,7 @@ esphome/components/cc1101/* @gabest11 @lygris
|
||||
esphome/components/ccs811/* @habbie
|
||||
esphome/components/cd74hc4067/* @asoehlke
|
||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||
esphome/components/ch423/* @dwmw2
|
||||
esphome/components/chsc6x/* @kkosik20
|
||||
esphome/components/climate/* @esphome/core
|
||||
esphome/components/climate_ir/* @glmnet
|
||||
@@ -132,6 +134,7 @@ esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||
esphome/components/dht/* @OttoWinter
|
||||
esphome/components/display_menu_base/* @numo68
|
||||
esphome/components/dlms_meter/* @SimonFischer04
|
||||
esphome/components/dps310/* @kbx81
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/ds2484/* @mrk-its
|
||||
@@ -481,6 +484,7 @@ esphome/components/switch/* @esphome/core
|
||||
esphome/components/switch/binary_sensor/* @ssieb
|
||||
esphome/components/sx126x/* @swoboda1337
|
||||
esphome/components/sx127x/* @swoboda1337
|
||||
esphome/components/sy6970/* @linkedupbits
|
||||
esphome/components/syslog/* @clydebarrow
|
||||
esphome/components/t6615/* @tylermenezes
|
||||
esphome/components/tc74/* @sethgirvan
|
||||
@@ -528,7 +532,7 @@ esphome/components/uart/packet_transport/* @clydebarrow
|
||||
esphome/components/udp/* @clydebarrow
|
||||
esphome/components/ufire_ec/* @pvizeli
|
||||
esphome/components/ufire_ise/* @pvizeli
|
||||
esphome/components/ultrasonic/* @OttoWinter
|
||||
esphome/components/ultrasonic/* @ssieb @swoboda1337
|
||||
esphome/components/update/* @jesserockz
|
||||
esphome/components/uponor_smatrix/* @kroimon
|
||||
esphome/components/usb_cdc_acm/* @kbx81
|
||||
|
||||
@@ -294,8 +294,13 @@ def has_api() -> bool:
|
||||
|
||||
|
||||
def has_ota() -> bool:
|
||||
"""Check if OTA is available."""
|
||||
return CONF_OTA in CORE.config
|
||||
"""Check if OTA upload is available (requires platform: esphome)."""
|
||||
if CONF_OTA not in CORE.config:
|
||||
return False
|
||||
return any(
|
||||
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
|
||||
for ota_item in CORE.config[CONF_OTA]
|
||||
)
|
||||
|
||||
|
||||
def has_mqtt_ip_lookup() -> bool:
|
||||
|
||||
@@ -12,7 +12,6 @@ from .const import (
|
||||
CORE_SUBCATEGORY_PATTERNS,
|
||||
DEMANGLED_PATTERNS,
|
||||
ESPHOME_COMPONENT_PATTERN,
|
||||
SECTION_TO_ATTR,
|
||||
SYMBOL_PATTERNS,
|
||||
)
|
||||
from .demangle import batch_demangle
|
||||
@@ -44,6 +43,7 @@ _READELF_SECTION_PATTERN = re.compile(
|
||||
# Component category prefixes
|
||||
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
|
||||
_COMPONENT_PREFIX_EXTERNAL = "[external]"
|
||||
_COMPONENT_PREFIX_LIB = "[lib]"
|
||||
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
|
||||
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
|
||||
|
||||
@@ -57,6 +57,16 @@ SymbolInfoType = tuple[str, int, str]
|
||||
# RAM sections - symbols in these sections consume RAM
|
||||
RAM_SECTIONS = frozenset([".data", ".bss"])
|
||||
|
||||
# nm symbol types for global/weak defined symbols (used for library symbol mapping)
|
||||
# Only global (uppercase) and weak symbols are safe to use - local symbols (lowercase)
|
||||
# can have name collisions across compilation units
|
||||
_NM_DEFINED_GLOBAL_TYPES = frozenset({"T", "D", "B", "R", "W", "V"})
|
||||
|
||||
# Pattern matching compiler-generated local names that can collide across compilation
|
||||
# units (e.g., packet$19, buf$20, flag$5261). These are unsafe for name-based lookup.
|
||||
# Does NOT match mangled C++ names with optimization suffixes (e.g., func$isra$0).
|
||||
_COMPILER_LOCAL_PATTERN = re.compile(r"^[a-zA-Z_]\w*\$\d+$")
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemorySection:
|
||||
@@ -91,6 +101,17 @@ class ComponentMemory:
|
||||
bss_size: int = 0 # Uninitialized data (ram only)
|
||||
symbol_count: int = 0
|
||||
|
||||
def add_section_size(self, section_name: str, size: int) -> None:
|
||||
"""Add size to the appropriate attribute for a section."""
|
||||
if section_name == ".text":
|
||||
self.text_size += size
|
||||
elif section_name == ".rodata":
|
||||
self.rodata_size += size
|
||||
elif section_name == ".data":
|
||||
self.data_size += size
|
||||
elif section_name == ".bss":
|
||||
self.bss_size += size
|
||||
|
||||
@property
|
||||
def flash_total(self) -> int:
|
||||
"""Total flash usage (text + rodata + data)."""
|
||||
@@ -167,12 +188,23 @@ class MemoryAnalyzer:
|
||||
self._elf_symbol_names: set[str] = set()
|
||||
# SDK symbols not in ELF (static/local symbols from closed-source libs)
|
||||
self._sdk_symbols: list[SDKSymbol] = []
|
||||
# CSWTCH symbols: list of (name, size, source_file, component)
|
||||
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
|
||||
# Library symbol mapping: symbol_name -> library_name
|
||||
self._lib_symbol_map: dict[str, str] = {}
|
||||
# Library dir to name mapping: "lib641" -> "espsoftwareserial",
|
||||
# "espressif__mdns" -> "mdns"
|
||||
self._lib_hash_to_name: dict[str, str] = {}
|
||||
# Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns"
|
||||
self._heuristic_to_lib: dict[str, str] = {}
|
||||
|
||||
def analyze(self) -> dict[str, ComponentMemory]:
|
||||
"""Analyze the ELF file and return component memory usage."""
|
||||
self._parse_sections()
|
||||
self._parse_symbols()
|
||||
self._scan_libraries()
|
||||
self._categorize_symbols()
|
||||
self._analyze_cswtch_symbols()
|
||||
self._analyze_sdk_libraries()
|
||||
return dict(self.components)
|
||||
|
||||
@@ -255,8 +287,7 @@ class MemoryAnalyzer:
|
||||
comp_mem.symbol_count += 1
|
||||
|
||||
# Update the appropriate size attribute based on section
|
||||
if attr_name := SECTION_TO_ATTR.get(section_name):
|
||||
setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size)
|
||||
comp_mem.add_section_size(section_name, size)
|
||||
|
||||
# Track uncategorized symbols
|
||||
if component == "other" and size > 0:
|
||||
@@ -316,15 +347,19 @@ class MemoryAnalyzer:
|
||||
# If no component match found, it's core
|
||||
return _COMPONENT_CORE
|
||||
|
||||
# Check library symbol map (more accurate than heuristic patterns)
|
||||
if lib_name := self._lib_symbol_map.get(symbol_name):
|
||||
return f"{_COMPONENT_PREFIX_LIB}{lib_name}"
|
||||
|
||||
# Check against symbol patterns
|
||||
for component, patterns in SYMBOL_PATTERNS.items():
|
||||
if any(pattern in symbol_name for pattern in patterns):
|
||||
return component
|
||||
return self._heuristic_to_lib.get(component, component)
|
||||
|
||||
# Check against demangled patterns
|
||||
for component, patterns in DEMANGLED_PATTERNS.items():
|
||||
if any(pattern in demangled for pattern in patterns):
|
||||
return component
|
||||
return self._heuristic_to_lib.get(component, component)
|
||||
|
||||
# Special cases that need more complex logic
|
||||
|
||||
@@ -372,6 +407,610 @@ class MemoryAnalyzer:
|
||||
|
||||
return "Other Core"
|
||||
|
||||
def _discover_pio_libraries(
|
||||
self,
|
||||
libraries: dict[str, list[Path]],
|
||||
hash_to_name: dict[str, str],
|
||||
) -> None:
|
||||
"""Discover PlatformIO third-party libraries from the build directory.
|
||||
|
||||
Scans ``lib<hex>/`` directories under ``.pioenvs/<env>/`` to find
|
||||
library names and their ``.a`` archive or ``.o`` file paths.
|
||||
|
||||
Args:
|
||||
libraries: Dict to populate with library name -> file path list mappings.
|
||||
Prefers ``.a`` archives when available, falls back to ``.o`` files
|
||||
(e.g., pioarduino ESP32 Arduino builds only produce ``.o`` files).
|
||||
hash_to_name: Dict to populate with dir name -> library name mappings
|
||||
for CSWTCH attribution (e.g., ``lib641`` -> ``espsoftwareserial``).
|
||||
"""
|
||||
build_dir = self.elf_path.parent
|
||||
|
||||
for entry in build_dir.iterdir():
|
||||
if not entry.is_dir() or not entry.name.startswith("lib"):
|
||||
continue
|
||||
# Validate that the suffix after "lib" is a hex hash
|
||||
hex_part = entry.name[3:]
|
||||
if not hex_part:
|
||||
continue
|
||||
try:
|
||||
int(hex_part, 16)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Each lib<hex>/ directory contains a subdirectory named after the library
|
||||
for lib_subdir in entry.iterdir():
|
||||
if not lib_subdir.is_dir():
|
||||
continue
|
||||
lib_name = lib_subdir.name.lower()
|
||||
|
||||
# Prefer .a archive (lib<LibraryName>.a), fall back to .o files
|
||||
# e.g., lib72a/ESPAsyncTCP/... has lib72a/libESPAsyncTCP.a
|
||||
archive = entry / f"lib{lib_subdir.name}.a"
|
||||
if archive.exists():
|
||||
file_paths = [archive]
|
||||
elif archives := list(entry.glob("*.a")):
|
||||
# Case-insensitive fallback
|
||||
file_paths = [archives[0]]
|
||||
else:
|
||||
# No .a archive (e.g., pioarduino CMake builds) - use .o files
|
||||
file_paths = sorted(lib_subdir.rglob("*.o"))
|
||||
|
||||
if file_paths:
|
||||
libraries[lib_name] = file_paths
|
||||
hash_to_name[entry.name] = lib_name
|
||||
_LOGGER.debug(
|
||||
"Discovered PlatformIO library: %s -> %s",
|
||||
lib_subdir.name,
|
||||
file_paths[0],
|
||||
)
|
||||
|
||||
def _discover_idf_managed_components(
|
||||
self,
|
||||
libraries: dict[str, list[Path]],
|
||||
hash_to_name: dict[str, str],
|
||||
) -> None:
|
||||
"""Discover ESP-IDF managed component libraries from the build directory.
|
||||
|
||||
ESP-IDF managed components (from the IDF component registry) use a
|
||||
``<vendor>__<name>`` naming convention. Source files live under
|
||||
``managed_components/<vendor>__<name>/`` and the compiled archives are at
|
||||
``esp-idf/<vendor>__<name>/lib<vendor>__<name>.a``.
|
||||
|
||||
Args:
|
||||
libraries: Dict to populate with library name -> file path list mappings.
|
||||
hash_to_name: Dict to populate with dir name -> library name mappings
|
||||
for CSWTCH attribution (e.g., ``espressif__mdns`` -> ``mdns``).
|
||||
"""
|
||||
build_dir = self.elf_path.parent
|
||||
|
||||
managed_dir = build_dir / "managed_components"
|
||||
if not managed_dir.is_dir():
|
||||
return
|
||||
|
||||
espidf_dir = build_dir / "esp-idf"
|
||||
|
||||
for entry in managed_dir.iterdir():
|
||||
if not entry.is_dir() or "__" not in entry.name:
|
||||
continue
|
||||
|
||||
# Extract the short name: espressif__mdns -> mdns
|
||||
full_name = entry.name # e.g., espressif__mdns
|
||||
short_name = full_name.split("__", 1)[1].lower()
|
||||
|
||||
# Find the .a archive under esp-idf/<vendor>__<name>/
|
||||
archive = espidf_dir / full_name / f"lib{full_name}.a"
|
||||
if archive.exists():
|
||||
libraries[short_name] = [archive]
|
||||
hash_to_name[full_name] = short_name
|
||||
_LOGGER.debug(
|
||||
"Discovered IDF managed component: %s -> %s",
|
||||
short_name,
|
||||
archive,
|
||||
)
|
||||
|
||||
def _build_library_symbol_map(
|
||||
self, libraries: dict[str, list[Path]]
|
||||
) -> dict[str, str]:
|
||||
"""Build a symbol-to-library mapping from library archives or object files.
|
||||
|
||||
Runs ``nm --defined-only`` on each ``.a`` or ``.o`` file to collect
|
||||
global and weak defined symbols.
|
||||
|
||||
Args:
|
||||
libraries: Dictionary mapping library name to list of file paths
|
||||
(``.a`` archives or ``.o`` object files).
|
||||
|
||||
Returns:
|
||||
Dictionary mapping symbol name to library name.
|
||||
"""
|
||||
symbol_map: dict[str, str] = {}
|
||||
|
||||
if not self.nm_path:
|
||||
return symbol_map
|
||||
|
||||
for lib_name, file_paths in libraries.items():
|
||||
result = run_tool(
|
||||
[self.nm_path, "--defined-only", *(str(p) for p in file_paths)],
|
||||
timeout=10,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
continue
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
|
||||
sym_type = parts[-2]
|
||||
sym_name = parts[-1]
|
||||
|
||||
# Include global defined symbols (uppercase) and weak symbols (W/V)
|
||||
if sym_type in _NM_DEFINED_GLOBAL_TYPES:
|
||||
symbol_map[sym_name] = lib_name
|
||||
|
||||
return symbol_map
|
||||
|
||||
@staticmethod
|
||||
def _build_heuristic_to_lib_mapping(
|
||||
library_names: set[str],
|
||||
) -> dict[str, str]:
|
||||
"""Build mapping from heuristic pattern categories to discovered libraries.
|
||||
|
||||
Heuristic categories like ``mdns_lib``, ``web_server_lib``, ``async_tcp``
|
||||
exist as approximations for library attribution. When we discover the
|
||||
actual library, symbols matching those heuristics should be redirected
|
||||
to the ``[lib]`` category instead.
|
||||
|
||||
The mapping is built by checking if the normalized category name
|
||||
(stripped of ``_lib`` suffix and underscores) appears as a substring
|
||||
of any discovered library name.
|
||||
|
||||
Examples::
|
||||
|
||||
mdns_lib -> mdns -> in "mdns" or "esp8266mdns" -> [lib]mdns
|
||||
web_server_lib -> webserver -> in "espasyncwebserver" -> [lib]espasyncwebserver
|
||||
async_tcp -> asynctcp -> in "espasynctcp" -> [lib]espasynctcp
|
||||
|
||||
Args:
|
||||
library_names: Set of discovered library names (lowercase).
|
||||
|
||||
Returns:
|
||||
Dictionary mapping heuristic category to ``[lib]<name>`` string.
|
||||
"""
|
||||
mapping: dict[str, str] = {}
|
||||
all_categories = set(SYMBOL_PATTERNS) | set(DEMANGLED_PATTERNS)
|
||||
|
||||
for category in all_categories:
|
||||
base = category.removesuffix("_lib").replace("_", "")
|
||||
# Collect all libraries whose name contains the base string
|
||||
candidates = [lib_name for lib_name in library_names if base in lib_name]
|
||||
if not candidates:
|
||||
continue
|
||||
|
||||
# Choose a deterministic "best" match:
|
||||
# 1. Prefer exact name matches over substring matches.
|
||||
# 2. Among non-exact matches, prefer the shortest library name.
|
||||
# 3. Break remaining ties lexicographically.
|
||||
best_lib = min(
|
||||
candidates,
|
||||
key=lambda lib_name, _base=base: (
|
||||
lib_name != _base,
|
||||
len(lib_name),
|
||||
lib_name,
|
||||
),
|
||||
)
|
||||
mapping[category] = f"{_COMPONENT_PREFIX_LIB}{best_lib}"
|
||||
|
||||
if mapping:
|
||||
_LOGGER.debug(
|
||||
"Heuristic-to-library redirects: %s",
|
||||
", ".join(f"{k} -> {v}" for k, v in sorted(mapping.items())),
|
||||
)
|
||||
|
||||
return mapping
|
||||
|
||||
def _parse_map_file(self) -> dict[str, str] | None:
|
||||
"""Parse linker map file to build authoritative symbol-to-library mapping.
|
||||
|
||||
The linker map file contains the definitive source attribution for every
|
||||
symbol, including local/static ones that ``nm`` cannot safely export.
|
||||
|
||||
Map file format (GNU ld)::
|
||||
|
||||
.text._mdns_service_task
|
||||
0x400e9fdc 0x65c .pioenvs/env/esp-idf/espressif__mdns/libespressif__mdns.a(mdns.c.o)
|
||||
|
||||
Each section entry has a ``.section.symbol_name`` line followed by an
|
||||
indented line with address, size, and source path.
|
||||
|
||||
Returns:
|
||||
Symbol-to-library dict, or ``None`` if no usable map file exists.
|
||||
"""
|
||||
map_path = self.elf_path.with_suffix(".map")
|
||||
if not map_path.exists() or map_path.stat().st_size < 10000:
|
||||
return None
|
||||
|
||||
_LOGGER.info("Parsing linker map file: %s", map_path.name)
|
||||
|
||||
try:
|
||||
map_text = map_path.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError as err:
|
||||
_LOGGER.warning("Failed to read map file: %s", err)
|
||||
return None
|
||||
|
||||
symbol_map: dict[str, str] = {}
|
||||
current_symbol: str | None = None
|
||||
section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.")
|
||||
|
||||
for line in map_text.splitlines():
|
||||
# Match section.symbol line: " .text.symbol_name"
|
||||
# Single space indent, starts with dot
|
||||
if len(line) > 2 and line[0] == " " and line[1] == ".":
|
||||
stripped = line.strip()
|
||||
for prefix in section_prefixes:
|
||||
if stripped.startswith(prefix):
|
||||
current_symbol = stripped[len(prefix) :]
|
||||
break
|
||||
else:
|
||||
current_symbol = None
|
||||
continue
|
||||
|
||||
# Match source attribution line: " 0xADDR 0xSIZE source_path"
|
||||
if current_symbol is None:
|
||||
continue
|
||||
|
||||
fields = line.split()
|
||||
# Skip compiler-generated local names (e.g., packet$19, buf$20)
|
||||
# that can collide across compilation units
|
||||
if (
|
||||
len(fields) >= 3
|
||||
and fields[0].startswith("0x")
|
||||
and fields[1].startswith("0x")
|
||||
and not _COMPILER_LOCAL_PATTERN.match(current_symbol)
|
||||
):
|
||||
source_path = fields[2]
|
||||
# Check if source path contains a known library directory
|
||||
for dir_key, lib_name in self._lib_hash_to_name.items():
|
||||
if dir_key in source_path:
|
||||
symbol_map[current_symbol] = lib_name
|
||||
break
|
||||
|
||||
current_symbol = None
|
||||
|
||||
return symbol_map or None
|
||||
|
||||
def _scan_libraries(self) -> None:
|
||||
"""Discover third-party libraries and build symbol mapping.
|
||||
|
||||
Scans both PlatformIO ``lib<hex>/`` directories (Arduino builds) and
|
||||
ESP-IDF ``managed_components/`` (IDF builds) to find library archives.
|
||||
|
||||
Uses the linker map file for authoritative symbol attribution when
|
||||
available, falling back to ``nm`` scanning with heuristic redirects.
|
||||
"""
|
||||
libraries: dict[str, list[Path]] = {}
|
||||
self._discover_pio_libraries(libraries, self._lib_hash_to_name)
|
||||
self._discover_idf_managed_components(libraries, self._lib_hash_to_name)
|
||||
|
||||
if not libraries:
|
||||
_LOGGER.debug("No third-party libraries found")
|
||||
return
|
||||
|
||||
_LOGGER.info(
|
||||
"Scanning %d libraries: %s",
|
||||
len(libraries),
|
||||
", ".join(sorted(libraries)),
|
||||
)
|
||||
|
||||
# Heuristic redirect catches local symbols (e.g., mdns_task_buffer$14)
|
||||
# that can't be safely added to the symbol map due to name collisions
|
||||
self._heuristic_to_lib = self._build_heuristic_to_lib_mapping(
|
||||
set(libraries.keys())
|
||||
)
|
||||
|
||||
# Try linker map file first (authoritative, includes local symbols)
|
||||
map_symbols = self._parse_map_file()
|
||||
if map_symbols is not None:
|
||||
self._lib_symbol_map = map_symbols
|
||||
_LOGGER.info(
|
||||
"Built library symbol map from linker map: %d symbols",
|
||||
len(self._lib_symbol_map),
|
||||
)
|
||||
return
|
||||
|
||||
# Fall back to nm scanning (global symbols only)
|
||||
self._lib_symbol_map = self._build_library_symbol_map(libraries)
|
||||
|
||||
_LOGGER.info(
|
||||
"Built library symbol map from nm: %d symbols from %d libraries",
|
||||
len(self._lib_symbol_map),
|
||||
len(libraries),
|
||||
)
|
||||
|
||||
def _find_object_files_dir(self) -> Path | None:
|
||||
"""Find the directory containing object files for this build.
|
||||
|
||||
Returns:
|
||||
Path to the directory containing .o files, or None if not found.
|
||||
"""
|
||||
# The ELF is typically at .pioenvs/<env>/firmware.elf
|
||||
# Object files are in .pioenvs/<env>/src/ and .pioenvs/<env>/lib*/
|
||||
pioenvs_dir = self.elf_path.parent
|
||||
if pioenvs_dir.exists() and any(pioenvs_dir.glob("src/*.o")):
|
||||
return pioenvs_dir
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _parse_nm_cswtch_output(
|
||||
output: str,
|
||||
base_dir: Path | None,
|
||||
cswtch_map: dict[str, list[tuple[str, int]]],
|
||||
) -> None:
|
||||
"""Parse nm output for CSWTCH symbols and add to cswtch_map.
|
||||
|
||||
Handles both ``.o`` files and ``.a`` archives.
|
||||
|
||||
nm output formats::
|
||||
|
||||
.o files: /path/file.o:hex_addr hex_size type name
|
||||
.a files: /path/lib.a:member.o:hex_addr hex_size type name
|
||||
|
||||
For ``.o`` files, paths are made relative to *base_dir* when possible.
|
||||
For ``.a`` archives (detected by ``:`` in the file portion), paths are
|
||||
formatted as ``archive_stem/member.o`` (e.g. ``liblwip2-536-feat/lwip-esp.o``).
|
||||
|
||||
Args:
|
||||
output: Raw stdout from ``nm --print-file-name -S``.
|
||||
base_dir: Base directory for computing relative paths of ``.o`` files.
|
||||
Pass ``None`` when scanning archives outside the build tree.
|
||||
cswtch_map: Dict to populate, mapping ``"CSWTCH$N:size"`` to source list.
|
||||
"""
|
||||
for line in output.splitlines():
|
||||
if "CSWTCH$" not in line:
|
||||
continue
|
||||
|
||||
# Split on last ":" that precedes a hex address.
|
||||
# For .o: "filepath.o" : "hex_addr hex_size type name"
|
||||
# For .a: "filepath.a:member.o" : "hex_addr hex_size type name"
|
||||
parts_after_colon = line.rsplit(":", 1)
|
||||
if len(parts_after_colon) != 2:
|
||||
continue
|
||||
|
||||
file_path = parts_after_colon[0]
|
||||
fields = parts_after_colon[1].split()
|
||||
# fields: [address, size, type, name]
|
||||
if len(fields) < 4:
|
||||
continue
|
||||
|
||||
sym_name = fields[3]
|
||||
if not sym_name.startswith("CSWTCH$"):
|
||||
continue
|
||||
|
||||
try:
|
||||
size = int(fields[1], 16)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
# Determine readable source path
|
||||
# Use ".a:" to detect archive format (not bare ":" which matches
|
||||
# Windows drive letters like "C:\...\file.o").
|
||||
if ".a:" in file_path:
|
||||
# Archive format: "archive.a:member.o" → "archive_stem/member.o"
|
||||
archive_part, member = file_path.rsplit(":", 1)
|
||||
archive_name = Path(archive_part).stem
|
||||
rel_path = f"{archive_name}/{member}"
|
||||
elif base_dir is not None:
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(base_dir))
|
||||
except ValueError:
|
||||
rel_path = file_path
|
||||
else:
|
||||
rel_path = file_path
|
||||
|
||||
key = f"{sym_name}:{size}"
|
||||
cswtch_map[key].append((rel_path, size))
|
||||
|
||||
def _run_nm_cswtch_scan(
|
||||
self,
|
||||
files: list[Path],
|
||||
base_dir: Path | None,
|
||||
cswtch_map: dict[str, list[tuple[str, int]]],
|
||||
) -> None:
|
||||
"""Run nm on *files* and add any CSWTCH symbols to *cswtch_map*.
|
||||
|
||||
Args:
|
||||
files: Object (``.o``) or archive (``.a``) files to scan.
|
||||
base_dir: Base directory for relative path computation (see
|
||||
:meth:`_parse_nm_cswtch_output`).
|
||||
cswtch_map: Dict to populate with results.
|
||||
"""
|
||||
if not self.nm_path or not files:
|
||||
return
|
||||
|
||||
_LOGGER.debug("Scanning %d files for CSWTCH symbols", len(files))
|
||||
|
||||
result = run_tool(
|
||||
[self.nm_path, "--print-file-name", "-S"] + [str(f) for f in files],
|
||||
timeout=30,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
_LOGGER.debug(
|
||||
"nm failed or timed out scanning %d files for CSWTCH symbols",
|
||||
len(files),
|
||||
)
|
||||
return
|
||||
|
||||
self._parse_nm_cswtch_output(result.stdout, base_dir, cswtch_map)
|
||||
|
||||
def _scan_cswtch_in_sdk_archives(
|
||||
self, cswtch_map: dict[str, list[tuple[str, int]]]
|
||||
) -> None:
|
||||
"""Scan SDK library archives (.a) for CSWTCH symbols.
|
||||
|
||||
Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source,
|
||||
so their CSWTCH symbols only exist inside ``.a`` archives. Results are
|
||||
merged into *cswtch_map* for keys not already found in ``.o`` files.
|
||||
|
||||
The same source file (e.g. ``lwip-esp.o``) often appears in multiple
|
||||
library variants (``liblwip2-536.a``, ``liblwip2-1460-feat.a``, etc.),
|
||||
so results are deduplicated by member name.
|
||||
"""
|
||||
sdk_dirs = self._find_sdk_library_dirs()
|
||||
if not sdk_dirs:
|
||||
return
|
||||
|
||||
sdk_archives = sorted(a for sdk_dir in sdk_dirs for a in sdk_dir.glob("*.a"))
|
||||
|
||||
sdk_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
self._run_nm_cswtch_scan(sdk_archives, None, sdk_map)
|
||||
|
||||
# Merge SDK results, deduplicating by member name.
|
||||
for key, sources in sdk_map.items():
|
||||
if key in cswtch_map:
|
||||
continue
|
||||
seen: dict[str, tuple[str, int]] = {}
|
||||
for path, sz in sources:
|
||||
member = Path(path).name
|
||||
if member not in seen:
|
||||
seen[member] = (path, sz)
|
||||
cswtch_map[key] = list(seen.values())
|
||||
|
||||
def _source_file_to_component(self, source_file: str) -> str:
|
||||
"""Map a source object file path to its component name.
|
||||
|
||||
Args:
|
||||
source_file: Relative path like 'src/esphome/components/wifi/wifi_component.cpp.o'
|
||||
|
||||
Returns:
|
||||
Component name like '[esphome]wifi' or the source file if unknown.
|
||||
"""
|
||||
parts = Path(source_file).parts
|
||||
|
||||
# ESPHome component: src/esphome/components/<name>/...
|
||||
if "components" in parts:
|
||||
idx = parts.index("components")
|
||||
if idx + 1 < len(parts):
|
||||
component_name = parts[idx + 1]
|
||||
if component_name in get_esphome_components():
|
||||
return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
|
||||
if component_name in self.external_components:
|
||||
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
|
||||
|
||||
# ESPHome core: src/esphome/core/... or src/esphome/...
|
||||
if "core" in parts and "esphome" in parts:
|
||||
return _COMPONENT_CORE
|
||||
if "esphome" in parts and "components" not in parts:
|
||||
return _COMPONENT_CORE
|
||||
|
||||
# Framework/library files - check for PlatformIO library hash dirs
|
||||
# e.g., lib65b/ESPAsyncTCP/... -> [lib]espasynctcp
|
||||
if parts and parts[0] in self._lib_hash_to_name:
|
||||
return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[0]]}"
|
||||
|
||||
# ESP-IDF managed components: managed_components/espressif__mdns/... -> [lib]mdns
|
||||
if (
|
||||
len(parts) >= 2
|
||||
and parts[0] == "managed_components"
|
||||
and parts[1] in self._lib_hash_to_name
|
||||
):
|
||||
return f"{_COMPONENT_PREFIX_LIB}{self._lib_hash_to_name[parts[1]]}"
|
||||
|
||||
# Other framework/library files - return the first path component
|
||||
# e.g., FrameworkArduino/... -> FrameworkArduino
|
||||
return parts[0] if parts else source_file
|
||||
|
||||
def _analyze_cswtch_symbols(self) -> None:
|
||||
"""Analyze CSWTCH (GCC switch table) symbols by tracing to source objects.
|
||||
|
||||
CSWTCH symbols are compiler-generated lookup tables for switch statements.
|
||||
They are local symbols, so the same name can appear in different object files.
|
||||
This method scans .o files and SDK archives to attribute them to their
|
||||
source components.
|
||||
"""
|
||||
obj_dir = self._find_object_files_dir()
|
||||
if obj_dir is None:
|
||||
_LOGGER.debug("No object files directory found, skipping CSWTCH analysis")
|
||||
return
|
||||
|
||||
# Scan build-dir object files for CSWTCH symbols
|
||||
cswtch_map: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
self._run_nm_cswtch_scan(sorted(obj_dir.rglob("*.o")), obj_dir, cswtch_map)
|
||||
|
||||
# Also scan SDK library archives (.a) for CSWTCH symbols.
|
||||
# Prebuilt SDK libraries (e.g. lwip, bearssl) are not compiled from source
|
||||
# so their symbols only exist inside .a archives, not as loose .o files.
|
||||
self._scan_cswtch_in_sdk_archives(cswtch_map)
|
||||
|
||||
if not cswtch_map:
|
||||
_LOGGER.debug("No CSWTCH symbols found in object files or SDK archives")
|
||||
return
|
||||
|
||||
# Collect CSWTCH symbols from the ELF (already parsed in sections)
|
||||
# Include section_name for re-attribution of component totals
|
||||
elf_cswtch = [
|
||||
(symbol_name, size, section_name)
|
||||
for section_name, section in self.sections.items()
|
||||
for symbol_name, size, _ in section.symbols
|
||||
if symbol_name.startswith("CSWTCH$")
|
||||
]
|
||||
|
||||
_LOGGER.debug(
|
||||
"Found %d CSWTCH symbols in ELF, %d unique in object files",
|
||||
len(elf_cswtch),
|
||||
len(cswtch_map),
|
||||
)
|
||||
|
||||
# Match ELF CSWTCH symbols to source files and re-attribute component totals.
|
||||
# _categorize_symbols() already ran and put these into "other" since CSWTCH$
|
||||
# names don't match any component pattern. We move the bytes to the correct
|
||||
# component based on the object file mapping.
|
||||
other_mem = self.components.get("other")
|
||||
|
||||
for sym_name, size, section_name in elf_cswtch:
|
||||
key = f"{sym_name}:{size}"
|
||||
sources = cswtch_map.get(key, [])
|
||||
|
||||
if len(sources) == 1:
|
||||
source_file = sources[0][0]
|
||||
component = self._source_file_to_component(source_file)
|
||||
elif len(sources) > 1:
|
||||
# Ambiguous - multiple object files have same CSWTCH name+size
|
||||
source_file = "ambiguous"
|
||||
component = "ambiguous"
|
||||
_LOGGER.debug(
|
||||
"Ambiguous CSWTCH %s (%d B) found in %d files: %s",
|
||||
sym_name,
|
||||
size,
|
||||
len(sources),
|
||||
", ".join(src for src, _ in sources),
|
||||
)
|
||||
else:
|
||||
source_file = "unknown"
|
||||
component = "unknown"
|
||||
|
||||
self._cswtch_symbols.append((sym_name, size, source_file, component))
|
||||
|
||||
# Re-attribute from "other" to the correct component
|
||||
if (
|
||||
component not in ("other", "unknown", "ambiguous")
|
||||
and other_mem is not None
|
||||
):
|
||||
other_mem.add_section_size(section_name, -size)
|
||||
if component not in self.components:
|
||||
self.components[component] = ComponentMemory(component)
|
||||
self.components[component].add_section_size(section_name, size)
|
||||
|
||||
# Sort by size descending
|
||||
self._cswtch_symbols.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
total_size = sum(size for _, size, _, _ in self._cswtch_symbols)
|
||||
_LOGGER.debug(
|
||||
"CSWTCH analysis: %d symbols, %d bytes total",
|
||||
len(self._cswtch_symbols),
|
||||
total_size,
|
||||
)
|
||||
|
||||
def get_unattributed_ram(self) -> tuple[int, int, int]:
|
||||
"""Get unattributed RAM sizes (SDK/framework overhead).
|
||||
|
||||
|
||||
@@ -4,7 +4,9 @@ from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
import heapq
|
||||
import json
|
||||
from operator import itemgetter
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -13,6 +15,7 @@ from . import (
|
||||
_COMPONENT_CORE,
|
||||
_COMPONENT_PREFIX_ESPHOME,
|
||||
_COMPONENT_PREFIX_EXTERNAL,
|
||||
_COMPONENT_PREFIX_LIB,
|
||||
RAM_SECTIONS,
|
||||
MemoryAnalyzer,
|
||||
)
|
||||
@@ -30,6 +33,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
)
|
||||
# Lower threshold for RAM symbols (RAM is more constrained)
|
||||
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
|
||||
# Number of top symbols to show in the largest symbols report
|
||||
TOP_SYMBOLS_LIMIT: int = 30
|
||||
# Width for symbol name display in top symbols report
|
||||
COL_TOP_SYMBOL_NAME: int = 55
|
||||
|
||||
# Column width constants
|
||||
COL_COMPONENT: int = 29
|
||||
@@ -148,6 +155,83 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
|
||||
return f"{demangled} ({size:,} B){section_label}"
|
||||
|
||||
def _add_top_symbols(self, lines: list[str]) -> None:
|
||||
"""Add a section showing the top largest symbols in the binary."""
|
||||
# Collect all symbols from all components: (symbol, demangled, size, section, component)
|
||||
all_symbols = [
|
||||
(symbol, demangled, size, section, component)
|
||||
for component, symbols in self._component_symbols.items()
|
||||
for symbol, demangled, size, section in symbols
|
||||
]
|
||||
|
||||
# Get top N symbols by size using heapq for efficiency
|
||||
top_symbols = heapq.nlargest(
|
||||
self.TOP_SYMBOLS_LIMIT, all_symbols, key=itemgetter(2)
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append(f"Top {self.TOP_SYMBOLS_LIMIT} Largest Symbols:")
|
||||
# Calculate truncation limit from column width (leaving room for "...")
|
||||
truncate_limit = self.COL_TOP_SYMBOL_NAME - 3
|
||||
for i, (_, demangled, size, section, component) in enumerate(top_symbols):
|
||||
# Format section label
|
||||
section_label = f"[{section[1:]}]" if section else ""
|
||||
# Truncate demangled name if too long
|
||||
demangled_display = (
|
||||
f"{demangled[:truncate_limit]}..."
|
||||
if len(demangled) > self.COL_TOP_SYMBOL_NAME
|
||||
else demangled
|
||||
)
|
||||
lines.append(
|
||||
f"{i + 1:>2}. {size:>7,} B {section_label:<8} {demangled_display:<{self.COL_TOP_SYMBOL_NAME}} {component}"
|
||||
)
|
||||
|
||||
def _add_cswtch_analysis(self, lines: list[str]) -> None:
|
||||
"""Add CSWTCH (GCC switch table lookup) analysis section."""
|
||||
self._add_section_header(lines, "CSWTCH Analysis (GCC Switch Table Lookups)")
|
||||
|
||||
total_size = sum(size for _, size, _, _ in self._cswtch_symbols)
|
||||
lines.append(
|
||||
f"Total: {len(self._cswtch_symbols)} switch table(s), {total_size:,} B"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
# Group by component
|
||||
by_component: dict[str, list[tuple[str, int, str]]] = defaultdict(list)
|
||||
for sym_name, size, source_file, component in self._cswtch_symbols:
|
||||
by_component[component].append((sym_name, size, source_file))
|
||||
|
||||
# Sort components by total size descending
|
||||
sorted_components = sorted(
|
||||
by_component.items(),
|
||||
key=lambda x: sum(s[1] for s in x[1]),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
for component, symbols in sorted_components:
|
||||
comp_total = sum(s[1] for s in symbols)
|
||||
lines.append(f"{component} ({comp_total:,} B, {len(symbols)} tables):")
|
||||
|
||||
# Group by source file within component
|
||||
by_file: dict[str, list[tuple[str, int]]] = defaultdict(list)
|
||||
for sym_name, size, source_file in symbols:
|
||||
by_file[source_file].append((sym_name, size))
|
||||
|
||||
for source_file, file_symbols in sorted(
|
||||
by_file.items(),
|
||||
key=lambda x: sum(s[1] for s in x[1]),
|
||||
reverse=True,
|
||||
):
|
||||
file_total = sum(s[1] for s in file_symbols)
|
||||
lines.append(
|
||||
f" {source_file} ({file_total:,} B, {len(file_symbols)} tables)"
|
||||
)
|
||||
for sym_name, size in sorted(
|
||||
file_symbols, key=lambda x: x[1], reverse=True
|
||||
):
|
||||
lines.append(f" {size:>6,} B {sym_name}")
|
||||
lines.append("")
|
||||
|
||||
def generate_report(self, detailed: bool = False) -> str:
|
||||
"""Generate a formatted memory report."""
|
||||
components = sorted(
|
||||
@@ -249,6 +333,9 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
"RAM",
|
||||
)
|
||||
|
||||
# Top largest symbols in the binary
|
||||
self._add_top_symbols(lines)
|
||||
|
||||
# Add ESPHome core detailed analysis if there are core symbols
|
||||
if self._esphome_core_symbols:
|
||||
self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis")
|
||||
@@ -322,6 +409,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
for name, mem in components
|
||||
if name.startswith(_COMPONENT_PREFIX_EXTERNAL)
|
||||
]
|
||||
library_components = [
|
||||
(name, mem)
|
||||
for name, mem in components
|
||||
if name.startswith(_COMPONENT_PREFIX_LIB)
|
||||
]
|
||||
|
||||
top_esphome_components = sorted(
|
||||
esphome_components, key=lambda x: x[1].flash_total, reverse=True
|
||||
@@ -332,6 +424,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
external_components, key=lambda x: x[1].flash_total, reverse=True
|
||||
)
|
||||
|
||||
# Include all library components
|
||||
top_library_components = sorted(
|
||||
library_components, key=lambda x: x[1].flash_total, reverse=True
|
||||
)
|
||||
|
||||
# Check if API component exists and ensure it's included
|
||||
api_component = None
|
||||
for name, mem in components:
|
||||
@@ -350,10 +447,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
if name in system_components_to_include
|
||||
]
|
||||
|
||||
# Combine all components to analyze: top ESPHome + all external + API if not already included + system components
|
||||
# Combine all components to analyze: top ESPHome + all external + libraries + API if not already included + system components
|
||||
components_to_analyze = (
|
||||
list(top_esphome_components)
|
||||
+ list(top_external_components)
|
||||
+ list(top_library_components)
|
||||
+ system_components
|
||||
)
|
||||
if api_component and api_component not in components_to_analyze:
|
||||
@@ -432,6 +530,10 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(f" ... and {len(large_ram_syms) - 10} more")
|
||||
lines.append("")
|
||||
|
||||
# CSWTCH (GCC switch table) analysis
|
||||
if self._cswtch_symbols:
|
||||
self._add_cswtch_analysis(lines)
|
||||
|
||||
lines.append(
|
||||
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
|
||||
)
|
||||
|
||||
@@ -66,15 +66,6 @@ SECTION_MAPPING = {
|
||||
),
|
||||
}
|
||||
|
||||
# Section to ComponentMemory attribute mapping
|
||||
# Maps section names to the attribute name in ComponentMemory dataclass
|
||||
SECTION_TO_ATTR = {
|
||||
".text": "text_size",
|
||||
".rodata": "rodata_size",
|
||||
".data": "data_size",
|
||||
".bss": "bss_size",
|
||||
}
|
||||
|
||||
# Component identification rules
|
||||
# Symbol patterns: patterns found in raw symbol names
|
||||
SYMBOL_PATTERNS = {
|
||||
@@ -513,7 +504,9 @@ SYMBOL_PATTERNS = {
|
||||
"__FUNCTION__$",
|
||||
"DAYS_IN_MONTH",
|
||||
"_DAYS_BEFORE_MONTH",
|
||||
"CSWTCH$",
|
||||
# Note: CSWTCH$ symbols are GCC switch table lookup tables.
|
||||
# They are attributed to their source object files via _analyze_cswtch_symbols()
|
||||
# rather than being lumped into libc.
|
||||
"dst$",
|
||||
"sulp",
|
||||
"_strtol_l", # String to long with locale
|
||||
|
||||
@@ -87,6 +87,7 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
size_t,
|
||||
std_ns,
|
||||
std_shared_ptr,
|
||||
std_span,
|
||||
std_string,
|
||||
std_string_ref,
|
||||
std_vector,
|
||||
|
||||
@@ -45,8 +45,6 @@ void AbsoluteHumidityComponent::dump_config() {
|
||||
this->temperature_sensor_->get_name().c_str(), this->humidity_sensor_->get_name().c_str());
|
||||
}
|
||||
|
||||
float AbsoluteHumidityComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void AbsoluteHumidityComponent::loop() {
|
||||
if (!this->next_update_) {
|
||||
return;
|
||||
|
||||
@@ -24,7 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -68,11 +68,6 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
/// This method is called during the ESPHome setup process to log the configuration.
|
||||
void dump_config() override;
|
||||
|
||||
/// Return the setup priority for this component.
|
||||
/// Components with higher priority are initialized earlier during setup.
|
||||
/// @return A float representing the setup priority.
|
||||
float get_setup_priority() const override;
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
/// Set the ADC channel to be used by the ADC sensor.
|
||||
/// @param channel Pointer to an adc_dt_spec structure representing the ADC channel.
|
||||
|
||||
@@ -79,7 +79,5 @@ void ADCSensor::set_sample_count(uint8_t sample_count) {
|
||||
|
||||
void ADCSensor::set_sampling_mode(SamplingMode sampling_mode) { this->sampling_mode_ = sampling_mode; }
|
||||
|
||||
float ADCSensor::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace adc
|
||||
} // namespace esphome
|
||||
|
||||
@@ -42,11 +42,11 @@ void ADCSensor::setup() {
|
||||
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
|
||||
init_config.unit_id = this->adc_unit_;
|
||||
init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
|
||||
#if USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || \
|
||||
USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
|
||||
init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
|
||||
// USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
|
||||
#endif // USE_ESP32_VARIANT_ESP32C2 || USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 ||
|
||||
// USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
|
||||
esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
|
||||
@@ -76,7 +76,7 @@ void ADCSensor::setup() {
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
// RISC-V variants and S3 use curve fitting calibration
|
||||
// RISC-V variants (except C2) and S3 use curve fitting calibration
|
||||
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
@@ -94,14 +94,14 @@ void ADCSensor::setup() {
|
||||
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
|
||||
this->setup_flags_.calibration_complete = false;
|
||||
}
|
||||
#else // Other ESP32 variants use line fitting calibration
|
||||
#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration
|
||||
adc_cali_line_fitting_config_t cali_config = {
|
||||
.unit_id = this->adc_unit_,
|
||||
.atten = this->attenuation_,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2)
|
||||
.default_vref = 1100, // Default reference voltage in mV
|
||||
#endif // !defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
#endif // !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2)
|
||||
};
|
||||
err = adc_cali_create_scheme_line_fitting(&cali_config, &handle);
|
||||
if (err == ESP_OK) {
|
||||
@@ -112,7 +112,7 @@ void ADCSensor::setup() {
|
||||
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
|
||||
this->setup_flags_.calibration_complete = false;
|
||||
}
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
}
|
||||
|
||||
this->setup_flags_.init_complete = true;
|
||||
@@ -189,7 +189,7 @@ float ADCSensor::sample_fixed_attenuation_() {
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else // Other ESP32 variants use line fitting calibration
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
this->calibration_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -247,7 +247,7 @@ float ADCSensor::sample_autorange_() {
|
||||
.unit_id = this->adc_unit_,
|
||||
.atten = atten,
|
||||
.bitwidth = ADC_BITWIDTH_DEFAULT,
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32C2)
|
||||
.default_vref = 1100,
|
||||
#endif
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
|
||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
@@ -118,6 +118,9 @@ async def to_code(config):
|
||||
cg.add(var.set_sampling_mode(config[CONF_SAMPLING_MODE]))
|
||||
|
||||
if CORE.is_esp32:
|
||||
# Re-enable ESP-IDF's ADC driver (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_adc")
|
||||
|
||||
if attenuation := config.get(CONF_ATTENUATION):
|
||||
if attenuation == "auto":
|
||||
cg.add(var.set_autorange(cg.global_ns.true))
|
||||
|
||||
@@ -9,8 +9,6 @@ static const char *const TAG = "adc128s102.sensor";
|
||||
|
||||
ADC128S102Sensor::ADC128S102Sensor(uint8_t channel) : channel_(channel) {}
|
||||
|
||||
float ADC128S102Sensor::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void ADC128S102Sensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC128S102 Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Pin: %u", this->channel_);
|
||||
|
||||
@@ -19,7 +19,6 @@ class ADC128S102Sensor : public PollingComponent,
|
||||
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
float sample() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -150,8 +150,6 @@ void AHT10Component::update() {
|
||||
this->restart_read_();
|
||||
}
|
||||
|
||||
float AHT10Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void AHT10Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AHT10:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
|
||||
@@ -16,7 +16,6 @@ class AHT10Component : public PollingComponent, public i2c::I2CDevice {
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void set_variant(AHT10Variant variant) { this->variant_ = variant; }
|
||||
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
|
||||
@@ -1,32 +1,15 @@
|
||||
#include "alarm_control_panel_state.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome::alarm_control_panel {
|
||||
|
||||
// Alarm control panel state strings indexed by AlarmControlPanelState enum (0-9)
|
||||
PROGMEM_STRING_TABLE(AlarmControlPanelStateStrings, "DISARMED", "ARMED_HOME", "ARMED_AWAY", "ARMED_NIGHT",
|
||||
"ARMED_VACATION", "ARMED_CUSTOM_BYPASS", "PENDING", "ARMING", "DISARMING", "TRIGGERED", "UNKNOWN");
|
||||
|
||||
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) {
|
||||
switch (state) {
|
||||
case ACP_STATE_DISARMED:
|
||||
return LOG_STR("DISARMED");
|
||||
case ACP_STATE_ARMED_HOME:
|
||||
return LOG_STR("ARMED_HOME");
|
||||
case ACP_STATE_ARMED_AWAY:
|
||||
return LOG_STR("ARMED_AWAY");
|
||||
case ACP_STATE_ARMED_NIGHT:
|
||||
return LOG_STR("ARMED_NIGHT");
|
||||
case ACP_STATE_ARMED_VACATION:
|
||||
return LOG_STR("ARMED_VACATION");
|
||||
case ACP_STATE_ARMED_CUSTOM_BYPASS:
|
||||
return LOG_STR("ARMED_CUSTOM_BYPASS");
|
||||
case ACP_STATE_PENDING:
|
||||
return LOG_STR("PENDING");
|
||||
case ACP_STATE_ARMING:
|
||||
return LOG_STR("ARMING");
|
||||
case ACP_STATE_DISARMING:
|
||||
return LOG_STR("DISARMING");
|
||||
case ACP_STATE_TRIGGERED:
|
||||
return LOG_STR("TRIGGERED");
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
return AlarmControlPanelStateStrings::get_log_str(static_cast<uint8_t>(state),
|
||||
AlarmControlPanelStateStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
} // namespace esphome::alarm_control_panel
|
||||
|
||||
@@ -176,7 +176,5 @@ void AM2315C::dump_config() {
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
|
||||
float AM2315C::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace am2315c
|
||||
} // namespace esphome
|
||||
|
||||
@@ -33,7 +33,6 @@ class AM2315C : public PollingComponent, public i2c::I2CDevice {
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
void setup() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
@@ -51,7 +51,6 @@ void AM2320Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
}
|
||||
float AM2320Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
bool AM2320Component::read_bytes_(uint8_t a_register, uint8_t *data, uint8_t len, uint32_t conversion) {
|
||||
if (!this->write_bytes(a_register, data, 2)) {
|
||||
|
||||
@@ -11,7 +11,6 @@ class AM2320Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
|
||||
@@ -384,7 +384,6 @@ void APDS9960::process_dataset_(int up, int down, int left, int right) {
|
||||
}
|
||||
}
|
||||
}
|
||||
float APDS9960::get_setup_priority() const { return setup_priority::DATA; }
|
||||
bool APDS9960::is_proximity_enabled_() const {
|
||||
return
|
||||
#ifdef USE_SENSOR
|
||||
|
||||
@@ -32,7 +32,6 @@ class APDS9960 : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
void loop() override;
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ service APIConnection {
|
||||
rpc time_command (TimeCommandRequest) returns (void) {}
|
||||
rpc update_command (UpdateCommandRequest) returns (void) {}
|
||||
rpc valve_command (ValveCommandRequest) returns (void) {}
|
||||
rpc water_heater_command (WaterHeaterCommandRequest) returns (void) {}
|
||||
|
||||
rpc subscribe_bluetooth_le_advertisements(SubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
|
||||
rpc bluetooth_device_request(BluetoothDeviceRequest) returns (void) {}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,7 @@ static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= AP
|
||||
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
|
||||
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
|
||||
|
||||
class APIConnection final : public APIServerConnection {
|
||||
class APIConnection final : public APIServerConnectionBase {
|
||||
public:
|
||||
friend class APIServer;
|
||||
friend class ListEntitiesIterator;
|
||||
@@ -47,72 +47,72 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
bool send_cover_state(cover::Cover *cover);
|
||||
void cover_command(const CoverCommandRequest &msg) override;
|
||||
void on_cover_command_request(const CoverCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
bool send_fan_state(fan::Fan *fan);
|
||||
void fan_command(const FanCommandRequest &msg) override;
|
||||
void on_fan_command_request(const FanCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
bool send_light_state(light::LightState *light);
|
||||
void light_command(const LightCommandRequest &msg) override;
|
||||
void on_light_command_request(const LightCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
bool send_sensor_state(sensor::Sensor *sensor);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
bool send_switch_state(switch_::Switch *a_switch);
|
||||
void switch_command(const SwitchCommandRequest &msg) override;
|
||||
void on_switch_command_request(const SwitchCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool send_text_sensor_state(text_sensor::TextSensor *text_sensor);
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
|
||||
void camera_image(const CameraImageRequest &msg) override;
|
||||
void on_camera_image_request(const CameraImageRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
bool send_climate_state(climate::Climate *climate);
|
||||
void climate_command(const ClimateCommandRequest &msg) override;
|
||||
void on_climate_command_request(const ClimateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
bool send_number_state(number::Number *number);
|
||||
void number_command(const NumberCommandRequest &msg) override;
|
||||
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
bool send_date_state(datetime::DateEntity *date);
|
||||
void date_command(const DateCommandRequest &msg) override;
|
||||
void on_date_command_request(const DateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
bool send_time_state(datetime::TimeEntity *time);
|
||||
void time_command(const TimeCommandRequest &msg) override;
|
||||
void on_time_command_request(const TimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
bool send_datetime_state(datetime::DateTimeEntity *datetime);
|
||||
void datetime_command(const DateTimeCommandRequest &msg) override;
|
||||
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
bool send_text_state(text::Text *text);
|
||||
void text_command(const TextCommandRequest &msg) override;
|
||||
void on_text_command_request(const TextCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
bool send_select_state(select::Select *select);
|
||||
void select_command(const SelectCommandRequest &msg) override;
|
||||
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void button_command(const ButtonCommandRequest &msg) override;
|
||||
void on_button_command_request(const ButtonCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
bool send_lock_state(lock::Lock *a_lock);
|
||||
void lock_command(const LockCommandRequest &msg) override;
|
||||
void on_lock_command_request(const LockCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
bool send_valve_state(valve::Valve *valve);
|
||||
void valve_command(const ValveCommandRequest &msg) override;
|
||||
void on_valve_command_request(const ValveCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
bool send_media_player_state(media_player::MediaPlayer *media_player);
|
||||
void media_player_command(const MediaPlayerCommandRequest &msg) override;
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
|
||||
#endif
|
||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
@@ -126,18 +126,18 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
void on_unsubscribe_bluetooth_le_advertisements_request() override;
|
||||
|
||||
void bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||
void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) override;
|
||||
void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) override;
|
||||
void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) override;
|
||||
void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||
void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) override;
|
||||
void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) override;
|
||||
bool send_subscribe_bluetooth_connections_free_response(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
|
||||
void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) override;
|
||||
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
|
||||
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
|
||||
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
|
||||
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
|
||||
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
|
||||
void on_subscribe_bluetooth_connections_free_request() override;
|
||||
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
|
||||
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
@@ -148,24 +148,24 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override;
|
||||
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
|
||||
void on_voice_assistant_response(const VoiceAssistantResponse &msg) override;
|
||||
void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override;
|
||||
void on_voice_assistant_audio(const VoiceAssistantAudio &msg) override;
|
||||
void on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) override;
|
||||
void on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) override;
|
||||
bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) override;
|
||||
void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
|
||||
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void zwave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
||||
void zwave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_WATER_HEATER
|
||||
@@ -174,7 +174,7 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
#endif
|
||||
|
||||
@@ -184,11 +184,11 @@ class APIConnection final : public APIServerConnection {
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool send_update_state(update::UpdateEntity *update);
|
||||
void update_command(const UpdateCommandRequest &msg) override;
|
||||
void on_update_command_request(const UpdateCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
void on_disconnect_response(const DisconnectResponse &value) override;
|
||||
void on_ping_response(const PingResponse &value) override {
|
||||
void on_disconnect_response() override;
|
||||
void on_ping_response() override {
|
||||
// we initiated ping
|
||||
this->flags_.sent_ping = false;
|
||||
}
|
||||
@@ -198,12 +198,12 @@ class APIConnection final : public APIServerConnection {
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void on_get_time_response(const GetTimeResponse &value) override;
|
||||
#endif
|
||||
bool send_hello_response(const HelloRequest &msg) override;
|
||||
bool send_disconnect_response(const DisconnectRequest &msg) override;
|
||||
bool send_ping_response(const PingRequest &msg) override;
|
||||
bool send_device_info_response(const DeviceInfoRequest &msg) override;
|
||||
void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
||||
void subscribe_states(const SubscribeStatesRequest &msg) override {
|
||||
void on_hello_request(const HelloRequest &msg) override;
|
||||
void on_disconnect_request() override;
|
||||
void on_ping_request() override;
|
||||
void on_device_info_request() override;
|
||||
void on_list_entities_request() override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
||||
void on_subscribe_states_request() override {
|
||||
this->flags_.state_subscription = true;
|
||||
// Start initial state iterator only if no iterator is active
|
||||
// If list_entities is running, we'll start initial_state when it completes
|
||||
@@ -211,21 +211,19 @@ class APIConnection final : public APIServerConnection {
|
||||
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
|
||||
}
|
||||
}
|
||||
void subscribe_logs(const SubscribeLogsRequest &msg) override {
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
|
||||
this->flags_.service_call_subscription = true;
|
||||
}
|
||||
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
void on_subscribe_home_assistant_states_request() override;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
@@ -235,7 +233,7 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
bool is_authenticated() override {
|
||||
@@ -255,17 +253,7 @@ class APIConnection final : public APIServerConnection {
|
||||
|
||||
void on_fatal_error() override;
|
||||
void on_no_setup_connection() override;
|
||||
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
|
||||
// FIXME: ensure no recursive writes can happen
|
||||
|
||||
// Get header padding size - used for both reserve and insert
|
||||
uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
// Get shared buffer from parent server
|
||||
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
this->prepare_first_message_buffer(shared_buf, header_padding,
|
||||
reserve_size + header_padding + this->helper_->frame_footer_size());
|
||||
return {&shared_buf};
|
||||
}
|
||||
bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override;
|
||||
|
||||
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) {
|
||||
shared_buf.clear();
|
||||
@@ -277,17 +265,41 @@ class APIConnection final : public APIServerConnection {
|
||||
shared_buf.resize(header_padding);
|
||||
}
|
||||
|
||||
// Convenience overload - computes frame overhead internally
|
||||
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) {
|
||||
const uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
const uint8_t footer_size = this->helper_->frame_footer_size();
|
||||
this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size);
|
||||
}
|
||||
|
||||
bool try_to_clear_buffer(bool log_out_of_space);
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||
|
||||
const char *get_name() const { return this->helper_->get_client_name(); }
|
||||
/// Get peer name (IP address) - cached at connection init time
|
||||
const char *get_peername() const { return this->helper_->get_client_peername(); }
|
||||
/// Get peer name (IP address) into caller-provided buffer, returns buf for convenience
|
||||
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||
return this->helper_->get_peername_to(buf);
|
||||
}
|
||||
|
||||
protected:
|
||||
// Helper function to handle authentication completion
|
||||
void complete_authentication_();
|
||||
|
||||
// Pattern B helpers: send response and return success/failure
|
||||
bool send_hello_response_(const HelloRequest &msg);
|
||||
bool send_disconnect_response_();
|
||||
bool send_ping_response_();
|
||||
bool send_device_info_response_();
|
||||
#ifdef USE_API_NOISE
|
||||
bool send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
bool send_subscribe_bluetooth_connections_free_response_();
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
bool send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
void try_send_camera_image_();
|
||||
#endif
|
||||
@@ -298,21 +310,21 @@ class APIConnection final : public APIServerConnection {
|
||||
|
||||
// Non-template helper to encode any ProtoMessage
|
||||
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
uint32_t remaining_size);
|
||||
|
||||
// Helper to fill entity state base and encode message
|
||||
static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type,
|
||||
APIConnection *conn, uint32_t remaining_size, bool is_single) {
|
||||
APIConnection *conn, uint32_t remaining_size) {
|
||||
msg.key = entity->get_object_id_hash();
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = entity->get_device_id();
|
||||
#endif
|
||||
return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single);
|
||||
return encode_message_to_buffer(msg, message_type, conn, remaining_size);
|
||||
}
|
||||
|
||||
// Helper to fill entity info base and encode message
|
||||
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type,
|
||||
APIConnection *conn, uint32_t remaining_size, bool is_single) {
|
||||
APIConnection *conn, uint32_t remaining_size) {
|
||||
// Set common fields that are shared by all entity types
|
||||
msg.key = entity->get_object_id_hash();
|
||||
|
||||
@@ -339,7 +351,7 @@ class APIConnection final : public APIServerConnection {
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = entity->get_device_id();
|
||||
#endif
|
||||
return encode_message_to_buffer(msg, message_type, conn, remaining_size, is_single);
|
||||
return encode_message_to_buffer(msg, message_type, conn, remaining_size);
|
||||
}
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
@@ -370,141 +382,108 @@ class APIConnection final : public APIServerConnection {
|
||||
}
|
||||
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
static uint16_t try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
static uint16_t try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
static uint16_t try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_SENSOR
|
||||
static uint16_t try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
static uint16_t try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
static uint16_t try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
static uint16_t try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
static uint16_t try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
static uint16_t try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
static uint16_t try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
static uint16_t try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
static uint16_t try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
static uint16_t try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
static uint16_t try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
static uint16_t try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
static uint16_t try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
static uint16_t try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
static uint16_t try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
uint32_t remaining_size);
|
||||
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
static uint16_t try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
static uint16_t try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
#endif
|
||||
|
||||
// Method for ListEntitiesDone batching
|
||||
static uint16_t try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
|
||||
// Method for DisconnectRequest batching
|
||||
static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
|
||||
// Batch message method for ping requests
|
||||
static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
static uint16_t try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
|
||||
|
||||
// === Optimal member ordering for 32-bit systems ===
|
||||
|
||||
@@ -539,7 +518,7 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
|
||||
// Function pointer type for message encoding
|
||||
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single);
|
||||
using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size);
|
||||
|
||||
// Generic batching mechanism for both state updates and entity info
|
||||
struct DeferredBatch {
|
||||
@@ -652,7 +631,7 @@ class APIConnection final : public APIServerConnection {
|
||||
|
||||
// Dispatch message encoding based on message_type - replaces function pointer storage
|
||||
// Switch assigns pointer, single call site for smaller code size
|
||||
uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool is_single);
|
||||
uint16_t dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size, bool batch_first);
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void log_batch_item_(const DeferredBatch::BatchItem &item) {
|
||||
@@ -684,19 +663,7 @@ class APIConnection final : public APIServerConnection {
|
||||
// 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,
|
||||
uint8_t aux_data_index = DeferredBatch::AUX_DATA_UNUSED) {
|
||||
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
||||
DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index};
|
||||
if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) &&
|
||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_batch_item_(item);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return this->schedule_message_(entity, message_type, estimated_size, aux_data_index);
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -16,7 +16,12 @@ static const char *const TAG = "api.frame_helper";
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
do { \
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
||||
this->get_peername_to(peername_buf); \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
@@ -240,13 +245,20 @@ APIError APIFrameHelper::try_send_tx_buf_() {
|
||||
return APIError::OK; // All buffers sent successfully
|
||||
}
|
||||
|
||||
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||
if (this->socket_) {
|
||||
this->socket_->getpeername_to(buf);
|
||||
} else {
|
||||
buf[0] = '\0';
|
||||
}
|
||||
return buf.data();
|
||||
}
|
||||
|
||||
APIError APIFrameHelper::init_common_() {
|
||||
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
|
||||
HELPER_LOG("Bad state for init %d", (int) state_);
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
// Cache peername now while socket is valid - needed for error logging after socket failure
|
||||
this->socket_->getpeername_to(this->client_peername_);
|
||||
int err = this->socket_->setblocking(false);
|
||||
if (err != 0) {
|
||||
state_ = State::FAILED;
|
||||
|
||||
@@ -90,8 +90,9 @@ class APIFrameHelper {
|
||||
|
||||
// Get client name (null-terminated)
|
||||
const char *get_client_name() const { return this->client_name_; }
|
||||
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
|
||||
const char *get_client_peername() const { return this->client_peername_; }
|
||||
// Get client peername/IP into caller-provided buffer (fetches on-demand from socket)
|
||||
// Returns pointer to buf for convenience in printf-style calls
|
||||
const char *get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const;
|
||||
// Set client name from buffer with length (truncates if needed)
|
||||
void set_client_name(const char *name, size_t len) {
|
||||
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
|
||||
@@ -105,6 +106,8 @@ class APIFrameHelper {
|
||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
|
||||
APIError close() {
|
||||
if (state_ == State::CLOSED)
|
||||
return APIError::OK; // Already closed
|
||||
state_ = State::CLOSED;
|
||||
int err = this->socket_->close();
|
||||
if (err == -1)
|
||||
@@ -231,8 +234,6 @@ class APIFrameHelper {
|
||||
|
||||
// Client name buffer - stores name from Hello message or initial peername
|
||||
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
|
||||
// Cached peername/IP address - captured at init time for availability after socket failure
|
||||
char client_peername_[socket::SOCKADDR_STR_LEN]{};
|
||||
|
||||
// Group smaller types together
|
||||
uint16_t rx_buf_len_ = 0;
|
||||
|
||||
@@ -29,7 +29,12 @@ static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
do { \
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
||||
this->get_peername_to(peername_buf); \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
|
||||
@@ -21,7 +21,12 @@ static const char *const TAG = "api.plaintext";
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
do { \
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN]; \
|
||||
this->get_peername_to(peername_buf); \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, peername_buf, ##__VA_ARGS__); \
|
||||
} while (0)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
|
||||
@@ -23,15 +23,8 @@ static inline void append_field_prefix(DumpBuffer &out, const char *field_name,
|
||||
out.append(indent, ' ').append(field_name).append(": ");
|
||||
}
|
||||
|
||||
static inline void append_with_newline(DumpBuffer &out, const char *str) {
|
||||
out.append(str);
|
||||
out.append("\n");
|
||||
}
|
||||
|
||||
static inline void append_uint(DumpBuffer &out, uint32_t value) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%" PRIu32, value);
|
||||
out.append(buf);
|
||||
out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32, value));
|
||||
}
|
||||
|
||||
// RAII helper for message dump formatting
|
||||
@@ -49,31 +42,23 @@ class MessageDumpHelper {
|
||||
|
||||
// Helper functions to reduce code duplication in dump methods
|
||||
static void dump_field(DumpBuffer &out, const char *field_name, int32_t value, int indent = 2) {
|
||||
char buffer[64];
|
||||
append_field_prefix(out, field_name, indent);
|
||||
snprintf(buffer, 64, "%" PRId32, value);
|
||||
append_with_newline(out, buffer);
|
||||
out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRId32 "\n", value));
|
||||
}
|
||||
|
||||
static void dump_field(DumpBuffer &out, const char *field_name, uint32_t value, int indent = 2) {
|
||||
char buffer[64];
|
||||
append_field_prefix(out, field_name, indent);
|
||||
snprintf(buffer, 64, "%" PRIu32, value);
|
||||
append_with_newline(out, buffer);
|
||||
out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu32 "\n", value));
|
||||
}
|
||||
|
||||
static void dump_field(DumpBuffer &out, const char *field_name, float value, int indent = 2) {
|
||||
char buffer[64];
|
||||
append_field_prefix(out, field_name, indent);
|
||||
snprintf(buffer, 64, "%g", value);
|
||||
append_with_newline(out, buffer);
|
||||
out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%g\n", value));
|
||||
}
|
||||
|
||||
static void dump_field(DumpBuffer &out, const char *field_name, uint64_t value, int indent = 2) {
|
||||
char buffer[64];
|
||||
append_field_prefix(out, field_name, indent);
|
||||
snprintf(buffer, 64, "%" PRIu64, value);
|
||||
append_with_newline(out, buffer);
|
||||
out.set_pos(buf_append_printf(out.data(), DumpBuffer::CAPACITY, out.pos(), "%" PRIu64 "\n", value));
|
||||
}
|
||||
|
||||
static void dump_field(DumpBuffer &out, const char *field_name, bool value, int indent = 2) {
|
||||
@@ -112,7 +97,7 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
|
||||
char hex_buf[format_hex_pretty_size(160)];
|
||||
append_field_prefix(out, field_name, indent);
|
||||
format_hex_pretty_to(hex_buf, data, len);
|
||||
append_with_newline(out, hex_buf);
|
||||
out.append(hex_buf).append("\n");
|
||||
}
|
||||
|
||||
template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::EntityCategory value) {
|
||||
|
||||
@@ -15,9 +15,29 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name, const
|
||||
DumpBuffer dump_buf;
|
||||
ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf));
|
||||
}
|
||||
void APIServerConnectionBase::log_receive_message_(const LogString *name) {
|
||||
ESP_LOGVV(TAG, "%s: {}", LOG_STR_ARG(name));
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||
case DisconnectRequest::MESSAGE_TYPE: // No setup required
|
||||
case PingRequest::MESSAGE_TYPE: // No setup required
|
||||
break;
|
||||
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
|
||||
if (!this->check_connection_setup_()) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (!this->check_authenticated_()) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: {
|
||||
HelloRequest msg;
|
||||
@@ -29,66 +49,52 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
break;
|
||||
}
|
||||
case DisconnectRequest::MESSAGE_TYPE: {
|
||||
DisconnectRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_request"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_request"));
|
||||
#endif
|
||||
this->on_disconnect_request(msg);
|
||||
this->on_disconnect_request();
|
||||
break;
|
||||
}
|
||||
case DisconnectResponse::MESSAGE_TYPE: {
|
||||
DisconnectResponse msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_response"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_response"));
|
||||
#endif
|
||||
this->on_disconnect_response(msg);
|
||||
this->on_disconnect_response();
|
||||
break;
|
||||
}
|
||||
case PingRequest::MESSAGE_TYPE: {
|
||||
PingRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_ping_request"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_ping_request"));
|
||||
#endif
|
||||
this->on_ping_request(msg);
|
||||
this->on_ping_request();
|
||||
break;
|
||||
}
|
||||
case PingResponse::MESSAGE_TYPE: {
|
||||
PingResponse msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_ping_response"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_ping_response"));
|
||||
#endif
|
||||
this->on_ping_response(msg);
|
||||
this->on_ping_response();
|
||||
break;
|
||||
}
|
||||
case DeviceInfoRequest::MESSAGE_TYPE: {
|
||||
DeviceInfoRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_device_info_request"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_device_info_request"));
|
||||
#endif
|
||||
this->on_device_info_request(msg);
|
||||
this->on_device_info_request();
|
||||
break;
|
||||
}
|
||||
case ListEntitiesRequest::MESSAGE_TYPE: {
|
||||
ListEntitiesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_list_entities_request"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_list_entities_request"));
|
||||
#endif
|
||||
this->on_list_entities_request(msg);
|
||||
this->on_list_entities_request();
|
||||
break;
|
||||
}
|
||||
case SubscribeStatesRequest::MESSAGE_TYPE: {
|
||||
SubscribeStatesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_states_request"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_states_request"));
|
||||
#endif
|
||||
this->on_subscribe_states_request(msg);
|
||||
this->on_subscribe_states_request();
|
||||
break;
|
||||
}
|
||||
case SubscribeLogsRequest::MESSAGE_TYPE: {
|
||||
@@ -146,12 +152,10 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
|
||||
SubscribeHomeassistantServicesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"));
|
||||
#endif
|
||||
this->on_subscribe_homeassistant_services_request(msg);
|
||||
this->on_subscribe_homeassistant_services_request();
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
@@ -166,12 +170,10 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
|
||||
SubscribeHomeAssistantStatesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"));
|
||||
#endif
|
||||
this->on_subscribe_home_assistant_states_request(msg);
|
||||
this->on_subscribe_home_assistant_states_request();
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
@@ -375,23 +377,19 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
|
||||
SubscribeBluetoothConnectionsFreeRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"));
|
||||
#endif
|
||||
this->on_subscribe_bluetooth_connections_free_request(msg);
|
||||
this->on_subscribe_bluetooth_connections_free_request();
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
|
||||
UnsubscribeBluetoothLEAdvertisementsRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"), msg);
|
||||
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"));
|
||||
#endif
|
||||
this->on_unsubscribe_bluetooth_le_advertisements_request(msg);
|
||||
this->on_unsubscribe_bluetooth_le_advertisements_request();
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
@@ -642,226 +640,4 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
}
|
||||
|
||||
void APIServerConnection::on_hello_request(const HelloRequest &msg) {
|
||||
if (!this->send_hello_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
|
||||
if (!this->send_disconnect_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_ping_request(const PingRequest &msg) {
|
||||
if (!this->send_ping_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) {
|
||||
if (!this->send_device_info_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { this->list_entities(msg); }
|
||||
void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) {
|
||||
this->subscribe_states(msg);
|
||||
}
|
||||
void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { this->subscribe_logs(msg); }
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServerConnection::on_subscribe_homeassistant_services_request(
|
||||
const SubscribeHomeassistantServicesRequest &msg) {
|
||||
this->subscribe_homeassistant_services(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
|
||||
this->subscribe_home_assistant_states(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
if (!this->send_noise_encryption_set_key_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { this->button_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { this->camera_image(msg); }
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { this->climate_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { this->cover_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { this->date_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
|
||||
this->datetime_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { this->fan_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { this->light_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { this->lock_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
|
||||
this->media_player_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { this->number_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { this->select_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { this->siren_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { this->switch_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { this->text_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { this->time_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { this->update_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { this->valve_command(msg); }
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request(
|
||||
const SubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
this->subscribe_bluetooth_le_advertisements(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
|
||||
this->bluetooth_device_request(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
|
||||
this->bluetooth_gatt_get_services(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
|
||||
this->bluetooth_gatt_read(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
|
||||
this->bluetooth_gatt_write(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
|
||||
this->bluetooth_gatt_read_descriptor(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
|
||||
this->bluetooth_gatt_write_descriptor(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
|
||||
this->bluetooth_gatt_notify(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_subscribe_bluetooth_connections_free_request(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||
if (!this->send_subscribe_bluetooth_connections_free_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request(
|
||||
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) {
|
||||
this->unsubscribe_bluetooth_le_advertisements(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
|
||||
this->bluetooth_scanner_set_mode(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
|
||||
this->subscribe_voice_assistant(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
|
||||
if (!this->send_voice_assistant_get_configuration_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||
this->voice_assistant_set_configuration(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
|
||||
this->alarm_control_panel_command(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { this->zwave_proxy_frame(msg); }
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
void APIServerConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
this->infrared_rf_transmit_raw_timings(msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements for messages
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||
case DisconnectRequest::MESSAGE_TYPE: // No setup required
|
||||
case PingRequest::MESSAGE_TYPE: // No setup required
|
||||
break; // Skip all checks for these messages
|
||||
case DeviceInfoRequest::MESSAGE_TYPE: // Connection setup only
|
||||
if (!this->check_connection_setup_()) {
|
||||
return; // Connection not setup
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// All other messages require authentication (which includes connection check)
|
||||
if (!this->check_authenticated_()) {
|
||||
return; // Authentication failed
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// Call base implementation to process the message
|
||||
APIServerConnectionBase::read_message(msg_size, msg_type, msg_data);
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -14,6 +14,7 @@ class APIServerConnectionBase : public ProtoService {
|
||||
protected:
|
||||
void log_send_message_(const char *name, const char *dump);
|
||||
void log_receive_message_(const LogString *name, const ProtoMessage &msg);
|
||||
void log_receive_message_(const LogString *name);
|
||||
|
||||
public:
|
||||
#endif
|
||||
@@ -23,20 +24,20 @@ class APIServerConnectionBase : public ProtoService {
|
||||
DumpBuffer dump_buf;
|
||||
this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
|
||||
#endif
|
||||
return this->send_message_(msg, message_type);
|
||||
return this->send_message_impl(msg, message_type);
|
||||
}
|
||||
|
||||
virtual void on_hello_request(const HelloRequest &value){};
|
||||
|
||||
virtual void on_disconnect_request(const DisconnectRequest &value){};
|
||||
virtual void on_disconnect_response(const DisconnectResponse &value){};
|
||||
virtual void on_ping_request(const PingRequest &value){};
|
||||
virtual void on_ping_response(const PingResponse &value){};
|
||||
virtual void on_device_info_request(const DeviceInfoRequest &value){};
|
||||
virtual void on_disconnect_request(){};
|
||||
virtual void on_disconnect_response(){};
|
||||
virtual void on_ping_request(){};
|
||||
virtual void on_ping_response(){};
|
||||
virtual void on_device_info_request(){};
|
||||
|
||||
virtual void on_list_entities_request(const ListEntitiesRequest &value){};
|
||||
virtual void on_list_entities_request(){};
|
||||
|
||||
virtual void on_subscribe_states_request(const SubscribeStatesRequest &value){};
|
||||
virtual void on_subscribe_states_request(){};
|
||||
|
||||
#ifdef USE_COVER
|
||||
virtual void on_cover_command_request(const CoverCommandRequest &value){};
|
||||
@@ -61,14 +62,14 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
||||
virtual void on_subscribe_homeassistant_services_request(){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
||||
virtual void on_subscribe_home_assistant_states_request(){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
@@ -147,12 +148,11 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &value){};
|
||||
virtual void on_subscribe_bluetooth_connections_free_request(){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_unsubscribe_bluetooth_le_advertisements_request(
|
||||
const UnsubscribeBluetoothLEAdvertisementsRequest &value){};
|
||||
virtual void on_unsubscribe_bluetooth_le_advertisements_request(){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
@@ -228,266 +228,4 @@ class APIServerConnectionBase : public ProtoService {
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
class APIServerConnection : public APIServerConnectionBase {
|
||||
public:
|
||||
virtual bool send_hello_response(const HelloRequest &msg) = 0;
|
||||
virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
|
||||
virtual bool send_ping_response(const PingRequest &msg) = 0;
|
||||
virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
|
||||
virtual void list_entities(const ListEntitiesRequest &msg) = 0;
|
||||
virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
|
||||
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
virtual bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
virtual void button_command(const ButtonCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
virtual void camera_image(const CameraImageRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
virtual void climate_command(const ClimateCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
virtual void cover_command(const CoverCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
virtual void date_command(const DateCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
virtual void datetime_command(const DateTimeCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
virtual void fan_command(const FanCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
virtual void light_command(const LightCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
virtual void lock_command(const LockCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
virtual void media_player_command(const MediaPlayerCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
virtual void number_command(const NumberCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
virtual void select_command(const SelectCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
virtual void siren_command(const SirenCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
virtual void switch_command(const SwitchCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
virtual void text_command(const TextCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
virtual void time_command(const TimeCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
virtual void update_command(const UpdateCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
virtual void valve_command(const ValveCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_device_request(const BluetoothDeviceRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_get_services(const BluetoothGATTGetServicesRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_read(const BluetoothGATTReadRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_write(const BluetoothGATTWriteRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_read_descriptor(const BluetoothGATTReadDescriptorRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_write_descriptor(const BluetoothGATTWriteDescriptorRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual bool send_subscribe_bluetooth_connections_free_response(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual bool send_voice_assistant_get_configuration_response(const VoiceAssistantConfigurationRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
virtual void voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void zwave_proxy_frame(const ZWaveProxyFrame &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
virtual void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) = 0;
|
||||
#endif
|
||||
protected:
|
||||
void on_hello_request(const HelloRequest &msg) override;
|
||||
void on_disconnect_request(const DisconnectRequest &msg) override;
|
||||
void on_ping_request(const PingRequest &msg) override;
|
||||
void on_device_info_request(const DeviceInfoRequest &msg) override;
|
||||
void on_list_entities_request(const ListEntitiesRequest &msg) override;
|
||||
void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BUTTON
|
||||
void on_button_command_request(const ButtonCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void on_camera_image_request(const CameraImageRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_CLIMATE
|
||||
void on_climate_command_request(const ClimateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_COVER
|
||||
void on_cover_command_request(const CoverCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATE
|
||||
void on_date_command_request(const DateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_DATETIME
|
||||
void on_date_time_command_request(const DateTimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_FAN
|
||||
void on_fan_command_request(const FanCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LIGHT
|
||||
void on_light_command_request(const LightCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_LOCK
|
||||
void on_lock_command_request(const LockCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_command_request(const MediaPlayerCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_NUMBER
|
||||
void on_number_command_request(const NumberCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SELECT
|
||||
void on_select_command_request(const SelectCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SIREN
|
||||
void on_siren_command_request(const SirenCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_SWITCH
|
||||
void on_switch_command_request(const SwitchCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_TEXT
|
||||
void on_text_command_request(const TextCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_DATETIME_TIME
|
||||
void on_time_command_request(const TimeCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_UPDATE
|
||||
void on_update_command_request(const UpdateCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VALVE
|
||||
void on_valve_command_request(const ValveCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_subscribe_bluetooth_le_advertisements_request(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_device_request(const BluetoothDeviceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_subscribe_bluetooth_connections_free_request(const SubscribeBluetoothConnectionsFreeRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_unsubscribe_bluetooth_le_advertisements_request(
|
||||
const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
void on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) override;
|
||||
#endif
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) override;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
#endif
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -192,11 +192,15 @@ void APIServer::loop() {
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before removal for the trigger
|
||||
// Save client info before closing socket and removal for the trigger
|
||||
char peername_buf[socket::SOCKADDR_STR_LEN];
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername());
|
||||
std::string client_peername(client->get_peername_to(peername_buf));
|
||||
#endif
|
||||
|
||||
// Close socket now (was deferred from on_fatal_error to allow getpeername)
|
||||
client->helper_->close();
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
std::swap(this->clients_[client_index], this->clients_.back());
|
||||
@@ -211,7 +215,7 @@ void APIServer::loop() {
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_->trigger(client_name, client_peername);
|
||||
this->client_disconnected_trigger_.trigger(client_name, client_peername);
|
||||
#endif
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
|
||||
@@ -227,12 +227,10 @@ class APIServer : public Component,
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *get_client_connected_trigger() const { return this->client_connected_trigger_; }
|
||||
Trigger<std::string, std::string> *get_client_connected_trigger() { return &this->client_connected_trigger_; }
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *get_client_disconnected_trigger() const {
|
||||
return this->client_disconnected_trigger_;
|
||||
}
|
||||
Trigger<std::string, std::string> *get_client_disconnected_trigger() { return &this->client_disconnected_trigger_; }
|
||||
#endif
|
||||
|
||||
protected:
|
||||
@@ -253,10 +251,10 @@ class APIServer : public Component,
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>();
|
||||
Trigger<std::string, std::string> client_connected_trigger_;
|
||||
#endif
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>();
|
||||
Trigger<std::string, std::string> client_disconnected_trigger_;
|
||||
#endif
|
||||
|
||||
// 4-byte aligned types
|
||||
|
||||
@@ -25,7 +25,9 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
|
||||
private:
|
||||
// Helper to convert value to string - handles the case where value is already a string
|
||||
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
|
||||
template<typename T> static std::string value_to_string(T &&val) {
|
||||
return to_string(std::forward<T>(val)); // NOLINT
|
||||
}
|
||||
|
||||
// Overloads for string types - needed because std::to_string doesn't support them
|
||||
static std::string value_to_string(char *val) {
|
||||
@@ -136,12 +138,10 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
void set_wants_response() { this->flags_.wants_response = true; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
|
||||
return this->success_trigger_with_response_;
|
||||
}
|
||||
Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() { return &this->success_trigger_with_response_; }
|
||||
#endif
|
||||
Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
|
||||
Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
|
||||
Trigger<Ts...> *get_success_trigger() { return &this->success_trigger_; }
|
||||
Trigger<std::string, Ts...> *get_error_trigger() { return &this->error_trigger_; }
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
@@ -187,14 +187,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
if (response.is_success()) {
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
if (this->flags_.wants_response) {
|
||||
this->success_trigger_with_response_->trigger(response.get_json(), args...);
|
||||
this->success_trigger_with_response_.trigger(response.get_json(), args...);
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
this->success_trigger_->trigger(args...);
|
||||
this->success_trigger_.trigger(args...);
|
||||
}
|
||||
} else {
|
||||
this->error_trigger_->trigger(response.get_error_message(), args...);
|
||||
this->error_trigger_.trigger(response.get_error_message(), args...);
|
||||
}
|
||||
},
|
||||
captured_args);
|
||||
@@ -251,10 +251,10 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
TemplatableStringValue<Ts...> response_template_{""};
|
||||
Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>();
|
||||
Trigger<JsonObjectConst, Ts...> success_trigger_with_response_;
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
|
||||
Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
|
||||
Trigger<Ts...> success_trigger_;
|
||||
Trigger<std::string, Ts...> error_trigger_;
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
|
||||
struct Flags {
|
||||
|
||||
@@ -112,8 +112,12 @@ class ProtoVarInt {
|
||||
uint64_t result = buffer[0] & 0x7F;
|
||||
uint8_t bitpos = 7;
|
||||
|
||||
// A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings
|
||||
// to avoid undefined behavior from shifting uint64_t by >= 64 bits.
|
||||
uint32_t max_len = std::min(len, uint32_t(10));
|
||||
|
||||
// Start from the second byte since we've already processed the first
|
||||
for (uint32_t i = 1; i < len; i++) {
|
||||
for (uint32_t i = 1; i < max_len; i++) {
|
||||
uint8_t val = buffer[i];
|
||||
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
|
||||
bitpos += 7;
|
||||
@@ -402,6 +406,20 @@ class DumpBuffer {
|
||||
const char *c_str() const { return buf_; }
|
||||
size_t size() const { return pos_; }
|
||||
|
||||
/// Get writable buffer pointer for use with buf_append_printf
|
||||
char *data() { return buf_; }
|
||||
/// Get current position for use with buf_append_printf
|
||||
size_t pos() const { return pos_; }
|
||||
/// Update position after buf_append_printf call
|
||||
void set_pos(size_t pos) {
|
||||
if (pos >= CAPACITY) {
|
||||
pos_ = CAPACITY - 1;
|
||||
} else {
|
||||
pos_ = pos;
|
||||
}
|
||||
buf_[pos_] = '\0';
|
||||
}
|
||||
|
||||
private:
|
||||
void append_impl_(const char *str, size_t len) {
|
||||
size_t space = CAPACITY - 1 - pos_;
|
||||
@@ -943,32 +961,16 @@ class ProtoService {
|
||||
virtual bool is_connection_setup() = 0;
|
||||
virtual void on_fatal_error() = 0;
|
||||
virtual void on_no_setup_connection() = 0;
|
||||
/**
|
||||
* Create a buffer with a reserved size.
|
||||
* @param reserve_size The number of bytes to pre-allocate in the buffer. This is a hint
|
||||
* to optimize memory usage and avoid reallocations during encoding.
|
||||
* Implementations should aim to allocate at least this size.
|
||||
* @return A ProtoWriteBuffer object with the reserved size.
|
||||
*/
|
||||
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
|
||||
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
|
||||
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0;
|
||||
|
||||
// Optimized method that pre-allocates buffer based on message size
|
||||
bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
|
||||
ProtoSize size;
|
||||
msg.calculate_size(size);
|
||||
uint32_t msg_size = size.get_size();
|
||||
|
||||
// Create a pre-sized buffer
|
||||
auto buffer = this->create_buffer(msg_size);
|
||||
|
||||
// Encode message into the buffer
|
||||
msg.encode(buffer);
|
||||
|
||||
// Send the buffer
|
||||
return this->send_buffer(buffer, message_type);
|
||||
}
|
||||
/**
|
||||
* Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending.
|
||||
* This is the implementation method - callers should use send_message() which adds logging.
|
||||
* @param msg The protobuf message to send.
|
||||
* @param message_type The message type identifier.
|
||||
* @return True if the message was sent successfully, false otherwise.
|
||||
*/
|
||||
virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0;
|
||||
|
||||
// Authentication helper methods
|
||||
inline bool check_connection_setup_() {
|
||||
|
||||
@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
// Build and send JSON response
|
||||
json::JsonBuilder builder;
|
||||
this->json_builder_(x..., builder.root());
|
||||
std::string json_str = builder.serialize();
|
||||
auto json_buf = builder.serialize();
|
||||
this->parent_->send_action_response(call_id, success, StringRef(error_message),
|
||||
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
|
||||
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -10,7 +10,6 @@ class AQISensor : public sensor::Sensor, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; }
|
||||
void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; }
|
||||
|
||||
@@ -41,8 +41,6 @@ void AS3935Component::dump_config() {
|
||||
#endif
|
||||
}
|
||||
|
||||
float AS3935Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void AS3935Component::loop() {
|
||||
if (!this->irq_pin_->digital_read())
|
||||
return;
|
||||
|
||||
@@ -74,7 +74,6 @@ class AS3935Component : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void loop() override;
|
||||
|
||||
void set_irq_pin(GPIOPin *irq_pin) { irq_pin_ = irq_pin; }
|
||||
|
||||
@@ -22,8 +22,6 @@ static const uint8_t REGISTER_STATUS = 0x0B; // 8 bytes / R
|
||||
static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R
|
||||
static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
|
||||
|
||||
float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void AS5600Sensor::dump_config() {
|
||||
LOG_SENSOR("", "AS5600 Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Out of Range Mode: %u", this->out_of_range_mode_);
|
||||
|
||||
@@ -14,7 +14,6 @@ class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>,
|
||||
public:
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; }
|
||||
void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; }
|
||||
|
||||
@@ -58,8 +58,6 @@ void AS7341Component::dump_config() {
|
||||
LOG_SENSOR(" ", "NIR", this->nir_);
|
||||
}
|
||||
|
||||
float AS7341Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void AS7341Component::update() {
|
||||
this->read_channels(this->channel_readings_);
|
||||
|
||||
|
||||
@@ -78,7 +78,6 @@ class AS7341Component : public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
void set_f1_sensor(sensor::Sensor *f1_sensor) { this->f1_ = f1_sensor; }
|
||||
|
||||
@@ -38,8 +38,10 @@ async def to_code(config):
|
||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||
elif CORE.is_rp2040:
|
||||
# https://github.com/khoih-prog/AsyncTCP_RP2040W
|
||||
cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0")
|
||||
# https://github.com/ayushsharma82/RPAsyncTCP
|
||||
# RPAsyncTCP is a drop-in replacement for AsyncTCP_RP2040W with better
|
||||
# ESPAsyncWebServer compatibility
|
||||
cg.add_library("ayushsharma82/RPAsyncTCP", "1.3.2")
|
||||
# Other platforms (host, etc) use socket-based implementation
|
||||
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
|
||||
#include <ESPAsyncTCP.h>
|
||||
#elif defined(USE_RP2040)
|
||||
// Use AsyncTCP_RP2040W library for RP2040
|
||||
#include <AsyncTCP_RP2040W.h>
|
||||
// Use RPAsyncTCP library for RP2040
|
||||
#include <RPAsyncTCP.h>
|
||||
#else
|
||||
// Use socket-based implementation for other platforms
|
||||
#include "async_tcp_socket.h"
|
||||
|
||||
@@ -146,7 +146,6 @@ void ATM90E26Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Active Reverse Energy A", this->reverse_active_energy_sensor_);
|
||||
LOG_SENSOR(" ", "Frequency", this->freq_sensor_);
|
||||
}
|
||||
float ATM90E26Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
uint16_t ATM90E26Component::read16_(uint8_t a_register) {
|
||||
uint8_t data[2];
|
||||
|
||||
@@ -13,7 +13,6 @@ class ATM90E26Component : public PollingComponent,
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
void set_voltage_sensor(sensor::Sensor *obj) { this->voltage_sensor_ = obj; }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
from esphome.components.esp32 import add_idf_component, include_builtin_idf_component
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
|
||||
import esphome.final_validate as fv
|
||||
@@ -166,6 +166,9 @@ def final_validate_audio_schema(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
|
||||
include_builtin_idf_component("esp_http_client")
|
||||
|
||||
add_idf_component(
|
||||
name="esphome/esp-audio-libs",
|
||||
ref="2.0.3",
|
||||
|
||||
@@ -6,8 +6,7 @@ namespace bang_bang {
|
||||
|
||||
static const char *const TAG = "bang_bang.climate";
|
||||
|
||||
BangBangClimate::BangBangClimate()
|
||||
: idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
|
||||
BangBangClimate::BangBangClimate() = default;
|
||||
|
||||
void BangBangClimate::setup() {
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
@@ -160,13 +159,13 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
|
||||
switch (action) {
|
||||
case climate::CLIMATE_ACTION_OFF:
|
||||
case climate::CLIMATE_ACTION_IDLE:
|
||||
trig = this->idle_trigger_;
|
||||
trig = &this->idle_trigger_;
|
||||
break;
|
||||
case climate::CLIMATE_ACTION_COOLING:
|
||||
trig = this->cool_trigger_;
|
||||
trig = &this->cool_trigger_;
|
||||
break;
|
||||
case climate::CLIMATE_ACTION_HEATING:
|
||||
trig = this->heat_trigger_;
|
||||
trig = &this->heat_trigger_;
|
||||
break;
|
||||
default:
|
||||
trig = nullptr;
|
||||
@@ -204,9 +203,9 @@ void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &awa
|
||||
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
|
||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
|
||||
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_idle_trigger() { return &this->idle_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_cool_trigger() { return &this->cool_trigger_; }
|
||||
Trigger<> *BangBangClimate::get_heat_trigger() { return &this->heat_trigger_; }
|
||||
|
||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
|
||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
|
||||
|
||||
@@ -30,9 +30,9 @@ class BangBangClimate : public climate::Climate, public Component {
|
||||
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
|
||||
void set_away_config(const BangBangClimateTargetTempConfig &away_config);
|
||||
|
||||
Trigger<> *get_idle_trigger() const;
|
||||
Trigger<> *get_cool_trigger() const;
|
||||
Trigger<> *get_heat_trigger() const;
|
||||
Trigger<> *get_idle_trigger();
|
||||
Trigger<> *get_cool_trigger();
|
||||
Trigger<> *get_heat_trigger();
|
||||
|
||||
protected:
|
||||
/// Override control to change settings of the climate device.
|
||||
@@ -57,17 +57,13 @@ class BangBangClimate : public climate::Climate, public Component {
|
||||
*
|
||||
* In idle mode, the controller is assumed to have both heating and cooling disabled.
|
||||
*/
|
||||
Trigger<> *idle_trigger_{nullptr};
|
||||
Trigger<> idle_trigger_;
|
||||
/** The trigger to call when the controller should switch to cooling mode.
|
||||
*/
|
||||
Trigger<> *cool_trigger_{nullptr};
|
||||
Trigger<> cool_trigger_;
|
||||
/** The trigger to call when the controller should switch to heating mode.
|
||||
*
|
||||
* A null value for this attribute means that the controller has no heating action
|
||||
* For example window blinds, where only cooling (blinds closed) and not-cooling
|
||||
* (blinds open) is possible.
|
||||
*/
|
||||
Trigger<> *heat_trigger_{nullptr};
|
||||
Trigger<> heat_trigger_;
|
||||
/** A reference to the trigger that was previously active.
|
||||
*
|
||||
* This is so that the previous trigger can be stopped before enabling a new one.
|
||||
|
||||
@@ -265,6 +265,4 @@ void BH1750Sensor::fail_and_reset_() {
|
||||
this->state_ = IDLE;
|
||||
}
|
||||
|
||||
float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace esphome::bh1750
|
||||
|
||||
@@ -21,7 +21,6 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c:
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
void loop() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
// State machine states
|
||||
|
||||
@@ -14,10 +14,7 @@ void log_binary_sensor(const char *tag, const char *prefix, const char *type, Bi
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
||||
|
||||
if (!obj->get_device_class_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Device Class: '%s'", prefix, obj->get_device_class_ref().c_str());
|
||||
}
|
||||
LOG_ENTITY_DEVICE_CLASS(tag, prefix, *obj);
|
||||
}
|
||||
|
||||
void BinarySensor::publish_state(bool new_state) {
|
||||
|
||||
@@ -159,6 +159,10 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"cbu": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -227,6 +231,10 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7231t-qfn32-tuya": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -295,6 +303,10 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7231n-qfn32-tuya": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -485,8 +497,7 @@ BK72XX_BOARD_PINS = {
|
||||
},
|
||||
"cb3s": {
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA_0": 21,
|
||||
"WIRE1_SDA_1": 21,
|
||||
"WIRE1_SDA": 21,
|
||||
"SERIAL1_RX": 10,
|
||||
"SERIAL1_TX": 11,
|
||||
"SERIAL2_TX": 0,
|
||||
@@ -647,6 +658,10 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"generic-bk7252": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE1_SCL": 20,
|
||||
"WIRE1_SDA": 21,
|
||||
"WIRE2_SCL": 0,
|
||||
@@ -1096,6 +1111,10 @@ BK72XX_BOARD_PINS = {
|
||||
"A0": 23,
|
||||
},
|
||||
"cb3se": {
|
||||
"SPI0_CS": 15,
|
||||
"SPI0_MISO": 17,
|
||||
"SPI0_MOSI": 16,
|
||||
"SPI0_SCK": 14,
|
||||
"WIRE2_SCL": 0,
|
||||
"WIRE2_SDA": 1,
|
||||
"SERIAL1_RX": 10,
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number";
|
||||
void CalibrationNumber::setup() {
|
||||
float value = 0.0f;
|
||||
if (this->restore_value_) {
|
||||
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
|
||||
this->pref_ = this->make_entity_preference<float>();
|
||||
if (!this->pref_.load(&value)) {
|
||||
value = 0.0f;
|
||||
}
|
||||
|
||||
@@ -199,7 +199,6 @@ void BME280Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->humidity_oversampling_));
|
||||
}
|
||||
float BME280Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; }
|
||||
|
||||
|
||||
@@ -76,7 +76,6 @@ class BME280Component : public PollingComponent {
|
||||
// (In most use cases you won't need these)
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -233,8 +233,6 @@ void BME680Component::dump_config() {
|
||||
}
|
||||
}
|
||||
|
||||
float BME680Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void BME680Component::update() {
|
||||
uint8_t meas_control = 0; // No need to fetch, we're setting all fields
|
||||
meas_control |= (this->temperature_oversampling_ & 0b111) << 5;
|
||||
|
||||
@@ -99,7 +99,6 @@ class BME680Component : public PollingComponent, public i2c::I2CDevice {
|
||||
// (In most use cases you won't need these)
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -89,8 +89,9 @@ async def to_code(config):
|
||||
var.set_state_save_interval(config[CONF_STATE_SAVE_INTERVAL].total_milliseconds)
|
||||
)
|
||||
|
||||
# Although this component does not use SPI, the BSEC library requires the SPI library
|
||||
# Although this component does not use SPI/Wire directly, the BSEC library requires them
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library("Wire", None)
|
||||
|
||||
cg.add_define("USE_BSEC")
|
||||
cg.add_library("boschsensortec/BSEC Software Library", "1.6.1480")
|
||||
|
||||
@@ -181,8 +181,6 @@ void BME680BSECComponent::dump_config() {
|
||||
LOG_SENSOR(" ", "Breath VOC Equivalent", this->breath_voc_equivalent_sensor_);
|
||||
}
|
||||
|
||||
float BME680BSECComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void BME680BSECComponent::loop() {
|
||||
this->run_();
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ class BME680BSECComponent : public Component, public i2c::I2CDevice {
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -106,8 +106,6 @@ void BME68xBSEC2Component::dump_config() {
|
||||
#endif
|
||||
}
|
||||
|
||||
float BME68xBSEC2Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void BME68xBSEC2Component::loop() {
|
||||
this->run_();
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ class BME68xBSEC2Component : public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void loop() override;
|
||||
|
||||
void set_algorithm_output(AlgorithmOutput algorithm_output) { this->algorithm_output_ = algorithm_output; }
|
||||
|
||||
@@ -263,7 +263,6 @@ void BMI160Component::update() {
|
||||
|
||||
this->status_clear_warning();
|
||||
}
|
||||
float BMI160Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace bmi160
|
||||
} // namespace esphome
|
||||
|
||||
@@ -14,8 +14,6 @@ class BMI160Component : public PollingComponent, public i2c::I2CDevice {
|
||||
|
||||
void update() override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
void set_accel_x_sensor(sensor::Sensor *accel_x_sensor) { accel_x_sensor_ = accel_x_sensor; }
|
||||
void set_accel_y_sensor(sensor::Sensor *accel_y_sensor) { accel_y_sensor_ = accel_y_sensor; }
|
||||
void set_accel_z_sensor(sensor::Sensor *accel_z_sensor) { accel_z_sensor_ = accel_z_sensor; }
|
||||
|
||||
@@ -131,7 +131,6 @@ bool BMP085Component::set_mode_(uint8_t mode) {
|
||||
ESP_LOGV(TAG, "Setting mode to 0x%02X", mode);
|
||||
return this->write_byte(BMP085_REGISTER_CONTROL, mode);
|
||||
}
|
||||
float BMP085Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
} // namespace bmp085
|
||||
} // namespace esphome
|
||||
|
||||
@@ -18,8 +18,6 @@ class BMP085Component : public PollingComponent, public i2c::I2CDevice {
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
float get_setup_priority() const override;
|
||||
|
||||
protected:
|
||||
struct CalibrationData {
|
||||
int16_t ac1, ac2, ac3;
|
||||
|
||||
@@ -148,7 +148,6 @@ void BMP280Component::dump_config() {
|
||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Oversampling: %s", oversampling_to_str(this->pressure_oversampling_));
|
||||
}
|
||||
float BMP280Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
inline uint8_t oversampling_to_time(BMP280Oversampling over_sampling) { return (1 << uint8_t(over_sampling)) >> 1; }
|
||||
|
||||
|
||||
@@ -64,7 +64,6 @@ class BMP280Component : public PollingComponent {
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
*/
|
||||
|
||||
#include "bmp3xx_base.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
#include <cinttypes>
|
||||
|
||||
namespace esphome {
|
||||
@@ -26,46 +27,18 @@ static const LogString *chip_type_to_str(uint8_t chip_type) {
|
||||
}
|
||||
}
|
||||
|
||||
// Oversampling strings indexed by Oversampling enum (0-5): NONE, X2, X4, X8, X16, X32
|
||||
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "");
|
||||
|
||||
static const LogString *oversampling_to_str(Oversampling oversampling) {
|
||||
switch (oversampling) {
|
||||
case Oversampling::OVERSAMPLING_NONE:
|
||||
return LOG_STR("None");
|
||||
case Oversampling::OVERSAMPLING_X2:
|
||||
return LOG_STR("2x");
|
||||
case Oversampling::OVERSAMPLING_X4:
|
||||
return LOG_STR("4x");
|
||||
case Oversampling::OVERSAMPLING_X8:
|
||||
return LOG_STR("8x");
|
||||
case Oversampling::OVERSAMPLING_X16:
|
||||
return LOG_STR("16x");
|
||||
case Oversampling::OVERSAMPLING_X32:
|
||||
return LOG_STR("32x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
|
||||
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
|
||||
|
||||
static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||
switch (filter) {
|
||||
case IIRFilter::IIR_FILTER_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case IIRFilter::IIR_FILTER_2:
|
||||
return LOG_STR("2x");
|
||||
case IIRFilter::IIR_FILTER_4:
|
||||
return LOG_STR("4x");
|
||||
case IIRFilter::IIR_FILTER_8:
|
||||
return LOG_STR("8x");
|
||||
case IIRFilter::IIR_FILTER_16:
|
||||
return LOG_STR("16x");
|
||||
case IIRFilter::IIR_FILTER_32:
|
||||
return LOG_STR("32x");
|
||||
case IIRFilter::IIR_FILTER_64:
|
||||
return LOG_STR("64x");
|
||||
case IIRFilter::IIR_FILTER_128:
|
||||
return LOG_STR("128x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
void BMP3XXComponent::setup() {
|
||||
@@ -179,7 +152,6 @@ void BMP3XXComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Oversampling: %s", LOG_STR_ARG(oversampling_to_str(this->pressure_oversampling_)));
|
||||
}
|
||||
}
|
||||
float BMP3XXComponent::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
inline uint8_t oversampling_to_time(Oversampling over_sampling) { return (1 << uint8_t(over_sampling)); }
|
||||
|
||||
|
||||
@@ -73,7 +73,6 @@ class BMP3XXComponent : public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void update() override;
|
||||
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
|
||||
|
||||
@@ -1,164 +1,5 @@
|
||||
import math
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_IIR_FILTER,
|
||||
CONF_OVERSAMPLING,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PASCAL,
|
||||
|
||||
CONFIG_SCHEMA = cv.invalid(
|
||||
"The bmp581 sensor component has been renamed to bmp581_i2c."
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@kahrendt"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
bmp581_ns = cg.esphome_ns.namespace("bmp581")
|
||||
|
||||
Oversampling = bmp581_ns.enum("Oversampling")
|
||||
OVERSAMPLING_OPTIONS = {
|
||||
"NONE": Oversampling.OVERSAMPLING_NONE,
|
||||
"2X": Oversampling.OVERSAMPLING_X2,
|
||||
"4X": Oversampling.OVERSAMPLING_X4,
|
||||
"8X": Oversampling.OVERSAMPLING_X8,
|
||||
"16X": Oversampling.OVERSAMPLING_X16,
|
||||
"32X": Oversampling.OVERSAMPLING_X32,
|
||||
"64X": Oversampling.OVERSAMPLING_X64,
|
||||
"128X": Oversampling.OVERSAMPLING_X128,
|
||||
}
|
||||
|
||||
IIRFilter = bmp581_ns.enum("IIRFilter")
|
||||
IIR_FILTER_OPTIONS = {
|
||||
"OFF": IIRFilter.IIR_FILTER_OFF,
|
||||
"2X": IIRFilter.IIR_FILTER_2,
|
||||
"4X": IIRFilter.IIR_FILTER_4,
|
||||
"8X": IIRFilter.IIR_FILTER_8,
|
||||
"16X": IIRFilter.IIR_FILTER_16,
|
||||
"32X": IIRFilter.IIR_FILTER_32,
|
||||
"64X": IIRFilter.IIR_FILTER_64,
|
||||
"128X": IIRFilter.IIR_FILTER_128,
|
||||
}
|
||||
|
||||
BMP581Component = bmp581_ns.class_(
|
||||
"BMP581Component", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
|
||||
def compute_measurement_conversion_time(config):
|
||||
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
|
||||
# - returns a rounded up time in ms
|
||||
|
||||
# Page 12 of datasheet
|
||||
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
|
||||
"NONE": 1.0,
|
||||
"2X": 1.7,
|
||||
"4X": 2.9,
|
||||
"8X": 5.4,
|
||||
"16X": 10.4,
|
||||
"32X": 20.4,
|
||||
"64X": 40.4,
|
||||
"128X": 80.4,
|
||||
}
|
||||
|
||||
# Page 12 of datasheet
|
||||
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
|
||||
"NONE": 1.0,
|
||||
"2X": 1.1,
|
||||
"4X": 1.5,
|
||||
"8X": 2.1,
|
||||
"16X": 3.3,
|
||||
"32X": 5.8,
|
||||
"64X": 10.8,
|
||||
"128X": 20.8,
|
||||
}
|
||||
|
||||
pressure_conversion_time = (
|
||||
0.0 # No conversion time necessary without a pressure sensor
|
||||
)
|
||||
if pressure_config := config.get(CONF_PRESSURE):
|
||||
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
|
||||
pressure_config.get(CONF_OVERSAMPLING)
|
||||
]
|
||||
|
||||
temperature_conversion_time = (
|
||||
1.0 # BMP581 always samples the temperature even if only reading pressure
|
||||
)
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
|
||||
temperature_config.get(CONF_OVERSAMPLING)
|
||||
]
|
||||
|
||||
# Datasheet indicates a 5% possible error in each conversion time listed
|
||||
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BMP581Component),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
||||
IIR_FILTER_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PASCAL,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
||||
IIR_FILTER_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x46))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
cg.add(
|
||||
var.set_temperature_oversampling_config(
|
||||
temperature_config[CONF_OVERSAMPLING]
|
||||
)
|
||||
)
|
||||
cg.add(
|
||||
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
|
||||
)
|
||||
|
||||
if pressure_config := config.get(CONF_PRESSURE):
|
||||
sens = await sensor.new_sensor(pressure_config)
|
||||
cg.add(var.set_pressure_sensor(sens))
|
||||
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
|
||||
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
|
||||
|
||||
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))
|
||||
|
||||
157
esphome/components/bmp581_base/__init__.py
Normal file
157
esphome/components/bmp581_base/__init__.py
Normal file
@@ -0,0 +1,157 @@
|
||||
import math
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_IIR_FILTER,
|
||||
CONF_OVERSAMPLING,
|
||||
CONF_PRESSURE,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_PASCAL,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
|
||||
|
||||
bmp581_ns = cg.esphome_ns.namespace("bmp581_base")
|
||||
|
||||
Oversampling = bmp581_ns.enum("Oversampling")
|
||||
OVERSAMPLING_OPTIONS = {
|
||||
"NONE": Oversampling.OVERSAMPLING_NONE,
|
||||
"2X": Oversampling.OVERSAMPLING_X2,
|
||||
"4X": Oversampling.OVERSAMPLING_X4,
|
||||
"8X": Oversampling.OVERSAMPLING_X8,
|
||||
"16X": Oversampling.OVERSAMPLING_X16,
|
||||
"32X": Oversampling.OVERSAMPLING_X32,
|
||||
"64X": Oversampling.OVERSAMPLING_X64,
|
||||
"128X": Oversampling.OVERSAMPLING_X128,
|
||||
}
|
||||
|
||||
IIRFilter = bmp581_ns.enum("IIRFilter")
|
||||
IIR_FILTER_OPTIONS = {
|
||||
"OFF": IIRFilter.IIR_FILTER_OFF,
|
||||
"2X": IIRFilter.IIR_FILTER_2,
|
||||
"4X": IIRFilter.IIR_FILTER_4,
|
||||
"8X": IIRFilter.IIR_FILTER_8,
|
||||
"16X": IIRFilter.IIR_FILTER_16,
|
||||
"32X": IIRFilter.IIR_FILTER_32,
|
||||
"64X": IIRFilter.IIR_FILTER_64,
|
||||
"128X": IIRFilter.IIR_FILTER_128,
|
||||
}
|
||||
|
||||
BMP581Component = bmp581_ns.class_("BMP581Component", cg.PollingComponent)
|
||||
|
||||
|
||||
def compute_measurement_conversion_time(config):
|
||||
# - adds up sensor conversion time based on temperature and pressure oversampling rates given in datasheet
|
||||
# - returns a rounded up time in ms
|
||||
|
||||
# Page 12 of datasheet
|
||||
PRESSURE_OVERSAMPLING_CONVERSION_TIMES = {
|
||||
"NONE": 1.0,
|
||||
"2X": 1.7,
|
||||
"4X": 2.9,
|
||||
"8X": 5.4,
|
||||
"16X": 10.4,
|
||||
"32X": 20.4,
|
||||
"64X": 40.4,
|
||||
"128X": 80.4,
|
||||
}
|
||||
|
||||
# Page 12 of datasheet
|
||||
TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES = {
|
||||
"NONE": 1.0,
|
||||
"2X": 1.1,
|
||||
"4X": 1.5,
|
||||
"8X": 2.1,
|
||||
"16X": 3.3,
|
||||
"32X": 5.8,
|
||||
"64X": 10.8,
|
||||
"128X": 20.8,
|
||||
}
|
||||
|
||||
pressure_conversion_time = (
|
||||
0.0 # No conversion time necessary without a pressure sensor
|
||||
)
|
||||
if pressure_config := config.get(CONF_PRESSURE):
|
||||
pressure_conversion_time = PRESSURE_OVERSAMPLING_CONVERSION_TIMES[
|
||||
pressure_config.get(CONF_OVERSAMPLING)
|
||||
]
|
||||
|
||||
temperature_conversion_time = (
|
||||
1.0 # BMP581 always samples the temperature even if only reading pressure
|
||||
)
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
temperature_conversion_time = TEMPERATURE_OVERSAMPLING_CONVERSION_TIMES[
|
||||
temperature_config.get(CONF_OVERSAMPLING)
|
||||
]
|
||||
|
||||
# Datasheet indicates a 5% possible error in each conversion time listed
|
||||
return math.ceil(1.05 * (pressure_conversion_time + temperature_conversion_time))
|
||||
|
||||
|
||||
CONFIG_SCHEMA_BASE = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BMP581Component),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=1,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="NONE"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
||||
IIR_FILTER_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PASCAL,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
).extend(
|
||||
{
|
||||
cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
|
||||
OVERSAMPLING_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
|
||||
IIR_FILTER_OPTIONS, upper=True
|
||||
),
|
||||
}
|
||||
),
|
||||
}
|
||||
).extend(cv.polling_component_schema("60s"))
|
||||
|
||||
|
||||
async def to_code_base(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
cg.add(
|
||||
var.set_temperature_oversampling_config(
|
||||
temperature_config[CONF_OVERSAMPLING]
|
||||
)
|
||||
)
|
||||
cg.add(
|
||||
var.set_temperature_iir_filter_config(temperature_config[CONF_IIR_FILTER])
|
||||
)
|
||||
|
||||
if pressure_config := config.get(CONF_PRESSURE):
|
||||
sens = await sensor.new_sensor(pressure_config)
|
||||
cg.add(var.set_pressure_sensor(sens))
|
||||
cg.add(var.set_pressure_oversampling_config(pressure_config[CONF_OVERSAMPLING]))
|
||||
cg.add(var.set_pressure_iir_filter_config(pressure_config[CONF_IIR_FILTER]))
|
||||
|
||||
cg.add(var.set_conversion_time(compute_measurement_conversion_time(config)))
|
||||
return var
|
||||
@@ -10,59 +10,27 @@
|
||||
* - All datasheet page references refer to Bosch Document Number BST-BMP581-DS004-04 (revision number 1.4)
|
||||
*/
|
||||
|
||||
#include "bmp581.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "bmp581_base.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/progmem.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bmp581 {
|
||||
namespace esphome::bmp581_base {
|
||||
|
||||
static const char *const TAG = "bmp581";
|
||||
|
||||
// Oversampling strings indexed by Oversampling enum (0-7): NONE, X2, X4, X8, X16, X32, X64, X128
|
||||
PROGMEM_STRING_TABLE(OversamplingStrings, "None", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
|
||||
|
||||
static const LogString *oversampling_to_str(Oversampling oversampling) {
|
||||
switch (oversampling) {
|
||||
case Oversampling::OVERSAMPLING_NONE:
|
||||
return LOG_STR("None");
|
||||
case Oversampling::OVERSAMPLING_X2:
|
||||
return LOG_STR("2x");
|
||||
case Oversampling::OVERSAMPLING_X4:
|
||||
return LOG_STR("4x");
|
||||
case Oversampling::OVERSAMPLING_X8:
|
||||
return LOG_STR("8x");
|
||||
case Oversampling::OVERSAMPLING_X16:
|
||||
return LOG_STR("16x");
|
||||
case Oversampling::OVERSAMPLING_X32:
|
||||
return LOG_STR("32x");
|
||||
case Oversampling::OVERSAMPLING_X64:
|
||||
return LOG_STR("64x");
|
||||
case Oversampling::OVERSAMPLING_X128:
|
||||
return LOG_STR("128x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
return OversamplingStrings::get_log_str(static_cast<uint8_t>(oversampling), OversamplingStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
// IIR filter strings indexed by IIRFilter enum (0-7): OFF, 2, 4, 8, 16, 32, 64, 128
|
||||
PROGMEM_STRING_TABLE(IIRFilterStrings, "OFF", "2x", "4x", "8x", "16x", "32x", "64x", "128x", "");
|
||||
|
||||
static const LogString *iir_filter_to_str(IIRFilter filter) {
|
||||
switch (filter) {
|
||||
case IIRFilter::IIR_FILTER_OFF:
|
||||
return LOG_STR("OFF");
|
||||
case IIRFilter::IIR_FILTER_2:
|
||||
return LOG_STR("2x");
|
||||
case IIRFilter::IIR_FILTER_4:
|
||||
return LOG_STR("4x");
|
||||
case IIRFilter::IIR_FILTER_8:
|
||||
return LOG_STR("8x");
|
||||
case IIRFilter::IIR_FILTER_16:
|
||||
return LOG_STR("16x");
|
||||
case IIRFilter::IIR_FILTER_32:
|
||||
return LOG_STR("32x");
|
||||
case IIRFilter::IIR_FILTER_64:
|
||||
return LOG_STR("64x");
|
||||
case IIRFilter::IIR_FILTER_128:
|
||||
return LOG_STR("128x");
|
||||
default:
|
||||
return LOG_STR("");
|
||||
}
|
||||
return IIRFilterStrings::get_log_str(static_cast<uint8_t>(filter), IIRFilterStrings::LAST_INDEX);
|
||||
}
|
||||
|
||||
void BMP581Component::dump_config() {
|
||||
@@ -91,7 +59,6 @@ void BMP581Component::dump_config() {
|
||||
break;
|
||||
}
|
||||
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " Measurement conversion time: %ums", this->conversion_time_);
|
||||
@@ -149,7 +116,7 @@ void BMP581Component::setup() {
|
||||
uint8_t chip_id;
|
||||
|
||||
// read chip id from sensor
|
||||
if (!this->read_byte(BMP581_CHIP_ID, &chip_id)) {
|
||||
if (!this->bmp_read_byte(BMP581_CHIP_ID, &chip_id)) {
|
||||
ESP_LOGE(TAG, "Read chip ID failed");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
@@ -172,7 +139,7 @@ void BMP581Component::setup() {
|
||||
// 3) Verify sensor status (check if NVM is okay) //
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
if (!this->read_byte(BMP581_STATUS, &this->status_.reg)) {
|
||||
if (!this->bmp_read_byte(BMP581_STATUS, &this->status_.reg)) {
|
||||
ESP_LOGE(TAG, "Failed to read status register");
|
||||
|
||||
this->error_code_ = ERROR_COMMUNICATION_FAILED;
|
||||
@@ -359,7 +326,7 @@ bool BMP581Component::check_data_readiness_() {
|
||||
|
||||
uint8_t status;
|
||||
|
||||
if (!this->read_byte(BMP581_INT_STATUS, &status)) {
|
||||
if (!this->bmp_read_byte(BMP581_INT_STATUS, &status)) {
|
||||
ESP_LOGE(TAG, "Failed to read interrupt status register");
|
||||
return false;
|
||||
}
|
||||
@@ -400,7 +367,7 @@ bool BMP581Component::prime_iir_filter_() {
|
||||
|
||||
// flush the IIR filter with forced measurements (we will only flush once)
|
||||
this->dsp_config_.bit.iir_flush_forced_en = true;
|
||||
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
|
||||
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
|
||||
ESP_LOGE(TAG, "Failed to write IIR source register");
|
||||
|
||||
return false;
|
||||
@@ -430,7 +397,7 @@ bool BMP581Component::prime_iir_filter_() {
|
||||
|
||||
// disable IIR filter flushings on future forced measurements
|
||||
this->dsp_config_.bit.iir_flush_forced_en = false;
|
||||
if (!this->write_byte(BMP581_DSP, this->dsp_config_.reg)) {
|
||||
if (!this->bmp_write_byte(BMP581_DSP, this->dsp_config_.reg)) {
|
||||
ESP_LOGE(TAG, "Failed to write IIR source register");
|
||||
|
||||
return false;
|
||||
@@ -454,7 +421,7 @@ bool BMP581Component::read_temperature_(float &temperature) {
|
||||
}
|
||||
|
||||
uint8_t data[3];
|
||||
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
|
||||
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 3)) {
|
||||
ESP_LOGW(TAG, "Failed to read measurement");
|
||||
this->status_set_warning();
|
||||
|
||||
@@ -483,7 +450,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float &
|
||||
}
|
||||
|
||||
uint8_t data[6];
|
||||
if (!this->read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
|
||||
if (!this->bmp_read_bytes(BMP581_MEASUREMENT_DATA, &data[0], 6)) {
|
||||
ESP_LOGW(TAG, "Failed to read measurement");
|
||||
this->status_set_warning();
|
||||
|
||||
@@ -507,7 +474,7 @@ bool BMP581Component::reset_() {
|
||||
// - returns the Power-On-Reboot interrupt status, which is asserted if successful
|
||||
|
||||
// writes reset command to BMP's command register
|
||||
if (!this->write_byte(BMP581_COMMAND, RESET_COMMAND)) {
|
||||
if (!this->bmp_write_byte(BMP581_COMMAND, RESET_COMMAND)) {
|
||||
ESP_LOGE(TAG, "Failed to write reset command");
|
||||
|
||||
return false;
|
||||
@@ -518,7 +485,7 @@ bool BMP581Component::reset_() {
|
||||
delay(3);
|
||||
|
||||
// read interrupt status register
|
||||
if (!this->read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
|
||||
if (!this->bmp_read_byte(BMP581_INT_STATUS, &this->int_status_.reg)) {
|
||||
ESP_LOGE(TAG, "Failed to read interrupt status register");
|
||||
|
||||
return false;
|
||||
@@ -562,7 +529,7 @@ bool BMP581Component::write_iir_settings_(IIRFilter temperature_iir, IIRFilter p
|
||||
// BMP581_DSP register and BMP581_DSP_IIR registers are successive
|
||||
// - allows us to write the IIR configuration with one command to both registers
|
||||
uint8_t register_data[2] = {this->dsp_config_.reg, this->iir_config_.reg};
|
||||
return this->write_bytes(BMP581_DSP, register_data, sizeof(register_data));
|
||||
return this->bmp_write_bytes(BMP581_DSP, register_data, sizeof(register_data));
|
||||
}
|
||||
|
||||
bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
|
||||
@@ -572,7 +539,7 @@ bool BMP581Component::write_interrupt_source_settings_(bool data_ready_enable) {
|
||||
this->int_source_.bit.drdy_data_reg_en = data_ready_enable;
|
||||
|
||||
// write interrupt source register
|
||||
return this->write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
|
||||
return this->bmp_write_byte(BMP581_INT_SOURCE, this->int_source_.reg);
|
||||
}
|
||||
|
||||
bool BMP581Component::write_oversampling_settings_(Oversampling temperature_oversampling,
|
||||
@@ -583,7 +550,7 @@ bool BMP581Component::write_oversampling_settings_(Oversampling temperature_over
|
||||
this->osr_config_.bit.osr_t = temperature_oversampling;
|
||||
this->osr_config_.bit.osr_p = pressure_oversampling;
|
||||
|
||||
return this->write_byte(BMP581_OSR, this->osr_config_.reg);
|
||||
return this->bmp_write_byte(BMP581_OSR, this->osr_config_.reg);
|
||||
}
|
||||
|
||||
bool BMP581Component::write_power_mode_(OperationMode mode) {
|
||||
@@ -593,8 +560,7 @@ bool BMP581Component::write_power_mode_(OperationMode mode) {
|
||||
this->odr_config_.bit.pwr_mode = mode;
|
||||
|
||||
// write odr register
|
||||
return this->write_byte(BMP581_ODR, this->odr_config_.reg);
|
||||
return this->bmp_write_byte(BMP581_ODR, this->odr_config_.reg);
|
||||
}
|
||||
|
||||
} // namespace bmp581
|
||||
} // namespace esphome
|
||||
} // namespace esphome::bmp581_base
|
||||
@@ -3,11 +3,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace bmp581 {
|
||||
namespace esphome::bmp581_base {
|
||||
|
||||
static const uint8_t BMP581_ASIC_ID = 0x50; // BMP581's ASIC chip ID (page 51 of datasheet)
|
||||
static const uint8_t RESET_COMMAND = 0xB6; // Soft reset command
|
||||
@@ -59,7 +57,7 @@ enum IIRFilter {
|
||||
IIR_FILTER_128 = 0x7
|
||||
};
|
||||
|
||||
class BMP581Component : public PollingComponent, public i2c::I2CDevice {
|
||||
class BMP581Component : public PollingComponent {
|
||||
public:
|
||||
void dump_config() override;
|
||||
|
||||
@@ -84,6 +82,11 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
|
||||
void set_conversion_time(uint8_t conversion_time) { this->conversion_time_ = conversion_time; }
|
||||
|
||||
protected:
|
||||
virtual bool bmp_read_byte(uint8_t a_register, uint8_t *data) = 0;
|
||||
virtual bool bmp_write_byte(uint8_t a_register, uint8_t data) = 0;
|
||||
virtual bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
|
||||
virtual bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
|
||||
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
|
||||
@@ -216,5 +219,4 @@ class BMP581Component : public PollingComponent, public i2c::I2CDevice {
|
||||
} odr_config_ = {.reg = 0};
|
||||
};
|
||||
|
||||
} // namespace bmp581
|
||||
} // namespace esphome
|
||||
} // namespace esphome::bmp581_base
|
||||
0
esphome/components/bmp581_i2c/__init__.py
Normal file
0
esphome/components/bmp581_i2c/__init__.py
Normal file
12
esphome/components/bmp581_i2c/bmp581_i2c.cpp
Normal file
12
esphome/components/bmp581_i2c/bmp581_i2c.cpp
Normal file
@@ -0,0 +1,12 @@
|
||||
#include "bmp581_i2c.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bmp581_i2c {
|
||||
|
||||
void BMP581I2CComponent::dump_config() {
|
||||
LOG_I2C_DEVICE(this);
|
||||
BMP581Component::dump_config();
|
||||
}
|
||||
|
||||
} // namespace esphome::bmp581_i2c
|
||||
24
esphome/components/bmp581_i2c/bmp581_i2c.h
Normal file
24
esphome/components/bmp581_i2c/bmp581_i2c.h
Normal file
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/bmp581_base/bmp581_base.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::bmp581_i2c {
|
||||
|
||||
static const char *const TAG = "bmp581_i2c.sensor";
|
||||
|
||||
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
|
||||
class BMP581I2CComponent : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
|
||||
public:
|
||||
bool bmp_read_byte(uint8_t a_register, uint8_t *data) override { return read_byte(a_register, data); }
|
||||
bool bmp_write_byte(uint8_t a_register, uint8_t data) override { return write_byte(a_register, data); }
|
||||
bool bmp_read_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
|
||||
return read_bytes(a_register, data, len);
|
||||
}
|
||||
bool bmp_write_bytes(uint8_t a_register, uint8_t *data, size_t len) override {
|
||||
return write_bytes(a_register, data, len);
|
||||
}
|
||||
void dump_config() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::bmp581_i2c
|
||||
23
esphome/components/bmp581_i2c/sensor.py
Normal file
23
esphome/components/bmp581_i2c/sensor.py
Normal file
@@ -0,0 +1,23 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
|
||||
from ..bmp581_base import CONFIG_SCHEMA_BASE, to_code_base
|
||||
|
||||
AUTO_LOAD = ["bmp581_base"]
|
||||
CODEOWNERS = ["@kahrendt", "@danielkent-net"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
bmp581_ns = cg.esphome_ns.namespace("bmp581_i2c")
|
||||
BMP581I2CComponent = bmp581_ns.class_(
|
||||
"BMP581I2CComponent", cg.PollingComponent, i2c.I2CDevice
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
|
||||
i2c.i2c_device_schema(default_address=0x46)
|
||||
).extend({cv.GenerateID(): cv.declare_id(BMP581I2CComponent)})
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await to_code_base(config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
@@ -12,10 +12,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o
|
||||
}
|
||||
|
||||
ESP_LOGCONFIG(tag, "%s%s '%s'", prefix, type, obj->get_name().c_str());
|
||||
|
||||
if (!obj->get_icon_ref().empty()) {
|
||||
ESP_LOGCONFIG(tag, "%s Icon: '%s'", prefix, obj->get_icon_ref().c_str());
|
||||
}
|
||||
LOG_ENTITY_ICON(tag, prefix, *obj);
|
||||
}
|
||||
|
||||
void Button::press() {
|
||||
|
||||
@@ -156,7 +156,7 @@ void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float
|
||||
for (auto &listener : this->listeners_) {
|
||||
listener->on_packet(packet, freq_offset, rssi, lqi);
|
||||
}
|
||||
this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi);
|
||||
this->packet_trigger_.trigger(packet, freq_offset, rssi, lqi);
|
||||
}
|
||||
|
||||
void CC1101Component::loop() {
|
||||
|
||||
@@ -79,7 +79,7 @@ class CC1101Component : public Component,
|
||||
// Packet mode operations
|
||||
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
|
||||
void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); }
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() { return &this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
uint16_t chip_id_{0};
|
||||
@@ -96,8 +96,7 @@ class CC1101Component : public Component,
|
||||
|
||||
// Packet handling
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
|
||||
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> packet_trigger_;
|
||||
std::vector<uint8_t> packet_;
|
||||
std::vector<CC1101Listener *> listeners_;
|
||||
|
||||
|
||||
@@ -7,8 +7,6 @@ namespace cd74hc4067 {
|
||||
|
||||
static const char *const TAG = "cd74hc4067";
|
||||
|
||||
float CD74HC4067Component::get_setup_priority() const { return setup_priority::DATA; }
|
||||
|
||||
void CD74HC4067Component::setup() {
|
||||
this->pin_s0_->setup();
|
||||
this->pin_s1_->setup();
|
||||
|
||||
@@ -13,7 +13,6 @@ class CD74HC4067Component : public Component {
|
||||
/// Set up the internal sensor array.
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
/// setting pin active by setting the right combination of the four multiplexer input pins
|
||||
void activate_pin(uint8_t pin);
|
||||
|
||||
103
esphome/components/ch423/__init__.py
Normal file
103
esphome/components/ch423/__init__.py
Normal file
@@ -0,0 +1,103 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
from esphome.components.i2c import I2CBus
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_I2C_ID,
|
||||
CONF_ID,
|
||||
CONF_INPUT,
|
||||
CONF_INVERTED,
|
||||
CONF_MODE,
|
||||
CONF_NUMBER,
|
||||
CONF_OPEN_DRAIN,
|
||||
CONF_OUTPUT,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@dwmw2"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
MULTI_CONF = True
|
||||
ch423_ns = cg.esphome_ns.namespace("ch423")
|
||||
|
||||
CH423Component = ch423_ns.class_("CH423Component", cg.Component, i2c.I2CDevice)
|
||||
CH423GPIOPin = ch423_ns.class_(
|
||||
"CH423GPIOPin", cg.GPIOPin, cg.Parented.template(CH423Component)
|
||||
)
|
||||
|
||||
CONF_CH423 = "ch423"
|
||||
|
||||
# Note that no address is configurable - each register in the CH423 has a dedicated i2c address
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(CH423Component),
|
||||
cv.GenerateID(CONF_I2C_ID): cv.use_id(I2CBus),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
# Can't use register_i2c_device because there is no CONF_ADDRESS
|
||||
parent = await cg.get_variable(config[CONF_I2C_ID])
|
||||
cg.add(var.set_i2c_bus(parent))
|
||||
|
||||
|
||||
# This is used as a final validation step so that modes have been fully transformed.
|
||||
def pin_mode_check(pin_config, _):
|
||||
if pin_config[CONF_MODE][CONF_INPUT] and pin_config[CONF_NUMBER] >= 8:
|
||||
raise cv.Invalid("CH423 only supports input on pins 0-7")
|
||||
if pin_config[CONF_MODE][CONF_OPEN_DRAIN] and pin_config[CONF_NUMBER] < 8:
|
||||
raise cv.Invalid("CH423 only supports open drain output on pins 8-23")
|
||||
|
||||
ch423_id = pin_config[CONF_CH423]
|
||||
pin_num = pin_config[CONF_NUMBER]
|
||||
is_output = pin_config[CONF_MODE][CONF_OUTPUT]
|
||||
is_open_drain = pin_config[CONF_MODE][CONF_OPEN_DRAIN]
|
||||
|
||||
# Track pin modes per CH423 instance in CORE.data
|
||||
ch423_modes = CORE.data.setdefault(CONF_CH423, {})
|
||||
if ch423_id not in ch423_modes:
|
||||
ch423_modes[ch423_id] = {"gpio_output": None, "gpo_open_drain": None}
|
||||
|
||||
if pin_num < 8:
|
||||
# GPIO pins (0-7): all must have same direction
|
||||
if ch423_modes[ch423_id]["gpio_output"] is None:
|
||||
ch423_modes[ch423_id]["gpio_output"] = is_output
|
||||
elif ch423_modes[ch423_id]["gpio_output"] != is_output:
|
||||
raise cv.Invalid(
|
||||
"CH423 GPIO pins (0-7) must all be configured as input or all as output"
|
||||
)
|
||||
# GPO pins (8-23): all must have same open-drain setting
|
||||
elif ch423_modes[ch423_id]["gpo_open_drain"] is None:
|
||||
ch423_modes[ch423_id]["gpo_open_drain"] = is_open_drain
|
||||
elif ch423_modes[ch423_id]["gpo_open_drain"] != is_open_drain:
|
||||
raise cv.Invalid(
|
||||
"CH423 GPO pins (8-23) must all be configured as push-pull or all as open-drain"
|
||||
)
|
||||
|
||||
|
||||
CH423_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
CH423GPIOPin,
|
||||
cv.int_range(min=0, max=23),
|
||||
modes=[CONF_INPUT, CONF_OUTPUT, CONF_OPEN_DRAIN],
|
||||
).extend(
|
||||
{
|
||||
cv.Required(CONF_CH423): cv.use_id(CH423Component),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register(CONF_CH423, CH423_PIN_SCHEMA, pin_mode_check)
|
||||
async def ch423_pin_to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
parent = await cg.get_variable(config[CONF_CH423])
|
||||
|
||||
cg.add(var.set_parent(parent))
|
||||
|
||||
num = config[CONF_NUMBER]
|
||||
cg.add(var.set_pin(num))
|
||||
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
||||
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||
return var
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user