mirror of
https://github.com/esphome/esphome.git
synced 2026-01-22 19:09:11 -07:00
Compare commits
1258 Commits
jesserockz
...
api_dispat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0e687ff0b7 | ||
|
|
6d1d7f137f | ||
|
|
38e16efa11 | ||
|
|
5e2f0f7f5e | ||
|
|
83c7afc46f | ||
|
|
10753f0f99 | ||
|
|
34a852d433 | ||
|
|
3922fbdef7 | ||
|
|
e5415abf20 | ||
|
|
67e1a92cce | ||
|
|
4c64511a15 | ||
|
|
75f3e0900e | ||
|
|
abd33c21bf | ||
|
|
d592ba2c5e | ||
|
|
321eba5184 | ||
|
|
82b9ec53fd | ||
|
|
b9262f967b | ||
|
|
949fb9a890 | ||
|
|
99952a701f | ||
|
|
88878adb6c | ||
|
|
17e3b49ebb | ||
|
|
a217747f5d | ||
|
|
790c9cbb84 | ||
|
|
da5fb6e24f | ||
|
|
a77439b4b7 | ||
|
|
1a049bdcbb | ||
|
|
79686239d3 | ||
|
|
c934e84e21 | ||
|
|
5e2f8cb018 | ||
|
|
6bd0af6d85 | ||
|
|
0f28a49822 | ||
|
|
66d96646b1 | ||
|
|
be4cf6505f | ||
|
|
e8ea7825a9 | ||
|
|
8c13eab731 | ||
|
|
bf4cbb0aee | ||
|
|
aaec4b7bd3 | ||
|
|
7bddcd4f64 | ||
|
|
af205a5267 | ||
|
|
c2599d7719 | ||
|
|
4ea6f23d9e | ||
|
|
f23fd52a26 | ||
|
|
cfd43c81fb | ||
|
|
3dcba675b4 | ||
|
|
bb51031ec6 | ||
|
|
ecb99cbcce | ||
|
|
0a514821c6 | ||
|
|
074fbb522c | ||
|
|
71d6ba242e | ||
|
|
37ffd64b48 | ||
|
|
ec65652567 | ||
|
|
731613421d | ||
|
|
58dfad4ed0 | ||
|
|
7eb029f4b9 | ||
|
|
8da8d938f0 | ||
|
|
64ac0d2bde | ||
|
|
7d3cdd15ad | ||
|
|
53baf02087 | ||
|
|
a0d2392344 | ||
|
|
fb3c092eaa | ||
|
|
c169cf1e77 | ||
|
|
fa4d8e083a | ||
|
|
2cfeccfd71 | ||
|
|
f36ca93752 | ||
|
|
dc8714c277 | ||
|
|
90fcb5fbcd | ||
|
|
932d0a5d8b | ||
|
|
4cafa18fa4 | ||
|
|
b12d7db5a7 | ||
|
|
e84345594d | ||
|
|
add7bec7f2 | ||
|
|
db84d8e8dc | ||
|
|
ad51e647af | ||
|
|
c45901746b | ||
|
|
033c469250 | ||
|
|
0900fd3cea | ||
|
|
ba8f3d3f63 | ||
|
|
2759f3828e | ||
|
|
f395767766 | ||
|
|
2dc222aea6 | ||
|
|
82d68c87e2 | ||
|
|
f213657753 | ||
|
|
e077e6cec7 | ||
|
|
339a3270f6 | ||
|
|
462b44ee23 | ||
|
|
52d3dba89c | ||
|
|
939d01dd99 | ||
|
|
4900f7c7ca | ||
|
|
48957aee8b | ||
|
|
e355ce04f7 | ||
|
|
758e5b89bb | ||
|
|
3ffdd1d451 | ||
|
|
4c1b8c8b96 | ||
|
|
3ca956cd6a | ||
|
|
2e24a11a1d | ||
|
|
10a03ad538 | ||
|
|
69839ec4dc | ||
|
|
28a66d4bf0 | ||
|
|
782d894801 | ||
|
|
06dd731c78 | ||
|
|
6af74302dc | ||
|
|
03380a6ecd | ||
|
|
8d8db11dd9 | ||
|
|
28886a896b | ||
|
|
05253991c2 | ||
|
|
96f0fda477 | ||
|
|
023fa4d220 | ||
|
|
a1f63c0dfc | ||
|
|
ef98f42e7e | ||
|
|
737e1284af | ||
|
|
8677918157 | ||
|
|
629c891dfc | ||
|
|
8e8ef83780 | ||
|
|
2a15f35e9d | ||
|
|
9bfa942cf2 | ||
|
|
b00adbddce | ||
|
|
a71030c4de | ||
|
|
6bb32c2e61 | ||
|
|
7bc2c685e0 | ||
|
|
9205338cc8 | ||
|
|
04336f7ba3 | ||
|
|
6f64312d08 | ||
|
|
79dfb86830 | ||
|
|
453dc29540 | ||
|
|
f4260d370c | ||
|
|
655f9489a8 | ||
|
|
4b3cc52afe | ||
|
|
fd3f15637a | ||
|
|
1311e1b8b0 | ||
|
|
64e84872da | ||
|
|
bc7379030e | ||
|
|
ecfb6dc8ed | ||
|
|
75d67af932 | ||
|
|
845dad6ee7 | ||
|
|
e2e86da64b | ||
|
|
90ec63589f | ||
|
|
ea308eaaa2 | ||
|
|
87f1fac2bf | ||
|
|
c23651527f | ||
|
|
2cc263a707 | ||
|
|
fb336718de | ||
|
|
e2e35bf965 | ||
|
|
bdd25c7268 | ||
|
|
82c788d6ce | ||
|
|
5167184cc7 | ||
|
|
a5d1b11204 | ||
|
|
dc8f2fd37e | ||
|
|
7c85886ce8 | ||
|
|
12f172436d | ||
|
|
e69ac0478e | ||
|
|
a45743c2b7 | ||
|
|
ebe1531927 | ||
|
|
a88a059c6a | ||
|
|
d314cbb0d5 | ||
|
|
4d75758eb2 | ||
|
|
0eecc29039 | ||
|
|
294fb67410 | ||
|
|
2f1f098b47 | ||
|
|
77be414261 | ||
|
|
c34fc3c4c7 | ||
|
|
8aac2f525e | ||
|
|
f85dcdca4e | ||
|
|
e7a1ef7aa1 | ||
|
|
7c2d2ef5a3 | ||
|
|
1449001747 | ||
|
|
f245c74520 | ||
|
|
da1658e4f9 | ||
|
|
80f9352a79 | ||
|
|
9ded501402 | ||
|
|
3d6a1811c5 | ||
|
|
a5ee047efb | ||
|
|
fb0090dcdc | ||
|
|
294bd4d042 | ||
|
|
e99b8d2daf | ||
|
|
6dbdeeb59b | ||
|
|
82fd62e9dd | ||
|
|
70f935d323 | ||
|
|
0f3e6cccd9 | ||
|
|
6ff323c56d | ||
|
|
096ec79ef9 | ||
|
|
bf5ba65558 | ||
|
|
62088dfaed | ||
|
|
dfcc3206f7 | ||
|
|
e173b7f0c2 | ||
|
|
f98e28a8a2 | ||
|
|
f63557f2e7 | ||
|
|
a353598961 | ||
|
|
bc33b44648 | ||
|
|
1579779967 | ||
|
|
cc6ea4cd14 | ||
|
|
303a8ff87a | ||
|
|
7d3a11a735 | ||
|
|
94b6344820 | ||
|
|
40307c079c | ||
|
|
debef6fde4 | ||
|
|
0cda83d29c | ||
|
|
32729c7ca7 | ||
|
|
b7fca5488a | ||
|
|
9c22772758 | ||
|
|
1e72f07fdf | ||
|
|
a592e96709 | ||
|
|
3980339868 | ||
|
|
afa66c17bd | ||
|
|
be2988b1d7 | ||
|
|
cf647f0c36 | ||
|
|
385ed4ca0c | ||
|
|
9188a8e326 | ||
|
|
0efb6d55c8 | ||
|
|
f748047b7b | ||
|
|
49bc767bf4 | ||
|
|
e12cc9a9a7 | ||
|
|
8e4470cdff | ||
|
|
bdb7e19fd0 | ||
|
|
0fc3f0e162 | ||
|
|
6fac66e63b | ||
|
|
71e06ea1b6 | ||
|
|
3df434fd55 | ||
|
|
729b2b2873 | ||
|
|
bc2adb6b5a | ||
|
|
aaff086aeb | ||
|
|
e4c0f18ee3 | ||
|
|
9c09a271f2 | ||
|
|
37578f3e22 | ||
|
|
4649599592 | ||
|
|
71f78e3a81 | ||
|
|
f7ca26eef8 | ||
|
|
0665fcea9e | ||
|
|
cd2b50c27f | ||
|
|
ca70f17b3b | ||
|
|
a5e08aaf74 | ||
|
|
947db4605a | ||
|
|
481a00a0b5 | ||
|
|
465019e510 | ||
|
|
a4d5f39fb6 | ||
|
|
5dd76966c3 | ||
|
|
db86f87fc3 | ||
|
|
e21334b7fa | ||
|
|
ba4c268956 | ||
|
|
068594be5e | ||
|
|
0fd45fc86e | ||
|
|
257fb98113 | ||
|
|
f8922b3cca | ||
|
|
b0b08f317b | ||
|
|
2c4667fb46 | ||
|
|
9eadfa21d8 | ||
|
|
953fd24458 | ||
|
|
1be171e084 | ||
|
|
5c83b99e0c | ||
|
|
743e611735 | ||
|
|
35ff850894 | ||
|
|
b666295b53 | ||
|
|
96cf8d97ab | ||
|
|
3c1a781a1c | ||
|
|
00bd1b0a02 | ||
|
|
b8482da421 | ||
|
|
756ece9ff3 | ||
|
|
4bb016fec3 | ||
|
|
32f0322dec | ||
|
|
1a1c13b722 | ||
|
|
139453822b | ||
|
|
7a33994666 | ||
|
|
f381d9011b | ||
|
|
96352f047d | ||
|
|
5e7a1fea8c | ||
|
|
64eb70444d | ||
|
|
0f39b1c49a | ||
|
|
e2d6363c68 | ||
|
|
cdeef700c2 | ||
|
|
86fd702841 | ||
|
|
6c62d4a923 | ||
|
|
6e42d009fb | ||
|
|
7d7769ea5d | ||
|
|
3908677fe2 | ||
|
|
9799a2b636 | ||
|
|
55c8129423 | ||
|
|
099474053e | ||
|
|
efafabed97 | ||
|
|
d209739f85 | ||
|
|
d463dd0f57 | ||
|
|
c33c14a46f | ||
|
|
2d0c109dc1 | ||
|
|
825d0bed88 | ||
|
|
cd1390916c | ||
|
|
149bdaf146 | ||
|
|
ad628c9cba | ||
|
|
b88f87799e | ||
|
|
1ff7cf1125 | ||
|
|
31db6e51eb | ||
|
|
681d9236f9 | ||
|
|
8aa8af735d | ||
|
|
943d0f103d | ||
|
|
8b195d7f63 | ||
|
|
649ad47e62 | ||
|
|
93dc5765bb | ||
|
|
b000b1b70c | ||
|
|
8707b6e01a | ||
|
|
34abd67f3e | ||
|
|
45f1db9233 | ||
|
|
beb4d1511a | ||
|
|
adeceee71f | ||
|
|
7d4b11d112 | ||
|
|
6733cd4ed1 | ||
|
|
07f361a404 | ||
|
|
ae981ea7f2 | ||
|
|
b7d0f5e36b | ||
|
|
3cbce4df42 | ||
|
|
7e77e40bda | ||
|
|
305805256d | ||
|
|
e36c669dc0 | ||
|
|
71aff9bc60 | ||
|
|
36d11c969f | ||
|
|
f76ce5d3bb | ||
|
|
0df454481e | ||
|
|
83c1a30cfb | ||
|
|
6cbd1479c6 | ||
|
|
3e6e438920 | ||
|
|
560886eb90 | ||
|
|
340bb5cef6 | ||
|
|
44a7c1d4a5 | ||
|
|
519c49f175 | ||
|
|
c96ffefa42 | ||
|
|
490ca8ad5a | ||
|
|
e385f87d6c | ||
|
|
58de53123a | ||
|
|
4f365c1716 | ||
|
|
981177da23 | ||
|
|
088bea9ccd | ||
|
|
36350f179e | ||
|
|
902f08c1bc | ||
|
|
47ad206ccd | ||
|
|
9f51546023 | ||
|
|
f6d679f056 | ||
|
|
93c45e88e7 | ||
|
|
da189da9ae | ||
|
|
c40a33cb48 | ||
|
|
9846beee7d | ||
|
|
81685f9132 | ||
|
|
14123d25c2 | ||
|
|
928819ffbd | ||
|
|
7f2f9636f5 | ||
|
|
b49fe146ad | ||
|
|
98bbd4136b | ||
|
|
d8d02f71ba | ||
|
|
26980df2b9 | ||
|
|
ffe39473d0 | ||
|
|
6af8d152ee | ||
|
|
de846a8f7a | ||
|
|
8e31316e3d | ||
|
|
fb6edb3243 | ||
|
|
244bd9256f | ||
|
|
1f61fd383c | ||
|
|
ce294ce0c1 | ||
|
|
dcbdc0ac51 | ||
|
|
daea06586d | ||
|
|
9c8bf2587b | ||
|
|
9871cb04ea | ||
|
|
7dc093815f | ||
|
|
087697106c | ||
|
|
9beebc7bfe | ||
|
|
4a948b7aae | ||
|
|
0d3bc21e97 | ||
|
|
7496894ae6 | ||
|
|
918d7217a9 | ||
|
|
2103d583f9 | ||
|
|
837c446926 | ||
|
|
480ea54ee0 | ||
|
|
97e7c34cb6 | ||
|
|
fe65b149f5 | ||
|
|
4106b97174 | ||
|
|
8648954b94 | ||
|
|
9f1fae0955 | ||
|
|
1d631c3c6d | ||
|
|
727161f1db | ||
|
|
bf5f628769 | ||
|
|
8563a5785f | ||
|
|
4082634e6d | ||
|
|
a74adb5865 | ||
|
|
2e4d7301f2 | ||
|
|
94845222ad | ||
|
|
7f6ac2deee | ||
|
|
a054aa9c52 | ||
|
|
22cb59b88c | ||
|
|
6968772a31 | ||
|
|
004f4b51d1 | ||
|
|
8c8dd7b4bc | ||
|
|
9778289d33 | ||
|
|
a43caf08a6 | ||
|
|
01e550fac9 | ||
|
|
ad4dd6a060 | ||
|
|
849d99b0dc | ||
|
|
f5df5f71a3 | ||
|
|
429be0a5ae | ||
|
|
148e4ec555 | ||
|
|
bb22f4d6a3 | ||
|
|
f94703360b | ||
|
|
f26bec1a5a | ||
|
|
d065f4ae62 | ||
|
|
ed2c3e626b | ||
|
|
1927f92358 | ||
|
|
939144174c | ||
|
|
59bcbe7fef | ||
|
|
8e00fedc67 | ||
|
|
0ac879ae0b | ||
|
|
22d1a18d22 | ||
|
|
ca203bff9b | ||
|
|
e01d16ce82 | ||
|
|
93b6b9835c | ||
|
|
d0ac5388d9 | ||
|
|
9097d646ca | ||
|
|
596a28e1fb | ||
|
|
5205ff5c43 | ||
|
|
c420bf5f4f | ||
|
|
18844e15dc | ||
|
|
af2f5b7348 | ||
|
|
bcbf0f0e26 | ||
|
|
4d460d4bc3 | ||
|
|
92f6f3ac0d | ||
|
|
bc63d246c8 | ||
|
|
b25f272d72 | ||
|
|
e3a3305adb | ||
|
|
c655c4e106 | ||
|
|
7fe8cdaa34 | ||
|
|
df97985048 | ||
|
|
3779675816 | ||
|
|
0005aad5b5 | ||
|
|
98c18517e2 | ||
|
|
e4dee935ce | ||
|
|
f8cb44fb3c | ||
|
|
101901fdb8 | ||
|
|
b8579d2040 | ||
|
|
3fca3df756 | ||
|
|
2f5db85997 | ||
|
|
e0d4361875 | ||
|
|
30bafc43bd | ||
|
|
3530437b48 | ||
|
|
81db42942c | ||
|
|
6cb0d9e0b5 | ||
|
|
19f7e36753 | ||
|
|
a963f97520 | ||
|
|
ad2d48e9b7 | ||
|
|
5c0d67ca14 | ||
|
|
3467329a7c | ||
|
|
d73fa370f3 | ||
|
|
78fd0a4870 | ||
|
|
3162bb475d | ||
|
|
c17503abd5 | ||
|
|
3433ee8171 | ||
|
|
344297b0a7 | ||
|
|
947456628e | ||
|
|
80dd6c111d | ||
|
|
b70188ba4b | ||
|
|
c6064aa2b4 | ||
|
|
6596f864be | ||
|
|
f61a40efb8 | ||
|
|
b049f0b480 | ||
|
|
b2641d29c1 | ||
|
|
7b8cfc768d | ||
|
|
04860567f7 | ||
|
|
b16edb5a99 | ||
|
|
15a995b2e7 | ||
|
|
f57e26c54e | ||
|
|
2b7bc1cd9f | ||
|
|
614a2f66a3 | ||
|
|
9047b02c92 | ||
|
|
e73d0477bb | ||
|
|
2b1e623eb4 | ||
|
|
c366d555e9 | ||
|
|
7efbd62730 | ||
|
|
b77c1d0af8 | ||
|
|
f8810ea6a8 | ||
|
|
40dd667211 | ||
|
|
848b572864 | ||
|
|
7c858fbccd | ||
|
|
a1814ea37d | ||
|
|
5892a1dbe2 | ||
|
|
29f524f432 | ||
|
|
4ec588ebd7 | ||
|
|
efdef61477 | ||
|
|
fe2b9f8c12 | ||
|
|
c6be55eb55 | ||
|
|
4c69925b84 | ||
|
|
bc6407df0a | ||
|
|
01982a8d0a | ||
|
|
b995cd6257 | ||
|
|
b16d7b7a95 | ||
|
|
42aea701d3 | ||
|
|
5f56c85182 | ||
|
|
52b4eb8950 | ||
|
|
eeb2b42a0f | ||
|
|
90772033d1 | ||
|
|
dadeb4d2a9 | ||
|
|
60a5029c88 | ||
|
|
d7ba16b48b | ||
|
|
fca9befa63 | ||
|
|
187cbde0db | ||
|
|
f5ae5cade8 | ||
|
|
3e66c28aff | ||
|
|
89703a1aef | ||
|
|
cba31617e9 | ||
|
|
a3eeb46961 | ||
|
|
128bd76f20 | ||
|
|
c0355fd2c6 | ||
|
|
a5fd440e25 | ||
|
|
592ef8be2a | ||
|
|
3bcc1c7297 | ||
|
|
3b44c3acd1 | ||
|
|
ec4911643a | ||
|
|
f4fedbab44 | ||
|
|
553d441ecc | ||
|
|
1946116438 | ||
|
|
ab28515fba | ||
|
|
4dc11fb95e | ||
|
|
e27094e0f3 | ||
|
|
88302201eb | ||
|
|
8afb172e83 | ||
|
|
562d024623 | ||
|
|
50b094547c | ||
|
|
a6c1e50985 | ||
|
|
96772bdfc6 | ||
|
|
ed154d373c | ||
|
|
a5e862ce36 | ||
|
|
ae55964bd9 | ||
|
|
c162309f41 | ||
|
|
62c667f1a0 | ||
|
|
3d08eae8e4 | ||
|
|
2af5a0a6dd | ||
|
|
6d24b04235 | ||
|
|
3ee8103353 | ||
|
|
1296165fce | ||
|
|
7100c22dc4 | ||
|
|
5718c0f5b8 | ||
|
|
25ebddfa1c | ||
|
|
2c0558fe23 | ||
|
|
7192108fc1 | ||
|
|
847696c342 | ||
|
|
912ae1fc87 | ||
|
|
a86f75d31d | ||
|
|
fe1e25b5c7 | ||
|
|
9b241b596a | ||
|
|
53b9c8d5bb | ||
|
|
2946bc9d72 | ||
|
|
67a20e212d | ||
|
|
a9ace366eb | ||
|
|
df3469efba | ||
|
|
0a3bbb8554 | ||
|
|
a15b9f5d3b | ||
|
|
e6334b0716 | ||
|
|
7a835baa5a | ||
|
|
c9c21a5728 | ||
|
|
956959fc32 | ||
|
|
6f67f74638 | ||
|
|
b3dd4543b7 | ||
|
|
4f17a28ac5 | ||
|
|
90736f367a | ||
|
|
9af88bd482 | ||
|
|
13b89f4934 | ||
|
|
d00a00d142 | ||
|
|
e662c39e16 | ||
|
|
95ef131285 | ||
|
|
7476f170f6 | ||
|
|
3b6bd55d1e | ||
|
|
10dbc9e884 | ||
|
|
860f619dfe | ||
|
|
17ddc9ee0c | ||
|
|
949689c318 | ||
|
|
86a2aac011 | ||
|
|
d0a402f201 | ||
|
|
05772d5365 | ||
|
|
c2a68f5147 | ||
|
|
697ca1c7be | ||
|
|
409346952f | ||
|
|
f4b3539d77 | ||
|
|
c12166c1a1 | ||
|
|
8d20f003cb | ||
|
|
88f857a2f0 | ||
|
|
fb7faadd99 | ||
|
|
5c8d6752fb | ||
|
|
dda81fbc2c | ||
|
|
c40dff5d63 | ||
|
|
6f07b54772 | ||
|
|
ce0f1dfcb6 | ||
|
|
9a3a5d48eb | ||
|
|
4a759eda02 | ||
|
|
26badf201d | ||
|
|
384f27cd6d | ||
|
|
ac1c5f9f58 | ||
|
|
8ad058fdf4 | ||
|
|
9024c3c67a | ||
|
|
fc81a47499 | ||
|
|
a331452076 | ||
|
|
b1c6e8168e | ||
|
|
b41cc0226e | ||
|
|
450429ddd5 | ||
|
|
f7b24f4b4b | ||
|
|
294c985380 | ||
|
|
720964b901 | ||
|
|
8895c8a987 | ||
|
|
740dcd72a2 | ||
|
|
ffd442624f | ||
|
|
088fd85694 | ||
|
|
d5b68d69d3 | ||
|
|
bb0f7bb393 | ||
|
|
d86a108f18 | ||
|
|
7828ed2d9e | ||
|
|
ebf14f50fb | ||
|
|
1546ff615b | ||
|
|
46cf1fb597 | ||
|
|
8bf8655054 | ||
|
|
a6d84948e2 | ||
|
|
fac20a1f97 | ||
|
|
c65586b5e1 | ||
|
|
b27b018b06 | ||
|
|
403da1e632 | ||
|
|
2371ec1f9e | ||
|
|
5e3ec2d34b | ||
|
|
78d84644c9 | ||
|
|
0cd0f8015a | ||
|
|
4b5424f695 | ||
|
|
a1d59040f7 | ||
|
|
0306398072 | ||
|
|
a7e0bf9013 | ||
|
|
ddb988cd83 | ||
|
|
04b54353f1 | ||
|
|
f058107c05 | ||
|
|
6b5b0815d7 | ||
|
|
8388497038 | ||
|
|
825b1113b6 | ||
|
|
9074ef792f | ||
|
|
0946f28511 | ||
|
|
23765cd4f5 | ||
|
|
e20c6468d0 | ||
|
|
b90516de1d | ||
|
|
ec5cc0f00f | ||
|
|
5dda5a976e | ||
|
|
915da9ae13 | ||
|
|
8652464f4e | ||
|
|
ce6ce1c1f8 | ||
|
|
39efe67e55 | ||
|
|
748ffa00f3 | ||
|
|
e8d9df2b0e | ||
|
|
17396d67de | ||
|
|
edd6a86714 | ||
|
|
85b4012c56 | ||
|
|
7d98433502 | ||
|
|
23774ae03b | ||
|
|
0dedbcdd71 | ||
|
|
4bdd08887e | ||
|
|
1fd8ebf386 | ||
|
|
d2fc3e749c | ||
|
|
71fbcbceaf | ||
|
|
27347b2088 | ||
|
|
599993d1a5 | ||
|
|
bf359cb8e3 | ||
|
|
509a704410 | ||
|
|
1f48e2b01f | ||
|
|
8b25b1eee6 | ||
|
|
3bbf30ff5f | ||
|
|
83613726d1 | ||
|
|
254b6a17f3 | ||
|
|
796e12bd70 | ||
|
|
ddbe17d3f6 | ||
|
|
591ec36f4a | ||
|
|
41eceb72ef | ||
|
|
0a5f094025 | ||
|
|
ca0f3ba262 | ||
|
|
30f4e782db | ||
|
|
192158ef1a | ||
|
|
602456db40 | ||
|
|
536e45668f | ||
|
|
10bf05ab0d | ||
|
|
5ad1af69e4 | ||
|
|
48f2911434 | ||
|
|
dbb0d6349a | ||
|
|
ac3598f12a | ||
|
|
66201be5ca | ||
|
|
ac0b0b652e | ||
|
|
d89ee2df42 | ||
|
|
418e248e5e | ||
|
|
8c2b141049 | ||
|
|
2f8e07302b | ||
|
|
c3776240b6 | ||
|
|
e370872ec1 | ||
|
|
d4e978369a | ||
|
|
8d5d7f5237 | ||
|
|
5cd498fbe9 | ||
|
|
250f515f08 | ||
|
|
0ec0a9e313 | ||
|
|
184f42ef03 | ||
|
|
499517418d | ||
|
|
606b9c1a6d | ||
|
|
971e954a54 | ||
|
|
e3aaf3219d | ||
|
|
0eea1c0e40 | ||
|
|
0773819778 | ||
|
|
170869b7db | ||
|
|
5dc54782e5 | ||
|
|
97b26fbefe | ||
|
|
686cc58d6c | ||
|
|
76a59759b2 | ||
|
|
93245a24b5 | ||
|
|
6a22ea1c7d | ||
|
|
56a02409c8 | ||
|
|
edeafd5a53 | ||
|
|
f67490b69b | ||
|
|
b76e34fb7b | ||
|
|
ddbda5032b | ||
|
|
5898d34b0a | ||
|
|
b0c02341ff | ||
|
|
19cbc8c33b | ||
|
|
02e61ef5d3 | ||
|
|
8d5d18064d | ||
|
|
c5ef7ebd27 | ||
|
|
047a3e0e8c | ||
|
|
13b23f840b | ||
|
|
147f6012b2 | ||
|
|
2c315595f0 | ||
|
|
20405c84ac | ||
|
|
0bc59b97de | ||
|
|
a3a3bdc7eb | ||
|
|
e767f30886 | ||
|
|
e8c250a03c | ||
|
|
d6725fc1ca | ||
|
|
8ec998ff30 | ||
|
|
23cc0c7f39 | ||
|
|
19b8bd6aa8 | ||
|
|
ed57e7c6b0 | ||
|
|
9f489c9f27 | ||
|
|
f036989361 | ||
|
|
6afa8141c0 | ||
|
|
587964c6f1 | ||
|
|
7aea82a273 | ||
|
|
20f946ccaf | ||
|
|
e5e972231c | ||
|
|
bfa80157f2 | ||
|
|
99b1b079d0 | ||
|
|
5697d549a8 | ||
|
|
754d2874e7 | ||
|
|
06de58ff8b | ||
|
|
a0b3527710 | ||
|
|
df24f48fa1 | ||
|
|
13d53590b2 | ||
|
|
5857f7b9a7 | ||
|
|
a5ea0cd41f | ||
|
|
d677934417 | ||
|
|
ba87a0b63c | ||
|
|
b725bb3dd1 | ||
|
|
c34ba3deb5 | ||
|
|
68b13340fb | ||
|
|
8831999ea6 | ||
|
|
c1853f8b84 | ||
|
|
2b9b7e2853 | ||
|
|
d3b18debf9 | ||
|
|
b01eb28d42 | ||
|
|
02019dd16c | ||
|
|
7be12f5ff6 | ||
|
|
a90d59b6ba | ||
|
|
e7fa156254 | ||
|
|
a8ab6b1c43 | ||
|
|
25ed7c890b | ||
|
|
85e3b63f05 | ||
|
|
a37bac1956 | ||
|
|
818a978dfc | ||
|
|
180aeb7d8e | ||
|
|
0764fa7292 | ||
|
|
17bf533ed7 | ||
|
|
d7eae1c1a0 | ||
|
|
7f2d979255 | ||
|
|
46b419ea8b | ||
|
|
b30b527ff9 | ||
|
|
41b1bfc504 | ||
|
|
f4f14a7507 | ||
|
|
61c29213a7 | ||
|
|
e6d7639209 | ||
|
|
3c07a186b2 | ||
|
|
8a725250a9 | ||
|
|
502b8a6073 | ||
|
|
6212c6f80f | ||
|
|
b03e3b8d4a | ||
|
|
a98e34d190 | ||
|
|
bf8d8b6e63 | ||
|
|
57599f7a98 | ||
|
|
ffccce7ffc | ||
|
|
bbd5d050a9 | ||
|
|
71a96fdcbf | ||
|
|
221e3c6c9c | ||
|
|
fb1679d572 | ||
|
|
c19065f112 | ||
|
|
f2b04a077e | ||
|
|
8e7841c880 | ||
|
|
1873490b24 | ||
|
|
4d231953f4 | ||
|
|
aa4c399657 | ||
|
|
1f99d18982 | ||
|
|
be37178ef8 | ||
|
|
fad86c655e | ||
|
|
4a7958586e | ||
|
|
f44ecd0891 | ||
|
|
3d0392d668 | ||
|
|
d300d2605b | ||
|
|
66cce6a2f2 | ||
|
|
65e3c6bfbb | ||
|
|
2a39060912 | ||
|
|
8714e80978 | ||
|
|
98de53f60b | ||
|
|
41e11e9a0e | ||
|
|
e7a4eac8bd | ||
|
|
1589a131db | ||
|
|
7d84f0e650 | ||
|
|
86fb0e317f | ||
|
|
32088d5ef7 | ||
|
|
63de88dd57 | ||
|
|
153a6440dc | ||
|
|
8937ed2269 | ||
|
|
02e922b56f | ||
|
|
bf9e901ab9 | ||
|
|
1234ef8de2 | ||
|
|
41697a7b1b | ||
|
|
912e265bc0 | ||
|
|
96ee6fb064 | ||
|
|
788dba8ef3 | ||
|
|
fdde9c4681 | ||
|
|
f195e73d38 | ||
|
|
b0d9ffc6a1 | ||
|
|
e17619841d | ||
|
|
eb6a7cf3b9 | ||
|
|
9901e2d72e | ||
|
|
1be4e23b68 | ||
|
|
e78094cc0a | ||
|
|
bcf961c0b0 | ||
|
|
f84a4c9753 | ||
|
|
df56ca0236 | ||
|
|
de0cd0ec67 | ||
|
|
67c30245c4 | ||
|
|
1f72757591 | ||
|
|
35c2fdf6af | ||
|
|
d1ecd841be | ||
|
|
828a49697c | ||
|
|
0551495501 | ||
|
|
2bbffe4a68 | ||
|
|
281ad90e39 | ||
|
|
ed50976a07 | ||
|
|
a3400037d9 | ||
|
|
f0d82f75bc | ||
|
|
349cb80e90 | ||
|
|
c263ee39af | ||
|
|
e99bc52756 | ||
|
|
7944b2b8e9 | ||
|
|
ca6ae746c1 | ||
|
|
deabac18b2 | ||
|
|
5cf8681c61 | ||
|
|
ca7ede8f96 | ||
|
|
4969682d52 | ||
|
|
8002fe0dd5 | ||
|
|
7dfdf965b7 | ||
|
|
b408795dd6 | ||
|
|
a5a099336b | ||
|
|
4ae56fc004 | ||
|
|
3f71c09b7b | ||
|
|
bd50a7f1ab | ||
|
|
51e4c45e5c | ||
|
|
e3fae49add | ||
|
|
610215ab60 | ||
|
|
74acbda435 | ||
|
|
25c4af777c | ||
|
|
ec186e6324 | ||
|
|
150b7a98f3 | ||
|
|
8ae7c1cff0 | ||
|
|
7f1d0eef98 | ||
|
|
1179ab33f2 | ||
|
|
a09faa1c10 | ||
|
|
c0319d9b2f | ||
|
|
4870cd2921 | ||
|
|
d4280ec68b | ||
|
|
52cdc11927 | ||
|
|
8345b8c9ce | ||
|
|
c56f0677c3 | ||
|
|
00e9e1421e | ||
|
|
93c72c6e6c | ||
|
|
9cea930dbd | ||
|
|
7b9bd70729 | ||
|
|
5115c7a100 | ||
|
|
5634494e64 | ||
|
|
aa8bd4abf1 | ||
|
|
17fd69dd7f | ||
|
|
1d9dae374b | ||
|
|
cb2241ad91 | ||
|
|
d8a7e9abc8 | ||
|
|
969abc3f29 | ||
|
|
766fdc8a1f | ||
|
|
4c37c20d76 | ||
|
|
7d314398e1 | ||
|
|
b69191e3a8 | ||
|
|
b27c6b3596 | ||
|
|
5453835963 | ||
|
|
4d55ba057c | ||
|
|
325c01242c | ||
|
|
45b32bca89 | ||
|
|
7620049214 | ||
|
|
3553495a60 | ||
|
|
3ce6db61d5 | ||
|
|
798ff32c40 | ||
|
|
430cee8bda | ||
|
|
1fe3fb25a6 | ||
|
|
685ed87581 | ||
|
|
ea3ea1eee7 | ||
|
|
c9edcb909b | ||
|
|
35bfc9f069 | ||
|
|
c4aec194b9 | ||
|
|
e8547b16f6 | ||
|
|
2bbe08cee0 | ||
|
|
0a0c369b88 | ||
|
|
5d2f454a94 | ||
|
|
04bcc5c879 | ||
|
|
d4db16665f | ||
|
|
20b7a494f6 | ||
|
|
fbdce3ad89 | ||
|
|
4fc8807f02 | ||
|
|
83075bfb5c | ||
|
|
4074ec0425 | ||
|
|
8e1694dd0f | ||
|
|
911df18855 | ||
|
|
6b049e93f8 | ||
|
|
a335dcc379 | ||
|
|
c6478c8a79 | ||
|
|
cc9d40cb60 | ||
|
|
0a6b7f9a1b | ||
|
|
daa1fb9a7a | ||
|
|
b7d543290b | ||
|
|
ea852b60ac | ||
|
|
ed341988ea | ||
|
|
057b6c8e30 | ||
|
|
44444fe071 | ||
|
|
797330d6ab | ||
|
|
a630d5b5f5 | ||
|
|
eb3dc82b5d | ||
|
|
34ed18d562 | ||
|
|
1ce02ee313 | ||
|
|
2a26a0188c | ||
|
|
50cb05d1b1 | ||
|
|
6e739ac453 | ||
|
|
7aa2fd9f0e | ||
|
|
8e254e1b03 | ||
|
|
1ad9d717ff | ||
|
|
104658e43a | ||
|
|
e7e4b995bf | ||
|
|
b35b54f2c2 | ||
|
|
f80aeb1d1d | ||
|
|
6a756ab3b6 | ||
|
|
58a697bed1 | ||
|
|
280960ac18 | ||
|
|
0640ff13aa | ||
|
|
545505691f | ||
|
|
11fcf81321 | ||
|
|
c565b37dc8 | ||
|
|
3d18495270 | ||
|
|
419e4e63e9 | ||
|
|
724aa2bf65 | ||
|
|
573fa8aeb3 | ||
|
|
8a672e34c5 | ||
|
|
bc49211dab | ||
|
|
4ef9c3667e | ||
|
|
6babe516ac | ||
|
|
e0b258ef7e | ||
|
|
ff0c3a89b1 | ||
|
|
2511b81048 | ||
|
|
6ffcd94edc | ||
|
|
2fcf73c812 | ||
|
|
dee0608af9 | ||
|
|
d11860a383 | ||
|
|
1c05115bf5 | ||
|
|
d7e7382d0b | ||
|
|
872388f6e3 | ||
|
|
1215ef920b | ||
|
|
d19d5a23ea | ||
|
|
f49a779f1d | ||
|
|
d8bf5b80e1 | ||
|
|
69483b9353 | ||
|
|
14e8548989 | ||
|
|
4abd93b661 | ||
|
|
5d925af76f | ||
|
|
b999c6064a | ||
|
|
94e3576978 | ||
|
|
7a22406a2d | ||
|
|
e60684494f | ||
|
|
9db28ed779 | ||
|
|
6fd8c5cee7 | ||
|
|
787ec43266 | ||
|
|
a4efc63bf2 | ||
|
|
80a8f1437e | ||
|
|
fcca94169d | ||
|
|
d1924088e3 | ||
|
|
fd31afe09c | ||
|
|
7a763712c5 | ||
|
|
7216be5da7 | ||
|
|
711b0a291b | ||
|
|
dfc96496c8 | ||
|
|
2a1c5ef333 | ||
|
|
9755209499 | ||
|
|
0b26e537d4 | ||
|
|
98c6233ec3 | ||
|
|
f711706b1a | ||
|
|
cee7789ab6 | ||
|
|
8a06c4380d | ||
|
|
72ecf7a288 | ||
|
|
ef98c7502d | ||
|
|
03d0e74b65 | ||
|
|
5b8fdc0364 | ||
|
|
593b4bd137 | ||
|
|
267e12d058 | ||
|
|
4a5e39b651 | ||
|
|
ea24fa5b78 | ||
|
|
bb2bb128f7 | ||
|
|
94e8a856d7 | ||
|
|
4c19fbf98e | ||
|
|
60f8938bfa | ||
|
|
55679662b5 | ||
|
|
53df959e49 | ||
|
|
8e6ef9966f | ||
|
|
1d52fceafa | ||
|
|
99186ed864 | ||
|
|
383931d484 | ||
|
|
0b49a54cb3 | ||
|
|
705c0f1891 | ||
|
|
544c3ffc95 | ||
|
|
33f252a45d | ||
|
|
f55d82a015 | ||
|
|
8cf33fdef0 | ||
|
|
f858d98811 | ||
|
|
2a6165d440 | ||
|
|
4586528c40 | ||
|
|
23a07baa19 | ||
|
|
f9040ca932 | ||
|
|
4cea7f0237 | ||
|
|
b1847d5e98 | ||
|
|
9ce4d2e952 | ||
|
|
247078e06d | ||
|
|
a0cd72de28 | ||
|
|
e467f569f0 | ||
|
|
e31c7b7dfc | ||
|
|
dc2e0c832b | ||
|
|
7ddf51bb51 | ||
|
|
8fb3856665 | ||
|
|
183dd74f3e | ||
|
|
4f29039b41 | ||
|
|
102fcbec20 | ||
|
|
d00e5212c7 | ||
|
|
0e6bfb62cd | ||
|
|
f576e8f635 | ||
|
|
e6dc10a440 | ||
|
|
aa930fb6b6 | ||
|
|
f327ed87e9 | ||
|
|
2de9be0589 | ||
|
|
345cde8645 | ||
|
|
cf152af9ae | ||
|
|
d6333dcfd9 | ||
|
|
0121f799f0 | ||
|
|
82c39580df | ||
|
|
53a578a46f | ||
|
|
62612ef80b | ||
|
|
61ac874c4c | ||
|
|
976b200ff6 | ||
|
|
852343b6d8 | ||
|
|
c56af9d52b | ||
|
|
05f18e2828 | ||
|
|
72804caab2 | ||
|
|
80cbe5c7c9 | ||
|
|
21892d1236 | ||
|
|
13824624f8 | ||
|
|
0fd72ecbab | ||
|
|
f848cb1546 | ||
|
|
633854081a | ||
|
|
4fed9a581b | ||
|
|
e9c1202aaa | ||
|
|
0a7ae279d0 | ||
|
|
0de2696543 | ||
|
|
a7dc239b71 | ||
|
|
fe0e6990f5 | ||
|
|
5ba65e92d9 | ||
|
|
a1452b52c9 | ||
|
|
dd2aa23a5f | ||
|
|
0e0359ba7d | ||
|
|
93b1b7aded | ||
|
|
9472dc6a53 | ||
|
|
67b681854e | ||
|
|
7b5990833e | ||
|
|
b6d5d04589 | ||
|
|
fdfbb3e944 | ||
|
|
faa7a3e37f | ||
|
|
23748b82bb | ||
|
|
bccb6f578a | ||
|
|
de8a5d6e9e | ||
|
|
a8eb3f7961 | ||
|
|
2dc85f5a42 | ||
|
|
82518b351d | ||
|
|
68f34a1683 | ||
|
|
bc6b72a422 | ||
|
|
599e28e1cb | ||
|
|
ee6b2ba6c6 | ||
|
|
0877b3e2af | ||
|
|
d1edb1e32a | ||
|
|
d1e6b8dd10 | ||
|
|
b32fc3bfdd | ||
|
|
1e24417db0 | ||
|
|
fb9387ecc5 | ||
|
|
6c5f4cdb70 | ||
|
|
aabacb7454 | ||
|
|
b5da84479e | ||
|
|
88d9361050 | ||
|
|
1d90388ffc | ||
|
|
b3c43ce31f | ||
|
|
6d9d22d422 | ||
|
|
86be1f56d0 | ||
|
|
a0c81ffd7a | ||
|
|
ec1dc42e58 | ||
|
|
866eaed73d | ||
|
|
a18374e1ad | ||
|
|
f7afcb3b24 | ||
|
|
3adcae783c | ||
|
|
73b40dd2e7 | ||
|
|
1e12614f9a | ||
|
|
aeaa7c699a | ||
|
|
f1c56b7254 | ||
|
|
e72e0d0646 | ||
|
|
5719d334aa | ||
|
|
bcb6b85333 | ||
|
|
5d765413ef | ||
|
|
efb2e5e7a8 | ||
|
|
5d5e346199 | ||
|
|
08a74890da | ||
|
|
0545b9c7f2 | ||
|
|
bbf7d32676 | ||
|
|
e83f4ae974 | ||
|
|
9b0d01e03f | ||
|
|
eae0d90a1e | ||
|
|
90c09a7650 | ||
|
|
aecf080211 | ||
|
|
8517420356 | ||
|
|
376be1f009 | ||
|
|
0021e76649 | ||
|
|
d440c4bc43 | ||
|
|
50840b2105 | ||
|
|
7502c6b6c0 | ||
|
|
919c32f0cc | ||
|
|
a28c951272 | ||
|
|
13d7c5a9a9 | ||
|
|
5f1383344d | ||
|
|
48f43d3eb1 | ||
|
|
4ac2141307 | ||
|
|
719d8cac97 | ||
|
|
99cbe53a8e | ||
|
|
a36af1bfac | ||
|
|
8b6aa319bf | ||
|
|
a16d321e1a | ||
|
|
74e70278e2 | ||
|
|
1332e24a2c | ||
|
|
5ab78ec461 | ||
|
|
ce701d3c31 | ||
|
|
5fca1be44d | ||
|
|
0bd4c333bd | ||
|
|
c6ed880732 | ||
|
|
da0f3c6cce | ||
|
|
e5d12d346a | ||
|
|
478e2e726b | ||
|
|
dbdac3707b | ||
|
|
bd89a88e34 | ||
|
|
d322d83745 | ||
|
|
463a581ab9 | ||
|
|
eae4bd222a | ||
|
|
a7bb7fc14d | ||
|
|
c047aa47eb | ||
|
|
61bca56316 | ||
|
|
9a37323eb8 | ||
|
|
99a54369bf | ||
|
|
f7533dfc5c | ||
|
|
ee7d95272d | ||
|
|
2b9b1d12e6 | ||
|
|
2cbb5c7d8e | ||
|
|
9686c7babe | ||
|
|
66bd4c96c4 | ||
|
|
dc47faa4b6 | ||
|
|
55ee0b116d | ||
|
|
c6957c08bc | ||
|
|
8fe6a323d8 | ||
|
|
8e51590c32 | ||
|
|
ae066d5627 | ||
|
|
6760279916 | ||
|
|
3c208050b0 | ||
|
|
bbc7c9fb37 | ||
|
|
e1c3862586 | ||
|
|
c24b7cb7bd | ||
|
|
c91e16549d | ||
|
|
6e70aca458 | ||
|
|
d9ffd0ac8e | ||
|
|
4641f73d19 | ||
|
|
9f0051c21f | ||
|
|
0331cb09e8 | ||
|
|
2f8946f86c | ||
|
|
88a3df4008 | ||
|
|
0adf514bd6 | ||
|
|
a1b5a2abcb | ||
|
|
068c62c6fe | ||
|
|
0e9f14f969 | ||
|
|
78315fd388 | ||
|
|
0ab69002df | ||
|
|
1eec1239ec | ||
|
|
57f4067fbf | ||
|
|
f4a9221232 | ||
|
|
3d4a75148d | ||
|
|
c2c5bd844d | ||
|
|
98a2f23024 | ||
|
|
c955897d1b | ||
|
|
9624efa21e | ||
|
|
831638210d | ||
|
|
cfdb0925ce | ||
|
|
83db3eddd9 | ||
|
|
cc2c5a544e | ||
|
|
8fba8c2800 | ||
|
|
51d1da8460 | ||
|
|
2f1257056d | ||
|
|
2f8f6967bf | ||
|
|
246527e618 | ||
|
|
3857cc9c83 | ||
|
|
a59a8c563e | ||
|
|
856829bcbb | ||
|
|
dd2b931f61 | ||
|
|
39beccbbb0 | ||
|
|
ff626b428f | ||
|
|
3915e1f012 | ||
|
|
7b460b6224 | ||
|
|
8fb8e79730 | ||
|
|
79bbc475f4 | ||
|
|
cef023283b | ||
|
|
d4fda79ada | ||
|
|
ff0bdcf4cd | ||
|
|
bfbc313144 | ||
|
|
31f2376f15 | ||
|
|
f76ecb6604 | ||
|
|
298cc58433 | ||
|
|
825c0593e1 | ||
|
|
87ed1dc3e3 | ||
|
|
67e9db021c | ||
|
|
3922950951 | ||
|
|
9c4aa0ba53 | ||
|
|
f5f1651b31 | ||
|
|
32f4e4ca13 | ||
|
|
962e0c4c33 | ||
|
|
2c01bc5795 | ||
|
|
0651f7cb3c | ||
|
|
01ac59ce2a | ||
|
|
c1fd597757 | ||
|
|
e79e244eee | ||
|
|
68ecc08111 | ||
|
|
3b5fbc359f | ||
|
|
583e5ea47f | ||
|
|
7b647c3fae | ||
|
|
a8b76c617c | ||
|
|
1bd8985dff | ||
|
|
25b5a6c4ae |
39
.github/workflows/ci.yml
vendored
39
.github/workflows/ci.yml
vendored
@@ -214,51 +214,17 @@ jobs:
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
./venv/Scripts/activate
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests
|
||||
- name: Run pytest
|
||||
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
integration-tests:
|
||||
name: Run integration tests
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@v4.2.2
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@v5.6.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.3
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
- name: Create Python virtual environment
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
python --version
|
||||
pip install -r requirements.txt -r requirements_test.txt
|
||||
pip install -e .
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
|
||||
clang-format:
|
||||
name: Check clang-format
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -528,7 +494,6 @@ jobs:
|
||||
- flake8
|
||||
- pylint
|
||||
- pytest
|
||||
- integration-tests
|
||||
- pyupgrade
|
||||
- clang-tidy
|
||||
- list-components
|
||||
|
||||
@@ -170,7 +170,6 @@ esphome/components/ft5x06/* @clydebarrow
|
||||
esphome/components/ft63x6/* @gpambrozio
|
||||
esphome/components/gcja5/* @gcormier
|
||||
esphome/components/gdk101/* @Szewcson
|
||||
esphome/components/gl_r01_i2c/* @pkejval
|
||||
esphome/components/globals/* @esphome/core
|
||||
esphome/components/gp2y1010au0f/* @zry98
|
||||
esphome/components/gp8403/* @jesserockz
|
||||
@@ -255,7 +254,6 @@ esphome/components/ln882x/* @lamauny
|
||||
esphome/components/lock/* @esphome/core
|
||||
esphome/components/logger/* @esphome/core
|
||||
esphome/components/logger/select/* @clydebarrow
|
||||
esphome/components/lps22/* @nagisa
|
||||
esphome/components/ltr390/* @latonita @sjtrny
|
||||
esphome/components/ltr501/* @latonita
|
||||
esphome/components/ltr_als_ps/* @latonita
|
||||
@@ -331,6 +329,7 @@ esphome/components/opentherm/* @olegtarasov
|
||||
esphome/components/openthread/* @mrene
|
||||
esphome/components/opt3001/* @ccutrer
|
||||
esphome/components/ota/* @esphome/core
|
||||
esphome/components/ota_base/* @esphome/core
|
||||
esphome/components/output/* @esphome/core
|
||||
esphome/components/packet_transport/* @clydebarrow
|
||||
esphome/components/pca6416a/* @Mat931
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.8.0-dev
|
||||
PROJECT_NUMBER = 2025.7.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -23,7 +23,7 @@ void APDS9960::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
|
||||
if (id != 0xAB && id != 0x9C && id != 0xA8) { // APDS9960 all should have one of these IDs
|
||||
this->error_code_ = WRONG_ID;
|
||||
this->mark_failed();
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,6 @@ import base64
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.config_helpers import get_logger_level
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ACTION,
|
||||
@@ -24,7 +23,7 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from esphome.core import coroutine_with_priority
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
DEPENDENCIES = ["network"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
@@ -321,10 +320,7 @@ def FILTER_SOURCE_FILES() -> list[str]:
|
||||
# api_pb2_dump.cpp is only needed when HAS_PROTO_MESSAGE_DUMP is defined
|
||||
# This is a particularly large file that still needs to be opened and read
|
||||
# all the way to the end even when ifdef'd out
|
||||
#
|
||||
# HAS_PROTO_MESSAGE_DUMP is defined when ESPHOME_LOG_HAS_VERY_VERBOSE is set,
|
||||
# which happens when the logger level is VERY_VERBOSE
|
||||
if get_logger_level() != "VERY_VERBOSE":
|
||||
if "HAS_PROTO_MESSAGE_DUMP" not in CORE.defines:
|
||||
return ["api_pb2_dump.cpp"]
|
||||
|
||||
return []
|
||||
|
||||
@@ -45,14 +45,14 @@ static const int CAMERA_STOP_STREAM = 5000;
|
||||
// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call object
|
||||
#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
|
||||
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
|
||||
if ((entity_var) == nullptr) \
|
||||
if (entity_var == nullptr) \
|
||||
return; \
|
||||
auto call = (entity_var)->make_call();
|
||||
auto call = entity_var->make_call();
|
||||
|
||||
// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if not found
|
||||
#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
|
||||
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
|
||||
if ((entity_var) == nullptr) \
|
||||
if (entity_var == nullptr) \
|
||||
return;
|
||||
|
||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
|
||||
@@ -1176,53 +1176,66 @@ void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequ
|
||||
#endif
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
bool APIConnection::check_voice_assistant_api_connection_() const {
|
||||
return voice_assistant::global_voice_assistant != nullptr &&
|
||||
voice_assistant::global_voice_assistant->get_api_connection() == this;
|
||||
}
|
||||
|
||||
void APIConnection::subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) {
|
||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
|
||||
}
|
||||
}
|
||||
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
|
||||
if (!this->check_voice_assistant_api_connection_()) {
|
||||
return;
|
||||
}
|
||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (msg.error) {
|
||||
voice_assistant::global_voice_assistant->failed_to_start();
|
||||
return;
|
||||
}
|
||||
if (msg.port == 0) {
|
||||
// Use API Audio
|
||||
voice_assistant::global_voice_assistant->start_streaming();
|
||||
} else {
|
||||
struct sockaddr_storage storage;
|
||||
socklen_t len = sizeof(storage);
|
||||
this->helper_->getpeername((struct sockaddr *) &storage, &len);
|
||||
voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port);
|
||||
if (msg.error) {
|
||||
voice_assistant::global_voice_assistant->failed_to_start();
|
||||
return;
|
||||
}
|
||||
if (msg.port == 0) {
|
||||
// Use API Audio
|
||||
voice_assistant::global_voice_assistant->start_streaming();
|
||||
} else {
|
||||
struct sockaddr_storage storage;
|
||||
socklen_t len = sizeof(storage);
|
||||
this->helper_->getpeername((struct sockaddr *) &storage, &len);
|
||||
voice_assistant::global_voice_assistant->start_streaming(&storage, msg.port);
|
||||
}
|
||||
}
|
||||
};
|
||||
void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) {
|
||||
if (this->check_voice_assistant_api_connection_()) {
|
||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
|
||||
return;
|
||||
}
|
||||
|
||||
voice_assistant::global_voice_assistant->on_event(msg);
|
||||
}
|
||||
}
|
||||
void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) {
|
||||
if (this->check_voice_assistant_api_connection_()) {
|
||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
|
||||
return;
|
||||
}
|
||||
|
||||
voice_assistant::global_voice_assistant->on_audio(msg);
|
||||
}
|
||||
};
|
||||
void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) {
|
||||
if (this->check_voice_assistant_api_connection_()) {
|
||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
|
||||
return;
|
||||
}
|
||||
|
||||
voice_assistant::global_voice_assistant->on_timer_event(msg);
|
||||
}
|
||||
};
|
||||
|
||||
void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) {
|
||||
if (this->check_voice_assistant_api_connection_()) {
|
||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
|
||||
return;
|
||||
}
|
||||
|
||||
voice_assistant::global_voice_assistant->on_announce(msg);
|
||||
}
|
||||
}
|
||||
@@ -1230,29 +1243,35 @@ void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnno
|
||||
VoiceAssistantConfigurationResponse APIConnection::voice_assistant_get_configuration(
|
||||
const VoiceAssistantConfigurationRequest &msg) {
|
||||
VoiceAssistantConfigurationResponse resp;
|
||||
if (!this->check_voice_assistant_api_connection_()) {
|
||||
return resp;
|
||||
}
|
||||
|
||||
auto &config = voice_assistant::global_voice_assistant->get_configuration();
|
||||
for (auto &wake_word : config.available_wake_words) {
|
||||
VoiceAssistantWakeWord resp_wake_word;
|
||||
resp_wake_word.id = wake_word.id;
|
||||
resp_wake_word.wake_word = wake_word.wake_word;
|
||||
for (const auto &lang : wake_word.trained_languages) {
|
||||
resp_wake_word.trained_languages.push_back(lang);
|
||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
|
||||
return resp;
|
||||
}
|
||||
resp.available_wake_words.push_back(std::move(resp_wake_word));
|
||||
|
||||
auto &config = voice_assistant::global_voice_assistant->get_configuration();
|
||||
for (auto &wake_word : config.available_wake_words) {
|
||||
VoiceAssistantWakeWord resp_wake_word;
|
||||
resp_wake_word.id = wake_word.id;
|
||||
resp_wake_word.wake_word = wake_word.wake_word;
|
||||
for (const auto &lang : wake_word.trained_languages) {
|
||||
resp_wake_word.trained_languages.push_back(lang);
|
||||
}
|
||||
resp.available_wake_words.push_back(std::move(resp_wake_word));
|
||||
}
|
||||
for (auto &wake_word_id : config.active_wake_words) {
|
||||
resp.active_wake_words.push_back(wake_word_id);
|
||||
}
|
||||
resp.max_active_wake_words = config.max_active_wake_words;
|
||||
}
|
||||
for (auto &wake_word_id : config.active_wake_words) {
|
||||
resp.active_wake_words.push_back(wake_word_id);
|
||||
}
|
||||
resp.max_active_wake_words = config.max_active_wake_words;
|
||||
return resp;
|
||||
}
|
||||
|
||||
void APIConnection::voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
|
||||
if (this->check_voice_assistant_api_connection_()) {
|
||||
if (voice_assistant::global_voice_assistant != nullptr) {
|
||||
if (voice_assistant::global_voice_assistant->get_api_connection() != this) {
|
||||
return;
|
||||
}
|
||||
|
||||
voice_assistant::global_voice_assistant->on_set_configuration(msg.active_wake_words);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,11 +301,6 @@ class APIConnection : public APIServerConnection {
|
||||
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
// Helper to check voice assistant validity and connection ownership
|
||||
inline bool check_voice_assistant_api_connection_() const;
|
||||
#endif
|
||||
|
||||
// Helper method to process multiple entities from an iterator in a batch
|
||||
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
|
||||
@@ -263,7 +263,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
|
||||
// Macro for entities without extra parameters
|
||||
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
|
||||
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
void APIServer::on_##entity_name##_update(entity_type *obj) { \
|
||||
if (obj->is_internal()) \
|
||||
return; \
|
||||
for (auto &c : this->clients_) \
|
||||
@@ -272,7 +272,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
|
||||
// Macro for entities with extra parameters (but parameters not used in send)
|
||||
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
|
||||
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
|
||||
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { \
|
||||
if (obj->is_internal()) \
|
||||
return; \
|
||||
for (auto &c : this->clients_) \
|
||||
@@ -358,13 +358,7 @@ void APIServer::on_event(event::Event *obj, const std::string &event_type) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
// Update is a special case - the method is called on_update, not on_update_update
|
||||
void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
if (obj->is_internal())
|
||||
return;
|
||||
for (auto &c : this->clients_)
|
||||
c->send_update_state(obj);
|
||||
}
|
||||
API_DISPATCH_UPDATE(update::UpdateEntity, update)
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
|
||||
@@ -59,12 +59,10 @@ bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device)
|
||||
// This achieves ~97% WiFi MTU utilization while staying under the limit
|
||||
static constexpr size_t FLUSH_BATCH_SIZE = 16;
|
||||
|
||||
namespace {
|
||||
// Batch buffer in anonymous namespace to avoid guard variable (saves 8 bytes)
|
||||
// Global batch buffer to avoid guard variable (saves 8 bytes)
|
||||
// This is initialized at program startup before any threads
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
|
||||
} // namespace
|
||||
static std::vector<api::BluetoothLERawAdvertisement> batch_buffer;
|
||||
|
||||
static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { return batch_buffer; }
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONF_BYTE_ORDER = "byte_order"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ON_STATE_CHANGE = "on_state_change"
|
||||
CONF_REQUEST_HEADERS = "request_headers"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32, time
|
||||
from esphome.components import time
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
@@ -116,20 +116,12 @@ def validate_pin_number(value):
|
||||
return value
|
||||
|
||||
|
||||
def _validate_ex1_wakeup_mode(value):
|
||||
if value == "ALL_LOW":
|
||||
esp32.only_on_variant(supported=[VARIANT_ESP32], msg_prefix="ALL_LOW")(value)
|
||||
if value == "ANY_LOW":
|
||||
esp32.only_on_variant(
|
||||
supported=[
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32H2,
|
||||
],
|
||||
msg_prefix="ANY_LOW",
|
||||
)(value)
|
||||
return value
|
||||
def validate_config(config):
|
||||
if get_esp32_variant() == VARIANT_ESP32C3 and CONF_ESP32_EXT1_WAKEUP in config:
|
||||
raise cv.Invalid("ESP32-C3 does not support wakeup from touch.")
|
||||
if get_esp32_variant() == VARIANT_ESP32C3 and CONF_TOUCH_WAKEUP in config:
|
||||
raise cv.Invalid("ESP32-C3 does not support wakeup from ext1")
|
||||
return config
|
||||
|
||||
|
||||
deep_sleep_ns = cg.esphome_ns.namespace("deep_sleep")
|
||||
@@ -156,7 +148,6 @@ WAKEUP_PIN_MODES = {
|
||||
esp_sleep_ext1_wakeup_mode_t = cg.global_ns.enum("esp_sleep_ext1_wakeup_mode_t")
|
||||
Ext1Wakeup = deep_sleep_ns.struct("Ext1Wakeup")
|
||||
EXT1_WAKEUP_MODES = {
|
||||
"ANY_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_LOW,
|
||||
"ALL_LOW": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ALL_LOW,
|
||||
"ANY_HIGH": esp_sleep_ext1_wakeup_mode_t.ESP_EXT1_WAKEUP_ANY_HIGH,
|
||||
}
|
||||
@@ -196,28 +187,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
|
||||
cv.only_on_esp32,
|
||||
esp32.only_on_variant(
|
||||
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from ext1"
|
||||
),
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PINS): cv.ensure_list(
|
||||
pins.internal_gpio_input_pin_schema, validate_pin_number
|
||||
),
|
||||
cv.Required(CONF_MODE): cv.All(
|
||||
cv.enum(EXT1_WAKEUP_MODES, upper=True),
|
||||
_validate_ex1_wakeup_mode,
|
||||
),
|
||||
cv.Required(CONF_MODE): cv.enum(EXT1_WAKEUP_MODES, upper=True),
|
||||
}
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_TOUCH_WAKEUP): cv.All(
|
||||
cv.only_on_esp32,
|
||||
esp32.only_on_variant(
|
||||
unsupported=[VARIANT_ESP32C3], msg_prefix="Wakeup from touch"
|
||||
),
|
||||
cv.boolean,
|
||||
),
|
||||
cv.Optional(CONF_TOUCH_WAKEUP): cv.All(cv.only_on_esp32, cv.boolean),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266]),
|
||||
|
||||
@@ -189,7 +189,7 @@ def get_download_types(storage_json):
|
||||
]
|
||||
|
||||
|
||||
def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This feature"):
|
||||
def only_on_variant(*, supported=None, unsupported=None):
|
||||
"""Config validator for features only available on some ESP32 variants."""
|
||||
if supported is not None and not isinstance(supported, list):
|
||||
supported = [supported]
|
||||
@@ -200,11 +200,11 @@ def only_on_variant(*, supported=None, unsupported=None, msg_prefix="This featur
|
||||
variant = get_esp32_variant()
|
||||
if supported is not None and variant not in supported:
|
||||
raise cv.Invalid(
|
||||
f"{msg_prefix} is only available on {', '.join(supported)}"
|
||||
f"This feature is only available on {', '.join(supported)}"
|
||||
)
|
||||
if unsupported is not None and variant in unsupported:
|
||||
raise cv.Invalid(
|
||||
f"{msg_prefix} is not available on {', '.join(unsupported)}"
|
||||
f"This feature is not available on {', '.join(unsupported)}"
|
||||
)
|
||||
return obj
|
||||
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "gl_r01_i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace gl_r01_i2c {
|
||||
|
||||
static const char *const TAG = "gl_r01_i2c";
|
||||
|
||||
// Register definitions from datasheet
|
||||
static const uint8_t REG_VERSION = 0x00;
|
||||
static const uint8_t REG_DISTANCE = 0x02;
|
||||
static const uint8_t REG_TRIGGER = 0x10;
|
||||
static const uint8_t CMD_TRIGGER = 0xB0;
|
||||
static const uint8_t RESTART_CMD1 = 0x5A;
|
||||
static const uint8_t RESTART_CMD2 = 0xA5;
|
||||
static const uint8_t READ_DELAY = 40; // minimum milliseconds from datasheet to safely read measurement result
|
||||
|
||||
void GLR01I2CComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up GL-R01 I2C...");
|
||||
// Verify sensor presence
|
||||
if (!this->read_byte_16(REG_VERSION, &this->version_)) {
|
||||
ESP_LOGE(TAG, "Failed to communicate with GL-R01 I2C sensor!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Found GL-R01 I2C with version 0x%04X", this->version_);
|
||||
}
|
||||
|
||||
void GLR01I2CComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "GL-R01 I2C:");
|
||||
ESP_LOGCONFIG(TAG, " Firmware Version: 0x%04X", this->version_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_SENSOR(" ", "Distance", this);
|
||||
}
|
||||
|
||||
void GLR01I2CComponent::update() {
|
||||
// Trigger a new measurement
|
||||
if (!this->write_byte(REG_TRIGGER, CMD_TRIGGER)) {
|
||||
ESP_LOGE(TAG, "Failed to trigger measurement!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule reading the result after the read delay
|
||||
this->set_timeout(READ_DELAY, [this]() { this->read_distance_(); });
|
||||
}
|
||||
|
||||
void GLR01I2CComponent::read_distance_() {
|
||||
uint16_t distance = 0;
|
||||
if (!this->read_byte_16(REG_DISTANCE, &distance)) {
|
||||
ESP_LOGE(TAG, "Failed to read distance value!");
|
||||
this->status_set_warning();
|
||||
return;
|
||||
}
|
||||
|
||||
if (distance == 0xFFFF) {
|
||||
ESP_LOGW(TAG, "Invalid measurement received!");
|
||||
this->status_set_warning();
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Distance: %umm", distance);
|
||||
this->publish_state(distance);
|
||||
this->status_clear_warning();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gl_r01_i2c
|
||||
} // namespace esphome
|
||||
@@ -1,22 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace gl_r01_i2c {
|
||||
|
||||
class GLR01I2CComponent : public sensor::Sensor, public i2c::I2CDevice, public PollingComponent {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void update() override;
|
||||
|
||||
protected:
|
||||
void read_distance_();
|
||||
uint16_t version_{0};
|
||||
};
|
||||
|
||||
} // namespace gl_r01_i2c
|
||||
} // namespace esphome
|
||||
@@ -1,36 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_MILLIMETER,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@pkejval"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
gl_r01_i2c_ns = cg.esphome_ns.namespace("gl_r01_i2c")
|
||||
GLR01I2CComponent = gl_r01_i2c_ns.class_(
|
||||
"GLR01I2CComponent", i2c.I2CDevice, cg.PollingComponent
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
GLR01I2CComponent,
|
||||
unit_of_measurement=UNIT_MILLIMETER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x74))
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await sensor.register_sensor(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
@@ -111,8 +111,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_MOISTURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_INTENSITY,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PRECIPITATION_INTENSITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon="mdi:weather-rainy",
|
||||
),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
|
||||
@@ -10,10 +10,8 @@ from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from esphome import core, external_files
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_BYTE_ORDER
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DEFAULTS,
|
||||
CONF_DITHER,
|
||||
CONF_FILE,
|
||||
CONF_ICON,
|
||||
@@ -40,7 +38,6 @@ CONF_OPAQUE = "opaque"
|
||||
CONF_CHROMA_KEY = "chroma_key"
|
||||
CONF_ALPHA_CHANNEL = "alpha_channel"
|
||||
CONF_INVERT_ALPHA = "invert_alpha"
|
||||
CONF_IMAGES = "images"
|
||||
|
||||
TRANSPARENCY_TYPES = (
|
||||
CONF_OPAQUE,
|
||||
@@ -191,10 +188,6 @@ class ImageRGB565(ImageEncoder):
|
||||
dither,
|
||||
invert_alpha,
|
||||
)
|
||||
self.big_endian = True
|
||||
|
||||
def set_big_endian(self, big_endian: bool) -> None:
|
||||
self.big_endian = big_endian
|
||||
|
||||
def convert(self, image, path):
|
||||
return image.convert("RGBA")
|
||||
@@ -212,16 +205,10 @@ class ImageRGB565(ImageEncoder):
|
||||
g = 1
|
||||
b = 0
|
||||
rgb = (r << 11) | (g << 5) | b
|
||||
if self.big_endian:
|
||||
self.data[self.index] = rgb >> 8
|
||||
self.index += 1
|
||||
self.data[self.index] = rgb & 0xFF
|
||||
self.index += 1
|
||||
else:
|
||||
self.data[self.index] = rgb & 0xFF
|
||||
self.index += 1
|
||||
self.data[self.index] = rgb >> 8
|
||||
self.index += 1
|
||||
self.data[self.index] = rgb >> 8
|
||||
self.index += 1
|
||||
self.data[self.index] = rgb & 0xFF
|
||||
self.index += 1
|
||||
if self.transparency == CONF_ALPHA_CHANNEL:
|
||||
if self.invert_alpha:
|
||||
a ^= 0xFF
|
||||
@@ -377,7 +364,7 @@ def validate_file_shorthand(value):
|
||||
value = cv.string_strict(value)
|
||||
parts = value.strip().split(":")
|
||||
if len(parts) == 2 and parts[0] in MDI_SOURCES:
|
||||
match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
|
||||
match = re.match(r"[a-zA-Z0-9\-]+", parts[1])
|
||||
if match is None:
|
||||
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
|
||||
return download_gh_svg(parts[1], parts[0])
|
||||
@@ -447,29 +434,20 @@ def validate_type(image_types):
|
||||
|
||||
|
||||
def validate_settings(value):
|
||||
"""
|
||||
Validate the settings for a single image configuration.
|
||||
"""
|
||||
conf_type = value[CONF_TYPE]
|
||||
type_class = IMAGE_TYPE[conf_type]
|
||||
type = value[CONF_TYPE]
|
||||
transparency = value[CONF_TRANSPARENCY].lower()
|
||||
if transparency not in type_class.allow_config:
|
||||
allow_config = IMAGE_TYPE[type].allow_config
|
||||
if transparency not in allow_config:
|
||||
raise cv.Invalid(
|
||||
f"Image format '{conf_type}' cannot have transparency: {transparency}"
|
||||
f"Image format '{type}' cannot have transparency: {transparency}"
|
||||
)
|
||||
invert_alpha = value.get(CONF_INVERT_ALPHA, False)
|
||||
if (
|
||||
invert_alpha
|
||||
and transparency != CONF_ALPHA_CHANNEL
|
||||
and CONF_INVERT_ALPHA not in type_class.allow_config
|
||||
and CONF_INVERT_ALPHA not in allow_config
|
||||
):
|
||||
raise cv.Invalid("No alpha channel to invert")
|
||||
if value.get(CONF_BYTE_ORDER) is not None and not callable(
|
||||
getattr(type_class, "set_big_endian", None)
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Image format '{conf_type}' does not support byte order configuration"
|
||||
)
|
||||
if file := value.get(CONF_FILE):
|
||||
file = Path(file)
|
||||
if is_svg_file(file):
|
||||
@@ -478,82 +456,31 @@ def validate_settings(value):
|
||||
try:
|
||||
Image.open(file)
|
||||
except UnidentifiedImageError as exc:
|
||||
raise cv.Invalid(
|
||||
f"File can't be opened as image: {file.absolute()}"
|
||||
) from exc
|
||||
raise cv.Invalid(f"File can't be opened as image: {file}") from exc
|
||||
return value
|
||||
|
||||
|
||||
IMAGE_ID_SCHEMA = {
|
||||
cv.Required(CONF_ID): cv.declare_id(Image_),
|
||||
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
}
|
||||
|
||||
|
||||
OPTIONS_SCHEMA = {
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
||||
"NONE", "FLOYDSTEINBERG", upper=True
|
||||
),
|
||||
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
||||
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
|
||||
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
||||
cv.Optional(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||
}
|
||||
|
||||
OPTIONS = [key.schema for key in OPTIONS_SCHEMA]
|
||||
|
||||
# image schema with no defaults, used with `CONF_IMAGES` in the config
|
||||
IMAGE_SCHEMA_NO_DEFAULTS = {
|
||||
**IMAGE_ID_SCHEMA,
|
||||
**{cv.Optional(key): OPTIONS_SCHEMA[key] for key in OPTIONS},
|
||||
}
|
||||
|
||||
BASE_SCHEMA = cv.Schema(
|
||||
{
|
||||
**IMAGE_ID_SCHEMA,
|
||||
**OPTIONS_SCHEMA,
|
||||
cv.Required(CONF_ID): cv.declare_id(Image_),
|
||||
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
||||
"NONE", "FLOYDSTEINBERG", upper=True
|
||||
),
|
||||
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
}
|
||||
).add_extra(validate_settings)
|
||||
|
||||
IMAGE_SCHEMA = BASE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def validate_defaults(value):
|
||||
"""
|
||||
Validate the options for images with defaults
|
||||
"""
|
||||
defaults = value[CONF_DEFAULTS]
|
||||
result = []
|
||||
for index, image in enumerate(value[CONF_IMAGES]):
|
||||
type = image.get(CONF_TYPE, defaults.get(CONF_TYPE))
|
||||
if type is None:
|
||||
raise cv.Invalid(
|
||||
"Type is required either in the image config or in the defaults",
|
||||
path=[CONF_IMAGES, index],
|
||||
)
|
||||
type_class = IMAGE_TYPE[type]
|
||||
# A default byte order should be simply ignored if the type does not support it
|
||||
available_options = [*OPTIONS]
|
||||
if (
|
||||
not callable(getattr(type_class, "set_big_endian", None))
|
||||
and CONF_BYTE_ORDER not in image
|
||||
):
|
||||
available_options.remove(CONF_BYTE_ORDER)
|
||||
config = {
|
||||
**{key: image.get(key, defaults.get(key)) for key in available_options},
|
||||
**{key.schema: image[key.schema] for key in IMAGE_ID_SCHEMA},
|
||||
}
|
||||
validate_settings(config)
|
||||
result.append(config)
|
||||
return result
|
||||
|
||||
|
||||
def typed_image_schema(image_type):
|
||||
"""
|
||||
Construct a schema for a specific image type, allowing transparency options
|
||||
@@ -596,33 +523,10 @@ def typed_image_schema(image_type):
|
||||
|
||||
# The config schema can be a (possibly empty) single list of images,
|
||||
# or a dictionary of image types each with a list of images
|
||||
# or a dictionary with keys `defaults:` and `images:`
|
||||
|
||||
|
||||
def _config_schema(config):
|
||||
if isinstance(config, list):
|
||||
return cv.Schema([IMAGE_SCHEMA])(config)
|
||||
if not isinstance(config, dict):
|
||||
raise cv.Invalid(
|
||||
"Badly formed image configuration, expected a list or a dictionary"
|
||||
)
|
||||
if CONF_DEFAULTS in config or CONF_IMAGES in config:
|
||||
return validate_defaults(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_DEFAULTS): OPTIONS_SCHEMA,
|
||||
cv.Required(CONF_IMAGES): cv.ensure_list(IMAGE_SCHEMA_NO_DEFAULTS),
|
||||
}
|
||||
)(config)
|
||||
)
|
||||
if CONF_ID in config or CONF_FILE in config:
|
||||
return cv.ensure_list(IMAGE_SCHEMA)([config])
|
||||
return cv.Schema(
|
||||
{cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}
|
||||
)(config)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = _config_schema
|
||||
CONFIG_SCHEMA = cv.Any(
|
||||
cv.Schema({cv.Optional(t.lower()): typed_image_schema(t) for t in IMAGE_TYPE}),
|
||||
cv.ensure_list(IMAGE_SCHEMA),
|
||||
)
|
||||
|
||||
|
||||
async def write_image(config, all_frames=False):
|
||||
@@ -681,9 +585,6 @@ async def write_image(config, all_frames=False):
|
||||
|
||||
total_rows = height * frame_count
|
||||
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
|
||||
if byte_order := config.get(CONF_BYTE_ORDER):
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
for frame_index in range(frame_count):
|
||||
image.seek(frame_index)
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
|
||||
@@ -90,25 +90,6 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// Implementation for ESP8266 with flash string support.
|
||||
// Note: USE_STORE_LOG_STR_IN_FLASH is only defined for ESP8266.
|
||||
//
|
||||
// This function handles format strings stored in flash memory (PROGMEM) to save RAM.
|
||||
// The buffer is used in a special way to avoid allocating extra memory:
|
||||
//
|
||||
// Memory layout during execution:
|
||||
// Step 1: Copy format string from flash to buffer
|
||||
// tx_buffer_: [format_string][null][.....................]
|
||||
// tx_buffer_at_: ------------------^
|
||||
// msg_start: saved here -----------^
|
||||
//
|
||||
// Step 2: format_log_to_buffer_with_terminator_ reads format string from beginning
|
||||
// and writes formatted output starting at msg_start position
|
||||
// tx_buffer_: [format_string][null][formatted_message][null]
|
||||
// tx_buffer_at_: -------------------------------------^
|
||||
//
|
||||
// Step 3: Output the formatted message (starting at msg_start)
|
||||
// write_msg_ and callbacks receive: this->tx_buffer_ + msg_start
|
||||
// which points to: [formatted_message][null]
|
||||
//
|
||||
void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format,
|
||||
va_list args) { // NOLINT
|
||||
if (level > this->level_for(tag) || global_recursion_guard_)
|
||||
@@ -140,8 +121,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
|
||||
if (this->baud_rate_ > 0) {
|
||||
this->write_msg_(this->tx_buffer_ + msg_start);
|
||||
}
|
||||
size_t msg_length =
|
||||
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
|
||||
size_t msg_length = this->tx_buffer_at_ - msg_start - 1; // -1 to exclude null terminator
|
||||
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
|
||||
|
||||
global_recursion_guard_ = false;
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
#include "lps22.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace lps22 {
|
||||
|
||||
static constexpr const char *const TAG = "lps22";
|
||||
|
||||
static constexpr uint8_t WHO_AM_I = 0x0F;
|
||||
static constexpr uint8_t LPS22HB_ID = 0xB1;
|
||||
static constexpr uint8_t LPS22HH_ID = 0xB3;
|
||||
static constexpr uint8_t CTRL_REG2 = 0x11;
|
||||
static constexpr uint8_t CTRL_REG2_ONE_SHOT_MASK = 0b1;
|
||||
static constexpr uint8_t STATUS = 0x27;
|
||||
static constexpr uint8_t STATUS_T_DA_MASK = 0b10;
|
||||
static constexpr uint8_t STATUS_P_DA_MASK = 0b01;
|
||||
static constexpr uint8_t TEMP_L = 0x2b;
|
||||
static constexpr uint8_t PRES_OUT_XL = 0x28;
|
||||
static constexpr uint8_t REF_P_XL = 0x28;
|
||||
static constexpr uint8_t READ_ATTEMPTS = 10;
|
||||
static constexpr uint8_t READ_INTERVAL = 5;
|
||||
static constexpr float PRESSURE_SCALE = 1.0f / 4096.0f;
|
||||
static constexpr float TEMPERATURE_SCALE = 0.01f;
|
||||
|
||||
void LPS22Component::setup() {
|
||||
uint8_t value = 0x00;
|
||||
this->read_register(WHO_AM_I, &value, 1);
|
||||
if (value != LPS22HB_ID && value != LPS22HH_ID) {
|
||||
ESP_LOGW(TAG, "device IDs as %02x, which isn't a known LPS22HB or LPS22HH ID", value);
|
||||
this->mark_failed();
|
||||
}
|
||||
}
|
||||
|
||||
void LPS22Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "LPS22:");
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
LOG_SENSOR(" ", "Pressure", this->pressure_sensor_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
void LPS22Component::update() {
|
||||
uint8_t value = 0x00;
|
||||
this->read_register(CTRL_REG2, &value, 1);
|
||||
value |= CTRL_REG2_ONE_SHOT_MASK;
|
||||
this->write_register(CTRL_REG2, &value, 1);
|
||||
this->set_retry(READ_INTERVAL, READ_ATTEMPTS, [this](uint8_t _) { return this->try_read_(); });
|
||||
}
|
||||
|
||||
RetryResult LPS22Component::try_read_() {
|
||||
uint8_t value = 0x00;
|
||||
this->read_register(STATUS, &value, 1);
|
||||
const uint8_t expected_status_mask = STATUS_T_DA_MASK | STATUS_P_DA_MASK;
|
||||
if ((value & expected_status_mask) != expected_status_mask) {
|
||||
ESP_LOGD(TAG, "STATUS not ready: %x", value);
|
||||
return RetryResult::RETRY;
|
||||
}
|
||||
|
||||
if (this->temperature_sensor_ != nullptr) {
|
||||
uint8_t t_buf[2]{0};
|
||||
this->read_register(TEMP_L, t_buf, 2);
|
||||
int16_t encoded = static_cast<int16_t>(encode_uint16(t_buf[1], t_buf[0]));
|
||||
float temp = TEMPERATURE_SCALE * static_cast<float>(encoded);
|
||||
this->temperature_sensor_->publish_state(temp);
|
||||
}
|
||||
if (this->pressure_sensor_ != nullptr) {
|
||||
uint8_t p_buf[3]{0};
|
||||
this->read_register(PRES_OUT_XL, p_buf, 3);
|
||||
uint32_t p_lsb = encode_uint24(p_buf[2], p_buf[1], p_buf[0]);
|
||||
this->pressure_sensor_->publish_state(PRESSURE_SCALE * static_cast<float>(p_lsb));
|
||||
}
|
||||
return RetryResult::DONE;
|
||||
}
|
||||
|
||||
} // namespace lps22
|
||||
} // namespace esphome
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace lps22 {
|
||||
|
||||
class LPS22Component : public sensor::Sensor, public PollingComponent, public i2c::I2CDevice {
|
||||
public:
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||
void set_pressure_sensor(sensor::Sensor *pressure_sensor) { this->pressure_sensor_ = pressure_sensor; }
|
||||
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *pressure_sensor_{nullptr};
|
||||
|
||||
RetryResult try_read_();
|
||||
};
|
||||
|
||||
} // namespace lps22
|
||||
} // namespace esphome
|
||||
@@ -1,58 +0,0 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.components import i2c, sensor
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_TEMPERATURE,
|
||||
CONF_PRESSURE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_HECTOPASCAL,
|
||||
ICON_THERMOMETER,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_PRESSURE,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@nagisa"]
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
lps22 = cg.esphome_ns.namespace("lps22")
|
||||
|
||||
LPS22Component = lps22.class_("LPS22Component", cg.PollingComponent, i2c.I2CDevice)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(LPS22Component),
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
icon=ICON_THERMOMETER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_HECTOPASCAL,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_PRESSURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.polling_component_schema("60s"))
|
||||
.extend(i2c.i2c_device_schema(0x5D)) # can also be 0x5C
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if temperature_config := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temperature_config)
|
||||
cg.add(var.set_temperature_sensor(sens))
|
||||
|
||||
if pressure_config := config.get(CONF_PRESSURE):
|
||||
sens = await sensor.new_sensor(pressure_config)
|
||||
cg.add(var.set_pressure_sensor(sens))
|
||||
@@ -11,7 +11,6 @@ CONF_AUTO_WAKE_ON_TOUCH = "auto_wake_on_touch"
|
||||
CONF_BACKGROUND_PRESSED_COLOR = "background_pressed_color"
|
||||
CONF_COMMAND_SPACING = "command_spacing"
|
||||
CONF_COMPONENT_NAME = "component_name"
|
||||
CONF_DUMP_DEVICE_INFO = "dump_device_info"
|
||||
CONF_EXIT_REPARSE_ON_START = "exit_reparse_on_start"
|
||||
CONF_FONT_ID = "font_id"
|
||||
CONF_FOREGROUND_PRESSED_COLOR = "foreground_pressed_color"
|
||||
|
||||
@@ -44,7 +44,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti
|
||||
return;
|
||||
|
||||
if (send_to_nextion) {
|
||||
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
|
||||
if (this->nextion_->is_sleeping() || !this->visible_) {
|
||||
this->needs_to_send_update_ = true;
|
||||
} else {
|
||||
this->needs_to_send_update_ = false;
|
||||
|
||||
@@ -15,7 +15,6 @@ from . import Nextion, nextion_ns, nextion_ref
|
||||
from .base_component import (
|
||||
CONF_AUTO_WAKE_ON_TOUCH,
|
||||
CONF_COMMAND_SPACING,
|
||||
CONF_DUMP_DEVICE_INFO,
|
||||
CONF_EXIT_REPARSE_ON_START,
|
||||
CONF_MAX_COMMANDS_PER_LOOP,
|
||||
CONF_MAX_QUEUE_SIZE,
|
||||
@@ -58,7 +57,6 @@ CONFIG_SCHEMA = (
|
||||
cv.positive_time_period_milliseconds,
|
||||
cv.Range(max=TimePeriod(milliseconds=255)),
|
||||
),
|
||||
cv.Optional(CONF_DUMP_DEVICE_INFO, default=False): cv.boolean,
|
||||
cv.Optional(CONF_EXIT_REPARSE_ON_START, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MAX_COMMANDS_PER_LOOP): cv.uint16_t,
|
||||
cv.Optional(CONF_MAX_QUEUE_SIZE): cv.positive_int,
|
||||
@@ -97,9 +95,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_SKIP_CONNECTION_HANDSHAKE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_START_UP_PAGE): cv.uint8_t,
|
||||
cv.Optional(CONF_TFT_URL): cv.url,
|
||||
cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.Any(
|
||||
0, cv.int_range(min=3, max=65535)
|
||||
),
|
||||
cv.Optional(CONF_TOUCH_SLEEP_TIMEOUT): cv.int_range(min=3, max=65535),
|
||||
cv.Optional(CONF_WAKE_UP_PAGE): cv.uint8_t,
|
||||
}
|
||||
)
|
||||
@@ -176,14 +172,9 @@ async def to_code(config):
|
||||
|
||||
cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH]))
|
||||
|
||||
if config[CONF_DUMP_DEVICE_INFO]:
|
||||
cg.add_define("USE_NEXTION_CONFIG_DUMP_DEVICE_INFO")
|
||||
cg.add(var.set_exit_reparse_on_start(config[CONF_EXIT_REPARSE_ON_START]))
|
||||
|
||||
if config[CONF_EXIT_REPARSE_ON_START]:
|
||||
cg.add_define("USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START")
|
||||
|
||||
if config[CONF_SKIP_CONNECTION_HANDSHAKE]:
|
||||
cg.add_define("USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE")
|
||||
cg.add(var.set_skip_connection_handshake(config[CONF_SKIP_CONNECTION_HANDSHAKE]))
|
||||
|
||||
if max_commands_per_loop := config.get(CONF_MAX_COMMANDS_PER_LOOP):
|
||||
cg.add_define("USE_NEXTION_MAX_COMMANDS_PER_LOOP")
|
||||
|
||||
@@ -13,11 +13,14 @@ void Nextion::setup() {
|
||||
this->is_setup_ = false;
|
||||
this->connection_state_.ignore_is_setup_ = true;
|
||||
|
||||
// Wake up the nextion and ensure clean communication state
|
||||
this->send_command_("sleep=0"); // Exit sleep mode if sleeping
|
||||
this->send_command_("bkcmd=0"); // Disable return data during init sequence
|
||||
// Wake up the nextion
|
||||
this->send_command_("bkcmd=0");
|
||||
this->send_command_("sleep=0");
|
||||
|
||||
// Reset device for clean state - critical for reliable communication
|
||||
this->send_command_("bkcmd=0");
|
||||
this->send_command_("sleep=0");
|
||||
|
||||
// Reboot it
|
||||
this->send_command_("rest");
|
||||
|
||||
this->connection_state_.ignore_is_setup_ = false;
|
||||
@@ -48,19 +51,24 @@ bool Nextion::check_connect_() {
|
||||
if (this->connection_state_.is_connected_)
|
||||
return true;
|
||||
|
||||
#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
|
||||
ESP_LOGW(TAG, "Connected (no handshake)"); // Log the connection status without handshake
|
||||
this->is_connected_ = true; // Set the connection status to true
|
||||
return true; // Return true indicating the connection is set
|
||||
#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
|
||||
// Check if the handshake should be skipped for the Nextion connection
|
||||
if (this->skip_connection_handshake_) {
|
||||
// Log the connection status without handshake
|
||||
ESP_LOGW(TAG, "Connected (no handshake)");
|
||||
// Set the connection status to true
|
||||
this->connection_state_.is_connected_ = true;
|
||||
// Return true indicating the connection is set
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this->comok_sent_ == 0) {
|
||||
this->reset_(false);
|
||||
|
||||
this->connection_state_.ignore_is_setup_ = true;
|
||||
this->send_command_("boguscommand=0"); // bogus command. needed sometimes after updating
|
||||
#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
|
||||
this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN");
|
||||
#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
|
||||
if (this->exit_reparse_on_start_) {
|
||||
this->send_command_("DRAKJHSUYDGBNCJHGJKSHBDN");
|
||||
}
|
||||
this->send_command_("connect");
|
||||
|
||||
this->comok_sent_ = App.get_loop_component_start_time();
|
||||
@@ -86,7 +94,7 @@ bool Nextion::check_connect_() {
|
||||
for (size_t i = 0; i < response.length(); i++) {
|
||||
ESP_LOGN(TAG, "resp: %s %d %d %c", response.c_str(), i, response[i], response[i]);
|
||||
}
|
||||
#endif // NEXTION_PROTOCOL_LOG
|
||||
#endif
|
||||
|
||||
ESP_LOGW(TAG, "Not connected");
|
||||
comok_sent_ = 0;
|
||||
@@ -110,19 +118,11 @@ bool Nextion::check_connect_() {
|
||||
this->is_detected_ = (connect_info.size() == 7);
|
||||
if (this->is_detected_) {
|
||||
ESP_LOGN(TAG, "Connect info: %zu", connect_info.size());
|
||||
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
|
||||
this->device_model_ = connect_info[2];
|
||||
this->firmware_version_ = connect_info[3];
|
||||
this->serial_number_ = connect_info[5];
|
||||
this->flash_size_ = connect_info[6];
|
||||
#else // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
ESP_LOGI(TAG,
|
||||
" Device Model: %s\n"
|
||||
" FW Version: %s\n"
|
||||
" Serial Number: %s\n"
|
||||
" Flash Size: %s\n",
|
||||
connect_info[2].c_str(), connect_info[3].c_str(), connect_info[5].c_str(), connect_info[6].c_str());
|
||||
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Bad connect value: '%s'", response.c_str());
|
||||
}
|
||||
@@ -130,7 +130,6 @@ bool Nextion::check_connect_() {
|
||||
this->connection_state_.ignore_is_setup_ = false;
|
||||
this->dump_config();
|
||||
return true;
|
||||
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
|
||||
}
|
||||
|
||||
void Nextion::reset_(bool reset_nextion) {
|
||||
@@ -145,33 +144,29 @@ void Nextion::reset_(bool reset_nextion) {
|
||||
|
||||
void Nextion::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Nextion:");
|
||||
|
||||
#ifdef USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
|
||||
ESP_LOGCONFIG(TAG, " Skip handshake: YES");
|
||||
#else // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
|
||||
if (this->skip_connection_handshake_) {
|
||||
ESP_LOGCONFIG(TAG, " Skip handshake: %s", YESNO(this->skip_connection_handshake_));
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Device Model: %s\n"
|
||||
" FW Version: %s\n"
|
||||
" Serial Number: %s\n"
|
||||
" Flash Size: %s",
|
||||
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
|
||||
this->flash_size_.c_str());
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
" Device Model: %s\n"
|
||||
" FW Version: %s\n"
|
||||
" Serial Number: %s\n"
|
||||
" Flash Size: %s\n"
|
||||
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
#ifdef USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
|
||||
" Exit reparse: YES\n"
|
||||
#endif // USE_NEXTION_CONFIG_EXIT_REPARSE_ON_START
|
||||
" Wake On Touch: %s\n"
|
||||
" Touch Timeout: %" PRIu16,
|
||||
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
this->device_model_.c_str(), this->firmware_version_.c_str(), this->serial_number_.c_str(),
|
||||
this->flash_size_.c_str(),
|
||||
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
YESNO(this->connection_state_.auto_wake_on_touch_), this->touch_sleep_timeout_);
|
||||
#endif // USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE
|
||||
|
||||
" Exit reparse: %s",
|
||||
YESNO(this->connection_state_.auto_wake_on_touch_), YESNO(this->exit_reparse_on_start_));
|
||||
#ifdef USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
ESP_LOGCONFIG(TAG, " Max commands per loop: %u", this->max_commands_per_loop_);
|
||||
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
|
||||
|
||||
if (this->touch_sleep_timeout_ != 0) {
|
||||
ESP_LOGCONFIG(TAG, " Touch Timeout: %" PRIu16, this->touch_sleep_timeout_);
|
||||
}
|
||||
|
||||
if (this->wake_up_page_ != 255) {
|
||||
ESP_LOGCONFIG(TAG, " Wake Up Page: %u", this->wake_up_page_);
|
||||
}
|
||||
@@ -319,10 +314,6 @@ void Nextion::loop() {
|
||||
this->set_wake_up_page(this->wake_up_page_);
|
||||
}
|
||||
|
||||
if (this->touch_sleep_timeout_ != 0) {
|
||||
this->set_touch_sleep_timeout(this->touch_sleep_timeout_);
|
||||
}
|
||||
|
||||
this->connection_state_.ignore_is_setup_ = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -932,6 +932,21 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
*/
|
||||
void set_backlight_brightness(float brightness);
|
||||
|
||||
/**
|
||||
* Sets whether the Nextion display should skip the connection handshake process.
|
||||
* @param skip_handshake True or false. When skip_connection_handshake is true,
|
||||
* the connection will be established without performing the handshake.
|
||||
* This can be useful when using Nextion Simulator.
|
||||
*
|
||||
* Example:
|
||||
* ```cpp
|
||||
* it.set_skip_connection_handshake(true);
|
||||
* ```
|
||||
*
|
||||
* When set to true, the display will be marked as connected without performing a handshake.
|
||||
*/
|
||||
void set_skip_connection_handshake(bool skip_handshake) { this->skip_connection_handshake_ = skip_handshake; }
|
||||
|
||||
/**
|
||||
* Sets Nextion mode between sleep and awake
|
||||
* @param True or false. Sleep=true to enter sleep mode or sleep=false to exit sleep mode.
|
||||
@@ -1164,39 +1179,18 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
void update_components_by_prefix(const std::string &prefix);
|
||||
|
||||
/**
|
||||
* Set the touch sleep timeout of the display using the `thsp` command.
|
||||
*
|
||||
* Sets internal No-touch-then-sleep timer to specified value in seconds.
|
||||
* Nextion will auto-enter sleep mode if and when this timer expires.
|
||||
*
|
||||
* @param touch_sleep_timeout Timeout in seconds.
|
||||
* Range: 3 to 65535 seconds (minimum 3 seconds, maximum ~18 hours 12 minutes 15 seconds)
|
||||
* Use 0 to disable touch sleep timeout.
|
||||
*
|
||||
* @note Once `thsp` is set, it will persist until reboot or reset. The Nextion device
|
||||
* needs to exit sleep mode to issue `thsp=0` to disable sleep on no touch.
|
||||
*
|
||||
* @note The display will only wake up by a restart or by setting up `thup` (auto wake on touch).
|
||||
* See set_auto_wake_on_touch() to configure wake behavior.
|
||||
* Set the touch sleep timeout of the display.
|
||||
* @param timeout Timeout in seconds.
|
||||
*
|
||||
* Example:
|
||||
* ```cpp
|
||||
* // Set 30 second touch timeout
|
||||
* it.set_touch_sleep_timeout(30);
|
||||
*
|
||||
* // Set maximum timeout (~18 hours)
|
||||
* it.set_touch_sleep_timeout(65535);
|
||||
*
|
||||
* // Disable touch sleep timeout
|
||||
* it.set_touch_sleep_timeout(0);
|
||||
* ```
|
||||
*
|
||||
* Related Nextion instruction: `thsp=<value>`
|
||||
*
|
||||
* @see set_auto_wake_on_touch() Configure automatic wake on touch
|
||||
* @see sleep() Manually control sleep state
|
||||
* After 30 seconds the display will go to sleep. Note: the display will only wakeup by a restart or by setting up
|
||||
* `thup`.
|
||||
*/
|
||||
void set_touch_sleep_timeout(uint16_t touch_sleep_timeout = 0);
|
||||
void set_touch_sleep_timeout(uint16_t touch_sleep_timeout);
|
||||
|
||||
/**
|
||||
* Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode.
|
||||
@@ -1242,6 +1236,20 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
*/
|
||||
void set_auto_wake_on_touch(bool auto_wake_on_touch);
|
||||
|
||||
/**
|
||||
* Sets if Nextion should exit the active reparse mode before the "connect" command is sent
|
||||
* @param exit_reparse_on_start True or false. When exit_reparse_on_start is true, the exit reparse command
|
||||
* will be sent before requesting the connection from Nextion.
|
||||
*
|
||||
* Example:
|
||||
* ```cpp
|
||||
* it.set_exit_reparse_on_start(true);
|
||||
* ```
|
||||
*
|
||||
* The display will be requested to leave active reparse mode before setup.
|
||||
*/
|
||||
void set_exit_reparse_on_start(bool exit_reparse_on_start) { this->exit_reparse_on_start_ = exit_reparse_on_start; }
|
||||
|
||||
/**
|
||||
* @brief Retrieves the number of commands pending in the Nextion command queue.
|
||||
*
|
||||
@@ -1284,7 +1292,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
* the Nextion display. A connection is considered established when:
|
||||
*
|
||||
* - The initial handshake with the display is completed successfully, or
|
||||
* - The handshake is skipped via USE_NEXTION_CONFIG_SKIP_CONNECTION_HANDSHAKE flag
|
||||
* - The handshake is skipped via skip_connection_handshake_ flag
|
||||
*
|
||||
* The connection status is particularly useful when:
|
||||
* - Troubleshooting communication issues
|
||||
@@ -1350,7 +1358,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
#ifdef USE_NEXTION_CONF_START_UP_PAGE
|
||||
uint8_t start_up_page_ = 255;
|
||||
#endif // USE_NEXTION_CONF_START_UP_PAGE
|
||||
bool auto_wake_on_touch_ = true;
|
||||
bool exit_reparse_on_start_ = false;
|
||||
bool skip_connection_handshake_ = false;
|
||||
|
||||
/**
|
||||
* Manually send a raw command to the display and don't wait for an acknowledgement packet.
|
||||
@@ -1457,12 +1466,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe
|
||||
optional<nextion_writer_t> writer_;
|
||||
optional<float> brightness_;
|
||||
|
||||
#ifdef USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
std::string device_model_;
|
||||
std::string firmware_version_;
|
||||
std::string serial_number_;
|
||||
std::string flash_size_;
|
||||
#endif // USE_NEXTION_CONFIG_DUMP_DEVICE_INFO
|
||||
|
||||
void remove_front_no_sensors_();
|
||||
|
||||
|
||||
@@ -15,15 +15,14 @@ void Nextion::set_wake_up_page(uint8_t wake_up_page) {
|
||||
this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true);
|
||||
}
|
||||
|
||||
void Nextion::set_touch_sleep_timeout(const uint16_t touch_sleep_timeout) {
|
||||
// Validate range: Nextion thsp command requires min 3, max 65535 seconds (0 disables)
|
||||
if (touch_sleep_timeout != 0 && touch_sleep_timeout < 3) {
|
||||
this->touch_sleep_timeout_ = 3; // Auto-correct to minimum valid value
|
||||
} else {
|
||||
this->touch_sleep_timeout_ = touch_sleep_timeout;
|
||||
void Nextion::set_touch_sleep_timeout(uint16_t touch_sleep_timeout) {
|
||||
if (touch_sleep_timeout < 3) {
|
||||
ESP_LOGD(TAG, "Sleep timeout out of bounds (3-65535)");
|
||||
return;
|
||||
}
|
||||
|
||||
this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", this->touch_sleep_timeout_, true);
|
||||
this->touch_sleep_timeout_ = touch_sleep_timeout;
|
||||
this->add_no_result_to_queue_with_set_internal_("touch_sleep_timeout", "thsp", touch_sleep_timeout, true);
|
||||
}
|
||||
|
||||
void Nextion::sleep(bool sleep) {
|
||||
|
||||
@@ -8,8 +8,8 @@ void NextionComponent::set_background_color(Color bco) {
|
||||
return; // This is a variable. no need to set color
|
||||
}
|
||||
this->bco_ = bco;
|
||||
this->component_flags_.bco_needs_update = true;
|
||||
this->component_flags_.bco_is_set = true;
|
||||
this->bco_needs_update_ = true;
|
||||
this->bco_is_set_ = true;
|
||||
this->update_component_settings();
|
||||
}
|
||||
|
||||
@@ -19,8 +19,8 @@ void NextionComponent::set_background_pressed_color(Color bco2) {
|
||||
}
|
||||
|
||||
this->bco2_ = bco2;
|
||||
this->component_flags_.bco2_needs_update = true;
|
||||
this->component_flags_.bco2_is_set = true;
|
||||
this->bco2_needs_update_ = true;
|
||||
this->bco2_is_set_ = true;
|
||||
this->update_component_settings();
|
||||
}
|
||||
|
||||
@@ -29,8 +29,8 @@ void NextionComponent::set_foreground_color(Color pco) {
|
||||
return; // This is a variable. no need to set color
|
||||
}
|
||||
this->pco_ = pco;
|
||||
this->component_flags_.pco_needs_update = true;
|
||||
this->component_flags_.pco_is_set = true;
|
||||
this->pco_needs_update_ = true;
|
||||
this->pco_is_set_ = true;
|
||||
this->update_component_settings();
|
||||
}
|
||||
|
||||
@@ -39,8 +39,8 @@ void NextionComponent::set_foreground_pressed_color(Color pco2) {
|
||||
return; // This is a variable. no need to set color
|
||||
}
|
||||
this->pco2_ = pco2;
|
||||
this->component_flags_.pco2_needs_update = true;
|
||||
this->component_flags_.pco2_is_set = true;
|
||||
this->pco2_needs_update_ = true;
|
||||
this->pco2_is_set_ = true;
|
||||
this->update_component_settings();
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ void NextionComponent::set_font_id(uint8_t font_id) {
|
||||
return; // This is a variable. no need to set color
|
||||
}
|
||||
this->font_id_ = font_id;
|
||||
this->component_flags_.font_id_needs_update = true;
|
||||
this->component_flags_.font_id_is_set = true;
|
||||
this->font_id_needs_update_ = true;
|
||||
this->font_id_is_set_ = true;
|
||||
this->update_component_settings();
|
||||
}
|
||||
|
||||
@@ -58,20 +58,20 @@ void NextionComponent::set_visible(bool visible) {
|
||||
if (this->variable_name_ == this->variable_name_to_send_) {
|
||||
return; // This is a variable. no need to set color
|
||||
}
|
||||
this->component_flags_.visible = visible;
|
||||
this->component_flags_.visible_needs_update = true;
|
||||
this->component_flags_.visible_is_set = true;
|
||||
this->visible_ = visible;
|
||||
this->visible_needs_update_ = true;
|
||||
this->visible_is_set_ = true;
|
||||
this->update_component_settings();
|
||||
}
|
||||
|
||||
void NextionComponent::update_component_settings(bool force_update) {
|
||||
if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->component_flags_.visible_is_set ||
|
||||
(!this->component_flags_.visible_needs_update && !this->component_flags_.visible)) {
|
||||
if (this->nextion_->is_sleeping() || !this->nextion_->is_setup() || !this->visible_is_set_ ||
|
||||
(!this->visible_needs_update_ && !this->visible_)) {
|
||||
this->needs_to_send_update_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->component_flags_.visible_needs_update || (force_update && this->component_flags_.visible_is_set)) {
|
||||
if (this->visible_needs_update_ || (force_update && this->visible_is_set_)) {
|
||||
std::string name_to_send = this->variable_name_;
|
||||
|
||||
size_t pos = name_to_send.find_last_of('.');
|
||||
@@ -79,9 +79,9 @@ void NextionComponent::update_component_settings(bool force_update) {
|
||||
name_to_send = name_to_send.substr(pos + 1);
|
||||
}
|
||||
|
||||
this->component_flags_.visible_needs_update = false;
|
||||
this->visible_needs_update_ = false;
|
||||
|
||||
if (this->component_flags_.visible) {
|
||||
if (this->visible_) {
|
||||
this->nextion_->show_component(name_to_send.c_str());
|
||||
this->send_state_to_nextion();
|
||||
} else {
|
||||
@@ -90,26 +90,26 @@ void NextionComponent::update_component_settings(bool force_update) {
|
||||
}
|
||||
}
|
||||
|
||||
if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) {
|
||||
if (this->bco_needs_update_ || (force_update && this->bco2_is_set_)) {
|
||||
this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_);
|
||||
this->component_flags_.bco_needs_update = false;
|
||||
this->bco_needs_update_ = false;
|
||||
}
|
||||
if (this->component_flags_.bco2_needs_update || (force_update && this->component_flags_.bco2_is_set)) {
|
||||
if (this->bco2_needs_update_ || (force_update && this->bco2_is_set_)) {
|
||||
this->nextion_->set_component_pressed_background_color(this->variable_name_.c_str(), this->bco2_);
|
||||
this->component_flags_.bco2_needs_update = false;
|
||||
this->bco2_needs_update_ = false;
|
||||
}
|
||||
if (this->component_flags_.pco_needs_update || (force_update && this->component_flags_.pco_is_set)) {
|
||||
if (this->pco_needs_update_ || (force_update && this->pco_is_set_)) {
|
||||
this->nextion_->set_component_foreground_color(this->variable_name_.c_str(), this->pco_);
|
||||
this->component_flags_.pco_needs_update = false;
|
||||
this->pco_needs_update_ = false;
|
||||
}
|
||||
if (this->component_flags_.pco2_needs_update || (force_update && this->component_flags_.pco2_is_set)) {
|
||||
if (this->pco2_needs_update_ || (force_update && this->pco2_is_set_)) {
|
||||
this->nextion_->set_component_pressed_foreground_color(this->variable_name_.c_str(), this->pco2_);
|
||||
this->component_flags_.pco2_needs_update = false;
|
||||
this->pco2_needs_update_ = false;
|
||||
}
|
||||
|
||||
if (this->component_flags_.font_id_needs_update || (force_update && this->component_flags_.font_id_is_set)) {
|
||||
if (this->font_id_needs_update_ || (force_update && this->font_id_is_set_)) {
|
||||
this->nextion_->set_component_font(this->variable_name_.c_str(), this->font_id_);
|
||||
this->component_flags_.font_id_needs_update = false;
|
||||
this->font_id_needs_update_ = false;
|
||||
}
|
||||
}
|
||||
} // namespace nextion
|
||||
|
||||
@@ -21,64 +21,29 @@ class NextionComponent : public NextionComponentBase {
|
||||
void set_visible(bool visible);
|
||||
|
||||
protected:
|
||||
/**
|
||||
* @brief Constructor initializes component state with visible=true (default state)
|
||||
*/
|
||||
NextionComponent() {
|
||||
component_flags_ = {}; // Zero-initialize all state
|
||||
component_flags_.visible = 1; // Set default visibility to true
|
||||
}
|
||||
|
||||
NextionBase *nextion_;
|
||||
|
||||
// Color and styling properties
|
||||
Color bco_; // Background color
|
||||
Color bco2_; // Pressed background color
|
||||
Color pco_; // Foreground color
|
||||
Color pco2_; // Pressed foreground color
|
||||
bool bco_needs_update_ = false;
|
||||
bool bco_is_set_ = false;
|
||||
Color bco_;
|
||||
bool bco2_needs_update_ = false;
|
||||
bool bco2_is_set_ = false;
|
||||
Color bco2_;
|
||||
bool pco_needs_update_ = false;
|
||||
bool pco_is_set_ = false;
|
||||
Color pco_;
|
||||
bool pco2_needs_update_ = false;
|
||||
bool pco2_is_set_ = false;
|
||||
Color pco2_;
|
||||
uint8_t font_id_ = 0;
|
||||
bool font_id_needs_update_ = false;
|
||||
bool font_id_is_set_ = false;
|
||||
|
||||
/**
|
||||
* @brief Component state management using compact bitfield structure
|
||||
*
|
||||
* Stores all component state flags and properties in a single 16-bit bitfield
|
||||
* for efficient memory usage and improved cache locality.
|
||||
*
|
||||
* Each component property maintains two state flags:
|
||||
* - needs_update: Indicates the property requires synchronization with the display
|
||||
* - is_set: Tracks whether the property has been explicitly configured
|
||||
*
|
||||
* The visible field stores both the update flags and the actual visibility state.
|
||||
*/
|
||||
struct ComponentState {
|
||||
// Background color flags
|
||||
uint16_t bco_needs_update : 1;
|
||||
uint16_t bco_is_set : 1;
|
||||
bool visible_ = true;
|
||||
bool visible_needs_update_ = false;
|
||||
bool visible_is_set_ = false;
|
||||
|
||||
// Pressed background color flags
|
||||
uint16_t bco2_needs_update : 1;
|
||||
uint16_t bco2_is_set : 1;
|
||||
|
||||
// Foreground color flags
|
||||
uint16_t pco_needs_update : 1;
|
||||
uint16_t pco_is_set : 1;
|
||||
|
||||
// Pressed foreground color flags
|
||||
uint16_t pco2_needs_update : 1;
|
||||
uint16_t pco2_is_set : 1;
|
||||
|
||||
// Font ID flags
|
||||
uint16_t font_id_needs_update : 1;
|
||||
uint16_t font_id_is_set : 1;
|
||||
|
||||
// Visibility flags
|
||||
uint16_t visible_needs_update : 1;
|
||||
uint16_t visible_is_set : 1;
|
||||
uint16_t visible : 1; // Actual visibility state
|
||||
|
||||
// Reserved bits for future expansion
|
||||
uint16_t reserved : 3;
|
||||
} component_flags_;
|
||||
// void send_state_to_nextion() = 0;
|
||||
};
|
||||
} // namespace nextion
|
||||
} // namespace esphome
|
||||
|
||||
@@ -53,7 +53,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
|
||||
|
||||
if (this->wave_chan_id_ == UINT8_MAX) {
|
||||
if (send_to_nextion) {
|
||||
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
|
||||
if (this->nextion_->is_sleeping() || !this->visible_) {
|
||||
this->needs_to_send_update_ = true;
|
||||
} else {
|
||||
this->needs_to_send_update_ = false;
|
||||
|
||||
@@ -28,7 +28,7 @@ void NextionSwitch::set_state(bool state, bool publish, bool send_to_nextion) {
|
||||
return;
|
||||
|
||||
if (send_to_nextion) {
|
||||
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
|
||||
if (this->nextion_->is_sleeping() || !this->visible_) {
|
||||
this->needs_to_send_update_ = true;
|
||||
} else {
|
||||
this->needs_to_send_update_ = false;
|
||||
|
||||
@@ -26,7 +26,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s
|
||||
return;
|
||||
|
||||
if (send_to_nextion) {
|
||||
if (this->nextion_->is_sleeping() || !this->component_flags_.visible) {
|
||||
if (this->nextion_->is_sleeping() || !this->visible_) {
|
||||
this->needs_to_send_update_ = true;
|
||||
} else {
|
||||
this->nextion_->add_no_result_to_queue_with_set(this, state);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "nfc.h"
|
||||
#include <cstdio>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
@@ -8,9 +7,29 @@ namespace nfc {
|
||||
|
||||
static const char *const TAG = "nfc";
|
||||
|
||||
std::string format_uid(const std::vector<uint8_t> &uid) { return format_hex_pretty(uid, '-', false); }
|
||||
std::string format_uid(std::vector<uint8_t> &uid) {
|
||||
char buf[(uid.size() * 2) + uid.size() - 1];
|
||||
int offset = 0;
|
||||
for (size_t i = 0; i < uid.size(); i++) {
|
||||
const char *format = "%02X";
|
||||
if (i + 1 < uid.size())
|
||||
format = "%02X-";
|
||||
offset += sprintf(buf + offset, format, uid[i]);
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
std::string format_bytes(const std::vector<uint8_t> &bytes) { return format_hex_pretty(bytes, ' ', false); }
|
||||
std::string format_bytes(std::vector<uint8_t> &bytes) {
|
||||
char buf[(bytes.size() * 2) + bytes.size() - 1];
|
||||
int offset = 0;
|
||||
for (size_t i = 0; i < bytes.size(); i++) {
|
||||
const char *format = "%02X";
|
||||
if (i + 1 < bytes.size())
|
||||
format = "%02X ";
|
||||
offset += sprintf(buf + offset, format, bytes[i]);
|
||||
}
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
uint8_t guess_tag_type(uint8_t uid_length) {
|
||||
if (uid_length == 4) {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "ndef_message.h"
|
||||
#include "ndef_record.h"
|
||||
#include "ndef_message.h"
|
||||
#include "nfc_tag.h"
|
||||
|
||||
#include <vector>
|
||||
@@ -53,8 +53,8 @@ static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7};
|
||||
static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5};
|
||||
|
||||
std::string format_uid(const std::vector<uint8_t> &uid);
|
||||
std::string format_bytes(const std::vector<uint8_t> &bytes);
|
||||
std::string format_uid(std::vector<uint8_t> &uid);
|
||||
std::string format_bytes(std::vector<uint8_t> &bytes);
|
||||
|
||||
uint8_t guess_tag_type(uint8_t uid_length);
|
||||
uint8_t get_mifare_classic_ndef_start_index(std::vector<uint8_t> &data);
|
||||
|
||||
26
esphome/components/runtime_stats/__init__.py
Normal file
26
esphome/components/runtime_stats/__init__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""
|
||||
Runtime statistics component for ESPHome.
|
||||
"""
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
|
||||
DEPENDENCIES = []
|
||||
|
||||
CONF_ENABLED = "enabled"
|
||||
CONF_LOG_INTERVAL = "log_interval"
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ENABLED, default=True): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_LOG_INTERVAL, default=60000
|
||||
): cv.positive_time_period_milliseconds,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
"""Generate code for the runtime statistics component."""
|
||||
cg.add(cg.App.set_runtime_stats_enabled(config[CONF_ENABLED]))
|
||||
cg.add(cg.App.set_runtime_stats_log_interval(config[CONF_LOG_INTERVAL]))
|
||||
@@ -224,7 +224,7 @@ bool SSD1306::is_sh1106_() const {
|
||||
}
|
||||
bool SSD1306::is_sh1107_() const { return this->model_ == SH1107_MODEL_128_64 || this->model_ == SH1107_MODEL_128_128; }
|
||||
bool SSD1306::is_ssd1305_() const {
|
||||
return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_32;
|
||||
return this->model_ == SSD1305_MODEL_128_64 || this->model_ == SSD1305_MODEL_128_64;
|
||||
}
|
||||
void SSD1306::update() {
|
||||
this->do_update_();
|
||||
|
||||
@@ -3132,7 +3132,7 @@ void HOT GDEY0583T81::display() {
|
||||
} else {
|
||||
// Partial out (PTOUT), makes the display exit partial mode
|
||||
this->command(0x92);
|
||||
ESP_LOGD(TAG, "Partial update done, next full update after %" PRIu32 " cycles",
|
||||
ESP_LOGD(TAG, "Partial update done, next full update after %d cycles",
|
||||
this->full_update_every_ - this->at_update_ - 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ enum JsonDetail { DETAIL_ALL, DETAIL_STATE };
|
||||
This is because only minimal changes were made to the ESPAsyncWebServer lib_dep, it was undesirable to put deferred
|
||||
update logic into that library. We need one deferred queue per connection so instead of one AsyncEventSource with
|
||||
multiple clients, we have multiple event sources with one client each. This is slightly awkward which is why it's
|
||||
implemented in a more straightforward way for ESP-IDF. Arduino platform will eventually go away and this workaround
|
||||
implemented in a more straightforward way for ESP-IDF. Arudino platform will eventually go away and this workaround
|
||||
can be forgotten.
|
||||
*/
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
@@ -40,4 +40,7 @@ async def to_code(config):
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("ESP8266WiFi", None)
|
||||
# https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json
|
||||
cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8")
|
||||
# Use fork with libretiny compatibility fix
|
||||
cg.add_library(
|
||||
"https://github.com/bdraco/ESPAsyncWebServer.git#libretiny_Fix", None
|
||||
)
|
||||
|
||||
@@ -2,8 +2,6 @@ from collections.abc import Callable
|
||||
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_LEVEL,
|
||||
CONF_LOGGER,
|
||||
KEY_CORE,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
@@ -159,20 +157,3 @@ def filter_source_files_from_platform(
|
||||
]
|
||||
|
||||
return filter_source_files
|
||||
|
||||
|
||||
def get_logger_level() -> str:
|
||||
"""Get the configured logger level.
|
||||
|
||||
This is used by components to determine what logging features to include
|
||||
based on the configured log level.
|
||||
|
||||
Returns:
|
||||
The configured logger level string, defaults to "DEBUG" if not configured
|
||||
"""
|
||||
# Check if logger config exists
|
||||
if CONF_LOGGER not in CORE.config:
|
||||
return "DEBUG"
|
||||
|
||||
logger_config = CORE.config[CONF_LOGGER]
|
||||
return logger_config.get(CONF_LEVEL, "DEBUG")
|
||||
|
||||
@@ -4,7 +4,7 @@ from enum import Enum
|
||||
|
||||
from esphome.enum import StrEnum
|
||||
|
||||
__version__ = "2025.8.0-dev"
|
||||
__version__ = "2025.7.0-dev"
|
||||
|
||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
|
||||
VALID_SUBSTITUTIONS_CHARACTERS = (
|
||||
|
||||
@@ -141,6 +141,10 @@ void Application::loop() {
|
||||
this->in_loop_ = false;
|
||||
this->app_state_ = new_app_state;
|
||||
|
||||
// Process any pending runtime stats printing after all components have run
|
||||
// This ensures stats printing doesn't affect component timing measurements
|
||||
runtime_stats.process_pending_stats(last_op_end_time);
|
||||
|
||||
// Use the last component's end time instead of calling millis() again
|
||||
auto elapsed = last_op_end_time - this->last_loop_;
|
||||
if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/runtime_stats.h"
|
||||
#include "esphome/core/scheduler.h"
|
||||
|
||||
#ifdef USE_DEVICES
|
||||
@@ -348,6 +349,18 @@ class Application {
|
||||
|
||||
uint32_t get_loop_interval() const { return static_cast<uint32_t>(this->loop_interval_); }
|
||||
|
||||
/** Enable or disable runtime statistics collection.
|
||||
*
|
||||
* @param enable Whether to enable runtime statistics collection.
|
||||
*/
|
||||
void set_runtime_stats_enabled(bool enable) { runtime_stats.set_enabled(enable); }
|
||||
|
||||
/** Set the interval at which runtime statistics are logged.
|
||||
*
|
||||
* @param interval The interval in milliseconds between logging of runtime statistics.
|
||||
*/
|
||||
void set_runtime_stats_log_interval(uint32_t interval) { runtime_stats.set_log_interval(interval); }
|
||||
|
||||
void schedule_dump_config() { this->dump_config_at_ = 0; }
|
||||
|
||||
void feed_wdt(uint32_t time = 0);
|
||||
|
||||
@@ -395,6 +395,9 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
|
||||
uint32_t curr_time = millis();
|
||||
|
||||
uint32_t blocking_time = curr_time - this->started_;
|
||||
|
||||
// Record component runtime stats
|
||||
runtime_stats.record_component_time(this->component_, blocking_time, curr_time);
|
||||
bool should_warn;
|
||||
if (this->component_ != nullptr) {
|
||||
should_warn = this->component_->should_warn_of_blocking(blocking_time);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
#include "esphome/core/runtime_stats.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
|
||||
@@ -258,60 +258,53 @@ std::string format_hex(const uint8_t *data, size_t length) {
|
||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
||||
|
||||
static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
|
||||
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator, bool show_length) {
|
||||
if (data == nullptr || length == 0)
|
||||
std::string format_hex_pretty(const uint8_t *data, size_t length) {
|
||||
if (length == 0)
|
||||
return "";
|
||||
std::string ret;
|
||||
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
|
||||
ret.resize(multiple * length - (separator ? 1 : 0));
|
||||
ret.resize(3 * length - 1);
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
|
||||
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
||||
if (separator && i != length - 1)
|
||||
ret[multiple * i + 2] = separator;
|
||||
ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
|
||||
ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
||||
if (i != length - 1)
|
||||
ret[3 * i + 2] = '.';
|
||||
}
|
||||
if (show_length && length > 4)
|
||||
return ret + " (" + std::to_string(length) + ")";
|
||||
if (length > 4)
|
||||
return ret + " (" + to_string(length) + ")";
|
||||
return ret;
|
||||
}
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator, bool show_length) {
|
||||
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||
}
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data) { return format_hex_pretty(data.data(), data.size()); }
|
||||
|
||||
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator, bool show_length) {
|
||||
if (data == nullptr || length == 0)
|
||||
std::string format_hex_pretty(const uint16_t *data, size_t length) {
|
||||
if (length == 0)
|
||||
return "";
|
||||
std::string ret;
|
||||
uint8_t multiple = separator ? 5 : 4; // 5 if separator is not \0, 4 otherwise
|
||||
ret.resize(multiple * length - (separator ? 1 : 0));
|
||||
ret.resize(5 * length - 1);
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
|
||||
ret[multiple * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
|
||||
ret[multiple * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4);
|
||||
ret[multiple * i + 3] = format_hex_pretty_char(data[i] & 0x000F);
|
||||
if (separator && i != length - 1)
|
||||
ret[multiple * i + 4] = separator;
|
||||
ret[5 * i] = format_hex_pretty_char((data[i] & 0xF000) >> 12);
|
||||
ret[5 * i + 1] = format_hex_pretty_char((data[i] & 0x0F00) >> 8);
|
||||
ret[5 * i + 2] = format_hex_pretty_char((data[i] & 0x00F0) >> 4);
|
||||
ret[5 * i + 3] = format_hex_pretty_char(data[i] & 0x000F);
|
||||
if (i != length - 1)
|
||||
ret[5 * i + 2] = '.';
|
||||
}
|
||||
if (show_length && length > 4)
|
||||
return ret + " (" + std::to_string(length) + ")";
|
||||
if (length > 4)
|
||||
return ret + " (" + to_string(length) + ")";
|
||||
return ret;
|
||||
}
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator, bool show_length) {
|
||||
return format_hex_pretty(data.data(), data.size(), separator, show_length);
|
||||
}
|
||||
std::string format_hex_pretty(const std::string &data, char separator, bool show_length) {
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data) { return format_hex_pretty(data.data(), data.size()); }
|
||||
std::string format_hex_pretty(const std::string &data) {
|
||||
if (data.empty())
|
||||
return "";
|
||||
std::string ret;
|
||||
uint8_t multiple = separator ? 3 : 2; // 3 if separator is not \0, 2 otherwise
|
||||
ret.resize(multiple * data.length() - (separator ? 1 : 0));
|
||||
ret.resize(3 * data.length() - 1);
|
||||
for (size_t i = 0; i < data.length(); i++) {
|
||||
ret[multiple * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
|
||||
ret[multiple * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
||||
if (separator && i != data.length() - 1)
|
||||
ret[multiple * i + 2] = separator;
|
||||
ret[3 * i] = format_hex_pretty_char((data[i] & 0xF0) >> 4);
|
||||
ret[3 * i + 1] = format_hex_pretty_char(data[i] & 0x0F);
|
||||
if (i != data.length() - 1)
|
||||
ret[3 * i + 2] = '.';
|
||||
}
|
||||
if (show_length && data.length() > 4)
|
||||
if (data.length() > 4)
|
||||
return ret + " (" + std::to_string(data.length()) + ")";
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -344,149 +344,20 @@ template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &dat
|
||||
return format_hex(data.data(), data.size());
|
||||
}
|
||||
|
||||
/** Format a byte array in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Converts binary data to a hexadecimal string representation with customizable formatting.
|
||||
* Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
|
||||
* Optionally includes the total byte count in parentheses at the end.
|
||||
*
|
||||
* @param data Pointer to the byte array to format.
|
||||
* @param length Number of bytes in the array.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
* @return Formatted hex string, e.g., "A1.B2.C3.D4.E5 (5)" or "A1:B2:C3" depending on parameters.
|
||||
*
|
||||
* @note Returns empty string if data is nullptr or length is 0.
|
||||
* @note The length will only be appended if show_length is true AND the length is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* uint8_t data[] = {0xA1, 0xB2, 0xC3};
|
||||
* format_hex_pretty(data, 3); // Returns "A1.B2.C3" (no length shown for <= 4 parts)
|
||||
* uint8_t data2[] = {0xA1, 0xB2, 0xC3, 0xD4, 0xE5};
|
||||
* format_hex_pretty(data2, 5); // Returns "A1.B2.C3.D4.E5 (5)"
|
||||
* format_hex_pretty(data2, 5, ':'); // Returns "A1:B2:C3:D4:E5 (5)"
|
||||
* format_hex_pretty(data2, 5, '.', false); // Returns "A1.B2.C3.D4.E5"
|
||||
* @endcode
|
||||
*/
|
||||
std::string format_hex_pretty(const uint8_t *data, size_t length, char separator = '.', bool show_length = true);
|
||||
|
||||
/** Format a 16-bit word array in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
|
||||
*
|
||||
* @param data Pointer to the 16-bit word array to format.
|
||||
* @param length Number of 16-bit words in the array.
|
||||
* @param separator Character to use between hex words (default: '.').
|
||||
* @param show_length Whether to append the word count in parentheses (default: true).
|
||||
* @return Formatted hex string with 4-digit hex values per word.
|
||||
*
|
||||
* @note The length will only be appended if show_length is true AND the length is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* uint16_t data[] = {0xA1B2, 0xC3D4};
|
||||
* format_hex_pretty(data, 2); // Returns "A1B2.C3D4" (no length shown for <= 4 parts)
|
||||
* uint16_t data2[] = {0xA1B2, 0xC3D4, 0xE5F6};
|
||||
* format_hex_pretty(data2, 3); // Returns "A1B2.C3D4.E5F6 (3)"
|
||||
* @endcode
|
||||
*/
|
||||
std::string format_hex_pretty(const uint16_t *data, size_t length, char separator = '.', bool show_length = true);
|
||||
|
||||
/** Format a byte vector in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit
|
||||
* uppercase hex value with customizable separator.
|
||||
*
|
||||
* @param data Vector of bytes to format.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
* @return Formatted hex string representation of the vector contents.
|
||||
*
|
||||
* @note The length will only be appended if show_length is true AND the vector size is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* std::vector<uint8_t> data = {0xDE, 0xAD, 0xBE, 0xEF};
|
||||
* format_hex_pretty(data); // Returns "DE.AD.BE.EF" (no length shown for <= 4 parts)
|
||||
* std::vector<uint8_t> data2 = {0xDE, 0xAD, 0xBE, 0xEF, 0xCA};
|
||||
* format_hex_pretty(data2); // Returns "DE.AD.BE.EF.CA (5)"
|
||||
* format_hex_pretty(data2, '-'); // Returns "DE-AD-BE-EF-CA (5)"
|
||||
* @endcode
|
||||
*/
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/** Format a 16-bit word vector in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted
|
||||
* as a 4-digit uppercase hex value in big-endian order.
|
||||
*
|
||||
* @param data Vector of 16-bit words to format.
|
||||
* @param separator Character to use between hex words (default: '.').
|
||||
* @param show_length Whether to append the word count in parentheses (default: true).
|
||||
* @return Formatted hex string representation of the vector contents.
|
||||
*
|
||||
* @note The length will only be appended if show_length is true AND the vector size is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* std::vector<uint16_t> data = {0x1234, 0x5678};
|
||||
* format_hex_pretty(data); // Returns "1234.5678" (no length shown for <= 4 parts)
|
||||
* std::vector<uint16_t> data2 = {0x1234, 0x5678, 0x9ABC};
|
||||
* format_hex_pretty(data2); // Returns "1234.5678.9ABC (3)"
|
||||
* @endcode
|
||||
*/
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/** Format a string's bytes in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Treats each character in the string as a byte and formats it in hex.
|
||||
* Useful for debugging binary data stored in std::string containers.
|
||||
*
|
||||
* @param data String whose bytes should be formatted as hex.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
* @return Formatted hex string representation of the string's byte contents.
|
||||
*
|
||||
* @note The length will only be appended if show_length is true AND the string length is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* std::string data = "ABC"; // ASCII: 0x41, 0x42, 0x43
|
||||
* format_hex_pretty(data); // Returns "41.42.43" (no length shown for <= 4 parts)
|
||||
* std::string data2 = "ABCDE";
|
||||
* format_hex_pretty(data2); // Returns "41.42.43.44.45 (5)"
|
||||
* @endcode
|
||||
*/
|
||||
std::string format_hex_pretty(const std::string &data, char separator = '.', bool show_length = true);
|
||||
|
||||
/** Format an unsigned integer in pretty-printed, human-readable hex format.
|
||||
*
|
||||
* Converts the integer to big-endian byte order and formats each byte as hex.
|
||||
* The most significant byte appears first in the output string.
|
||||
*
|
||||
* @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
|
||||
* @param val The unsigned integer value to format.
|
||||
* @param separator Character to use between hex bytes (default: '.').
|
||||
* @param show_length Whether to append the byte count in parentheses (default: true).
|
||||
* @return Formatted hex string with most significant byte first.
|
||||
*
|
||||
* @note The length will only be appended if show_length is true AND sizeof(T) is greater than 4.
|
||||
*
|
||||
* Example:
|
||||
* @code
|
||||
* uint32_t value = 0x12345678;
|
||||
* format_hex_pretty(value); // Returns "12.34.56.78" (no length shown for <= 4 parts)
|
||||
* uint64_t value2 = 0x123456789ABCDEF0;
|
||||
* format_hex_pretty(value2); // Returns "12.34.56.78.9A.BC.DE.F0 (8)"
|
||||
* format_hex_pretty(value2, ':'); // Returns "12:34:56:78:9A:BC:DE:F0 (8)"
|
||||
* format_hex_pretty<uint16_t>(0x1234); // Returns "12.34"
|
||||
* @endcode
|
||||
*/
|
||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>
|
||||
std::string format_hex_pretty(T val, char separator = '.', bool show_length = true) {
|
||||
/// Format the byte array \p data of length \p len in pretty-printed, human-readable hex.
|
||||
std::string format_hex_pretty(const uint8_t *data, size_t length);
|
||||
/// Format the word array \p data of length \p len in pretty-printed, human-readable hex.
|
||||
std::string format_hex_pretty(const uint16_t *data, size_t length);
|
||||
/// Format the vector \p data in pretty-printed, human-readable hex.
|
||||
std::string format_hex_pretty(const std::vector<uint8_t> &data);
|
||||
/// Format the vector \p data in pretty-printed, human-readable hex.
|
||||
std::string format_hex_pretty(const std::vector<uint16_t> &data);
|
||||
/// Format the string \p data in pretty-printed, human-readable hex.
|
||||
std::string format_hex_pretty(const std::string &data);
|
||||
/// Format an unsigned integer in pretty-printed, human-readable hex, starting with the most significant byte.
|
||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex_pretty(T val) {
|
||||
val = convert_big_endian(val);
|
||||
return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T), separator, show_length);
|
||||
return format_hex_pretty(reinterpret_cast<uint8_t *>(&val), sizeof(T));
|
||||
}
|
||||
|
||||
/// Format the byte array \p data of length \p len in binary.
|
||||
|
||||
92
esphome/core/runtime_stats.cpp
Normal file
92
esphome/core/runtime_stats.cpp
Normal file
@@ -0,0 +1,92 @@
|
||||
#include "esphome/core/runtime_stats.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
RuntimeStatsCollector runtime_stats;
|
||||
|
||||
void RuntimeStatsCollector::record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time) {
|
||||
if (!this->enabled_ || component == nullptr)
|
||||
return;
|
||||
|
||||
// Check if we have cached the name for this component
|
||||
auto name_it = this->component_names_cache_.find(component);
|
||||
if (name_it == this->component_names_cache_.end()) {
|
||||
// First time seeing this component, cache its name
|
||||
const char *source = component->get_component_source();
|
||||
this->component_names_cache_[component] = source;
|
||||
this->component_stats_[source].record_time(duration_ms);
|
||||
} else {
|
||||
// Use cached name - no string operations, just map lookup
|
||||
this->component_stats_[name_it->second].record_time(duration_ms);
|
||||
}
|
||||
|
||||
// If next_log_time_ is 0, initialize it
|
||||
if (this->next_log_time_ == 0) {
|
||||
this->next_log_time_ = current_time + this->log_interval_;
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't print stats here anymore - let process_pending_stats handle it
|
||||
}
|
||||
|
||||
void RuntimeStatsCollector::log_stats_() {
|
||||
ESP_LOGI(RUNTIME_TAG, "Component Runtime Statistics");
|
||||
ESP_LOGI(RUNTIME_TAG, "Period stats (last %" PRIu32 "ms):", this->log_interval_);
|
||||
|
||||
// First collect stats we want to display
|
||||
std::vector<ComponentStatPair> stats_to_display;
|
||||
|
||||
for (const auto &it : this->component_stats_) {
|
||||
const ComponentRuntimeStats &stats = it.second;
|
||||
if (stats.get_period_count() > 0) {
|
||||
ComponentStatPair pair = {it.first, &stats};
|
||||
stats_to_display.push_back(pair);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by period runtime (descending)
|
||||
std::sort(stats_to_display.begin(), stats_to_display.end(), std::greater<ComponentStatPair>());
|
||||
|
||||
// Log top components by period runtime
|
||||
for (const auto &it : stats_to_display) {
|
||||
const std::string &source = it.name;
|
||||
const ComponentRuntimeStats *stats = it.stats;
|
||||
|
||||
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source.c_str(),
|
||||
stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(),
|
||||
stats->get_period_time_ms());
|
||||
}
|
||||
|
||||
// Log total stats since boot
|
||||
ESP_LOGI(RUNTIME_TAG, "Total stats (since boot):");
|
||||
|
||||
// Re-sort by total runtime for all-time stats
|
||||
std::sort(stats_to_display.begin(), stats_to_display.end(),
|
||||
[](const ComponentStatPair &a, const ComponentStatPair &b) {
|
||||
return a.stats->get_total_time_ms() > b.stats->get_total_time_ms();
|
||||
});
|
||||
|
||||
for (const auto &it : stats_to_display) {
|
||||
const std::string &source = it.name;
|
||||
const ComponentRuntimeStats *stats = it.stats;
|
||||
|
||||
ESP_LOGI(RUNTIME_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", source.c_str(),
|
||||
stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
|
||||
stats->get_total_time_ms());
|
||||
}
|
||||
}
|
||||
|
||||
void RuntimeStatsCollector::process_pending_stats(uint32_t current_time) {
|
||||
if (!this->enabled_ || this->next_log_time_ == 0)
|
||||
return;
|
||||
|
||||
if (current_time >= this->next_log_time_) {
|
||||
this->log_stats_();
|
||||
this->reset_stats_();
|
||||
this->next_log_time_ = current_time + this->log_interval_;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
121
esphome/core/runtime_stats.h
Normal file
121
esphome/core/runtime_stats.h
Normal file
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <algorithm>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
static const char *const RUNTIME_TAG = "runtime";
|
||||
|
||||
class Component; // Forward declaration
|
||||
|
||||
class ComponentRuntimeStats {
|
||||
public:
|
||||
ComponentRuntimeStats()
|
||||
: period_count_(0),
|
||||
total_count_(0),
|
||||
period_time_ms_(0),
|
||||
total_time_ms_(0),
|
||||
period_max_time_ms_(0),
|
||||
total_max_time_ms_(0) {}
|
||||
|
||||
void record_time(uint32_t duration_ms) {
|
||||
// Update period counters
|
||||
this->period_count_++;
|
||||
this->period_time_ms_ += duration_ms;
|
||||
if (duration_ms > this->period_max_time_ms_)
|
||||
this->period_max_time_ms_ = duration_ms;
|
||||
|
||||
// Update total counters
|
||||
this->total_count_++;
|
||||
this->total_time_ms_ += duration_ms;
|
||||
if (duration_ms > this->total_max_time_ms_)
|
||||
this->total_max_time_ms_ = duration_ms;
|
||||
}
|
||||
|
||||
void reset_period_stats() {
|
||||
this->period_count_ = 0;
|
||||
this->period_time_ms_ = 0;
|
||||
this->period_max_time_ms_ = 0;
|
||||
}
|
||||
|
||||
// Period stats (reset each logging interval)
|
||||
uint32_t get_period_count() const { return this->period_count_; }
|
||||
uint32_t get_period_time_ms() const { return this->period_time_ms_; }
|
||||
uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
|
||||
float get_period_avg_time_ms() const {
|
||||
return this->period_count_ > 0 ? this->period_time_ms_ / static_cast<float>(this->period_count_) : 0.0f;
|
||||
}
|
||||
|
||||
// Total stats (persistent until reboot)
|
||||
uint32_t get_total_count() const { return this->total_count_; }
|
||||
uint32_t get_total_time_ms() const { return this->total_time_ms_; }
|
||||
uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
|
||||
float get_total_avg_time_ms() const {
|
||||
return this->total_count_ > 0 ? this->total_time_ms_ / static_cast<float>(this->total_count_) : 0.0f;
|
||||
}
|
||||
|
||||
protected:
|
||||
// Period stats (reset each logging interval)
|
||||
uint32_t period_count_;
|
||||
uint32_t period_time_ms_;
|
||||
uint32_t period_max_time_ms_;
|
||||
|
||||
// Total stats (persistent until reboot)
|
||||
uint32_t total_count_;
|
||||
uint32_t total_time_ms_;
|
||||
uint32_t total_max_time_ms_;
|
||||
};
|
||||
|
||||
// For sorting components by run time
|
||||
struct ComponentStatPair {
|
||||
std::string name;
|
||||
const ComponentRuntimeStats *stats;
|
||||
|
||||
bool operator>(const ComponentStatPair &other) const {
|
||||
// Sort by period time as that's what we're displaying in the logs
|
||||
return stats->get_period_time_ms() > other.stats->get_period_time_ms();
|
||||
}
|
||||
};
|
||||
|
||||
class RuntimeStatsCollector {
|
||||
public:
|
||||
RuntimeStatsCollector() : log_interval_(60000), next_log_time_(0), enabled_(true) {}
|
||||
|
||||
void set_log_interval(uint32_t log_interval) { this->log_interval_ = log_interval; }
|
||||
uint32_t get_log_interval() const { return this->log_interval_; }
|
||||
|
||||
void set_enabled(bool enabled) { this->enabled_ = enabled; }
|
||||
bool is_enabled() const { return this->enabled_; }
|
||||
|
||||
void record_component_time(Component *component, uint32_t duration_ms, uint32_t current_time);
|
||||
|
||||
// Process any pending stats printing (should be called after component loop)
|
||||
void process_pending_stats(uint32_t current_time);
|
||||
|
||||
protected:
|
||||
void log_stats_();
|
||||
|
||||
void reset_stats_() {
|
||||
for (auto &it : this->component_stats_) {
|
||||
it.second.reset_period_stats();
|
||||
}
|
||||
}
|
||||
|
||||
// Back to string keys, but we'll cache the source name per component
|
||||
std::map<std::string, ComponentRuntimeStats> component_stats_;
|
||||
std::map<Component *, std::string> component_names_cache_;
|
||||
uint32_t log_interval_;
|
||||
uint32_t next_log_time_;
|
||||
bool enabled_;
|
||||
};
|
||||
|
||||
// Global instance for runtime stats collection
|
||||
extern RuntimeStatsCollector runtime_stats;
|
||||
|
||||
} // namespace esphome
|
||||
@@ -1,13 +1,13 @@
|
||||
#include "scheduler.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include "application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
@@ -86,10 +86,8 @@ void HOT Scheduler::set_timer_common_(Component *component, SchedulerItem::Type
|
||||
// ESP8266 and RP2040 are excluded because they don't need thread-safe defer handling
|
||||
if (delay == 0 && type == SchedulerItem::TIMEOUT) {
|
||||
// Put in defer queue for guaranteed FIFO execution
|
||||
if (is_name_valid_(name_cstr)) {
|
||||
LockGuard guard{this->lock_};
|
||||
this->cancel_item_locked_(component, name_cstr, type);
|
||||
}
|
||||
LockGuard guard{this->lock_};
|
||||
this->cancel_item_locked_(component, name_cstr, type);
|
||||
this->defer_queue_.push_back(std::move(item));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
|
||||
esptool==4.9.0
|
||||
click==8.1.7
|
||||
esphome-dashboard==20250514.0
|
||||
aioesphomeapi==34.2.0
|
||||
aioesphomeapi==34.1.0
|
||||
zeroconf==0.147.0
|
||||
puremagic==1.30
|
||||
ruamel.yaml==0.18.14 # dashboard_import
|
||||
|
||||
@@ -1,71 +1,29 @@
|
||||
"""Fixtures for component tests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Generator
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
# Add package root to python path
|
||||
here = Path(__file__).parent
|
||||
package_root = here.parent.parent
|
||||
sys.path.insert(0, package_root.as_posix())
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
from esphome.__main__ import generate_cpp_contents # noqa: E402
|
||||
from esphome.config import read_config # noqa: E402
|
||||
from esphome.core import CORE # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def config_path(request: pytest.FixtureRequest) -> Generator[None]:
|
||||
"""Set CORE.config_path to the component's config directory and reset it after the test."""
|
||||
original_path = CORE.config_path
|
||||
config_dir = Path(request.fspath).parent / "config"
|
||||
|
||||
# Check if config directory exists, if not use parent directory
|
||||
if config_dir.exists():
|
||||
# Set config_path to a dummy yaml file in the config directory
|
||||
# This ensures CORE.config_dir points to the config directory
|
||||
CORE.config_path = str(config_dir / "dummy.yaml")
|
||||
else:
|
||||
CORE.config_path = str(Path(request.fspath).parent / "dummy.yaml")
|
||||
|
||||
yield
|
||||
CORE.config_path = original_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def component_fixture_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
|
||||
"""Return a function to get absolute paths relative to the component's fixtures directory."""
|
||||
|
||||
def _get_path(file_name: str) -> Path:
|
||||
"""Get the absolute path of a file relative to the component's fixtures directory."""
|
||||
return (Path(request.fspath).parent / "fixtures" / file_name).absolute()
|
||||
|
||||
return _get_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def component_config_path(request: pytest.FixtureRequest) -> Callable[[str], Path]:
|
||||
"""Return a function to get absolute paths relative to the component's config directory."""
|
||||
|
||||
def _get_path(file_name: str) -> Path:
|
||||
"""Get the absolute path of a file relative to the component's config directory."""
|
||||
return (Path(request.fspath).parent / "config" / file_name).absolute()
|
||||
|
||||
return _get_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def generate_main() -> Generator[Callable[[str | Path], str]]:
|
||||
def generate_main():
|
||||
"""Generates the C++ main.cpp file and returns it in string form."""
|
||||
|
||||
def generator(path: str | Path) -> str:
|
||||
CORE.config_path = str(path)
|
||||
def generator(path: str) -> str:
|
||||
CORE.config_path = path
|
||||
CORE.config = read_config({})
|
||||
generate_cpp_contents(CORE.config)
|
||||
print(CORE.cpp_main_section)
|
||||
return CORE.cpp_main_section
|
||||
|
||||
yield generator
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 685 B |
@@ -1,20 +0,0 @@
|
||||
esphome:
|
||||
name: test
|
||||
|
||||
esp32:
|
||||
board: esp32s3box
|
||||
|
||||
image:
|
||||
- file: image.png
|
||||
byte_order: little_endian
|
||||
id: cat_img
|
||||
type: rgb565
|
||||
|
||||
spi:
|
||||
mosi_pin: 6
|
||||
clk_pin: 7
|
||||
|
||||
display:
|
||||
- platform: mipi_spi
|
||||
id: lcd_display
|
||||
model: s3box
|
||||
@@ -1,183 +0,0 @@
|
||||
"""Tests for image configuration validation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from esphome import config_validation as cv
|
||||
from esphome.components.image import CONFIG_SCHEMA
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("config", "error_match"),
|
||||
[
|
||||
pytest.param(
|
||||
"a string",
|
||||
"Badly formed image configuration, expected a list or a dictionary",
|
||||
id="invalid_string_config",
|
||||
),
|
||||
pytest.param(
|
||||
{"id": "image_id", "type": "rgb565"},
|
||||
r"required key not provided @ data\[0\]\['file'\]",
|
||||
id="missing_file",
|
||||
),
|
||||
pytest.param(
|
||||
{"file": "image.png", "type": "rgb565"},
|
||||
r"required key not provided @ data\[0\]\['id'\]",
|
||||
id="missing_id",
|
||||
),
|
||||
pytest.param(
|
||||
{"id": "mdi_id", "file": "mdi:weather-##", "type": "rgb565"},
|
||||
"Could not parse mdi icon name",
|
||||
id="invalid_mdi_icon",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
"type": "binary",
|
||||
"transparency": "alpha_channel",
|
||||
},
|
||||
"Image format 'BINARY' cannot have transparency",
|
||||
id="binary_with_transparency",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
"type": "rgb565",
|
||||
"transparency": "chroma_key",
|
||||
"invert_alpha": True,
|
||||
},
|
||||
"No alpha channel to invert",
|
||||
id="invert_alpha_without_alpha_channel",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
"type": "binary",
|
||||
"byte_order": "big_endian",
|
||||
},
|
||||
"Image format 'BINARY' does not support byte order configuration",
|
||||
id="binary_with_byte_order",
|
||||
),
|
||||
pytest.param(
|
||||
{"id": "image_id", "file": "bad.png", "type": "binary"},
|
||||
"File can't be opened as image",
|
||||
id="invalid_image_file",
|
||||
),
|
||||
pytest.param(
|
||||
{"defaults": {}, "images": [{"id": "image_id", "file": "image.png"}]},
|
||||
"Type is required either in the image config or in the defaults",
|
||||
id="missing_type_in_defaults",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_image_configuration_errors(
|
||||
config: Any,
|
||||
error_match: str,
|
||||
) -> None:
|
||||
"""Test detection of invalid configuration."""
|
||||
with pytest.raises(cv.Invalid, match=error_match):
|
||||
CONFIG_SCHEMA(config)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
pytest.param(
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
"type": "rgb565",
|
||||
"transparency": "chroma_key",
|
||||
"byte_order": "little_endian",
|
||||
"dither": "FloydSteinberg",
|
||||
"resize": "100x100",
|
||||
"invert_alpha": False,
|
||||
},
|
||||
id="single_image_all_options",
|
||||
),
|
||||
pytest.param(
|
||||
[
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
"type": "binary",
|
||||
}
|
||||
],
|
||||
id="list_of_images",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"defaults": {
|
||||
"type": "rgb565",
|
||||
"transparency": "chroma_key",
|
||||
"byte_order": "little_endian",
|
||||
"dither": "FloydSteinberg",
|
||||
"resize": "100x100",
|
||||
"invert_alpha": False,
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
}
|
||||
],
|
||||
},
|
||||
id="images_with_defaults",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"rgb565": {
|
||||
"alpha_channel": [
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
"transparency": "alpha_channel",
|
||||
"byte_order": "little_endian",
|
||||
"dither": "FloydSteinberg",
|
||||
"resize": "100x100",
|
||||
"invert_alpha": False,
|
||||
}
|
||||
]
|
||||
},
|
||||
"binary": [
|
||||
{
|
||||
"id": "image_id",
|
||||
"file": "image.png",
|
||||
"transparency": "opaque",
|
||||
"dither": "FloydSteinberg",
|
||||
"resize": "100x100",
|
||||
"invert_alpha": False,
|
||||
}
|
||||
],
|
||||
},
|
||||
id="type_based_organization",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_image_configuration_success(
|
||||
config: dict[str, Any] | list[dict[str, Any]],
|
||||
) -> None:
|
||||
"""Test successful configuration validation."""
|
||||
CONFIG_SCHEMA(config)
|
||||
|
||||
|
||||
def test_image_generation(
|
||||
generate_main: Callable[[str | Path], str],
|
||||
component_config_path: Callable[[str], Path],
|
||||
) -> None:
|
||||
"""Test image generation configuration."""
|
||||
|
||||
main_cpp = generate_main(component_config_path("image_test.yaml"))
|
||||
assert "uint8_t_id[] PROGMEM = {0x24, 0x21, 0x24, 0x21" in main_cpp
|
||||
assert (
|
||||
"cat_img = new image::Image(uint8_t_id, 32, 24, image::IMAGE_TYPE_RGB565, image::TRANSPARENCY_OPAQUE);"
|
||||
in main_cpp
|
||||
)
|
||||
@@ -1,12 +0,0 @@
|
||||
i2c:
|
||||
- id: i2c_gl_r01_i2c
|
||||
scl: ${scl_pin}
|
||||
sda: ${sda_pin}
|
||||
|
||||
sensor:
|
||||
- platform: gl_r01_i2c
|
||||
id: tof
|
||||
name: "ToF sensor"
|
||||
i2c_id: i2c_gl_r01_i2c
|
||||
address: 0x74
|
||||
update_interval: 15s
|
||||
@@ -1,5 +0,0 @@
|
||||
substitutions:
|
||||
scl_pin: GPIO16
|
||||
sda_pin: GPIO17
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,5 +0,0 @@
|
||||
substitutions:
|
||||
scl_pin: GPIO5
|
||||
sda_pin: GPIO4
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,5 +0,0 @@
|
||||
substitutions:
|
||||
scl_pin: GPIO5
|
||||
sda_pin: GPIO4
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,5 +0,0 @@
|
||||
substitutions:
|
||||
scl_pin: GPIO16
|
||||
sda_pin: GPIO17
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,5 +0,0 @@
|
||||
substitutions:
|
||||
scl_pin: GPIO5
|
||||
sda_pin: GPIO4
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,5 +0,0 @@
|
||||
substitutions:
|
||||
scl_pin: GPIO5
|
||||
sda_pin: GPIO4
|
||||
|
||||
<<: !include common.yaml
|
||||
17
tests/components/image/test.esp32-ard.yaml
Normal file
17
tests/components/image/test.esp32-ard.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
spi:
|
||||
- id: spi_main_lcd
|
||||
clk_pin: 16
|
||||
mosi_pin: 17
|
||||
miso_pin: 32
|
||||
|
||||
display:
|
||||
- platform: ili9xxx
|
||||
id: main_lcd
|
||||
model: ili9342
|
||||
cs_pin: 14
|
||||
dc_pin: 13
|
||||
reset_pin: 21
|
||||
invert_colors: true
|
||||
|
||||
<<: !include common.yaml
|
||||
|
||||
16
tests/components/image/test.esp32-c3-ard.yaml
Normal file
16
tests/components/image/test.esp32-c3-ard.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
spi:
|
||||
- id: spi_main_lcd
|
||||
clk_pin: 6
|
||||
mosi_pin: 7
|
||||
miso_pin: 5
|
||||
|
||||
display:
|
||||
- platform: ili9xxx
|
||||
id: main_lcd
|
||||
model: ili9342
|
||||
cs_pin: 3
|
||||
dc_pin: 11
|
||||
reset_pin: 10
|
||||
invert_colors: true
|
||||
|
||||
<<: !include common.yaml
|
||||
16
tests/components/image/test.esp32-c3-idf.yaml
Normal file
16
tests/components/image/test.esp32-c3-idf.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
spi:
|
||||
- id: spi_main_lcd
|
||||
clk_pin: 6
|
||||
mosi_pin: 7
|
||||
miso_pin: 5
|
||||
|
||||
display:
|
||||
- platform: ili9xxx
|
||||
id: main_lcd
|
||||
model: ili9342
|
||||
cs_pin: 3
|
||||
dc_pin: 11
|
||||
reset_pin: 10
|
||||
invert_colors: true
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -13,13 +13,4 @@ display:
|
||||
reset_pin: 16
|
||||
invert_colors: true
|
||||
|
||||
image:
|
||||
defaults:
|
||||
type: rgb565
|
||||
transparency: opaque
|
||||
byte_order: little_endian
|
||||
resize: 50x50
|
||||
dither: FloydSteinberg
|
||||
images:
|
||||
- id: test_image
|
||||
file: ../../pnglogo.png
|
||||
<<: !include common.yaml
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
sensor:
|
||||
- platform: lps22
|
||||
address: 0x5d
|
||||
update_interval: 10s
|
||||
temperature:
|
||||
name: "LPS22 Temperature"
|
||||
pressure:
|
||||
name: "LPS22 Pressure"
|
||||
@@ -1,6 +0,0 @@
|
||||
i2c:
|
||||
- id: i2c_lps22
|
||||
scl: 16
|
||||
sda: 17
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,6 +0,0 @@
|
||||
i2c:
|
||||
- id: i2c_lps22
|
||||
scl: 5
|
||||
sda: 4
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,6 +0,0 @@
|
||||
i2c:
|
||||
- id: i2c_lps22
|
||||
scl: 5
|
||||
sda: 4
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,6 +0,0 @@
|
||||
i2c:
|
||||
- id: i2c_lps22
|
||||
scl: 16
|
||||
sda: 17
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,6 +0,0 @@
|
||||
i2c:
|
||||
- id: i2c_lps22
|
||||
scl: 5
|
||||
sda: 4
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -1,6 +0,0 @@
|
||||
i2c:
|
||||
- id: i2c_lps22
|
||||
scl: 5
|
||||
sda: 4
|
||||
|
||||
<<: !include common.yaml
|
||||
@@ -78,268 +78,3 @@ pytest -s tests/integration/test_host_mode_basic.py
|
||||
- Each test gets its own temporary directory and unique port
|
||||
- Port allocation minimizes race conditions by holding the socket until just before ESPHome starts
|
||||
- Output from ESPHome processes is displayed for debugging
|
||||
|
||||
## Integration Test Writing Guide
|
||||
|
||||
### Test Patterns and Best Practices
|
||||
|
||||
#### 1. Test File Naming Convention
|
||||
- Use descriptive names: `test_{category}_{feature}.py`
|
||||
- Common categories: `host_mode`, `api`, `scheduler`, `light`, `areas_and_devices`
|
||||
- Examples:
|
||||
- `test_host_mode_basic.py` - Basic host mode functionality
|
||||
- `test_api_message_batching.py` - API message batching
|
||||
- `test_scheduler_stress.py` - Scheduler stress testing
|
||||
|
||||
#### 2. Essential Imports
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from aioesphomeapi import EntityState, SensorState
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
```
|
||||
|
||||
#### 3. Common Test Patterns
|
||||
|
||||
##### Basic Entity Test
|
||||
```python
|
||||
@pytest.mark.asyncio
|
||||
async def test_my_sensor(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test sensor functionality."""
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
# Get entity list
|
||||
entities, services = await client.list_entities_services()
|
||||
|
||||
# Find specific entity
|
||||
sensor = next((e for e in entities if e.object_id == "my_sensor"), None)
|
||||
assert sensor is not None
|
||||
```
|
||||
|
||||
##### State Subscription Pattern
|
||||
```python
|
||||
# Track state changes with futures
|
||||
loop = asyncio.get_running_loop()
|
||||
states: dict[int, EntityState] = {}
|
||||
state_future: asyncio.Future[EntityState] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
states[state.key] = state
|
||||
# Check for specific condition using isinstance
|
||||
if isinstance(state, SensorState) and state.state == expected_value:
|
||||
if not state_future.done():
|
||||
state_future.set_result(state)
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
# Wait for state with timeout
|
||||
try:
|
||||
result = await asyncio.wait_for(state_future, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
pytest.fail(f"Expected state not received. Got: {list(states.values())}")
|
||||
```
|
||||
|
||||
##### Service Execution Pattern
|
||||
```python
|
||||
# Find and execute service
|
||||
entities, services = await client.list_entities_services()
|
||||
my_service = next((s for s in services if s.name == "my_service"), None)
|
||||
assert my_service is not None
|
||||
|
||||
# Execute with parameters
|
||||
client.execute_service(my_service, {"param1": "value1", "param2": 42})
|
||||
```
|
||||
|
||||
##### Multiple Entity Tracking
|
||||
```python
|
||||
# For tests with many entities
|
||||
loop = asyncio.get_running_loop()
|
||||
entity_count = 50
|
||||
received_states: set[int] = set()
|
||||
all_states_future: asyncio.Future[bool] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
received_states.add(state.key)
|
||||
if len(received_states) >= entity_count and not all_states_future.done():
|
||||
all_states_future.set_result(True)
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
await asyncio.wait_for(all_states_future, timeout=10.0)
|
||||
```
|
||||
|
||||
#### 4. YAML Fixture Guidelines
|
||||
|
||||
##### Naming Convention
|
||||
- Match test function name: `test_my_feature` → `fixtures/my_feature.yaml`
|
||||
- Note: Remove `test_` prefix for fixture filename
|
||||
|
||||
##### Basic Structure
|
||||
```yaml
|
||||
esphome:
|
||||
name: test-name # Use kebab-case
|
||||
# Optional: areas, devices, platformio_options
|
||||
|
||||
host: # Always use host platform for integration tests
|
||||
api: # Port injected automatically
|
||||
logger:
|
||||
level: DEBUG # Optional: Set log level
|
||||
|
||||
# Component configurations
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "My Sensor"
|
||||
id: my_sensor
|
||||
lambda: return 42.0;
|
||||
update_interval: 0.1s # Fast updates for testing
|
||||
```
|
||||
|
||||
##### Advanced Features
|
||||
```yaml
|
||||
# External components for custom test code
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH # Replaced by test framework
|
||||
components: [my_test_component]
|
||||
|
||||
# Areas and devices
|
||||
esphome:
|
||||
name: test-device
|
||||
areas:
|
||||
- id: living_room
|
||||
name: "Living Room"
|
||||
- id: kitchen
|
||||
name: "Kitchen"
|
||||
parent_id: living_room
|
||||
devices:
|
||||
- id: my_device
|
||||
name: "Test Device"
|
||||
area_id: living_room
|
||||
|
||||
# API services
|
||||
api:
|
||||
services:
|
||||
- service: test_service
|
||||
variables:
|
||||
my_param: string
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Service called with: %s"
|
||||
args: [my_param.c_str()]
|
||||
```
|
||||
|
||||
#### 5. Testing Complex Scenarios
|
||||
|
||||
##### External Components
|
||||
Create C++ components in `fixtures/external_components/` for:
|
||||
- Stress testing
|
||||
- Custom entity behaviors
|
||||
- Scheduler testing
|
||||
- Memory management tests
|
||||
|
||||
##### Log Line Monitoring
|
||||
```python
|
||||
log_lines: list[str] = []
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
log_lines.append(line)
|
||||
if "expected message" in line:
|
||||
# Handle specific log messages
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=on_log_line):
|
||||
# Test implementation
|
||||
```
|
||||
|
||||
Example using futures for specific log patterns:
|
||||
```python
|
||||
import re
|
||||
|
||||
loop = asyncio.get_running_loop()
|
||||
connected_future = loop.create_future()
|
||||
service_future = loop.create_future()
|
||||
|
||||
# Patterns to match
|
||||
connected_pattern = re.compile(r"Client .* connected from")
|
||||
service_pattern = re.compile(r"Service called")
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
if not connected_future.done() and connected_pattern.search(line):
|
||||
connected_future.set_result(True)
|
||||
elif not service_future.done() and service_pattern.search(line):
|
||||
service_future.set_result(True)
|
||||
|
||||
async with run_compiled(yaml_config, line_callback=check_output):
|
||||
async with api_client_connected() as client:
|
||||
# Wait for specific log message
|
||||
await asyncio.wait_for(connected_future, timeout=5.0)
|
||||
|
||||
# Do test actions...
|
||||
|
||||
# Wait for service log
|
||||
await asyncio.wait_for(service_future, timeout=5.0)
|
||||
```
|
||||
|
||||
**Note**: Tests that monitor log messages typically have fewer race conditions compared to state-based testing, making them more reliable. However, be aware that the host platform currently does not have a thread-safe logger, so logging from threads will not work correctly.
|
||||
|
||||
##### Timeout Handling
|
||||
```python
|
||||
# Always use timeouts for async operations
|
||||
try:
|
||||
result = await asyncio.wait_for(some_future, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
pytest.fail("Operation timed out - check test expectations")
|
||||
```
|
||||
|
||||
#### 6. Common Assertions
|
||||
|
||||
```python
|
||||
# Device info
|
||||
assert device_info.name == "expected-name"
|
||||
assert device_info.compilation_time is not None
|
||||
|
||||
# Entity properties
|
||||
assert sensor.accuracy_decimals == 2
|
||||
assert sensor.state_class == 1 # measurement
|
||||
assert sensor.force_update is True
|
||||
|
||||
# Service availability
|
||||
assert len(services) > 0
|
||||
assert any(s.name == "expected_service" for s in services)
|
||||
|
||||
# State values
|
||||
assert state.state == expected_value
|
||||
assert state.missing_state is False
|
||||
```
|
||||
|
||||
#### 7. Debugging Tips
|
||||
|
||||
- Use `pytest -s` to see ESPHome output during tests
|
||||
- Add descriptive failure messages to assertions
|
||||
- Use `pytest.fail()` with detailed error info for timeouts
|
||||
- Check `log_lines` for compilation or runtime errors
|
||||
- Enable debug logging in YAML fixtures when needed
|
||||
|
||||
#### 8. Performance Considerations
|
||||
|
||||
- Use short update intervals (0.1s) for faster tests
|
||||
- Set reasonable timeouts (5-10s for most operations)
|
||||
- Batch multiple assertions when possible
|
||||
- Clean up resources properly using context managers
|
||||
|
||||
#### 9. Test Categories
|
||||
|
||||
- **Basic Tests**: Minimal functionality verification
|
||||
- **Entity Tests**: Sensor, switch, light behavior
|
||||
- **API Tests**: Message batching, services, events
|
||||
- **Scheduler Tests**: Timing, defer operations, stress
|
||||
- **Memory Tests**: Conditional compilation, optimization
|
||||
- **Integration Tests**: Areas, devices, complex interactions
|
||||
|
||||
@@ -165,19 +165,6 @@ async def compile_esphome(
|
||||
"""Compile an ESPHome configuration and return the binary path."""
|
||||
|
||||
async def _compile(config_path: Path) -> Path:
|
||||
# Create a unique PlatformIO directory for this test to avoid race conditions
|
||||
platformio_dir = integration_test_dir / ".platformio"
|
||||
platformio_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create cache directory as well
|
||||
platformio_cache_dir = platformio_dir / ".cache"
|
||||
platformio_cache_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Set up environment with isolated PlatformIO directories
|
||||
env = os.environ.copy()
|
||||
env["PLATFORMIO_CORE_DIR"] = str(platformio_dir)
|
||||
env["PLATFORMIO_CACHE_DIR"] = str(platformio_cache_dir)
|
||||
|
||||
# Retry compilation up to 3 times if we get a segfault
|
||||
max_retries = 3
|
||||
for attempt in range(max_retries):
|
||||
@@ -192,7 +179,6 @@ async def compile_esphome(
|
||||
stdin=asyncio.subprocess.DEVNULL,
|
||||
# Start in a new process group to isolate signal handling
|
||||
start_new_session=True,
|
||||
env=env,
|
||||
)
|
||||
await proc.wait()
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ api:
|
||||
- action: test_simple_service
|
||||
then:
|
||||
- logger.log: "Simple service called"
|
||||
- binary_sensor.template.publish:
|
||||
id: service_called_sensor
|
||||
state: ON
|
||||
- action: test_service_with_args
|
||||
variables:
|
||||
arg_string: string
|
||||
@@ -16,14 +19,53 @@ api:
|
||||
- logger.log:
|
||||
format: "Service called with: %s, %d, %d, %.2f"
|
||||
args: [arg_string.c_str(), arg_int, arg_bool, arg_float]
|
||||
- sensor.template.publish:
|
||||
id: service_arg_sensor
|
||||
state: !lambda 'return arg_float;'
|
||||
on_client_connected:
|
||||
- logger.log:
|
||||
format: "Client %s connected from %s"
|
||||
args: [client_info.c_str(), client_address.c_str()]
|
||||
- binary_sensor.template.publish:
|
||||
id: client_connected
|
||||
state: ON
|
||||
- text_sensor.template.publish:
|
||||
id: last_client_info
|
||||
state: !lambda 'return client_info;'
|
||||
on_client_disconnected:
|
||||
- logger.log:
|
||||
format: "Client %s disconnected from %s"
|
||||
args: [client_info.c_str(), client_address.c_str()]
|
||||
- binary_sensor.template.publish:
|
||||
id: client_connected
|
||||
state: OFF
|
||||
- binary_sensor.template.publish:
|
||||
id: client_disconnected_event
|
||||
state: ON
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
name: "Client Connected"
|
||||
id: client_connected
|
||||
device_class: connectivity
|
||||
- platform: template
|
||||
name: "Client Disconnected Event"
|
||||
id: client_disconnected_event
|
||||
- platform: template
|
||||
name: "Service Called"
|
||||
id: service_called_sensor
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "Service Argument Value"
|
||||
id: service_arg_sensor
|
||||
unit_of_measurement: ""
|
||||
accuracy_decimals: 2
|
||||
|
||||
text_sensor:
|
||||
- platform: template
|
||||
name: "Last Client Info"
|
||||
id: last_client_info
|
||||
|
||||
109
tests/integration/fixtures/defer_fifo_simple.yaml
Normal file
109
tests/integration/fixtures/defer_fifo_simple.yaml
Normal file
@@ -0,0 +1,109 @@
|
||||
esphome:
|
||||
name: defer-fifo-simple
|
||||
|
||||
host:
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
api:
|
||||
services:
|
||||
- service: test_set_timeout
|
||||
then:
|
||||
- lambda: |-
|
||||
// Test set_timeout with 0 delay (direct scheduler call)
|
||||
static int set_timeout_order = 0;
|
||||
static bool set_timeout_passed = true;
|
||||
|
||||
// Reset for this test
|
||||
set_timeout_order = 0;
|
||||
set_timeout_passed = true;
|
||||
|
||||
ESP_LOGD("defer_test", "Testing set_timeout(0) for FIFO order...");
|
||||
for (int i = 0; i < 10; i++) {
|
||||
int expected = i;
|
||||
App.scheduler.set_timeout((Component*)nullptr, nullptr, 0, [expected]() {
|
||||
ESP_LOGD("defer_test", "set_timeout(0) item %d executed, order %d", expected, set_timeout_order);
|
||||
if (set_timeout_order != expected) {
|
||||
ESP_LOGE("defer_test", "FIFO violation in set_timeout: expected %d but got execution order %d", expected, set_timeout_order);
|
||||
set_timeout_passed = false;
|
||||
}
|
||||
set_timeout_order++;
|
||||
|
||||
if (set_timeout_order == 10) {
|
||||
if (set_timeout_passed) {
|
||||
ESP_LOGI("defer_test", "✓ Test PASSED - set_timeout(0) maintains FIFO order");
|
||||
id(test_result)->trigger("passed");
|
||||
} else {
|
||||
ESP_LOGE("defer_test", "✗ Test FAILED - set_timeout(0) executed out of order");
|
||||
id(test_result)->trigger("failed");
|
||||
}
|
||||
id(test_complete)->trigger("test_finished");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ESP_LOGD("defer_test", "Deferred 10 items using set_timeout(0), waiting for execution...");
|
||||
|
||||
- service: test_defer
|
||||
then:
|
||||
- lambda: |-
|
||||
// Test defer() method (component method)
|
||||
static int defer_order = 0;
|
||||
static bool defer_passed = true;
|
||||
|
||||
// Reset for this test
|
||||
defer_order = 0;
|
||||
defer_passed = true;
|
||||
|
||||
ESP_LOGD("defer_test", "Testing defer() for FIFO order...");
|
||||
|
||||
// Create a test component class that exposes defer()
|
||||
class TestComponent : public Component {
|
||||
public:
|
||||
void test_defer() {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
int expected = i;
|
||||
this->defer([expected]() {
|
||||
ESP_LOGD("defer_test", "defer() item %d executed, order %d", expected, defer_order);
|
||||
if (defer_order != expected) {
|
||||
ESP_LOGE("defer_test", "FIFO violation in defer: expected %d but got execution order %d", expected, defer_order);
|
||||
defer_passed = false;
|
||||
}
|
||||
defer_order++;
|
||||
|
||||
if (defer_order == 10) {
|
||||
if (defer_passed) {
|
||||
ESP_LOGI("defer_test", "✓ Test PASSED - defer() maintains FIFO order");
|
||||
id(test_result)->trigger("passed");
|
||||
} else {
|
||||
ESP_LOGE("defer_test", "✗ Test FAILED - defer() executed out of order");
|
||||
id(test_result)->trigger("failed");
|
||||
}
|
||||
id(test_complete)->trigger("test_finished");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Use a static instance so it doesn't go out of scope
|
||||
static TestComponent test_component;
|
||||
test_component.test_defer();
|
||||
|
||||
ESP_LOGD("defer_test", "Deferred 10 items using defer(), waiting for execution...");
|
||||
|
||||
event:
|
||||
- platform: template
|
||||
name: "Test Complete"
|
||||
id: test_complete
|
||||
device_class: button
|
||||
event_types:
|
||||
- "test_finished"
|
||||
- platform: template
|
||||
name: "Test Result"
|
||||
id: test_result
|
||||
device_class: button
|
||||
event_types:
|
||||
- "passed"
|
||||
- "failed"
|
||||
38
tests/integration/fixtures/defer_stress.yaml
Normal file
38
tests/integration/fixtures/defer_stress.yaml
Normal file
@@ -0,0 +1,38 @@
|
||||
esphome:
|
||||
name: defer-stress-test
|
||||
|
||||
external_components:
|
||||
- source:
|
||||
type: local
|
||||
path: EXTERNAL_COMPONENT_PATH
|
||||
components: [defer_stress_component]
|
||||
|
||||
host:
|
||||
|
||||
logger:
|
||||
level: VERBOSE
|
||||
|
||||
defer_stress_component:
|
||||
id: defer_stress
|
||||
|
||||
api:
|
||||
services:
|
||||
- service: run_stress_test
|
||||
then:
|
||||
- lambda: |-
|
||||
id(defer_stress)->run_multi_thread_test();
|
||||
|
||||
event:
|
||||
- platform: template
|
||||
name: "Test Complete"
|
||||
id: test_complete
|
||||
device_class: button
|
||||
event_types:
|
||||
- "test_finished"
|
||||
- platform: template
|
||||
name: "Test Result"
|
||||
id: test_result
|
||||
device_class: button
|
||||
event_types:
|
||||
- "passed"
|
||||
- "failed"
|
||||
@@ -26,6 +26,7 @@ void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
|
||||
|
||||
// Schedule final check
|
||||
this->set_timeout("final_check", 200, [this]() {
|
||||
ESP_LOGI(TAG, "String lifetime tests complete");
|
||||
ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_);
|
||||
ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_);
|
||||
|
||||
@@ -34,7 +35,6 @@ void SchedulerStringLifetimeComponent::run_string_lifetime_test() {
|
||||
} else {
|
||||
ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_);
|
||||
}
|
||||
ESP_LOGI(TAG, "String lifetime tests complete");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from aioesphomeapi import UserService, UserServiceArgType
|
||||
from aioesphomeapi import (
|
||||
BinarySensorInfo,
|
||||
EntityState,
|
||||
SensorInfo,
|
||||
TextSensorInfo,
|
||||
UserService,
|
||||
UserServiceArgType,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
@@ -19,45 +25,50 @@ async def test_api_conditional_memory(
|
||||
) -> None:
|
||||
"""Test API triggers and services work correctly with conditional compilation."""
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Track log messages
|
||||
connected_future = loop.create_future()
|
||||
disconnected_future = loop.create_future()
|
||||
service_simple_future = loop.create_future()
|
||||
service_args_future = loop.create_future()
|
||||
|
||||
# Patterns to match in logs
|
||||
connected_pattern = re.compile(r"Client .* connected from")
|
||||
disconnected_pattern = re.compile(r"Client .* disconnected from")
|
||||
service_simple_pattern = re.compile(r"Simple service called")
|
||||
service_args_pattern = re.compile(
|
||||
r"Service called with: test_string, 123, 1, 42\.50"
|
||||
)
|
||||
|
||||
def check_output(line: str) -> None:
|
||||
"""Check log output for expected messages."""
|
||||
if not connected_future.done() and connected_pattern.search(line):
|
||||
connected_future.set_result(True)
|
||||
elif not disconnected_future.done() and disconnected_pattern.search(line):
|
||||
disconnected_future.set_result(True)
|
||||
elif not service_simple_future.done() and service_simple_pattern.search(line):
|
||||
service_simple_future.set_result(True)
|
||||
elif not service_args_future.done() and service_args_pattern.search(line):
|
||||
service_args_future.set_result(True)
|
||||
|
||||
# Run with log monitoring
|
||||
async with run_compiled(yaml_config, line_callback=check_output):
|
||||
# Keep ESPHome process running throughout the test
|
||||
async with run_compiled(yaml_config):
|
||||
# First connection
|
||||
async with api_client_connected() as client:
|
||||
# Verify device info
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "api-conditional-memory-test"
|
||||
|
||||
# Wait for connection log
|
||||
await asyncio.wait_for(connected_future, timeout=5.0)
|
||||
# List entities and services
|
||||
entity_info, services = await asyncio.wait_for(
|
||||
client.list_entities_services(), timeout=5.0
|
||||
)
|
||||
|
||||
# List services
|
||||
_, services = await client.list_entities_services()
|
||||
# Find our entities
|
||||
client_connected: BinarySensorInfo | None = None
|
||||
client_disconnected_event: BinarySensorInfo | None = None
|
||||
service_called_sensor: BinarySensorInfo | None = None
|
||||
service_arg_sensor: SensorInfo | None = None
|
||||
last_client_info: TextSensorInfo | None = None
|
||||
|
||||
for entity in entity_info:
|
||||
if isinstance(entity, BinarySensorInfo):
|
||||
if entity.object_id == "client_connected":
|
||||
client_connected = entity
|
||||
elif entity.object_id == "client_disconnected_event":
|
||||
client_disconnected_event = entity
|
||||
elif entity.object_id == "service_called":
|
||||
service_called_sensor = entity
|
||||
elif isinstance(entity, SensorInfo):
|
||||
if entity.object_id == "service_argument_value":
|
||||
service_arg_sensor = entity
|
||||
elif isinstance(entity, TextSensorInfo):
|
||||
if entity.object_id == "last_client_info":
|
||||
last_client_info = entity
|
||||
|
||||
# Verify all entities exist
|
||||
assert client_connected is not None, "client_connected sensor not found"
|
||||
assert client_disconnected_event is not None, (
|
||||
"client_disconnected_event sensor not found"
|
||||
)
|
||||
assert service_called_sensor is not None, "service_called sensor not found"
|
||||
assert service_arg_sensor is not None, "service_arg_sensor not found"
|
||||
assert last_client_info is not None, "last_client_info sensor not found"
|
||||
|
||||
# Verify services exist
|
||||
assert len(services) == 2, f"Expected 2 services, found {len(services)}"
|
||||
@@ -87,11 +98,66 @@ async def test_api_conditional_memory(
|
||||
assert arg_types["arg_bool"] == UserServiceArgType.BOOL
|
||||
assert arg_types["arg_float"] == UserServiceArgType.FLOAT
|
||||
|
||||
# Track state changes
|
||||
states: dict[int, EntityState] = {}
|
||||
states_future: asyncio.Future[None] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
states[state.key] = state
|
||||
# Check if we have initial states for connection sensors
|
||||
if (
|
||||
client_connected.key in states
|
||||
and last_client_info.key in states
|
||||
and not states_future.done()
|
||||
):
|
||||
states_future.set_result(None)
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
# Wait for initial states
|
||||
await asyncio.wait_for(states_future, timeout=5.0)
|
||||
|
||||
# Verify on_client_connected trigger fired
|
||||
connected_state = states.get(client_connected.key)
|
||||
assert connected_state is not None
|
||||
assert connected_state.state is True, "Client should be connected"
|
||||
|
||||
# Verify client info was captured
|
||||
client_info_state = states.get(last_client_info.key)
|
||||
assert client_info_state is not None
|
||||
assert isinstance(client_info_state.state, str)
|
||||
assert len(client_info_state.state) > 0, "Client info should not be empty"
|
||||
|
||||
# Test simple service
|
||||
service_future: asyncio.Future[None] = loop.create_future()
|
||||
|
||||
def check_service_called(state: EntityState) -> None:
|
||||
if state.key == service_called_sensor.key and state.state is True:
|
||||
if not service_future.done():
|
||||
service_future.set_result(None)
|
||||
|
||||
# Update callback to check for service execution
|
||||
client.subscribe_states(check_service_called)
|
||||
|
||||
# Call simple service
|
||||
client.execute_service(simple_service, {})
|
||||
|
||||
# Wait for service log
|
||||
await asyncio.wait_for(service_simple_future, timeout=5.0)
|
||||
# Wait for service to execute
|
||||
await asyncio.wait_for(service_future, timeout=5.0)
|
||||
|
||||
# Test service with arguments
|
||||
arg_future: asyncio.Future[None] = loop.create_future()
|
||||
expected_float = 42.5
|
||||
|
||||
def check_arg_sensor(state: EntityState) -> None:
|
||||
if (
|
||||
state.key == service_arg_sensor.key
|
||||
and abs(state.state - expected_float) < 0.01
|
||||
):
|
||||
if not arg_future.done():
|
||||
arg_future.set_result(None)
|
||||
|
||||
client.subscribe_states(check_arg_sensor)
|
||||
|
||||
# Call service with arguments
|
||||
client.execute_service(
|
||||
@@ -100,12 +166,43 @@ async def test_api_conditional_memory(
|
||||
"arg_string": "test_string",
|
||||
"arg_int": 123,
|
||||
"arg_bool": True,
|
||||
"arg_float": 42.5,
|
||||
"arg_float": expected_float,
|
||||
},
|
||||
)
|
||||
|
||||
# Wait for service with args log
|
||||
await asyncio.wait_for(service_args_future, timeout=5.0)
|
||||
# Wait for service with args to execute
|
||||
await asyncio.wait_for(arg_future, timeout=5.0)
|
||||
|
||||
# Client disconnected here, wait for disconnect log
|
||||
await asyncio.wait_for(disconnected_future, timeout=5.0)
|
||||
# After disconnecting first client, reconnect and verify triggers work
|
||||
async with api_client_connected() as client2:
|
||||
# Subscribe to states with new client
|
||||
states2: dict[int, EntityState] = {}
|
||||
states_ready_future: asyncio.Future[None] = loop.create_future()
|
||||
|
||||
def on_state2(state: EntityState) -> None:
|
||||
states2[state.key] = state
|
||||
# Check if we have received both required states
|
||||
if (
|
||||
client_connected.key in states2
|
||||
and client_disconnected_event.key in states2
|
||||
and not states_ready_future.done()
|
||||
):
|
||||
states_ready_future.set_result(None)
|
||||
|
||||
client2.subscribe_states(on_state2)
|
||||
|
||||
# Wait for both connected and disconnected event states
|
||||
await asyncio.wait_for(states_ready_future, timeout=5.0)
|
||||
|
||||
# Verify client is connected again (on_client_connected fired)
|
||||
assert states2[client_connected.key].state is True, (
|
||||
"Client should be reconnected"
|
||||
)
|
||||
|
||||
# The client_disconnected_event should be ON from when we disconnected
|
||||
# (it was set ON by on_client_disconnected trigger)
|
||||
disconnected_state = states2.get(client_disconnected_event.key)
|
||||
assert disconnected_state is not None
|
||||
assert disconnected_state.state is True, (
|
||||
"Disconnect event should be ON from previous disconnect"
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from aioesphomeapi import LogLevel, SensorInfo
|
||||
from aioesphomeapi import LogLevel
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
@@ -63,7 +63,7 @@ async def test_api_vv_logging(
|
||||
entity_info, _ = await client.list_entities_services()
|
||||
|
||||
# Count sensors
|
||||
sensor_count = sum(1 for e in entity_info if isinstance(e, SensorInfo))
|
||||
sensor_count = sum(1 for e in entity_info if hasattr(e, "unit_of_measurement"))
|
||||
assert sensor_count >= 10, f"Expected at least 10 sensors, got {sensor_count}"
|
||||
|
||||
# Wait for sensor updates to flow with VV logging active
|
||||
|
||||
@@ -76,8 +76,8 @@ async def test_areas_and_devices(
|
||||
# Get entity list to verify device_id mapping
|
||||
entities = await client.list_entities_services()
|
||||
|
||||
# Collect sensor entities (all entities have device_id)
|
||||
sensor_entities = entities[0]
|
||||
# Collect sensor entities
|
||||
sensor_entities = [e for e in entities[0] if hasattr(e, "device_id")]
|
||||
assert len(sensor_entities) >= 4, (
|
||||
f"Expected at least 4 sensor entities, got {len(sensor_entities)}"
|
||||
)
|
||||
|
||||
117
tests/integration/test_defer_fifo_simple.py
Normal file
117
tests/integration/test_defer_fifo_simple.py
Normal file
@@ -0,0 +1,117 @@
|
||||
"""Simple test that defer() maintains FIFO order."""
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import EntityState, Event, EventInfo, UserService
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_defer_fifo_simple(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that defer() maintains FIFO order with a simple test."""
|
||||
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
# Verify we can connect
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "defer-fifo-simple"
|
||||
|
||||
# List entities and services
|
||||
entity_info, services = await asyncio.wait_for(
|
||||
client.list_entities_services(), timeout=5.0
|
||||
)
|
||||
|
||||
# Find our test entities
|
||||
test_complete_entity: EventInfo | None = None
|
||||
test_result_entity: EventInfo | None = None
|
||||
|
||||
for entity in entity_info:
|
||||
if isinstance(entity, EventInfo):
|
||||
if entity.object_id == "test_complete":
|
||||
test_complete_entity = entity
|
||||
elif entity.object_id == "test_result":
|
||||
test_result_entity = entity
|
||||
|
||||
assert test_complete_entity is not None, "test_complete event not found"
|
||||
assert test_result_entity is not None, "test_result event not found"
|
||||
|
||||
# Find our test services
|
||||
test_set_timeout_service: UserService | None = None
|
||||
test_defer_service: UserService | None = None
|
||||
for service in services:
|
||||
if service.name == "test_set_timeout":
|
||||
test_set_timeout_service = service
|
||||
elif service.name == "test_defer":
|
||||
test_defer_service = service
|
||||
|
||||
assert test_set_timeout_service is not None, (
|
||||
"test_set_timeout service not found"
|
||||
)
|
||||
assert test_defer_service is not None, "test_defer service not found"
|
||||
|
||||
# Get the event loop
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
# Subscribe to states
|
||||
# (events are delivered as EventStates through subscribe_states)
|
||||
test_complete_future: asyncio.Future[bool] = loop.create_future()
|
||||
test_result_future: asyncio.Future[bool] = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if not isinstance(state, Event):
|
||||
return
|
||||
|
||||
if (
|
||||
state.key == test_complete_entity.key
|
||||
and state.event_type == "test_finished"
|
||||
and not test_complete_future.done()
|
||||
):
|
||||
test_complete_future.set_result(True)
|
||||
return
|
||||
|
||||
if state.key == test_result_entity.key and not test_result_future.done():
|
||||
if state.event_type == "passed":
|
||||
test_result_future.set_result(True)
|
||||
elif state.event_type == "failed":
|
||||
test_result_future.set_result(False)
|
||||
|
||||
client.subscribe_states(on_state)
|
||||
|
||||
# Test 1: Test set_timeout(0)
|
||||
client.execute_service(test_set_timeout_service, {})
|
||||
|
||||
# Wait for first test completion
|
||||
try:
|
||||
await asyncio.wait_for(test_complete_future, timeout=5.0)
|
||||
test1_passed = await asyncio.wait_for(test_result_future, timeout=1.0)
|
||||
except asyncio.TimeoutError:
|
||||
pytest.fail("Test set_timeout(0) did not complete within 5 seconds")
|
||||
|
||||
assert test1_passed is True, (
|
||||
"set_timeout(0) FIFO test failed - items executed out of order"
|
||||
)
|
||||
|
||||
# Reset futures for second test
|
||||
test_complete_future = loop.create_future()
|
||||
test_result_future = loop.create_future()
|
||||
|
||||
# Test 2: Test defer()
|
||||
client.execute_service(test_defer_service, {})
|
||||
|
||||
# Wait for second test completion
|
||||
try:
|
||||
await asyncio.wait_for(test_complete_future, timeout=5.0)
|
||||
test2_passed = await asyncio.wait_for(test_result_future, timeout=1.0)
|
||||
except asyncio.TimeoutError:
|
||||
pytest.fail("Test defer() did not complete within 5 seconds")
|
||||
|
||||
# Verify the test passed
|
||||
assert test2_passed is True, (
|
||||
"defer() FIFO test failed - items executed out of order"
|
||||
)
|
||||
137
tests/integration/test_defer_stress.py
Normal file
137
tests/integration/test_defer_stress.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Stress test for defer() thread safety with multiple threads."""
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from aioesphomeapi import UserService
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_defer_stress(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
"""Test that defer() doesn't crash when called rapidly from multiple threads."""
|
||||
|
||||
# Get the absolute path to the external components directory
|
||||
external_components_path = str(
|
||||
Path(__file__).parent / "fixtures" / "external_components"
|
||||
)
|
||||
|
||||
# Replace the placeholder in the YAML config with the actual path
|
||||
yaml_config = yaml_config.replace(
|
||||
"EXTERNAL_COMPONENT_PATH", external_components_path
|
||||
)
|
||||
|
||||
# Create a future to signal test completion
|
||||
loop = asyncio.get_event_loop()
|
||||
test_complete_future: asyncio.Future[None] = loop.create_future()
|
||||
|
||||
# Track executed defers and their order
|
||||
executed_defers: set[int] = set()
|
||||
thread_executions: dict[
|
||||
int, list[int]
|
||||
] = {} # thread_id -> list of indices in execution order
|
||||
fifo_violations: list[str] = []
|
||||
|
||||
def on_log_line(line: str) -> None:
|
||||
# Track all executed defers with thread and index info
|
||||
match = re.search(r"Executed defer (\d+) \(thread (\d+), index (\d+)\)", line)
|
||||
if not match:
|
||||
return
|
||||
|
||||
defer_id = int(match.group(1))
|
||||
thread_id = int(match.group(2))
|
||||
index = int(match.group(3))
|
||||
|
||||
executed_defers.add(defer_id)
|
||||
|
||||
# Track execution order per thread
|
||||
if thread_id not in thread_executions:
|
||||
thread_executions[thread_id] = []
|
||||
|
||||
# Check FIFO ordering within thread
|
||||
if thread_executions[thread_id] and thread_executions[thread_id][-1] >= index:
|
||||
fifo_violations.append(
|
||||
f"Thread {thread_id}: index {index} executed after "
|
||||
f"{thread_executions[thread_id][-1]}"
|
||||
)
|
||||
|
||||
thread_executions[thread_id].append(index)
|
||||
|
||||
# Check if we've executed all 1000 defers (0-999)
|
||||
if len(executed_defers) == 1000 and not test_complete_future.done():
|
||||
test_complete_future.set_result(None)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config, line_callback=on_log_line),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Verify we can connect
|
||||
device_info = await client.device_info()
|
||||
assert device_info is not None
|
||||
assert device_info.name == "defer-stress-test"
|
||||
|
||||
# List entities and services
|
||||
entity_info, services = await asyncio.wait_for(
|
||||
client.list_entities_services(), timeout=5.0
|
||||
)
|
||||
|
||||
# Find our test service
|
||||
run_stress_test_service: UserService | None = None
|
||||
for service in services:
|
||||
if service.name == "run_stress_test":
|
||||
run_stress_test_service = service
|
||||
break
|
||||
|
||||
assert run_stress_test_service is not None, "run_stress_test service not found"
|
||||
|
||||
# Call the run_stress_test service to start the test
|
||||
client.execute_service(run_stress_test_service, {})
|
||||
|
||||
# Wait for all defers to execute (should be quick)
|
||||
try:
|
||||
await asyncio.wait_for(test_complete_future, timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
# Report how many we got
|
||||
pytest.fail(
|
||||
f"Stress test timed out. Only {len(executed_defers)} of "
|
||||
f"1000 defers executed. Missing IDs: "
|
||||
f"{sorted(set(range(1000)) - executed_defers)[:10]}..."
|
||||
)
|
||||
|
||||
# Verify all defers executed
|
||||
assert len(executed_defers) == 1000, (
|
||||
f"Expected 1000 defers, got {len(executed_defers)}"
|
||||
)
|
||||
|
||||
# Verify we have all IDs from 0-999
|
||||
expected_ids = set(range(1000))
|
||||
missing_ids = expected_ids - executed_defers
|
||||
assert not missing_ids, f"Missing defer IDs: {sorted(missing_ids)}"
|
||||
|
||||
# Verify FIFO ordering was maintained within each thread
|
||||
assert not fifo_violations, "FIFO ordering violations detected:\n" + "\n".join(
|
||||
fifo_violations[:10]
|
||||
)
|
||||
|
||||
# Verify each thread executed all its defers in order
|
||||
for thread_id, indices in thread_executions.items():
|
||||
assert len(indices) == 100, (
|
||||
f"Thread {thread_id} executed {len(indices)} defers, expected 100"
|
||||
)
|
||||
# Indices should be 0-99 in ascending order
|
||||
assert indices == list(range(100)), (
|
||||
f"Thread {thread_id} executed indices out of order: {indices[:10]}..."
|
||||
)
|
||||
|
||||
# If we got here without crashing and with proper ordering, the test passed
|
||||
assert True, (
|
||||
"Test completed successfully - all 1000 defers executed with "
|
||||
"FIFO ordering preserved"
|
||||
)
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import BinarySensorState, EntityState, SensorState, TextSensorState
|
||||
from aioesphomeapi import EntityState
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
@@ -40,22 +40,28 @@ async def test_device_id_in_state(
|
||||
entity_device_mapping: dict[int, int] = {}
|
||||
|
||||
for entity in all_entities:
|
||||
# All entities have name and key attributes
|
||||
if entity.name == "Temperature":
|
||||
entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
|
||||
elif entity.name == "Humidity":
|
||||
entity_device_mapping[entity.key] = device_ids["Humidity Monitor"]
|
||||
elif entity.name == "Motion Detected":
|
||||
entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
|
||||
elif entity.name == "Temperature Monitor Power":
|
||||
entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
|
||||
elif entity.name == "Temperature Status":
|
||||
entity_device_mapping[entity.key] = device_ids["Temperature Monitor"]
|
||||
elif entity.name == "Motion Light":
|
||||
entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
|
||||
elif entity.name == "No Device Sensor":
|
||||
# Entity without device_id should have device_id 0
|
||||
entity_device_mapping[entity.key] = 0
|
||||
if hasattr(entity, "name") and hasattr(entity, "key"):
|
||||
if entity.name == "Temperature":
|
||||
entity_device_mapping[entity.key] = device_ids[
|
||||
"Temperature Monitor"
|
||||
]
|
||||
elif entity.name == "Humidity":
|
||||
entity_device_mapping[entity.key] = device_ids["Humidity Monitor"]
|
||||
elif entity.name == "Motion Detected":
|
||||
entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
|
||||
elif entity.name == "Temperature Monitor Power":
|
||||
entity_device_mapping[entity.key] = device_ids[
|
||||
"Temperature Monitor"
|
||||
]
|
||||
elif entity.name == "Temperature Status":
|
||||
entity_device_mapping[entity.key] = device_ids[
|
||||
"Temperature Monitor"
|
||||
]
|
||||
elif entity.name == "Motion Light":
|
||||
entity_device_mapping[entity.key] = device_ids["Motion Sensor"]
|
||||
elif entity.name == "No Device Sensor":
|
||||
# Entity without device_id should have device_id 0
|
||||
entity_device_mapping[entity.key] = 0
|
||||
|
||||
assert len(entity_device_mapping) >= 6, (
|
||||
f"Expected at least 6 mapped entities, got {len(entity_device_mapping)}"
|
||||
@@ -105,7 +111,7 @@ async def test_device_id_in_state(
|
||||
(
|
||||
s
|
||||
for s in states.values()
|
||||
if isinstance(s, SensorState)
|
||||
if hasattr(s, "state")
|
||||
and isinstance(s.state, float)
|
||||
and s.device_id != 0
|
||||
),
|
||||
@@ -116,7 +122,11 @@ async def test_device_id_in_state(
|
||||
|
||||
# Find a binary sensor state
|
||||
binary_sensor_state = next(
|
||||
(s for s in states.values() if isinstance(s, BinarySensorState)),
|
||||
(
|
||||
s
|
||||
for s in states.values()
|
||||
if hasattr(s, "state") and isinstance(s.state, bool)
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert binary_sensor_state is not None, "No binary sensor state found"
|
||||
@@ -126,7 +136,11 @@ async def test_device_id_in_state(
|
||||
|
||||
# Find a text sensor state
|
||||
text_sensor_state = next(
|
||||
(s for s in states.values() if isinstance(s, TextSensorState)),
|
||||
(
|
||||
s
|
||||
for s in states.values()
|
||||
if hasattr(s, "state") and isinstance(s.state, str)
|
||||
),
|
||||
None,
|
||||
)
|
||||
assert text_sensor_state is not None, "No text sensor state found"
|
||||
|
||||
@@ -51,6 +51,9 @@ async def test_entity_icon(
|
||||
entity = entity_map[entity_name]
|
||||
|
||||
# Check icon field
|
||||
assert hasattr(entity, "icon"), (
|
||||
f"{entity_name}: Entity should have icon attribute"
|
||||
)
|
||||
assert entity.icon == expected_icon, (
|
||||
f"{entity_name}: icon mismatch - "
|
||||
f"expected '{expected_icon}', got '{entity.icon}'"
|
||||
@@ -64,6 +67,9 @@ async def test_entity_icon(
|
||||
entity = entity_map[entity_name]
|
||||
|
||||
# Check icon field is empty
|
||||
assert hasattr(entity, "icon"), (
|
||||
f"{entity_name}: Entity should have icon attribute"
|
||||
)
|
||||
assert entity.icon == "", (
|
||||
f"{entity_name}: icon should be empty string for entities without icons, "
|
||||
f"got '{entity.icon}'"
|
||||
|
||||
@@ -25,8 +25,8 @@ async def test_host_mode_entity_fields(
|
||||
# Create a map of entity names to entity info
|
||||
entity_map = {}
|
||||
for entity in entities[0]:
|
||||
# All entities should have a name attribute
|
||||
entity_map[entity.name] = entity
|
||||
if hasattr(entity, "name"):
|
||||
entity_map[entity.name] = entity
|
||||
|
||||
# Test entities that should be visible via API (non-internal)
|
||||
visible_test_cases = [
|
||||
|
||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import EntityState, SensorState
|
||||
from aioesphomeapi import EntityState
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
@@ -30,7 +30,7 @@ async def test_host_mode_many_entities(
|
||||
sensor_states = [
|
||||
s
|
||||
for s in states.values()
|
||||
if isinstance(s, SensorState) and isinstance(s.state, float)
|
||||
if hasattr(s, "state") and isinstance(s.state, float)
|
||||
]
|
||||
# When we have received states from at least 50 sensors, resolve the future
|
||||
if len(sensor_states) >= 50 and not sensor_count_future.done():
|
||||
@@ -45,7 +45,7 @@ async def test_host_mode_many_entities(
|
||||
sensor_states = [
|
||||
s
|
||||
for s in states.values()
|
||||
if isinstance(s, SensorState) and isinstance(s.state, float)
|
||||
if hasattr(s, "state") and isinstance(s.state, float)
|
||||
]
|
||||
pytest.fail(
|
||||
f"Did not receive states from at least 50 sensors within 10 seconds. "
|
||||
@@ -61,7 +61,7 @@ async def test_host_mode_many_entities(
|
||||
sensor_states = [
|
||||
s
|
||||
for s in states.values()
|
||||
if isinstance(s, SensorState) and isinstance(s.state, float)
|
||||
if hasattr(s, "state") and isinstance(s.state, float)
|
||||
]
|
||||
|
||||
assert sensor_count >= 50, (
|
||||
|
||||
@@ -19,17 +19,16 @@ async def test_host_mode_with_sensor(
|
||||
) -> None:
|
||||
"""Test Host mode with a sensor component."""
|
||||
# Write, compile and run the ESPHome device, then connect to API
|
||||
loop = asyncio.get_running_loop()
|
||||
async with run_compiled(yaml_config), api_client_connected() as client:
|
||||
# Subscribe to state changes
|
||||
states: dict[int, EntityState] = {}
|
||||
sensor_future: asyncio.Future[EntityState] = loop.create_future()
|
||||
sensor_future: asyncio.Future[EntityState] = asyncio.Future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
states[state.key] = state
|
||||
# If this is our sensor with value 42.0, resolve the future
|
||||
if (
|
||||
isinstance(state, aioesphomeapi.SensorState)
|
||||
hasattr(state, "state")
|
||||
and state.state == 42.0
|
||||
and not sensor_future.done()
|
||||
):
|
||||
|
||||
@@ -7,7 +7,6 @@ including RGB, color temperature, effects, transitions, and flash.
|
||||
import asyncio
|
||||
from typing import Any
|
||||
|
||||
from aioesphomeapi import LightState
|
||||
import pytest
|
||||
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
@@ -77,7 +76,7 @@ async def test_light_calls(
|
||||
client.light_command(key=rgbcw_light.key, white=0.6)
|
||||
state = await wait_for_state_change(rgbcw_light.key)
|
||||
# White might need more tolerance or might not be directly settable
|
||||
if isinstance(state, LightState) and state.white is not None:
|
||||
if hasattr(state, "white"):
|
||||
assert state.white == pytest.approx(0.6, abs=0.1)
|
||||
|
||||
# Test 8: color_temperature only
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import patch
|
||||
|
||||
from esphome.config_helpers import filter_source_files_from_platform, get_logger_level
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
from esphome.const import (
|
||||
CONF_LEVEL,
|
||||
CONF_LOGGER,
|
||||
KEY_CORE,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
@@ -110,26 +108,3 @@ def test_filter_source_files_from_platform_handles_missing_data() -> None:
|
||||
excluded: list[str] = filter_func()
|
||||
# Should return empty list when platform/framework not set
|
||||
assert excluded == []
|
||||
|
||||
|
||||
def test_get_logger_level() -> None:
|
||||
"""Test get_logger_level helper function."""
|
||||
# Test no logger config - should return default DEBUG
|
||||
mock_config = {}
|
||||
with patch("esphome.config_helpers.CORE.config", mock_config):
|
||||
assert get_logger_level() == "DEBUG"
|
||||
|
||||
# Test with logger set to INFO
|
||||
mock_config = {CONF_LOGGER: {CONF_LEVEL: "INFO"}}
|
||||
with patch("esphome.config_helpers.CORE.config", mock_config):
|
||||
assert get_logger_level() == "INFO"
|
||||
|
||||
# Test with VERY_VERBOSE
|
||||
mock_config = {CONF_LOGGER: {CONF_LEVEL: "VERY_VERBOSE"}}
|
||||
with patch("esphome.config_helpers.CORE.config", mock_config):
|
||||
assert get_logger_level() == "VERY_VERBOSE"
|
||||
|
||||
# Test with logger missing level (uses default DEBUG)
|
||||
mock_config = {CONF_LOGGER: {}}
|
||||
with patch("esphome.config_helpers.CORE.config", mock_config):
|
||||
assert get_logger_level() == "DEBUG"
|
||||
|
||||
Reference in New Issue
Block a user