mirror of
https://github.com/esphome/esphome.git
synced 2026-08-23 14:46:20 +00:00
Compare commits
625
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2022c2ed9 | ||
|
|
230b6926ab | ||
|
|
4f315ae8ba | ||
|
|
4e88823faf | ||
|
|
ec6123e0ef | ||
|
|
da3c6dec7f | ||
|
|
cfac36c647 | ||
|
|
d179f43e7c | ||
|
|
e842af8acb | ||
|
|
ad2a3e120a | ||
|
|
e5fcaf3892 | ||
|
|
af16c444d4 | ||
|
|
327630523c | ||
|
|
0e4f4237fd | ||
|
|
96a74cd217 | ||
|
|
dc64c93f39 | ||
|
|
a3beb8fb3d | ||
|
|
d5e3d44fd4 | ||
|
|
32097fcb00 | ||
|
|
4bb0fa8761 | ||
|
|
1b109518f7 | ||
|
|
5793a5f08a | ||
|
|
9d0d1b42d5 | ||
|
|
8230c16f12 | ||
|
|
26941ef953 | ||
|
|
fd077062c9 | ||
|
|
a1d0713a54 | ||
|
|
ef007e00d1 | ||
|
|
9e96d53868 | ||
|
|
905c806009 | ||
|
|
42f59f94c9 | ||
|
|
35bb8388a2 | ||
|
|
4b317deea3 | ||
|
|
99ec278c67 | ||
|
|
6b7a56be67 | ||
|
|
6a0e7dc595 | ||
|
|
6fda5b9a07 | ||
|
|
9adba2ac06 | ||
|
|
5ac6e2b3c6 | ||
|
|
b08026e413 | ||
|
|
f6c6708f24 | ||
|
|
48c6948ae8 | ||
|
|
7445b47648 | ||
|
|
f65aa87450 | ||
|
|
2dfa23f5e5 | ||
|
|
e4911b4cc2 | ||
|
|
e714ef4dfc | ||
|
|
4146f23ba3 | ||
|
|
f0203528f5 | ||
|
|
cd291fb35b | ||
|
|
e9466ff738 | ||
|
|
b9317de2e8 | ||
|
|
4d6300b400 | ||
|
|
030e79fa5a | ||
|
|
da9c0bdd14 | ||
|
|
a1e6370e8b | ||
|
|
dada0f2c2b | ||
|
|
c7d7a7ba75 | ||
|
|
a5e87d3c8f | ||
|
|
ba1ad765ea | ||
|
|
767bb61a14 | ||
|
|
21abf1692b | ||
|
|
a22c2a9768 | ||
|
|
18206967a1 | ||
|
|
70765dd52a | ||
|
|
32a0fff6b8 | ||
|
|
2f0ad8a57d | ||
|
|
75150fda52 | ||
|
|
791e2b8204 | ||
|
|
ce52b93249 | ||
|
|
d9e18ee982 | ||
|
|
13d1933873 | ||
|
|
44d447ada0 | ||
|
|
f49a5879c3 | ||
|
|
918444ecbb | ||
|
|
d26d3fdc33 | ||
|
|
227f5c67e2 | ||
|
|
c15df918f5 | ||
|
|
3d540b1597 | ||
|
|
ca79cb1a20 | ||
|
|
e557e84468 | ||
|
|
5c30acfe5b | ||
|
|
cc27854fb2 | ||
|
|
044d593f09 | ||
|
|
68e04f5982 | ||
|
|
d6d80ee323 | ||
|
|
f0651e5c9b | ||
|
|
f24b731f95 | ||
|
|
01ad424d12 | ||
|
|
435d522683 | ||
|
|
a63c3bc0c7 | ||
|
|
b83ce91528 | ||
|
|
a30238aab6 | ||
|
|
47156c9a5b | ||
|
|
78240c9a46 | ||
|
|
5a9f06e584 | ||
|
|
ecb007da70 | ||
|
|
ad1a4fca36 | ||
|
|
832a738588 | ||
|
|
fedb3ac5c1 | ||
|
|
cb4e55e444 | ||
|
|
3ef5a8e6a4 | ||
|
|
0e915e9b8b | ||
|
|
dba3b287dd | ||
|
|
ce019f508d | ||
|
|
5c2286cc4a | ||
|
|
efc0a94112 | ||
|
|
c60062c418 | ||
|
|
763a1d9371 | ||
|
|
4db1666024 | ||
|
|
0dc69aab1e | ||
|
|
a282cb095e | ||
|
|
259e7182a3 | ||
|
|
74bdf275d2 | ||
|
|
14499223fd | ||
|
|
6aab523dd9 | ||
|
|
1f31e51446 | ||
|
|
c062d0c717 | ||
|
|
1b0d20e72e | ||
|
|
2218743c4a | ||
|
|
de709dec71 | ||
|
|
2e3dc3c514 | ||
|
|
0dfe029863 | ||
|
|
149ca8c954 | ||
|
|
45c4a9b143 | ||
|
|
bf0a4ea40f | ||
|
|
0d33c3bf07 | ||
|
|
2acba9b364 | ||
|
|
8b8ef9bc61 | ||
|
|
36b0b62a8f | ||
|
|
a174c306d8 | ||
|
|
35f3087fcc | ||
|
|
8475a4bf09 | ||
|
|
715a7ab58f | ||
|
|
f4bc7dda35 | ||
|
|
3e4ace6b84 | ||
|
|
d72e2f1c60 | ||
|
|
5ba4dc7704 | ||
|
|
cbbc0a8fa9 | ||
|
|
1f0fa0ef8b | ||
|
|
7d246893be | ||
|
|
225c3f4cdb | ||
|
|
ce3f90d377 | ||
|
|
3697401bc7 | ||
|
|
c3297c2663 | ||
|
|
7be255b9ba | ||
|
|
c3129d598c | ||
|
|
82b20313b0 | ||
|
|
dcabaedff1 | ||
|
|
2a1af47cef | ||
|
|
27aef88077 | ||
|
|
a5fde9429d | ||
|
|
e63d995d1f | ||
|
|
ad0859c5bd | ||
|
|
385e2a92d2 | ||
|
|
bff8bea83e | ||
|
|
f3ae985f67 | ||
|
|
0c5586c7dc | ||
|
|
4ece0da68c | ||
|
|
712ee229e5 | ||
|
|
0df7736d75 | ||
|
|
80c9c4cef3 | ||
|
|
bc98ba3b46 | ||
|
|
3974a810bc | ||
|
|
dcf921d5f6 | ||
|
|
49d060fdfd | ||
|
|
f4844e2d69 | ||
|
|
f1c751c3b4 | ||
|
|
466d67788e | ||
|
|
f2f5ea127e | ||
|
|
ebafc439ac | ||
|
|
2acde317d2 | ||
|
|
d1d2f186ab | ||
|
|
f2ff1cc986 | ||
|
|
77704c0909 | ||
|
|
11964b86a8 | ||
|
|
f8484158ad | ||
|
|
a7777e4589 | ||
|
|
4e6a8e297e | ||
|
|
2315f54f2e | ||
|
|
86ac8f661b | ||
|
|
7694ffb5ac | ||
|
|
7f6aaeb46c | ||
|
|
7a938af0f8 | ||
|
|
689861ffc9 | ||
|
|
776b3844f9 | ||
|
|
eec01ea793 | ||
|
|
b888d41cb9 | ||
|
|
8b76d61e8f | ||
|
|
8bcf42921a | ||
|
|
b3f058613c | ||
|
|
97701122be | ||
|
|
f1d043c840 | ||
|
|
1eda5a8534 | ||
|
|
040c91259d | ||
|
|
1652088009 | ||
|
|
ff8f9786e9 | ||
|
|
4cbfdd3200 | ||
|
|
520bf3f63b | ||
|
|
1f2b532394 | ||
|
|
1cf5df3d59 | ||
|
|
a2a93cc186 | ||
|
|
ae141a4712 | ||
|
|
6171561b70 | ||
|
|
91836a8c69 | ||
|
|
35af304911 | ||
|
|
f4482275cb | ||
|
|
4aba6c4670 | ||
|
|
5f2d0b9e93 | ||
|
|
d653a550bb | ||
|
|
f599e37375 | ||
|
|
6f79518ee2 | ||
|
|
fd7e340e81 | ||
|
|
84a8ac3772 | ||
|
|
961bff4790 | ||
|
|
469fd5b2c4 | ||
|
|
cb02e15184 | ||
|
|
910147139a | ||
|
|
ab49aff931 | ||
|
|
4a459e3d0d | ||
|
|
612775908e | ||
|
|
680753dbef | ||
|
|
8a4ade10e6 | ||
|
|
8cffd44ffd | ||
|
|
e16008fc58 | ||
|
|
35a1136bea | ||
|
|
3b091676bf | ||
|
|
ef93ebb67b | ||
|
|
622be18709 | ||
|
|
ade6dbabae | ||
|
|
182dc7f636 | ||
|
|
d7af5f37b6 | ||
|
|
121795fa8d | ||
|
|
38be558603 | ||
|
|
4e48764bbe | ||
|
|
deecfade13 | ||
|
|
ec237a2d06 | ||
|
|
a2267eb686 | ||
|
|
dbd6f323c0 | ||
|
|
e4cb1d4f27 | ||
|
|
62a4cb3fe4 | ||
|
|
d3eaac6f47 | ||
|
|
2eec194d87 | ||
|
|
3a9a6edc0e | ||
|
|
11875ec040 | ||
|
|
eddb5981da | ||
|
|
51221688f6 | ||
|
|
f695e9dceb | ||
|
|
d8f6368716 | ||
|
|
a97b836012 | ||
|
|
afeb1a5ea1 | ||
|
|
557ba9953d | ||
|
|
a890527ab3 | ||
|
|
2d2750e031 | ||
|
|
0d508ccbc0 | ||
|
|
bf7a617934 | ||
|
|
803eef7de6 | ||
|
|
fb06b4d631 | ||
|
|
447a411d45 | ||
|
|
ae2893992a | ||
|
|
6b46e9fd03 | ||
|
|
2f053371ce | ||
|
|
47e87d7eb2 | ||
|
|
3bd4cdb760 | ||
|
|
9533c09b69 | ||
|
|
2ea5c87e99 | ||
|
|
2dac9112be | ||
|
|
efa3018123 | ||
|
|
478584354e | ||
|
|
53228c183a | ||
|
|
9c7f68b813 | ||
|
|
ef6c609e93 | ||
|
|
b62d59ca26 | ||
|
|
e52703617e | ||
|
|
57cbadbe9f | ||
|
|
a6ca775a48 | ||
|
|
315f41e884 | ||
|
|
65609bb94b | ||
|
|
09a875f359 | ||
|
|
6bdec7e907 | ||
|
|
4a1138820d | ||
|
|
bbcc9f690a | ||
|
|
7c07276c71 | ||
|
|
60cf44de3b | ||
|
|
072b65c0e2 | ||
|
|
f71867ed2d | ||
|
|
c206f83528 | ||
|
|
9676e0c0f0 | ||
|
|
cedecf1f83 | ||
|
|
e7d3c1b92b | ||
|
|
f28be76c84 | ||
|
|
3242196f9c | ||
|
|
c7a96f77d2 | ||
|
|
fc8a48d70b | ||
|
|
cdde9dd230 | ||
|
|
935db6a63c | ||
|
|
90b29e231d | ||
|
|
315abf21f8 | ||
|
|
24e5b1800f | ||
|
|
11eca53a18 | ||
|
|
f08e06f574 | ||
|
|
27f4db8458 | ||
|
|
e99e0cc8b8 | ||
|
|
3b2fc9f59a | ||
|
|
57e738adf8 | ||
|
|
d119ad6c60 | ||
|
|
70b8573110 | ||
|
|
74fc2e367a | ||
|
|
ea10f94376 | ||
|
|
735d4076c1 | ||
|
|
a48fbd1cd1 | ||
|
|
e47ec9acad | ||
|
|
8e6af6aa8c | ||
|
|
9bb899d8df | ||
|
|
35c77165ca | ||
|
|
ef1d77885d | ||
|
|
dccf55eadc | ||
|
|
65704e881f | ||
|
|
a30e82459f | ||
|
|
5a300e92f1 | ||
|
|
8e9fb0f93c | ||
|
|
44b14d8655 | ||
|
|
4eb31c45da | ||
|
|
227522364a | ||
|
|
4bea2dd058 | ||
|
|
db0e52a36b | ||
|
|
a05a392fdd | ||
|
|
6a963ba794 | ||
|
|
cbf9e0ab14 | ||
|
|
a527b21761 | ||
|
|
4d63564cdd | ||
|
|
42e255c1e5 | ||
|
|
012b571a2e | ||
|
|
a1d59049b4 | ||
|
|
4a16365f13 | ||
|
|
587873a2da | ||
|
|
aad89cd939 | ||
|
|
4101ad4fb1 | ||
|
|
048025e9dd | ||
|
|
e9d6132c61 | ||
|
|
8ad1ed98b7 | ||
|
|
7298eed5b3 | ||
|
|
78a1e88915 | ||
|
|
b87875881e | ||
|
|
d8c02221cc | ||
|
|
76d8c9eb46 | ||
|
|
a703acf85f | ||
|
|
7beda110d4 | ||
|
|
7fb18f395f | ||
|
|
c1a7c87a64 | ||
|
|
d96ddf3612 | ||
|
|
e18852e084 | ||
|
|
1f868d22fb | ||
|
|
2ecf7af416 | ||
|
|
91a4eb0f57 | ||
|
|
97ed253c38 | ||
|
|
be2395da66 | ||
|
|
cbf1241401 | ||
|
|
ce87ca2e11 | ||
|
|
33aea420ac | ||
|
|
e3d29e9fff | ||
|
|
03bf037cb8 | ||
|
|
4dc2d34399 | ||
|
|
e9f8233f67 | ||
|
|
c07a5a8da4 | ||
|
|
b4514e5af2 | ||
|
|
93a768531d | ||
|
|
4898dc0800 | ||
|
|
e3cf2407ca | ||
|
|
45cbe0aa4b | ||
|
|
937c69e420 | ||
|
|
0cb307aea5 | ||
|
|
1f1cab9c37 | ||
|
|
6d23a2c9e5 | ||
|
|
85335de839 | ||
|
|
0f7cd3fada | ||
|
|
78a41161cb | ||
|
|
653f9e8f2a | ||
|
|
510f667bd1 | ||
|
|
c93cf55967 | ||
|
|
9bd83e5d2d | ||
|
|
f3f6fe8a7d | ||
|
|
efe1b0e744 | ||
|
|
6cb446baf8 | ||
|
|
0fa081cd80 | ||
|
|
b8ce65282e | ||
|
|
8fd8766167 | ||
|
|
225c62dbf1 | ||
|
|
bb77ad01eb | ||
|
|
0b0194cf41 | ||
|
|
8b9773c4bd | ||
|
|
662978c402 | ||
|
|
e6bf3e0e8f | ||
|
|
078bcba473 | ||
|
|
85acafb646 | ||
|
|
18a2aa2097 | ||
|
|
d58291945c | ||
|
|
88f1c4474f | ||
|
|
6ed01b06c3 | ||
|
|
24eb6354f9 | ||
|
|
d8bdf2fc5b | ||
|
|
355d02ee6a | ||
|
|
f5ac480dff | ||
|
|
19555e8eaf | ||
|
|
d47dc0d992 | ||
|
|
c448174ea9 | ||
|
|
6a363c1746 | ||
|
|
b0a214830a | ||
|
|
38744e4222 | ||
|
|
9624abcd57 | ||
|
|
2c229debe8 | ||
|
|
170a180458 | ||
|
|
5857bfcf52 | ||
|
|
378a01a838 | ||
|
|
e266a449e0 | ||
|
|
6a0ce046fd | ||
|
|
6b6344b454 | ||
|
|
51f477e2c7 | ||
|
|
a0e82cd000 | ||
|
|
a56c465d6d | ||
|
|
b3d908d10e | ||
|
|
09fd3fb53b | ||
|
|
73ab04330e | ||
|
|
d883ed5f8a | ||
|
|
a255f3b07c | ||
|
|
57b69cae81 | ||
|
|
9fccf1ce33 | ||
|
|
b8bb4453e0 | ||
|
|
91eed262c8 | ||
|
|
1ce0a456c9 | ||
|
|
c1a0ed0d46 | ||
|
|
fdbd4157ff | ||
|
|
eb0cb06951 | ||
|
|
4969d9bf4a | ||
|
|
1c5441b69e | ||
|
|
ec8437fea2 | ||
|
|
8513dfeebd | ||
|
|
b42babf749 | ||
|
|
e1c4984498 | ||
|
|
3063b9067a | ||
|
|
5b56867c71 | ||
|
|
a73180e073 | ||
|
|
b7a3181d13 | ||
|
|
1958746ae0 | ||
|
|
62c27826ee | ||
|
|
c95e0b90f5 | ||
|
|
2a09cd81d3 | ||
|
|
cc3f9be805 | ||
|
|
b05efb0811 | ||
|
|
321bc3b459 | ||
|
|
84a3c39562 | ||
|
|
5cf2135038 | ||
|
|
5da6a2abeb | ||
|
|
b3afd951fe | ||
|
|
8dd065997f | ||
|
|
13e84e9bff | ||
|
|
7ad97697b2 | ||
|
|
5bd328e6c4 | ||
|
|
4990093540 | ||
|
|
878883bb67 | ||
|
|
8c671ae8ea | ||
|
|
ac1160de25 | ||
|
|
5cec72020f | ||
|
|
ef2f16713a | ||
|
|
5605d079d4 | ||
|
|
0268abc1e4 | ||
|
|
c103e4d974 | ||
|
|
06fc234b12 | ||
|
|
12f8b14418 | ||
|
|
790c6d6353 | ||
|
|
a397fcf373 | ||
|
|
b0b3017d78 | ||
|
|
47780fec6e | ||
|
|
c3e7d37d29 | ||
|
|
77fa411f04 | ||
|
|
bc6b997350 | ||
|
|
0c0f482acf | ||
|
|
1bdddcc55e | ||
|
|
a5a1a017d0 | ||
|
|
9f3df90dba | ||
|
|
e20c43520b | ||
|
|
9b12934307 | ||
|
|
b6ea116b16 | ||
|
|
eb38afb569 | ||
|
|
60896b3533 | ||
|
|
46baef5231 | ||
|
|
f93d46e684 | ||
|
|
d33a097152 | ||
|
|
b2452fe397 | ||
|
|
14a58b788d | ||
|
|
454f299cd2 | ||
|
|
70a3880c9f | ||
|
|
8008eda57f | ||
|
|
466f890e1f | ||
|
|
88a5cdb53c | ||
|
|
70cf20640c | ||
|
|
8af1b9a523 | ||
|
|
9c543c9776 | ||
|
|
fd47d5bcd2 | ||
|
|
57b7f03e42 | ||
|
|
4ab2a7ba98 | ||
|
|
370f83769c | ||
|
|
96aabe428a | ||
|
|
cb2dfddf17 | ||
|
|
20f0e37f43 | ||
|
|
8e25bfddb2 | ||
|
|
f4eefa111d | ||
|
|
2bbca6229a | ||
|
|
a50c4db187 | ||
|
|
e918d37aa8 | ||
|
|
1ecbb57b24 | ||
|
|
2e0b330366 | ||
|
|
aea97d1022 | ||
|
|
6392620f6c | ||
|
|
65f5c302c5 | ||
|
|
16be3b1bbb | ||
|
|
84940f1726 | ||
|
|
519ea38d73 | ||
|
|
a0f0ad8729 | ||
|
|
7f033fd993 | ||
|
|
51af18cdfb | ||
|
|
efacdd57ac | ||
|
|
2280d3352d | ||
|
|
0fad14be59 | ||
|
|
57a9c71da8 | ||
|
|
04422a0437 | ||
|
|
3d8a3c6527 | ||
|
|
e618c99f7d | ||
|
|
89df2787f2 | ||
|
|
53f1cc116a | ||
|
|
1483b7c10b | ||
|
|
e121fd3a37 | ||
|
|
c8e816bf48 | ||
|
|
53935615f1 | ||
|
|
360c51329c | ||
|
|
10b13558c3 | ||
|
|
2e183f262b | ||
|
|
d82c2eaeb1 | ||
|
|
37de984df5 | ||
|
|
c96c05eea4 | ||
|
|
2cf4087e2f | ||
|
|
7fe0847e5c | ||
|
|
80815f1dc7 | ||
|
|
5b19c139cb | ||
|
|
e187522490 | ||
|
|
6982cfeb9a | ||
|
|
79b8bf2657 | ||
|
|
f9705772bb | ||
|
|
e36b8c5446 | ||
|
|
9c52c349d6 | ||
|
|
d4861d89b0 | ||
|
|
3166136db0 | ||
|
|
8f5f349e1b | ||
|
|
83b4da1777 | ||
|
|
12c1b15578 | ||
|
|
7796911837 | ||
|
|
e6debb980d | ||
|
|
b3d5fccba5 | ||
|
|
6a84d67786 | ||
|
|
d4e3603a19 | ||
|
|
e032f37e04 | ||
|
|
b90aeb17e3 | ||
|
|
3a51514fc1 | ||
|
|
6ad3e0d8ad | ||
|
|
7a3f700206 | ||
|
|
857bcaef4d | ||
|
|
5a5496f7f7 | ||
|
|
9d9953ea47 | ||
|
|
7d9c18a330 | ||
|
|
ca47309a36 | ||
|
|
113259275c | ||
|
|
5a4337c240 | ||
|
|
2508899cad | ||
|
|
38b6f222b5 | ||
|
|
8bbaa09891 | ||
|
|
976a5ed988 | ||
|
|
c72de504fd | ||
|
|
ff167e5193 | ||
|
|
2861cbb749 | ||
|
|
8bf699cf79 | ||
|
|
375cf95240 | ||
|
|
f5fc145dd3 | ||
|
|
f44398b09e | ||
|
|
d64592d2ea | ||
|
|
102973524e | ||
|
|
1307cc25c5 | ||
|
|
353ef2dee1 | ||
|
|
18e3eac419 | ||
|
|
18f39a0e53 | ||
|
|
d9cc72c650 | ||
|
|
f0f9af2ad6 | ||
|
|
0fe9d87124 | ||
|
|
5398ca98eb | ||
|
|
bbf2788684 | ||
|
|
c7e1d044a3 | ||
|
|
0620ce81c3 | ||
|
|
2564ad0deb | ||
|
|
03d5b131ca | ||
|
|
3ac8ff1176 | ||
|
|
fa45c4a016 | ||
|
|
3d7c0eac57 | ||
|
|
e3936f500f | ||
|
|
cea86989c9 | ||
|
|
e82453e80e | ||
|
|
3eec5085da | ||
|
|
57c8648210 | ||
|
|
8f9aca154f | ||
|
|
1db7309f4a | ||
|
|
1d0a1d2b38 | ||
|
|
46fa42fa33 | ||
|
|
9bd548ba92 | ||
|
|
82c3c1f50a | ||
|
|
3e77654719 | ||
|
|
04ad9524f5 | ||
|
|
72da2b1ce2 | ||
|
|
ab58f1080a | ||
|
|
47942fb166 | ||
|
|
f326c3f04d | ||
|
|
3b51ec2ae7 | ||
|
|
28384673f6 | ||
|
|
b361342bde | ||
|
|
d34eed6d2d | ||
|
|
12238eec3b | ||
|
|
fe53ff6b6b | ||
|
|
d0f38d7644 |
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.12.3
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.12.4
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
+27
-11
@@ -762,9 +762,11 @@ def _wrap_to_code(name, comp, yaml_util):
|
||||
async def wrapped(conf):
|
||||
cg.add(cg.LineComment(f"{name}:"))
|
||||
if comp.config_schema is not None:
|
||||
conf_str = yaml_util.dump(conf)
|
||||
# sort_keys: voluptuous fills defaults in set order, so an
|
||||
# unsorted dump would churn main.cpp and relink every run
|
||||
conf_str = yaml_util.dump(conf, sort_keys=True)
|
||||
conf_str = conf_str.replace("//", "")
|
||||
# remove tailing \ to avoid multi-line comment warning
|
||||
# remove trailing \ to avoid multi-line comment warning
|
||||
conf_str = conf_str.replace("\\\n", "\n")
|
||||
cg.add(cg.LineComment(indent(conf_str)))
|
||||
await coro(conf)
|
||||
@@ -855,7 +857,20 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
toolchain.create_factory_bin()
|
||||
toolchain.create_ota_bin()
|
||||
toolchain.create_elf_copy()
|
||||
toolchain.get_idedata()
|
||||
from esphome.build_helpers.idedata import IDEDATA_BEST_EFFORT_ERRORS
|
||||
|
||||
try:
|
||||
if toolchain.get_idedata() is None:
|
||||
_LOGGER.warning("No idedata was generated for this build")
|
||||
except IDEDATA_BEST_EFFORT_ERRORS as err:
|
||||
# The firmware already built; an idedata failure must not fail
|
||||
# a successful build.
|
||||
_LOGGER.warning(
|
||||
"Could not generate idedata: %s (IDE, clang-tidy, and "
|
||||
"memory-analysis data will be unavailable for this build)",
|
||||
err,
|
||||
)
|
||||
_LOGGER.debug("Idedata failure detail", exc_info=True)
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
@@ -2719,10 +2734,14 @@ def run_esphome(argv):
|
||||
# Skipped when -s overrides are passed, since the cache was written
|
||||
# against the previous substitution set.
|
||||
config: ConfigType | None = None
|
||||
cache_eligible = (
|
||||
cache_write_eligible = (
|
||||
args.command in ("upload", "logs") and not command_line_substitutions
|
||||
)
|
||||
if cache_eligible:
|
||||
# An explicit --toolchain must re-run the per-platform validators, so
|
||||
# gate only the cache read; the refresh below saves the result unless
|
||||
# the sidecar records a different toolchain.
|
||||
cache_read_eligible = cache_write_eligible and args.toolchain is None
|
||||
if cache_read_eligible:
|
||||
from esphome.compiled_config import load_compiled_config
|
||||
|
||||
config = load_compiled_config(conf_path)
|
||||
@@ -2746,17 +2765,14 @@ def run_esphome(argv):
|
||||
return 2
|
||||
CORE.config = config
|
||||
|
||||
# Fallback for platforms whose validators didn't set the toolchain
|
||||
# (only the esp32 component reads esp32.framework.toolchain). All
|
||||
# other platforms only support PlatformIO today. Must run before the
|
||||
# cache refresh below so its sidecar records the same toolchain a
|
||||
# compile would.
|
||||
# The cache fast path skips validation, and legacy sidecars lack the
|
||||
# toolchain field. Must run before the cache refresh below.
|
||||
if CORE.toolchain is None:
|
||||
CORE.toolchain = Toolchain.PLATFORMIO
|
||||
|
||||
# Refresh the cache so the next upload/logs hits the fast path
|
||||
# instead of re-running read_config.
|
||||
if cache_eligible and cache_missed:
|
||||
if cache_write_eligible and cache_missed:
|
||||
from esphome.compiled_config import save_compiled_config_and_sidecar
|
||||
|
||||
save_compiled_config_and_sidecar(config)
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
"""Arduino-core backend for the shared PlatformIO library converter.
|
||||
|
||||
Bundled names build straight from the framework tree; everything else goes
|
||||
through ``esphome.platformio.library``. Mirrors ``lib_ldf_mode=off``: each
|
||||
library builds its own archive; all include dirs join one global path.
|
||||
|
||||
Deviations from PlatformIO: flat-layout libraries get the recursive default
|
||||
source filter; ``dot_a_linkage`` is honored; bundled libraries never run a
|
||||
manifest ``extraScript``; manifest ``-I`` flags join the global include path;
|
||||
``precompiled``/``ldflags`` properties are refused by name.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from esphome.core import CORE, EsphomeError, Library
|
||||
from esphome.helpers import walk_files
|
||||
from esphome.platformio.extra_script import apply_extra_script
|
||||
from esphome.platformio.library import (
|
||||
DEFAULT_BUILD_INCLUDE_DIR,
|
||||
DEFAULT_BUILD_SRC_FILTER,
|
||||
LIBRARY_HEADER_SUFFIXES,
|
||||
SRC_FILE_EXTENSIONS,
|
||||
ConvertedLibrary,
|
||||
InvalidLibrary,
|
||||
LibraryBackend,
|
||||
_url_or_none,
|
||||
check_library_data,
|
||||
collect_filtered_files,
|
||||
convert_libraries,
|
||||
ensure_list,
|
||||
is_lib_ignored,
|
||||
lex_build_flags,
|
||||
lib_ignore_set,
|
||||
normalize_dependencies,
|
||||
parse_library_json,
|
||||
parse_library_properties,
|
||||
warn_properties_depends,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArduinoLibrary:
|
||||
"""One resolved library, ready for the ninja generator."""
|
||||
|
||||
name: str
|
||||
sources: list[Path] = field(default_factory=list)
|
||||
include_dirs: list[Path] = field(default_factory=list)
|
||||
# Extra compile flags private to this library's own sources
|
||||
flags: list[str] = field(default_factory=list)
|
||||
# PlatformIO's build.libArchive / Arduino's dot_a_linkage: when False the
|
||||
# objects go to the linker directly (symbols nothing references survive)
|
||||
lib_archive: bool = True
|
||||
# Link inputs the library contributes (-L dirs / -l libs, e.g. from
|
||||
# precompiled vendor blobs) and -Wl, options for the firmware link
|
||||
link_dirs: list[Path] = field(default_factory=list)
|
||||
link_libs: list[str] = field(default_factory=list)
|
||||
link_flags: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# Filename-plain names only: leading alnum/underscore, then word chars,
|
||||
# dot, space, plus, or hyphen. An allowlist excludes separators, drive
|
||||
# colons, and dot-only names by shape instead of enumerating them.
|
||||
_SAFE_LIBRARY_NAME_RE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_. +-]*\Z")
|
||||
|
||||
|
||||
def _is_safe_library_name(name: object) -> bool:
|
||||
"""Whether a name may be joined under the framework's libraries dir."""
|
||||
return isinstance(name, str) and _SAFE_LIBRARY_NAME_RE.fullmatch(name) is not None
|
||||
|
||||
|
||||
def _manifest_build(name: str, data: object) -> dict:
|
||||
"""The manifest's ``build`` section; a malformed manifest must fail
|
||||
naming the library, not with an AttributeError."""
|
||||
build = data.get("build", {}) if isinstance(data, dict) else None
|
||||
if not isinstance(build, dict):
|
||||
raise EsphomeError(f"Library {name} has a malformed manifest")
|
||||
return build
|
||||
|
||||
|
||||
def _resolve_src_dir(name: str, read_path: Path, build: dict) -> str:
|
||||
"""Resolve PIO's source dir: manifest srcDir, else src/Src, else the root."""
|
||||
if "srcDir" not in build:
|
||||
return next((d for d in ("src", "Src") if (read_path / d).is_dir()), ".")
|
||||
# A declared srcDir (falsy included) that does not resolve is a
|
||||
# manifest error
|
||||
src_dir = build["srcDir"]
|
||||
if not (isinstance(src_dir, str) and src_dir and (read_path / src_dir).is_dir()):
|
||||
raise EsphomeError(
|
||||
f"Library {name} declares srcDir {src_dir!r} which does not exist"
|
||||
)
|
||||
return src_dir
|
||||
|
||||
|
||||
def _reject_unsupported_link_fields(name: str, data: dict) -> None:
|
||||
# PIO's Arduino lib builder honors these; building without them would
|
||||
# fail at link with no stated cause. library.properties values are
|
||||
# strings, so "false" (the spec's explicit opt-out) is not a declaration.
|
||||
precompiled = data.get("precompiled")
|
||||
if precompiled and str(precompiled).strip().lower() != "false":
|
||||
raise EsphomeError(
|
||||
f"Library {name} declares precompiled, which this backend does not support"
|
||||
)
|
||||
if data.get("ldflags"):
|
||||
raise EsphomeError(
|
||||
f"Library {name} declares ldflags, which this backend does not support"
|
||||
)
|
||||
|
||||
|
||||
def _resolve_lib_archive(name: str, data: dict, build: dict) -> bool:
|
||||
"""build.libArchive, else dot_a_linkage (Arduino IDE's property, ignored
|
||||
by PIO -- a deliberate extra), else archive."""
|
||||
|
||||
# Strict parse: bool("false") is True
|
||||
def _parse(key: str, raw: object) -> bool:
|
||||
if isinstance(raw, bool):
|
||||
return raw
|
||||
value = str(raw).strip().lower()
|
||||
if value in ("true", "false"):
|
||||
return value == "true"
|
||||
raise EsphomeError(f"Library {name} has a malformed {key} value {raw!r}")
|
||||
|
||||
if "libArchive" in build:
|
||||
return _parse("libArchive", build["libArchive"])
|
||||
if "dot_a_linkage" in data:
|
||||
return _parse("dot_a_linkage", data["dot_a_linkage"])
|
||||
return True
|
||||
|
||||
|
||||
def _classify_build_flags(
|
||||
name: str, read_path: Path, lib: ArduinoLibrary, flag_tokens: list[str]
|
||||
) -> list[str]:
|
||||
"""Route the lexed build.flags into the library's flag lists.
|
||||
|
||||
Returns the ``-I`` arguments for the include-dir resolution.
|
||||
"""
|
||||
include_flags: list[str] = []
|
||||
for tok in flag_tokens:
|
||||
if tok.startswith("-I"):
|
||||
include_flags.append(tok[2:])
|
||||
elif tok.startswith("-L"):
|
||||
link_dir = (read_path / tok[2:]).resolve()
|
||||
if not link_dir.is_dir():
|
||||
# Kept anyway (the linker ignores missing -L dirs); the
|
||||
# warning names the culprit before a bare "cannot find -lfoo"
|
||||
_LOGGER.warning(
|
||||
"Library %s declares library dir %s which does not exist",
|
||||
name,
|
||||
tok[2:],
|
||||
)
|
||||
lib.link_dirs.append(link_dir)
|
||||
elif tok.startswith("-l"):
|
||||
lib.link_libs.append(tok[2:])
|
||||
elif tok.startswith("-Wl,"):
|
||||
lib.link_flags.append(tok)
|
||||
else:
|
||||
lib.flags.append(tok)
|
||||
return include_flags
|
||||
|
||||
|
||||
def _resolve_include_dirs(
|
||||
name: str,
|
||||
read_path: Path,
|
||||
lib: ArduinoLibrary,
|
||||
build: dict,
|
||||
src_dir: str,
|
||||
include_flags: list[str],
|
||||
) -> None:
|
||||
include_dir = build.get("includeDir", DEFAULT_BUILD_INCLUDE_DIR)
|
||||
if not isinstance(include_dir, str):
|
||||
raise EsphomeError(f"Library {name} has a malformed includeDir")
|
||||
for d, explicit in [
|
||||
(include_dir, "includeDir" in build),
|
||||
(src_dir, False), # _resolve_src_dir already validated it
|
||||
*((flag, True) for flag in include_flags),
|
||||
]:
|
||||
if (path := (read_path / d)).is_dir():
|
||||
lib.include_dirs.append(path.resolve())
|
||||
elif explicit:
|
||||
# Warn-and-drop is intended (unlike srcDir, which raises): a
|
||||
# missing include dir is harmless until a header is actually
|
||||
# needed, and the compile names it then
|
||||
_LOGGER.warning(
|
||||
"Library %s declares include dir %s which does not exist", name, d
|
||||
)
|
||||
|
||||
|
||||
def _collect_lib_sources(
|
||||
name: str,
|
||||
read_path: Path,
|
||||
lib: ArduinoLibrary,
|
||||
src_dir: str,
|
||||
src_filter: list[str],
|
||||
) -> None:
|
||||
matched = collect_filtered_files(read_path / src_dir, src_filter)
|
||||
lib.sources = sorted(
|
||||
path.resolve()
|
||||
for f in matched
|
||||
if (path := Path(f)).suffix in SRC_FILE_EXTENSIONS
|
||||
)
|
||||
# A source-like suffix the case-sensitive map rejects (.CPP, .ino) is a
|
||||
# dropped compilation unit that surfaces as undefined symbols at link;
|
||||
# headers and metadata files fall through silently (header-only
|
||||
# libraries are routine)
|
||||
source_like = {s.lower() for s in SRC_FILE_EXTENSIONS} | {".ino"}
|
||||
if dropped := [
|
||||
Path(f).name
|
||||
for f in matched
|
||||
if Path(f).suffix not in SRC_FILE_EXTENSIONS
|
||||
and Path(f).suffix.lower() in source_like
|
||||
]:
|
||||
_LOGGER.warning(
|
||||
"Library %s: %d file(s) with unmapped source suffixes are not compiled: %s",
|
||||
name,
|
||||
len(dropped),
|
||||
", ".join(sorted(dropped)),
|
||||
)
|
||||
if not lib.sources and not any(
|
||||
Path(f).suffix.lower() in LIBRARY_HEADER_SUFFIXES for f in matched
|
||||
):
|
||||
# Matched headers mean a header-only library; a filter matching
|
||||
# nothing (or only inert files) is a manifest/tree problem whether
|
||||
# or not it was declared. The truly empty tree raises via
|
||||
# _assert_tree_has_code.
|
||||
_LOGGER.warning("Library %s: no source files matched", name)
|
||||
|
||||
|
||||
def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary:
|
||||
"""Resolve one library's sources, include dirs, and flags (PIO semantics)."""
|
||||
build = _manifest_build(name, data)
|
||||
_reject_unsupported_link_fields(name, data)
|
||||
src_dir = _resolve_src_dir(name, read_path, build)
|
||||
src_filter = ensure_list(build.get("srcFilter", DEFAULT_BUILD_SRC_FILTER))
|
||||
if not all(isinstance(entry, str) for entry in src_filter):
|
||||
raise EsphomeError(f"Library {name} has a malformed srcFilter")
|
||||
lib = ArduinoLibrary(name=name, lib_archive=_resolve_lib_archive(name, data, build))
|
||||
# PlatformIO shell-lexes each build.flags entry
|
||||
include_flags = _classify_build_flags(
|
||||
name, read_path, lib, lex_build_flags(build.get("flags", []), f"library {name}")
|
||||
)
|
||||
_resolve_include_dirs(name, read_path, lib, build, src_dir, include_flags)
|
||||
_collect_lib_sources(name, read_path, lib, src_dir, src_filter)
|
||||
return lib
|
||||
|
||||
|
||||
def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary:
|
||||
"""A library bundled with the Arduino core, read from the framework tree.
|
||||
|
||||
``library.json`` wins over ``library.properties`` when both exist, as in
|
||||
PlatformIO's LibBuilderFactory; only the JSON manifest can carry a
|
||||
``build`` section (srcDir, srcFilter, flags).
|
||||
"""
|
||||
lib_dir = framework_path / "libraries" / name
|
||||
manifest_json = lib_dir / "library.json"
|
||||
if manifest_json.is_file():
|
||||
data = parse_library_json(manifest_json)
|
||||
else:
|
||||
manifest = lib_dir / "library.properties"
|
||||
data = parse_library_properties(manifest) if manifest.is_file() else {}
|
||||
if isinstance(data, dict):
|
||||
# Bundled manifest deps are never walked; make the skip visible
|
||||
if data.get("dependencies"):
|
||||
_LOGGER.warning(
|
||||
"Bundled library %s declares dependencies, which are not "
|
||||
"resolved automatically; add them with add_library() if needed",
|
||||
name,
|
||||
)
|
||||
warn_properties_depends(name, data)
|
||||
build = data.get("build")
|
||||
if isinstance(build, dict) and build.get("extraScript"):
|
||||
# apply_extra_script only runs on the converted path; building
|
||||
# without the script's flags would miscompile
|
||||
raise EsphomeError(
|
||||
f"Bundled library {name} declares an extraScript, which is "
|
||||
"not run for bundled libraries"
|
||||
)
|
||||
lib = _library_info(name, lib_dir, data)
|
||||
_assert_tree_has_code(
|
||||
name,
|
||||
lib_dir,
|
||||
"the framework install may be incomplete (run 'esphome clean-all')",
|
||||
)
|
||||
return lib
|
||||
|
||||
|
||||
def _assert_tree_has_code(name: str, root: Path, hint: str) -> None:
|
||||
"""An empty or half-extracted tree can never link; fail by name (a
|
||||
warning would scroll away and resurface as undefined symbols)."""
|
||||
if not any(
|
||||
Path(p).suffix in SRC_FILE_EXTENSIONS
|
||||
or Path(p).suffix.lower() in LIBRARY_HEADER_SUFFIXES
|
||||
for p in walk_files(root)
|
||||
):
|
||||
raise EsphomeError(f"Library {name} has no sources or headers; {hint}")
|
||||
|
||||
|
||||
def _external_short_name(name: str) -> str:
|
||||
"""The short library name of a requested spec.
|
||||
|
||||
"owner/Name" and plain names take the last path segment; the
|
||||
"Name=<url>" custom-name form takes the declared name (the URL tail is
|
||||
a repository path, not a library name). Git tails (".git", "#ref") are
|
||||
stripped like the walk's own URL normalization -- deliberately further
|
||||
than CORE.add_library's keying, because the comparand here is a manifest
|
||||
dependency name, never a spec.
|
||||
"""
|
||||
head, sep, tail = name.partition("=")
|
||||
if sep and "://" in tail:
|
||||
return head
|
||||
short = name.rsplit("/", maxsplit=1)[-1]
|
||||
return short.partition("#")[0].removesuffix(".git")
|
||||
|
||||
|
||||
def _warn_unfulfilled_provides(
|
||||
provided_requests: list[str], satisfied: set[str]
|
||||
) -> None:
|
||||
"""Reconcile the provides() promise: every dependency the walk skipped
|
||||
on the backend's word must have been added from the framework tree (or
|
||||
knowingly satisfied by a converted/external library); an unfulfilled
|
||||
promise would surface only as undefined symbols at link."""
|
||||
for name in provided_requests:
|
||||
if name not in satisfied:
|
||||
_LOGGER.warning(
|
||||
"provides() skipped dependency %s but nothing added it; "
|
||||
"the build is missing a library",
|
||||
name,
|
||||
)
|
||||
|
||||
|
||||
def resolve_libraries(
|
||||
framework_path: Path, *, pio_platform: str, board_mcu: str, cache_key: str
|
||||
) -> list[ArduinoLibrary]:
|
||||
"""Resolve every ``cg.add_library()`` entry into an :class:`ArduinoLibrary`.
|
||||
|
||||
``pio_platform``/``board_mcu`` filter manifests the way PlatformIO would
|
||||
for that core (e.g. ``espressif8266``/``esp8266``); ``cache_key`` keys the
|
||||
shared converter's download cache.
|
||||
|
||||
The returned list is not topologically sorted, so the caller must link
|
||||
the archives inside one ``--start-group``/``--end-group`` pair (the
|
||||
bundled-first grouping is incidental).
|
||||
"""
|
||||
bundled: list[ArduinoLibrary] = []
|
||||
external: list[Library] = []
|
||||
# PlatformIO's lib_ignore covers framework-bundled libraries too; the
|
||||
# shared converter only filters the registry/git ones.
|
||||
lib_ignore = lib_ignore_set()
|
||||
# Exact on-disk directory names, so membership is case-sensitive on
|
||||
# every filesystem (a per-name is_dir() probe would match "wire" on
|
||||
# macOS/Windows and build the bundled Wire twice); the safety guard
|
||||
# stays fused with the lookup (path traversal)
|
||||
libraries_dir = framework_path / "libraries"
|
||||
if not libraries_dir.is_dir():
|
||||
# Falling back to the registry would fail later with a misleading
|
||||
# package-not-found error for every bundled name
|
||||
raise EsphomeError(
|
||||
f"{libraries_dir} is missing; the framework install may be "
|
||||
"incomplete (run 'esphome clean-all')"
|
||||
)
|
||||
bundled_dir_names = frozenset(p.name for p in libraries_dir.iterdir() if p.is_dir())
|
||||
|
||||
def _provided(name: object) -> bool:
|
||||
return _is_safe_library_name(name) and name in bundled_dir_names
|
||||
|
||||
for library in CORE.platformio_libraries.values():
|
||||
if is_lib_ignored(library.name, lib_ignore):
|
||||
continue
|
||||
# Bundled only for a bare name with a matching framework dir; pinned
|
||||
# or unmatched names resolve from the registry, as under PlatformIO.
|
||||
if not library.repository and not library.version and _provided(library.name):
|
||||
# Bundled libraries' own manifest deps are not walked (none of
|
||||
# the ESP8266 core's declare any; _bundled_library warns if one does)
|
||||
bundled.append(_bundled_library(framework_path, library.name))
|
||||
else:
|
||||
external.append(library)
|
||||
|
||||
converted: list[ArduinoLibrary] = []
|
||||
bundled_names = {lib.name for lib in bundled}
|
||||
converted_manifest_names: set[str] = set()
|
||||
# Ordered set of bundled dependency names to add once conversion is done
|
||||
pending_bundled: dict[str, None] = {}
|
||||
# Deps matching a separately-requested external are already in the build
|
||||
# (a duplicate archive means duplicate-symbol link errors)
|
||||
external_short_names = {
|
||||
_external_short_name(lib.name) for lib in external if lib.name
|
||||
}
|
||||
|
||||
def _add_bundled_dependencies(component: ConvertedLibrary) -> None:
|
||||
# A version-less bare-name dependency ("Hash" in ESPAsyncWebServer)
|
||||
# is a core-bundled library; the shared converter skips it because
|
||||
# it cannot be resolved from the registry.
|
||||
for dep in normalize_dependencies(
|
||||
component.data.get("dependencies"), component.name
|
||||
):
|
||||
name = dep.get("name")
|
||||
if isinstance(name, str) and "/" in name:
|
||||
owner, _, pkg = name.partition("/")
|
||||
if _is_safe_library_name(owner) and _is_safe_library_name(pkg):
|
||||
# PIO's owner-qualified spelling ("Owner/Pkg"); the
|
||||
# converter resolves it from the registry
|
||||
continue
|
||||
if not _is_safe_library_name(name):
|
||||
# The name becomes a path component under the framework
|
||||
# tree; never join a traversal or a non-string
|
||||
_LOGGER.warning(
|
||||
"Ignoring malformed dependency entry %r of library %s",
|
||||
dep,
|
||||
component.name,
|
||||
)
|
||||
continue
|
||||
if name in external_short_names:
|
||||
if _provided(name):
|
||||
# A bundled copy really is suppressed; an accidental
|
||||
# short-name collision would surface as link errors
|
||||
_LOGGER.warning(
|
||||
"Dependency %s of %s is assumed satisfied by a "
|
||||
"requested external library; the bundled copy is "
|
||||
"not added",
|
||||
name,
|
||||
component.name,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Dependency %s of %s assumed satisfied by a requested "
|
||||
"external library",
|
||||
name,
|
||||
component.name,
|
||||
)
|
||||
continue
|
||||
if name in bundled_names or is_lib_ignored(name, lib_ignore):
|
||||
continue
|
||||
if _url_or_none(dep.get("version")) is not None:
|
||||
# A URL names one specific source (the walk resolves it as
|
||||
# git); the bundled copy must never be added on top
|
||||
continue
|
||||
if dep.get("owner") or not _provided(name):
|
||||
# Owner-less names in the framework tree prefer the bundled
|
||||
# copy (PIO's process_dependencies); everything else resolves
|
||||
# via the converter, and the walk reports any real drops
|
||||
continue
|
||||
try:
|
||||
# framework=None: the walk already ran dependency_is_usable
|
||||
# on this entry and warned for any non-platform cause, so
|
||||
# debug here is what keeps one manifest fault from warning
|
||||
# twice. That invariant is pinned by
|
||||
# test_nonplatform_rejection_warns_once_through_real_converter,
|
||||
# which fails if the walk stops evaluating these deps.
|
||||
check_library_data(dep, pio_platform, None)
|
||||
except InvalidLibrary as err:
|
||||
_LOGGER.debug("Skip bundled candidate %s: %s", name, err)
|
||||
continue
|
||||
# Deferred: a later-emitted library's manifest name may satisfy
|
||||
# this; adding now could double the archive
|
||||
pending_bundled.setdefault(name)
|
||||
|
||||
def _emit(component: ConvertedLibrary) -> None:
|
||||
apply_extra_script(
|
||||
component, board_mcu=lambda: board_mcu, pio_platform=pio_platform
|
||||
)
|
||||
_assert_tree_has_code(
|
||||
component.get_require_name(),
|
||||
component.source_dir,
|
||||
"the download may be incomplete (run 'esphome clean-all')",
|
||||
)
|
||||
if isinstance(manifest_name := component.data.get("name"), str):
|
||||
converted_manifest_names.add(manifest_name)
|
||||
converted.append(
|
||||
_library_info(
|
||||
component.get_require_name(), component.source_dir, component.data
|
||||
)
|
||||
)
|
||||
_add_bundled_dependencies(component)
|
||||
|
||||
backend = LibraryBackend(
|
||||
platform=pio_platform,
|
||||
framework="arduino",
|
||||
emit=_emit,
|
||||
cache_key=cache_key,
|
||||
# The walk must not resolve bundled names from the registry;
|
||||
# _add_bundled_dependencies adds them after emit
|
||||
provides=_provided,
|
||||
)
|
||||
if external:
|
||||
convert_libraries(external, backend)
|
||||
for name in pending_bundled:
|
||||
if name in converted_manifest_names:
|
||||
# Manifest-name evidence: the converted library is this library,
|
||||
# so the bundled copy would double the archive. Warn like the
|
||||
# external_short_names twin: a coincidental collision would
|
||||
# otherwise surface only as link errors
|
||||
_LOGGER.warning(
|
||||
"Dependency %s is assumed satisfied by a converted library's "
|
||||
"manifest name; the bundled copy is not added",
|
||||
name,
|
||||
)
|
||||
continue
|
||||
bundled_names.add(name)
|
||||
bundled.append(_bundled_library(framework_path, name))
|
||||
|
||||
_warn_unfulfilled_provides(
|
||||
backend.provided_requests,
|
||||
bundled_names | converted_manifest_names | external_short_names,
|
||||
)
|
||||
|
||||
return bundled + converted
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Native (PlatformIO-free) build support for the ESP8266 Arduino core.
|
||||
|
||||
This package downloads the Arduino ESP8266 core and the xtensa-lx106
|
||||
toolchain, generates a ninja build for them plus the ESPHome sources, and
|
||||
drives the build directly — the ESP8266 equivalent of ``esphome.espidf``.
|
||||
|
||||
Deliberately importable without the esp8266 component to avoid circular
|
||||
imports; the component wires these modules in via lazy imports.
|
||||
"""
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Download and install the Arduino ESP8266 core, toolchain, and ninja.
|
||||
|
||||
Artifacts land in a machine-global cache (shared across projects, like the
|
||||
ESP-IDF install in ``esphome.espidf.framework``):
|
||||
|
||||
<cache>/arduino8266/frameworks/<version>/ framework-arduinoespressif8266
|
||||
<cache>/arduino8266/toolchains/<version>/ toolchain-xtensa (gcc 10.3)
|
||||
|
||||
Packages come from the PlatformIO registry (identical bits to the PlatformIO
|
||||
backend); ``ESPHOME_ARDUINO8266_*_MIRRORS`` overrides the URLs. ninja comes
|
||||
from PATH or the ninja PyPI wheel.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
from esphome.build_helpers.ccache import ccache_defaults_env
|
||||
from esphome.build_helpers.ninja import find_ninja
|
||||
from esphome.build_helpers.tools_cache import ARDUINO8266_TOOLS_CACHE, tools_cache_path
|
||||
from esphome.core import EsphomeError, Version
|
||||
from esphome.framework_helpers import str_to_lst_of_str
|
||||
from esphome.platformio.registry import install_package, prefetch_packages
|
||||
|
||||
FRAMEWORK_PACKAGE = "framework-arduinoespressif8266"
|
||||
TOOLCHAIN_PACKAGE = "toolchain-xtensa"
|
||||
# gcc 10.3, the toolchain Arduino core 3.x builds with; the build
|
||||
# generator's compile flags are tuned to it.
|
||||
TOOLCHAIN_VERSION = "2.100300.220621"
|
||||
|
||||
ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS = str_to_lst_of_str(
|
||||
os.environ.get("ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS", "")
|
||||
)
|
||||
ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS = str_to_lst_of_str(
|
||||
os.environ.get("ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS", "")
|
||||
)
|
||||
|
||||
|
||||
def get_arduino8266_tools_path() -> Path:
|
||||
# Machine-global so all projects share one install; see
|
||||
# espidf.framework.get_idf_tools_path for the location rationale.
|
||||
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
|
||||
|
||||
|
||||
# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the
|
||||
# encoder below cannot name 3.0.0/3.0.1 either (see its docstring)
|
||||
MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
|
||||
|
||||
|
||||
def framework_package_version(ver: Version) -> str:
|
||||
"""Map an Arduino core version to its registry package version (3.1.2 ->
|
||||
3.30102.0; the leading 3 is the package major).
|
||||
|
||||
Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor
|
||||
at MIN_FRAMEWORK_VERSION.
|
||||
"""
|
||||
if ver.major > 3:
|
||||
raise EsphomeError(
|
||||
f"Arduino core {ver} is not supported yet; "
|
||||
"the newest known core series is 3.x"
|
||||
)
|
||||
if ver <= Version(2, 6, 2):
|
||||
# Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same
|
||||
# boundary as _format_framework_arduino_version's era guard)
|
||||
raise EsphomeError(
|
||||
f"Arduino core {ver} uses an older package encoding than this "
|
||||
"helper implements (newer than 2.6.2)"
|
||||
)
|
||||
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
|
||||
|
||||
def get_framework_path(package_version: str) -> Path:
|
||||
return get_arduino8266_tools_path() / "frameworks" / package_version
|
||||
|
||||
|
||||
def get_toolchain_path() -> Path:
|
||||
return get_arduino8266_tools_path() / "toolchains" / TOOLCHAIN_VERSION
|
||||
|
||||
|
||||
class InstalledPaths(NamedTuple):
|
||||
"""Locations of the installed framework, toolchain, and ninja binary."""
|
||||
|
||||
framework: Path
|
||||
toolchain: Path
|
||||
ninja: Path
|
||||
|
||||
|
||||
def check_and_install(framework_version: Version) -> InstalledPaths:
|
||||
"""Ensure framework, toolchain, and ninja are installed; return their paths."""
|
||||
if framework_version < MIN_FRAMEWORK_VERSION:
|
||||
# Config validation enforces this too; keep the module honest when
|
||||
# called directly.
|
||||
raise EsphomeError(
|
||||
f"The native toolchain requires the Arduino core "
|
||||
f">= {MIN_FRAMEWORK_VERSION}, got {framework_version}"
|
||||
)
|
||||
# Probe the cheap local dependency before ~110 MB of downloads
|
||||
ninja_path = find_ninja()
|
||||
package_version = framework_package_version(framework_version)
|
||||
framework_path = get_framework_path(package_version)
|
||||
downloads_dir = get_arduino8266_tools_path() / "downloads"
|
||||
toolchain_path = get_toolchain_path()
|
||||
# One spec per package: the prefetch and the installs must agree
|
||||
specs = (
|
||||
(
|
||||
FRAMEWORK_PACKAGE,
|
||||
package_version,
|
||||
framework_path,
|
||||
ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS,
|
||||
("cores/esp8266", "tools/sdk", "libraries"),
|
||||
),
|
||||
(
|
||||
TOOLCHAIN_PACKAGE,
|
||||
TOOLCHAIN_VERSION,
|
||||
toolchain_path,
|
||||
ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS,
|
||||
# xtensa-lx106-elf pins the target: every gcc package has a bin/
|
||||
("bin", "xtensa-lx106-elf"),
|
||||
),
|
||||
)
|
||||
# Fetch both archives at once; the installs below verify and extract
|
||||
prefetch_packages([spec[:4] for spec in specs], downloads_dir)
|
||||
for name, version, dest, mirrors, expect in specs:
|
||||
install_package(name, version, dest, mirrors, downloads_dir, expect=expect)
|
||||
return InstalledPaths(
|
||||
framework=framework_path, toolchain=toolchain_path, ninja=ninja_path
|
||||
)
|
||||
|
||||
|
||||
def toolchain_tool(toolchain_path: Path, name: str) -> Path:
|
||||
"""Path to one toolchain tool (gcc, g++, ar, size, addr2line, ...).
|
||||
|
||||
The single owner of the ``bin/xtensa-lx106-elf-<name>`` layout and the
|
||||
Windows suffix, so a toolchain package bump touches one spot.
|
||||
"""
|
||||
suffix = ".exe" if os.name == "nt" else ""
|
||||
return toolchain_path / "bin" / f"xtensa-lx106-elf-{name}{suffix}"
|
||||
|
||||
|
||||
def get_build_env(toolchain_path: Path, ccache: str | None) -> dict[str, str]:
|
||||
env = os.environ.copy()
|
||||
# Drop empty entries: a trailing separator from an absent PATH would
|
||||
# make the shell search the current directory for tools
|
||||
parts = [
|
||||
str(toolchain_path / "bin"),
|
||||
*filter(None, env.get("PATH", "").split(os.pathsep)),
|
||||
]
|
||||
env["PATH"] = os.pathsep.join(parts)
|
||||
env.update(ccache_env(ccache))
|
||||
return env
|
||||
|
||||
|
||||
def ccache_env(ccache: str | None) -> dict[str, str]:
|
||||
"""Return ccache settings for the build subprocess (not os.environ).
|
||||
|
||||
``ccache`` is the pre-resolved binary (resolve_ccache_path), or None
|
||||
when disabled. Values the user already set in the environment are
|
||||
respected.
|
||||
"""
|
||||
if ccache is None:
|
||||
return {}
|
||||
return ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
|
||||
@@ -0,0 +1,107 @@
|
||||
"""Tiny cross-platform build steps invoked from the generated ninja file.
|
||||
|
||||
Plain script (not ``python -m``): it runs from ninja with whatever Python
|
||||
started esphome and must not depend on the package being importable.
|
||||
|
||||
Subcommands:
|
||||
ar <ar-binary> <archive> <rspfile> remove stale archive, then ``ar rc``
|
||||
copy <src> <dst> copy a file
|
||||
|
||||
The ar rspfile carries one object path per line (the generating rule must
|
||||
use ``$in_newline``, never ``$in``).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def _read_rspfile(rspfile: str) -> list[str]:
|
||||
r"""The object paths listed in ``rspfile``, unquoted.
|
||||
|
||||
GNU ar treats backslashes in response files as escapes (corrupts
|
||||
Windows paths), so the caller expands the list into argv; strip the
|
||||
simple surrounding quote ninja adds to special paths, then undo
|
||||
ninja's POSIX escape for an embedded quote ('a'\\''b.o' -> a'b.o).
|
||||
"""
|
||||
return [
|
||||
line[1:-1].replace("'\\''", "'")
|
||||
if len(line) >= 2 and line[0] == line[-1] and line[0] in "'\""
|
||||
else line
|
||||
for line in Path(rspfile).read_text(encoding="utf-8").splitlines()
|
||||
if line
|
||||
]
|
||||
|
||||
|
||||
def _run_ar(ar: str, archive: str, rspfile: str) -> int:
|
||||
# Remove first: ``ar rc`` replaces members but never drops ones whose
|
||||
# source was removed from the build, which would leak stale objects.
|
||||
Path(archive).unlink(missing_ok=True)
|
||||
objects = _read_rspfile(rspfile)
|
||||
if not objects:
|
||||
# An empty archive would "succeed" here and fail far away at link
|
||||
print(f"ar: no objects listed in {rspfile} for {archive}", file=sys.stderr)
|
||||
return 1
|
||||
# Batch by argv length: expanding the rspfile gives back the Windows
|
||||
# 32767-char command-line limit it existed to avoid. "rc" creates,
|
||||
# "q" appends the remainder.
|
||||
op = "rc"
|
||||
ok = False
|
||||
try:
|
||||
while objects:
|
||||
batch = [objects.pop(0)]
|
||||
batch_len = len(batch[0])
|
||||
while objects and batch_len + len(objects[0]) < 25000:
|
||||
batch_len += len(objects[0]) + 1
|
||||
batch.append(objects.pop(0))
|
||||
rc = subprocess.run(
|
||||
[ar, op, archive, *batch], check=False, close_fds=False
|
||||
).returncode
|
||||
if rc != 0:
|
||||
return rc
|
||||
op = "q"
|
||||
ok = True
|
||||
return 0
|
||||
finally:
|
||||
if not ok:
|
||||
# Any failure (bad exit, missing ar binary, interrupt) must not
|
||||
# leave a truncated archive behind
|
||||
Path(archive).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _run_copy(src: str, dst: str) -> int:
|
||||
try:
|
||||
shutil.copyfile(src, dst)
|
||||
except OSError as err:
|
||||
# Never leave a partially written output (e.g. a firmware image);
|
||||
# SameFileError means dst IS src, where unlinking destroys the input
|
||||
if not isinstance(err, shutil.SameFileError):
|
||||
Path(dst).unlink(missing_ok=True)
|
||||
raise
|
||||
return 0
|
||||
|
||||
|
||||
# mode -> (handler, expected operand count); surplus argv means a
|
||||
# mis-specified ninja rule and must error, not silently drop operands
|
||||
_MODES = {"ar": (_run_ar, 3), "copy": (_run_copy, 2)}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
if entry := _MODES.get(mode):
|
||||
handler, argc = entry
|
||||
args = sys.argv[2:]
|
||||
if len(args) != argc:
|
||||
print(
|
||||
f"build_tool {mode}: expected {argc} arguments, got {len(args)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
return handler(*args)
|
||||
print(f"unknown build_tool mode: {mode}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1 @@
|
||||
"""Build helpers shared by the native (non-PlatformIO) toolchains."""
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Shared ccache policy for build backends: env-knob parsing, binary
|
||||
resolution, and default ``CCACHE_*`` values."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
|
||||
from esphome.helpers import FALSY_BOOL_STRINGS, TRUTHY_BOOL_STRINGS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# cv.boolean's spelling tables plus the 1/0 env convention
|
||||
TRUTHY_ENV_STRINGS = TRUTHY_BOOL_STRINGS | {"1"}
|
||||
FALSY_ENV_STRINGS = FALSY_BOOL_STRINGS | {"0"}
|
||||
|
||||
|
||||
def _ccache_runs(ccache: str) -> bool:
|
||||
"""Return True when the ``ccache`` found on PATH actually runs."""
|
||||
return tool_version_runs(
|
||||
ccache,
|
||||
"Ignoring ccache at %s because it failed to run; compiling without ccache",
|
||||
)
|
||||
|
||||
|
||||
def parse_enable_env(name: str) -> bool | None:
|
||||
"""Strictly parse an on/off environment knob; None when unset or invalid.
|
||||
|
||||
``bool(str)`` truthiness would flip ``no``/``off`` to enabled, so only
|
||||
1/true/yes/on and 0/false/no/off count; anything else warns and reads
|
||||
as unset so the caller's default policy applies.
|
||||
"""
|
||||
raw = os.environ.get(name)
|
||||
if raw is None:
|
||||
return None
|
||||
lowered = raw.strip().lower()
|
||||
if lowered in TRUTHY_ENV_STRINGS:
|
||||
return True
|
||||
if lowered in FALSY_ENV_STRINGS:
|
||||
return False
|
||||
_LOGGER.warning("Ignoring unrecognized %s=%r; use 1 or 0", name, raw)
|
||||
return None
|
||||
|
||||
|
||||
def resolve_ccache_path() -> str | None:
|
||||
"""The ccache binary to wrap compiles with, or None when disabled.
|
||||
|
||||
An explicit ``ESPHOME_CCACHE_ENABLE=1`` skips the runnability probe; the
|
||||
Windows extended-length prefix is stripped before probing (#18399).
|
||||
"""
|
||||
import shutil
|
||||
|
||||
explicit = parse_enable_env("ESPHOME_CCACHE_ENABLE")
|
||||
if explicit is False:
|
||||
return None
|
||||
ccache = shutil.which("ccache")
|
||||
if ccache is None:
|
||||
if explicit:
|
||||
_LOGGER.warning(
|
||||
"ESPHOME_CCACHE_ENABLE is set but no ccache binary is on PATH; "
|
||||
"compiling without ccache"
|
||||
)
|
||||
return None
|
||||
ccache = strip_win_long_path_prefix(ccache)
|
||||
if not explicit and not _ccache_runs(ccache):
|
||||
return None
|
||||
return ccache
|
||||
|
||||
|
||||
def ccache_defaults_env(cache_dir: Path) -> dict[str, str]:
|
||||
"""Default ``CCACHE_*`` values for a build subprocess (not os.environ).
|
||||
|
||||
Values the user already set in the environment are respected. Depend
|
||||
mode is on: both native backends emit depfiles (-MMD / CMake), which
|
||||
keeps cache-miss overhead low.
|
||||
"""
|
||||
from esphome.core import CORE
|
||||
|
||||
# An unset build_path means the env was built before preload; fail loudly
|
||||
# rather than silently drop CCACHE_BASEDIR.
|
||||
if CORE.build_path is None:
|
||||
raise ValueError(
|
||||
"CORE.build_path must be set before constructing the build environment"
|
||||
)
|
||||
defaults = {
|
||||
"CCACHE_DIR": str(cache_dir),
|
||||
"CCACHE_NOHASHDIR": "true",
|
||||
"CCACHE_DEPEND": "1",
|
||||
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()),
|
||||
}
|
||||
return {k: v for k, v in defaults.items() if k not in os.environ}
|
||||
@@ -1,10 +1,10 @@
|
||||
"""Derive idedata from an ESP-IDF native-toolchain ``compile_commands.json``.
|
||||
"""Derive idedata from a native (non-PlatformIO) build's ``compile_commands.json``.
|
||||
|
||||
PlatformIO exposes a curated ``pio run -t idedata`` JSON; the native ESP-IDF
|
||||
toolchain has no such command, but its CMake build emits
|
||||
``build/compile_commands.json`` (CMAKE_EXPORT_COMPILE_COMMANDS). This module
|
||||
turns that file into the same fields consumers (IDE integration, clang-tidy)
|
||||
expect:
|
||||
PlatformIO exposes a curated ``pio run -t idedata`` JSON; the native
|
||||
toolchains have no such command, but each build produces a
|
||||
``compile_commands.json`` (CMAKE_EXPORT_COMPILE_COMMANDS for ESP-IDF, ninja's
|
||||
compdb tool otherwise). This module turns that file into the same fields
|
||||
consumers (IDE integration, clang-tidy) expect:
|
||||
|
||||
{cc_path, cxx_path, cxx_flags, defines, includes: {build, toolchain}}
|
||||
"""
|
||||
@@ -18,6 +18,20 @@ from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.helpers import write_file
|
||||
|
||||
# Everything idedata generation may raise after a successful link. Broad on
|
||||
# purpose, and shared by every consumer: idedata is a bonus artifact, so
|
||||
# these must be caught and warned about, never allowed to fail the build.
|
||||
IDEDATA_BEST_EFFORT_ERRORS = (
|
||||
EsphomeError,
|
||||
LookupError,
|
||||
OSError,
|
||||
RuntimeError,
|
||||
ValueError,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# C++ translation-unit suffixes used to identify ESPHome source files.
|
||||
@@ -120,7 +134,18 @@ def _pick_entry(entries: list[dict]) -> dict:
|
||||
raise ValueError("no C++ translation unit found in compile_commands.json")
|
||||
|
||||
|
||||
def _parse_entry(entry: dict) -> tuple[str, list[str], list[str], list[str]]:
|
||||
# Compiler launchers that may prefix a compile command; a closed launcher
|
||||
# denylist beats enumerating compiler names, an open set.
|
||||
_LAUNCHER_STEMS = frozenset({"ccache", "sccache", "distcc", "icecc", "buildcache"})
|
||||
|
||||
|
||||
def _is_launcher(token: str) -> bool:
|
||||
return Path(token).stem.lower() in _LAUNCHER_STEMS
|
||||
|
||||
|
||||
def parse_entry(
|
||||
entry: dict, launcher: str | None = None
|
||||
) -> tuple[str, list[str], list[str], list[str]]:
|
||||
"""Parse one compile_commands entry -> (cxx_path, defines, includes, cxx_flags)."""
|
||||
directory = Path(entry["directory"])
|
||||
tokens = _expand_response_files(_split_command(entry["command"]), directory)
|
||||
@@ -136,6 +161,20 @@ def _parse_entry(entry: dict) -> tuple[str, list[str], list[str], list[str]]:
|
||||
raw = os.path.normpath(directory / raw)
|
||||
return raw.replace("\\", "/")
|
||||
|
||||
# A launcher-wrapped command ("ccache g++ ...") names the compiler second
|
||||
if launcher is not None and tokens[:1] == [launcher]:
|
||||
tokens = tokens[1:]
|
||||
if not tokens:
|
||||
# _split_command("") is [] by design, and a command that is only
|
||||
# the launcher strips to nothing; fail like _pick_entry does
|
||||
# instead of an IndexError traceback
|
||||
raise ValueError(f"empty compile command for {entry.get('file')}")
|
||||
if _is_launcher(tokens[0]) and len(tokens) > 1 and not tokens[1].startswith("-"):
|
||||
# A stale compile DB built with a launcher the current run no longer
|
||||
# configures: the real compiler is the next token. Warn: the DB is
|
||||
# stale and worth regenerating.
|
||||
_LOGGER.warning("Stripping unconfigured launcher %s", tokens[0])
|
||||
tokens = tokens[1:]
|
||||
# token0 is the compiler path; the rest of the command already uses forward
|
||||
# slashes on Windows, so normalize it too for a consistent idedata file.
|
||||
cxx_path = tokens[0].replace("\\", "/")
|
||||
@@ -168,7 +207,7 @@ def _parse_entry(entry: dict) -> tuple[str, list[str], list[str], list[str]]:
|
||||
return cxx_path, defines, includes, cxx_flags
|
||||
|
||||
|
||||
def _get_toolchain_includes(cxx_path: str) -> list[str]:
|
||||
def get_toolchain_includes(cxx_path: str) -> list[str]:
|
||||
"""Query the compiler for its builtin ``#include <...>`` search dirs."""
|
||||
result = subprocess.run(
|
||||
[cxx_path, "-E", "-x", "c++", "-", "-v"],
|
||||
@@ -219,26 +258,114 @@ def _cc_path_from_cxx(cxx_path: str) -> str:
|
||||
return f"{stem}{suffix}"
|
||||
|
||||
|
||||
def idedata_from_build(compile_commands: Path) -> dict:
|
||||
def load_or_build_idedata(
|
||||
compile_commands: Path,
|
||||
elf_path: Path,
|
||||
cache: Path,
|
||||
launcher: str | None = None,
|
||||
) -> dict | None:
|
||||
"""Return idedata for a compile_commands.json build, cached on mtime.
|
||||
|
||||
Shared by the native ESP-IDF and ESP8266 Arduino toolchains. Returns None
|
||||
when the compile DB doesn't exist yet (nothing was built). ``launcher``
|
||||
is the compiler-launcher path (ccache) the build was generated with, if
|
||||
any; commands in the compile DB are prefixed with it.
|
||||
"""
|
||||
if not compile_commands.is_file():
|
||||
_LOGGER.debug("No %s yet; skipping idedata generation", compile_commands)
|
||||
return None
|
||||
|
||||
if cache.is_file() and cache.stat().st_mtime >= compile_commands.stat().st_mtime:
|
||||
try:
|
||||
cached = json.loads(cache.read_text(encoding="utf-8"))
|
||||
except (ValueError, OSError) as err:
|
||||
# A recurring cause (interrupted write, disk full) would otherwise
|
||||
# look like unexplained slow builds
|
||||
_LOGGER.warning("Discarding unreadable idedata cache %s: %s", cache, err)
|
||||
else:
|
||||
# Rebuild pre-cc_path caches on the field, not the timestamp;
|
||||
# the type check keeps "in" from substring-matching a string
|
||||
if isinstance(cached, dict) and "cc_path" in cached:
|
||||
return cached
|
||||
|
||||
data = idedata_from_build(compile_commands, launcher)
|
||||
data["prog_path"] = str(elf_path)
|
||||
cache.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Atomic so a crash mid-write cannot leave a truncated cache
|
||||
write_file(cache, json.dumps(data, indent=2) + "\n")
|
||||
return data
|
||||
|
||||
|
||||
def reject_launcher_compiler(cxx_path: str) -> None:
|
||||
"""Reject a compile DB that names a launcher (ccache) as the compiler.
|
||||
|
||||
Reject before the toolchain probe, which would fail opaquely on a
|
||||
launcher; the unusable compile DB must never be cached or consumed.
|
||||
"""
|
||||
if _is_launcher(cxx_path):
|
||||
raise EsphomeError(
|
||||
f"compile_commands.json names the launcher {cxx_path} as the "
|
||||
"compiler; the compile database is unusable"
|
||||
)
|
||||
|
||||
|
||||
def idedata_from_build(compile_commands: Path, launcher: str | None = None) -> dict:
|
||||
"""Parse compile_commands.json into the idedata fields consumers expect.
|
||||
|
||||
A single ESP-IDF compile entry only carries its own component's REQUIRES
|
||||
include set, but consumers (clang-tidy) analyze ESPHome headers that
|
||||
transitively pull in other components. So take cxx_path / cxx_flags /
|
||||
defines from a representative ESPHome TU, but union the include dirs across
|
||||
all ESPHome TUs to get a project-wide superset (as PlatformIO's idedata
|
||||
provides).
|
||||
A single compile entry only carries the include set its own translation
|
||||
unit was built with (per-component under ESP-IDF), but consumers
|
||||
(clang-tidy) analyze ESPHome headers that transitively pull in other
|
||||
components. So take cxx_path / cxx_flags / defines from a representative
|
||||
ESPHome TU, but union the include dirs across all ESPHome TUs to get a
|
||||
project-wide superset (as PlatformIO's idedata provides).
|
||||
"""
|
||||
entries = json.loads(Path(compile_commands).read_text(encoding="utf-8"))
|
||||
cxx_path, defines, _, cxx_flags = _parse_entry(_pick_entry(entries))
|
||||
|
||||
build_includes: dict[str, None] = {}
|
||||
representative = _pick_entry(entries)
|
||||
cxx_path, defines, rep_includes, cxx_flags = parse_entry(representative, launcher)
|
||||
reject_launcher_compiler(cxx_path)
|
||||
|
||||
# Seed with the representative's includes so it is not parsed twice
|
||||
has_esphome_tu = _is_esphome_src(representative["file"])
|
||||
build_includes: dict[str, None] = dict.fromkeys(
|
||||
rep_includes if has_esphome_tu else ()
|
||||
)
|
||||
|
||||
def _shape(entry: dict) -> str:
|
||||
# The command minus its TU-specific paths: entries sharing a shape
|
||||
# carry identical include sets (one ninja rule), so tokenize once
|
||||
# per shape instead of once per TU. Response-file commands never
|
||||
# dedupe: per-object .rsp names strip to one shape while the files
|
||||
# may hold different include sets.
|
||||
command = entry["command"]
|
||||
if "@" in command:
|
||||
return f"unique:{entry['file']}"
|
||||
return command.replace(entry.get("file", ""), "").replace(
|
||||
entry.get("output", ""), ""
|
||||
)
|
||||
|
||||
seen_shapes = {_shape(representative)}
|
||||
for entry in entries:
|
||||
if not _is_esphome_src(entry["file"]):
|
||||
if entry is representative or not _is_esphome_src(entry["file"]):
|
||||
continue
|
||||
for inc in _parse_entry(entry)[2]:
|
||||
has_esphome_tu = True
|
||||
if (shape := _shape(entry)) in seen_shapes:
|
||||
_LOGGER.debug("Include union: %s shares a command shape", entry["file"])
|
||||
continue
|
||||
seen_shapes.add(shape)
|
||||
for inc in parse_entry(entry, launcher)[2]:
|
||||
build_includes.setdefault(inc, None)
|
||||
|
||||
if not has_esphome_tu:
|
||||
# _pick_entry fell back to an arbitrary C++ entry: idedata built
|
||||
# from it breaks clang-tidy/IDE consumers, and a one-time warning
|
||||
# would be cached into permanence. The best-effort call sites
|
||||
# downgrade this to a build warning.
|
||||
raise EsphomeError(
|
||||
f"No ESPHome translation unit found in {compile_commands}; "
|
||||
"refusing to cache unusable idedata"
|
||||
)
|
||||
|
||||
return {
|
||||
"cc_path": _cc_path_from_cxx(cxx_path),
|
||||
"cxx_path": cxx_path,
|
||||
@@ -246,6 +373,6 @@ def idedata_from_build(compile_commands: Path) -> dict:
|
||||
"defines": defines,
|
||||
"includes": {
|
||||
"build": list(build_includes),
|
||||
"toolchain": _get_toolchain_includes(cxx_path),
|
||||
"toolchain": get_toolchain_includes(cxx_path),
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Platform-neutral helpers for ninja-driven native builds."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ninja_runs(binary: str) -> bool:
|
||||
"""Whether the ninja found on PATH actually runs (see tool_version_runs)."""
|
||||
return tool_version_runs(
|
||||
binary,
|
||||
"Ignoring ninja at %s because it failed to run; "
|
||||
"falling back to the bundled wheel",
|
||||
)
|
||||
|
||||
|
||||
def find_ninja() -> Path:
|
||||
"""Locate the ninja binary: a runnable PATH hit first, else the ninja
|
||||
PyPI wheel."""
|
||||
if binary := shutil.which("ninja"):
|
||||
binary = strip_win_long_path_prefix(binary)
|
||||
if _ninja_runs(binary):
|
||||
return Path(binary)
|
||||
import_error: ImportError | None = None
|
||||
try:
|
||||
import ninja
|
||||
except ImportError as err:
|
||||
import_error = err
|
||||
wheel_binary = None
|
||||
else:
|
||||
wheel_binary = Path(ninja.BIN_DIR) / (
|
||||
"ninja.exe" if os.name == "nt" else "ninja"
|
||||
)
|
||||
if wheel_binary is None or not wheel_binary.is_file():
|
||||
raise EsphomeError(
|
||||
"ninja not found on PATH or in the ninja package; reinstall the "
|
||||
"esphome Python environment"
|
||||
) from import_error
|
||||
return wheel_binary
|
||||
|
||||
|
||||
def escape(value: Path | str) -> str:
|
||||
"""Escape a path or token for a ninja file."""
|
||||
return str(value).replace("$", "$$").replace(":", "$:").replace(" ", "$ ")
|
||||
|
||||
|
||||
def quote_arg(tok: str) -> str:
|
||||
"""Quote with the CreateProcess argv rule (as ``subprocess.list2cmdline``):
|
||||
backslash runs double only before a quote. Windows-only; ``$`` must
|
||||
already be doubled for ninja.
|
||||
"""
|
||||
quoted = re.sub(r'(\\*)"', lambda m: m.group(1) * 2 + '\\"', tok)
|
||||
quoted = re.sub(r"(\\+)\Z", lambda m: m.group(1) * 2, quoted)
|
||||
return f'"{quoted}"'
|
||||
|
||||
|
||||
# Force-quote any token containing a character outside the shlex.quote-style
|
||||
# safe set: ninja hands POSIX commands to /bin/sh -c, so bare (, ;, <, *, `
|
||||
# and friends would be re-parsed as shell syntax.
|
||||
_NEEDS_QUOTE = re.compile(r"[^\w@%+=:,./-]")
|
||||
|
||||
|
||||
def shell_token(tok: str, force: bool = False) -> str:
|
||||
"""Re-quote a lexed token for the platform shell; ``force`` always quotes.
|
||||
|
||||
Single quotes on POSIX (/bin/sh), the argv rule on Windows
|
||||
(CreateProcess). ``$`` is doubled first because ninja expands it before
|
||||
the command reaches the shell.
|
||||
"""
|
||||
tok = tok.replace("$", "$$") # ninja would expand a bare $ to nothing
|
||||
if not (force or not tok or _NEEDS_QUOTE.search(tok)):
|
||||
return tok
|
||||
# An empty token must become '' / "" or it vanishes from the argv
|
||||
if os.name == "nt":
|
||||
return quote_arg(tok)
|
||||
# shlex.quote's rule; inlined because the $-doubled token must not be
|
||||
# re-examined for safe characters
|
||||
return "'" + tok.replace("'", "'\"'\"'") + "'"
|
||||
|
||||
|
||||
def quote_path(value: Path | str) -> str:
|
||||
"""Force-quote a path for the ninja command line (shell/CreateProcess)."""
|
||||
return shell_token(str(value), force=True)
|
||||
@@ -0,0 +1,24 @@
|
||||
"""The PlatformIO-format size bar shared by the native toolchains."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
def format_bar(used: int, total: int) -> str:
|
||||
"""Match PlatformIO's ``_format_availale_bytes`` (sic, pioupload.py) exactly."""
|
||||
pct_raw = used / total if total else 0
|
||||
blocks = 10
|
||||
filled = min(int(round(blocks * pct_raw)), blocks)
|
||||
progress = "=" * filled
|
||||
return (
|
||||
f"[{progress:<{blocks}}] {pct_raw: 6.1%} "
|
||||
f"(used {used:d} bytes from {total:d} bytes)"
|
||||
)
|
||||
|
||||
|
||||
def print_size_line(label: str, used: int, total: int) -> None:
|
||||
"""One PlatformIO-format summary line (``RAM``/``Flash``).
|
||||
|
||||
The label padding is part of the format: ``script/ci_memory_impact_extract.py``
|
||||
matches these lines verbatim.
|
||||
"""
|
||||
print(f"{label + ':':<7}{format_bar(used, total)}")
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Machine-global tools cache location shared by the native backends."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def tools_cache_path(env_var: str, subdir: str) -> Path:
|
||||
"""A backend's machine-global tools directory, with an env override.
|
||||
|
||||
A blank/whitespace override is treated as unset: ``Path("")`` resolves
|
||||
to the CWD, which ``clean-all`` would then delete.
|
||||
"""
|
||||
import platformdirs
|
||||
|
||||
from esphome.helpers import get_str_env
|
||||
|
||||
if prefix := get_str_env(env_var, "").strip():
|
||||
# resolve(): symlinked prefixes otherwise trip idf.py's
|
||||
# venv-mismatch warning on every build
|
||||
return Path(prefix).expanduser().resolve()
|
||||
# appauthor=False keeps the Windows path short (no vendor segment);
|
||||
# deep IDF trees run into MAX_PATH otherwise
|
||||
return (
|
||||
Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / subdir
|
||||
).resolve()
|
||||
|
||||
|
||||
# (env override, cache subdir) per native backend. writer.clean_all wipes
|
||||
# every entry via tools_cache_path, so listing a cache here is the single
|
||||
# step that registers it for removal; the backends' own path getters use
|
||||
# the same named pairs so the two cannot drift.
|
||||
IDF_TOOLS_CACHE = ("ESPHOME_ESP_IDF_PREFIX", "idf")
|
||||
SDK_NRF_TOOLS_CACHE = ("ESPHOME_SDK_NRF_PREFIX", "sdk-nrf")
|
||||
ARDUINO8266_TOOLS_CACHE = ("ESPHOME_ARDUINO8266_PREFIX", "arduino8266")
|
||||
TOOLS_CACHE_SPECS = (IDF_TOOLS_CACHE, SDK_NRF_TOOLS_CACHE, ARDUINO8266_TOOLS_CACHE)
|
||||
@@ -100,6 +100,21 @@ def _refresh_sidecar() -> bool:
|
||||
)
|
||||
return False
|
||||
if old is not None and old.can_apply_to_core():
|
||||
if (
|
||||
old.toolchain is not None
|
||||
and CORE.toolchain is not None
|
||||
and old.toolchain != CORE.toolchain.value
|
||||
):
|
||||
# Platforms normalize toolchain-sensitive keys differently;
|
||||
# never cache a config validated under a different toolchain
|
||||
# than the compile's
|
||||
_LOGGER.debug(
|
||||
"Not caching: config validated with toolchain %r but the "
|
||||
"last compile used %r",
|
||||
CORE.toolchain.value,
|
||||
old.toolchain,
|
||||
)
|
||||
return False
|
||||
# Compile-written; nothing to refresh.
|
||||
return True
|
||||
if CORE.build_path is not None and CORE.build_path.exists():
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
@@ -16,6 +18,7 @@ from esphome.components.esp32 import (
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
@@ -225,7 +228,7 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
}
|
||||
|
||||
|
||||
def validate_adc_pin(value):
|
||||
def validate_adc_pin(value: Any) -> ConfigType | str:
|
||||
if str(value).upper() == "VCC":
|
||||
if CORE.is_rp2:
|
||||
return pins.internal_gpio_input_pin_schema(29)
|
||||
|
||||
@@ -52,7 +52,7 @@ _attenuation = cv.enum(ATTENUATION_MODES, lower=True)
|
||||
_sampling_mode = cv.enum(SAMPLING_MODES, lower=True)
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
|
||||
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
|
||||
|
||||
@@ -120,7 +120,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
|
||||
def _overlay_io_channels():
|
||||
def _overlay_io_channels() -> str:
|
||||
channel_count = CORE.data[CONF_ADC_CHANNEL_ID]
|
||||
entries = ", ".join(f"<&adc {channel_id}>" for channel_id in range(channel_count))
|
||||
return f"""
|
||||
@@ -132,7 +132,7 @@ def _overlay_io_channels():
|
||||
"""
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
@@ -129,7 +130,7 @@ def _register_provisioning_source(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
def validate_encryption_key(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
try:
|
||||
decoded = base64.b64decode(value, validate=True)
|
||||
@@ -217,7 +218,7 @@ def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _validate_supports_response(value):
|
||||
def _validate_supports_response(value: Any) -> str:
|
||||
"""Validate supports_response after auto-detection has set the value."""
|
||||
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
|
||||
|
||||
@@ -256,7 +257,7 @@ ENCRYPTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def _encryption_schema(config):
|
||||
def _encryption_schema(config: ConfigType | None) -> ConfigType:
|
||||
if config is None:
|
||||
config = {}
|
||||
return ENCRYPTION_SCHEMA(config)
|
||||
@@ -393,7 +394,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
if actions := config.get(CONF_ACTIONS, []):
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.Pvariable] = []
|
||||
triggers: list[cg.MockObj] = []
|
||||
for conf in actions:
|
||||
func_args: list[tuple[MockObj, str]] = []
|
||||
service_template_args: list[MockObj] = [] # User service argument types
|
||||
@@ -581,7 +582,7 @@ async def homeassistant_service_to_code(
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
):
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||
@@ -647,7 +648,7 @@ async def homeassistant_service_to_code(
|
||||
return var
|
||||
|
||||
|
||||
def validate_homeassistant_event(value):
|
||||
def validate_homeassistant_event(value: Any) -> str:
|
||||
value = cv.string(value)
|
||||
if not value.startswith("esphome."):
|
||||
raise cv.Invalid(
|
||||
@@ -676,7 +677,12 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
|
||||
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
||||
async def homeassistant_event_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
@@ -724,7 +730,12 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
|
||||
async def homeassistant_tag_scanned_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
@@ -740,7 +751,7 @@ CONF_SUCCESS = "success"
|
||||
CONF_ERROR_MESSAGE = "error_message"
|
||||
|
||||
|
||||
def _validate_api_respond_data(config):
|
||||
def _validate_api_respond_data(config: ConfigType) -> ConfigType:
|
||||
"""Set flag during validation so AUTO_LOAD can include json component."""
|
||||
if CONF_DATA in config:
|
||||
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
|
||||
@@ -824,7 +835,12 @@ API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
|
||||
@automation.register_condition(
|
||||
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
|
||||
)
|
||||
async def api_connected_to_code(config, condition_id, template_arg, args):
|
||||
async def api_connected_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
var = cg.new_Pvariable(condition_id, template_arg)
|
||||
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
|
||||
cg.add(var.set_state_subscription_only(templ))
|
||||
|
||||
@@ -423,12 +423,6 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
|
||||
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
|
||||
#endif
|
||||
|
||||
float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
|
||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
bool has_subscriber = false;
|
||||
@@ -553,10 +547,6 @@ const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_sta
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t APIServer::get_port() const { return this->port_; }
|
||||
|
||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
|
||||
const LogString *fail_log_msg, bool make_active) {
|
||||
|
||||
@@ -51,8 +51,8 @@ class APIServer final : public Component,
|
||||
public:
|
||||
APIServer();
|
||||
void setup() override;
|
||||
uint16_t get_port() const;
|
||||
float get_setup_priority() const override;
|
||||
uint16_t get_port() const { return this->port_; }
|
||||
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
@@ -63,9 +63,9 @@ class APIServer final : public Component,
|
||||
#ifdef USE_CAMERA
|
||||
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
|
||||
#endif
|
||||
void set_port(uint16_t port);
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
void set_batch_delay(uint16_t batch_delay);
|
||||
void set_port(uint16_t port) { this->port_ = port; }
|
||||
void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
void set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
uint16_t get_batch_delay() const { return batch_delay_; }
|
||||
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
|
||||
|
||||
|
||||
@@ -4,9 +4,12 @@ The platform analog of esp32_ble / rp2040_ble: owns the Beken BDK BLE stack
|
||||
bring-up and the controller BLE address. Consumers (bk72xx_ble_tracker) build
|
||||
on this component and contain no SDK calls of their own.
|
||||
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7238/BK7252N/BK7253
|
||||
(BLE 5.2), and any future BLE-5.x SoC. Known non-5.x families are rejected in
|
||||
to_code; unknown families are capability-checked at compile time via
|
||||
Supported SoCs (BLE 5.x): BK7231N/BK7236 (BLE 5.1), BK7252N/BK7253 (BLE 5.2),
|
||||
and any future BLE-5.x SoC. BK7238 (BLE 5.2) is blocked for now: with BLE
|
||||
compiled in, the Beken SDK erases the bootloader flash sector at boot because
|
||||
LibreTiny's partition table has no BLE bonding entry (esphome#18646,
|
||||
libretiny-eu/libretiny#408). Known non-5.x families and BK7238 are rejected in
|
||||
to_code. Unknown families are capability-checked at compile time via
|
||||
`__has_include("app_ble.h")`, a header only on the BLE 5.x include path
|
||||
(ble_api.h ships for every SoC, so it cannot be the probe). A non-5.x build
|
||||
fails with a clear #error.
|
||||
@@ -65,6 +68,14 @@ def _unsupported_family_message(family: str) -> str | None:
|
||||
)
|
||||
if family == FAMILY_BK7231Q:
|
||||
return "bk72xx_ble does not support BK7231Q: this SoC has no BLE"
|
||||
if family == FAMILY_BK7238:
|
||||
return (
|
||||
"bk72xx_ble is disabled on BK7238: with BLE compiled in, the Beken SDK "
|
||||
"erases the bootloader flash sector at boot and the device can no longer "
|
||||
"start (see https://github.com/esphome/esphome/issues/18646); support "
|
||||
"returns once the LibreTiny partition table fix "
|
||||
"(libretiny-eu/libretiny#408) is released"
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
@@ -113,18 +124,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
# BK7231N, but NOT on BK7238 (its BLE stack has no such symbol; the address is
|
||||
# derived from the WiFi MAC instead — the BDK's own fallback). Tell the C++
|
||||
# which path is available so it doesn't reference a missing symbol.
|
||||
family = libretiny.get_libretiny_family()
|
||||
if family == FAMILY_BK7231N:
|
||||
if libretiny.get_libretiny_family() == FAMILY_BK7231N:
|
||||
cg.add_define("BK72XX_BLE_HAS_COMMON_BDADDR")
|
||||
elif family == FAMILY_BK7238:
|
||||
# ESPHome's LibreTiny disables BLE on BK7238 because the SDK can hang at
|
||||
# WiFi STA startup when BLE init runs. This component re-enables BLE, so
|
||||
# warn loudly: BK7238 is accepted but not hardware-verified and may be
|
||||
# WiFi-unstable with BLE on.
|
||||
_LOGGER.warning(
|
||||
"bk72xx_ble on BK7238: enabling BLE is known to risk a WiFi STA startup "
|
||||
"hang on this family and is not yet hardware-verified. Expect possible "
|
||||
"instability."
|
||||
)
|
||||
|
||||
cg.add_define("USE_BK72XX_BLE")
|
||||
|
||||
@@ -16,14 +16,15 @@ from esphome.const import (
|
||||
DEVICE_CLASS_RESTART,
|
||||
DEVICE_CLASS_UPDATE,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_device_class,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -88,7 +89,7 @@ _CALLBACK_AUTOMATIONS = (
|
||||
|
||||
|
||||
@setup_entity("button")
|
||||
async def setup_button_core_(var, config):
|
||||
async def setup_button_core_(var: MockObj, config: ConfigType) -> None:
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
setup_device_class(config)
|
||||
@@ -101,7 +102,7 @@ async def setup_button_core_(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_button(var, config):
|
||||
async def register_button(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("button", config)
|
||||
@@ -109,7 +110,7 @@ async def register_button(var, config):
|
||||
await setup_button_core_(var, config)
|
||||
|
||||
|
||||
async def new_button(config, *args):
|
||||
async def new_button(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_button(var, config)
|
||||
return var
|
||||
@@ -125,11 +126,16 @@ BUTTON_PRESS_SCHEMA = maybe_simple_id(
|
||||
@automation.register_action(
|
||||
"button.press", PressAction, BUTTON_PRESS_SCHEMA, synchronous=True
|
||||
)
|
||||
async def button_press_to_code(config, action_id, template_arg, args):
|
||||
async def button_press_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(button_ns.using)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#include "captive_index.h"
|
||||
#include "scan_list.h"
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
@@ -33,8 +34,10 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
// Invariant: only bounded in-memory work under the lock; the network send
|
||||
// happens later in request->send()
|
||||
wifi::ScanResultsLock lock(wifi::global_wifi_component);
|
||||
for (const auto &scan : wifi::global_wifi_component->get_scan_result()) {
|
||||
if (scan.get_is_hidden())
|
||||
const auto &results = wifi::global_wifi_component->get_scan_result();
|
||||
for (const auto &scan : results) {
|
||||
bool with_auth = false;
|
||||
if (!should_show_scan_entry(results, scan, with_auth))
|
||||
continue;
|
||||
|
||||
json_escape_into_buffer(escaped_ssid, scan.get_ssid());
|
||||
@@ -44,10 +47,10 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
stream->print(ESPHOME_F("\",\"rssi\":"));
|
||||
stream->print(scan.get_rssi());
|
||||
stream->print(ESPHOME_F(",\"lock\":"));
|
||||
stream->print(scan.get_with_auth());
|
||||
stream->print(with_auth);
|
||||
stream->print(ESPHOME_F("}"));
|
||||
#else
|
||||
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", escaped_ssid, scan.get_rssi(), scan.get_with_auth());
|
||||
stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", escaped_ssid, scan.get_rssi(), with_auth);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
// A scan lists every BSSID, so one SSID can appear several times. Returns true for
|
||||
// the strongest entry per SSID (earliest on ties), never for hidden entries. scan
|
||||
// must be an element of results. with_auth is written only when returning true and
|
||||
// is set if any entry with that SSID needs a key. Templated for host tests.
|
||||
template<typename Results, typename Entry>
|
||||
bool should_show_scan_entry(const Results &results, const Entry &scan, bool &with_auth) {
|
||||
if (scan.get_is_hidden())
|
||||
return false;
|
||||
const int8_t rssi = scan.get_rssi();
|
||||
bool any_auth = false;
|
||||
for (const auto &other : results) {
|
||||
if (other.get_is_hidden() || !other.ssid_equals(scan))
|
||||
continue;
|
||||
// Same array, so address order is index order. scan fails both checks against itself.
|
||||
if (other.get_rssi() > rssi || (other.get_rssi() == rssi && &other < &scan))
|
||||
return false;
|
||||
any_auth |= other.get_with_auth();
|
||||
}
|
||||
with_auth = any_auth;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::captive_portal
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
@@ -48,13 +50,19 @@ from esphome.const import (
|
||||
CONF_VISUAL,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, Lambda, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import LambdaExpression, MockObjClass
|
||||
from esphome.cpp_generator import (
|
||||
LambdaExpression,
|
||||
MockObj,
|
||||
MockObjClass,
|
||||
TemplateArgsType,
|
||||
)
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -132,7 +140,7 @@ VISUAL_TEMPERATURE_STEP_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def visual_temperature_step(value):
|
||||
def visual_temperature_step(value: Any) -> ConfigType:
|
||||
# Allow defining target/current temperature steps separately
|
||||
if isinstance(value, dict):
|
||||
return VISUAL_TEMPERATURE_STEP_SCHEMA(value)
|
||||
@@ -273,7 +281,7 @@ def climate_schema(
|
||||
|
||||
|
||||
@setup_entity("climate")
|
||||
async def setup_climate_core_(var, config):
|
||||
async def setup_climate_core_(var: MockObj, config: ConfigType) -> None:
|
||||
visual = config.get(CONF_VISUAL, {})
|
||||
if (min_temp := visual.get(CONF_MIN_TEMPERATURE)) is not None:
|
||||
cg.add_define("USE_CLIMATE_VISUAL_OVERRIDES")
|
||||
@@ -443,7 +451,7 @@ async def setup_climate_core_(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_climate(var, config):
|
||||
async def register_climate(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("climate", config)
|
||||
@@ -451,7 +459,7 @@ async def register_climate(var, config):
|
||||
await setup_climate_core_(var, config)
|
||||
|
||||
|
||||
async def new_climate(config, *args):
|
||||
async def new_climate(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_climate(var, config)
|
||||
return var
|
||||
@@ -485,7 +493,12 @@ CLIMATE_CONTROL_ACTION_SCHEMA = cv.Schema(
|
||||
CLIMATE_CONTROL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
async def climate_control_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
# All configured fields are folded into a single stateless lambda whose
|
||||
@@ -549,5 +562,5 @@ async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(climate_ns.using)
|
||||
|
||||
@@ -511,29 +511,6 @@ ClimateTraits Climate::get_traits() {
|
||||
return traits;
|
||||
}
|
||||
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||
this->visual_min_temperature_override_ = visual_min_temperature_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
|
||||
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_temperature_step_override(float target, float current) {
|
||||
this->visual_target_temperature_step_override_ = target;
|
||||
this->visual_current_temperature_step_override_ = current;
|
||||
}
|
||||
|
||||
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
|
||||
this->visual_min_humidity_override_ = visual_min_humidity_override;
|
||||
}
|
||||
|
||||
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
|
||||
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
||||
}
|
||||
#endif
|
||||
|
||||
ClimateCall Climate::make_call() { return ClimateCall(this); }
|
||||
|
||||
ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
||||
|
||||
@@ -228,11 +228,22 @@ class Climate : public EntityBase {
|
||||
ClimateTraits get_traits();
|
||||
|
||||
#ifdef USE_CLIMATE_VISUAL_OVERRIDES
|
||||
void set_visual_min_temperature_override(float visual_min_temperature_override);
|
||||
void set_visual_max_temperature_override(float visual_max_temperature_override);
|
||||
void set_visual_temperature_step_override(float target, float current);
|
||||
void set_visual_min_humidity_override(float visual_min_humidity_override);
|
||||
void set_visual_max_humidity_override(float visual_max_humidity_override);
|
||||
void set_visual_min_temperature_override(float visual_min_temperature_override) {
|
||||
this->visual_min_temperature_override_ = visual_min_temperature_override;
|
||||
}
|
||||
void set_visual_max_temperature_override(float visual_max_temperature_override) {
|
||||
this->visual_max_temperature_override_ = visual_max_temperature_override;
|
||||
}
|
||||
void set_visual_temperature_step_override(float target, float current) {
|
||||
this->visual_target_temperature_step_override_ = target;
|
||||
this->visual_current_temperature_step_override_ = current;
|
||||
}
|
||||
void set_visual_min_humidity_override(float visual_min_humidity_override) {
|
||||
this->visual_min_humidity_override_ = visual_min_humidity_override;
|
||||
}
|
||||
void set_visual_max_humidity_override(float visual_max_humidity_override) {
|
||||
this->visual_max_humidity_override_ = visual_max_humidity_override;
|
||||
}
|
||||
#endif
|
||||
|
||||
/// Set the supported custom fan modes (stored on Climate, referenced by ClimateTraits).
|
||||
|
||||
@@ -46,7 +46,7 @@ from esphome.core.entity_helpers import (
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import LambdaExpression, MockObj, MockObjClass
|
||||
from esphome.types import ConfigType, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType, TemplateArgsType
|
||||
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
|
||||
@@ -162,7 +162,7 @@ _COVER_SCHEMA = (
|
||||
_COVER_SCHEMA.add_extra(entity_duplicate_validator("cover"))
|
||||
|
||||
|
||||
def _validate_mqtt_state_topics(config):
|
||||
def _validate_mqtt_state_topics(config: ConfigType) -> ConfigType:
|
||||
if config.get(CONF_MQTT_JSON_STATE_PAYLOAD):
|
||||
if CONF_POSITION_STATE_TOPIC in config:
|
||||
raise cv.Invalid(
|
||||
@@ -201,7 +201,7 @@ def cover_schema(
|
||||
|
||||
|
||||
@setup_entity("cover")
|
||||
async def setup_cover_core_(var, config):
|
||||
async def setup_cover_core_(var: MockObj, config: ConfigType) -> None:
|
||||
setup_device_class(config)
|
||||
|
||||
if CONF_ON_OPEN in config:
|
||||
@@ -235,7 +235,7 @@ async def setup_cover_core_(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_cover(var, config):
|
||||
async def register_cover(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("cover", config)
|
||||
@@ -243,7 +243,7 @@ async def register_cover(var, config):
|
||||
await setup_cover_core_(var, config)
|
||||
|
||||
|
||||
async def new_cover(config, *args):
|
||||
async def new_cover(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_cover(var, config)
|
||||
return var
|
||||
@@ -259,7 +259,12 @@ COVER_ACTION_SCHEMA = maybe_simple_id(
|
||||
@automation.register_action(
|
||||
"cover.open", OpenAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_open_to_code(config, action_id, template_arg, args):
|
||||
async def cover_open_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -267,7 +272,12 @@ async def cover_open_to_code(config, action_id, template_arg, args):
|
||||
@automation.register_action(
|
||||
"cover.close", CloseAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_close_to_code(config, action_id, template_arg, args):
|
||||
async def cover_close_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -275,7 +285,12 @@ async def cover_close_to_code(config, action_id, template_arg, args):
|
||||
@automation.register_action(
|
||||
"cover.stop", StopAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_stop_to_code(config, action_id, template_arg, args):
|
||||
async def cover_stop_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -283,7 +298,12 @@ async def cover_stop_to_code(config, action_id, template_arg, args):
|
||||
@automation.register_action(
|
||||
"cover.toggle", ToggleAction, COVER_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def cover_toggle_to_code(config, action_id, template_arg, args):
|
||||
async def cover_toggle_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -421,5 +441,5 @@ automation.register_condition(
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(cover_ns.using)
|
||||
|
||||
@@ -135,10 +135,6 @@ CoverCall &CoverCall::set_stop(bool stop) {
|
||||
this->stop_ = stop;
|
||||
return *this;
|
||||
}
|
||||
bool CoverCall::get_stop() const { return this->stop_; }
|
||||
|
||||
CoverCall Cover::make_call() { return {this}; }
|
||||
|
||||
void Cover::publish_state(bool save) {
|
||||
this->position = clamp(this->position, 0.0f, 1.0f);
|
||||
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
|
||||
@@ -184,9 +180,6 @@ optional<CoverRestoreState> Cover::restore_state_() {
|
||||
return recovered;
|
||||
}
|
||||
|
||||
bool Cover::is_fully_open() const { return this->position == COVER_OPEN; }
|
||||
bool Cover::is_fully_closed() const { return this->position == COVER_CLOSED; }
|
||||
|
||||
CoverCall CoverRestoreState::to_call(Cover *cover) {
|
||||
auto call = cover->make_call();
|
||||
auto traits = cover->get_traits();
|
||||
|
||||
@@ -50,7 +50,7 @@ class CoverCall {
|
||||
void perform();
|
||||
|
||||
const optional<float> &get_position() const;
|
||||
bool get_stop() const;
|
||||
bool get_stop() const { return this->stop_; }
|
||||
const optional<float> &get_tilt() const;
|
||||
const optional<bool> &get_toggle() const;
|
||||
|
||||
@@ -123,7 +123,7 @@ class Cover : public EntityBase {
|
||||
float tilt{COVER_OPEN};
|
||||
|
||||
/// Construct a new cover call used to control the cover.
|
||||
CoverCall make_call();
|
||||
CoverCall make_call() { return {this}; }
|
||||
|
||||
template<typename F> void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward<F>(f)); }
|
||||
|
||||
@@ -139,9 +139,9 @@ class Cover : public EntityBase {
|
||||
virtual CoverTraits get_traits() = 0;
|
||||
|
||||
/// Helper method to check if the cover is fully open. Equivalent to comparing .position against 1.0
|
||||
bool is_fully_open() const;
|
||||
bool is_fully_open() const { return this->position == COVER_OPEN; }
|
||||
/// Helper method to check if the cover is fully closed. Equivalent to comparing .position against 0.0
|
||||
bool is_fully_closed() const;
|
||||
bool is_fully_closed() const { return this->position == COVER_CLOSED; }
|
||||
|
||||
protected:
|
||||
friend CoverCall;
|
||||
|
||||
@@ -2,6 +2,7 @@ import base64
|
||||
from pathlib import Path
|
||||
import re
|
||||
import secrets
|
||||
from typing import Any
|
||||
|
||||
import requests
|
||||
from ruamel.yaml import YAML
|
||||
@@ -13,6 +14,7 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ESPHOME, CONF_PROJECT, CONF_REF, CONF_WIFI
|
||||
import esphome.final_validate as fv
|
||||
from esphome.happy_eyeballs import ensure_happy_eyeballs
|
||||
from esphome.types import ConfigType
|
||||
from esphome.yaml_util import dump
|
||||
|
||||
dashboard_import_ns = cg.esphome_ns.namespace("dashboard_import")
|
||||
@@ -23,14 +25,14 @@ DEPENDENCIES = ["api"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
|
||||
def validate_import_url(value):
|
||||
def validate_import_url(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
value = cv.Length(max=255)(value)
|
||||
validate_source_shorthand(value)
|
||||
return value
|
||||
|
||||
|
||||
def validate_full_url(config):
|
||||
def validate_full_url(config: ConfigType) -> ConfigType:
|
||||
if not config[CONF_IMPORT_FULL_CONFIG]:
|
||||
return config
|
||||
source = validate_source_shorthand(config[CONF_PACKAGE_IMPORT_URL])
|
||||
@@ -55,7 +57,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
full_config = fv.full_config.get()[CONF_ESPHOME]
|
||||
if CONF_PROJECT not in full_config:
|
||||
raise cv.Invalid(
|
||||
@@ -73,7 +75,7 @@ wifi:
|
||||
"""
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_DASHBOARD_IMPORT")
|
||||
url = config[CONF_PACKAGE_IMPORT_URL]
|
||||
if config[CONF_IMPORT_FULL_CONFIG]:
|
||||
|
||||
@@ -37,8 +37,6 @@ void DateEntity::publish_state() {
|
||||
#endif
|
||||
}
|
||||
|
||||
DateCall DateEntity::make_call() { return DateCall(this); }
|
||||
|
||||
void DateCall::validate_() {
|
||||
if (this->year_.has_value() && (this->year_ < 1970 || this->year_ > 3000)) {
|
||||
ESP_LOGE(TAG, "Year must be between 1970 and 3000");
|
||||
|
||||
@@ -96,6 +96,8 @@ class DateCall {
|
||||
optional<uint8_t> day_;
|
||||
};
|
||||
|
||||
inline DateCall DateEntity::make_call() { return DateCall(this); }
|
||||
|
||||
template<typename... Ts> class DateSetAction final : public Action<Ts...>, public Parented<DateEntity> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(ESPTime, date)
|
||||
|
||||
@@ -53,8 +53,6 @@ void DateTimeEntity::publish_state() {
|
||||
#endif
|
||||
}
|
||||
|
||||
DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }
|
||||
|
||||
ESPTime DateTimeEntity::state_as_esptime() const {
|
||||
ESPTime obj;
|
||||
obj.year = this->year_;
|
||||
|
||||
@@ -121,6 +121,8 @@ class DateTimeCall {
|
||||
optional<uint8_t> second_;
|
||||
};
|
||||
|
||||
inline DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }
|
||||
|
||||
template<typename... Ts> class DateTimeSetAction final : public Action<Ts...>, public Parented<DateTimeEntity> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(ESPTime, datetime)
|
||||
|
||||
@@ -33,8 +33,6 @@ void TimeEntity::publish_state() {
|
||||
#endif
|
||||
}
|
||||
|
||||
TimeCall TimeEntity::make_call() { return TimeCall(this); }
|
||||
|
||||
void TimeCall::validate_() {
|
||||
if (this->hour_.has_value() && this->hour_ > 23) {
|
||||
ESP_LOGE(TAG, "Hour must be between 0 and 23");
|
||||
|
||||
@@ -98,6 +98,8 @@ class TimeCall {
|
||||
optional<uint8_t> second_;
|
||||
};
|
||||
|
||||
inline TimeCall TimeEntity::make_call() { return TimeCall(this); }
|
||||
|
||||
template<typename... Ts> class TimeSetAction final : public Action<Ts...>, public Parented<TimeEntity> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(ESPTime, time)
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["logger"]
|
||||
@@ -45,7 +46,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("HWINFO", True)
|
||||
# gdb thread support
|
||||
|
||||
@@ -21,6 +21,7 @@ from esphome.const import (
|
||||
UNIT_MILLISECOND,
|
||||
UNIT_PERCENT,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_DEBUG_ID,
|
||||
@@ -111,7 +112,7 @@ CONFIG_SCHEMA = {
|
||||
}
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
|
||||
|
||||
if free_conf := config.get(CONF_FREE):
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.const import (
|
||||
ICON_CHIP,
|
||||
ICON_RESTART,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import ( # noqa: F401 pylint: disable=unused-import
|
||||
CONF_DEBUG_ID,
|
||||
@@ -33,7 +34,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
debug_component = await cg.get_variable(config[CONF_DEBUG_ID])
|
||||
|
||||
if CONF_DEVICE in config:
|
||||
|
||||
@@ -163,6 +163,11 @@ def validate_config(config: ConfigType) -> ConfigType:
|
||||
"You need to remove the global wakeup_pin_mode and define it per pin"
|
||||
)
|
||||
if wakeup_pins:
|
||||
if CONF_WAKEUP_PIN_MODE in wakeup_pins[0]:
|
||||
raise cv.Invalid(
|
||||
"Specify wakeup_pin_mode either at the top level under deep_sleep "
|
||||
"or under the pin entry, not both"
|
||||
)
|
||||
wakeup_pins[0][CONF_WAKEUP_PIN_MODE] = config.pop(CONF_WAKEUP_PIN_MODE)
|
||||
elif (
|
||||
isinstance(config.get(CONF_WAKEUP_PIN), list)
|
||||
|
||||
@@ -43,10 +43,6 @@ void DeepSleepComponent::loop() {
|
||||
this->begin_sleep();
|
||||
}
|
||||
|
||||
void DeepSleepComponent::set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }
|
||||
|
||||
void DeepSleepComponent::set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
|
||||
|
||||
void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
if (this->prevent_ && !manual) {
|
||||
this->next_enter_deep_sleep_ = true;
|
||||
@@ -76,8 +72,4 @@ void DeepSleepComponent::begin_sleep(bool manual) {
|
||||
|
||||
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }
|
||||
|
||||
void DeepSleepComponent::prevent_deep_sleep() { this->prevent_ = true; }
|
||||
|
||||
void DeepSleepComponent::allow_deep_sleep() { this->prevent_ = false; }
|
||||
|
||||
} // namespace esphome::deep_sleep
|
||||
|
||||
@@ -132,7 +132,7 @@ template<typename... Ts> class PreventDeepSleepAction;
|
||||
class DeepSleepComponent final : public Component {
|
||||
public:
|
||||
/// Set the duration in ms the component should sleep once it's in deep sleep mode.
|
||||
void set_sleep_duration(uint32_t time_ms);
|
||||
void set_sleep_duration(uint32_t time_ms) { this->sleep_duration_ = uint64_t(time_ms) * 1000; }
|
||||
#if defined(USE_ESP32)
|
||||
/** Set the pin to wake up to on the ESP32 once it's in deep sleep mode.
|
||||
* Use the inverted property to set the wakeup level.
|
||||
@@ -157,7 +157,7 @@ class DeepSleepComponent final : public Component {
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
void set_touch_wakeup(bool touch_wakeup);
|
||||
void set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
|
||||
#endif
|
||||
|
||||
// Set the duration in ms for how long the code should run before entering
|
||||
@@ -166,7 +166,7 @@ class DeepSleepComponent final : public Component {
|
||||
#endif // USE_ESP32
|
||||
|
||||
/// Set a duration in ms for how long the code should run before entering deep sleep mode.
|
||||
void set_run_duration(uint32_t time_ms);
|
||||
void set_run_duration(uint32_t time_ms) { this->run_duration_ = time_ms; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
@@ -176,8 +176,8 @@ class DeepSleepComponent final : public Component {
|
||||
/// Helper to enter deep sleep mode
|
||||
void begin_sleep(bool manual = false);
|
||||
|
||||
void prevent_deep_sleep();
|
||||
void allow_deep_sleep();
|
||||
void prevent_deep_sleep() { this->prevent_ = true; }
|
||||
void allow_deep_sleep() { this->prevent_ = false; }
|
||||
|
||||
protected:
|
||||
// Returns nullopt if no run duration is set. Otherwise, returns the run
|
||||
|
||||
@@ -74,12 +74,6 @@ void DeepSleepComponent::set_wakeup_pin_mode(WakeupPinMode wakeup_pin_mode) {
|
||||
void DeepSleepComponent::set_ext1_wakeup(Ext1Wakeup ext1_wakeup) { this->ext1_wakeup_ = ext1_wakeup; }
|
||||
#endif
|
||||
|
||||
#if !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C5) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
|
||||
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32H2)
|
||||
void DeepSleepComponent::set_touch_wakeup(bool touch_wakeup) { this->touch_wakeup_ = touch_wakeup; }
|
||||
#endif
|
||||
|
||||
void DeepSleepComponent::set_run_duration(WakeupCauseToRunDuration wakeup_cause_to_run_duration) {
|
||||
wakeup_cause_to_run_duration_ = wakeup_cause_to_run_duration;
|
||||
}
|
||||
|
||||
@@ -685,9 +685,6 @@ void Display::show_page(DisplayPage *page) {
|
||||
}
|
||||
}
|
||||
|
||||
void Display::show_next_page() { this->page_->show_next(); }
|
||||
void Display::show_prev_page() { this->page_->show_prev(); }
|
||||
|
||||
void Display::do_update_() {
|
||||
if (this->auto_clear_enabled_) {
|
||||
this->clear();
|
||||
@@ -892,9 +889,6 @@ void DisplayPage::show_prev() {
|
||||
this->prev_->show();
|
||||
}
|
||||
|
||||
void DisplayPage::set_parent(Display *parent) { this->parent_ = parent; }
|
||||
void DisplayPage::set_prev(DisplayPage *prev) { this->prev_ = prev; }
|
||||
void DisplayPage::set_next(DisplayPage *next) { this->next_ = next; }
|
||||
const display_writer_t &DisplayPage::get_writer() const { return this->writer_; }
|
||||
|
||||
const LogString *text_align_to_string(TextAlign textalign) {
|
||||
|
||||
@@ -802,9 +802,9 @@ class DisplayPage final {
|
||||
void show();
|
||||
void show_next();
|
||||
void show_prev();
|
||||
void set_parent(Display *parent);
|
||||
void set_prev(DisplayPage *prev);
|
||||
void set_next(DisplayPage *next);
|
||||
void set_parent(Display *parent) { this->parent_ = parent; }
|
||||
void set_prev(DisplayPage *prev) { this->prev_ = prev; }
|
||||
void set_next(DisplayPage *next) { this->next_ = next; }
|
||||
const display_writer_t &get_writer() const;
|
||||
|
||||
protected:
|
||||
@@ -814,6 +814,9 @@ class DisplayPage final {
|
||||
DisplayPage *next_{nullptr};
|
||||
};
|
||||
|
||||
inline void Display::show_next_page() { this->page_->show_next(); }
|
||||
inline void Display::show_prev_page() { this->page_->show_prev(); }
|
||||
|
||||
template<typename... Ts> class DisplayPageShowAction final : public Action<Ts...> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(DisplayPage *, page)
|
||||
|
||||
@@ -233,6 +233,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"esp_driver_touch_sens", # Touch sensor driver - only needed by esp32_touch
|
||||
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
|
||||
"esp_eth", # Ethernet driver - only needed by ethernet component
|
||||
"esp_gdbstub", # GDB stub panic handler - unused by ESPHome; bt pulls it back
|
||||
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
|
||||
"esp_http_client", # HTTP client - only needed by http_request component
|
||||
"esp_https_ota", # ESP-IDF HTTPS OTA - ESPHome has its own OTA implementation
|
||||
@@ -1071,19 +1072,11 @@ def _check_esp_idf_versions(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _validate_toolchain(value) -> Toolchain:
|
||||
return Toolchain(
|
||||
cv.one_of(Toolchain.PLATFORMIO, Toolchain.ESP_IDF, lower=True)(value)
|
||||
)
|
||||
|
||||
|
||||
def _resolve_toolchain(value: ConfigType) -> ConfigType:
|
||||
# Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default.
|
||||
# Runs before _detect_variant so downstream validators can rely on
|
||||
# CORE.toolchain instead of re-resolving it from the config dict.
|
||||
if CORE.toolchain is None:
|
||||
CORE.toolchain = value.get(CONF_TOOLCHAIN, Toolchain.ESP_IDF)
|
||||
return value
|
||||
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.ESP_IDF)
|
||||
_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
|
||||
# Runs before _detect_variant so downstream validators can rely on
|
||||
# CORE.toolchain instead of re-resolving it from the config dict.
|
||||
_resolve_toolchain = cv.resolve_toolchain("ESP32", _TOOLCHAINS, Toolchain.ESP_IDF)
|
||||
|
||||
|
||||
def _check_versions(config: ConfigType) -> ConfigType:
|
||||
|
||||
@@ -643,8 +643,28 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
|
||||
App.wake_loop_threadsafe();
|
||||
return;
|
||||
|
||||
// Log the result of connection parameter updates: a peer can reject or
|
||||
// never answer an update, and without this the link silently stays on the
|
||||
// old parameters (visible only as unexplained supervision timeouts).
|
||||
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: {
|
||||
if (param->update_conn_params.status != ESP_BT_STATUS_SUCCESS) {
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(param->update_conn_params.bda, mac_s);
|
||||
ESP_LOGW(TAG, "[%s] Conn param update failed, status=%d", mac_s, param->update_conn_params.status);
|
||||
}
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
else {
|
||||
char mac_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(param->update_conn_params.bda, mac_s);
|
||||
ESP_LOGV(TAG, "[%s] Conn params updated: interval=%u (x1.25ms) latency=%u timeout=%u (x10ms)", mac_s,
|
||||
param->update_conn_params.conn_int, param->update_conn_params.latency,
|
||||
param->update_conn_params.timeout);
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
// Ignore these GAP events as they are not relevant for our use case
|
||||
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
|
||||
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete
|
||||
case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm
|
||||
|
||||
@@ -3,6 +3,7 @@ from pathlib import Path
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
from typing import Any
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
@@ -31,6 +32,7 @@ from esphome.core import (
|
||||
from esphome.core.config import BOARD_MAX_LENGTH
|
||||
from esphome.helpers import IS_MACOS, copy_file_if_changed
|
||||
from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.storage_json import StorageJSON
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .boards import BOARDS, ESP8266_LD_SCRIPTS
|
||||
@@ -88,7 +90,7 @@ def lambdas_use_scanf_float(config: ConfigType) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def set_core_data(config):
|
||||
def set_core_data(config: ConfigType) -> ConfigType:
|
||||
CORE.data[KEY_ESP8266] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_ESP8266
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
|
||||
@@ -102,7 +104,7 @@ def set_core_data(config):
|
||||
return config
|
||||
|
||||
|
||||
def get_download_types(storage_json):
|
||||
def get_download_types(storage_json: StorageJSON) -> list[dict[str, str]]:
|
||||
"""Binary-download entries for a built ESP8266 firmware.
|
||||
|
||||
Used by device-builder (esphome/device-builder), via
|
||||
@@ -134,7 +136,16 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||
return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
if ver <= cv.Version(2, 6, 2):
|
||||
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
# Same encoding the native toolchain uses for its package download, so a
|
||||
# version bump cannot drift between the two paths.
|
||||
from esphome.arduino8266.framework import framework_package_version
|
||||
|
||||
try:
|
||||
return f"~{framework_package_version(ver)}"
|
||||
except EsphomeError as err:
|
||||
# Anchor the 4.x rejection to the framework version line instead of
|
||||
# aborting with a bare traceback-level error
|
||||
raise cv.Invalid(str(err), path=[CONF_VERSION]) from err
|
||||
|
||||
|
||||
# NOTE: Keep this in mind when updating the recommended version:
|
||||
@@ -157,7 +168,7 @@ ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
|
||||
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
|
||||
|
||||
|
||||
def _arduino_check_versions(value):
|
||||
def _arduino_check_versions(value: ConfigType) -> ConfigType:
|
||||
value = value.copy()
|
||||
lookups = {
|
||||
"dev": (cv.Version(3, 1, 2), "https://github.com/esp8266/Arduino.git"),
|
||||
@@ -200,7 +211,7 @@ def _arduino_check_versions(value):
|
||||
return value
|
||||
|
||||
|
||||
def _parse_platform_version(value):
|
||||
def _parse_platform_version(value: Any) -> str:
|
||||
try:
|
||||
# if platform version is a valid version constraint, prefix the default package
|
||||
cv.platformio_version_constraint(value)
|
||||
@@ -244,6 +255,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean,
|
||||
}
|
||||
),
|
||||
# Until the native toolchain lands, PlatformIO is the only backend;
|
||||
# reject a --toolchain this platform cannot serve yet.
|
||||
cv.require_platformio_toolchain("ESP8266"),
|
||||
set_core_data,
|
||||
)
|
||||
|
||||
@@ -275,7 +289,7 @@ def check_rosetta() -> None:
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.PLATFORM)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add(esp8266_ns.setup_preferences())
|
||||
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
@@ -395,8 +409,8 @@ async def to_code(config):
|
||||
)
|
||||
|
||||
if config[CONF_BOARD] in BOARDS:
|
||||
flash_size = BOARDS[config[CONF_BOARD]][KEY_FLASH_SIZE]
|
||||
ld_scripts = ESP8266_LD_SCRIPTS[flash_size]
|
||||
board_data = BOARDS[config[CONF_BOARD]]
|
||||
ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]]
|
||||
|
||||
if ver <= cv.Version(2, 3, 0):
|
||||
# No ld script support
|
||||
@@ -405,7 +419,9 @@ async def to_code(config):
|
||||
# Old ld script path
|
||||
ld_script = ld_scripts[0]
|
||||
else:
|
||||
ld_script = ld_scripts[1]
|
||||
# A per-board override preserves a layout the board shipped
|
||||
# with (see d1_wroom_02 in boards.py)
|
||||
ld_script = board_data.get("ldscript", ld_scripts[1])
|
||||
|
||||
if ld_script is not None:
|
||||
cg.add_platformio_option("board_build.ldscript", ld_script)
|
||||
@@ -504,7 +520,7 @@ ESP8266_EXCEPTION_CODES = {
|
||||
}
|
||||
|
||||
|
||||
def _decode_pc(config, addr):
|
||||
def _decode_pc(config: ConfigType, addr: str) -> None:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
idedata = toolchain.get_idedata(config)
|
||||
@@ -525,7 +541,7 @@ def _decode_pc(config, addr):
|
||||
_LOGGER.warning("Decoded %s", translation)
|
||||
|
||||
|
||||
def _parse_register(config, regex, line):
|
||||
def _parse_register(config: ConfigType, regex: re.Pattern[str], line: str) -> None:
|
||||
match = regex.match(line)
|
||||
if match is not None:
|
||||
_decode_pc(config, match.group(1))
|
||||
@@ -549,7 +565,7 @@ STACKTRACE_BAD_ALLOC_RE = re.compile(
|
||||
STACKTRACE_ESP8266_BACKTRACE_PC_RE = re.compile(r"4[0-9a-f]{7}")
|
||||
|
||||
|
||||
def process_stacktrace(config, line, backtrace_state):
|
||||
def process_stacktrace(config: ConfigType, line: str, backtrace_state: bool) -> bool:
|
||||
line = line.strip()
|
||||
# ESP8266 Exception type
|
||||
match = re.match(STACKTRACE_ESP8266_EXCEPTION_TYPE_RE, line)
|
||||
|
||||
@@ -199,6 +199,15 @@ BOARDS = {
|
||||
"name": "WeMos D1 mini Pro",
|
||||
"flash_size": FLASH_SIZE_16_MB,
|
||||
},
|
||||
"d1_wroom_02": {
|
||||
"name": "WeMos D1 ESP-WROOM-02",
|
||||
"flash_size": FLASH_SIZE_2_MB,
|
||||
# This board joined BOARDS after shipping with the manifest default
|
||||
# (64 KB filesystem region); the flash-size default (2m.ld) would
|
||||
# move _FS_end and with it the preferences sector, wiping existing
|
||||
# devices' flash-backed state on update.
|
||||
"ldscript": "eagle.flash.2m64.ld",
|
||||
},
|
||||
"d1": {
|
||||
"name": "WEMOS D1 R1",
|
||||
"flash_size": FLASH_SIZE_4_MB,
|
||||
@@ -360,3 +369,112 @@ BOARDS = {
|
||||
"flash_size": FLASH_SIZE_4_MB,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# Per-board variant dir + identity defines from platform-espressif8266 4.x
|
||||
# build.extra_flags; the shared -DESP8266/-DARDUINO_ARCH_ESP8266 are added
|
||||
# by the generator.
|
||||
#
|
||||
# Regenerate ESP8266_BOARD_BUILD with (v4.2.1 is the platform version the
|
||||
# native toolchain mirrors; regenerate against the tag when bumping it):
|
||||
#
|
||||
# git clone -b v4.2.1 https://github.com/platformio/platform-espressif8266
|
||||
# python3 - <<'EOF'
|
||||
# import json, glob, os
|
||||
# for f in sorted(glob.glob("platform-espressif8266/boards/*.json")):
|
||||
# b = json.load(open(f))["build"]
|
||||
# extra = b["extra_flags"]
|
||||
# extra = extra.split() if isinstance(extra, str) else extra
|
||||
# defines = [
|
||||
# e[2:] for e in extra if e not in ("-DESP8266", "-DARDUINO_ARCH_ESP8266")
|
||||
# ]
|
||||
# entries = ", ".join(f'"{d}"' for d in defines) + ("," if len(defines) == 1 else "")
|
||||
# board = os.path.splitext(os.path.basename(f))[0]
|
||||
# print(f' "{board}": {{"variant": "{b["variant"]}", "defines": ({entries})}},')
|
||||
# EOF
|
||||
ESP8266_BOARD_BUILD = {
|
||||
"agruminolemon": {
|
||||
"variant": "agruminolemonv4",
|
||||
"defines": ("ARDUINO_ESP8266_AGRUMINO_LEMON_V4",),
|
||||
},
|
||||
"d1": {"variant": "d1", "defines": ("ARDUINO_ESP8266_WEMOS_D1R1",)},
|
||||
"d1_mini": {"variant": "d1_mini", "defines": ("ARDUINO_ESP8266_WEMOS_D1MINI",)},
|
||||
"d1_mini_lite": {
|
||||
"variant": "d1_mini",
|
||||
"defines": ("ARDUINO_ESP8266_WEMOS_D1MINILITE",),
|
||||
},
|
||||
"d1_mini_pro": {
|
||||
"variant": "d1_mini",
|
||||
"defines": ("ARDUINO_ESP8266_WEMOS_D1MINIPRO",),
|
||||
},
|
||||
"d1_wroom_02": {
|
||||
"variant": "d1_mini",
|
||||
"defines": ("ARDUINO_ESP8266_WEMOS_D1WROOM02",),
|
||||
},
|
||||
"eduinowifi": {
|
||||
"variant": "eduinowifi",
|
||||
"defines": ("ARDUINO_ESP8266_SCHIRMILABS_EDUINO_WIFI",),
|
||||
},
|
||||
"esp01": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP01",)},
|
||||
"esp01_1m": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP01",)},
|
||||
"esp07": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP07",)},
|
||||
"esp07s": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_ESP07",)},
|
||||
"esp12e": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_ESP12",)},
|
||||
"esp210": {"variant": "generic", "defines": ("ARDUINO_ESP8266_ESP210",)},
|
||||
"esp8285": {"variant": "esp8285", "defines": ("ARDUINO_ESP8266_ESP01",)},
|
||||
"esp_wroom_02": {
|
||||
"variant": "nodemcu",
|
||||
"defines": ("ARDUINO_ESP8266_ESP_WROOM_02",),
|
||||
},
|
||||
"espduino": {"variant": "ESPDuino", "defines": ("ARDUINO_ESP8266_ESP13",)},
|
||||
"espectro": {"variant": "espectro", "defines": ("ARDUINO_ESP8266_ESPECTRO_CORE",)},
|
||||
"espino": {"variant": "espino", "defines": ("ARDUINO_ESP8266_ESP12",)},
|
||||
"espinotee": {"variant": "espinotee", "defines": ("ARDUINO_ESP8266_ESP13",)},
|
||||
"espmxdevkit": {
|
||||
"variant": "esp8285",
|
||||
"defines": ("ARDUINO_ESP8266_ESP01", "LED_BUILTIN=16"),
|
||||
},
|
||||
"espresso_lite_v1": {
|
||||
"variant": "espresso_lite_v1",
|
||||
"defines": ("ARDUINO_ESP8266_ESPRESSO_LITE_V1",),
|
||||
},
|
||||
"espresso_lite_v2": {
|
||||
"variant": "espresso_lite_v2",
|
||||
"defines": ("ARDUINO_ESP8266_ESPRESSO_LITE_V2",),
|
||||
},
|
||||
"gen4iod": {"variant": "generic", "defines": ("ARDUINO_GEN4_IOD",)},
|
||||
"heltec_wifi_kit_8": {
|
||||
"variant": "wifi_kit_8",
|
||||
"defines": ("ARDUINO_wifi_kit_8",),
|
||||
},
|
||||
"huzzah": {"variant": "adafruit", "defines": ("ARDUINO_ESP8266_ADAFRUIT_HUZZAH",)},
|
||||
"inventone": {"variant": "inventone", "defines": ("ARDUINO_ESP8266_INVENT_ONE",)},
|
||||
"modwifi": {"variant": "generic", "defines": ("ARDUINO_MOD_WIFI_ESP8266",)},
|
||||
"nodemcu": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_NODEMCU",)},
|
||||
"nodemcuv2": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_NODEMCU_ESP12E",)},
|
||||
"oak": {"variant": "oak", "defines": ("ARDUINO_ESP8266_OAK",)},
|
||||
"phoenix_v1": {
|
||||
"variant": "phoenix_v1",
|
||||
"defines": ("ARDUINO_ESP8266_PHOENIX_V1",),
|
||||
},
|
||||
"phoenix_v2": {
|
||||
"variant": "phoenix_v2",
|
||||
"defines": ("ARDUINO_ESP8266_PHOENIX_V2",),
|
||||
},
|
||||
"sonoff_basic": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_BASIC",)},
|
||||
"sonoff_s20": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_S20",)},
|
||||
"sonoff_sv": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_SV",)},
|
||||
"sonoff_th": {"variant": "itead", "defines": ("ARDUINO_ESP8266_SONOFF_TH",)},
|
||||
"sparkfunBlynk": {"variant": "thing", "defines": ("ARDUINO_ESP8266_THING",)},
|
||||
"thing": {"variant": "thing", "defines": ("ARDUINO_ESP8266_THING",)},
|
||||
"thingdev": {"variant": "thing", "defines": ("ARDUINO_ESP8266_THING_DEV",)},
|
||||
"wifi_slot": {"variant": "wifi_slot", "defines": ("ARDUINO_AMPERKA_WIFI_SLOT",)},
|
||||
"wifiduino": {"variant": "wifiduino", "defines": ("ARDUINO_WIFIDUINO_ESP8266",)},
|
||||
"wifinfo": {"variant": "wifinfo", "defines": ("ARDUINO_WIFINFO",)},
|
||||
"wio_link": {"variant": "wiolink", "defines": ("ARDUINO_ESP8266_WIO_LINK",)},
|
||||
"wio_node": {"variant": "nodemcu", "defines": ("ARDUINO_ESP8266_ESP_WROOM_02",)},
|
||||
"xinabox_cw01": {
|
||||
"variant": "xinabox",
|
||||
"defines": ("ARDUINO_ESP8266_XINABOX_CW01",),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Linker-script surgery shared with the native (PlatformIO-free) toolchain.
|
||||
|
||||
These mirror the PlatformIO extra scripts in this directory
|
||||
(``relocate_ratetable.py.script`` and ``testing_mode.py.script``), which run
|
||||
inside SCons and must stay self-contained. The native build generator applies
|
||||
the same patches to the linker scripts it generates, so the logic lives here
|
||||
as plain functions. Keep both in sync when changing either.
|
||||
``segment_length`` is native-toolchain-only and has no script twin.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Collection
|
||||
import hashlib
|
||||
import re
|
||||
|
||||
# Move the NONOS SDK wifi rate tables from flash to DRAM; see
|
||||
# relocate_ratetable.py.script for the full background (NONOS SDK issue 320).
|
||||
RATETABLE_RULE = "*libnet80211.a:ieee80211_phy.o(.irom.text .irom.text.*)"
|
||||
_RATETABLE_COMMENT = (
|
||||
"/* ESPHome: wifi rate tables must live in DRAM, see NONOS SDK issue 320 */"
|
||||
)
|
||||
# Match the whole line: "_data_start" is also a substring of the
|
||||
# "_dport0_data_start" line in the earlier .dport0.data section
|
||||
_RATETABLE_ANCHOR = re.compile(r"^\s*_data_start = ABSOLUTE\(\.\);", re.MULTILINE)
|
||||
|
||||
# Memory sizes for testing mode (allow larger builds for CI component grouping)
|
||||
TESTING_IRAM_SIZE = "0x200000" # 2MB
|
||||
TESTING_DRAM_SIZE = "0x200000" # 2MB
|
||||
TESTING_FLASH_SIZE = "0x2000000" # 32MB
|
||||
|
||||
|
||||
def relocate_ratetable(content: str) -> str:
|
||||
"""Insert the rate-table DRAM rule into a generated common linker script."""
|
||||
if RATETABLE_RULE in content:
|
||||
return content
|
||||
match = _RATETABLE_ANCHOR.search(content)
|
||||
if match is None:
|
||||
raise RuntimeError(
|
||||
"'_data_start' anchor not found in the generated linker script; "
|
||||
"cannot apply wifi rate table DRAM relocation "
|
||||
"(has the Arduino core linker script changed?)"
|
||||
)
|
||||
insert_pos = match.end()
|
||||
return (
|
||||
content[:insert_pos]
|
||||
+ f"\n {_RATETABLE_COMMENT}"
|
||||
+ f"\n {RATETABLE_RULE}"
|
||||
+ content[insert_pos:]
|
||||
)
|
||||
|
||||
|
||||
_TESTING_SEGMENT_SIZES = {
|
||||
"iram1_0_seg": TESTING_IRAM_SIZE,
|
||||
"dram0_0_seg": TESTING_DRAM_SIZE,
|
||||
"irom0_0_seg": TESTING_FLASH_SIZE,
|
||||
}
|
||||
|
||||
|
||||
def _segment_line_re(segment_name: str) -> re.Pattern[str]:
|
||||
"""The MEMORY line for one segment: ``<seg> : org = 0x..., len = 0x...``.
|
||||
|
||||
Anchored to the start of the line so a name never matches inside a
|
||||
longer one (``ram0_0_seg`` must not read ``dram0_0_seg``). The size
|
||||
group stops at the hex digits, leaving any ``ul`` suffix (from the
|
||||
preprocessed ``MMU_IRAM_SIZE``) in place.
|
||||
"""
|
||||
return re.compile(
|
||||
rf"(^[ \t]*{re.escape(segment_name)}"
|
||||
r"\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)"
|
||||
r"(0x[0-9a-fA-F]+)",
|
||||
re.MULTILINE,
|
||||
)
|
||||
|
||||
|
||||
def apply_testing_memory_patches(content: str, segments: Collection[str]) -> str:
|
||||
"""Enlarge the named memory segments so grouped CI test builds can link.
|
||||
|
||||
Each caller passes the segments its linker script defines: the
|
||||
generated common ld carries ``iram1_0_seg``; the flash ld carries
|
||||
``dram0_0_seg`` and ``irom0_0_seg``. A segment that fails to match
|
||||
raises, since a silently kept real memory limit would fail grouped
|
||||
builds far from the cause.
|
||||
"""
|
||||
for segment in _TESTING_SEGMENT_SIZES:
|
||||
if segment not in segments and _segment_line_re(segment).search(content):
|
||||
raise RuntimeError(
|
||||
f"Testing-mode segment {segment} is present in the linker "
|
||||
"script but was not selected for patching"
|
||||
)
|
||||
for segment in segments:
|
||||
if segment not in _TESTING_SEGMENT_SIZES:
|
||||
raise RuntimeError(f"Unknown testing-mode segment {segment!r}")
|
||||
content, count = _segment_line_re(segment).subn(
|
||||
rf"\g<1>{_TESTING_SEGMENT_SIZES[segment]}", content
|
||||
)
|
||||
if count == 0:
|
||||
raise RuntimeError(
|
||||
f"Testing-mode memory patch failed: segment {segment} "
|
||||
"not found (has the Arduino core linker script changed?)"
|
||||
)
|
||||
return content
|
||||
|
||||
|
||||
def segment_length(content: str, segment_name: str) -> int | None:
|
||||
"""Read a memory segment's length from linker script content."""
|
||||
match = _segment_line_re(segment_name).search(content)
|
||||
return int(match.group(2), 16) if match else None
|
||||
|
||||
|
||||
def surgery_fingerprint() -> str:
|
||||
"""Hash of this module's source; linker-script caches include it so an
|
||||
edit here invalidates them."""
|
||||
import inspect
|
||||
import sys
|
||||
|
||||
source = inspect.getsource(sys.modules[__name__])
|
||||
return hashlib.sha256(source.encode()).hexdigest()
|
||||
@@ -15,6 +15,9 @@ CONF_ENABLE_SERIAL1 = "enable_serial1"
|
||||
KEY_WAVEFORM_REQUIRED = "waveform_required"
|
||||
KEY_SERIAL_REQUIRED = "serial_required"
|
||||
KEY_SERIAL1_REQUIRED = "serial1_required"
|
||||
# Set for the native (non-PlatformIO) toolchain's build generator
|
||||
KEY_FLASH_MODE = "flash_mode"
|
||||
KEY_SCANF_FLOAT = "scanf_float"
|
||||
|
||||
# esp8266 namespace is already defined by arduino, manually prefix esphome
|
||||
esp8266_ns = cg.global_ns.namespace("esphome").namespace("esp8266")
|
||||
|
||||
@@ -118,8 +118,6 @@ static const LogString *get_exception_cause(uint32_t cause) {
|
||||
}
|
||||
|
||||
static const LogString *get_reset_reason(uint32_t reason) {
|
||||
if (reason == REASON_WDT_RST)
|
||||
return LOG_STR("Hardware WDT");
|
||||
if (reason == REASON_EXCEPTION_RST)
|
||||
return LOG_STR("Exception");
|
||||
if (reason == REASON_SOFT_WDT_RST)
|
||||
@@ -162,13 +160,20 @@ void crash_handler_log() {
|
||||
if (!is_crash_reason(resetInfo.reason))
|
||||
return;
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
if (resetInfo.reason == REASON_WDT_RST) {
|
||||
// A hardware WDT reset happens entirely in hardware: the postmortem hook
|
||||
// never runs, so rst_info epc1/exccause and the RTC backtrace are
|
||||
// leftovers from an earlier crash. Don't misattribute them (#18596).
|
||||
ESP_LOGE(TAG, " Reason: Hardware WDT (no crash state is recorded for hardware WDT resets)");
|
||||
return;
|
||||
}
|
||||
|
||||
// Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost).
|
||||
// Both resetInfo and RTC data survive until the next reset, so this can be
|
||||
// called multiple times (logger init + API subscribe) with the same result.
|
||||
uint32_t backtrace[MAX_BACKTRACE];
|
||||
uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE);
|
||||
|
||||
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
|
||||
// GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific
|
||||
// ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match
|
||||
// the Arduino core's postmortem handler behavior.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -18,6 +19,8 @@ from esphome.const import (
|
||||
PLATFORM_ESP8266,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from . import boards
|
||||
from .const import KEY_BOARD, KEY_ESP8266, KEY_PIN_INITIAL_STATES, esp8266_ns
|
||||
@@ -27,7 +30,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
ESP8266GPIOPin = esp8266_ns.class_("ESP8266GPIOPin", cg.InternalGPIOPin)
|
||||
|
||||
|
||||
def _lookup_pin(value):
|
||||
def _lookup_pin(value: str) -> int:
|
||||
board = CORE.data[KEY_ESP8266][KEY_BOARD]
|
||||
board_pins = boards.ESP8266_BOARD_PINS.get(board, {})
|
||||
|
||||
@@ -42,7 +45,7 @@ def _lookup_pin(value):
|
||||
raise cv.Invalid(f"Cannot resolve pin name '{value}' for board {board}.")
|
||||
|
||||
|
||||
def _translate_pin(value):
|
||||
def _translate_pin(value: Any) -> int:
|
||||
if isinstance(value, dict) or value is None:
|
||||
raise cv.Invalid(
|
||||
"This variable only supports pin numbers, not full pin schemas "
|
||||
@@ -69,7 +72,7 @@ _ESP_SDIO_PINS = {
|
||||
}
|
||||
|
||||
|
||||
def validate_gpio_pin(value):
|
||||
def validate_gpio_pin(value: Any) -> int:
|
||||
value = _translate_pin(value)
|
||||
if value < 0 or value > 17:
|
||||
raise cv.Invalid(f"ESP8266: Invalid pin number: {value}")
|
||||
@@ -86,7 +89,7 @@ def validate_gpio_pin(value):
|
||||
return value
|
||||
|
||||
|
||||
def validate_supports(value):
|
||||
def validate_supports(value: ConfigType) -> ConfigType:
|
||||
num = value[CONF_NUMBER]
|
||||
mode = value[CONF_MODE]
|
||||
is_input = mode[CONF_INPUT]
|
||||
@@ -160,7 +163,7 @@ class PinInitialState:
|
||||
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register(PLATFORM_ESP8266, ESP8266_PIN_SCHEMA)
|
||||
async def esp8266_pin_to_code(config):
|
||||
async def esp8266_pin_to_code(config: ConfigType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
mode = config[CONF_MODE]
|
||||
@@ -192,7 +195,7 @@ async def esp8266_pin_to_code(config):
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WORKAROUNDS)
|
||||
async def add_pin_initial_states_array():
|
||||
async def add_pin_initial_states_array() -> None:
|
||||
# Add includes at the very end, so that they override everything
|
||||
initial_states: list[PinInitialState] = CORE.data[KEY_ESP8266][
|
||||
KEY_PIN_INITIAL_STATES
|
||||
|
||||
@@ -588,8 +588,6 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
|
||||
}
|
||||
|
||||
float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
|
||||
uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
|
||||
void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
void ESPHomeOTAComponent::log_socket_error_(const LogString *msg) {
|
||||
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
||||
|
||||
@@ -39,14 +39,14 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
#endif // USE_OTA_PASSWORD
|
||||
|
||||
/// Manually set the port OTA should listen on
|
||||
void set_port(uint16_t port);
|
||||
void set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
void loop() override;
|
||||
|
||||
uint16_t get_port() const;
|
||||
uint16_t get_port() const { return this->port_; }
|
||||
|
||||
protected:
|
||||
void handle_handshake_();
|
||||
|
||||
@@ -10,14 +10,6 @@ EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-
|
||||
|
||||
EthernetComponent::EthernetComponent() { global_eth_component = this; }
|
||||
|
||||
float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; }
|
||||
|
||||
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
|
||||
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
void EthernetComponent::notify_ip_state_listeners_() {
|
||||
auto ips = this->get_ip_addresses();
|
||||
|
||||
@@ -125,7 +125,7 @@ class EthernetComponent final : public Component {
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
float get_setup_priority() const override { return setup_priority::ETHERNET; }
|
||||
void on_powerdown() override { powerdown(); }
|
||||
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
|
||||
|
||||
@@ -146,9 +146,9 @@ class EthernetComponent final : public Component {
|
||||
esp_netif_t *get_esp_netif() { return this->eth_netif_; }
|
||||
#endif
|
||||
|
||||
void set_type(EthernetType type);
|
||||
void set_type(EthernetType type) { this->type_ = type; }
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
void set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
|
||||
#endif
|
||||
void set_fixed_mac(const std::array<uint8_t, MAC_ADDRESS_SIZE> &mac) { this->fixed_mac_ = mac; }
|
||||
|
||||
@@ -171,35 +171,35 @@ class EthernetComponent final : public Component {
|
||||
esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; }
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_miso_pin(uint8_t miso_pin);
|
||||
void set_mosi_pin(uint8_t mosi_pin);
|
||||
void set_cs_pin(uint8_t cs_pin);
|
||||
void set_interrupt_pin(uint8_t interrupt_pin);
|
||||
void set_reset_pin(uint8_t reset_pin);
|
||||
void set_clock_speed(int clock_speed);
|
||||
void set_interface(spi_host_device_t interface);
|
||||
void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
|
||||
void set_interface(spi_host_device_t interface) { this->interface_ = interface; }
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void set_polling_interval(uint32_t polling_interval);
|
||||
void set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
|
||||
#endif
|
||||
#else
|
||||
void set_phy_addr(uint8_t phy_addr);
|
||||
void set_power_pin(int power_pin);
|
||||
void set_mdc_pin(uint8_t mdc_pin);
|
||||
void set_mdio_pin(uint8_t mdio_pin);
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_clk_mode(emac_rmii_clock_mode_t clk_mode);
|
||||
void set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
|
||||
void set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
|
||||
void set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
|
||||
void set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
|
||||
void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
|
||||
void add_phy_register(PHYRegister register_value);
|
||||
#endif // USE_ETHERNET_SPI
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_RP2
|
||||
void set_clk_pin(uint8_t clk_pin);
|
||||
void set_miso_pin(uint8_t miso_pin);
|
||||
void set_mosi_pin(uint8_t mosi_pin);
|
||||
void set_cs_pin(uint8_t cs_pin);
|
||||
void set_interrupt_pin(int8_t interrupt_pin);
|
||||
void set_reset_pin(int8_t reset_pin);
|
||||
void set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
#endif // USE_RP2
|
||||
|
||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||
|
||||
@@ -908,25 +908,7 @@ void EthernetComponent::dump_connect_params_() {
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
}
|
||||
|
||||
#ifdef USE_ETHERNET_SPI
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
|
||||
void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; }
|
||||
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
|
||||
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
|
||||
#endif
|
||||
#else
|
||||
void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
|
||||
void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
|
||||
void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
|
||||
void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
|
||||
#ifndef USE_ETHERNET_SPI
|
||||
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
|
||||
#endif
|
||||
|
||||
|
||||
@@ -355,13 +355,6 @@ void EthernetComponent::dump_connect_params_() {
|
||||
this->get_eth_mac_address_pretty_into_buffer(mac_buf));
|
||||
}
|
||||
|
||||
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
|
||||
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
|
||||
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
|
||||
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
||||
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||
|
||||
void EthernetComponent::enable() {
|
||||
// RP2040 uses arduino-pico's LwipIntfDev which manages link state internally;
|
||||
// there is no clean enable/disable hook today. The YAML option is accepted on
|
||||
|
||||
@@ -153,11 +153,6 @@ void FanRestoreState::apply(Fan &fan) {
|
||||
fan.publish_state();
|
||||
}
|
||||
|
||||
FanCall Fan::turn_on() { return this->make_call().set_state(true); }
|
||||
FanCall Fan::turn_off() { return this->make_call().set_state(false); }
|
||||
FanCall Fan::toggle() { return this->make_call().set_state(!this->state); }
|
||||
FanCall Fan::make_call() { return FanCall(*this); }
|
||||
|
||||
const char *Fan::find_preset_mode_(const char *preset_mode) {
|
||||
return this->find_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
|
||||
}
|
||||
|
||||
@@ -115,10 +115,10 @@ class Fan : public EntityBase {
|
||||
/// The current direction of the fan
|
||||
FanDirection direction{FanDirection::FORWARD};
|
||||
|
||||
FanCall turn_on();
|
||||
FanCall turn_off();
|
||||
FanCall toggle();
|
||||
FanCall make_call();
|
||||
FanCall turn_on() { return this->make_call().set_state(true); }
|
||||
FanCall turn_off() { return this->make_call().set_state(false); }
|
||||
FanCall toggle() { return this->make_call().set_state(!this->state); }
|
||||
FanCall make_call() { return FanCall(*this); }
|
||||
|
||||
/// Register a callback that will be called each time the state changes.
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
@@ -75,12 +76,12 @@ def compute_local_image_path(value: str | ConfigType) -> Path:
|
||||
return external_files.compute_local_file_path(DOMAIN, url)
|
||||
|
||||
|
||||
def local_path(value):
|
||||
def local_path(value: str | ConfigType) -> str:
|
||||
value = value[CONF_PATH] if isinstance(value, dict) else value
|
||||
return str(CORE.relative_config_path(value))
|
||||
|
||||
|
||||
def download_file(url, path):
|
||||
def download_file(url: str, path: Path) -> str:
|
||||
# The shared NETWORK_TIMEOUT applies; a per-caller timeout would be
|
||||
# silently ignored on a per-run memo hit anyway (memos key by path).
|
||||
external_files.download_content(url, path)
|
||||
@@ -98,7 +99,7 @@ def download_gh_svg(value: str | ConfigType, source: str) -> str:
|
||||
return download_file(url, path)
|
||||
|
||||
|
||||
def download_image(value):
|
||||
def download_image(value: str | ConfigType) -> str:
|
||||
value = value[CONF_URL] if isinstance(value, dict) else value
|
||||
return download_file(value, compute_local_image_path(value))
|
||||
|
||||
@@ -146,7 +147,7 @@ def _extract_entry_ref(entry: ConfigType) -> RemoteFile | None:
|
||||
PREFETCH_FILES = external_files.single_stage_prefetch(_extract_entry_ref)
|
||||
|
||||
|
||||
def validate_file_shorthand(value):
|
||||
def validate_file_shorthand(value: Any) -> str:
|
||||
value = cv.string_strict(value)
|
||||
if (remote := _parse_remote_shorthand(value)) is not None:
|
||||
return download_file(remote.url, remote.path)
|
||||
@@ -163,8 +164,8 @@ LOCAL_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def mdi_schema(source):
|
||||
def validate_mdi(value):
|
||||
def mdi_schema(source: str) -> cv.All:
|
||||
def validate_mdi(value: ConfigType) -> str:
|
||||
return download_gh_svg(value, source)
|
||||
|
||||
return cv.All(
|
||||
@@ -259,7 +260,9 @@ async def new_image(config: ConfigType) -> MockObj:
|
||||
return var
|
||||
|
||||
|
||||
async def write_image(config, all_frames=False):
|
||||
async def write_image(
|
||||
config: ConfigType, all_frames: bool = False
|
||||
) -> tuple[MockObj, int, int, MockObj, MockObj, int]:
|
||||
path = Path(config[CONF_FILE])
|
||||
if not path.is_file():
|
||||
raise core.EsphomeError(f"Could not load image file {path}")
|
||||
|
||||
@@ -8,7 +8,8 @@ from esphome.const import (
|
||||
CONF_TYPE,
|
||||
CONF_VALUE,
|
||||
)
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
from esphome.core import ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -62,7 +63,7 @@ CONFIG_SCHEMA = _globals_schema
|
||||
|
||||
# Run with low priority so that namespaces are registered first
|
||||
@coroutine_with_priority(CoroPriority.LATE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
type_ = cg.RawExpression(config[CONF_TYPE])
|
||||
restore = config[CONF_RESTORE_VALUE]
|
||||
|
||||
@@ -104,7 +105,12 @@ async def to_code(config):
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def globals_set_to_code(config, action_id, template_arg, args):
|
||||
async def globals_set_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])
|
||||
template_arg = cg.TemplateArguments(full_id.type, *template_arg)
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -12,6 +12,7 @@ from esphome.const import (
|
||||
CONF_PIN,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -68,7 +69,7 @@ def _pin_shared_only_with_deep_sleep(pin_num: int) -> bool:
|
||||
return any(path and path[0] == "deep_sleep" for path, _, _ in pin_users)
|
||||
|
||||
|
||||
def _final_validate(config) -> None:
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
use_interrupt = config[CONF_USE_INTERRUPT]
|
||||
if not use_interrupt:
|
||||
return
|
||||
@@ -124,7 +125,7 @@ def _final_validate(config) -> None:
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components.one_wire import OneWireBus
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -16,7 +17,7 @@ CONFIG_SCHEMA = output.BINARY_OUTPUT_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await output.register_output(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INTERLOCK, CONF_PIN
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import gpio_ns
|
||||
|
||||
@@ -24,7 +25,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await switch.new_switch(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
from collections.abc import Callable, Iterable
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ATTRIBUTE, CONF_ENTITY_ID, CONF_INTERNAL
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@OttoWinter", "@esphome/core"]
|
||||
homeassistant_ns = cg.esphome_ns.namespace("homeassistant")
|
||||
|
||||
|
||||
def validate_entity_domain(platform, supported_domains):
|
||||
def validator(config):
|
||||
def validate_entity_domain(
|
||||
platform: str, supported_domains: Iterable[str]
|
||||
) -> Callable[[ConfigType], ConfigType]:
|
||||
def validator(config: ConfigType) -> ConfigType:
|
||||
domain = config[CONF_ENTITY_ID].split(".", 1)[0]
|
||||
if domain not in supported_domains:
|
||||
raise cv.Invalid(
|
||||
@@ -34,7 +40,7 @@ HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
def setup_home_assistant_entity(var, config):
|
||||
def setup_home_assistant_entity(var: MockObj, config: ConfigType) -> None:
|
||||
cg.add(var.set_entity_id(config[CONF_ENTITY_ID]))
|
||||
if CONF_ATTRIBUTE in config:
|
||||
cg.add(var.set_attribute(config[CONF_ATTRIBUTE]))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import binary_sensor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(HomeassistantBinarySensor).ex
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await binary_sensor.new_binary_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import number
|
||||
import esphome.config_validation as cv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
|
||||
@@ -22,7 +23,7 @@ CONFIG_SCHEMA = (
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = await number.new_number(
|
||||
config,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = sensor.sensor_schema(HomeassistantSensor, accuracy_decimals=1).e
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import switch
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_CONTROL_SCHEMA,
|
||||
@@ -36,7 +37,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import text_sensor
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import (
|
||||
HOME_ASSISTANT_IMPORT_SCHEMA,
|
||||
@@ -18,7 +19,7 @@ CONFIG_SCHEMA = text_sensor.text_sensor_schema(HomeassistantTextSensor).extend(
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await text_sensor.new_text_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
setup_home_assistant_entity(var, config)
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_TIMEZONE
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .. import homeassistant_ns
|
||||
|
||||
@@ -16,7 +17,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await time_.register_time(var, config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -11,6 +11,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE
|
||||
from esphome.platformio.toolchain import copy_ccache_script
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const import KEY_HOST
|
||||
|
||||
@@ -22,7 +23,7 @@ AUTO_LOAD = ["network", "preferences"]
|
||||
IS_TARGET_PLATFORM = True
|
||||
|
||||
|
||||
def set_core_data(config):
|
||||
def set_core_data(config: ConfigType) -> ConfigType:
|
||||
CORE.data[KEY_HOST] = {}
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_HOST
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "host"
|
||||
@@ -36,11 +37,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MAC_ADDRESS, default="98:35:69:ab:f6:79"): cv.mac_address,
|
||||
}
|
||||
),
|
||||
cv.require_platformio_toolchain("host"),
|
||||
set_core_data,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_build_flag("-DUSE_HOST")
|
||||
cg.add_define("USE_NATIVE_64BIT_TIME")
|
||||
# The prefs file finds stored preferences by key, so key migration is possible
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -14,6 +15,8 @@ from esphome.const import (
|
||||
CONF_PULLDOWN,
|
||||
CONF_PULLUP,
|
||||
)
|
||||
from esphome.cpp_generator import MockObj
|
||||
from esphome.types import ConfigType
|
||||
|
||||
from .const import host_ns
|
||||
|
||||
@@ -22,7 +25,7 @@ _LOGGER = logging.getLogger(__name__)
|
||||
HostGPIOPin = host_ns.class_("HostGPIOPin", cg.InternalGPIOPin)
|
||||
|
||||
|
||||
def _translate_pin(value):
|
||||
def _translate_pin(value: Any) -> int | str:
|
||||
if isinstance(value, dict) or value is None:
|
||||
raise cv.Invalid(
|
||||
"This variable only supports pin numbers, not full pin schemas "
|
||||
@@ -41,7 +44,7 @@ def _translate_pin(value):
|
||||
return value
|
||||
|
||||
|
||||
def validate_gpio_pin(value):
|
||||
def validate_gpio_pin(value: Any) -> int | str:
|
||||
return _translate_pin(value)
|
||||
|
||||
|
||||
@@ -53,7 +56,7 @@ HOST_PIN_SCHEMA = pins.gpio_base_schema(
|
||||
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("host", HOST_PIN_SCHEMA)
|
||||
async def host_pin_to_code(config):
|
||||
async def host_pin_to_code(config: ConfigType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
cg.add(var.set_pin(num))
|
||||
|
||||
@@ -2,6 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import time as time_
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
|
||||
@@ -14,7 +15,7 @@ CONFIG_SCHEMA = time_.TIME_SCHEMA.extend(
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await time_.register_time(var, config)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from typing import Any
|
||||
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
@@ -52,9 +53,10 @@ from esphome.const import (
|
||||
PLATFORM_RP2,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -96,13 +98,13 @@ CONF_SCL_PULLUP_ENABLED = "scl_pullup_enabled"
|
||||
MULTI_CONF = True
|
||||
|
||||
|
||||
def validate_device(value):
|
||||
def validate_device(value: str) -> str:
|
||||
if not re.match(r"^/(?:[^/]+/)*[^/]+$", value):
|
||||
raise cv.Invalid("Device must be an absolute device path (e.g., /dev/i2c-0)")
|
||||
return value
|
||||
|
||||
|
||||
def _bus_declare_type(value):
|
||||
def _bus_declare_type(value: Any) -> ID:
|
||||
if CORE.is_esp32:
|
||||
return cv.declare_id(IDFI2CBus)(value)
|
||||
if CORE.using_arduino:
|
||||
@@ -114,7 +116,7 @@ def _bus_declare_type(value):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def _rp2040_i2c_controller(pin):
|
||||
def _rp2040_i2c_controller(pin: int) -> int:
|
||||
"""Return the I2C controller number (0 or 1) for a given RP2040/RP2350 GPIO pin.
|
||||
|
||||
See RP2040 datasheet Table 2 (section 1.4.3, "GPIO Functions"):
|
||||
@@ -125,7 +127,7 @@ def _rp2040_i2c_controller(pin):
|
||||
return (pin // 2) % 2
|
||||
|
||||
|
||||
def validate_config(config):
|
||||
def validate_config(config: ConfigType) -> ConfigType:
|
||||
if CORE.is_esp32:
|
||||
return cv.require_framework_version(
|
||||
esp_idf=cv.Version(5, 4, 2), esp32_arduino=cv.Version(3, 2, 1)
|
||||
@@ -142,7 +144,7 @@ def validate_config(config):
|
||||
return config
|
||||
|
||||
|
||||
def validate_host_config(config):
|
||||
def validate_host_config(config: ConfigType) -> ConfigType:
|
||||
if CORE.is_host:
|
||||
# Host I2C is currently only supported on Linux
|
||||
if not sys.platform.lower().startswith("linux"):
|
||||
@@ -229,7 +231,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
def _final_validate(config: ConfigType) -> None:
|
||||
full_config = fv.full_config.get()[CONF_I2C]
|
||||
if CORE.using_zephyr and len(full_config) > 1:
|
||||
raise cv.Invalid("Second i2c is not implemented on Zephyr yet")
|
||||
@@ -281,7 +283,7 @@ FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.BUS)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(i2c_ns.using)
|
||||
cg.add_define("USE_I2C")
|
||||
if CORE.is_esp32:
|
||||
@@ -358,7 +360,7 @@ async def to_code(config):
|
||||
cg.add(var.set_lp_mode(bool(config[CONF_LOW_POWER_MODE])))
|
||||
|
||||
|
||||
def i2c_device_schema(default_address):
|
||||
def i2c_device_schema(default_address: int | None) -> cv.Schema:
|
||||
"""Create a schema for a i2c device.
|
||||
|
||||
:param default_address: The default address of the i2c device, can be None to represent
|
||||
@@ -375,7 +377,7 @@ def i2c_device_schema(default_address):
|
||||
return cv.Schema(schema)
|
||||
|
||||
|
||||
async def register_i2c_device(var, config):
|
||||
async def register_i2c_device(var: MockObj, config: ConfigType) -> None:
|
||||
"""Register an i2c device with the given config.
|
||||
|
||||
Sets the i2c bus to use and the i2c address.
|
||||
@@ -390,11 +392,11 @@ async def register_i2c_device(var, config):
|
||||
def final_validate_device_schema(
|
||||
name: str,
|
||||
*,
|
||||
min_frequency: cv.frequency = None,
|
||||
max_frequency: cv.frequency = None,
|
||||
min_timeout: cv.time_period = None,
|
||||
max_timeout: cv.time_period = None,
|
||||
):
|
||||
min_frequency: Any = None,
|
||||
max_frequency: Any = None,
|
||||
min_timeout: Any = None,
|
||||
max_timeout: Any = None,
|
||||
) -> cv.Schema:
|
||||
hub_schema = {}
|
||||
if (min_frequency is not None) and (max_frequency is not None):
|
||||
hub_schema[cv.Required(CONF_FREQUENCY)] = cv.Range(
|
||||
|
||||
@@ -75,8 +75,6 @@ void Infrared::dump_config() {
|
||||
YESNO(this->traits_.get_supports_receiver()));
|
||||
}
|
||||
|
||||
InfraredCall Infrared::make_call() { return InfraredCall(this); }
|
||||
|
||||
void Infrared::control(const InfraredCall &call) {
|
||||
if (this->transmitter_ == nullptr) {
|
||||
ESP_LOGW(TAG, "No transmitter configured");
|
||||
|
||||
@@ -134,7 +134,7 @@ class Infrared : public Component, public EntityBase, public remote_base::Remote
|
||||
const InfraredTraits &get_traits() const { return this->traits_; }
|
||||
|
||||
/// Create a call object for transmitting
|
||||
InfraredCall make_call();
|
||||
InfraredCall make_call() { return InfraredCall(this); }
|
||||
|
||||
/// Get capability flags for this infrared instance
|
||||
uint32_t get_capability_flags() const;
|
||||
|
||||
@@ -300,7 +300,7 @@ FRAMEWORK_SCHEMA = cv.All(
|
||||
_check_debug_order,
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(_notify_old_style)
|
||||
CONFIG_SCHEMA = cv.All(_notify_old_style, cv.require_platformio_toolchain("LibreTiny"))
|
||||
|
||||
BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
@@ -314,6 +314,7 @@ BASE_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
BASE_SCHEMA.add_extra(_detect_variant)
|
||||
BASE_SCHEMA.add_extra(cv.require_platformio_toolchain("LibreTiny"))
|
||||
BASE_SCHEMA.add_extra(_update_core_data)
|
||||
|
||||
|
||||
|
||||
@@ -13,8 +13,6 @@ ESPColorView ESPRangeView::operator[](int32_t index) const {
|
||||
index = interpret_index(index, this->size()) + this->begin_;
|
||||
return (*this->parent_)[index];
|
||||
}
|
||||
ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; }
|
||||
ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; }
|
||||
|
||||
void ESPRangeView::set(const Color &color) {
|
||||
for (int32_t i = this->begin_; i < this->end_; i++) {
|
||||
|
||||
@@ -75,4 +75,7 @@ class ESPRangeIterator {
|
||||
int32_t i_;
|
||||
};
|
||||
|
||||
inline ESPRangeIterator ESPRangeView::begin() { return {*this, this->begin_}; }
|
||||
inline ESPRangeIterator ESPRangeView::end() { return {*this, this->end_}; }
|
||||
|
||||
} // namespace esphome::light
|
||||
|
||||
@@ -157,8 +157,6 @@ void LightState::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
|
||||
|
||||
void LightState::publish_state() {
|
||||
if (this->remote_values_listeners_) {
|
||||
for (auto *listener : *this->remote_values_listeners_) {
|
||||
@@ -194,25 +192,11 @@ void LightState::add_target_state_reached_listener(LightTargetStateReachedListen
|
||||
this->target_state_reached_listeners_->push_back(listener);
|
||||
}
|
||||
|
||||
void LightState::set_default_transition_length(uint32_t default_transition_length) {
|
||||
this->default_transition_length_ = default_transition_length;
|
||||
}
|
||||
uint32_t LightState::get_default_transition_length() const { return this->default_transition_length_; }
|
||||
void LightState::set_flash_transition_length(uint32_t flash_transition_length) {
|
||||
this->flash_transition_length_ = flash_transition_length;
|
||||
}
|
||||
uint32_t LightState::get_flash_transition_length() const { return this->flash_transition_length_; }
|
||||
void LightState::set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
|
||||
void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
void LightState::set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; }
|
||||
bool LightState::supports_effects() { return !this->effects_.empty(); }
|
||||
const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; }
|
||||
void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) {
|
||||
// Called once from Python codegen during setup with all effects from YAML config
|
||||
this->effects_ = effects;
|
||||
}
|
||||
|
||||
void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
|
||||
void LightState::current_values_as_brightness(float *brightness) {
|
||||
this->current_values.as_brightness(brightness);
|
||||
*brightness = this->gamma_correct_lut(*brightness);
|
||||
@@ -333,8 +317,6 @@ float LightState::gamma_uncorrect_lut(float value) const {
|
||||
}
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
bool LightState::is_transformer_active() { return this->is_transformer_active_; }
|
||||
|
||||
void LightState::start_effect_(uint32_t effect_index) {
|
||||
this->stop_effect_();
|
||||
if (effect_index == 0)
|
||||
|
||||
@@ -109,7 +109,7 @@ class LightState : public EntityBase, public Component {
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
/// Shortly after HARDWARE.
|
||||
float get_setup_priority() const override;
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE - 1.0f; }
|
||||
|
||||
/** The current values of the light as outputted to the light.
|
||||
*
|
||||
@@ -157,15 +157,19 @@ class LightState : public EntityBase, public Component {
|
||||
void add_target_state_reached_listener(LightTargetStateReachedListener *listener);
|
||||
|
||||
/// Set the default transition length, i.e. the transition length when no transition is provided.
|
||||
void set_default_transition_length(uint32_t default_transition_length);
|
||||
uint32_t get_default_transition_length() const;
|
||||
void set_default_transition_length(uint32_t default_transition_length) {
|
||||
this->default_transition_length_ = default_transition_length;
|
||||
}
|
||||
uint32_t get_default_transition_length() const { return this->default_transition_length_; }
|
||||
|
||||
/// Set the flash transition length
|
||||
void set_flash_transition_length(uint32_t flash_transition_length);
|
||||
uint32_t get_flash_transition_length() const;
|
||||
void set_flash_transition_length(uint32_t flash_transition_length) {
|
||||
this->flash_transition_length_ = flash_transition_length;
|
||||
}
|
||||
uint32_t get_flash_transition_length() const { return this->flash_transition_length_; }
|
||||
|
||||
/// Set the gamma correction factor
|
||||
void set_gamma_correct(float gamma_correct);
|
||||
void set_gamma_correct(float gamma_correct) { this->gamma_correct_ = gamma_correct; }
|
||||
float get_gamma_correct() const { return this->gamma_correct_; }
|
||||
|
||||
#ifdef USE_LIGHT_GAMMA_LUT
|
||||
@@ -186,17 +190,17 @@ class LightState : public EntityBase, public Component {
|
||||
#endif // USE_LIGHT_GAMMA_LUT
|
||||
|
||||
/// Set the restore mode of this light
|
||||
void set_restore_mode(LightRestoreMode restore_mode);
|
||||
void set_restore_mode(LightRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
|
||||
|
||||
/// Set a callback to populate the initial state defaults during setup.
|
||||
/// The callback is called once, then cleared. Values live in flash as code.
|
||||
void set_initial_state(void (*callback)(LightStateRTCState &));
|
||||
void set_initial_state(void (*callback)(LightStateRTCState &)) { this->initial_state_callback_ = callback; }
|
||||
|
||||
/// Return whether the light has any effects that meet the trait requirements.
|
||||
bool supports_effects();
|
||||
bool supports_effects() const { return !this->effects_.empty(); }
|
||||
|
||||
/// Get all effects for this light state.
|
||||
const FixedVector<LightEffect *> &get_effects() const;
|
||||
const FixedVector<LightEffect *> &get_effects() const { return this->effects_; }
|
||||
|
||||
/// Add effects for this light state.
|
||||
void add_effects(const std::initializer_list<LightEffect *> &effects);
|
||||
@@ -254,7 +258,7 @@ class LightState : public EntityBase, public Component {
|
||||
}
|
||||
|
||||
/// The result of all the current_values_as_* methods have gamma correction applied.
|
||||
void current_values_as_binary(bool *binary);
|
||||
void current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); }
|
||||
|
||||
void current_values_as_brightness(float *brightness);
|
||||
|
||||
@@ -281,7 +285,7 @@ class LightState : public EntityBase, public Component {
|
||||
* return;
|
||||
* }
|
||||
*/
|
||||
bool is_transformer_active();
|
||||
bool is_transformer_active() const { return this->is_transformer_active_; }
|
||||
|
||||
protected:
|
||||
friend LightOutput;
|
||||
|
||||
@@ -12,13 +12,14 @@ from esphome.const import (
|
||||
CONF_ON_UNLOCK,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import (
|
||||
entity_duplicate_validator,
|
||||
queue_entity_register,
|
||||
setup_entity,
|
||||
)
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
|
||||
from esphome.types import ConfigType, SafeExpType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
IS_PLATFORM_COMPONENT = True
|
||||
@@ -102,7 +103,7 @@ _CALLBACK_AUTOMATIONS = (
|
||||
|
||||
|
||||
@setup_entity("lock")
|
||||
async def _setup_lock_core(var, config):
|
||||
async def _setup_lock_core(var: MockObj, config: ConfigType) -> None:
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
if mqtt_id := config.get(CONF_MQTT_ID):
|
||||
@@ -113,7 +114,7 @@ async def _setup_lock_core(var, config):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
|
||||
async def register_lock(var, config):
|
||||
async def register_lock(var: MockObj, config: ConfigType) -> None:
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
var = cg.Pvariable(config[CONF_ID], var)
|
||||
queue_entity_register("lock", config)
|
||||
@@ -121,7 +122,7 @@ async def register_lock(var, config):
|
||||
await _setup_lock_core(var, config)
|
||||
|
||||
|
||||
async def new_lock(config, *args):
|
||||
async def new_lock(config: ConfigType, *args: SafeExpType) -> MockObj:
|
||||
var = cg.new_Pvariable(config[CONF_ID], *args)
|
||||
await register_lock(var, config)
|
||||
return var
|
||||
@@ -143,23 +144,38 @@ LOCK_ACTION_SCHEMA = maybe_simple_id(
|
||||
@automation.register_action(
|
||||
"lock.open", OpenAction, LOCK_ACTION_SCHEMA, synchronous=True
|
||||
)
|
||||
async def lock_action_to_code(config, action_id, template_arg, args):
|
||||
async def lock_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_condition("lock.is_locked", LockCondition, LOCK_ACTION_SCHEMA)
|
||||
async def lock_is_on_to_code(config, condition_id, template_arg, args):
|
||||
async def lock_is_on_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, True)
|
||||
|
||||
|
||||
@automation.register_condition("lock.is_unlocked", LockCondition, LOCK_ACTION_SCHEMA)
|
||||
async def lock_is_off_to_code(config, condition_id, template_arg, args):
|
||||
async def lock_is_off_to_code(
|
||||
config: ConfigType,
|
||||
condition_id: ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: TemplateArgsType,
|
||||
) -> MockObj:
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
return cg.new_Pvariable(condition_id, template_arg, paren, False)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.CORE)
|
||||
async def to_code(config):
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
cg.add_global(lock_ns.using)
|
||||
|
||||
@@ -201,17 +201,10 @@ void Logger::process_messages_() {
|
||||
#endif // USE_ESPHOME_TASK_LOG_BUFFER
|
||||
}
|
||||
|
||||
void Logger::set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
||||
#ifdef USE_LOGGER_RUNTIME_TAG_LEVELS
|
||||
void Logger::set_log_level(const char *tag, uint8_t log_level) { this->log_levels_[tag] = log_level; }
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
UARTSelection Logger::get_uart() const { return this->uart_; }
|
||||
#endif
|
||||
|
||||
float Logger::get_setup_priority() const { return setup_priority::BUS + 500.0f; }
|
||||
|
||||
// Log level strings - packed into flash on ESP8266, indexed by log level (0-7)
|
||||
PROGMEM_STRING_TABLE(LogLevelStrings, "NONE", "ERROR", "WARN", "INFO", "CONFIG", "DEBUG", "VERBOSE", "VERY_VERBOSE");
|
||||
|
||||
|
||||
@@ -148,7 +148,7 @@ class Logger final : public Component {
|
||||
void loop() override;
|
||||
#endif
|
||||
/// Manually set the baud rate for serial, set to 0 to disable.
|
||||
void set_baud_rate(uint32_t baud_rate);
|
||||
void set_baud_rate(uint32_t baud_rate) { this->baud_rate_ = baud_rate; }
|
||||
uint32_t get_baud_rate() const { return baud_rate_; }
|
||||
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
|
||||
Stream *get_hw_serial() const { return hw_serial_; }
|
||||
@@ -163,7 +163,7 @@ class Logger final : public Component {
|
||||
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
|
||||
void set_uart_selection(UARTSelection uart_selection) { uart_ = uart_selection; }
|
||||
/// Get the UART used by the logger.
|
||||
UARTSelection get_uart() const;
|
||||
UARTSelection get_uart() const { return this->uart_; }
|
||||
#endif
|
||||
|
||||
/// Set the default log level for this logger.
|
||||
@@ -197,7 +197,7 @@ class Logger final : public Component {
|
||||
void add_level_listener(LoggerLevelListener *listener) { this->level_listeners_.push_back(listener); }
|
||||
#endif
|
||||
|
||||
float get_setup_priority() const override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS + 500.0f; }
|
||||
|
||||
void log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args); // NOLINT
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
|
||||
@@ -8,6 +8,7 @@ from esphome.const import (
|
||||
CONF_ON_STATE,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_USE_FAHRENHEIT,
|
||||
)
|
||||
from esphome.core import ID, Lambda
|
||||
from esphome.cpp_generator import LambdaExpression, MockObj
|
||||
@@ -71,6 +72,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(
|
||||
CONF_TELEMETRY_REQUEST_MIN_INTERVAL, default="60s"
|
||||
): cv.update_interval,
|
||||
cv.Optional(CONF_USE_FAHRENHEIT, default=False): cv.boolean,
|
||||
cv.Optional(CONF_VANE): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ON_STATE): automation.validate_automation({}),
|
||||
@@ -114,6 +116,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
config[CONF_TELEMETRY_REQUEST_MIN_INTERVAL]
|
||||
)
|
||||
)
|
||||
cg.add(var.set_use_fahrenheit(config[CONF_USE_FAHRENHEIT]))
|
||||
if on_state := config.get(CONF_VANE, {}).get(CONF_ON_STATE):
|
||||
cg.add_global(mitsubishi_ns.using)
|
||||
for conf in on_state:
|
||||
|
||||
@@ -83,6 +83,7 @@ class MitsubishiCN105 {
|
||||
return this->is_telemetry_polling_enabled() ? !std::isnan(this->status_.room_temperature)
|
||||
: !std::isnan(this->status_.target_temperature);
|
||||
}
|
||||
bool is_temperature_encoding_b() const { return this->property_context_.use_temperature_encoding_b; }
|
||||
|
||||
void set_power(bool power_on);
|
||||
void set_target_temperature(float target_temperature);
|
||||
|
||||
@@ -50,7 +50,11 @@ static constexpr std::optional<Left> reverse_map_lookup(const std::array<std::pa
|
||||
return key.has_value() ? reverse_map_lookup(map, *key) : std::nullopt;
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::dump_config() { LOG_CLIMATE("", "Mitsubishi CN105 Climate", this); }
|
||||
void MitsubishiCN105Climate::dump_config() {
|
||||
LOG_CLIMATE("", "Mitsubishi CN105 Climate", this);
|
||||
ESP_LOGCONFIG(TAG, " Temperature unit: °%c",
|
||||
this->parent_->get_temperature_mapping().get_use_fahrenheit() ? 'F' : 'C');
|
||||
}
|
||||
|
||||
void MitsubishiCN105Climate::setup() {
|
||||
this->parent_->add_on_status_callback([this]() { this->apply_values_(); });
|
||||
@@ -72,13 +76,15 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
|
||||
traits.set_supported_swing_modes(this->supported_swing_modes_);
|
||||
|
||||
traits.set_visual_min_temperature(16.0f);
|
||||
traits.set_visual_max_temperature(31.0f);
|
||||
const bool use_fahrenheit = this->parent_->get_temperature_mapping().get_use_fahrenheit();
|
||||
traits.set_temperature_unit(use_fahrenheit ? TemperatureUnit::FAHRENHEIT : TemperatureUnit::CELSIUS);
|
||||
traits.set_visual_min_temperature(use_fahrenheit ? 61.0f : 16.0f);
|
||||
traits.set_visual_max_temperature(use_fahrenheit ? 88.0f : 31.0f);
|
||||
traits.set_visual_temperature_step(1.0f);
|
||||
|
||||
if (this->parent_->is_telemetry_polling_enabled()) {
|
||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||
traits.set_visual_current_temperature_step(0.5f);
|
||||
traits.set_visual_current_temperature_step(use_fahrenheit ? 1.0f : 0.5f);
|
||||
}
|
||||
|
||||
return traits;
|
||||
@@ -86,7 +92,7 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||
|
||||
void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
|
||||
if (const auto target_temperature = call.get_target_temperature()) {
|
||||
this->parent_->set_target_temperature(*target_temperature);
|
||||
this->parent_->set_target_temperature(this->parent_->get_temperature_mapping().to_mitsubishi(*target_temperature));
|
||||
}
|
||||
|
||||
if (const auto mode = call.get_mode()) {
|
||||
@@ -139,10 +145,10 @@ void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
|
||||
void MitsubishiCN105Climate::apply_values_() {
|
||||
const auto &status = this->parent_->status();
|
||||
|
||||
this->target_temperature = status.target_temperature;
|
||||
this->target_temperature = this->parent_->get_temperature_mapping().from_mitsubishi(status.target_temperature);
|
||||
|
||||
if (this->parent_->is_telemetry_polling_enabled()) {
|
||||
this->current_temperature = status.room_temperature;
|
||||
this->current_temperature = this->parent_->get_temperature_mapping().from_mitsubishi(status.room_temperature);
|
||||
}
|
||||
|
||||
if (status.power_on) {
|
||||
|
||||
@@ -27,6 +27,13 @@ void MitsubishiCN105Component::setup() { this->hp_.initialize(); }
|
||||
|
||||
void MitsubishiCN105Component::loop() {
|
||||
if (this->hp_.update()) {
|
||||
// Encoding A only supports whole °C values and cannot represent native °F setpoints accurately.
|
||||
// See https://github.com/esphome/esphome/pull/15488#issuecomment-5268304343
|
||||
if (this->temperature_mapping_.get_use_fahrenheit() && !this->hp_.is_temperature_encoding_b()) {
|
||||
ESP_LOGE(TAG, "Unit reports encoding A, which cannot accurately convert °F setpoints; disable 'use_fahrenheit'");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
this->notify_status_listeners_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,43 @@
|
||||
#include "mitsubishi_cn105.h"
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
#include <utility>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
namespace esphome::mitsubishi_cn105 {
|
||||
|
||||
struct TemperatureMapping {
|
||||
float to_mitsubishi(float value) const {
|
||||
if (!this->use_fahrenheit_) {
|
||||
return value;
|
||||
}
|
||||
const int fahrenheit = std::clamp(static_cast<int>(std::round(value)), 61, 88);
|
||||
return 0.5f * (fahrenheit - 28 + (fahrenheit > 68) - (fahrenheit < 68));
|
||||
}
|
||||
|
||||
float from_mitsubishi(float value) const {
|
||||
if (!this->use_fahrenheit_) {
|
||||
return value;
|
||||
}
|
||||
if (value < 16.0f || value > 30.5f) {
|
||||
return celsius_to_fahrenheit(value);
|
||||
}
|
||||
const int mitsubishi_half_degrees = static_cast<int>(std::round(value * 2.0f));
|
||||
return mitsubishi_half_degrees + 29 - (mitsubishi_half_degrees >= 40) - (mitsubishi_half_degrees > 40);
|
||||
}
|
||||
|
||||
bool get_use_fahrenheit() const { return this->use_fahrenheit_; }
|
||||
void set_use_fahrenheit(bool value) { this->use_fahrenheit_ = value; }
|
||||
|
||||
protected:
|
||||
bool use_fahrenheit_{false};
|
||||
};
|
||||
|
||||
enum VerticalVaneMode : uint8_t {
|
||||
VERTICAL_VANE_MODE_AUTO = static_cast<uint8_t>(MitsubishiCN105::VaneMode::AUTO),
|
||||
VERTICAL_VANE_MODE_POSITION_1 = static_cast<uint8_t>(MitsubishiCN105::VaneMode::POSITION_1),
|
||||
@@ -60,6 +90,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
||||
|
||||
void set_update_interval(uint32_t ms) { this->hp_.set_update_interval(ms); }
|
||||
void set_telemetry_request_min_interval(uint32_t ms) { this->hp_.set_telemetry_request_min_interval(ms); }
|
||||
void set_use_fahrenheit(bool value) { this->temperature_mapping_.set_use_fahrenheit(value); }
|
||||
|
||||
void set_remote_temperature(float temperature) { this->hp_.set_remote_temperature(temperature); }
|
||||
void clear_remote_temperature() { this->hp_.clear_remote_temperature(); }
|
||||
@@ -75,6 +106,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
||||
const MitsubishiCN105::Status &status() const { return this->hp_.status(); }
|
||||
bool is_status_initialized() const { return this->hp_.is_status_initialized(); }
|
||||
bool is_telemetry_polling_enabled() const { return this->hp_.is_telemetry_polling_enabled(); }
|
||||
const TemperatureMapping &get_temperature_mapping() const { return this->temperature_mapping_; }
|
||||
|
||||
template<typename F> void add_on_status_callback(F &&callback) {
|
||||
this->status_callback_.add(std::forward<F>(callback));
|
||||
@@ -99,6 +131,7 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
|
||||
}
|
||||
|
||||
MitsubishiCN105 hp_;
|
||||
TemperatureMapping temperature_mapping_;
|
||||
CallbackManager<void()> status_callback_;
|
||||
LazyCallbackManager<void(const VaneState &)> vane_state_callback_;
|
||||
};
|
||||
|
||||
@@ -668,9 +668,7 @@ void MQTTClientComponent::on_message(const std::string &topic, const std::string
|
||||
// Setters
|
||||
void MQTTClientComponent::disable_log_message() { this->log_message_.topic = ""; }
|
||||
bool MQTTClientComponent::is_log_message_enabled() const { return !this->log_message_.topic.empty(); }
|
||||
void MQTTClientComponent::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
void MQTTClientComponent::register_mqtt_component(MQTTComponent *component) { this->children_.push_back(component); }
|
||||
void MQTTClientComponent::set_log_level(int level) { this->log_level_ = level; }
|
||||
void MQTTClientComponent::set_keep_alive(uint16_t keep_alive_s) { this->mqtt_backend_.set_keep_alive(keep_alive_s); }
|
||||
void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this->log_message_ = std::move(message); }
|
||||
const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; }
|
||||
@@ -683,10 +681,6 @@ void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, cons
|
||||
}
|
||||
}
|
||||
const std::string &MQTTClientComponent::get_topic_prefix() const { return this->topic_prefix_; }
|
||||
void MQTTClientComponent::set_publish_nan_as_none(bool publish_nan_as_none) {
|
||||
this->publish_nan_as_none_ = publish_nan_as_none;
|
||||
}
|
||||
bool MQTTClientComponent::is_publish_nan_as_none() const { return this->publish_nan_as_none_; }
|
||||
void MQTTClientComponent::disable_birth_message() {
|
||||
this->birth_message_.topic = "";
|
||||
this->recalculate_availability_();
|
||||
@@ -766,8 +760,6 @@ MQTTClientComponent *global_mqtt_client = nullptr; // NOLINT(cppcoreguidelines-
|
||||
|
||||
// MQTTMessageTrigger
|
||||
MQTTMessageTrigger::MQTTMessageTrigger(std::string topic) : topic_(std::move(topic)) {}
|
||||
void MQTTMessageTrigger::set_qos(uint8_t qos) { this->qos_ = qos; }
|
||||
void MQTTMessageTrigger::set_payload(const std::string &payload) { this->payload_ = payload; }
|
||||
void MQTTMessageTrigger::setup() {
|
||||
global_mqtt_client->subscribe(
|
||||
this->topic_,
|
||||
|
||||
@@ -159,7 +159,7 @@ class MQTTClientComponent final : public Component {
|
||||
|
||||
/// Manually set the topic used for logging.
|
||||
void set_log_message_template(MQTTMessage &&message);
|
||||
void set_log_level(int level);
|
||||
void set_log_level(int level) { this->log_level_ = level; }
|
||||
/// Get the topic used for logging. Defaults to "<topic_prefix>/debug" and the value is cached for speed.
|
||||
void disable_log_message();
|
||||
bool is_log_message_enabled() const;
|
||||
@@ -241,7 +241,7 @@ class MQTTClientComponent final : public Component {
|
||||
|
||||
void check_connected();
|
||||
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
void set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
|
||||
|
||||
void register_mqtt_component(MQTTComponent *component);
|
||||
|
||||
@@ -262,8 +262,8 @@ class MQTTClientComponent final : public Component {
|
||||
void set_on_disconnect(mqtt_on_disconnect_callback_t &&callback);
|
||||
|
||||
// Publish None state instead of NaN for Home Assistant
|
||||
void set_publish_nan_as_none(bool publish_nan_as_none);
|
||||
bool is_publish_nan_as_none() const;
|
||||
void set_publish_nan_as_none(bool publish_nan_as_none) { this->publish_nan_as_none_ = publish_nan_as_none; }
|
||||
bool is_publish_nan_as_none() const { return this->publish_nan_as_none_; }
|
||||
|
||||
void set_wait_for_connection(bool wait_for_connection) { this->wait_for_connection_ = wait_for_connection; }
|
||||
|
||||
@@ -344,8 +344,8 @@ class MQTTMessageTrigger final : public Trigger<std::string>, public Component {
|
||||
public:
|
||||
explicit MQTTMessageTrigger(std::string topic);
|
||||
|
||||
void set_qos(uint8_t qos);
|
||||
void set_payload(const std::string &payload);
|
||||
void set_qos(uint8_t qos) { this->qos_ = qos; }
|
||||
void set_payload(const std::string &payload) { this->payload_ = payload; }
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override;
|
||||
|
||||
@@ -118,8 +118,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo
|
||||
root[MQTT_TARGET_TEMPERATURE_STEP] = roundf(traits.get_visual_target_temperature_step() * 10) * 0.1f;
|
||||
// current_temp_step
|
||||
root[MQTT_CURRENT_TEMPERATURE_STEP] = roundf(traits.get_visual_current_temperature_step() * 10) * 0.1f;
|
||||
// temperature units are always coerced to Celsius internally
|
||||
root[MQTT_TEMPERATURE_UNIT] = "C";
|
||||
root[MQTT_TEMPERATURE_UNIT] = traits.get_temperature_unit() == TemperatureUnit::FAHRENHEIT ? "F" : "C";
|
||||
|
||||
// min_humidity
|
||||
root[MQTT_MIN_HUMIDITY] = traits.get_visual_min_humidity();
|
||||
|
||||
@@ -340,10 +340,6 @@ bool MQTTComponent::send_discovery_() {
|
||||
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
}
|
||||
|
||||
uint8_t MQTTComponent::get_qos() const { return this->qos_; }
|
||||
|
||||
bool MQTTComponent::get_retain() const { return this->retain_; }
|
||||
|
||||
bool MQTTComponent::is_discovery_enabled() const {
|
||||
return this->discovery_enabled_ && global_mqtt_client->is_discovery_enabled();
|
||||
}
|
||||
|
||||
@@ -108,11 +108,11 @@ class MQTTComponent : public Component {
|
||||
|
||||
/// Set QOS for state messages.
|
||||
void set_qos(uint8_t qos);
|
||||
uint8_t get_qos() const;
|
||||
uint8_t get_qos() const { return this->qos_; }
|
||||
|
||||
/// Set whether state message should be retained.
|
||||
void set_retain(bool retain);
|
||||
bool get_retain() const;
|
||||
bool get_retain() const { return this->retain_; }
|
||||
|
||||
/// Disable discovery. Sets friendly name to "".
|
||||
void disable_discovery();
|
||||
|
||||
@@ -39,8 +39,6 @@ uint32_t MQTTSensorComponent::get_expire_after() const {
|
||||
return *this->expire_after_;
|
||||
return 0;
|
||||
}
|
||||
void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire_after_ = expire_after; }
|
||||
void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; }
|
||||
|
||||
void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {
|
||||
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
|
||||
|
||||
@@ -22,9 +22,9 @@ class MQTTSensorComponent final : public mqtt::MQTTComponent {
|
||||
explicit MQTTSensorComponent(sensor::Sensor *sensor);
|
||||
|
||||
/// Setup an expiry, 0 disables it
|
||||
void set_expire_after(uint32_t expire_after);
|
||||
void set_expire_after(uint32_t expire_after) { this->expire_after_ = expire_after; }
|
||||
/// Disable Home Assistant value expiry.
|
||||
void disable_expire_after();
|
||||
void disable_expire_after() { this->expire_after_ = 0; }
|
||||
|
||||
void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user