mirror of
https://github.com/esphome/esphome.git
synced 2026-07-08 16:05:34 +00:00
Compare commits
894 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd82a91d8f | |||
| 86ec218f75 | |||
| 2a6ec597b4 | |||
| 8dd69207ea | |||
| d203a46ef8 | |||
| 1920d8a887 | |||
| 95dea59382 | |||
| f3cddcee21 | |||
| 21e384cafd | |||
| 32db055b98 | |||
| 2c87260046 | |||
| 51ccad8461 | |||
| 7f500c4b6e | |||
| 564d155cb6 | |||
| edf5542559 | |||
| 51335e8830 | |||
| 391ffe34f8 | |||
| 12ead0408a | |||
| 2d39cc2540 | |||
| a9a8f4cb3b | |||
| 8fa2e75afa | |||
| 0b01f9fc42 | |||
| ed8c062d9f | |||
| 5e516e78e4 | |||
| 896b6ec8c9 | |||
| 9e7cdaf475 | |||
| a3fd1d5d00 | |||
| 7257bed1e9 | |||
| 5a9977cf5c | |||
| 12b3aec567 | |||
| d59c006ff9 | |||
| 02ada93ea5 | |||
| 6e87f8eb4e | |||
| 7df550f2a9 | |||
| b02f0e3c5f | |||
| 151f71e033 | |||
| 7ac001e994 | |||
| de177d2445 | |||
| a9cb7143dc | |||
| 902258b56e | |||
| c2a96ea293 | |||
| 37a3c3ab3a | |||
| a8ed781f3e | |||
| 63f0d054b7 | |||
| e7dcf54a77 | |||
| 1ba5504944 | |||
| 5637116378 | |||
| cdc4ba6295 | |||
| d1aa1881bb | |||
| 14107ec452 | |||
| 2ca6681896 | |||
| 40a65d36b4 | |||
| 16ec237ac6 | |||
| 0afcdbfe73 | |||
| b9439036d4 | |||
| cb23f9453f | |||
| 0858ecbb8e | |||
| 96da6dd075 | |||
| 2c31bdc6a2 | |||
| 0a3393bed3 | |||
| c2c50ceea7 | |||
| 2341d510d3 | |||
| 9d6f2f71e8 | |||
| e1334cf57f | |||
| a1aff7cadf | |||
| 2271ac6470 | |||
| 8fe36cde23 | |||
| fdd5956c1e | |||
| 609003c897 | |||
| 403ba262c6 | |||
| f8be27ce6d | |||
| 2c10adba85 | |||
| a50d70c8d3 | |||
| 4d86049c21 | |||
| 44037c4f9b | |||
| 5856d05701 | |||
| a2a048e3bf | |||
| 9f4c773963 | |||
| ef0eef8117 | |||
| 097e6eb41f | |||
| 73a49493a2 | |||
| 4a93d5b544 | |||
| cc0655a904 | |||
| a859cb3cce | |||
| 9e4e2d78dc | |||
| 47909d5299 | |||
| 16667bf5be | |||
| ef3afe3e21 | |||
| 3a47317fc8 | |||
| 89066e3e20 | |||
| af9366fdd4 | |||
| 448402ca2c | |||
| fc67551edc | |||
| 98d3dce672 | |||
| 4cb93d4df8 | |||
| 91e66cfd9d | |||
| 6cf32af33f | |||
| 9a80c980cb | |||
| c9e6c85e6a | |||
| e88c9ba066 | |||
| 45be290392 | |||
| 3f28ab88ca | |||
| 1d07f37d62 | |||
| 16c5224341 | |||
| e83372e2f3 | |||
| 6b9be033d6 | |||
| 2531fb1a02 | |||
| 3e845d387a | |||
| 5cc03d9bef | |||
| 0fa96b6e1e | |||
| be2e4a5278 | |||
| 80bd6489cf | |||
| ccf672d7ee | |||
| 6154b673c2 | |||
| 3bde7ec978 | |||
| 8caa11dcf4 | |||
| 1b70df2c1f | |||
| 4122fa5ddd | |||
| c5d42b0569 | |||
| 37f9541f32 | |||
| 8bbfadb59a | |||
| b9e8da92c7 | |||
| 0c5f055d45 | |||
| 342020e1d3 | |||
| 62f9bc79c4 | |||
| 53bfb02a21 | |||
| 83484a8828 | |||
| ece235218f | |||
| f3409acfa8 | |||
| 77b7201eb8 | |||
| 6b91df8d75 | |||
| 1670f04a87 | |||
| 1adf05e2d5 | |||
| a94bb74d04 | |||
| c19c75220b | |||
| 97382ed814 | |||
| 5f06679d78 | |||
| 851e8b6c0d | |||
| 9a729608d5 | |||
| 53fa346ddc | |||
| b3210de374 | |||
| 82ccc37ba1 | |||
| 3826e95506 | |||
| b083491e74 | |||
| 73ca0ff106 | |||
| bba11b3b1e | |||
| a40d97f346 | |||
| d6c67d5c35 | |||
| 0816b27398 | |||
| 9133582aa0 | |||
| f36b0fcb61 | |||
| bb0a5dc8a8 | |||
| 0c260e483e | |||
| b8ce907976 | |||
| ffce637ea5 | |||
| d6fba39037 | |||
| 5d5c2723b2 | |||
| 06d1498c47 | |||
| 2142bc1b76 | |||
| f81e04b036 | |||
| c3327d0b43 | |||
| 8577c26358 | |||
| 80730fd012 | |||
| 5ee3e94ca1 | |||
| 037f75e0ff | |||
| c47f4fbc1c | |||
| 2f86e48a83 | |||
| 0bbba75757 | |||
| 9362d9745e | |||
| c8f708c13c | |||
| 05590a3a21 | |||
| cdf2867baf | |||
| b142557979 | |||
| db405c483e | |||
| 808c7b67b3 | |||
| 7131eafc09 | |||
| 7b4af76a61 | |||
| 2cd93daa5e | |||
| f86bb2bdb0 | |||
| 414182fe6d | |||
| 2ee0df1da3 | |||
| e1252e32d1 | |||
| 1183ef825b | |||
| c09edb94c1 | |||
| 9948adc6a0 | |||
| ccb467b219 | |||
| 1377776d21 | |||
| 29501ef4f8 | |||
| d97c23b8e3 | |||
| 92d5e7b18c | |||
| 15ce4b3616 | |||
| 254e1f3abb | |||
| deb6b97eea | |||
| 22ea2764d4 | |||
| 632dbc8fe8 | |||
| 98d9871620 | |||
| a064eceb9b | |||
| 49107f2174 | |||
| e9c2659147 | |||
| 18b54f075e | |||
| 45e40223ac | |||
| 1ab1534028 | |||
| 039efdb02a | |||
| b0447dc521 | |||
| aacbaab5f8 | |||
| dc5032f72f | |||
| c263c2c382 | |||
| 910784ca84 | |||
| 0b99e8f08d | |||
| 93be539789 | |||
| 390bb0451f | |||
| 14c3e2d9d9 | |||
| 23c7e0f803 | |||
| cb4d1d1b5e | |||
| 2ba807efe8 | |||
| c8cf9b74b1 | |||
| 33475703da | |||
| 1b7d0f9c0b | |||
| 1d881ef6f4 | |||
| 3a838d897f | |||
| da130c900f | |||
| 440734dadf | |||
| df2ddc47ec | |||
| 4b1c4ba5c0 | |||
| 6002badb3c | |||
| e8f51fec88 | |||
| 7cec2d3029 | |||
| 2b0c471ed7 | |||
| 064bd13ebb | |||
| 2627490a11 | |||
| 4219d6d367 | |||
| 33f9ad9cee | |||
| 18a082de30 | |||
| 7f418d969e | |||
| fe9f19d9ed | |||
| d37f8876d7 | |||
| d7c42bc9ec | |||
| efc508a82b | |||
| 0edc0fd9c8 | |||
| cc4c13930f | |||
| 234ca7c951 | |||
| 447c4669b1 | |||
| 27942f1973 | |||
| 158a119a5a | |||
| b126f3af3b | |||
| d4e1e32a30 | |||
| 271b423b22 | |||
| 417858f098 | |||
| c52042e023 | |||
| f12531e7e0 | |||
| ca279110c9 | |||
| f2968e0449 | |||
| 0043be6165 | |||
| 0716c9f722 | |||
| fcf5637aa5 | |||
| 5e3c44d48f | |||
| d6d3bbbad8 | |||
| 86b7933081 | |||
| 7cceb72cc3 | |||
| 56f7b3e61b | |||
| 22062d79a2 | |||
| ab3b677113 | |||
| cdb445f69d | |||
| 1eed1adfa0 | |||
| a6c08576be | |||
| f41aa8b18c | |||
| 6700347a48 | |||
| b147830ef9 | |||
| bd844fcd0a | |||
| 8936be628f | |||
| 5920fa97e4 | |||
| 326769e43c | |||
| 7524590bcf | |||
| 15ec46abfe | |||
| 920af91db6 | |||
| a744261934 | |||
| 59c1368440 | |||
| 7e8e085a04 | |||
| 22b25724ae | |||
| 89719cf4b2 | |||
| e15b19b223 | |||
| 2ca13972b9 | |||
| 7bb4e75459 | |||
| fd8e510745 | |||
| 25c74c8f99 | |||
| 05d285ba86 | |||
| 186ca4e458 | |||
| 618312f0ee | |||
| 70d188202a | |||
| 4a21afe7ce | |||
| fd1d016795 | |||
| 03c091adfc | |||
| a3a88acfcf | |||
| 07f8ae6c82 | |||
| 25c30ac5bb | |||
| a76767a0ab | |||
| 511d185772 | |||
| c4c19c8a6c | |||
| fe2d60ccec | |||
| 657890695f | |||
| 8a5f008aee | |||
| f8a22b87b8 | |||
| 7f38d95424 | |||
| bb7d96b954 | |||
| 8daa946afa | |||
| ddc40f44fa | |||
| 409640c0ee | |||
| 822c9161c6 | |||
| ad198fd77b | |||
| a060f175ad | |||
| 73f305ff9c | |||
| b6ff7185e7 | |||
| 928f6f1866 | |||
| 02f7aee680 | |||
| e7c3277eeb | |||
| bef5e4de9c | |||
| 04bcd9f56b | |||
| 03c0ce704b | |||
| b27165a842 | |||
| 3d4ebe74ce | |||
| 4e16f270a3 | |||
| c52a48ed38 | |||
| 236f6b1935 | |||
| e8e700a683 | |||
| 4df3d3554e | |||
| d0f37ae694 | |||
| 6561c9bc95 | |||
| 794098de99 | |||
| b84d773bec | |||
| dcbf3c8728 | |||
| 30c8c68703 | |||
| 9513edc468 | |||
| 6356e3def9 | |||
| 8d988723cd | |||
| 8ca6ee4349 | |||
| 780e009bf4 | |||
| 04d80cfb75 | |||
| 9404eadaf8 | |||
| 4d2ef09a29 | |||
| 89bb5d9e42 | |||
| 9dd3ec258c | |||
| c709010c4c | |||
| 6e468936ec | |||
| 2c7ef4f758 | |||
| 06a127f64b | |||
| fba21e6dd4 | |||
| 4b50d14496 | |||
| e82f0f4432 | |||
| 00f809f5f0 | |||
| c31ac662bd | |||
| d6ce5dda81 | |||
| dadbdd0f7b | |||
| d96be88ff5 | |||
| d2686b49be | |||
| 468ce74c8e | |||
| b3fc43c13c | |||
| 308e8e78cd | |||
| 470d9160a5 | |||
| 9902447834 | |||
| 7c1b9f0cb4 | |||
| fecedeb018 | |||
| 9418f35cc3 | |||
| 08a0608a48 | |||
| b721cd48e5 | |||
| 75f55adbfa | |||
| a379e5a635 | |||
| 019db74582 | |||
| 31f4b4d00d | |||
| 0db9137d91 | |||
| f3ca86b670 | |||
| 088a8a4338 | |||
| 5d3893368d | |||
| 5b9cab02be | |||
| cac751e9e8 | |||
| 6ba5c9a705 | |||
| c681dc8872 | |||
| d0285cdc41 | |||
| 9547a54fac | |||
| b05dbfccd3 | |||
| aef2d74e41 | |||
| e1c849d5d2 | |||
| c11ad7f0e6 | |||
| 88536ff72b | |||
| 93d7ec4d72 | |||
| 66a5ad0d75 | |||
| 771404668d | |||
| 76c567a71c | |||
| e7730cff00 | |||
| d5dc4a39cb | |||
| 50b3f9d25c | |||
| ad5811280a | |||
| 9be1876fae | |||
| 1b3a7f0b6a | |||
| 3f143d9f19 | |||
| a9b5f95c76 | |||
| 0c4a44566f | |||
| 2c705810cd | |||
| a530aeec22 | |||
| d9e76da806 | |||
| e4b89a69d4 | |||
| 04cff1c916 | |||
| 5e842a8b20 | |||
| be6c3c52ac | |||
| 9fea8fe01b | |||
| d55fe9a34b | |||
| 66919ef969 | |||
| ea7cfffdda | |||
| 888f3d804b | |||
| 545395a6f0 | |||
| f2dfb5e1dc | |||
| 3f700bac1c | |||
| a0cd35c5fc | |||
| e7b8ec18f1 | |||
| 77f2c371b2 | |||
| 45f20d9c06 | |||
| f57fa4cc8d | |||
| abc870006c | |||
| 15ffbb0b05 | |||
| 8b62c35ea7 | |||
| 0e106d843c | |||
| cbebb81196 | |||
| 05ae69b766 | |||
| 9b489c9eba | |||
| df11e2765e | |||
| f53ee70caa | |||
| d8deb2255d | |||
| 035f985693 | |||
| 086c1bb505 | |||
| c26c5935b6 | |||
| de7572bd3e | |||
| 5777908da7 | |||
| 587bf68091 | |||
| 2c83c6a79f | |||
| 4f4b2bfdec | |||
| 0469612d07 | |||
| 8f3db96291 | |||
| a9cceebb33 | |||
| 9ab5f5d451 | |||
| 219d5170e0 | |||
| 7b8ba9bf20 | |||
| 3db436e48e | |||
| 3c7956e72d | |||
| 42dbb51022 | |||
| 9654140c00 | |||
| 8a915dcbbe | |||
| b2378e830e | |||
| 65b7c73bf3 | |||
| 6e3bc7b1dd | |||
| 82629c397f | |||
| a16b8fc0ac | |||
| 74e4b69654 | |||
| 07e51886f3 | |||
| e59a2b3ede | |||
| 5084c32f3c | |||
| c0b7f41397 | |||
| 6c07c15c50 | |||
| 666fb7cf39 | |||
| a2c0d70c2c | |||
| 80fe54ed69 | |||
| 44870323da | |||
| 58ab630965 | |||
| 64098122e7 | |||
| 8a8f6824a2 | |||
| b2c12d88fe | |||
| e8b1dce67b | |||
| fbf63d8e3b | |||
| 06d6322fe3 | |||
| de14e7055e | |||
| 3392e4d73b | |||
| b0be02e16d | |||
| d11e7cab46 | |||
| e25d740968 | |||
| 291679126f | |||
| 99a805cba6 | |||
| bb37887a8c | |||
| 5c5ea8824e | |||
| 22d90d702d | |||
| 9961c8180a | |||
| d6f3186b3d | |||
| 05ddc85412 | |||
| 6f0460b0ee | |||
| 44d314d069 | |||
| e210e414bd | |||
| cce7a09fa9 | |||
| e1d0c6da09 | |||
| 9518d88a2a | |||
| 4a5d8449fd | |||
| 3df4ef9362 | |||
| 01f4275202 | |||
| a061397469 | |||
| 2777d35990 | |||
| c49c23d5d9 | |||
| 0e2a10c5f0 | |||
| f5c37bf486 | |||
| 0ff5270632 | |||
| 5df4fd0a27 | |||
| c0143ac6d6 | |||
| c8e7f78a25 | |||
| b6d7e8e14d | |||
| 55103c0652 | |||
| 61ea6c3b2f | |||
| e11a91411b | |||
| 0c883b80c4 | |||
| 4928e678d1 | |||
| 22fc3aab39 | |||
| c37ab1de84 | |||
| 246a8bff0c | |||
| 9abba79c54 | |||
| 5ba880f19b | |||
| b2e8544c58 | |||
| ac19d05db2 | |||
| 065773ed4c | |||
| 37146ff565 | |||
| cba34e770e | |||
| 8911d9d28f | |||
| 9371159a7e | |||
| 43a6fe9b6c | |||
| 989330d6bc | |||
| ee78d7a0c0 | |||
| 96793a99ce | |||
| 380c0db020 | |||
| 95544dddf8 | |||
| b209c903bb | |||
| 4f69c487da | |||
| 78602ccacb | |||
| 1f1b20f4fe | |||
| d53ff7892a | |||
| b6f0bb9b6b | |||
| cfde0613bb | |||
| 903c67c994 | |||
| d8d479cef7 | |||
| f48106b160 | |||
| 1b5bf2c848 | |||
| c4fa476c3c | |||
| 60d66ca2dc | |||
| db15b94cd7 | |||
| ae49b67321 | |||
| c77241940b | |||
| 97d713ee64 | |||
| bc04a1a0ff | |||
| c4869bad88 | |||
| 7a87348855 | |||
| 2e623fd6c3 | |||
| 727fa07377 | |||
| 5510b45f3b | |||
| 3615a7b90c | |||
| d1de50c0e5 | |||
| dc56cd1d1f | |||
| d2a819eb77 | |||
| 0ac61cbb9b | |||
| c9c99a22e0 | |||
| 91250fd46c | |||
| 641914cdbe | |||
| 840859ab7c | |||
| 97b712da98 | |||
| b5c36140fa | |||
| 48a9c1cd67 | |||
| 38f671a923 | |||
| cb232d8288 | |||
| 2fa244715d | |||
| b9b1af1c3d | |||
| a1d91ac779 | |||
| 585e195044 | |||
| 39572d9628 | |||
| f278250740 | |||
| 54f410901f | |||
| 00242443e1 | |||
| 82da4935b6 | |||
| 1c5fd8bbd4 | |||
| 77a7cbcffd | |||
| 3160457ca6 | |||
| 590ee81f7a | |||
| 9d4357c619 | |||
| 80a2acca4f | |||
| f68a3ed15d | |||
| 6d3d8970a6 | |||
| 073ca63f60 | |||
| 0e18e4461e | |||
| 3e7424b307 | |||
| 48b5cae6c4 | |||
| a1760a1980 | |||
| 19bbd39e33 | |||
| 3f97b3b706 | |||
| 82e620bcf5 | |||
| 80e0761bf1 | |||
| c0781d3680 | |||
| b7cb65ec49 | |||
| fdbfac15db | |||
| 28424d6acd | |||
| b679b04d14 | |||
| b7d651dd17 | |||
| 757e8d90e6 | |||
| 7d52a9587f | |||
| 067d773aac | |||
| 089d1e55e7 | |||
| 6c0998f220 | |||
| 49cc389bf0 | |||
| 8480e8df9f | |||
| e7d4f2608b | |||
| d1b4813197 | |||
| 298ee7b92e | |||
| 5c56b99742 | |||
| b9d70dcda2 | |||
| 5e3857abf7 | |||
| bb567827a1 | |||
| 280f874edc | |||
| f6755aabae | |||
| 52af4bced0 | |||
| 63e757807e | |||
| edd63e3d2d | |||
| 32133e2f46 | |||
| 2255c68377 | |||
| 9c1d1a0d9f | |||
| 8698b01bc7 | |||
| 3411ce2150 | |||
| 29e1e8bdfd | |||
| 317dd5b2da | |||
| 4ae7633418 | |||
| c3a0eeceec | |||
| 4fe173b644 | |||
| 1c7f769ec7 | |||
| 72ca514cc2 | |||
| 20314b4d63 | |||
| 017d1b2872 | |||
| ef9fc87351 | |||
| 0f7ac1726d | |||
| bd3f8e006c | |||
| 07406c96e1 | |||
| 4044520ccc | |||
| 656389f215 | |||
| 04db37a34a | |||
| 15846137a6 | |||
| 50e7571f4c | |||
| 1ccfcfc8d8 | |||
| 527d4964f6 | |||
| 67ba68a1a0 | |||
| 8bd474fd01 | |||
| 54edc46c7f | |||
| 08035261b8 | |||
| e8b45e53fd | |||
| d325890148 | |||
| 8da1e3ce21 | |||
| c149be20fc | |||
| 4c3bb1596e | |||
| 1912dcf03d | |||
| ae16c3bae7 | |||
| be000eab4e | |||
| a05d0202e6 | |||
| 6c253f0c71 | |||
| 962cbfb9d8 | |||
| d52f8c9c6f | |||
| ee4d67930f | |||
| cced0a82b5 | |||
| 478a876b01 | |||
| 789da5fdf8 | |||
| bd08a56210 | |||
| f7843582e8 | |||
| 2c749e9dbe | |||
| 8479664df1 | |||
| 5a1d6428b2 | |||
| a39be5a461 | |||
| da930310b1 | |||
| af296eb600 | |||
| 2c11c65faf | |||
| 29d890bb0f | |||
| efa39ae591 | |||
| 4b57ac3236 | |||
| 997f825cd3 | |||
| 27fe866d5e | |||
| c5c6ce6b0e | |||
| 15e2a778d4 | |||
| 1f5a35a99f | |||
| 0975755a9d | |||
| 19f4845185 | |||
| 0d5b7df77d | |||
| 534857db9c | |||
| 0a81a7a50b | |||
| 23ef233b60 | |||
| 24fb74f78b | |||
| 2e167835ea | |||
| a60e5c5c4f | |||
| 3dcc9ab765 | |||
| d61e2f9c29 | |||
| 5dffceda59 | |||
| d1a636a5c3 | |||
| 3f558f63d8 | |||
| df77213f2c | |||
| e601162cdd | |||
| 62da60df47 | |||
| 8bb577de64 | |||
| ede8235aae | |||
| 37a0cec53d | |||
| 78ab63581b | |||
| 1beeb9ab5c | |||
| 228874a52b | |||
| bb05cfb711 | |||
| b134c4679c | |||
| 2e705a919f | |||
| 1dac501b04 | |||
| 905e81330e | |||
| 3460a8c922 | |||
| 2ff876c629 | |||
| 08dc487b5b | |||
| 4dc6b12ec5 | |||
| cca4777f64 | |||
| af00d601be | |||
| fe3c2ba555 | |||
| 6554ad7c7e | |||
| 4abbed0cd4 | |||
| 72263eda85 | |||
| abf7074518 | |||
| ad2da0af52 | |||
| 7d9d90d3f8 | |||
| 70e47f301d | |||
| 1614eb9c9c | |||
| a694003fe3 | |||
| 500aa7bf1d | |||
| 63c1496115 | |||
| 843d06df3f | |||
| 30cc51eac9 | |||
| ebf1047da7 | |||
| 869678953d | |||
| 4a52900352 | |||
| 02c37bb6d6 | |||
| 918bbfb0d3 | |||
| 063c6a9e45 | |||
| daee71a2c1 | |||
| 0d32a5321c | |||
| e199145f1c | |||
| 1f945a334a | |||
| fb6c7d81d5 | |||
| 417f4535af | |||
| ee94bc4715 | |||
| 6801604533 | |||
| 5c388a5200 | |||
| d239a2400d | |||
| 93ce582ad3 | |||
| 6e70987451 | |||
| 263fff0ba2 | |||
| ded457c2c1 | |||
| b539a5aa51 | |||
| 5fddce6638 | |||
| ee1f521325 | |||
| ede2da2fbc | |||
| 509f06afac | |||
| 1753074eef | |||
| e013b48675 | |||
| 49e4ae54be | |||
| d5c9c56fdf | |||
| a468261523 | |||
| 462ac29563 | |||
| 6f198adb0c | |||
| e521522b38 | |||
| 5a07908dfa | |||
| 9571a979eb | |||
| 48ba007c22 | |||
| 6ff17fbf7c | |||
| 416b97311b | |||
| 7fb09da7cf | |||
| 6ecb01dedc | |||
| 518f08b909 | |||
| 2eac106f11 | |||
| f77da803c9 | |||
| f8f98bf428 | |||
| abe37c9841 | |||
| 8589f80d8f | |||
| 0e38acd67a | |||
| a3f279c1cf | |||
| 35037d1a5b | |||
| d206c75b0b | |||
| 1d3054ef5e | |||
| db6aa58f40 | |||
| 48115eca18 | |||
| edfc3e3501 | |||
| 1a37632891 | |||
| b85a49cdb3 | |||
| 9c0eed8a67 | |||
| 887375ebef | |||
| 403235e2d4 | |||
| 9ce01fc369 | |||
| b0a35559b3 | |||
| efe54e3b5e | |||
| 5af871acce | |||
| a2f0607c1e | |||
| b67b2cc3ab | |||
| 7a2a149061 | |||
| afbc45bf32 | |||
| c1265a9490 | |||
| 94712b3961 | |||
| d29288547e | |||
| 1b4de55efd | |||
| cceb109303 | |||
| 17a810b939 | |||
| 4aa8f57d36 | |||
| f2c98d6126 | |||
| 7a5c3cee0d | |||
| 9aa17984df | |||
| da616e0557 | |||
| d2026b4cd7 | |||
| ed74790eed | |||
| bf2e22da4f | |||
| bd50b80882 | |||
| b11ad26c4f | |||
| f7459670d3 | |||
| 5304750215 | |||
| a8171da003 | |||
| 916cf0d8b7 | |||
| 0484b2852d | |||
| b5a8e1c94c | |||
| 01a46f665f | |||
| 535980b9bd | |||
| b0085e21f7 | |||
| 6daca09794 | |||
| 7b53a98950 | |||
| 4cc1e6a910 | |||
| 4d05e4d576 | |||
| eefad194d0 | |||
| ba7134ee3f | |||
| 264c8faedd | |||
| 3c227eeca4 | |||
| c8598fe620 | |||
| 2f9b76f129 | |||
| 9a8b00a428 | |||
| eaf0d03a37 | |||
| e7f2021864 | |||
| dff9780d3a | |||
| 20239d1bb3 | |||
| ee7d63f73a | |||
| 76c151c6e6 | |||
| 9c9365c146 | |||
| 7e118178b3 | |||
| 66d2ac8cb9 | |||
| e4c233b6ce | |||
| be853afc24 | |||
| 565443b710 | |||
| 3b869f1720 | |||
| 5f82017a31 | |||
| bd055e75b9 | |||
| d90754dc0a | |||
| 387f615dae | |||
| 02e310f2c9 | |||
| d83738df87 | |||
| 09fc028895 | |||
| 82cfa00a97 | |||
| 4a038978d2 | |||
| bd38041d04 | |||
| 9cd7b0b32b | |||
| f73bcc0e7b | |||
| 652c669777 | |||
| fb89900c64 | |||
| fb35ddebb9 | |||
| a3d7e76992 | |||
| 5bb863f7da | |||
| 81ed70325c | |||
| e826d71bd8 | |||
| 4cd3f6c36a | |||
| 6b4b8cb2f9 | |||
| 0c4827d348 | |||
| 81872d9822 | |||
| ffb9a00e26 | |||
| f2c827f9a2 | |||
| f2cb5db9e0 | |||
| 066419019f | |||
| 15da6d0a0b | |||
| 6303bc3e35 | |||
| 0f4dc6702d | |||
| f48c8a6444 | |||
| 38404b2013 | |||
| 5a6d64814a | |||
| 36776b40c2 | |||
| 58c3ba7ac6 | |||
| 931b47673c | |||
| 79d9fbf645 | |||
| f24e7709ac | |||
| 903971de12 | |||
| b04e427f01 | |||
| e0c03b2dfa | |||
| 7dff631dcb | |||
| 36aba385af | |||
| 136d17366f | |||
| db7870ef5f | |||
| bbc88d92ea | |||
| 1604b5d6e4 | |||
| 7fd535179e | |||
| e3a457e402 | |||
| 0dcff82bb4 | |||
| cde8b66719 | |||
| 0e1433329d | |||
| 60fef5e656 | |||
| 725e774fe7 | |||
| 9aa98ed6c6 | |||
| 7b251dcc31 | |||
| 8a08c688f6 | |||
| d6461251f9 |
@@ -286,6 +286,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
* **Documentation Contributions:**
|
||||
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
|
||||
* The contribution workflow is the same as for the codebase.
|
||||
* When editing a component's documentation page, also update the corresponding component index page to ensure both pages remain in sync.
|
||||
|
||||
* **Best Practices:**
|
||||
* **Component Development:** Keep dependencies minimal, provide clear error messages, and write comprehensive docstrings and tests.
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
ce05c28e9dc0b12c4f6e7454986ffea5123ac974a949da841be698c535f2083e
|
||||
9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
|
||||
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Developer breaking change (an API change that could break external components)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — [policy](https://developers.esphome.io/contributing/code/#what-constitutes-a-c-breaking-change)
|
||||
- [ ] Developer breaking change (an API change that could break external components) — [policy](https://developers.esphome.io/contributing/code/#what-is-considered-public-c-api)
|
||||
- [ ] Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — [policy](https://developers.esphome.io/contributing/code/#c-user-expectations)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
- [ ] Other
|
||||
|
||||
@@ -24,7 +25,7 @@
|
||||
- [ ] ESP32
|
||||
- [ ] ESP32 IDF
|
||||
- [ ] ESP8266
|
||||
- [ ] RP2040
|
||||
- [ ] RP2040/RP2350
|
||||
- [ ] BK72xx
|
||||
- [ ] RTL87xx
|
||||
- [ ] LN882x
|
||||
|
||||
@@ -47,7 +47,7 @@ runs:
|
||||
|
||||
- name: Build and push to ghcr by digest
|
||||
id: build-ghcr
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
@@ -73,7 +73,7 @@ runs:
|
||||
|
||||
- name: Build and push to dockerhub by digest
|
||||
id: build-dockerhub
|
||||
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
|
||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||
env:
|
||||
DOCKER_BUILD_SUMMARY: false
|
||||
DOCKER_BUILD_RECORD_UPLOAD: false
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
@@ -14,6 +14,7 @@ module.exports = {
|
||||
'chained-pr',
|
||||
'core',
|
||||
'small-pr',
|
||||
'medium-pr',
|
||||
'dashboard',
|
||||
'github-actions',
|
||||
'by-code-owner',
|
||||
@@ -27,6 +28,7 @@ module.exports = {
|
||||
'new-feature',
|
||||
'breaking-change',
|
||||
'developer-breaking-change',
|
||||
'undocumented-api-change',
|
||||
'code-quality',
|
||||
'deprecated-component'
|
||||
],
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
const fs = require('fs');
|
||||
const { DOCS_PR_PATTERNS } = require('./constants');
|
||||
const {
|
||||
COMPONENT_REGEX,
|
||||
detectComponents,
|
||||
hasCoreChanges,
|
||||
hasDashboardChanges,
|
||||
hasGitHubActionsChanges,
|
||||
} = require('../detect-tags');
|
||||
const { loadCodeowners, getEffectiveOwners } = require('../codeowners');
|
||||
|
||||
// Strategy: Merge branch detection
|
||||
async function detectMergeBranch(context) {
|
||||
@@ -20,15 +28,13 @@ async function detectMergeBranch(context) {
|
||||
// Strategy: Component and platform labeling
|
||||
async function detectComponentPlatforms(changedFiles, apiData) {
|
||||
const labels = new Set();
|
||||
const componentRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const targetPlatformRegex = new RegExp(`^esphome\/components\/(${apiData.targetPlatforms.join('|')})/`);
|
||||
|
||||
for (const file of changedFiles) {
|
||||
const componentMatch = file.match(componentRegex);
|
||||
if (componentMatch) {
|
||||
labels.add(`component: ${componentMatch[1]}`);
|
||||
}
|
||||
for (const comp of detectComponents(changedFiles)) {
|
||||
labels.add(`component: ${comp}`);
|
||||
}
|
||||
|
||||
for (const file of changedFiles) {
|
||||
const platformMatch = file.match(targetPlatformRegex);
|
||||
if (platformMatch) {
|
||||
labels.add(`platform: ${platformMatch[1]}`);
|
||||
@@ -90,20 +96,14 @@ async function detectNewPlatforms(prFiles, apiData) {
|
||||
// Strategy: Core files detection
|
||||
async function detectCoreChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const coreFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
|
||||
if (coreFiles.length > 0) {
|
||||
if (hasCoreChanges(changedFiles)) {
|
||||
labels.add('core');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: PR size detection
|
||||
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
|
||||
async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD) {
|
||||
const labels = new Set();
|
||||
|
||||
if (totalChanges <= SMALL_PR_THRESHOLD) {
|
||||
@@ -111,6 +111,11 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange
|
||||
return labels;
|
||||
}
|
||||
|
||||
if (totalChanges <= MEDIUM_PR_THRESHOLD) {
|
||||
labels.add('medium-pr');
|
||||
return labels;
|
||||
}
|
||||
|
||||
const testAdditions = prFiles
|
||||
.filter(file => file.filename.startsWith('tests/'))
|
||||
.reduce((sum, file) => sum + (file.additions || 0), 0);
|
||||
@@ -131,80 +136,33 @@ async function detectPRSize(prFiles, totalAdditions, totalDeletions, totalChange
|
||||
// Strategy: Dashboard changes
|
||||
async function detectDashboardChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const dashboardFiles = changedFiles.filter(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
|
||||
if (dashboardFiles.length > 0) {
|
||||
if (hasDashboardChanges(changedFiles)) {
|
||||
labels.add('dashboard');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: GitHub Actions changes
|
||||
async function detectGitHubActionsChanges(changedFiles) {
|
||||
const labels = new Set();
|
||||
const githubActionsFiles = changedFiles.filter(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
|
||||
if (githubActionsFiles.length > 0) {
|
||||
if (hasGitHubActionsChanges(changedFiles)) {
|
||||
labels.add('github-actions');
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Code owner detection
|
||||
async function detectCodeOwner(github, context, changedFiles) {
|
||||
const labels = new Set();
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
try {
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS',
|
||||
});
|
||||
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
const codeownersPatterns = loadCodeowners();
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
const codeownersRegexes = codeownersLines.map(line => {
|
||||
const parts = line.split(/\s+/);
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
let regex;
|
||||
if (pattern.endsWith('*')) {
|
||||
const dir = pattern.slice(0, -1);
|
||||
regex = new RegExp(`^${dir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
|
||||
} else if (pattern.includes('*')) {
|
||||
// First escape all regex special chars except *, then replace * with .*
|
||||
const regexPattern = pattern
|
||||
.replace(/[.+?^${}()|[\]\\]/g, '\\$&')
|
||||
.replace(/\*/g, '.*');
|
||||
regex = new RegExp(`^${regexPattern}$`);
|
||||
} else {
|
||||
regex = new RegExp(`^${pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`);
|
||||
}
|
||||
|
||||
return { regex, owners };
|
||||
});
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { regex, owners } of codeownersRegexes) {
|
||||
if (regex.test(file) && owners.some(owner => owner === `@${prAuthor}`)) {
|
||||
labels.add('by-code-owner');
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
// Check if PR author is a codeowner of any changed file
|
||||
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
|
||||
if (effective.users.has(prAuthor)) {
|
||||
labels.add('by-code-owner');
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Failed to read or parse CODEOWNERS file:', error.message);
|
||||
@@ -238,6 +196,7 @@ async function detectPRTemplateCheckboxes(context) {
|
||||
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
||||
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
||||
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
|
||||
{ pattern: /- \[x\] Undocumented C\+\+ API change \(removal or change of undocumented public methods that lambda users may depend on\)/i, label: 'undocumented-api-change' },
|
||||
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||
];
|
||||
|
||||
@@ -258,7 +217,7 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
// Compile regex once for better performance
|
||||
const componentFileRegex = /^esphome\/components\/([^\/]+)\//;
|
||||
const componentFileRegex = COMPONENT_REGEX;
|
||||
|
||||
// Get files that are modified or added in components directory
|
||||
const componentFiles = changedFiles.filter(file => componentFileRegex.test(file));
|
||||
|
||||
@@ -35,6 +35,7 @@ async function fetchApiData() {
|
||||
module.exports = async ({ github, context }) => {
|
||||
// Environment variables
|
||||
const SMALL_PR_THRESHOLD = parseInt(process.env.SMALL_PR_THRESHOLD);
|
||||
const MEDIUM_PR_THRESHOLD = parseInt(process.env.MEDIUM_PR_THRESHOLD);
|
||||
const MAX_LABELS = parseInt(process.env.MAX_LABELS);
|
||||
const TOO_BIG_THRESHOLD = parseInt(process.env.TOO_BIG_THRESHOLD);
|
||||
const COMPONENT_LABEL_THRESHOLD = parseInt(process.env.COMPONENT_LABEL_THRESHOLD);
|
||||
@@ -120,7 +121,7 @@ module.exports = async ({ github, context }) => {
|
||||
detectNewComponents(prFiles),
|
||||
detectNewPlatforms(prFiles, apiData),
|
||||
detectCoreChanges(changedFiles),
|
||||
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, TOO_BIG_THRESHOLD),
|
||||
detectPRSize(prFiles, totalAdditions, totalDeletions, totalChanges, isMegaPR, SMALL_PR_THRESHOLD, MEDIUM_PR_THRESHOLD, TOO_BIG_THRESHOLD),
|
||||
detectDashboardChanges(changedFiles),
|
||||
detectGitHubActionsChanges(changedFiles),
|
||||
detectCodeOwner(github, context, changedFiles),
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
// Shared CODEOWNERS parsing and matching utilities.
|
||||
//
|
||||
// Used by:
|
||||
// - codeowner-review-request.yml
|
||||
// - codeowner-approved-label-update.yml
|
||||
// - auto-label-pr/detectors.js (detectCodeOwner)
|
||||
|
||||
/**
|
||||
* Convert a CODEOWNERS glob pattern to a RegExp.
|
||||
*
|
||||
* Handles **, *, and ? wildcards after escaping regex-special characters.
|
||||
*/
|
||||
function globToRegex(pattern) {
|
||||
let regexStr = pattern
|
||||
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1')
|
||||
.replace(/\*\*/g, '\x00GLOBSTAR\x00') // protect ** from next replace
|
||||
.replace(/\*/g, '[^/]*') // single star
|
||||
.replace(/\x00GLOBSTAR\x00/g, '.*') // restore globstar
|
||||
.replace(/\?/g, '.');
|
||||
return new RegExp('^' + regexStr + '$');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse raw CODEOWNERS file content into an array of
|
||||
* { pattern, regex, owners } objects.
|
||||
*
|
||||
* Each `owners` entry is the raw string from the file (e.g. "@user" or
|
||||
* "@esphome/core").
|
||||
*/
|
||||
function parseCodeowners(content) {
|
||||
const lines = content
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
const patterns = [];
|
||||
for (const line of lines) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
const regex = globToRegex(pattern);
|
||||
patterns.push({ pattern, regex, owners });
|
||||
}
|
||||
return patterns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch and parse the CODEOWNERS file via the GitHub API.
|
||||
*
|
||||
* @param {object} github - octokit instance from actions/github-script
|
||||
* @param {string} owner - repo owner
|
||||
* @param {string} repo - repo name
|
||||
* @param {string} [ref] - git ref (SHA / branch) to read from
|
||||
* @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>}
|
||||
*/
|
||||
async function fetchCodeowners(github, owner, repo, ref) {
|
||||
const params = { owner, repo, path: 'CODEOWNERS' };
|
||||
if (ref) params.ref = ref;
|
||||
|
||||
const { data: file } = await github.rest.repos.getContent(params);
|
||||
const content = Buffer.from(file.content, 'base64').toString('utf8');
|
||||
return parseCodeowners(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify raw owner strings into individual users and teams.
|
||||
*
|
||||
* @param {string[]} rawOwners - e.g. ["@user1", "@esphome/core"]
|
||||
* @returns {{ users: string[], teams: string[] }}
|
||||
* users – login names without "@"
|
||||
* teams – team slugs without the "org/" prefix
|
||||
*/
|
||||
function classifyOwners(rawOwners) {
|
||||
const users = [];
|
||||
const teams = [];
|
||||
for (const o of rawOwners) {
|
||||
const clean = o.startsWith('@') ? o.slice(1) : o;
|
||||
if (clean.includes('/')) {
|
||||
teams.push(clean.split('/')[1]);
|
||||
} else {
|
||||
users.push(clean);
|
||||
}
|
||||
}
|
||||
return { users, teams };
|
||||
}
|
||||
|
||||
/**
|
||||
* For each file, find its effective codeowners using GitHub's
|
||||
* "last match wins" semantics, then union across all files.
|
||||
*
|
||||
* @param {string[]} files - list of file paths
|
||||
* @param {Array} codeownersPatterns - from parseCodeowners / fetchCodeowners
|
||||
* @returns {{ users: Set<string>, teams: Set<string>, matchedFileCount: number }}
|
||||
*/
|
||||
function getEffectiveOwners(files, codeownersPatterns) {
|
||||
const users = new Set();
|
||||
const teams = new Set();
|
||||
let matchedFileCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
// Last matching pattern wins for each file
|
||||
let effectiveOwners = null;
|
||||
for (const { regex, owners } of codeownersPatterns) {
|
||||
if (regex.test(file)) {
|
||||
effectiveOwners = owners;
|
||||
}
|
||||
}
|
||||
if (effectiveOwners) {
|
||||
matchedFileCount++;
|
||||
const classified = classifyOwners(effectiveOwners);
|
||||
for (const u of classified.users) users.add(u);
|
||||
for (const t of classified.teams) teams.add(t);
|
||||
}
|
||||
}
|
||||
|
||||
return { users, teams, matchedFileCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse the CODEOWNERS file from disk.
|
||||
*
|
||||
* Use this when the repo is already checked out (avoids an API call).
|
||||
*
|
||||
* @param {string} [repoRoot='.'] - path to the repo root
|
||||
* @returns {Array<{pattern: string, regex: RegExp, owners: string[]}>}
|
||||
*/
|
||||
function loadCodeowners(repoRoot = '.') {
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const content = fs.readFileSync(path.join(repoRoot, 'CODEOWNERS'), 'utf8');
|
||||
return parseCodeowners(content);
|
||||
}
|
||||
|
||||
/** Possible label actions returned by determineLabelAction. */
|
||||
const LabelAction = Object.freeze({
|
||||
ADD: 'add',
|
||||
REMOVE: 'remove',
|
||||
NONE: 'none',
|
||||
});
|
||||
|
||||
/**
|
||||
* Determine what label action is needed for a PR based on codeowner approvals.
|
||||
*
|
||||
* Checks changed files against CODEOWNERS patterns, reviews, and current labels
|
||||
* to decide if the label should be added, removed, or left unchanged.
|
||||
*
|
||||
* @param {object} github - octokit instance from actions/github-script
|
||||
* @param {string} owner - repo owner
|
||||
* @param {string} repo - repo name
|
||||
* @param {number} pr_number - pull request number
|
||||
* @param {Array} codeownersPatterns - from loadCodeowners / fetchCodeowners
|
||||
* @param {string} labelName - label to manage
|
||||
* @returns {Promise<LabelAction>}
|
||||
*/
|
||||
async function determineLabelAction(github, owner, repo, pr_number, codeownersPatterns, labelName) {
|
||||
// Get the list of changed files in this PR
|
||||
const prFiles = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
{ owner, repo, pull_number: pr_number }
|
||||
);
|
||||
|
||||
const changedFiles = prFiles.map(file => file.filename);
|
||||
console.log(`Found ${changedFiles.length} changed files`);
|
||||
|
||||
if (changedFiles.length === 0) {
|
||||
console.log('No changed files found');
|
||||
return LabelAction.NONE;
|
||||
}
|
||||
|
||||
// Get effective owners using last-match-wins semantics
|
||||
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
|
||||
const componentCodeowners = effective.users;
|
||||
|
||||
console.log(`Component-specific codeowners: ${Array.from(componentCodeowners).join(', ') || '(none)'}`);
|
||||
|
||||
// Get current labels
|
||||
const { data: currentLabels } = await github.rest.issues.listLabelsOnIssue({
|
||||
owner, repo, issue_number: pr_number
|
||||
});
|
||||
const hasLabel = currentLabels.some(label => label.name === labelName);
|
||||
|
||||
if (componentCodeowners.size === 0) {
|
||||
console.log('No component-specific codeowners found');
|
||||
return hasLabel ? LabelAction.REMOVE : LabelAction.NONE;
|
||||
}
|
||||
|
||||
// Get all reviews and find latest per user
|
||||
const reviews = await github.paginate(
|
||||
github.rest.pulls.listReviews,
|
||||
{ owner, repo, pull_number: pr_number }
|
||||
);
|
||||
|
||||
const latestReviewByUser = new Map();
|
||||
for (const review of reviews) {
|
||||
if (!review.user || review.user.type === 'Bot' || review.state === 'COMMENTED') continue;
|
||||
latestReviewByUser.set(review.user.login, review);
|
||||
}
|
||||
|
||||
// Check if any component-specific codeowner has an active approval
|
||||
let hasCodeownerApproval = false;
|
||||
for (const [login, review] of latestReviewByUser) {
|
||||
if (review.state === 'APPROVED' && componentCodeowners.has(login)) {
|
||||
console.log(`Codeowner '${login}' has approved`);
|
||||
hasCodeownerApproval = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasCodeownerApproval && !hasLabel) return LabelAction.ADD;
|
||||
if (!hasCodeownerApproval && hasLabel) return LabelAction.REMOVE;
|
||||
|
||||
console.log(`Label already ${hasLabel ? 'present' : 'absent'}, no change needed`);
|
||||
return LabelAction.NONE;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
globToRegex,
|
||||
parseCodeowners,
|
||||
fetchCodeowners,
|
||||
loadCodeowners,
|
||||
classifyOwners,
|
||||
getEffectiveOwners,
|
||||
LabelAction,
|
||||
determineLabelAction
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Shared tag detection from changed file paths.
|
||||
* Used by pr-title-check and auto-label-pr workflows.
|
||||
*/
|
||||
|
||||
const COMPONENT_REGEX = /^esphome\/components\/([^\/]+)\//;
|
||||
|
||||
/**
|
||||
* Detect component names from changed files.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {Set<string>} Set of component names
|
||||
*/
|
||||
function detectComponents(changedFiles) {
|
||||
const components = new Set();
|
||||
for (const file of changedFiles) {
|
||||
const match = file.match(COMPONENT_REGEX);
|
||||
if (match) {
|
||||
components.add(match[1]);
|
||||
}
|
||||
}
|
||||
return components;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if core files were changed.
|
||||
* Core files are in esphome/core/ or top-level esphome/ directory.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasCoreChanges(changedFiles) {
|
||||
return changedFiles.some(file =>
|
||||
file.startsWith('esphome/core/') ||
|
||||
(file.startsWith('esphome/') && file.split('/').length === 2)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if dashboard files were changed.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasDashboardChanges(changedFiles) {
|
||||
return changedFiles.some(file =>
|
||||
file.startsWith('esphome/dashboard/') ||
|
||||
file.startsWith('esphome/components/dashboard_import/')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if GitHub Actions files were changed.
|
||||
* @param {string[]} changedFiles - List of changed file paths
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasGitHubActionsChanges(changedFiles) {
|
||||
return changedFiles.some(file =>
|
||||
file.startsWith('.github/workflows/')
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
COMPONENT_REGEX,
|
||||
detectComponents,
|
||||
hasCoreChanges,
|
||||
hasDashboardChanges,
|
||||
hasGitHubActionsChanges,
|
||||
};
|
||||
@@ -12,6 +12,7 @@ permissions:
|
||||
|
||||
env:
|
||||
SMALL_PR_THRESHOLD: 30
|
||||
MEDIUM_PR_THRESHOLD: 100
|
||||
MAX_LABELS: 15
|
||||
TOO_BIG_THRESHOLD: 1000
|
||||
COMPONENT_LABEL_THRESHOLD: 10
|
||||
@@ -26,7 +27,7 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
run: git diff
|
||||
- if: failure()
|
||||
name: Archive artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: generated-proto-files
|
||||
path: |
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
|
||||
|
||||
- if: failure()
|
||||
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Request changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
|
||||
})
|
||||
|
||||
- if: success()
|
||||
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Set TAG
|
||||
run: |
|
||||
|
||||
+112
-31
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -106,6 +106,7 @@ jobs:
|
||||
script/build_codeowners.py --check
|
||||
script/build_language_schema.py --check
|
||||
script/generate-esp32-boards.py --check
|
||||
script/generate-rp2040-boards.py --check
|
||||
|
||||
pytest:
|
||||
name: Run pytest
|
||||
@@ -153,12 +154,12 @@ jobs:
|
||||
. venv/bin/activate
|
||||
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -170,6 +171,8 @@ jobs:
|
||||
- common
|
||||
outputs:
|
||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
|
||||
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
@@ -182,6 +185,7 @@ jobs:
|
||||
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
|
||||
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
benchmarks: ${{ steps.determine.outputs.benchmarks }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -194,7 +198,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -210,6 +214,8 @@ jobs:
|
||||
|
||||
# Extract individual fields
|
||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
@@ -222,9 +228,10 @@ jobs:
|
||||
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
|
||||
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -246,7 +253,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -261,9 +268,20 @@ jobs:
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run integration tests
|
||||
env:
|
||||
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
|
||||
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
|
||||
echo "Running all integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
else
|
||||
# Parse JSON array into bash array to avoid shell expansion issues
|
||||
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
|
||||
echo "Running ${#test_files[@]} specific integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
|
||||
fi
|
||||
|
||||
cpp-unit-tests:
|
||||
name: Run C++ unit tests
|
||||
@@ -292,6 +310,40 @@ jobs:
|
||||
script/cpp_unit_test.py $ARGS
|
||||
fi
|
||||
|
||||
benchmarks:
|
||||
name: Run CodSpeed benchmarks
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: >-
|
||||
(github.event_name == 'push' && github.ref_name == 'dev') ||
|
||||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Build benchmarks
|
||||
id: build
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py)
|
||||
# --build-only prints BUILD_BINARY=<path> to stdout
|
||||
BINARY=$(script/cpp_benchmark.py --all --build-only | grep '^BUILD_BINARY=' | tail -1 | cut -d= -f2-)
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4
|
||||
with:
|
||||
run: ${{ steps.build.outputs.binary }}
|
||||
mode: simulation
|
||||
|
||||
clang-tidy-single:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -335,14 +387,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -414,14 +466,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -503,14 +555,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -686,7 +738,7 @@ jobs:
|
||||
ram_usage: ${{ steps.extract.outputs.ram_usage }}
|
||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
||||
cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }}
|
||||
skip: ${{ steps.check-script.outputs.skip }}
|
||||
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
|
||||
steps:
|
||||
- name: Check out target branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -705,10 +757,39 @@ jobs:
|
||||
echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis"
|
||||
fi
|
||||
|
||||
# All remaining steps only run if script exists
|
||||
# Check if test files exist on the target branch for the requested
|
||||
# components and platform. When a PR adds new test files for a platform,
|
||||
# the target branch won't have them yet, so skip instead of failing.
|
||||
# This check must be done here (not in determine-jobs.py) because
|
||||
# determine-jobs runs on the PR branch and cannot see what the target
|
||||
# branch has.
|
||||
- name: Check for test files on target branch
|
||||
id: check-tests
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
run: |
|
||||
components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
|
||||
platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
|
||||
found=false
|
||||
for component in $(echo "$components" | jq -r '.[]'); do
|
||||
# Check for test files matching the platform (test.platform.yaml or test-*.platform.yaml)
|
||||
for f in tests/components/${component}/test*.${platform}.yaml; do
|
||||
if [ -f "$f" ]; then
|
||||
found=true
|
||||
break 2
|
||||
fi
|
||||
done
|
||||
done
|
||||
if [ "$found" = false ]; then
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
echo "::warning::No test files found on target branch for platform ${platform}, skipping memory impact analysis"
|
||||
else
|
||||
echo "skip=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
# All remaining steps only run if script and tests exist
|
||||
- name: Generate cache key
|
||||
id: cache-key
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
run: |
|
||||
# Get the commit SHA of the target branch
|
||||
target_sha=$(git rev-parse HEAD)
|
||||
@@ -735,14 +816,14 @@ jobs:
|
||||
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
|
||||
- name: Cache status
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
run: |
|
||||
if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then
|
||||
echo "✓ Cache hit! Using cached memory analysis results."
|
||||
@@ -752,21 +833,21 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Restore Python
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Build, compile, and analyze memory
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
id: build
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -800,15 +881,15 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
|
||||
- name: Extract memory usage for outputs
|
||||
id: extract
|
||||
if: steps.check-script.outputs.skip != 'true'
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
run: |
|
||||
if [ -f memory-analysis-target.json ]; then
|
||||
ram=$(jq -r '.ram_bytes' memory-analysis-target.json)
|
||||
@@ -822,7 +903,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: memory-analysis-target.json
|
||||
@@ -848,7 +929,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -886,7 +967,7 @@ jobs:
|
||||
--platform "$platform"
|
||||
|
||||
- name: Upload memory analysis JSON
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: memory-analysis-pr.json
|
||||
@@ -916,13 +997,13 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Download target analysis JSON
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: memory-analysis-target
|
||||
path: ./memory-analysis
|
||||
continue-on-error: true
|
||||
- name: Download PR analysis JSON
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: memory-analysis-pr
|
||||
path: ./memory-analysis
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Adds/removes a 'code-owner-approved' label when a component-specific
|
||||
# codeowner approves (or dismisses) a PR.
|
||||
#
|
||||
# Uses pull_request_target so that fork PRs do not require workflow approval.
|
||||
# The label is reconciled on every PR update; for review events specifically,
|
||||
# this means the label is applied on the next push after a codeowner review.
|
||||
|
||||
name: Codeowner Approved Label
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
branches-ignore:
|
||||
- release
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
pull-requests: read
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
codeowner-approved:
|
||||
name: Run
|
||||
if: ${{ github.repository == 'esphome/esphome' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
sparse-checkout: |
|
||||
.github/scripts/codeowners.js
|
||||
CODEOWNERS
|
||||
|
||||
- name: Check codeowner approval and update label
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
with:
|
||||
script: |
|
||||
const { loadCodeowners, determineLabelAction, LabelAction } = require('./.github/scripts/codeowners.js');
|
||||
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const pr_number = parseInt(process.env.PR_NUMBER, 10);
|
||||
const LABEL_NAME = 'code-owner-approved';
|
||||
|
||||
console.log(`Processing PR #${pr_number} for codeowner approval label`);
|
||||
|
||||
const codeownersPatterns = loadCodeowners();
|
||||
const action = await determineLabelAction(
|
||||
github, owner, repo, pr_number, codeownersPatterns, LABEL_NAME
|
||||
);
|
||||
|
||||
if (action === LabelAction.NONE) {
|
||||
console.log('No label change needed');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === LabelAction.ADD) {
|
||||
await github.rest.issues.addLabels({
|
||||
owner, repo, issue_number: pr_number, labels: [LABEL_NAME]
|
||||
});
|
||||
console.log(`Added '${LABEL_NAME}' label`);
|
||||
} else if (action === LabelAction.REMOVE) {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner, repo, issue_number: pr_number, name: LABEL_NAME
|
||||
});
|
||||
console.log(`Removed '${LABEL_NAME}' label`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.status === 403) {
|
||||
console.log(`Warning: insufficient permissions to update label (expected for fork PRs)`);
|
||||
} else if (error.status === 404) {
|
||||
console.log(`Label '${LABEL_NAME}' not present, nothing to remove`);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ on:
|
||||
# Needs to be pull_request_target to get write permissions
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
branches-ignore:
|
||||
- release
|
||||
- beta
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
@@ -24,10 +27,17 @@ jobs:
|
||||
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout base branch
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
|
||||
- name: Request reviews from component codeowners
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const { loadCodeowners, getEffectiveOwners } = require('./.github/scripts/codeowners.js');
|
||||
|
||||
const owner = context.repo.owner;
|
||||
const repo = context.repo.repo;
|
||||
const pr_number = context.payload.pull_request.number;
|
||||
@@ -38,12 +48,15 @@ jobs:
|
||||
const BOT_COMMENT_MARKER = '<!-- codeowner-review-request-bot -->';
|
||||
|
||||
try {
|
||||
// Get the list of changed files in this PR
|
||||
const { data: files } = await github.rest.pulls.listFiles({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
// Get the list of changed files in this PR (with pagination)
|
||||
const files = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
}
|
||||
);
|
||||
|
||||
const changedFiles = files.map(file => file.filename);
|
||||
console.log(`Found ${changedFiles.length} changed files`);
|
||||
@@ -53,32 +66,10 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch CODEOWNERS file from root
|
||||
const { data: codeownersFile } = await github.rest.repos.getContent({
|
||||
owner,
|
||||
repo,
|
||||
path: 'CODEOWNERS',
|
||||
ref: context.payload.pull_request.base.sha
|
||||
});
|
||||
const codeownersContent = Buffer.from(codeownersFile.content, 'base64').toString('utf8');
|
||||
// Parse CODEOWNERS from the checked-out base branch
|
||||
const codeownersPatterns = loadCodeowners();
|
||||
|
||||
// Parse CODEOWNERS file to extract all patterns and their owners
|
||||
const codeownersLines = codeownersContent.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line && !line.startsWith('#'));
|
||||
|
||||
const codeownersPatterns = [];
|
||||
|
||||
// Convert CODEOWNERS pattern to regex (robust glob handling)
|
||||
function globToRegex(pattern) {
|
||||
// Escape regex special characters except for glob wildcards
|
||||
let regexStr = pattern
|
||||
.replace(/([.+^=!:${}()|[\]\\])/g, '\\$1') // escape regex chars
|
||||
.replace(/\*\*/g, '.*') // globstar
|
||||
.replace(/\*/g, '[^/]*') // single star
|
||||
.replace(/\?/g, '.'); // question mark
|
||||
return new RegExp('^' + regexStr + '$');
|
||||
}
|
||||
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
|
||||
|
||||
// Helper function to create comment body
|
||||
function createCommentBody(reviewersList, teamsList, matchedFileCount, isSuccessful = true) {
|
||||
@@ -93,50 +84,11 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
for (const line of codeownersLines) {
|
||||
const parts = line.split(/\s+/);
|
||||
if (parts.length < 2) continue;
|
||||
|
||||
const pattern = parts[0];
|
||||
const owners = parts.slice(1);
|
||||
|
||||
// Use robust glob-to-regex conversion
|
||||
const regex = globToRegex(pattern);
|
||||
codeownersPatterns.push({ pattern, regex, owners });
|
||||
}
|
||||
|
||||
console.log(`Parsed ${codeownersPatterns.length} codeowner patterns`);
|
||||
|
||||
// Match changed files against CODEOWNERS patterns
|
||||
const matchedOwners = new Set();
|
||||
const matchedTeams = new Set();
|
||||
const fileMatches = new Map(); // Track which files matched which patterns
|
||||
|
||||
for (const file of changedFiles) {
|
||||
for (const { pattern, regex, owners } of codeownersPatterns) {
|
||||
if (regex.test(file)) {
|
||||
console.log(`File '${file}' matches pattern '${pattern}' with owners: ${owners.join(', ')}`);
|
||||
|
||||
if (!fileMatches.has(file)) {
|
||||
fileMatches.set(file, []);
|
||||
}
|
||||
fileMatches.get(file).push({ pattern, owners });
|
||||
|
||||
// Add owners to the appropriate set (remove @ prefix)
|
||||
for (const owner of owners) {
|
||||
const cleanOwner = owner.startsWith('@') ? owner.slice(1) : owner;
|
||||
if (cleanOwner.includes('/')) {
|
||||
// Team mention (org/team-name)
|
||||
const teamName = cleanOwner.split('/')[1];
|
||||
matchedTeams.add(teamName);
|
||||
} else {
|
||||
// Individual user
|
||||
matchedOwners.add(cleanOwner);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Match changed files against CODEOWNERS patterns using last-match-wins semantics
|
||||
const effective = getEffectiveOwners(changedFiles, codeownersPatterns);
|
||||
const matchedOwners = effective.users;
|
||||
const matchedTeams = effective.teams;
|
||||
const matchedFileCount = effective.matchedFileCount;
|
||||
|
||||
if (matchedOwners.size === 0 && matchedTeams.size === 0) {
|
||||
console.log('No codeowners found for any changed files');
|
||||
@@ -170,11 +122,14 @@ jobs:
|
||||
}
|
||||
|
||||
// Check for completed reviews to avoid re-requesting users who have already reviewed
|
||||
const { data: reviews } = await github.rest.pulls.listReviews({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
});
|
||||
const reviews = await github.paginate(
|
||||
github.rest.pulls.listReviews,
|
||||
{
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pr_number
|
||||
}
|
||||
);
|
||||
|
||||
const reviewedUsers = new Set();
|
||||
reviews.forEach(review => {
|
||||
@@ -247,7 +202,7 @@ jobs:
|
||||
}
|
||||
|
||||
const totalReviewers = reviewersList.length + teamsList.length;
|
||||
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${fileMatches.size} matched files`);
|
||||
console.log(`Requesting reviews from ${reviewersList.length} users and ${teamsList.length} teams for ${matchedFileCount} matched files`);
|
||||
|
||||
// Request reviews
|
||||
try {
|
||||
@@ -279,7 +234,7 @@ jobs:
|
||||
|
||||
// Only add a comment if there are new codeowners to mention (not previously pinged)
|
||||
if (reviewersList.length > 0 || teamsList.length > 0) {
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, true);
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, true);
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
@@ -297,7 +252,7 @@ jobs:
|
||||
|
||||
// Only try to add a comment if there are new codeowners to mention
|
||||
if (reviewersList.length > 0 || teamsList.length > 0) {
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, fileMatches.size, false);
|
||||
const commentBody = createCommentBody(reviewersList, teamsList, matchedFileCount, false);
|
||||
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@45cbd0c69e560cd9e7cd7f8c32362050c9b7ded2 # v4.32.2
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
name: PR Title Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, edited, synchronize, reopened]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
script: |
|
||||
const {
|
||||
detectComponents,
|
||||
hasCoreChanges,
|
||||
hasDashboardChanges,
|
||||
hasGitHubActionsChanges,
|
||||
} = require('./.github/scripts/detect-tags.js');
|
||||
|
||||
const title = context.payload.pull_request.title;
|
||||
const author = context.payload.pull_request.user.login;
|
||||
|
||||
// Skip bot PRs (e.g. dependabot) - they have their own title format
|
||||
if (author === 'dependabot[bot]') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Block titles starting with "word:" or "word(scope):" patterns
|
||||
const commitStylePattern = /^\w+(\(.*?\))?[!]?\s*:/;
|
||||
if (commitStylePattern.test(title)) {
|
||||
core.setFailed(
|
||||
`PR title should not start with a "prefix:" style format.\n` +
|
||||
`Please use the format: [component] Brief description\n`
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get changed files to detect tags
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.issue.number,
|
||||
});
|
||||
const filenames = files.map(f => f.filename);
|
||||
|
||||
// Detect tags from changed files using shared logic
|
||||
const tags = new Set();
|
||||
|
||||
for (const comp of detectComponents(filenames)) {
|
||||
tags.add(comp);
|
||||
}
|
||||
if (hasCoreChanges(filenames)) tags.add('core');
|
||||
if (hasDashboardChanges(filenames)) tags.add('dashboard');
|
||||
if (hasGitHubActionsChanges(filenames)) tags.add('ci');
|
||||
|
||||
if (tags.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for angle brackets not wrapped in backticks.
|
||||
// Astro docs MDX treats bare < as JSX component opening tags.
|
||||
const stripped = title.replace(/`[^`]*`/g, '');
|
||||
if (/[<>]/.test(stripped)) {
|
||||
core.setFailed(
|
||||
'PR title contains `<` or `>` not wrapped in backticks.\n' +
|
||||
'Astro docs MDX interprets bare `<` as JSX components.\n' +
|
||||
'Please wrap angle brackets with backticks, e.g.: [component] Add `<feature>` support'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check title starts with [tag] prefix
|
||||
const bracketPattern = /^\[\w+\]/;
|
||||
if (!bracketPattern.test(title)) {
|
||||
const suggestion = [...tags].map(c => `[${c}]`).join('');
|
||||
// Skip if the suggested prefix would be too long for a readable title
|
||||
if (suggestion.length > 40) {
|
||||
return;
|
||||
}
|
||||
core.setFailed(
|
||||
`PR modifies: ${[...tags].join(', ')}\n` +
|
||||
`Title must start with a [tag] prefix.\n` +
|
||||
`Suggested: ${suggestion} <description>`
|
||||
);
|
||||
}
|
||||
@@ -99,15 +99,15 @@ jobs:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -138,7 +138,7 @@ jobs:
|
||||
# version: ${{ needs.init.outputs.tag }}
|
||||
|
||||
- name: Upload digests
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: digests-${{ matrix.platform.arch }}
|
||||
path: /tmp/digests
|
||||
@@ -171,24 +171,24 @@ jobs:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
merge-multiple: true
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: matrix.registry == 'ghcr'
|
||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Stale
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||
with:
|
||||
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
||||
remove-stale-when-updated: true
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: 3.13
|
||||
python-version: "3.14"
|
||||
|
||||
- name: Install Home Assistant
|
||||
run: |
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.15.0
|
||||
rev: v0.15.6
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
@@ -37,7 +37,7 @@ repos:
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v3.20.0
|
||||
rev: v3.21.2
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py311-plus]
|
||||
|
||||
+14
-2
@@ -54,6 +54,8 @@ esphome/components/atm90e32/* @circuitsetup @descipher
|
||||
esphome/components/audio/* @kahrendt
|
||||
esphome/components/audio_adc/* @kbx81
|
||||
esphome/components/audio_dac/* @kbx81
|
||||
esphome/components/audio_file/* @kahrendt
|
||||
esphome/components/audio_file/media_source/* @kahrendt
|
||||
esphome/components/axs15231/* @clydebarrow
|
||||
esphome/components/b_parasite/* @rbaron
|
||||
esphome/components/ballu/* @bazuchan
|
||||
@@ -130,6 +132,7 @@ esphome/components/dashboard_import/* @esphome/core
|
||||
esphome/components/datetime/* @jesserockz @rfdarter
|
||||
esphome/components/debug/* @esphome/core
|
||||
esphome/components/delonghi/* @grob6000
|
||||
esphome/components/dew_point/* @CFlix
|
||||
esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||
esphome/components/dht/* @OttoWinter
|
||||
@@ -213,6 +216,7 @@ esphome/components/hbridge/light/* @DotNetDann
|
||||
esphome/components/hbridge/switch/* @dwmw2
|
||||
esphome/components/hc8/* @omartijn
|
||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/hdc302x/* @joshuasing
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
@@ -240,7 +244,6 @@ esphome/components/hyt271/* @Philippe12
|
||||
esphome/components/i2c/* @esphome/core
|
||||
esphome/components/i2c_device/* @gabest11
|
||||
esphome/components/i2s_audio/* @jesserockz
|
||||
esphome/components/i2s_audio/media_player/* @jesserockz
|
||||
esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
@@ -315,6 +318,7 @@ esphome/components/mcp9808/* @k7hpn
|
||||
esphome/components/md5/* @esphome/core
|
||||
esphome/components/mdns/* @esphome/core
|
||||
esphome/components/media_player/* @jesserockz
|
||||
esphome/components/media_source/* @kahrendt
|
||||
esphome/components/micro_wake_word/* @jesserockz @kahrendt
|
||||
esphome/components/micronova/* @edenhaus @jorre05
|
||||
esphome/components/microphone/* @jesserockz @kahrendt
|
||||
@@ -406,11 +410,13 @@ esphome/components/restart/* @esphome/core
|
||||
esphome/components/rf_bridge/* @jesserockz
|
||||
esphome/components/rgbct/* @jesserockz
|
||||
esphome/components/rp2040/* @jesserockz
|
||||
esphome/components/rp2040_ble/* @bdraco
|
||||
esphome/components/rp2040_pio_led_strip/* @Papa-DMan
|
||||
esphome/components/rp2040_pwm/* @jesserockz
|
||||
esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||
esphome/components/rtl87xx/* @kuba2k2
|
||||
esphome/components/rtttl/* @glmnet
|
||||
esphome/components/rtttl/* @glmnet @ximex
|
||||
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
@@ -427,8 +433,10 @@ esphome/components/select/* @esphome/core
|
||||
esphome/components/sen0321/* @notjj
|
||||
esphome/components/sen21231/* @shreyaskarnik
|
||||
esphome/components/sen5x/* @martgras
|
||||
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
|
||||
esphome/components/sensirion_common/* @martgras
|
||||
esphome/components/sensor/* @esphome/core
|
||||
esphome/components/serial_proxy/* @kbx81
|
||||
esphome/components/sfa30/* @ghsensdev
|
||||
esphome/components/sgp40/* @SenexCrenshaw
|
||||
esphome/components/sgp4x/* @martgras @SenexCrenshaw
|
||||
@@ -449,8 +457,11 @@ esphome/components/sn74hc165/* @jesserockz
|
||||
esphome/components/socket/* @esphome/core
|
||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
|
||||
esphome/components/sound_level/* @kahrendt
|
||||
esphome/components/spa06_base/* @danielkent-net
|
||||
esphome/components/spa06_i2c/* @danielkent-net
|
||||
esphome/components/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
|
||||
esphome/components/speaker_source/* @kahrendt
|
||||
esphome/components/spi/* @clydebarrow @esphome/core
|
||||
esphome/components/spi_device/* @clydebarrow
|
||||
esphome/components/spi_led_strip/* @clydebarrow
|
||||
@@ -581,6 +592,7 @@ esphome/components/xl9535/* @mreditor97
|
||||
esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
|
||||
esphome/components/xxtea/* @clydebarrow
|
||||
esphome/components/zephyr/* @tomaszduda23
|
||||
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
|
||||
esphome/components/zhlt01/* @cfeenstra1024
|
||||
esphome/components/zigbee/* @tomaszduda23
|
||||
esphome/components/zio_ultrasonic/* @kahrendt
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.2.1
|
||||
PROJECT_NUMBER = 2026.4.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
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/)
|
||||
# ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/) [](https://codspeed.io/esphome/esphome)
|
||||
|
||||
<a href="https://esphome.io/">
|
||||
<picture>
|
||||
|
||||
+294
-30
@@ -1,6 +1,7 @@
|
||||
# PYTHON_ARGCOMPLETE_OK
|
||||
import argparse
|
||||
from collections.abc import Callable
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import getpass
|
||||
@@ -9,6 +10,8 @@ import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Protocol
|
||||
@@ -23,6 +26,7 @@ import esphome.codegen as cg
|
||||
from esphome.config import iter_component_configs, read_config, strip_default_ids
|
||||
from esphome.const import (
|
||||
ALLOWED_NAME_CHARS,
|
||||
ARGUMENT_HELP_DEVICE,
|
||||
CONF_API,
|
||||
CONF_BAUD_RATE,
|
||||
CONF_BROKER,
|
||||
@@ -43,7 +47,9 @@ from esphome.const import (
|
||||
CONF_SUBSTITUTIONS,
|
||||
CONF_TOPIC,
|
||||
ENV_NOGITIGNORE,
|
||||
KEY_CORE,
|
||||
KEY_NATIVE_IDF,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
@@ -55,7 +61,11 @@ from esphome.helpers import get_bool_env, indent, is_ip_address
|
||||
from esphome.log import AnsiFore, color, setup_log
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import (
|
||||
PICOTOOL_PACKAGE,
|
||||
detect_rp2040_bootsel,
|
||||
get_picotool_path,
|
||||
get_serial_ports,
|
||||
is_picotool_usb_permission_error,
|
||||
list_yaml_files,
|
||||
run_external_command,
|
||||
run_external_process,
|
||||
@@ -64,9 +74,26 @@ from esphome.util import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ESPHOME_COMMAND = [sys.executable, "-m", "esphome"]
|
||||
|
||||
# Maximum buffer size for serial log reading to prevent unbounded memory growth
|
||||
SERIAL_BUFFER_MAX_SIZE = 65536
|
||||
|
||||
_RP2040_BOOTSEL_INSTRUCTIONS = (
|
||||
"To enter BOOTSEL mode:\n"
|
||||
" 1. Unplug the device\n"
|
||||
" 2. Hold the BOOT/BOOTSEL button\n"
|
||||
" 3. Plug in the USB cable while holding the button\n"
|
||||
" 4. Release the button - the device should appear as a USB drive (RPI-RP2)\n"
|
||||
"Then run the upload command again."
|
||||
)
|
||||
|
||||
_RP2040_UDEV_HINT = (
|
||||
"You may need to add a udev rule for RP2040 devices. "
|
||||
"See: https://github.com/raspberrypi/picotool"
|
||||
"/blob/master/udev/60-picotool.rules"
|
||||
)
|
||||
|
||||
# Special non-component keys that appear in configs
|
||||
_NON_COMPONENT_KEYS = frozenset(
|
||||
{
|
||||
@@ -162,6 +189,7 @@ class PortType(StrEnum):
|
||||
NETWORK = "NETWORK"
|
||||
MQTT = "MQTT"
|
||||
MQTTIP = "MQTTIP"
|
||||
BOOTSEL = "BOOTSEL"
|
||||
|
||||
|
||||
# Magic MQTT port types that require special handling
|
||||
@@ -240,6 +268,19 @@ def choose_upload_log_host(
|
||||
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
|
||||
]
|
||||
|
||||
# Add RP2040 BOOTSEL device option when uploading
|
||||
bootsel_permission_error = False
|
||||
if (
|
||||
purpose == Purpose.UPLOADING
|
||||
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
|
||||
and (picotool := _find_picotool()) is not None
|
||||
):
|
||||
bootsel = detect_rp2040_bootsel(picotool)
|
||||
if bootsel.device_count > 0:
|
||||
options.append(("RP2040 BOOTSEL (via picotool)", "BOOTSEL"))
|
||||
elif bootsel.permission_error:
|
||||
bootsel_permission_error = True
|
||||
|
||||
if purpose == Purpose.LOGGING:
|
||||
if has_mqtt_logging():
|
||||
mqtt_config = CORE.config[CONF_MQTT]
|
||||
@@ -257,6 +298,25 @@ def choose_upload_log_host(
|
||||
if has_mqtt_ip_lookup():
|
||||
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
|
||||
|
||||
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
|
||||
if (
|
||||
purpose == Purpose.UPLOADING
|
||||
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
|
||||
and not any(get_port_type(opt[1]) == PortType.BOOTSEL for opt in options)
|
||||
):
|
||||
if bootsel_permission_error:
|
||||
_LOGGER.warning(
|
||||
"An RP2040 device in BOOTSEL mode was detected but could "
|
||||
"not be accessed due to USB permissions."
|
||||
)
|
||||
if sys.platform.startswith("linux"):
|
||||
_LOGGER.warning(_RP2040_UDEV_HINT)
|
||||
if not options:
|
||||
raise EsphomeError(
|
||||
f"No RP2040 device found. {_RP2040_BOOTSEL_INSTRUCTIONS}"
|
||||
)
|
||||
_LOGGER.info("Tip: %s", _RP2040_BOOTSEL_INSTRUCTIONS)
|
||||
|
||||
if check_default is not None and check_default in [opt[1] for opt in options]:
|
||||
return [check_default]
|
||||
return [choose_prompt(options, purpose=purpose)]
|
||||
@@ -403,10 +463,13 @@ def get_port_type(port: str) -> PortType:
|
||||
|
||||
Returns:
|
||||
PortType.SERIAL for serial ports (/dev/ttyUSB0, COM1, etc.)
|
||||
PortType.BOOTSEL for RP2040 BOOTSEL upload via picotool
|
||||
PortType.MQTT for MQTT logging
|
||||
PortType.MQTTIP for MQTT IP lookup
|
||||
PortType.NETWORK for IP addresses, hostnames, or mDNS names
|
||||
"""
|
||||
if port == "BOOTSEL":
|
||||
return PortType.BOOTSEL
|
||||
if port.startswith("/") or port.startswith("COM"):
|
||||
return PortType.SERIAL
|
||||
if port == "MQTT":
|
||||
@@ -431,6 +494,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
return 1
|
||||
_LOGGER.info("Starting log output from %s with baud rate %s", port, baud_rate)
|
||||
|
||||
process_stacktrace = None
|
||||
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
process_stacktrace = getattr(module, "process_stacktrace")
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
backtrace_state = False
|
||||
ser = serial.Serial()
|
||||
ser.baudrate = baud_rate
|
||||
@@ -472,9 +543,14 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
)
|
||||
safe_print(parser.parse_line(line, time_str))
|
||||
|
||||
backtrace_state = platformio_api.process_stacktrace(
|
||||
config, line, backtrace_state=backtrace_state
|
||||
)
|
||||
if process_stacktrace:
|
||||
backtrace_state = process_stacktrace(
|
||||
config, line, backtrace_state
|
||||
)
|
||||
else:
|
||||
backtrace_state = platformio_api.process_stacktrace(
|
||||
config, line, backtrace_state=backtrace_state
|
||||
)
|
||||
except serial.SerialException:
|
||||
_LOGGER.error("Serial port closed!")
|
||||
return 0
|
||||
@@ -614,6 +690,47 @@ def _check_and_emit_build_info() -> None:
|
||||
)
|
||||
|
||||
|
||||
def _get_configured_xtal_freq() -> int | None:
|
||||
"""Read the configured crystal frequency from the sdkconfig file."""
|
||||
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
|
||||
if not sdkconfig_path.is_file():
|
||||
return None
|
||||
with suppress(OSError, ValueError):
|
||||
content = sdkconfig_path.read_text()
|
||||
for line in content.splitlines():
|
||||
if line.startswith("CONFIG_XTAL_FREQ="):
|
||||
return int(line.split("=", 1)[1])
|
||||
return None
|
||||
|
||||
|
||||
def _make_crystal_freq_callback(
|
||||
configured_freq: int,
|
||||
) -> Callable[[str], str | None]:
|
||||
"""Create a callback that checks esptool crystal frequency output."""
|
||||
crystal_re = re.compile(r"Crystal frequency:\s+(\d+)\s*MHz")
|
||||
|
||||
def check_crystal_line(line: str) -> str | None:
|
||||
if not (match := crystal_re.search(line)):
|
||||
return None
|
||||
detected = int(match.group(1))
|
||||
if detected == configured_freq:
|
||||
return None
|
||||
return (
|
||||
f"\n\033[33mWARNING: Crystal frequency mismatch! "
|
||||
f"Device reports {detected}MHz but firmware is configured "
|
||||
f"for {configured_freq}MHz.\n"
|
||||
f"UART logging and other clock-dependent features will not "
|
||||
f"work correctly.\n"
|
||||
f"Set the correct crystal frequency with sdkconfig_options:\n"
|
||||
f" esp32:\n"
|
||||
f" framework:\n"
|
||||
f" sdkconfig_options:\n"
|
||||
f" CONFIG_XTAL_FREQ_{detected}: 'y'\033[0m\n\n"
|
||||
)
|
||||
|
||||
return check_crystal_line
|
||||
|
||||
|
||||
def upload_using_esptool(
|
||||
config: ConfigType, port: str, file: str, speed: int
|
||||
) -> str | int:
|
||||
@@ -642,6 +759,14 @@ def upload_using_esptool(
|
||||
|
||||
mcu = get_esp32_variant().lower()
|
||||
|
||||
line_callbacks: list[Callable[[str], str | None]] = []
|
||||
if (
|
||||
CORE.is_esp32
|
||||
and file is None
|
||||
and (configured_freq := _get_configured_xtal_freq()) is not None
|
||||
):
|
||||
line_callbacks.append(_make_crystal_freq_callback(configured_freq))
|
||||
|
||||
def run_esptool(baud_rate):
|
||||
cmd = [
|
||||
"esptool",
|
||||
@@ -666,9 +791,13 @@ def upload_using_esptool(
|
||||
if os.environ.get("ESPHOME_USE_SUBPROCESS") is None:
|
||||
import esptool
|
||||
|
||||
return run_external_command(esptool.main, *cmd) # pylint: disable=no-member
|
||||
return run_external_command(
|
||||
esptool.main, # pylint: disable=no-member
|
||||
*cmd,
|
||||
line_callbacks=line_callbacks,
|
||||
)
|
||||
|
||||
return run_external_process(*cmd)
|
||||
return run_external_process(*cmd, line_callbacks=line_callbacks)
|
||||
|
||||
rc = run_esptool(first_baudrate)
|
||||
if rc == 0 or first_baudrate == 115200:
|
||||
@@ -681,15 +810,140 @@ def upload_using_esptool(
|
||||
return run_esptool(115200)
|
||||
|
||||
|
||||
def upload_using_platformio(config: ConfigType, port: str):
|
||||
def upload_using_platformio(config: ConfigType, port: str) -> int:
|
||||
from esphome import platformio_api
|
||||
|
||||
# RP2040 platform-raspberrypi build recipe expects firmware.bin.signed for
|
||||
# the upload target, but 'nobuild' skips the build phase that creates it.
|
||||
# Create it here so the upload doesn't fail.
|
||||
if CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040:
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
build_dir = Path(idedata.firmware_elf_path).parent
|
||||
firmware_bin = build_dir / "firmware.bin"
|
||||
signed_bin = build_dir / "firmware.bin.signed"
|
||||
if firmware_bin.is_file() and not signed_bin.is_file():
|
||||
shutil.copy2(firmware_bin, signed_bin)
|
||||
|
||||
upload_args = ["-t", "upload", "-t", "nobuild"]
|
||||
if port is not None:
|
||||
upload_args += ["--upload-port", port]
|
||||
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
|
||||
|
||||
|
||||
def _find_picotool() -> Path | None:
|
||||
"""Find the picotool binary from PlatformIO packages."""
|
||||
from esphome import platformio_api
|
||||
|
||||
try:
|
||||
idedata = platformio_api.get_idedata(CORE.config)
|
||||
except Exception: # noqa: BLE001 # pylint: disable=broad-except
|
||||
return None
|
||||
return get_picotool_path(idedata.cc_path)
|
||||
|
||||
|
||||
def upload_using_picotool(config: ConfigType) -> int:
|
||||
"""Upload firmware to RP2040 in BOOTSEL mode using picotool.
|
||||
|
||||
Uses picotool to load the ELF firmware directly via USB, avoiding
|
||||
the mass storage copy approach that causes "disk not ejected properly"
|
||||
warnings on macOS.
|
||||
"""
|
||||
from esphome import platformio_api
|
||||
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
firmware_elf = Path(idedata.firmware_elf_path)
|
||||
|
||||
if not firmware_elf.is_file():
|
||||
_LOGGER.error(
|
||||
"Firmware ELF file not found at %s. "
|
||||
"Make sure the project has been compiled first.",
|
||||
firmware_elf,
|
||||
)
|
||||
return 1
|
||||
|
||||
picotool = get_picotool_path(idedata.cc_path)
|
||||
if picotool is None:
|
||||
_LOGGER.error(
|
||||
"picotool not found. Ensure the RP2040 PlatformIO platform "
|
||||
"is installed (%s).",
|
||||
PICOTOOL_PACKAGE,
|
||||
)
|
||||
return 1
|
||||
|
||||
_LOGGER.info("Uploading firmware to RP2040 via picotool...")
|
||||
try:
|
||||
# Don't capture stdout — let picotool write directly to the terminal
|
||||
# so progress bars display in real-time with \r updates.
|
||||
# Capture stderr only so we can detect permission errors.
|
||||
result = subprocess.run(
|
||||
[str(picotool), "load", "-v", "-x", str(firmware_elf)],
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=60,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
_LOGGER.error("picotool upload timed out after 60 seconds.")
|
||||
return 1
|
||||
except OSError as err:
|
||||
_LOGGER.error("Failed to run picotool: %s", err)
|
||||
return 1
|
||||
|
||||
if result.returncode != 0:
|
||||
stderr = result.stderr.decode("utf-8", errors="replace").strip()
|
||||
if stderr:
|
||||
for line in stderr.splitlines():
|
||||
safe_print(line)
|
||||
if is_picotool_usb_permission_error(stderr):
|
||||
msg = "Permission denied accessing USB device."
|
||||
if sys.platform.startswith("linux"):
|
||||
msg += f" {_RP2040_UDEV_HINT}"
|
||||
_LOGGER.error(msg)
|
||||
else:
|
||||
_LOGGER.error("picotool upload failed (exit code %d).", result.returncode)
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def _wait_for_serial_port(
|
||||
port: str | None = None,
|
||||
timeout: float = 30.0,
|
||||
known_ports: set[str] | None = None,
|
||||
) -> None:
|
||||
"""Wait for a serial port to appear, e.g. after a device reboot.
|
||||
|
||||
USB-CDC devices disappear briefly after flashing while the device
|
||||
reboots and re-enumerates on the USB bus.
|
||||
|
||||
If port is given, wait for that specific path. If known_ports is
|
||||
given, wait for a new port that wasn't in the set. Otherwise wait
|
||||
for any serial port to appear.
|
||||
"""
|
||||
|
||||
def _port_found() -> bool:
|
||||
if port is not None:
|
||||
if os.name == "posix":
|
||||
return os.path.exists(port)
|
||||
return any(p.path == port for p in get_serial_ports())
|
||||
ports = get_serial_ports()
|
||||
if known_ports is not None:
|
||||
return any(p.path not in known_ports for p in ports)
|
||||
return bool(ports)
|
||||
|
||||
if _port_found():
|
||||
return
|
||||
if port is not None:
|
||||
_LOGGER.info("Waiting for %s to come online...", port)
|
||||
else:
|
||||
_LOGGER.info("Waiting for device to reboot...")
|
||||
start = time.monotonic()
|
||||
while time.monotonic() - start < timeout:
|
||||
time.sleep(0.05)
|
||||
if _port_found():
|
||||
time.sleep(0.05)
|
||||
return
|
||||
|
||||
|
||||
def check_permissions(port: str):
|
||||
if os.name == "posix" and get_port_type(port) == PortType.SERIAL:
|
||||
# Check if we can open selected serial port
|
||||
@@ -719,7 +973,15 @@ def upload_program(
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
if get_port_type(host) == PortType.SERIAL:
|
||||
port_type = get_port_type(host)
|
||||
|
||||
if port_type == PortType.BOOTSEL:
|
||||
exit_code = upload_using_picotool(config)
|
||||
# Return None for device - BOOTSEL can't be used for logging,
|
||||
# so command_run will show the interactive chooser for log source
|
||||
return exit_code, None
|
||||
|
||||
if port_type == PortType.SERIAL:
|
||||
check_permissions(host)
|
||||
|
||||
exit_code = 1
|
||||
@@ -773,6 +1035,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
|
||||
port_type = get_port_type(port)
|
||||
|
||||
if port_type == PortType.SERIAL:
|
||||
_wait_for_serial_port(port)
|
||||
check_permissions(port)
|
||||
return run_miniterm(config, port, args)
|
||||
|
||||
@@ -911,6 +1174,9 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
purpose=Purpose.UPLOADING,
|
||||
)
|
||||
|
||||
# Snapshot current serial ports before upload so we can detect new ones
|
||||
pre_upload_ports = {p.path for p in get_serial_ports()}
|
||||
|
||||
exit_code, successful_device = upload_program(config, args, devices)
|
||||
if exit_code == 0:
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
@@ -921,6 +1187,19 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if args.no_logs:
|
||||
return 0
|
||||
|
||||
# After BOOTSEL upload, wait for a new serial port to appear
|
||||
# so it shows up in the log chooser
|
||||
if (
|
||||
successful_device is None
|
||||
and CORE.data.get(KEY_CORE, {}).get(KEY_TARGET_PLATFORM) == PLATFORM_RP2040
|
||||
):
|
||||
_wait_for_serial_port(known_ports=pre_upload_ports)
|
||||
# If exactly one new serial port appeared, use it directly
|
||||
serial_ports = get_serial_ports()
|
||||
new_ports = [p for p in serial_ports if p.path not in pre_upload_ports]
|
||||
if len(new_ports) == 1:
|
||||
successful_device = new_ports[0].path
|
||||
|
||||
# For logs, prefer the device we successfully uploaded to
|
||||
devices = choose_upload_log_host(
|
||||
default=successful_device,
|
||||
@@ -944,12 +1223,6 @@ def command_clean_all(args: ArgsProtocol) -> int | None:
|
||||
return 0
|
||||
|
||||
|
||||
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.get_fingerprint(config)
|
||||
|
||||
|
||||
def command_version(args: ArgsProtocol) -> int | None:
|
||||
safe_print(f"Version: {const.__version__}")
|
||||
return 0
|
||||
@@ -1036,9 +1309,8 @@ def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
files = list_yaml_files(args.configuration)
|
||||
|
||||
def build_command(f):
|
||||
if CORE.dashboard:
|
||||
return ["esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"]
|
||||
return ["esphome", "run", f, "--no-logs", "--device", "OTA"]
|
||||
dashboard = ["--dashboard"] if CORE.dashboard else []
|
||||
return [*ESPHOME_COMMAND, *dashboard, "run", f, "--no-logs", "--device", "OTA"]
|
||||
|
||||
return run_multiple_configs(files, build_command)
|
||||
|
||||
@@ -1187,7 +1459,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
|
||||
new_path.write_text(new_raw, encoding="utf-8")
|
||||
|
||||
rc = run_external_process("esphome", "config", str(new_path))
|
||||
rc = run_external_process(*ESPHOME_COMMAND, "config", str(new_path))
|
||||
if rc != 0:
|
||||
print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
|
||||
new_path.unlink()
|
||||
@@ -1205,7 +1477,7 @@ def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
cli_args.insert(0, "--dashboard")
|
||||
|
||||
try:
|
||||
rc = run_external_process("esphome", *cli_args)
|
||||
rc = run_external_process(*ESPHOME_COMMAND, *cli_args)
|
||||
except KeyboardInterrupt:
|
||||
rc = 1
|
||||
if rc != 0:
|
||||
@@ -1237,7 +1509,6 @@ POST_CONFIG_ACTIONS = {
|
||||
"run": command_run,
|
||||
"clean": command_clean,
|
||||
"clean-mqtt": command_clean_mqtt,
|
||||
"mqtt-fingerprint": command_mqtt_fingerprint,
|
||||
"idedata": command_idedata,
|
||||
"rename": command_rename,
|
||||
"discover": command_discover,
|
||||
@@ -1361,7 +1632,7 @@ def parse_args(argv):
|
||||
parser_upload.add_argument(
|
||||
"--device",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
help=ARGUMENT_HELP_DEVICE,
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--upload_speed",
|
||||
@@ -1384,7 +1655,7 @@ def parse_args(argv):
|
||||
parser_logs.add_argument(
|
||||
"--device",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
help=ARGUMENT_HELP_DEVICE,
|
||||
)
|
||||
parser_logs.add_argument(
|
||||
"--reset",
|
||||
@@ -1414,7 +1685,7 @@ def parse_args(argv):
|
||||
parser_run.add_argument(
|
||||
"--device",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
help=ARGUMENT_HELP_DEVICE,
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--upload_speed",
|
||||
@@ -1451,13 +1722,6 @@ def parse_args(argv):
|
||||
)
|
||||
parser_wizard.add_argument("configuration", help="Your YAML configuration file.")
|
||||
|
||||
parser_fingerprint = subparsers.add_parser(
|
||||
"mqtt-fingerprint", help="Get the SSL fingerprint from a MQTT broker."
|
||||
)
|
||||
parser_fingerprint.add_argument(
|
||||
"configuration", help="Your YAML configuration file(s).", nargs="+"
|
||||
)
|
||||
|
||||
subparsers.add_parser("version", help="Print the ESPHome version and exit.")
|
||||
|
||||
parser_clean = subparsers.add_parser(
|
||||
@@ -1610,7 +1874,7 @@ def run_esphome(argv):
|
||||
# argv[0] is the program path, skip it since we prefix with "esphome"
|
||||
def build_command(f):
|
||||
return (
|
||||
["esphome"]
|
||||
[*ESPHOME_COMMAND]
|
||||
+ [arg for arg in argv[1:] if arg not in args.configuration]
|
||||
+ [str(f)]
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Memory usage analyzer for ESPHome compiled binaries."""
|
||||
|
||||
from collections import defaultdict
|
||||
from collections import Counter, defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -40,6 +40,15 @@ _READELF_SECTION_PATTERN = re.compile(
|
||||
r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)"
|
||||
)
|
||||
|
||||
# Regex for extracting call targets from objdump disassembly
|
||||
# Matches direct call instructions across architectures:
|
||||
# Xtensa: call0/call4/call8/call12/callx0/callx4/callx8/callx12 <addr> <symbol>
|
||||
# ARM: bl/blx <addr> <symbol>
|
||||
# Captures the mangled symbol name inside angle brackets.
|
||||
_CALL_TARGET_PATTERN = re.compile(
|
||||
r"\t(?:call(?:0|4|8|12)|callx(?:0|4|8|12)|blx?)\s+[\da-fA-F]+ <([^>]+)>"
|
||||
)
|
||||
|
||||
# Component category prefixes
|
||||
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
|
||||
_COMPONENT_PREFIX_EXTERNAL = "[external]"
|
||||
@@ -192,20 +201,27 @@ class MemoryAnalyzer:
|
||||
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
|
||||
# Library symbol mapping: symbol_name -> library_name
|
||||
self._lib_symbol_map: dict[str, str] = {}
|
||||
# Source file symbol mapping: symbol_name -> component_name
|
||||
# Used for extern "C" and other symbols without C++ namespace
|
||||
self._source_symbol_map: dict[str, str] = {}
|
||||
# Library dir to name mapping: "lib641" -> "espsoftwareserial",
|
||||
# "espressif__mdns" -> "mdns"
|
||||
self._lib_hash_to_name: dict[str, str] = {}
|
||||
# Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns"
|
||||
self._heuristic_to_lib: dict[str, str] = {}
|
||||
# Function call counts: mangled_name -> call_count
|
||||
self._function_call_counts: Counter[str] = Counter()
|
||||
|
||||
def analyze(self) -> dict[str, ComponentMemory]:
|
||||
"""Analyze the ELF file and return component memory usage."""
|
||||
self._parse_sections()
|
||||
self._parse_symbols()
|
||||
self._scan_libraries()
|
||||
self._scan_source_symbols()
|
||||
self._categorize_symbols()
|
||||
self._analyze_cswtch_symbols()
|
||||
self._analyze_sdk_libraries()
|
||||
self._analyze_function_calls()
|
||||
return dict(self.components)
|
||||
|
||||
def _parse_sections(self) -> None:
|
||||
@@ -351,6 +367,11 @@ class MemoryAnalyzer:
|
||||
if lib_name := self._lib_symbol_map.get(symbol_name):
|
||||
return f"{_COMPONENT_PREFIX_LIB}{lib_name}"
|
||||
|
||||
# Check source file mapping (catches extern "C" functions in ESPHome sources)
|
||||
# Must be before heuristic patterns since source attribution is authoritative
|
||||
if component := self._source_symbol_map.get(symbol_name):
|
||||
return component
|
||||
|
||||
# Check against symbol patterns
|
||||
for component, patterns in SYMBOL_PATTERNS.items():
|
||||
if any(pattern in symbol_name for pattern in patterns):
|
||||
@@ -384,8 +405,9 @@ class MemoryAnalyzer:
|
||||
return
|
||||
|
||||
_LOGGER.info("Demangling %d symbols", len(symbols))
|
||||
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path)
|
||||
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
|
||||
demangled = batch_demangle(symbols, objdump_path=self.objdump_path)
|
||||
self._demangle_cache.update(demangled)
|
||||
_LOGGER.info("Successfully demangled %d symbols", len(demangled))
|
||||
|
||||
def _demangle_symbol(self, symbol: str) -> str:
|
||||
"""Get demangled C++ symbol name from cache."""
|
||||
@@ -640,6 +662,7 @@ class MemoryAnalyzer:
|
||||
return None
|
||||
|
||||
symbol_map: dict[str, str] = {}
|
||||
source_symbol_map: dict[str, str] = {}
|
||||
current_symbol: str | None = None
|
||||
section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.")
|
||||
|
||||
@@ -675,9 +698,18 @@ class MemoryAnalyzer:
|
||||
if dir_key in source_path:
|
||||
symbol_map[current_symbol] = lib_name
|
||||
break
|
||||
else:
|
||||
# Map ESPHome source files to components for extern "C"
|
||||
# and other symbols without C++ namespace
|
||||
component = self._source_file_to_component(source_path)
|
||||
if component.startswith(
|
||||
(_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL)
|
||||
):
|
||||
source_symbol_map[current_symbol] = component
|
||||
|
||||
current_symbol = None
|
||||
|
||||
self._source_symbol_map = source_symbol_map
|
||||
return symbol_map or None
|
||||
|
||||
def _scan_libraries(self) -> None:
|
||||
@@ -728,6 +760,112 @@ class MemoryAnalyzer:
|
||||
len(libraries),
|
||||
)
|
||||
|
||||
def _scan_source_symbols(self) -> None:
|
||||
"""Scan ESPHome source object files to map extern "C" symbols to components.
|
||||
|
||||
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
|
||||
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
|
||||
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
|
||||
|
||||
Skips scanning if ``_source_symbol_map`` was already populated by
|
||||
``_parse_map_file()``.
|
||||
"""
|
||||
if self._source_symbol_map or not self.nm_path:
|
||||
return
|
||||
|
||||
obj_dir = self._find_object_files_dir()
|
||||
if obj_dir is None:
|
||||
return
|
||||
|
||||
# Find ESPHome source object files
|
||||
esphome_src_dir = obj_dir / "src" / "esphome"
|
||||
if not esphome_src_dir.is_dir():
|
||||
return
|
||||
|
||||
obj_files = sorted(esphome_src_dir.rglob("*.o"))
|
||||
if not obj_files:
|
||||
return
|
||||
|
||||
# Run nm with --print-file-name to get file:symbol mapping
|
||||
result = run_tool(
|
||||
[self.nm_path, "--print-file-name", "-g", "--defined-only"]
|
||||
+ [str(f) for f in obj_files],
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
_LOGGER.debug("nm scan of source objects failed")
|
||||
return
|
||||
|
||||
self._source_symbol_map = self._parse_nm_source_output(result.stdout, obj_dir)
|
||||
if self._source_symbol_map:
|
||||
_LOGGER.info(
|
||||
"Built source symbol map from nm: %d symbols",
|
||||
len(self._source_symbol_map),
|
||||
)
|
||||
|
||||
def _parse_nm_source_output(self, output: str, base_dir: Path) -> dict[str, str]:
|
||||
"""Parse nm output to map non-namespaced symbols to ESPHome components.
|
||||
|
||||
Extracts global defined symbols from ESPHome source object files that
|
||||
don't use C++ namespacing (e.g. ``extern "C"`` functions).
|
||||
|
||||
Args:
|
||||
output: Raw stdout from ``nm --print-file-name -g --defined-only``
|
||||
or ``nm --print-file-name -S``.
|
||||
base_dir: Build directory for computing relative paths.
|
||||
|
||||
Returns:
|
||||
Dict mapping symbol names to component names.
|
||||
"""
|
||||
source_map: dict[str, str] = {}
|
||||
for line in output.splitlines():
|
||||
# Format: /path/to/file.o: addr type name
|
||||
# or: /path/to/file.o: addr size type name (with -S)
|
||||
colon_idx = line.rfind(".o:")
|
||||
if colon_idx == -1:
|
||||
continue
|
||||
|
||||
file_path = line[: colon_idx + 2]
|
||||
fields = line[colon_idx + 3 :].split()
|
||||
if len(fields) < 3:
|
||||
continue
|
||||
|
||||
# With -S flag, format is: addr size type name
|
||||
# Without -S flag: addr type name
|
||||
# type is a single char; size is hex digits
|
||||
# Detect by checking if fields[1] is a single uppercase letter (type)
|
||||
if len(fields[1]) == 1 and fields[1].isalpha():
|
||||
# addr type name
|
||||
sym_type = fields[1]
|
||||
symbol_name = fields[2]
|
||||
elif len(fields) >= 4:
|
||||
# addr size type name
|
||||
sym_type = fields[2]
|
||||
symbol_name = fields[3]
|
||||
else:
|
||||
continue
|
||||
|
||||
# Only global defined symbols (uppercase type)
|
||||
if not sym_type.isupper() or sym_type == "U":
|
||||
continue
|
||||
|
||||
# Skip symbols already in esphome:: namespace
|
||||
if symbol_name.startswith("_ZN7esphome"):
|
||||
continue
|
||||
|
||||
# Make path relative to base_dir for _source_file_to_component
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(base_dir))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
component = self._source_file_to_component(rel_path)
|
||||
if component.startswith(
|
||||
(_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL)
|
||||
):
|
||||
source_map[symbol_name] = component
|
||||
|
||||
return source_map
|
||||
|
||||
def _find_object_files_dir(self) -> Path | None:
|
||||
"""Find the directory containing object files for this build.
|
||||
|
||||
@@ -1011,6 +1149,43 @@ class MemoryAnalyzer:
|
||||
total_size,
|
||||
)
|
||||
|
||||
def _analyze_function_calls(self) -> None:
|
||||
"""Count function call sites by parsing disassembly output.
|
||||
|
||||
Parses direct call instructions (call0/call8/bl/blx) from objdump -d
|
||||
to count how many times each function is called. This helps identify
|
||||
inlining candidates — frequently called small functions benefit most
|
||||
from inlining.
|
||||
"""
|
||||
result = run_tool(
|
||||
[self.objdump_path, "-d", str(self.elf_path)],
|
||||
timeout=60,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
_LOGGER.debug("Failed to disassemble ELF for function call analysis")
|
||||
return
|
||||
|
||||
self._function_call_counts = Counter(
|
||||
match.group(1)
|
||||
for line in result.stdout.splitlines()
|
||||
if (match := _CALL_TARGET_PATTERN.search(line))
|
||||
)
|
||||
|
||||
# Demangle any call targets not already in the cache
|
||||
missing = [
|
||||
name
|
||||
for name in self._function_call_counts
|
||||
if name not in self._demangle_cache
|
||||
]
|
||||
if missing:
|
||||
self._batch_demangle_symbols(missing)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Function call analysis: %d unique targets, %d total calls",
|
||||
len(self._function_call_counts),
|
||||
sum(self._function_call_counts.values()),
|
||||
)
|
||||
|
||||
def get_unattributed_ram(self) -> tuple[int, int, int]:
|
||||
"""Get unattributed RAM sizes (SDK/framework overhead).
|
||||
|
||||
|
||||
@@ -231,6 +231,110 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(f" {size:>6,} B {sym_name}")
|
||||
lines.append("")
|
||||
|
||||
# Number of top called functions to show
|
||||
TOP_CALLS_LIMIT: int = 50
|
||||
# Number of inlining candidates to show
|
||||
INLINE_CANDIDATES_LIMIT: int = 25
|
||||
# Maximum function size in bytes to consider for inlining
|
||||
INLINE_SIZE_THRESHOLD: int = 16
|
||||
|
||||
def _build_symbol_sizes(self) -> dict[str, int]:
|
||||
"""Build a size lookup from all component symbols: mangled_name -> size."""
|
||||
return {
|
||||
symbol: size
|
||||
for symbols in self._component_symbols.values()
|
||||
for symbol, _, size, _ in symbols
|
||||
}
|
||||
|
||||
def _format_call_row(
|
||||
self, index: int, mangled: str, count: int, symbol_sizes: dict[str, int]
|
||||
) -> str:
|
||||
"""Format a single row for call frequency tables."""
|
||||
demangled = self._demangle_cache.get(mangled, mangled)
|
||||
if len(demangled) > 80:
|
||||
demangled = f"{demangled[:77]}..."
|
||||
size = symbol_sizes.get(mangled)
|
||||
size_str = f"{size:>5,} B" if size is not None else " ?"
|
||||
return f"{index:>3} {count:>5} {size_str} {demangled}"
|
||||
|
||||
def _add_call_table_header(self, lines: list[str]) -> None:
|
||||
"""Add the header row for call frequency tables."""
|
||||
lines.append(f"{'#':>3} {'Calls':>5} {'Size':>7} Function")
|
||||
lines.append(f"{'---':>3} {'-----':>5} {'-------':>7} {'-' * 60}")
|
||||
|
||||
def _add_function_call_analysis(self, lines: list[str]) -> None:
|
||||
"""Add function call frequency analysis section.
|
||||
|
||||
Shows the most frequently called functions by call site count.
|
||||
"""
|
||||
self._add_section_header(lines, "Top Called Functions")
|
||||
|
||||
symbol_sizes = self._build_symbol_sizes()
|
||||
|
||||
# Sort by call count descending
|
||||
sorted_calls = sorted(
|
||||
self._function_call_counts.items(), key=lambda x: x[1], reverse=True
|
||||
)
|
||||
|
||||
self._add_call_table_header(lines)
|
||||
|
||||
for i, (mangled, count) in enumerate(sorted_calls[: self.TOP_CALLS_LIMIT]):
|
||||
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
|
||||
|
||||
total_calls = sum(self._function_call_counts.values())
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"Total: {len(self._function_call_counts)} unique targets, "
|
||||
f"{total_calls:,} call sites"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
def _add_inline_candidates(self, lines: list[str]) -> None:
|
||||
"""Add inlining candidates section.
|
||||
|
||||
Shows frequently called functions that are small enough to benefit
|
||||
from inlining (< 16 bytes). These are the best candidates for
|
||||
reducing call overhead.
|
||||
"""
|
||||
self._add_section_header(
|
||||
lines,
|
||||
f"Inlining Candidates (<{self.INLINE_SIZE_THRESHOLD} B, by call count)",
|
||||
)
|
||||
|
||||
symbol_sizes = self._build_symbol_sizes()
|
||||
|
||||
# Filter to small functions with known size, sort by call count
|
||||
candidates = sorted(
|
||||
(
|
||||
(mangled, count)
|
||||
for mangled, count in self._function_call_counts.items()
|
||||
if mangled in symbol_sizes
|
||||
and symbol_sizes[mangled] < self.INLINE_SIZE_THRESHOLD
|
||||
),
|
||||
key=lambda x: x[1],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
if not candidates:
|
||||
lines.append("No candidates found.")
|
||||
lines.append("")
|
||||
return
|
||||
|
||||
self._add_call_table_header(lines)
|
||||
|
||||
for i, (mangled, count) in enumerate(
|
||||
candidates[: self.INLINE_CANDIDATES_LIMIT]
|
||||
):
|
||||
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
|
||||
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"Showing top {min(len(candidates), self.INLINE_CANDIDATES_LIMIT)} "
|
||||
f"of {len(candidates)} functions under "
|
||||
f"{self.INLINE_SIZE_THRESHOLD} B"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
def generate_report(self, detailed: bool = False) -> str:
|
||||
"""Generate a formatted memory report."""
|
||||
components = sorted(
|
||||
@@ -533,6 +637,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
if self._cswtch_symbols:
|
||||
self._add_cswtch_analysis(lines)
|
||||
|
||||
# Function call frequency analysis
|
||||
if self._function_call_counts:
|
||||
self._add_function_call_analysis(lines)
|
||||
self._add_inline_candidates(lines)
|
||||
|
||||
lines.append(
|
||||
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
|
||||
)
|
||||
|
||||
@@ -256,7 +256,7 @@ SYMBOL_PATTERNS = {
|
||||
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
|
||||
# Order matters! More specific categories must come before general ones.
|
||||
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
|
||||
"mdns_lib": ["mdns"],
|
||||
"mdns_lib": ["mdns", "packet$"],
|
||||
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
|
||||
"memory_mgmt": [
|
||||
"mem_",
|
||||
@@ -408,7 +408,6 @@ SYMBOL_PATTERNS = {
|
||||
],
|
||||
"arduino_core": [
|
||||
"pinMode",
|
||||
"resetPins",
|
||||
"millis",
|
||||
"micros",
|
||||
"delay(", # More specific - Arduino delay function with parenthesis
|
||||
@@ -794,7 +793,6 @@ SYMBOL_PATTERNS = {
|
||||
"s_dp",
|
||||
"s_ni",
|
||||
"s_reg_dump",
|
||||
"packet$",
|
||||
"d_mult_table",
|
||||
"K",
|
||||
"fcstab",
|
||||
|
||||
+76
-5
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -57,8 +59,42 @@ def maybe_conf(conf, *validators):
|
||||
return validate
|
||||
|
||||
|
||||
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
|
||||
return ACTION_REGISTRY.register(name, action_type, schema)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def register_action(
|
||||
name: str,
|
||||
action_type: MockObjClass,
|
||||
schema: cv.Schema,
|
||||
*,
|
||||
synchronous: bool | None = None,
|
||||
):
|
||||
"""Register an action type.
|
||||
|
||||
All callers must pass ``synchronous`` explicitly.
|
||||
|
||||
``synchronous=True`` — the action never defers ``play_next_()`` to a
|
||||
later point (callback, timer, or ``loop()``). Trigger arguments are
|
||||
only used during the initial call, so string args can use non-owning
|
||||
StringRef for zero-copy access.
|
||||
|
||||
``synchronous=False`` — the action defers ``play_next_()`` via a
|
||||
callback, timer, or ``Component::loop()``. Trigger arguments must
|
||||
outlive the initial call, so string args use owning std::string to
|
||||
prevent dangling references.
|
||||
"""
|
||||
if synchronous is None:
|
||||
_LOGGER.warning(
|
||||
"register_action('%s', ...) is missing the synchronous= parameter. "
|
||||
"Defaulting to synchronous=False (safe but prevents StringRef "
|
||||
"optimization). Check the C++ class: use synchronous=False if "
|
||||
"play_next_() is deferred to a callback, timer, or loop(); "
|
||||
"use synchronous=True if play_next_() always runs before the "
|
||||
"initial play/play_complex call returns",
|
||||
name,
|
||||
)
|
||||
synchronous = False
|
||||
return ACTION_REGISTRY.register(name, action_type, schema, synchronous=synchronous)
|
||||
|
||||
|
||||
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
|
||||
@@ -335,7 +371,10 @@ async def component_is_idle_condition_to_code(
|
||||
|
||||
|
||||
@register_action(
|
||||
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
|
||||
"delay",
|
||||
DelayAction,
|
||||
cv.templatable(cv.positive_time_period_milliseconds),
|
||||
synchronous=False,
|
||||
)
|
||||
async def delay_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -366,6 +405,7 @@ async def delay_action_to_code(
|
||||
cv.has_at_least_one_key(CONF_THEN, CONF_ELSE),
|
||||
cv.has_at_least_one_key(CONF_CONDITION, CONF_ANY, CONF_ALL),
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def if_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -394,6 +434,7 @@ async def if_action_to_code(
|
||||
cv.Required(CONF_THEN): validate_action_list,
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def while_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -417,6 +458,7 @@ async def while_action_to_code(
|
||||
cv.Required(CONF_THEN): validate_action_list,
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def repeat_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -445,7 +487,7 @@ _validate_wait_until = cv.maybe_simple_value(
|
||||
)
|
||||
|
||||
|
||||
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
|
||||
@register_action("wait_until", WaitUntilAction, _validate_wait_until, synchronous=False)
|
||||
async def wait_until_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
@@ -461,7 +503,12 @@ async def wait_until_action_to_code(
|
||||
return var
|
||||
|
||||
|
||||
@register_action("lambda", LambdaAction, cv.lambda_)
|
||||
# Lambda executes user C++ inline and returns — synchronous by execution model.
|
||||
# User code could theoretically store the StringRef for deferred use, but StringRef
|
||||
# is a view type and storing views beyond their scope is always unsafe regardless
|
||||
# of this optimization. Marking non-synchronous would disable StringRef for nearly
|
||||
# all user services since most use lambda.
|
||||
@register_action("lambda", LambdaAction, cv.lambda_, synchronous=True)
|
||||
async def lambda_action_to_code(
|
||||
config: ConfigType,
|
||||
action_id: ID,
|
||||
@@ -480,6 +527,7 @@ async def lambda_action_to_code(
|
||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def component_update_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -499,6 +547,7 @@ async def component_update_action_to_code(
|
||||
cv.Required(CONF_ID): cv.use_id(cg.PollingComponent),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def component_suspend_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -521,6 +570,7 @@ async def component_suspend_action_to_code(
|
||||
),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def component_resume_action_to_code(
|
||||
config: ConfigType,
|
||||
@@ -578,6 +628,27 @@ async def build_condition_list(
|
||||
return conditions
|
||||
|
||||
|
||||
def has_non_synchronous_actions(actions: ConfigType) -> bool:
|
||||
"""Check if a validated action list contains any non-synchronous actions.
|
||||
|
||||
Non-synchronous actions (delay, wait_until, script.wait, etc.) store
|
||||
trigger args for later execution, making non-owning types like StringRef
|
||||
unsafe.
|
||||
"""
|
||||
if isinstance(actions, list):
|
||||
return any(has_non_synchronous_actions(item) for item in actions)
|
||||
if isinstance(actions, dict):
|
||||
for key in actions:
|
||||
if key in ACTION_REGISTRY and not ACTION_REGISTRY[key].synchronous:
|
||||
return True
|
||||
return any(
|
||||
has_non_synchronous_actions(v)
|
||||
for v in actions.values()
|
||||
if isinstance(v, (list, dict))
|
||||
)
|
||||
return False
|
||||
|
||||
|
||||
async def build_automation(
|
||||
trigger: MockObj, args: TemplateArgsType, config: ConfigType
|
||||
) -> MockObj:
|
||||
|
||||
@@ -72,7 +72,7 @@ def get_component_cmakelists(minimal: bool = False) -> str:
|
||||
|
||||
# Extract compile definitions from build flags (-DXXX -> XXX)
|
||||
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
|
||||
compile_defs_str = "\n ".join(compile_defs) if compile_defs else ""
|
||||
compile_defs_str = "\n ".join(sorted(compile_defs)) if compile_defs else ""
|
||||
|
||||
# Extract compile options (-W flags, excluding linker flags)
|
||||
compile_opts = [
|
||||
@@ -80,11 +80,11 @@ def get_component_cmakelists(minimal: bool = False) -> str:
|
||||
for flag in CORE.build_flags
|
||||
if flag.startswith("-W") and not flag.startswith("-Wl,")
|
||||
]
|
||||
compile_opts_str = "\n ".join(compile_opts) if compile_opts else ""
|
||||
compile_opts_str = "\n ".join(sorted(compile_opts)) if compile_opts else ""
|
||||
|
||||
# Extract linker options (-Wl, flags)
|
||||
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
|
||||
link_opts_str = "\n ".join(link_opts) if link_opts else ""
|
||||
link_opts_str = "\n ".join(sorted(link_opts)) if link_opts else ""
|
||||
|
||||
return f"""\
|
||||
# Auto-generated by ESPHome
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
from esphome.cpp_generator import ( # noqa: F401
|
||||
ArrayInitializer,
|
||||
Expression,
|
||||
FlashStringLiteral,
|
||||
LineComment,
|
||||
LogStringLiteral,
|
||||
MockObj,
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "absolute_humidity.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace absolute_humidity {
|
||||
namespace esphome::absolute_humidity {
|
||||
|
||||
static const char *const TAG = "absolute_humidity.sensor";
|
||||
static const char *const TAG{"absolute_humidity.sensor"};
|
||||
|
||||
void AbsoluteHumidityComponent::setup() {
|
||||
this->temperature_sensor_->add_on_state_callback([this](float state) {
|
||||
this->temperature_ = state;
|
||||
this->enable_loop();
|
||||
});
|
||||
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
|
||||
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
|
||||
// Get initial value
|
||||
if (this->temperature_sensor_->has_state()) {
|
||||
this->temperature_callback_(this->temperature_sensor_->get_state());
|
||||
this->temperature_ = this->temperature_sensor_->get_state();
|
||||
}
|
||||
|
||||
this->humidity_sensor_->add_on_state_callback([this](float state) {
|
||||
this->humidity_ = state;
|
||||
this->enable_loop();
|
||||
});
|
||||
ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str());
|
||||
this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); });
|
||||
// Get initial value
|
||||
if (this->humidity_sensor_->has_state()) {
|
||||
this->humidity_callback_(this->humidity_sensor_->get_state());
|
||||
this->humidity_ = this->humidity_sensor_->get_state();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,14 +53,12 @@ void AbsoluteHumidityComponent::dump_config() {
|
||||
}
|
||||
|
||||
void AbsoluteHumidityComponent::loop() {
|
||||
if (!this->next_update_) {
|
||||
return;
|
||||
}
|
||||
this->next_update_ = false;
|
||||
// Only run once
|
||||
this->disable_loop();
|
||||
|
||||
// Ensure we have source data
|
||||
const bool no_temperature = std::isnan(this->temperature_);
|
||||
const bool no_humidity = std::isnan(this->humidity_);
|
||||
const bool no_temperature{std::isnan(this->temperature_)};
|
||||
const bool no_humidity{std::isnan(this->humidity_)};
|
||||
if (no_temperature || no_humidity) {
|
||||
if (no_temperature) {
|
||||
ESP_LOGW(TAG, "No valid state from temperature sensor!");
|
||||
@@ -67,9 +72,9 @@ void AbsoluteHumidityComponent::loop() {
|
||||
}
|
||||
|
||||
// Convert to desired units
|
||||
const float temperature_c = this->temperature_;
|
||||
const float temperature_k = temperature_c + 273.15;
|
||||
const float hr = this->humidity_ / 100;
|
||||
const float temperature_c{this->temperature_};
|
||||
const float temperature_k{temperature_c + 273.15f};
|
||||
const float hr{this->humidity_ / 100.0f};
|
||||
|
||||
// Calculate saturation vapor pressure
|
||||
float es;
|
||||
@@ -90,12 +95,9 @@ void AbsoluteHumidityComponent::loop() {
|
||||
}
|
||||
|
||||
// Calculate absolute humidity
|
||||
const float absolute_humidity = vapor_density(es, hr, temperature_k);
|
||||
const float absolute_humidity{vapor_density(es, hr, temperature_k)};
|
||||
|
||||
ESP_LOGD(TAG,
|
||||
"Saturation vapor pressure %f kPa\n"
|
||||
"Publishing absolute humidity %f g/m³",
|
||||
es, absolute_humidity);
|
||||
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity);
|
||||
|
||||
// Publish absolute humidity
|
||||
this->status_clear_warning();
|
||||
@@ -106,16 +108,16 @@ void AbsoluteHumidityComponent::loop() {
|
||||
// More accurate than Tetens in normal meteorologic conditions
|
||||
float AbsoluteHumidityComponent::es_buck(float temperature_c) {
|
||||
float a, b, c, d;
|
||||
if (temperature_c >= 0) {
|
||||
a = 0.61121;
|
||||
b = 18.678;
|
||||
c = 234.5;
|
||||
d = 257.14;
|
||||
if (temperature_c >= 0.0f) {
|
||||
a = 0.61121f;
|
||||
b = 18.678f;
|
||||
c = 234.5f;
|
||||
d = 257.14f;
|
||||
} else {
|
||||
a = 0.61115;
|
||||
b = 18.678;
|
||||
c = 233.7;
|
||||
d = 279.82;
|
||||
a = 0.61115f;
|
||||
b = 18.678f;
|
||||
c = 233.7f;
|
||||
d = 279.82f;
|
||||
}
|
||||
return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c)));
|
||||
}
|
||||
@@ -123,14 +125,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) {
|
||||
// Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation)
|
||||
float AbsoluteHumidityComponent::es_tetens(float temperature_c) {
|
||||
float a, b;
|
||||
if (temperature_c >= 0) {
|
||||
a = 17.27;
|
||||
b = 237.3;
|
||||
if (temperature_c >= 0.0f) {
|
||||
a = 17.27f;
|
||||
b = 237.3f;
|
||||
} else {
|
||||
a = 21.875;
|
||||
b = 265.5;
|
||||
a = 21.875f;
|
||||
b = 265.5f;
|
||||
}
|
||||
return 0.61078 * expf((a * temperature_c) / (temperature_c + b));
|
||||
return 0.61078f * expf((a * temperature_c) / (temperature_c + b));
|
||||
}
|
||||
|
||||
// Wobus equation
|
||||
@@ -149,18 +151,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
|
||||
//
|
||||
// Baker, Schlatter 17-MAY-1982 Original version.
|
||||
|
||||
const float c0 = +0.99999683e00;
|
||||
const float c1 = -0.90826951e-02;
|
||||
const float c2 = +0.78736169e-04;
|
||||
const float c3 = -0.61117958e-06;
|
||||
const float c4 = +0.43884187e-08;
|
||||
const float c5 = -0.29883885e-10;
|
||||
const float c6 = +0.21874425e-12;
|
||||
const float c7 = -0.17892321e-14;
|
||||
const float c8 = +0.11112018e-16;
|
||||
const float c9 = -0.30994571e-19;
|
||||
const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))));
|
||||
return 0.61078 / pow(p, 8);
|
||||
constexpr float c0{+0.99999683e+00f};
|
||||
constexpr float c1{-0.90826951e-02f};
|
||||
constexpr float c2{+0.78736169e-04f};
|
||||
constexpr float c3{-0.61117958e-06f};
|
||||
constexpr float c4{+0.43884187e-08f};
|
||||
constexpr float c5{-0.29883885e-10f};
|
||||
constexpr float c6{+0.21874425e-12f};
|
||||
constexpr float c7{-0.17892321e-14f};
|
||||
constexpr float c8{+0.11112018e-16f};
|
||||
constexpr float c9{-0.30994571e-19f};
|
||||
const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))};
|
||||
return 0.61078f / powf(p, 8.0f);
|
||||
}
|
||||
|
||||
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
|
||||
@@ -171,11 +173,10 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
|
||||
// hr = relative humidity [0-1]
|
||||
// ta = absolute temperature (K)
|
||||
|
||||
const float ea = hr * es * 1000; // vapor pressure of the air (Pa)
|
||||
const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹)
|
||||
const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹)
|
||||
const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa)
|
||||
const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹)
|
||||
const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹)
|
||||
return (ea * mw) / (r * ta);
|
||||
}
|
||||
|
||||
} // namespace absolute_humidity
|
||||
} // namespace esphome
|
||||
} // namespace esphome::absolute_humidity
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace absolute_humidity {
|
||||
namespace esphome::absolute_humidity {
|
||||
|
||||
/// Enum listing all implemented saturation vapor pressure equations.
|
||||
enum SaturationVaporPressureEquation {
|
||||
@@ -16,8 +15,6 @@ enum SaturationVaporPressureEquation {
|
||||
/// This class implements calculation of absolute humidity from temperature and relative humidity.
|
||||
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
public:
|
||||
AbsoluteHumidityComponent() = default;
|
||||
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; }
|
||||
@@ -27,15 +24,6 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
void temperature_callback_(float state) {
|
||||
this->next_update_ = true;
|
||||
this->temperature_ = state;
|
||||
}
|
||||
void humidity_callback_(float state) {
|
||||
this->next_update_ = true;
|
||||
this->humidity_ = state;
|
||||
}
|
||||
|
||||
/** Buck equation for saturation vapor pressure in kPa.
|
||||
*
|
||||
* @param temperature_c Air temperature in °C.
|
||||
@@ -57,19 +45,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
* @param es Saturation vapor pressure in kPa.
|
||||
* @param hr Relative humidity 0 to 1.
|
||||
* @param ta Absolute temperature in K.
|
||||
* @param heater_duration The duration in ms that the heater should turn on for when measuring.
|
||||
*/
|
||||
static float vapor_density(float es, float hr, float ta);
|
||||
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
|
||||
bool next_update_{false};
|
||||
|
||||
float temperature_{NAN};
|
||||
float humidity_{NAN};
|
||||
SaturationVaporPressureEquation equation_;
|
||||
};
|
||||
|
||||
} // namespace absolute_humidity
|
||||
} // namespace esphome
|
||||
} // namespace esphome::absolute_humidity
|
||||
|
||||
@@ -199,12 +199,19 @@ void AcDimmer::setup() {
|
||||
setTimer1Callback(&timer_interrupt);
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
|
||||
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
|
||||
// For ESP32, we can't use dynamic interval calculation because the timerX functions
|
||||
// are not callable from ISR (placed in flash storage).
|
||||
// Here we just use an interrupt firing every 50 µs.
|
||||
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
|
||||
if (dimmer_timer == nullptr) {
|
||||
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
|
||||
if (dimmer_timer == nullptr) {
|
||||
ESP_LOGE(TAG, "Failed to create GPTimer for AC dimmer");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
|
||||
// For ESP32, we can't use dynamic interval calculation because the timerX functions
|
||||
// are not callable from ISR (placed in flash storage).
|
||||
// Here we just use an interrupt firing every 50 µs.
|
||||
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,8 @@ namespace adc {
|
||||
|
||||
#ifdef USE_ESP32
|
||||
// clang-format off
|
||||
#if (ESP_IDF_VERSION_MAJOR == 5 && \
|
||||
#if ESP_IDF_VERSION_MAJOR >= 6 || \
|
||||
(ESP_IDF_VERSION_MAJOR == 5 && \
|
||||
((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \
|
||||
(ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \
|
||||
(ESP_IDF_VERSION_MINOR >= 2)) \
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
#endif // CYW43_USES_VSYS_PIN
|
||||
#include <hardware/adc.h>
|
||||
|
||||
// PICO_VSYS_PIN is defined in pico-sdk board headers (e.g. boards/pico2.h),
|
||||
// but the Arduino framework's config_autogen.h includes a generic board header
|
||||
// that doesn't define it. Provide the standard value (pin 29) as a fallback.
|
||||
#ifndef PICO_VSYS_PIN
|
||||
#define PICO_VSYS_PIN 29 // NOLINT(cppcoreguidelines-macro-usage)
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
|
||||
@@ -58,7 +58,10 @@ void HOT AddressableLightDisplay::draw_absolute_pixel_internal(int x, int y, Col
|
||||
|
||||
if (this->pixel_mapper_f_.has_value()) {
|
||||
// Params are passed by reference, so they may be modified in call.
|
||||
this->addressable_light_buffer_[(*this->pixel_mapper_f_)(x, y)] = color;
|
||||
int index = (*this->pixel_mapper_f_)(x, y);
|
||||
if (index < 0 || static_cast<size_t>(index) >= this->addressable_light_buffer_.size())
|
||||
return;
|
||||
this->addressable_light_buffer_[index] = color;
|
||||
} else {
|
||||
this->addressable_light_buffer_[y * this->get_width_internal() + x] = color;
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ class AddressableLightDisplay : public display::DisplayBuffer {
|
||||
// - Save the current effect index.
|
||||
this->last_effect_index_ = light_state_->get_current_effect_index();
|
||||
// - Disable any current effect.
|
||||
light_state_->make_call().set_effect(0).perform();
|
||||
light_state_->make_call().set_effect(uint32_t{0}).perform();
|
||||
}
|
||||
}
|
||||
enabled_ = enabled;
|
||||
|
||||
@@ -121,7 +121,7 @@ void ADE7880::update() {
|
||||
this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) {
|
||||
return chan->forward_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) {
|
||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) {
|
||||
return chan->reverse_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
}
|
||||
@@ -137,7 +137,7 @@ void ADE7880::update() {
|
||||
this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) {
|
||||
return chan->forward_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) {
|
||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) {
|
||||
return chan->reverse_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
}
|
||||
@@ -153,7 +153,7 @@ void ADE7880::update() {
|
||||
this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) {
|
||||
return chan->forward_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) {
|
||||
this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) {
|
||||
return chan->reverse_active_energy_total += val / 14400.0f;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,6 +85,9 @@ constexpr uint16_t CWATTHR = 0xE402;
|
||||
constexpr uint16_t AFWATTHR = 0xE403;
|
||||
constexpr uint16_t BFWATTHR = 0xE404;
|
||||
constexpr uint16_t CFWATTHR = 0xE405;
|
||||
constexpr uint16_t ARWATTHR = 0xE406;
|
||||
constexpr uint16_t BRWATTHR = 0xE407;
|
||||
constexpr uint16_t CRWATTHR = 0xE408;
|
||||
constexpr uint16_t AFVARHR = 0xE409;
|
||||
constexpr uint16_t BFVARHR = 0xE40A;
|
||||
constexpr uint16_t CFVARHR = 0xE40B;
|
||||
|
||||
@@ -173,19 +173,8 @@ float ADS1115Component::request_measurement(ADS1115Multiplexer multiplexer, ADS1
|
||||
}
|
||||
|
||||
if (resolution == ADS1015_12_BITS) {
|
||||
bool negative = (raw_conversion >> 15) == 1;
|
||||
|
||||
// shift raw_conversion as it's only 12-bits, left justified
|
||||
raw_conversion = raw_conversion >> (16 - ADS1015_12_BITS);
|
||||
|
||||
// check if number was negative in order to keep the sign
|
||||
if (negative) {
|
||||
// the number was negative
|
||||
// 1) set the negative bit back
|
||||
raw_conversion |= 0x8000;
|
||||
// 2) reset the former (shifted) negative bit
|
||||
raw_conversion &= 0xF7FF;
|
||||
}
|
||||
// ADS1015 returns 12-bit value left-justified in 16 bits; shift right and sign-extend
|
||||
raw_conversion = static_cast<uint16_t>(static_cast<int16_t>(raw_conversion) >> (16 - ADS1015_12_BITS));
|
||||
}
|
||||
|
||||
auto signed_conversion = static_cast<int16_t>(raw_conversion);
|
||||
|
||||
@@ -92,6 +92,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
|
||||
"ags10.new_i2c_address",
|
||||
AGS10NewI2cAddressAction,
|
||||
AGS10_NEW_I2C_ADDRESS_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
@@ -121,6 +122,7 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
|
||||
"ags10.set_zero_point",
|
||||
AGS10SetZeroPointAction,
|
||||
AGS10_SET_ZERO_POINT_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
|
||||
@@ -34,7 +34,10 @@ SET_AUTO_MUTE_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"aic3204.set_auto_mute_mode", SetAutoMuteAction, SET_AUTO_MUTE_ACTION_SCHEMA
|
||||
"aic3204.set_auto_mute_mode",
|
||||
SetAutoMuteAction,
|
||||
SET_AUTO_MUTE_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
|
||||
@@ -186,8 +186,8 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
|
||||
)
|
||||
|
||||
|
||||
@setup_entity("alarm_control_panel")
|
||||
async def setup_alarm_control_panel_core_(var, config):
|
||||
await setup_entity(var, config, "alarm_control_panel")
|
||||
for conf in config.get(CONF_ON_STATE, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
await automation.build_automation(trigger, [], conf)
|
||||
@@ -243,7 +243,10 @@ async def new_alarm_control_panel(config, *args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
"alarm_control_panel.arm_away",
|
||||
ArmAwayAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
@@ -255,7 +258,10 @@ async def alarm_action_arm_away_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
"alarm_control_panel.arm_home",
|
||||
ArmHomeAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
@@ -267,7 +273,10 @@ async def alarm_action_arm_home_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.arm_night", ArmNightAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
"alarm_control_panel.arm_night",
|
||||
ArmNightAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
@@ -279,7 +288,10 @@ async def alarm_action_arm_night_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
"alarm_control_panel.disarm",
|
||||
DisarmAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
@@ -291,7 +303,10 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
"alarm_control_panel.pending",
|
||||
PendingAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
@@ -299,7 +314,10 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
"alarm_control_panel.triggered",
|
||||
TriggeredAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
@@ -307,7 +325,10 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
"alarm_control_panel.chime",
|
||||
ChimeAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
@@ -315,7 +336,10 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
|
||||
"alarm_control_panel.ready",
|
||||
ReadyAction,
|
||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_condition(
|
||||
"alarm_control_panel.ready",
|
||||
|
||||
@@ -51,22 +51,6 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
|
||||
}
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
|
||||
this->cleared_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
|
||||
this->chime_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
|
||||
this->ready_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
|
||||
const char *code) {
|
||||
auto call = this->make_call();
|
||||
|
||||
@@ -37,25 +37,24 @@ class AlarmControlPanel : public EntityBase {
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_state_callback(std::function<void()> &&callback);
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel clears from triggered
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_cleared_callback(std::function<void()> &&callback);
|
||||
/** Add a callback for when the state of the alarm_control_panel clears from triggered. */
|
||||
template<typename F> void add_on_cleared_callback(F &&callback) {
|
||||
this->cleared_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/** Add a callback for when a chime zone goes from closed to open
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_chime_callback(std::function<void()> &&callback);
|
||||
/** Add a callback for when a chime zone goes from closed to open. */
|
||||
template<typename F> void add_on_chime_callback(F &&callback) {
|
||||
this->chime_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/** Add a callback for when a ready state changes
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_ready_callback(std::function<void()> &&callback);
|
||||
/** Add a callback for when a ready state changes. */
|
||||
template<typename F> void add_on_ready_callback(F &&callback) {
|
||||
this->ready_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
|
||||
/** A numeric representation of the supported features as per HomeAssistant
|
||||
*
|
||||
|
||||
@@ -12,7 +12,14 @@ AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent
|
||||
|
||||
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
|
||||
if (code != nullptr) {
|
||||
this->code_ = std::string(code);
|
||||
return this->set_code(code, strlen(code));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code, size_t len) {
|
||||
if (code != nullptr) {
|
||||
this->code_ = std::string(code, len);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ class AlarmControlPanelCall {
|
||||
AlarmControlPanelCall(AlarmControlPanel *parent);
|
||||
|
||||
AlarmControlPanelCall &set_code(const char *code);
|
||||
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
|
||||
AlarmControlPanelCall &set_code(const char *code, size_t len);
|
||||
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str(), code.size()); }
|
||||
AlarmControlPanelCall &arm_away();
|
||||
AlarmControlPanelCall &arm_home();
|
||||
AlarmControlPanelCall &arm_night();
|
||||
|
||||
@@ -125,7 +125,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
|
||||
this->current_sensor_->publish_state(NAN);
|
||||
if (this->speed_sensor_ != nullptr)
|
||||
this->speed_sensor_->publish_state(NAN);
|
||||
if (this->speed_sensor_ != nullptr)
|
||||
if (this->voltage_sensor_ != nullptr)
|
||||
this->voltage_sensor_->publish_state(NAN);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -63,8 +63,9 @@ void Am43Component::control(const CoverCall &call) {
|
||||
ESP_LOGW(TAG, "[%s] Error writing stop command to device, error = %d", this->get_name().c_str(), status);
|
||||
}
|
||||
}
|
||||
if (call.get_position().has_value()) {
|
||||
auto pos = *call.get_position();
|
||||
auto opt_pos = call.get_position();
|
||||
if (opt_pos.has_value()) {
|
||||
auto pos = *opt_pos;
|
||||
|
||||
if (this->invert_position_)
|
||||
pos = 1 - pos;
|
||||
|
||||
@@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
|
||||
uint8_t current_sensor_;
|
||||
// The AM43 often gets into a state where it spams loads of battery update
|
||||
// notifications. Here we will limit to no more than every 10s.
|
||||
uint8_t last_battery_update_;
|
||||
uint32_t last_battery_update_;
|
||||
};
|
||||
|
||||
} // namespace am43
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
@@ -69,9 +69,15 @@ SET_FRAME_SCHEMA = cv.Schema(
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action("animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA)
|
||||
@automation.register_action("animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA)
|
||||
@automation.register_action("animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA)
|
||||
@automation.register_action(
|
||||
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
async def animation_action_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
@@ -24,8 +24,9 @@ void Anova::loop() {
|
||||
}
|
||||
|
||||
void Anova::control(const ClimateCall &call) {
|
||||
if (call.get_mode().has_value()) {
|
||||
ClimateMode mode = *call.get_mode();
|
||||
auto mode_val = call.get_mode();
|
||||
if (mode_val.has_value()) {
|
||||
ClimateMode mode = *mode_val;
|
||||
AnovaPacket *pkt;
|
||||
switch (mode) {
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
@@ -45,8 +46,9 @@ void Anova::control(const ClimateCall &call) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
if (call.get_target_temperature().has_value()) {
|
||||
auto *pkt = this->codec_->get_set_target_temp_request(*call.get_target_temperature());
|
||||
auto target_temp = call.get_target_temperature();
|
||||
if (target_temp.has_value()) {
|
||||
auto *pkt = this->codec_->get_set_target_temp_request(*target_temp);
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
@@ -67,10 +69,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG,
|
||||
"[%s] No control service found at device, not an Anova..?\n"
|
||||
"[%s] Note, this component does not currently support Anova Nano.",
|
||||
this->get_name().c_str(), this->get_name().c_str());
|
||||
ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str());
|
||||
ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str());
|
||||
break;
|
||||
}
|
||||
this->char_handle_ = chr->handle;
|
||||
@@ -144,9 +144,12 @@ void Anova::update() {
|
||||
return;
|
||||
|
||||
if (this->current_request_ < 2) {
|
||||
auto *pkt = this->codec_->get_read_device_status_request();
|
||||
if (this->current_request_ == 0)
|
||||
this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
|
||||
AnovaPacket *pkt;
|
||||
if (this->current_request_ == 0) {
|
||||
pkt = this->codec_->get_set_unit_request(this->fahrenheit_ ? 'f' : 'c');
|
||||
} else {
|
||||
pkt = this->codec_->get_read_device_status_request();
|
||||
}
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
|
||||
@@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() {
|
||||
|
||||
uint8_t buf[128];
|
||||
for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) {
|
||||
// The ESP's i2c driver has a limited buffer size.
|
||||
// This way of retrieving the data should be wrong according to the datasheet
|
||||
// but it seems to work.
|
||||
// Read in 32-byte chunks due to ESP8266 I2C buffer limit.
|
||||
// Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF
|
||||
// and advances its internal pointer after every 4th byte.
|
||||
uint8_t read = std::min(32, fifo_level * 4 - pos);
|
||||
APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed.");
|
||||
APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed.");
|
||||
}
|
||||
|
||||
if (millis() - this->gesture_start_ > 500) {
|
||||
|
||||
@@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
|
||||
"bool": cg.bool_,
|
||||
"int": cg.int32,
|
||||
"float": cg.float_,
|
||||
"string": cg.std_string,
|
||||
"string": cg.StringRef,
|
||||
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
|
||||
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
|
||||
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
|
||||
@@ -233,8 +233,8 @@ def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||
|
||||
# API needs 1 listening socket + typically 3 concurrent client connections
|
||||
# (not max_connections, which is the upper limit rarely reached)
|
||||
sockets_needed = 1 + 3
|
||||
socket.consume_sockets(sockets_needed, "api")(config)
|
||||
socket.consume_sockets(3, "api")(config)
|
||||
socket.consume_sockets(1, "api", socket.SocketType.TCP_LISTEN)(config)
|
||||
return config
|
||||
|
||||
|
||||
@@ -301,11 +301,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
# Maximum queued send buffers per connection before dropping connection
|
||||
# Each buffer uses ~8-12 bytes overhead plus actual message size
|
||||
# Platform defaults based on available RAM and typical message rates:
|
||||
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
|
||||
cv.SplitDefault(
|
||||
CONF_MAX_SEND_QUEUE,
|
||||
esp8266=5, # Limited RAM, need to fail fast
|
||||
esp8266=4, # Limited RAM, need to fail fast
|
||||
esp32=8, # More RAM, can buffer more
|
||||
rp2040=5, # Limited RAM
|
||||
rp2040=8, # Moderate RAM
|
||||
bk72xx=8, # Moderate RAM
|
||||
nrf52=8, # Moderate RAM
|
||||
rtl87xx=8, # Moderate RAM
|
||||
@@ -380,9 +381,18 @@ async def to_code(config: ConfigType) -> None:
|
||||
if is_optional:
|
||||
func_args.append((cg.bool_, "return_response"))
|
||||
|
||||
# Check if action chain has non-synchronous actions that would make
|
||||
# non-owning StringRef dangle (rx_buf_ reused after delay)
|
||||
has_non_synchronous = automation.has_non_synchronous_actions(
|
||||
conf.get(CONF_THEN, [])
|
||||
)
|
||||
|
||||
service_arg_names: list[str] = []
|
||||
for name, var_ in conf[CONF_VARIABLES].items():
|
||||
native = SERVICE_ARG_NATIVE_TYPES[var_]
|
||||
# Fall back to std::string for string args if non-synchronous actions exist
|
||||
if has_non_synchronous and native is cg.StringRef:
|
||||
native = cg.std_string
|
||||
service_template_args.append(native)
|
||||
func_args.append((native, name))
|
||||
service_arg_names.append(name)
|
||||
@@ -444,7 +454,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
# and plaintext disabled. Only a factory reset can remove it.
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
cg.add_define("USE_API_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.10")
|
||||
cg.add_library("esphome/noise-c", "0.1.11")
|
||||
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
|
||||
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
|
||||
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
|
||||
else:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
|
||||
@@ -509,11 +522,13 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
||||
"homeassistant.action",
|
||||
HomeAssistantServiceCallAction,
|
||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
@automation.register_action(
|
||||
"homeassistant.service",
|
||||
HomeAssistantServiceCallAction,
|
||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_service_to_code(
|
||||
config: ConfigType,
|
||||
@@ -524,24 +539,31 @@ async def homeassistant_service_to_code(
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, cg.std_string)
|
||||
cg.add(var.set_service(templ))
|
||||
|
||||
# Initialize FixedVectors with exact sizes from config
|
||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||
for key, value in config[CONF_DATA].items():
|
||||
# output_type=None because lambdas can return non-string types (int,
|
||||
# float, char*) that TemplatableStringValue converts via to_string.
|
||||
# Static strings are manually wrapped for PROGMEM on ESP8266.
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data(key, templ))
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data_template(key, templ))
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||
for key, value in config[CONF_VARIABLES].items():
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_variable(key, templ))
|
||||
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
if on_error := config.get(CONF_ON_ERROR):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
|
||||
@@ -604,29 +626,37 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
|
||||
"homeassistant.event",
|
||||
HomeAssistantServiceCallAction,
|
||||
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, cg.std_string)
|
||||
cg.add(var.set_service(templ))
|
||||
|
||||
# Initialize FixedVectors with exact sizes from config
|
||||
cg.add(var.init_data(len(config[CONF_DATA])))
|
||||
for key, value in config[CONF_DATA].items():
|
||||
# output_type=None because lambdas can return non-string types (int,
|
||||
# float, char*) that TemplatableStringValue converts via to_string.
|
||||
# Static strings are manually wrapped for PROGMEM on ESP8266.
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data(key, templ))
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
cg.add(var.init_data_template(len(config[CONF_DATA_TEMPLATE])))
|
||||
for key, value in config[CONF_DATA_TEMPLATE].items():
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_data_template(key, templ))
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_data_template(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||
for key, value in config[CONF_VARIABLES].items():
|
||||
templ = await cg.templatable(value, args, None)
|
||||
cg.add(var.add_variable(key, templ))
|
||||
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
return var
|
||||
|
||||
@@ -644,16 +674,17 @@ HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA = cv.maybe_simple_value(
|
||||
"homeassistant.tag_scanned",
|
||||
HomeAssistantServiceCallAction,
|
||||
HOMEASSISTANT_TAG_SCANNED_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
cg.add(var.set_service("esphome.tag_scanned"))
|
||||
cg.add(var.set_service(cg.FlashStringLiteral("esphome.tag_scanned")))
|
||||
# Initialize FixedVector with exact size (1 data field)
|
||||
cg.add(var.init_data(1))
|
||||
templ = await cg.templatable(config[CONF_TAG], args, cg.std_string)
|
||||
cg.add(var.add_data("tag_id", templ))
|
||||
cg.add(var.add_data(cg.FlashStringLiteral("tag_id"), templ))
|
||||
return var
|
||||
|
||||
|
||||
@@ -685,6 +716,7 @@ API_RESPOND_ACTION_SCHEMA = cv.All(
|
||||
"api.respond",
|
||||
APIRespondAction,
|
||||
API_RESPOND_ACTION_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def api_respond_to_code(
|
||||
config: ConfigType,
|
||||
|
||||
@@ -58,6 +58,7 @@ service APIConnection {
|
||||
rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {}
|
||||
rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {}
|
||||
rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {}
|
||||
rpc bluetooth_set_connection_params(BluetoothSetConnectionParamsRequest) returns (BluetoothSetConnectionParamsResponse) {}
|
||||
|
||||
rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {}
|
||||
rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {}
|
||||
@@ -69,6 +70,12 @@ service APIConnection {
|
||||
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
|
||||
|
||||
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
|
||||
|
||||
rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {}
|
||||
rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {}
|
||||
rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {}
|
||||
rpc serial_proxy_request(SerialProxyRequest) returns (void) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +205,17 @@ message DeviceInfo {
|
||||
uint32 area_id = 3;
|
||||
}
|
||||
|
||||
enum SerialProxyPortType {
|
||||
SERIAL_PROXY_PORT_TYPE_TTL = 0;
|
||||
SERIAL_PROXY_PORT_TYPE_RS232 = 1;
|
||||
SERIAL_PROXY_PORT_TYPE_RS485 = 2;
|
||||
}
|
||||
|
||||
message SerialProxyInfo {
|
||||
string name = 1; // Human-readable port name
|
||||
SerialProxyPortType port_type = 2; // Port type (RS232, RS485)
|
||||
}
|
||||
|
||||
message DeviceInfoResponse {
|
||||
option (id) = 10;
|
||||
option (source) = SOURCE_SERVER;
|
||||
@@ -260,6 +278,9 @@ message DeviceInfoResponse {
|
||||
// Indicates if Z-Wave proxy support is available and features supported
|
||||
uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"];
|
||||
uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"];
|
||||
|
||||
// Serial proxy instance metadata
|
||||
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
|
||||
}
|
||||
|
||||
message ListEntitiesRequest {
|
||||
@@ -834,6 +855,33 @@ message GetTimeRequest {
|
||||
option (source) = SOURCE_SERVER;
|
||||
}
|
||||
|
||||
enum DSTRuleType {
|
||||
DST_RULE_TYPE_NONE = 0;
|
||||
DST_RULE_TYPE_MONTH_WEEK_DAY = 1;
|
||||
DST_RULE_TYPE_JULIAN_NO_LEAP = 2;
|
||||
DST_RULE_TYPE_DAY_OF_YEAR = 3;
|
||||
}
|
||||
|
||||
message DSTRule {
|
||||
option (source) = SOURCE_CLIENT;
|
||||
|
||||
sint32 time_seconds = 1;
|
||||
uint32 day = 2;
|
||||
DSTRuleType type = 3;
|
||||
uint32 month = 4;
|
||||
uint32 week = 5;
|
||||
uint32 day_of_week = 6;
|
||||
}
|
||||
|
||||
message ParsedTimezone {
|
||||
option (source) = SOURCE_CLIENT;
|
||||
|
||||
sint32 std_offset_seconds = 1;
|
||||
sint32 dst_offset_seconds = 2;
|
||||
DSTRule dst_start = 3;
|
||||
DSTRule dst_end = 4;
|
||||
}
|
||||
|
||||
message GetTimeResponse {
|
||||
option (id) = 37;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
@@ -841,6 +889,7 @@ message GetTimeResponse {
|
||||
|
||||
fixed32 epoch_seconds = 1;
|
||||
string timezone = 2;
|
||||
ParsedTimezone parsed_timezone = 3;
|
||||
}
|
||||
|
||||
// ==================== USER-DEFINES SERVICES ====================
|
||||
@@ -989,6 +1038,7 @@ enum ClimateAction {
|
||||
CLIMATE_ACTION_IDLE = 4;
|
||||
CLIMATE_ACTION_DRYING = 5;
|
||||
CLIMATE_ACTION_FAN = 6;
|
||||
CLIMATE_ACTION_DEFROSTING = 7;
|
||||
}
|
||||
enum ClimatePreset {
|
||||
CLIMATE_PRESET_NONE = 0;
|
||||
@@ -1554,11 +1604,11 @@ message BluetoothLEAdvertisementResponse {
|
||||
}
|
||||
|
||||
message BluetoothLERawAdvertisement {
|
||||
uint64 address = 1;
|
||||
sint32 rssi = 2;
|
||||
uint64 address = 1 [(force) = true];
|
||||
sint32 rssi = 2 [(force) = true];
|
||||
uint32 address_type = 3;
|
||||
|
||||
bytes data = 4 [(fixed_array_size) = 62];
|
||||
bytes data = 4 [(fixed_array_size) = 62, (force) = true];
|
||||
}
|
||||
|
||||
message BluetoothLERawAdvertisementsResponse {
|
||||
@@ -2488,3 +2538,134 @@ message InfraredRFReceiveEvent {
|
||||
fixed32 key = 2; // Key identifying the receiver instance
|
||||
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
|
||||
}
|
||||
|
||||
// ==================== SERIAL PROXY ====================
|
||||
|
||||
enum SerialProxyParity {
|
||||
SERIAL_PROXY_PARITY_NONE = 0;
|
||||
SERIAL_PROXY_PARITY_EVEN = 1;
|
||||
SERIAL_PROXY_PARITY_ODD = 2;
|
||||
}
|
||||
|
||||
// Configure UART parameters for a serial proxy instance
|
||||
message SerialProxyConfigureRequest {
|
||||
option (id) = 138;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
uint32 baudrate = 2; // Baud rate in bits per second
|
||||
bool flow_control = 3; // Enable hardware flow control
|
||||
SerialProxyParity parity = 4; // Parity setting
|
||||
uint32 stop_bits = 5; // Number of stop bits (1 or 2)
|
||||
uint32 data_size = 6; // Number of data bits (5-8)
|
||||
}
|
||||
|
||||
// Data received from a serial device, forwarded to clients
|
||||
message SerialProxyDataReceived {
|
||||
option (id) = 139;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
option (no_delay) = true;
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
bytes data = 2; // Raw data received from the serial device
|
||||
}
|
||||
|
||||
// Write data to a serial device
|
||||
message SerialProxyWriteRequest {
|
||||
option (id) = 140;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
option (no_delay) = true;
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
bytes data = 2; // Raw data to write to the serial device
|
||||
}
|
||||
|
||||
// Set modem control pin states (RTS and DTR)
|
||||
message SerialProxySetModemPinsRequest {
|
||||
option (id) = 141;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
|
||||
}
|
||||
|
||||
// Request current modem control pin states
|
||||
message SerialProxyGetModemPinsRequest {
|
||||
option (id) = 142;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
}
|
||||
|
||||
// Response with current modem control pin states
|
||||
message SerialProxyGetModemPinsResponse {
|
||||
option (id) = 143;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags
|
||||
}
|
||||
|
||||
enum SerialProxyRequestType {
|
||||
SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance
|
||||
SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance
|
||||
SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent)
|
||||
}
|
||||
|
||||
enum SerialProxyStatus {
|
||||
SERIAL_PROXY_STATUS_OK = 0; // Completed successfully; TX drain confirmed
|
||||
SERIAL_PROXY_STATUS_ASSUMED_SUCCESS = 1; // Platform cannot confirm TX drain; success assumed
|
||||
SERIAL_PROXY_STATUS_ERROR = 2; // Driver or hardware error
|
||||
SERIAL_PROXY_STATUS_TIMEOUT = 3; // Timed out before TX completed
|
||||
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4; // Request type not supported by this instance
|
||||
}
|
||||
|
||||
// Generic request message for simple serial proxy operations
|
||||
message SerialProxyRequest {
|
||||
option (id) = 144;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
SerialProxyRequestType type = 2; // Request type
|
||||
}
|
||||
|
||||
// Response to a SerialProxyRequest (e.g. flush completion or failure)
|
||||
message SerialProxyRequestResponse {
|
||||
option (id) = 147;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_SERIAL_PROXY";
|
||||
|
||||
uint32 instance = 1; // Instance index (0-based)
|
||||
SerialProxyRequestType type = 2; // Which request type this responds to
|
||||
SerialProxyStatus status = 3; // Result status
|
||||
string error_message = 4; // Additional detail on failure (optional)
|
||||
}
|
||||
|
||||
// ==================== BLUETOOTH CONNECTION PARAMS ====================
|
||||
message BluetoothSetConnectionParamsRequest {
|
||||
option (id) = 145;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
|
||||
uint64 address = 1;
|
||||
uint32 min_interval = 2; // units of 1.25ms
|
||||
uint32 max_interval = 3; // units of 1.25ms
|
||||
uint32 latency = 4;
|
||||
uint32 timeout = 5; // units of 10ms
|
||||
}
|
||||
|
||||
message BluetoothSetConnectionParamsResponse {
|
||||
option (id) = 146;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
|
||||
uint64 address = 1;
|
||||
int32 error = 2;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
#include "api_buffer.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
void APIBuffer::grow_(size_t n) {
|
||||
auto new_data = make_buffer(n);
|
||||
if (this->size_)
|
||||
std::memcpy(new_data.get(), this->data_.get(), this->size_);
|
||||
this->data_ = std::move(new_data);
|
||||
this->capacity_ = n;
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
/// Helper to use make_unique_for_overwrite where available (skips zero-fill),
|
||||
/// falling back to make_unique on older GCC (ESP8266, LibreTiny).
|
||||
inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) {
|
||||
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
|
||||
return std::make_unique<uint8_t[]>(n);
|
||||
#else
|
||||
return std::make_unique_for_overwrite<uint8_t[]>(n);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Byte buffer that skips zero-initialization on resize().
|
||||
///
|
||||
/// std::vector<uint8_t>::resize() zero-fills new bytes via memset. For the
|
||||
/// shared protobuf write buffer, every byte is overwritten by the encoder,
|
||||
/// making the zero-fill pure waste. For the receive buffer, bytes are
|
||||
/// overwritten by socket reads.
|
||||
///
|
||||
/// Designed for bulk clear/resize/overwrite patterns. grow_() allocates
|
||||
/// exactly the requested size (no growth factor) since callers resize to
|
||||
/// known sizes rather than appending incrementally.
|
||||
///
|
||||
/// Safe because: callers always write exactly the number of bytes they
|
||||
/// resize for. In the protobuf write path, debug_check_bounds_ validates
|
||||
/// writes in debug builds.
|
||||
class APIBuffer {
|
||||
public:
|
||||
void clear() { this->size_ = 0; }
|
||||
inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE {
|
||||
if (n > this->capacity_)
|
||||
this->grow_(n);
|
||||
}
|
||||
inline void resize(size_t n) ESPHOME_ALWAYS_INLINE {
|
||||
this->reserve(n);
|
||||
this->size_ = n; // no zero-fill
|
||||
}
|
||||
/// Reserve capacity for max(reserve_size, new_size) bytes, then set size to new_size.
|
||||
/// Single grow_ check regardless of argument order.
|
||||
inline void reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
|
||||
this->reserve(std::max(reserve_size, new_size));
|
||||
this->size_ = new_size;
|
||||
}
|
||||
uint8_t *data() { return this->data_.get(); }
|
||||
const uint8_t *data() const { return this->data_.get(); }
|
||||
size_t size() const { return this->size_; }
|
||||
bool empty() const { return this->size_ == 0; }
|
||||
uint8_t &operator[](size_t i) { return this->data_[i]; }
|
||||
const uint8_t &operator[](size_t i) const { return this->data_[i]; }
|
||||
/// Release all memory (equivalent to std::vector swap trick).
|
||||
void release() {
|
||||
this->data_.reset();
|
||||
this->size_ = 0;
|
||||
this->capacity_ = 0;
|
||||
}
|
||||
|
||||
protected:
|
||||
void grow_(size_t n);
|
||||
std::unique_ptr<uint8_t[]> data_;
|
||||
size_t size_{0};
|
||||
size_t capacity_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,23 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_API
|
||||
#include "api_frame_helper.h"
|
||||
#ifdef USE_API_NOISE
|
||||
#include "api_frame_helper_noise.h"
|
||||
#endif
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
#include "api_frame_helper_plaintext.h"
|
||||
#endif
|
||||
#include "api_pb2.h"
|
||||
#include "api_pb2_service.h"
|
||||
#include "api_server.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
#include "esphome/components/esp32/crash_handler.h"
|
||||
#endif
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
#include "esphome/components/rp2040/crash_handler.h"
|
||||
#endif
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
@@ -123,7 +135,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
if (!this->flags_.service_call_subscription)
|
||||
return;
|
||||
this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
|
||||
this->send_message(call);
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
|
||||
@@ -142,12 +154,13 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override;
|
||||
void on_subscribe_bluetooth_connections_free_request() override;
|
||||
void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override;
|
||||
void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override;
|
||||
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
void send_time_request() {
|
||||
GetTimeRequest req;
|
||||
this->send_message(req, GetTimeRequest::MESSAGE_TYPE);
|
||||
this->send_message(req);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -182,6 +195,15 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) override;
|
||||
void on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) override;
|
||||
void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) override;
|
||||
void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) override;
|
||||
void on_serial_proxy_request(const SerialProxyRequest &msg) override;
|
||||
void send_serial_proxy_data(const SerialProxyDataReceived &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void send_event(event::Event *event);
|
||||
#endif
|
||||
@@ -219,6 +241,12 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->flags_.log_subscription = msg.level;
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
#ifdef USE_ESP32_CRASH_HANDLER
|
||||
esp32::crash_handler_log();
|
||||
#endif
|
||||
#ifdef USE_RP2040_CRASH_HANDLER
|
||||
rp2040::crash_handler_log();
|
||||
#endif
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request() override { this->flags_.service_call_subscription = true; }
|
||||
@@ -247,6 +275,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
return static_cast<ConnectionState>(this->flags_.connection_state) == ConnectionState::CONNECTED ||
|
||||
this->is_authenticated();
|
||||
}
|
||||
bool is_marked_for_removal() const { return this->flags_.remove; }
|
||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||
|
||||
// Get client API version for feature detection
|
||||
@@ -257,26 +286,44 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
void on_fatal_error() override;
|
||||
void on_no_setup_connection() override;
|
||||
bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) override;
|
||||
|
||||
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t header_padding, size_t total_size) {
|
||||
// Function pointer type for type-erased message encoding
|
||||
using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &);
|
||||
// Function pointer type for type-erased size calculation
|
||||
using CalculateSizeFn = uint32_t (*)(const void *);
|
||||
|
||||
template<typename T> bool send_message(const T &msg) {
|
||||
if constexpr (T::ESTIMATED_SIZE == 0) {
|
||||
return this->send_message_(0, T::MESSAGE_TYPE, &encode_msg_noop, &msg);
|
||||
} else {
|
||||
return this->send_message_(msg.calculate_size(), T::MESSAGE_TYPE, &proto_encode_msg<T>, &msg);
|
||||
}
|
||||
}
|
||||
|
||||
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) {
|
||||
shared_buf.clear();
|
||||
// Reserve space for header padding + message + footer
|
||||
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
|
||||
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
|
||||
shared_buf.reserve(total_size);
|
||||
// Resize to add header padding so message encoding starts at the correct position
|
||||
shared_buf.resize(header_padding);
|
||||
// Reserve full size but only set initial size to header padding
|
||||
// so message encoding starts at the correct position
|
||||
shared_buf.reserve_and_resize(total_size, header_padding);
|
||||
}
|
||||
|
||||
// Convenience overload - computes frame overhead internally
|
||||
void prepare_first_message_buffer(std::vector<uint8_t> &shared_buf, size_t payload_size) {
|
||||
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) {
|
||||
const uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
const uint8_t footer_size = this->helper_->frame_footer_size();
|
||||
this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size);
|
||||
}
|
||||
|
||||
bool try_to_clear_buffer(bool log_out_of_space);
|
||||
bool try_to_clear_buffer(bool log_out_of_space) {
|
||||
if (this->flags_.remove)
|
||||
return false;
|
||||
if (this->helper_->can_write_without_blocking())
|
||||
return true;
|
||||
return this->try_to_clear_buffer_slow_(log_out_of_space);
|
||||
}
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||
|
||||
const char *get_name() const { return this->helper_->get_client_name(); }
|
||||
@@ -286,6 +333,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
}
|
||||
|
||||
protected:
|
||||
bool try_to_clear_buffer_slow_(bool log_out_of_space);
|
||||
|
||||
// Helper function to handle authentication completion
|
||||
void complete_authentication_();
|
||||
|
||||
@@ -312,50 +361,67 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void process_state_subscriptions_();
|
||||
#endif
|
||||
|
||||
// Non-template helper to encode any ProtoMessage
|
||||
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
|
||||
uint32_t remaining_size);
|
||||
|
||||
// Helper to fill entity state base and encode message
|
||||
static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg, uint8_t message_type,
|
||||
APIConnection *conn, uint32_t remaining_size) {
|
||||
msg.key = entity->get_object_id_hash();
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = entity->get_device_id();
|
||||
#endif
|
||||
return encode_message_to_buffer(msg, message_type, conn, remaining_size);
|
||||
// Size thunk — converts void* back to concrete type for direct calculate_size() call
|
||||
template<typename T> static uint32_t calc_size(const void *msg) {
|
||||
return static_cast<const T *>(msg)->calculate_size();
|
||||
}
|
||||
|
||||
// Helper to fill entity info base and encode message
|
||||
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg, uint8_t message_type,
|
||||
APIConnection *conn, uint32_t remaining_size) {
|
||||
// Set common fields that are shared by all entity types
|
||||
msg.key = entity->get_object_id_hash();
|
||||
// Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
|
||||
static void encode_msg_noop(const void *, ProtoWriteBuffer &) {}
|
||||
|
||||
// API 1.14+ clients compute object_id client-side from the entity name
|
||||
// For older clients, we must send object_id for backward compatibility
|
||||
// See: https://github.com/esphome/backlog/issues/76
|
||||
// TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then
|
||||
// Buffer must remain in scope until encode_message_to_buffer is called
|
||||
char object_id_buf[OBJECT_ID_MAX_LEN];
|
||||
if (!conn->client_supports_api_version(1, 14)) {
|
||||
msg.object_id = entity->get_object_id_to(object_id_buf);
|
||||
// Non-template buffer management for send_message
|
||||
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
|
||||
|
||||
// Non-template buffer management for batch encoding
|
||||
static uint16_t encode_to_buffer(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
|
||||
APIConnection *conn, uint32_t remaining_size);
|
||||
|
||||
// Thin template wrapper — computes size, delegates buffer work to non-template helper
|
||||
template<typename T> static uint16_t encode_message_to_buffer(T &msg, APIConnection *conn, uint32_t remaining_size) {
|
||||
if constexpr (T::ESTIMATED_SIZE == 0) {
|
||||
return encode_to_buffer(0, &encode_msg_noop, &msg, conn, remaining_size);
|
||||
} else {
|
||||
return encode_to_buffer(msg.calculate_size(), &proto_encode_msg<T>, &msg, conn, remaining_size);
|
||||
}
|
||||
}
|
||||
|
||||
if (entity->has_own_name()) {
|
||||
msg.name = entity->get_name();
|
||||
}
|
||||
// Non-template core — fills state fields and encodes
|
||||
static uint16_t fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg,
|
||||
CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn,
|
||||
uint32_t remaining_size);
|
||||
|
||||
// Set common EntityBase properties
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.icon = entity->get_icon_ref();
|
||||
#endif
|
||||
msg.disabled_by_default = entity->is_disabled_by_default();
|
||||
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
|
||||
#ifdef USE_DEVICES
|
||||
msg.device_id = entity->get_device_id();
|
||||
#endif
|
||||
return encode_message_to_buffer(msg, message_type, conn, remaining_size);
|
||||
// Thin template wrapper
|
||||
template<typename T>
|
||||
static uint16_t fill_and_encode_entity_state(EntityBase *entity, T &msg, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
return fill_and_encode_entity_state(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
|
||||
}
|
||||
|
||||
// Non-template core — fills info fields, allocates buffers, and encodes
|
||||
static uint16_t fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg,
|
||||
CalculateSizeFn size_fn, MessageEncodeFn encode_fn, APIConnection *conn,
|
||||
uint32_t remaining_size);
|
||||
|
||||
// Thin template wrapper
|
||||
template<typename T>
|
||||
static uint16_t fill_and_encode_entity_info(EntityBase *entity, T &msg, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
return fill_and_encode_entity_info(entity, msg, &calc_size<T>, &proto_encode_msg<T>, conn, remaining_size);
|
||||
}
|
||||
|
||||
// Non-template core — fills device_class, then delegates to fill_and_encode_entity_info
|
||||
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
|
||||
StringRef &device_class_field, CalculateSizeFn size_fn,
|
||||
MessageEncodeFn encode_fn, APIConnection *conn,
|
||||
uint32_t remaining_size);
|
||||
|
||||
// Thin template wrapper
|
||||
template<typename T>
|
||||
static uint16_t fill_and_encode_entity_info_with_device_class(EntityBase *entity, T &msg,
|
||||
StringRef &device_class_field, APIConnection *conn,
|
||||
uint32_t remaining_size) {
|
||||
return fill_and_encode_entity_info_with_device_class(entity, msg, device_class_field, &calc_size<T>,
|
||||
&proto_encode_msg<T>, conn, remaining_size);
|
||||
}
|
||||
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
@@ -370,6 +436,10 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
||||
}
|
||||
|
||||
// Send keepalive ping or disconnect unresponsive client.
|
||||
// Cold path — extracted from loop() to reduce instruction cache pressure.
|
||||
void __attribute__((noinline)) check_keepalive_(uint32_t now);
|
||||
|
||||
// Process active iterator (list_entities/initial_state) during connection setup.
|
||||
// Extracted from loop() — only runs during initial handshake, NONE in steady state.
|
||||
void __attribute__((noinline)) process_active_iterator_();
|
||||
@@ -485,7 +555,13 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// === Optimal member ordering for 32-bit systems ===
|
||||
|
||||
// Group 1: Pointers (4 bytes each on 32-bit)
|
||||
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
||||
std::unique_ptr<APIFrameHelper> helper_;
|
||||
#elif defined(USE_API_NOISE)
|
||||
std::unique_ptr<APINoiseFrameHelper> helper_;
|
||||
#elif defined(USE_API_PLAINTEXT)
|
||||
std::unique_ptr<APIPlaintextFrameHelper> helper_;
|
||||
#endif
|
||||
APIServer *parent_;
|
||||
|
||||
// Group 2: Iterator union (saves ~16 bytes vs separate iterators)
|
||||
@@ -541,6 +617,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
uint8_t aux_data_index = AUX_DATA_UNUSED);
|
||||
// Add item to the front of the batch (for high priority messages like ping)
|
||||
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
|
||||
// Single push_back site to avoid duplicate _M_realloc_insert instantiation
|
||||
void push_item(const BatchItem &item);
|
||||
|
||||
// Clear all items
|
||||
void clear() {
|
||||
@@ -621,8 +699,8 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
|
||||
bool schedule_batch_();
|
||||
void process_batch_();
|
||||
void process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
|
||||
uint8_t footer_size) __attribute__((noinline));
|
||||
void process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding, uint8_t footer_size)
|
||||
__attribute__((noinline));
|
||||
void clear_batch_() {
|
||||
this->deferred_batch_.clear();
|
||||
this->flags_.batch_scheduled = false;
|
||||
|
||||
@@ -100,149 +100,61 @@ const LogString *api_error_to_logstr(APIError err) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
// Default implementation for loop - handles sending buffered data
|
||||
APIError APIFrameHelper::loop() {
|
||||
if (this->tx_buf_count_ > 0) {
|
||||
APIError err = try_send_tx_buf_();
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
return err;
|
||||
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
|
||||
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
|
||||
int err = errno;
|
||||
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
|
||||
HELPER_LOG("Socket write failed with errno %d", err);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
}
|
||||
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
// Common socket write error handling
|
||||
APIError APIFrameHelper::handle_socket_write_error_() {
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
HELPER_LOG("Socket write failed with errno %d", errno);
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
|
||||
// Helper method to buffer data from IOVs
|
||||
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
|
||||
uint16_t offset) {
|
||||
// Check if queue is full
|
||||
if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
|
||||
HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
|
||||
this->state_ = State::FAILED;
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t buffer_size = total_write_len - offset;
|
||||
auto &buffer = this->tx_buf_[this->tx_buf_tail_];
|
||||
buffer = std::make_unique<SendBuffer>(SendBuffer{
|
||||
.data = std::make_unique<uint8_t[]>(buffer_size),
|
||||
.size = buffer_size,
|
||||
.offset = 0,
|
||||
});
|
||||
|
||||
uint16_t to_skip = offset;
|
||||
uint16_t write_pos = 0;
|
||||
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
if (to_skip >= iov[i].iov_len) {
|
||||
// Skip this entire segment
|
||||
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
|
||||
} else {
|
||||
// Include this segment (partially or fully)
|
||||
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
|
||||
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
|
||||
std::memcpy(buffer->data.get() + write_pos, src, len);
|
||||
write_pos += len;
|
||||
to_skip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Update circular buffer tracking
|
||||
this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->tx_buf_count_++;
|
||||
}
|
||||
|
||||
// This method writes data to socket or buffers it
|
||||
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
|
||||
// Returns OK if all data was sent or successfully queued.
|
||||
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
|
||||
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
|
||||
// Returns APIError::OK if successful (or would block, but data has been buffered)
|
||||
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
|
||||
|
||||
if (iovcnt == 0)
|
||||
return APIError::OK; // Nothing to do, success
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Try to send any existing buffered data first if there is any
|
||||
if (this->tx_buf_count_ > 0) {
|
||||
APIError send_result = try_send_tx_buf_();
|
||||
// If real error occurred (not just WOULD_BLOCK), return it
|
||||
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
|
||||
return send_result;
|
||||
}
|
||||
uint16_t skip = 0;
|
||||
|
||||
// If there is still data in the buffer, we can't send, buffer
|
||||
// the new data and return
|
||||
if (this->tx_buf_count_ > 0) {
|
||||
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
|
||||
return APIError::OK; // Success, data buffered
|
||||
}
|
||||
// Drain any existing backlog first
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
APIError err = this->drain_overflow_and_handle_errors_();
|
||||
if (err != APIError::OK)
|
||||
return err;
|
||||
}
|
||||
|
||||
// Try to send directly if no buffered data
|
||||
// Optimize for single iovec case (common for plaintext API)
|
||||
ssize_t sent =
|
||||
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
// If backlog is clear, try direct send
|
||||
if (this->overflow_buf_.empty()) [[likely]] {
|
||||
ssize_t sent =
|
||||
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
|
||||
if (sent == -1) {
|
||||
APIError err = this->handle_socket_write_error_();
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
// Socket would block, buffer the data
|
||||
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
|
||||
return APIError::OK; // Success, data buffered
|
||||
}
|
||||
return err; // Socket write failed
|
||||
} else if (static_cast<uint16_t>(sent) < total_write_len) {
|
||||
// Partially sent, buffer the remaining data
|
||||
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent));
|
||||
}
|
||||
|
||||
return APIError::OK; // Success, all data sent or buffered
|
||||
}
|
||||
|
||||
// Common implementation for trying to send buffered data
|
||||
// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
|
||||
APIError APIFrameHelper::try_send_tx_buf_() {
|
||||
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
|
||||
while (this->tx_buf_count_ > 0) {
|
||||
// Get the first buffer in the queue
|
||||
SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
|
||||
|
||||
// Try to send the remaining data in this buffer
|
||||
ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
|
||||
|
||||
if (sent == -1) {
|
||||
return this->handle_socket_write_error_();
|
||||
} else if (sent == 0) {
|
||||
// Nothing sent but not an error
|
||||
return APIError::WOULD_BLOCK;
|
||||
} else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
|
||||
// Partially sent, update offset
|
||||
// Cast to ensure no overflow issues with uint16_t
|
||||
front_buffer->offset += static_cast<uint16_t>(sent);
|
||||
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
|
||||
if (sent == -1) [[unlikely]] {
|
||||
int err = errno;
|
||||
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
|
||||
HELPER_LOG("Socket write failed with errno %d", err);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
|
||||
return APIError::OK;
|
||||
} else {
|
||||
// Buffer completely sent, remove it from the queue
|
||||
this->tx_buf_[this->tx_buf_head_].reset();
|
||||
this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->tx_buf_count_--;
|
||||
// Continue loop to try sending the next buffer
|
||||
skip = static_cast<uint16_t>(sent);
|
||||
}
|
||||
}
|
||||
|
||||
return APIError::OK; // All buffers sent successfully
|
||||
// Queue unsent data into overflow buffer
|
||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
|
||||
HELPER_LOG("Overflow buffer full, dropping connection");
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||
@@ -278,11 +190,12 @@ APIError APIFrameHelper::init_common_() {
|
||||
|
||||
APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
|
||||
if (received == -1) {
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN) {
|
||||
const int err = errno;
|
||||
if (err == EWOULDBLOCK || err == EAGAIN) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket read failed with errno %d", errno);
|
||||
HELPER_LOG("Socket read failed with errno %d", err);
|
||||
return APIError::SOCKET_READ_FAILED;
|
||||
} else if (received == 0) {
|
||||
state_ = State::FAILED;
|
||||
|
||||
@@ -5,13 +5,15 @@
|
||||
#include <memory>
|
||||
#include <span>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
#include "esphome/components/api/api_overflow_buffer.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -29,12 +31,14 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
|
||||
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
|
||||
#endif
|
||||
|
||||
// Extra byte reserved in rx_buf_ beyond the message size so protobuf
|
||||
// StringRef fields can be null-terminated in-place after decode.
|
||||
static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
|
||||
|
||||
// Maximum number of messages to batch in a single write operation
|
||||
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
|
||||
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
|
||||
|
||||
class ProtoWriteBuffer;
|
||||
|
||||
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
|
||||
static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32;
|
||||
|
||||
@@ -101,9 +105,9 @@ class APIFrameHelper {
|
||||
}
|
||||
virtual ~APIFrameHelper() = default;
|
||||
virtual APIError init() = 0;
|
||||
virtual APIError loop();
|
||||
virtual APIError loop() = 0;
|
||||
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
|
||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
|
||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); }
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
|
||||
APIError close() {
|
||||
if (state_ == State::CLOSED)
|
||||
@@ -130,34 +134,41 @@ class APIFrameHelper {
|
||||
//
|
||||
// For log messages: Use Nagle to coalesce multiple small log packets into
|
||||
// fewer larger packets, reducing WiFi overhead. However, we limit batching
|
||||
// to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained
|
||||
// devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into
|
||||
// shared pbufs, but holding data too long waiting for Nagle's timer causes
|
||||
// buffer exhaustion and dropped messages.
|
||||
// to avoid excessive LWIP buffer pressure on memory-constrained devices.
|
||||
// LWIP's TCP_OVERSIZE option coalesces the data into shared pbufs, but
|
||||
// holding data too long waiting for Nagle's timer causes buffer exhaustion
|
||||
// and dropped messages.
|
||||
//
|
||||
// Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all)
|
||||
// ESP32 (TCP_SND_BUF=4×MSS+) / RP2040 (8×MSS) / LibreTiny (4×MSS): 4 logs per cycle
|
||||
// ESP8266 (2×MSS): 3 logs per cycle (tightest buffers)
|
||||
//
|
||||
// Flow (ESP32/RP2040/LT): Log 1 (Nagle on) -> Log 2 -> Log 3 -> Log 4 (NODELAY, flush)
|
||||
// Flow (ESP8266): Log 1 (Nagle on) -> Log 2 -> Log 3 (NODELAY, flush all)
|
||||
//
|
||||
void set_nodelay_for_message(bool is_log_message) {
|
||||
if (!is_log_message) {
|
||||
if (this->nodelay_state_ != NODELAY_ON) {
|
||||
if (this->nodelay_counter_) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
this->nodelay_counter_ = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd)
|
||||
if (this->nodelay_state_ == NODELAY_ON) {
|
||||
// Log message: enable Nagle on first, flush after LOG_NAGLE_COUNT
|
||||
if (!this->nodelay_counter_)
|
||||
this->set_nodelay_raw_(false);
|
||||
this->nodelay_state_ = 1;
|
||||
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
|
||||
if (++this->nodelay_counter_ > LOG_NAGLE_COUNT) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
} else {
|
||||
this->nodelay_state_++;
|
||||
this->nodelay_counter_ = 0;
|
||||
}
|
||||
}
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
// Resize buffer to include footer space if needed (e.g. Noise MAC)
|
||||
if (frame_footer_size_)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
||||
MessageInfo msg{type, 0,
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
// Write multiple protobuf messages in a single operation
|
||||
// messages contains (message_type, offset, length) for each message in the buffer
|
||||
// The buffer contains all messages with appropriate padding before each
|
||||
@@ -174,37 +185,28 @@ class APIFrameHelper {
|
||||
// rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
|
||||
// and clearing would lose partially received data.
|
||||
if (this->rx_buf_len_ == 0) {
|
||||
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
|
||||
std::vector<uint8_t>().swap(this->rx_buf_);
|
||||
this->rx_buf_.release();
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
// Buffer containing data to be sent
|
||||
struct SendBuffer {
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
uint16_t size{0}; // Total size of the buffer
|
||||
uint16_t offset{0}; // Current offset within the buffer
|
||||
|
||||
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
|
||||
uint16_t remaining() const { return size - offset; }
|
||||
const uint8_t *current_data() const { return data.get() + offset; }
|
||||
};
|
||||
// Drain backlogged overflow data to the socket and handle errors.
|
||||
// Called when overflow_buf_.empty() is false. Out-of-line to keep the
|
||||
// fast path (empty check) inline at call sites.
|
||||
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
|
||||
APIError drain_overflow_and_handle_errors_();
|
||||
|
||||
// Common implementation for writing raw data to socket
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
|
||||
|
||||
// Try to send data from the tx buffer
|
||||
APIError try_send_tx_buf_();
|
||||
|
||||
// Helper method to buffer data from IOVs
|
||||
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
|
||||
|
||||
// Common socket write error handling
|
||||
APIError handle_socket_write_error_();
|
||||
template<typename StateEnum>
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
|
||||
const std::string &info, StateEnum &state, StateEnum failed_state);
|
||||
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
|
||||
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
|
||||
APIError check_socket_write_err_(int err) {
|
||||
if (err == EWOULDBLOCK || err == EAGAIN)
|
||||
return APIError::WOULD_BLOCK;
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
|
||||
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
@@ -228,9 +230,20 @@ class APIFrameHelper {
|
||||
EXPLICIT_REJECT = 8, // Noise only
|
||||
};
|
||||
|
||||
// Containers (size varies, but typically 12+ bytes on 32-bit)
|
||||
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
|
||||
std::vector<uint8_t> rx_buf_;
|
||||
// Fast inline state check for read_packet/write_protobuf_messages hot path.
|
||||
// Returns OK only in DATA state; maps CLOSED/FAILED to BAD_STATE and any
|
||||
// other intermediate state to WOULD_BLOCK.
|
||||
inline APIError ESPHOME_ALWAYS_INLINE check_data_state_() const {
|
||||
if (this->state_ == State::DATA)
|
||||
return APIError::OK;
|
||||
if (this->state_ == State::CLOSED || this->state_ == State::FAILED)
|
||||
return APIError::BAD_STATE;
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
// Backlog for unsent data when TCP send buffer is full (rarely used in production)
|
||||
APIOverflowBuffer overflow_buf_;
|
||||
APIBuffer rx_buf_;
|
||||
|
||||
// Client name buffer - stores name from Hello message or initial peername
|
||||
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
|
||||
@@ -240,15 +253,17 @@ class APIFrameHelper {
|
||||
State state_{State::INITIALIZE};
|
||||
uint8_t frame_header_padding_{0};
|
||||
uint8_t frame_footer_size_{0};
|
||||
uint8_t tx_buf_head_{0};
|
||||
uint8_t tx_buf_tail_{0};
|
||||
uint8_t tx_buf_count_{0};
|
||||
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
|
||||
// (immediate send). Values 1-2 count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
|
||||
static constexpr int8_t NODELAY_ON = -1;
|
||||
static constexpr int8_t LOG_NAGLE_COUNT = 2;
|
||||
int8_t nodelay_state_{NODELAY_ON};
|
||||
// Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send).
|
||||
// Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0.
|
||||
// ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
|
||||
// ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more.
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr uint8_t LOG_NAGLE_COUNT = 2;
|
||||
#else
|
||||
static constexpr uint8_t LOG_NAGLE_COUNT = 3;
|
||||
#endif
|
||||
uint8_t nodelay_counter_{0};
|
||||
|
||||
// Internal helper to set TCP_NODELAY socket option
|
||||
void set_nodelay_raw_(bool enable) {
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.noise";
|
||||
#ifdef USE_ESP8266
|
||||
static const char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
|
||||
static constexpr char PROLOGUE_INIT[] PROGMEM = "NoiseAPIInit";
|
||||
#else
|
||||
static const char *const PROLOGUE_INIT = "NoiseAPIInit";
|
||||
#endif
|
||||
@@ -153,8 +153,10 @@ APIError APINoiseFrameHelper::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
return this->drain_overflow_and_handle_errors_();
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_.
|
||||
@@ -194,17 +196,20 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
|
||||
|
||||
// Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
|
||||
uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
|
||||
bool is_data = (state_ == State::DATA);
|
||||
uint16_t limit = is_data ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
|
||||
if (msg_size > limit) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
|
||||
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
|
||||
return is_data ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
|
||||
}
|
||||
|
||||
// Reserve space for body
|
||||
if (this->rx_buf_.size() != msg_size) {
|
||||
this->rx_buf_.resize(msg_size);
|
||||
}
|
||||
// Reserve space for body (+ null terminator in DATA state so protobuf
|
||||
// StringRef fields can be safely null-terminated in-place after decode.
|
||||
// During handshake, rx_buf_.size() is used in prologue construction, so
|
||||
// the buffer must be exactly msg_size to avoid prologue mismatch.)
|
||||
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
|
||||
this->rx_buf_.resize(alloc_size);
|
||||
|
||||
if (rx_buf_len_ < msg_size) {
|
||||
// more data to read
|
||||
@@ -255,16 +260,19 @@ APIError APINoiseFrameHelper::state_action_() {
|
||||
// ignore contents, may be used in future for flags
|
||||
// Resize for: existing prologue + 2 size bytes + frame data
|
||||
size_t old_size = this->prologue_.size();
|
||||
this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
|
||||
this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
|
||||
this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
|
||||
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
|
||||
size_t rx_size = this->rx_buf_.size();
|
||||
this->prologue_.resize(old_size + 2 + rx_size);
|
||||
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
|
||||
this->prologue_[old_size + 1] = (uint8_t) rx_size;
|
||||
if (rx_size > 0) {
|
||||
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
|
||||
}
|
||||
|
||||
state_ = State::SERVER_HELLO;
|
||||
}
|
||||
if (state_ == State::SERVER_HELLO) {
|
||||
// send server hello
|
||||
const std::string &name = App.get_name();
|
||||
const auto &name = App.get_name();
|
||||
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||
get_mac_address_into_buffer(mac);
|
||||
|
||||
@@ -370,6 +378,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||
reason_len = std::min(reason_len, sizeof(data) - 1);
|
||||
if (reason_len > 0) {
|
||||
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||
}
|
||||
@@ -377,6 +386,7 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
|
||||
// Normal memory access
|
||||
const char *reason_str = LOG_STR_ARG(reason);
|
||||
size_t reason_len = strlen(reason_str);
|
||||
reason_len = std::min(reason_len, sizeof(data) - 1);
|
||||
if (reason_len > 0) {
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
|
||||
std::memcpy(data + 1, reason_str, reason_len);
|
||||
@@ -392,14 +402,9 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
|
||||
state_ = orig_state;
|
||||
}
|
||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
APIError aerr = this->state_action_();
|
||||
if (aerr != APIError::OK) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
}
|
||||
|
||||
if (this->state_ != State::DATA) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
aerr = this->try_read_frame_();
|
||||
if (aerr != APIError::OK)
|
||||
@@ -407,7 +412,18 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
|
||||
// read_packet() must only be called in DATA state; the extra
|
||||
// RX_BUF_NULL_TERMINATOR byte is only allocated in DATA state
|
||||
// (see try_read_frame_), so calling this during handshake would
|
||||
// underflow the size calculation below.
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
// rx_buf_ has RX_BUF_NULL_TERMINATOR extra byte for null termination
|
||||
// (only added in DATA state — see try_read_frame_), so subtract it
|
||||
// to get the actual encrypted data size for decryption.
|
||||
size_t encrypted_size = this->rx_buf_.size() - RX_BUF_NULL_TERMINATOR;
|
||||
noise_buffer_set_inout(mbuf, this->rx_buf_.data(), encrypted_size, encrypted_size);
|
||||
int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
|
||||
APIError decrypt_err =
|
||||
handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
|
||||
@@ -436,23 +452,10 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = type;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
// Resize to include MAC space (required for Noise encryption)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
||||
MessageInfo msg{type, 0,
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
|
||||
APIError aerr = state_action_();
|
||||
if (aerr != APIError::OK) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
}
|
||||
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
if (messages.empty()) {
|
||||
return APIError::OK;
|
||||
@@ -474,7 +477,7 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
|
||||
// buf_start[1], buf_start[2] to be set after encryption
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
const uint8_t msg_offset = 3;
|
||||
constexpr uint8_t msg_offset = 3;
|
||||
buf_start[msg_offset] = static_cast<uint8_t>(msg.message_type >> 8); // type high byte
|
||||
buf_start[msg_offset + 1] = static_cast<uint8_t>(msg.message_type); // type low byte
|
||||
buf_start[msg_offset + 2] = static_cast<uint8_t>(msg.payload_size >> 8); // data_len high byte
|
||||
@@ -563,8 +566,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
// set_prologue copies it into handshakestate, so we can get rid of it now
|
||||
// Use swap idiom to actually release memory (= {} only clears size, not capacity)
|
||||
std::vector<uint8_t>().swap(prologue_);
|
||||
prologue_.release();
|
||||
|
||||
err = noise_handshakestate_start(handshake_);
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
@@ -574,7 +576,9 @@ APIError APINoiseFrameHelper::init_handshake_() {
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::check_handshake_finished_() {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(state_ == State::HANDSHAKE);
|
||||
#endif
|
||||
|
||||
int action = noise_handshakestate_get_action(handshake_);
|
||||
if (action == NOISE_ACTION_READ_MESSAGE || action == NOISE_ACTION_WRITE_MESSAGE)
|
||||
|
||||
@@ -22,7 +22,6 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
@@ -43,8 +42,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
// Reference to noise context (4 bytes on 32-bit)
|
||||
APINoiseContext &ctx_;
|
||||
|
||||
// Vector (12 bytes on 32-bit)
|
||||
std::vector<uint8_t> prologue_;
|
||||
// Buffer for noise handshake prologue (released after handshake)
|
||||
APIBuffer prologue_;
|
||||
|
||||
// NoiseProtocolId (size depends on implementation)
|
||||
NoiseProtocolId nid_;
|
||||
|
||||
@@ -64,8 +64,10 @@ APIError APIPlaintextFrameHelper::loop() {
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
return this->drain_overflow_and_handle_errors_();
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_.
|
||||
@@ -128,45 +130,44 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
||||
|
||||
// Skip indicator byte at position 0
|
||||
uint8_t varint_pos = 1;
|
||||
uint32_t consumed = 0;
|
||||
|
||||
auto msg_size_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
|
||||
// rx_header_buf_pos_ >= 3 and varint_pos == 1, so len >= 2
|
||||
auto msg_size_varint = ProtoVarInt::parse_non_empty(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
|
||||
if (!msg_size_varint.has_value()) {
|
||||
// not enough data there yet
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
|
||||
if (msg_size_varint.value > MAX_MESSAGE_SIZE) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
|
||||
MAX_MESSAGE_SIZE);
|
||||
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u",
|
||||
static_cast<uint32_t>(msg_size_varint.value), MAX_MESSAGE_SIZE);
|
||||
return APIError::BAD_DATA_PACKET;
|
||||
}
|
||||
rx_header_parsed_len_ = msg_size_varint->as_uint16();
|
||||
rx_header_parsed_len_ = static_cast<uint16_t>(msg_size_varint.value);
|
||||
|
||||
// Move to next varint position
|
||||
varint_pos += consumed;
|
||||
varint_pos += msg_size_varint.consumed;
|
||||
|
||||
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos, &consumed);
|
||||
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[varint_pos], rx_header_buf_pos_ - varint_pos);
|
||||
if (!msg_type_varint.has_value()) {
|
||||
// not enough data there yet
|
||||
continue;
|
||||
}
|
||||
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
|
||||
if (msg_type_varint.value > std::numeric_limits<uint16_t>::max()) {
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
|
||||
std::numeric_limits<uint16_t>::max());
|
||||
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u",
|
||||
static_cast<uint32_t>(msg_type_varint.value), std::numeric_limits<uint16_t>::max());
|
||||
return APIError::BAD_DATA_PACKET;
|
||||
}
|
||||
rx_header_parsed_type_ = msg_type_varint->as_uint16();
|
||||
rx_header_parsed_type_ = static_cast<uint16_t>(msg_type_varint.value);
|
||||
rx_header_parsed_ = true;
|
||||
}
|
||||
// header reading done
|
||||
|
||||
// Reserve space for body
|
||||
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
|
||||
this->rx_buf_.resize(this->rx_header_parsed_len_);
|
||||
}
|
||||
// Reserve space for body (+ null terminator so protobuf StringRef fields
|
||||
// can be safely null-terminated in-place after decode)
|
||||
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
|
||||
|
||||
if (rx_buf_len_ < rx_header_parsed_len_) {
|
||||
// more data to read
|
||||
@@ -194,11 +195,11 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
if (this->state_ != State::DATA) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
APIError aerr = this->check_data_state_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
APIError aerr = this->try_read_frame_();
|
||||
aerr = this->try_read_frame_();
|
||||
if (aerr != APIError::OK) {
|
||||
if (aerr == APIError::BAD_INDICATOR) {
|
||||
// Make sure to tell the remote that we don't
|
||||
@@ -236,16 +237,11 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = this->rx_header_parsed_type_;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
|
||||
std::span<const MessageInfo> messages) {
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
APIError aerr = this->check_data_state_();
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
if (messages.empty()) {
|
||||
return APIError::OK;
|
||||
@@ -258,9 +254,11 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
|
||||
uint16_t total_write_len = 0;
|
||||
|
||||
for (const auto &msg : messages) {
|
||||
// Calculate varint sizes for header layout
|
||||
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.payload_size));
|
||||
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.message_type));
|
||||
// Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead
|
||||
uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE
|
||||
? 1
|
||||
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
|
||||
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// Calculate where to start writing the header
|
||||
@@ -282,8 +280,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
|
||||
//
|
||||
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
|
||||
// [0] - 0x00 indicator byte
|
||||
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
|
||||
// [4-5] - Message type varint (2 bytes, for types 128-32767)
|
||||
// [1-3] - Payload size varint (3 bytes, for sizes 16384-65535)
|
||||
// [4-5] - Message type varint (2 bytes, for types 128-16383)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// The message starts at offset + frame_header_padding_
|
||||
|
||||
@@ -19,7 +19,6 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -90,4 +90,10 @@ extend google.protobuf.FieldOptions {
|
||||
// - uint16_t <field>_length_{0};
|
||||
// - uint16_t <field>_count_{0};
|
||||
optional bool packed_buffer = 50015 [default=false];
|
||||
|
||||
// force: Always encode this field, even when its value equals the proto3 default.
|
||||
// Skips the zero/empty check in calculate_size() and encode(), using the _force
|
||||
// variant of the calc_ method. Use on fields that are almost always non-default
|
||||
// to eliminate dead branches on hot paths.
|
||||
optional bool force = 50016 [default=false];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
#include "api_overflow_buffer.h"
|
||||
#ifdef USE_API
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
APIOverflowBuffer::~APIOverflowBuffer() {
|
||||
for (auto *entry : this->queue_) {
|
||||
if (entry != nullptr)
|
||||
Entry::destroy(entry);
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
|
||||
while (this->count_ > 0) {
|
||||
Entry *front = this->queue_[this->head_];
|
||||
|
||||
ssize_t sent = socket->write(front->current_data(), front->remaining());
|
||||
|
||||
if (sent <= 0) {
|
||||
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
|
||||
// 0 = nothing sent (treat as no progress)
|
||||
return sent;
|
||||
}
|
||||
|
||||
if (static_cast<uint16_t>(sent) < front->remaining()) {
|
||||
// Partially sent, update offset and stop
|
||||
front->offset += static_cast<uint16_t>(sent);
|
||||
return sent;
|
||||
}
|
||||
|
||||
// Entry fully sent — free it and advance
|
||||
Entry::destroy(front);
|
||||
this->queue_[this->head_] = nullptr;
|
||||
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->count_--;
|
||||
}
|
||||
|
||||
return 0; // All drained
|
||||
}
|
||||
|
||||
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
|
||||
if (this->count_ >= API_MAX_SEND_QUEUE)
|
||||
return false;
|
||||
|
||||
uint16_t buffer_size = total_len - skip;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
|
||||
auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0};
|
||||
this->queue_[this->tail_] = entry;
|
||||
|
||||
uint16_t to_skip = skip;
|
||||
uint16_t write_pos = 0;
|
||||
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
if (to_skip >= iov[i].iov_len) {
|
||||
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
|
||||
} else {
|
||||
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
|
||||
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
|
||||
std::memcpy(entry->data + write_pos, src, len);
|
||||
write_pos += len;
|
||||
to_skip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->count_++;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
#endif // USE_API
|
||||
@@ -0,0 +1,76 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_API
|
||||
|
||||
#include "esphome/components/socket/headers.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
|
||||
///
|
||||
/// Under normal operation this buffer is **never used** — data goes straight
|
||||
/// from the frame helper to the socket. It only fills when the LWIP TCP
|
||||
/// send buffer is full (slow client, congested network, heavy logging).
|
||||
/// The queue drains automatically on subsequent write/loop calls once the
|
||||
/// socket becomes writable again.
|
||||
///
|
||||
/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python
|
||||
/// config). If the queue fills completely the connection is marked failed.
|
||||
class APIOverflowBuffer {
|
||||
public:
|
||||
/// A single heap-allocated send-backlog entry.
|
||||
/// Lifetime is manually managed — see destroy().
|
||||
struct Entry {
|
||||
uint8_t *data;
|
||||
uint16_t size; // Total size of the buffer
|
||||
uint16_t offset; // Current send offset within the buffer
|
||||
|
||||
uint16_t remaining() const { return this->size - this->offset; }
|
||||
const uint8_t *current_data() const { return this->data + this->offset; }
|
||||
|
||||
/// Free this entry and its data buffer.
|
||||
static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) {
|
||||
delete[] entry->data;
|
||||
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
};
|
||||
|
||||
~APIOverflowBuffer();
|
||||
|
||||
/// True when no backlogged data is waiting.
|
||||
bool empty() const { return this->count_ == 0; }
|
||||
|
||||
/// True when the queue has no room for another entry.
|
||||
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
|
||||
|
||||
/// Number of entries currently queued.
|
||||
uint8_t count() const { return this->count_; }
|
||||
|
||||
/// Try to drain queued data to the socket.
|
||||
/// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress,
|
||||
/// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors).
|
||||
/// Callers only need to act on -1; 0 and positive values both mean "no error".
|
||||
/// Frees entries as they are fully sent.
|
||||
ssize_t try_drain(socket::Socket *socket);
|
||||
|
||||
/// Enqueue unsent IOV data into the backlog.
|
||||
/// Copies iov data starting at byte offset `skip` into a new entry.
|
||||
/// Returns false if the queue is full (caller should fail the connection).
|
||||
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
|
||||
|
||||
protected:
|
||||
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
|
||||
uint8_t head_{0};
|
||||
uint8_t tail_{0};
|
||||
uint8_t count_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
#endif // USE_API
|
||||
+1344
-895
File diff suppressed because it is too large
Load Diff
+485
-225
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
// This file was automatically generated with a tool.
|
||||
// See script/api_protobuf/api_protobuf.py
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
#ifndef USE_API_VARINT64
|
||||
#define USE_API_VARINT64
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace esphome::api {} // namespace esphome::api
|
||||
@@ -13,7 +13,7 @@ namespace esphome::api {
|
||||
static inline void append_quoted_string(DumpBuffer &out, const StringRef &ref) {
|
||||
out.append("'");
|
||||
if (!ref.empty()) {
|
||||
out.append(ref.c_str());
|
||||
out.append(ref.c_str(), ref.size());
|
||||
}
|
||||
out.append("'");
|
||||
}
|
||||
@@ -100,6 +100,18 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
|
||||
out.append(hex_buf).append("\n");
|
||||
}
|
||||
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::SerialProxyPortType value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_PORT_TYPE_TTL:
|
||||
return "SERIAL_PROXY_PORT_TYPE_TTL";
|
||||
case enums::SERIAL_PROXY_PORT_TYPE_RS232:
|
||||
return "SERIAL_PROXY_PORT_TYPE_RS232";
|
||||
case enums::SERIAL_PROXY_PORT_TYPE_RS485:
|
||||
return "SERIAL_PROXY_PORT_TYPE_RS485";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::EntityCategory value) {
|
||||
switch (value) {
|
||||
case enums::ENTITY_CATEGORY_NONE:
|
||||
@@ -208,6 +220,20 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::DSTRuleType>(enums::DSTRuleType value) {
|
||||
switch (value) {
|
||||
case enums::DST_RULE_TYPE_NONE:
|
||||
return "DST_RULE_TYPE_NONE";
|
||||
case enums::DST_RULE_TYPE_MONTH_WEEK_DAY:
|
||||
return "DST_RULE_TYPE_MONTH_WEEK_DAY";
|
||||
case enums::DST_RULE_TYPE_JULIAN_NO_LEAP:
|
||||
return "DST_RULE_TYPE_JULIAN_NO_LEAP";
|
||||
case enums::DST_RULE_TYPE_DAY_OF_YEAR:
|
||||
return "DST_RULE_TYPE_DAY_OF_YEAR";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
|
||||
switch (value) {
|
||||
@@ -321,6 +347,8 @@ template<> const char *proto_enum_to_string<enums::ClimateAction>(enums::Climate
|
||||
return "CLIMATE_ACTION_DRYING";
|
||||
case enums::CLIMATE_ACTION_FAN:
|
||||
return "CLIMATE_ACTION_FAN";
|
||||
case enums::CLIMATE_ACTION_DEFROSTING:
|
||||
return "CLIMATE_ACTION_DEFROSTING";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -736,6 +764,48 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyParity>(enums::SerialProxyParity value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_PARITY_NONE:
|
||||
return "SERIAL_PROXY_PARITY_NONE";
|
||||
case enums::SERIAL_PROXY_PARITY_EVEN:
|
||||
return "SERIAL_PROXY_PARITY_EVEN";
|
||||
case enums::SERIAL_PROXY_PARITY_ODD:
|
||||
return "SERIAL_PROXY_PARITY_ODD";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyRequestType>(enums::SerialProxyRequestType value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
return "SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE";
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
return "SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE";
|
||||
case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH:
|
||||
return "SERIAL_PROXY_REQUEST_TYPE_FLUSH";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::SerialProxyStatus value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_STATUS_OK:
|
||||
return "SERIAL_PROXY_STATUS_OK";
|
||||
case enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS:
|
||||
return "SERIAL_PROXY_STATUS_ASSUMED_SUCCESS";
|
||||
case enums::SERIAL_PROXY_STATUS_ERROR:
|
||||
return "SERIAL_PROXY_STATUS_ERROR";
|
||||
case enums::SERIAL_PROXY_STATUS_TIMEOUT:
|
||||
return "SERIAL_PROXY_STATUS_TIMEOUT";
|
||||
case enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED:
|
||||
return "SERIAL_PROXY_STATUS_NOT_SUPPORTED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "HelloRequest");
|
||||
@@ -785,6 +855,14 @@ const char *DeviceInfo::dump_to(DumpBuffer &out) const {
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
const char *SerialProxyInfo::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SerialProxyInfo");
|
||||
dump_field(out, "name", this->name);
|
||||
dump_field(out, "port_type", static_cast<enums::SerialProxyPortType>(this->port_type));
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "DeviceInfoResponse");
|
||||
dump_field(out, "name", this->name);
|
||||
@@ -845,6 +923,13 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
dump_field(out, "zwave_home_id", this->zwave_home_id);
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
for (const auto &it : this->serial_proxies) {
|
||||
out.append(" serial_proxies: ");
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
@@ -1252,10 +1337,35 @@ const char *GetTimeRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append("GetTimeRequest {}");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *DSTRule::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "DSTRule");
|
||||
dump_field(out, "time_seconds", this->time_seconds);
|
||||
dump_field(out, "day", this->day);
|
||||
dump_field(out, "type", static_cast<enums::DSTRuleType>(this->type));
|
||||
dump_field(out, "month", this->month);
|
||||
dump_field(out, "week", this->week);
|
||||
dump_field(out, "day_of_week", this->day_of_week);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ParsedTimezone::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ParsedTimezone");
|
||||
dump_field(out, "std_offset_seconds", this->std_offset_seconds);
|
||||
dump_field(out, "dst_offset_seconds", this->dst_offset_seconds);
|
||||
out.append(" dst_start: ");
|
||||
this->dst_start.dump_to(out);
|
||||
out.append("\n");
|
||||
out.append(" dst_end: ");
|
||||
this->dst_end.dump_to(out);
|
||||
out.append("\n");
|
||||
return out.c_str();
|
||||
}
|
||||
const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "GetTimeResponse");
|
||||
dump_field(out, "epoch_seconds", this->epoch_seconds);
|
||||
dump_field(out, "timezone", this->timezone);
|
||||
out.append(" parsed_timezone: ");
|
||||
this->parsed_timezone.dump_to(out);
|
||||
out.append("\n");
|
||||
return out.c_str();
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
@@ -2469,6 +2579,78 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SerialProxyConfigureRequest");
|
||||
dump_field(out, "instance", this->instance);
|
||||
dump_field(out, "baudrate", this->baudrate);
|
||||
dump_field(out, "flow_control", this->flow_control);
|
||||
dump_field(out, "parity", static_cast<enums::SerialProxyParity>(this->parity));
|
||||
dump_field(out, "stop_bits", this->stop_bits);
|
||||
dump_field(out, "data_size", this->data_size);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SerialProxyDataReceived");
|
||||
dump_field(out, "instance", this->instance);
|
||||
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SerialProxyWriteRequest");
|
||||
dump_field(out, "instance", this->instance);
|
||||
dump_bytes_field(out, "data", this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest");
|
||||
dump_field(out, "instance", this->instance);
|
||||
dump_field(out, "line_states", this->line_states);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest");
|
||||
dump_field(out, "instance", this->instance);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse");
|
||||
dump_field(out, "instance", this->instance);
|
||||
dump_field(out, "line_states", this->line_states);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SerialProxyRequest");
|
||||
dump_field(out, "instance", this->instance);
|
||||
dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type));
|
||||
return out.c_str();
|
||||
}
|
||||
const char *SerialProxyRequestResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "SerialProxyRequestResponse");
|
||||
dump_field(out, "instance", this->instance);
|
||||
dump_field(out, "type", static_cast<enums::SerialProxyRequestType>(this->type));
|
||||
dump_field(out, "status", static_cast<enums::SerialProxyStatus>(this->status));
|
||||
dump_field(out, "error_message", this->error_message);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothSetConnectionParamsRequest");
|
||||
dump_field(out, "address", this->address);
|
||||
dump_field(out, "min_interval", this->min_interval);
|
||||
dump_field(out, "max_interval", this->max_interval);
|
||||
dump_field(out, "latency", this->latency);
|
||||
dump_field(out, "timeout", this->timeout);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothSetConnectionParamsResponse");
|
||||
dump_field(out, "address", this->address);
|
||||
dump_field(out, "error", this->error);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
|
||||
@@ -634,6 +634,72 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_infrared_rf_transmit_raw_timings_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case SerialProxyConfigureRequest::MESSAGE_TYPE: {
|
||||
SerialProxyConfigureRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_serial_proxy_configure_request"), msg);
|
||||
#endif
|
||||
this->on_serial_proxy_configure_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case SerialProxyWriteRequest::MESSAGE_TYPE: {
|
||||
SerialProxyWriteRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_serial_proxy_write_request"), msg);
|
||||
#endif
|
||||
this->on_serial_proxy_write_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case SerialProxySetModemPinsRequest::MESSAGE_TYPE: {
|
||||
SerialProxySetModemPinsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_serial_proxy_set_modem_pins_request"), msg);
|
||||
#endif
|
||||
this->on_serial_proxy_set_modem_pins_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case SerialProxyGetModemPinsRequest::MESSAGE_TYPE: {
|
||||
SerialProxyGetModemPinsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_serial_proxy_get_modem_pins_request"), msg);
|
||||
#endif
|
||||
this->on_serial_proxy_get_modem_pins_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
case SerialProxyRequest::MESSAGE_TYPE: {
|
||||
SerialProxyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_serial_proxy_request"), msg);
|
||||
#endif
|
||||
this->on_serial_proxy_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case BluetoothSetConnectionParamsRequest::MESSAGE_TYPE: {
|
||||
BluetoothSetConnectionParamsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_bluetooth_set_connection_params_request"), msg);
|
||||
#endif
|
||||
this->on_bluetooth_set_connection_params_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -19,14 +19,6 @@ class APIServerConnectionBase : public ProtoService {
|
||||
public:
|
||||
#endif
|
||||
|
||||
bool send_message(const ProtoMessage &msg, uint8_t message_type) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
DumpBuffer dump_buf;
|
||||
this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
|
||||
#endif
|
||||
return this->send_message_impl(msg, message_type);
|
||||
}
|
||||
|
||||
virtual void on_hello_request(const HelloRequest &value){};
|
||||
|
||||
virtual void on_disconnect_request(){};
|
||||
@@ -224,6 +216,28 @@ class APIServerConnectionBase : public ProtoService {
|
||||
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){};
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
virtual void on_serial_proxy_request(const SerialProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
@@ -28,10 +28,12 @@ static const char *const TAG = "api";
|
||||
// APIServer
|
||||
APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
APIServer::APIServer() {
|
||||
global_api_server = this;
|
||||
// Pre-allocate shared write buffer
|
||||
shared_write_buffer_.reserve(64);
|
||||
APIServer::APIServer() { global_api_server = this; }
|
||||
|
||||
void APIServer::socket_failed_(const LogString *msg) {
|
||||
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
|
||||
this->destroy_socket_();
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
void APIServer::setup() {
|
||||
@@ -52,22 +54,20 @@ void APIServer::setup() {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
|
||||
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections
|
||||
if (this->socket_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not create socket");
|
||||
this->mark_failed();
|
||||
this->socket_failed_(LOG_STR("creation"));
|
||||
return;
|
||||
}
|
||||
int enable = 1;
|
||||
int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
|
||||
ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno);
|
||||
// we can still continue
|
||||
}
|
||||
err = this->socket_->setblocking(false);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
|
||||
this->mark_failed();
|
||||
this->socket_failed_(LOG_STR("nonblocking"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,28 +75,28 @@ void APIServer::setup() {
|
||||
|
||||
socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
|
||||
if (sl == 0) {
|
||||
ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno);
|
||||
this->mark_failed();
|
||||
this->socket_failed_(LOG_STR("set sockaddr"));
|
||||
return;
|
||||
}
|
||||
|
||||
err = this->socket_->bind((struct sockaddr *) &server, sl);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
|
||||
this->mark_failed();
|
||||
this->socket_failed_(LOG_STR("bind"));
|
||||
return;
|
||||
}
|
||||
|
||||
err = this->socket_->listen(this->listen_backlog_);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
|
||||
this->mark_failed();
|
||||
this->socket_failed_(LOG_STR("listen"));
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
if (logger::global_logger != nullptr) {
|
||||
logger::global_logger->add_log_listener(this);
|
||||
logger::global_logger->add_log_callback(
|
||||
this, [](void *self, uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
static_cast<APIServer *>(self)->on_log(level, tag, message, message_len);
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -199,7 +199,7 @@ void APIServer::remove_client_(size_t client_index) {
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIServer::accept_new_connections_() {
|
||||
void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
while (true) {
|
||||
struct sockaddr_storage source_addr;
|
||||
socklen_t addr_len = sizeof(source_addr);
|
||||
@@ -359,11 +359,11 @@ void APIServer::on_update(update::UpdateEntity *obj) {
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) {
|
||||
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
// We could add code to manage a second subscription type, but, since this message type is
|
||||
// very infrequent and small, we simply send it to all clients
|
||||
for (auto &c : this->clients_)
|
||||
c->send_message(msg, api::ZWaveProxyRequest::MESSAGE_TYPE);
|
||||
c->send_message(msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -433,8 +433,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
// Helper to add subscription (reduces duplication)
|
||||
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
|
||||
bool once) {
|
||||
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
|
||||
std::function<void(StringRef)> &&f, bool once) {
|
||||
this->state_subs_.push_back(HomeAssistantStateSubscription{
|
||||
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
|
||||
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
|
||||
@@ -443,7 +443,7 @@ void APIServer::add_state_subscription_(const char *entity_id, const char *attri
|
||||
|
||||
// Helper to add subscription with heap-allocated strings (reduces duplication)
|
||||
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> f, bool once) {
|
||||
std::function<void(StringRef)> &&f, bool once) {
|
||||
HomeAssistantStateSubscription sub;
|
||||
// Allocate heap storage for the strings
|
||||
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
|
||||
@@ -463,29 +463,29 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
|
||||
|
||||
// New const char* overload (for internal components - zero allocation)
|
||||
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
|
||||
std::function<void(StringRef)> f) {
|
||||
std::function<void(StringRef)> &&f) {
|
||||
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
|
||||
std::function<void(StringRef)> f) {
|
||||
std::function<void(StringRef)> &&f) {
|
||||
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
|
||||
}
|
||||
|
||||
// std::string overload with StringRef callback (zero-allocation callback)
|
||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> f) {
|
||||
std::function<void(StringRef)> &&f) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> f) {
|
||||
std::function<void(StringRef)> &&f) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
|
||||
}
|
||||
|
||||
// Legacy helper: wraps std::string callback and delegates to StringRef version
|
||||
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f, bool once) {
|
||||
std::function<void(const std::string &)> &&f, bool once) {
|
||||
// Wrap callback to convert StringRef -> std::string, then delegate
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute),
|
||||
std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
|
||||
@@ -494,12 +494,12 @@ void APIServer::add_state_subscription_(std::string entity_id, optional<std::str
|
||||
|
||||
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
|
||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f) {
|
||||
std::function<void(const std::string &)> &&f) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
|
||||
}
|
||||
|
||||
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f) {
|
||||
std::function<void(const std::string &)> &&f) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
|
||||
}
|
||||
|
||||
@@ -531,7 +531,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
|
||||
this->set_noise_psk(active_psk);
|
||||
for (auto &c : this->clients_) {
|
||||
DisconnectRequest req;
|
||||
c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
|
||||
c->send_message(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -582,11 +582,7 @@ void APIServer::request_time() {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool APIServer::is_connected(bool state_subscription_only) const {
|
||||
if (!state_subscription_only) {
|
||||
return !this->clients_.empty();
|
||||
}
|
||||
|
||||
bool APIServer::is_connected_with_state_subscription() const {
|
||||
for (const auto &client : this->clients_) {
|
||||
if (client->flags_.state_subscription) {
|
||||
return true;
|
||||
@@ -623,10 +619,7 @@ void APIServer::on_shutdown() {
|
||||
this->shutting_down_ = true;
|
||||
|
||||
// Close the listening socket to prevent new connections
|
||||
if (this->socket_) {
|
||||
this->socket_->close();
|
||||
this->socket_ = nullptr;
|
||||
}
|
||||
this->destroy_socket_();
|
||||
|
||||
// Change batch delay to 5ms for quick flushing during shutdown
|
||||
this->batch_delay_ = 5;
|
||||
@@ -634,7 +627,7 @@ void APIServer::on_shutdown() {
|
||||
// Send disconnect requests to all connected clients
|
||||
for (auto &c : this->clients_) {
|
||||
DisconnectRequest req;
|
||||
if (!c->send_message(req, DisconnectRequest::MESSAGE_TYPE)) {
|
||||
if (!c->send_message(req)) {
|
||||
// If we can't send the disconnect request directly (tx_buffer full),
|
||||
// schedule it at the front of the batch so it will be sent with priority
|
||||
c->schedule_message_front_(nullptr, DisconnectRequest::MESSAGE_TYPE, DisconnectRequest::ESTIMATED_SIZE);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_API
|
||||
#include "api_buffer.h"
|
||||
#include "api_noise_context.h"
|
||||
#include "api_pb2.h"
|
||||
#include "api_pb2_service.h"
|
||||
@@ -35,15 +36,11 @@ struct SavedNoisePsk {
|
||||
} PACKED; // NOLINT
|
||||
#endif
|
||||
|
||||
class APIServer : public Component,
|
||||
public Controller
|
||||
#ifdef USE_LOGGER
|
||||
,
|
||||
public logger::LogListener
|
||||
#endif
|
||||
class APIServer final : public Component,
|
||||
public Controller
|
||||
#ifdef USE_CAMERA
|
||||
,
|
||||
public camera::CameraListener
|
||||
public camera::CameraListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
@@ -56,7 +53,7 @@ class APIServer : public Component,
|
||||
void on_shutdown() override;
|
||||
bool teardown() override;
|
||||
#ifdef USE_LOGGER
|
||||
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override;
|
||||
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len);
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
|
||||
@@ -69,7 +66,7 @@ class APIServer : public Component,
|
||||
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
|
||||
|
||||
// Get reference to shared buffer for API connections
|
||||
std::vector<uint8_t> &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||
@@ -183,13 +180,14 @@ class APIServer : public Component,
|
||||
void on_update(update::UpdateEntity *obj) override;
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
|
||||
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
|
||||
#endif
|
||||
|
||||
bool is_connected(bool state_subscription_only = false) const;
|
||||
bool is_connected() const { return !this->clients_.empty(); }
|
||||
bool is_connected_with_state_subscription() const;
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
struct HomeAssistantStateSubscription {
|
||||
@@ -205,20 +203,20 @@ class APIServer : public Component,
|
||||
};
|
||||
|
||||
// New const char* overload (for internal components - zero allocation)
|
||||
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
|
||||
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
|
||||
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
|
||||
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f);
|
||||
|
||||
// std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback)
|
||||
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> f);
|
||||
std::function<void(StringRef)> &&f);
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> f);
|
||||
std::function<void(StringRef)> &&f);
|
||||
|
||||
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback)
|
||||
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f);
|
||||
std::function<void(const std::string &)> &&f);
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f);
|
||||
std::function<void(const std::string &)> &&f);
|
||||
|
||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||
#endif
|
||||
@@ -245,16 +243,23 @@ class APIServer : public Component,
|
||||
#endif // USE_API_NOISE
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
// Helper methods to reduce code duplication
|
||||
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
|
||||
bool once);
|
||||
void add_state_subscription_(std::string entity_id, optional<std::string> attribute, std::function<void(StringRef)> f,
|
||||
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> &&f,
|
||||
bool once);
|
||||
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(StringRef)> &&f, bool once);
|
||||
// Legacy helper: wraps std::string callback and delegates to StringRef version
|
||||
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f, bool once);
|
||||
std::function<void(const std::string &)> &&f, bool once);
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
// No explicit close() needed — listen sockets have no active connections on
|
||||
// failure/shutdown. Destructor handles fd cleanup (close or abort per platform).
|
||||
inline void destroy_socket_() {
|
||||
delete this->socket_;
|
||||
this->socket_ = nullptr;
|
||||
}
|
||||
void socket_failed_(const LogString *msg);
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
socket::ListenSocket *socket_{nullptr};
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
Trigger<std::string, std::string> client_connected_trigger_;
|
||||
#endif
|
||||
@@ -268,7 +273,11 @@ class APIServer : public Component,
|
||||
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
std::vector<std::unique_ptr<APIConnection>> clients_;
|
||||
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
|
||||
// Shared proto write buffer for all connections.
|
||||
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
|
||||
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
|
||||
// since the buffer would almost always reallocate on first use.
|
||||
APIBuffer shared_write_buffer_;
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||
#endif
|
||||
@@ -316,7 +325,10 @@ template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
|
||||
TEMPLATABLE_VALUE(bool, state_subscription_only)
|
||||
public:
|
||||
bool check(const Ts &...x) override {
|
||||
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
|
||||
if (this->state_subscription_only_.value(x...)) {
|
||||
return global_api_server->is_connected_with_state_subscription();
|
||||
}
|
||||
return global_api_server->is_connected();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import importlib
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
import warnings
|
||||
@@ -18,6 +19,7 @@ import contextlib
|
||||
|
||||
from esphome.const import CONF_KEY, CONF_PORT, __version__
|
||||
from esphome.core import CORE
|
||||
from esphome.platformio_api import process_stacktrace
|
||||
|
||||
from . import CONF_ENCRYPTION
|
||||
|
||||
@@ -55,9 +57,19 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
addresses=addresses, # Pass all addresses for automatic retry
|
||||
)
|
||||
dashboard = CORE.dashboard
|
||||
backtrace_state = False
|
||||
|
||||
# Try platform-specific stacktrace handler first, fall back to generic
|
||||
platform_process_stacktrace = None
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
platform_process_stacktrace = getattr(module, "process_stacktrace")
|
||||
except (AttributeError, ImportError):
|
||||
pass
|
||||
|
||||
def on_log(msg: SubscribeLogsResponse) -> None:
|
||||
"""Handle a new log message."""
|
||||
nonlocal backtrace_state
|
||||
time_ = datetime.now()
|
||||
message: bytes = msg.message
|
||||
text = message.decode("utf8", "backslashreplace")
|
||||
@@ -67,6 +79,15 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
)
|
||||
for parsed_msg in parse_log_message(text, timestamp):
|
||||
print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
|
||||
for raw_line in text.splitlines():
|
||||
if platform_process_stacktrace:
|
||||
backtrace_state = platform_process_stacktrace(
|
||||
config, raw_line, backtrace_state
|
||||
)
|
||||
else:
|
||||
backtrace_state = process_stacktrace(
|
||||
config, raw_line, backtrace_state=backtrace_state
|
||||
)
|
||||
|
||||
stop = await async_run(cli, on_log, name=name)
|
||||
try:
|
||||
|
||||
@@ -136,8 +136,9 @@ class CustomAPIDevice {
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
|
||||
auto *obj = static_cast<T *>(this);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
|
||||
[obj, callback](StringRef state) { (obj->*callback)(state); });
|
||||
}
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
|
||||
@@ -148,10 +149,12 @@ class CustomAPIDevice {
|
||||
ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
|
||||
auto *obj = static_cast<T *>(this);
|
||||
// Explicit type to disambiguate overload resolution
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
|
||||
std::function<void(const std::string &)>(f));
|
||||
global_api_server->subscribe_home_assistant_state(
|
||||
entity_id, optional<std::string>(attribute),
|
||||
std::function<void(const std::string &)>(
|
||||
[obj, callback](const std::string &state) { (obj->*callback)(state); }));
|
||||
}
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
|
||||
@@ -176,8 +179,10 @@ class CustomAPIDevice {
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
|
||||
auto *obj = static_cast<T *>(this);
|
||||
global_api_server->subscribe_home_assistant_state(
|
||||
entity_id, optional<std::string>(attribute),
|
||||
[obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); });
|
||||
}
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
|
||||
@@ -188,10 +193,12 @@ class CustomAPIDevice {
|
||||
ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
||||
auto *obj = static_cast<T *>(this);
|
||||
// Explicit type to disambiguate overload resolution
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
|
||||
std::function<void(const std::string &)>(f));
|
||||
global_api_server->subscribe_home_assistant_state(
|
||||
entity_id, optional<std::string>(attribute),
|
||||
std::function<void(const std::string &)>(
|
||||
[obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); }));
|
||||
}
|
||||
#else
|
||||
template<typename T>
|
||||
|
||||
@@ -36,6 +36,8 @@ template<typename... X> class TemplatableStringValue : public TemplatableValue<s
|
||||
static std::string value_to_string(const char *val) { return std::string(val); } // For lambdas returning .c_str()
|
||||
static std::string value_to_string(const std::string &val) { return val; }
|
||||
static std::string value_to_string(std::string &&val) { return std::move(val); }
|
||||
static std::string value_to_string(const StringRef &val) { return val.str(); }
|
||||
static std::string value_to_string(StringRef &&val) { return val.str(); }
|
||||
|
||||
public:
|
||||
TemplatableStringValue() : TemplatableValue<std::string, X...>() {}
|
||||
@@ -128,6 +130,20 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
this->add_kv_(this->variables_, key, std::forward<V>(value));
|
||||
}
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
// On ESP8266, ESPHOME_F() returns __FlashStringHelper* (PROGMEM pointer).
|
||||
// Store as const char* — populate_service_map copies from PROGMEM at play() time.
|
||||
template<typename V> void add_data(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->data_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
template<typename V> void add_data_template(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->data_template_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
template<typename V> void add_variable(const __FlashStringHelper *key, V &&value) {
|
||||
this->add_kv_(this->variables_, reinterpret_cast<const char *>(key), std::forward<V>(value));
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
template<typename T> void set_response_template(T response_template) {
|
||||
this->response_template_ = response_template;
|
||||
@@ -219,7 +235,32 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
Ts... x) {
|
||||
dest.init(source.size());
|
||||
|
||||
// Count non-static strings to allocate exact storage needed
|
||||
#ifdef USE_ESP8266
|
||||
// On ESP8266, all static strings from codegen are FLASH_STRING (PROGMEM),
|
||||
// so is_static_string() is always false — the zero-copy STATIC_STRING fast
|
||||
// path from the non-ESP8266 branch cannot trigger. We copy all keys and
|
||||
// values unconditionally: keys via _P functions (may be in PROGMEM), values
|
||||
// via value() which handles FLASH_STRING internally.
|
||||
value_storage.init(source.size() * 2);
|
||||
|
||||
for (auto &it : source) {
|
||||
auto &kv = dest.emplace_back();
|
||||
|
||||
// Key: copy from possible PROGMEM
|
||||
{
|
||||
size_t key_len = strlen_P(it.key);
|
||||
value_storage.push_back(std::string(key_len, '\0'));
|
||||
memcpy_P(value_storage.back().data(), it.key, key_len);
|
||||
kv.key = StringRef(value_storage.back());
|
||||
}
|
||||
|
||||
// Value: value() handles FLASH_STRING via _P functions internally
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
#else
|
||||
// On non-ESP8266, strings are directly readable from flash-mapped memory.
|
||||
// Count non-static strings to allocate exact storage needed.
|
||||
size_t lambda_count = 0;
|
||||
for (const auto &it : source) {
|
||||
if (!it.value.is_static_string()) {
|
||||
@@ -233,14 +274,15 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
kv.key = StringRef(it.key);
|
||||
|
||||
if (it.value.is_static_string()) {
|
||||
// Static string from YAML - zero allocation
|
||||
// Static string — pointer directly readable, zero allocation
|
||||
kv.value = StringRef(it.value.get_static_string());
|
||||
} else {
|
||||
// Lambda evaluation - store result, reference it
|
||||
// Lambda — evaluate and store result
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
APIServer *parent_;
|
||||
|
||||
@@ -94,7 +94,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);
|
||||
return this->client_->send_message(resp);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ class APIConnection;
|
||||
return this->client_->schedule_message_(entity, ResponseType::MESSAGE_TYPE, ResponseType::ESTIMATED_SIZE); \
|
||||
}
|
||||
|
||||
class ListEntitiesIterator : public ComponentIterator {
|
||||
class ListEntitiesIterator final : public ComponentIterator {
|
||||
public:
|
||||
ListEntitiesIterator(APIConnection *client);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "proto.h"
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -7,24 +8,71 @@ namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.proto";
|
||||
|
||||
uint32_t ProtoSize::varint_slow(uint32_t value) { return varint_wide(value); }
|
||||
|
||||
void ProtoWriteBuffer::encode_varint_raw_slow_(uint32_t value) {
|
||||
do {
|
||||
this->debug_check_bounds_(1);
|
||||
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
|
||||
value >>= 7;
|
||||
} while (value > 0x7F);
|
||||
this->debug_check_bounds_(1);
|
||||
*this->pos_++ = static_cast<uint8_t>(value);
|
||||
}
|
||||
|
||||
ProtoVarIntResult ProtoVarInt::parse_slow(const uint8_t *buffer, uint32_t len) {
|
||||
// Multi-byte varint: first byte already checked to have high bit set
|
||||
uint32_t result32 = buffer[0] & 0x7F;
|
||||
#ifdef USE_API_VARINT64
|
||||
uint32_t limit = std::min(len, uint32_t(4));
|
||||
#else
|
||||
uint32_t limit = std::min(len, uint32_t(5));
|
||||
#endif
|
||||
for (uint32_t i = 1; i < limit; i++) {
|
||||
uint8_t val = buffer[i];
|
||||
result32 |= uint32_t(val & 0x7F) << (i * 7);
|
||||
if ((val & 0x80) == 0) {
|
||||
return {result32, i + 1};
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_VARINT64
|
||||
return parse_wide(buffer, len, result32);
|
||||
#else
|
||||
return {0, PROTO_VARINT_PARSE_FAILED};
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_API_VARINT64
|
||||
ProtoVarIntResult ProtoVarInt::parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) {
|
||||
uint64_t result64 = result32;
|
||||
uint32_t limit = std::min(len, uint32_t(10));
|
||||
for (uint32_t i = 4; i < limit; i++) {
|
||||
uint8_t val = buffer[i];
|
||||
result64 |= uint64_t(val & 0x7F) << (i * 7);
|
||||
if ((val & 0x80) == 0) {
|
||||
return {result64, i + 1};
|
||||
}
|
||||
}
|
||||
return {0, PROTO_VARINT_PARSE_FAILED};
|
||||
}
|
||||
#endif
|
||||
|
||||
uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id) {
|
||||
uint32_t count = 0;
|
||||
const uint8_t *ptr = buffer;
|
||||
const uint8_t *end = buffer + length;
|
||||
|
||||
while (ptr < end) {
|
||||
uint32_t consumed;
|
||||
|
||||
// Parse field header (tag)
|
||||
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||
// Parse field header (tag) - ptr < end guarantees len >= 1
|
||||
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
break; // Invalid data, stop counting
|
||||
}
|
||||
|
||||
uint32_t tag = res->as_uint32();
|
||||
uint32_t tag = static_cast<uint32_t>(res.value);
|
||||
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
||||
uint32_t field_id = tag >> 3;
|
||||
ptr += consumed;
|
||||
ptr += res.consumed;
|
||||
|
||||
// Count if this is the target field
|
||||
if (field_id == target_field_id) {
|
||||
@@ -34,20 +82,20 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
|
||||
// Skip field data based on wire type
|
||||
switch (field_type) {
|
||||
case WIRE_TYPE_VARINT: { // VarInt - parse and skip
|
||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||
res = ProtoVarInt::parse(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
return count; // Invalid data, return what we have
|
||||
}
|
||||
ptr += consumed;
|
||||
ptr += res.consumed;
|
||||
break;
|
||||
}
|
||||
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited - parse length and skip data
|
||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||
res = ProtoVarInt::parse(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
return count;
|
||||
}
|
||||
uint32_t field_length = res->as_uint32();
|
||||
ptr += consumed;
|
||||
uint32_t field_length = static_cast<uint32_t>(res.value);
|
||||
ptr += res.consumed;
|
||||
if (field_length > static_cast<size_t>(end - ptr)) {
|
||||
return count; // Out of bounds
|
||||
}
|
||||
@@ -70,46 +118,130 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
|
||||
return count;
|
||||
}
|
||||
|
||||
// Single-pass encode for repeated submessage elements (non-template core).
|
||||
// Writes field tag, reserves 1 byte for length varint, encodes the submessage body,
|
||||
// then backpatches the actual length. For the common case (body < 128 bytes), this is
|
||||
// just a single byte write with no memmove — all current repeated submessage types
|
||||
// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take
|
||||
// this fast path.
|
||||
//
|
||||
// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT
|
||||
// characteristic with many descriptors). It is safe because calculate_size() already
|
||||
// reserved space for the full multi-byte varint — the shift fills that reserved space:
|
||||
//
|
||||
// calculate_size() allocates per element: tag + varint_size(body) + body_size
|
||||
//
|
||||
// After encode, before memmove (1 byte reserved, body written):
|
||||
// [tag][__][body ..... body][??]
|
||||
// ^ ^-- unused byte (v2 space from calculate_size)
|
||||
// len_pos
|
||||
//
|
||||
// After memmove(body_start+1, body_start, body_size):
|
||||
// [tag][__][__][body ..... body]
|
||||
// ^ ^-- body shifted forward, fills v2 space exactly
|
||||
// len_pos
|
||||
//
|
||||
// After writing 2-byte varint at len_pos:
|
||||
// [tag][v1][v2][body ..... body]
|
||||
// ^-- pos_ = element end, within buffer
|
||||
void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value,
|
||||
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
|
||||
this->encode_field_raw(field_id, 2);
|
||||
// Reserve 1 byte for length varint (optimistic: submessage < 128 bytes)
|
||||
uint8_t *len_pos = this->pos_;
|
||||
this->debug_check_bounds_(1);
|
||||
this->pos_++;
|
||||
uint8_t *body_start = this->pos_;
|
||||
encode_fn(value, *this);
|
||||
uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start);
|
||||
if (body_size < 128) [[likely]] {
|
||||
// Common case: 1-byte varint, just backpatch
|
||||
*len_pos = static_cast<uint8_t>(body_size);
|
||||
return;
|
||||
}
|
||||
// Compute extra bytes needed for varint beyond the 1 already reserved
|
||||
uint8_t extra = ProtoSize::varint(body_size) - 1;
|
||||
// Shift body forward to make room for the extra varint bytes
|
||||
this->debug_check_bounds_(extra);
|
||||
std::memmove(body_start + extra, body_start, body_size);
|
||||
uint8_t *end = this->pos_ + extra;
|
||||
// Write the full varint at len_pos
|
||||
this->pos_ = len_pos;
|
||||
this->encode_varint_raw(body_size);
|
||||
this->pos_ = end;
|
||||
}
|
||||
|
||||
// Non-template core for encode_optional_sub_message.
|
||||
void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
|
||||
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
|
||||
if (nested_size == 0)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 2);
|
||||
this->encode_varint_raw(nested_size);
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
uint8_t *start = this->pos_;
|
||||
encode_fn(value, *this);
|
||||
if (static_cast<uint32_t>(this->pos_ - start) != nested_size)
|
||||
this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start);
|
||||
#else
|
||||
encode_fn(value, *this);
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) {
|
||||
if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) {
|
||||
ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes,
|
||||
this->pos_ - this->buffer_->data(), this->buffer_->size());
|
||||
abort();
|
||||
}
|
||||
}
|
||||
void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual) {
|
||||
ESP_LOGE(TAG, "encode_message: size mismatch for field %" PRIu32 ": calculated=%" PRIu32 " actual=%td", field_id,
|
||||
expected, actual);
|
||||
abort();
|
||||
}
|
||||
#endif
|
||||
|
||||
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
const uint8_t *ptr = buffer;
|
||||
const uint8_t *end = buffer + length;
|
||||
|
||||
while (ptr < end) {
|
||||
uint32_t consumed;
|
||||
|
||||
// Parse field header
|
||||
auto res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||
// Parse field header - ptr < end guarantees len >= 1
|
||||
auto res = ProtoVarInt::parse_non_empty(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
ESP_LOGV(TAG, "Invalid field start at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t tag = res->as_uint32();
|
||||
uint32_t tag = static_cast<uint32_t>(res.value);
|
||||
uint32_t field_type = tag & WIRE_TYPE_MASK;
|
||||
uint32_t field_id = tag >> 3;
|
||||
ptr += consumed;
|
||||
ptr += res.consumed;
|
||||
|
||||
switch (field_type) {
|
||||
case WIRE_TYPE_VARINT: { // VarInt
|
||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||
res = ProtoVarInt::parse(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
ESP_LOGV(TAG, "Invalid VarInt at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
if (!this->decode_varint(field_id, *res)) {
|
||||
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu32 "!", field_id, res->as_uint32());
|
||||
if (!this->decode_varint(field_id, res.value)) {
|
||||
ESP_LOGV(TAG, "Cannot decode VarInt field %" PRIu32 " with value %" PRIu64 "!", field_id,
|
||||
static_cast<uint64_t>(res.value));
|
||||
}
|
||||
ptr += consumed;
|
||||
ptr += res.consumed;
|
||||
break;
|
||||
}
|
||||
case WIRE_TYPE_LENGTH_DELIMITED: { // Length-delimited
|
||||
res = ProtoVarInt::parse(ptr, end - ptr, &consumed);
|
||||
res = ProtoVarInt::parse(ptr, end - ptr);
|
||||
if (!res.has_value()) {
|
||||
ESP_LOGV(TAG, "Invalid Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
}
|
||||
uint32_t field_length = res->as_uint32();
|
||||
ptr += consumed;
|
||||
uint32_t field_length = static_cast<uint32_t>(res.value);
|
||||
ptr += res.consumed;
|
||||
if (field_length > static_cast<size_t>(end - ptr)) {
|
||||
ESP_LOGV(TAG, "Out-of-bounds Length Delimited at offset %ld", (long) (ptr - buffer));
|
||||
return;
|
||||
|
||||
+207
-449
@@ -1,5 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "api_pb2_defines.h"
|
||||
#include "api_buffer.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -97,75 +99,60 @@ inline void encode_varint_to_buffer(uint32_t val, uint8_t *buffer) {
|
||||
* within the same function scope where temporaries are created.
|
||||
*/
|
||||
|
||||
/// Representation of a VarInt - in ProtoBuf should be 64bit but we only use 32bit
|
||||
/// Type used for decoded varint values - uint64_t when BLE needs 64-bit addresses, uint32_t otherwise
|
||||
#ifdef USE_API_VARINT64
|
||||
using proto_varint_value_t = uint64_t;
|
||||
#else
|
||||
using proto_varint_value_t = uint32_t;
|
||||
#endif
|
||||
|
||||
/// Sentinel value for consumed field indicating parse failure
|
||||
inline constexpr uint32_t PROTO_VARINT_PARSE_FAILED = 0;
|
||||
|
||||
/// Result of parsing a varint: value + number of bytes consumed.
|
||||
/// consumed == PROTO_VARINT_PARSE_FAILED indicates parse failure (not enough data or invalid).
|
||||
struct ProtoVarIntResult {
|
||||
proto_varint_value_t value;
|
||||
uint32_t consumed; // PROTO_VARINT_PARSE_FAILED = parse failed
|
||||
|
||||
constexpr bool has_value() const { return this->consumed != PROTO_VARINT_PARSE_FAILED; }
|
||||
};
|
||||
|
||||
/// Static varint parsing methods for the protobuf wire format.
|
||||
class ProtoVarInt {
|
||||
public:
|
||||
ProtoVarInt() : value_(0) {}
|
||||
explicit ProtoVarInt(uint64_t value) : value_(value) {}
|
||||
|
||||
/// Parse a varint from buffer. consumed must be a valid pointer (not null).
|
||||
static optional<ProtoVarInt> parse(const uint8_t *buffer, uint32_t len, uint32_t *consumed) {
|
||||
/// Parse a varint from buffer. Caller must ensure len >= 1.
|
||||
/// Returns result with consumed=0 on failure (truncated multi-byte varint).
|
||||
static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse_non_empty(const uint8_t *buffer, uint32_t len) {
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(consumed != nullptr);
|
||||
assert(len > 0);
|
||||
#endif
|
||||
// Fast path: single-byte varints (0-127) are the most common case
|
||||
// (booleans, small enums, field tags, small message sizes/types).
|
||||
if ((buffer[0] & 0x80) == 0) [[likely]]
|
||||
return {buffer[0], 1};
|
||||
return parse_slow(buffer, len);
|
||||
}
|
||||
|
||||
/// Parse a varint from buffer (safe for empty buffers).
|
||||
/// Returns result with consumed=0 on failure (empty buffer or truncated varint).
|
||||
static inline ProtoVarIntResult ESPHOME_ALWAYS_INLINE parse(const uint8_t *buffer, uint32_t len) {
|
||||
if (len == 0)
|
||||
return {};
|
||||
|
||||
// Most common case: single-byte varint (values 0-127)
|
||||
if ((buffer[0] & 0x80) == 0) {
|
||||
*consumed = 1;
|
||||
return ProtoVarInt(buffer[0]);
|
||||
}
|
||||
|
||||
// General case for multi-byte varints
|
||||
// Since we know buffer[0]'s high bit is set, initialize with its value
|
||||
uint64_t result = buffer[0] & 0x7F;
|
||||
uint8_t bitpos = 7;
|
||||
|
||||
// A 64-bit varint is at most 10 bytes (ceil(64/7)). Reject overlong encodings
|
||||
// to avoid undefined behavior from shifting uint64_t by >= 64 bits.
|
||||
uint32_t max_len = std::min(len, uint32_t(10));
|
||||
|
||||
// Start from the second byte since we've already processed the first
|
||||
for (uint32_t i = 1; i < max_len; i++) {
|
||||
uint8_t val = buffer[i];
|
||||
result |= uint64_t(val & 0x7F) << uint64_t(bitpos);
|
||||
bitpos += 7;
|
||||
if ((val & 0x80) == 0) {
|
||||
*consumed = i + 1;
|
||||
return ProtoVarInt(result);
|
||||
}
|
||||
}
|
||||
|
||||
return {}; // Incomplete or invalid varint
|
||||
}
|
||||
|
||||
constexpr uint16_t as_uint16() const { return this->value_; }
|
||||
constexpr uint32_t as_uint32() const { return this->value_; }
|
||||
constexpr uint64_t as_uint64() const { return this->value_; }
|
||||
constexpr bool as_bool() const { return this->value_; }
|
||||
constexpr int32_t as_int32() const {
|
||||
// Not ZigZag encoded
|
||||
return static_cast<int32_t>(this->as_int64());
|
||||
}
|
||||
constexpr int64_t as_int64() const {
|
||||
// Not ZigZag encoded
|
||||
return static_cast<int64_t>(this->value_);
|
||||
}
|
||||
constexpr int32_t as_sint32() const {
|
||||
// with ZigZag encoding
|
||||
return decode_zigzag32(static_cast<uint32_t>(this->value_));
|
||||
}
|
||||
constexpr int64_t as_sint64() const {
|
||||
// with ZigZag encoding
|
||||
return decode_zigzag64(this->value_);
|
||||
return {0, PROTO_VARINT_PARSE_FAILED};
|
||||
return parse_non_empty(buffer, len);
|
||||
}
|
||||
|
||||
protected:
|
||||
uint64_t value_;
|
||||
// Slow path for multi-byte varints (>= 128), outlined to keep fast path small
|
||||
static ProtoVarIntResult parse_slow(const uint8_t *buffer, uint32_t len) __attribute__((noinline));
|
||||
|
||||
#ifdef USE_API_VARINT64
|
||||
/// Continue parsing varint bytes 4-9 with 64-bit arithmetic.
|
||||
static ProtoVarIntResult parse_wide(const uint8_t *buffer, uint32_t len, uint32_t result32) __attribute__((noinline));
|
||||
#endif
|
||||
};
|
||||
|
||||
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
|
||||
// Forward declarations for decode_to_message and related encoding helpers
|
||||
class ProtoDecodableMessage;
|
||||
class ProtoMessage;
|
||||
class ProtoSize;
|
||||
@@ -217,21 +204,24 @@ class Proto32Bit {
|
||||
|
||||
class ProtoWriteBuffer {
|
||||
public:
|
||||
ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
|
||||
void write(uint8_t value) { this->buffer_->push_back(value); }
|
||||
void encode_varint_raw(uint32_t value) {
|
||||
while (value > 0x7F) {
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
|
||||
value >>= 7;
|
||||
ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
|
||||
ProtoWriteBuffer(APIBuffer *buffer, size_t write_pos) : buffer_(buffer), pos_(buffer->data() + write_pos) {}
|
||||
inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint32_t value) {
|
||||
if (value < 128) [[likely]] {
|
||||
this->debug_check_bounds_(1);
|
||||
*this->pos_++ = static_cast<uint8_t>(value);
|
||||
return;
|
||||
}
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||
this->encode_varint_raw_slow_(value);
|
||||
}
|
||||
void encode_varint_raw_64(uint64_t value) {
|
||||
while (value > 0x7F) {
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value | 0x80));
|
||||
this->debug_check_bounds_(1);
|
||||
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
|
||||
value >>= 7;
|
||||
}
|
||||
this->buffer_->push_back(static_cast<uint8_t>(value));
|
||||
this->debug_check_bounds_(1);
|
||||
*this->pos_++ = static_cast<uint8_t>(value);
|
||||
}
|
||||
/**
|
||||
* Encode a field key (tag/wire type combination).
|
||||
@@ -245,23 +235,18 @@ class ProtoWriteBuffer {
|
||||
*
|
||||
* Following https://protobuf.dev/programming-guides/encoding/#structure
|
||||
*/
|
||||
void encode_field_raw(uint32_t field_id, uint32_t type) {
|
||||
uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
|
||||
this->encode_varint_raw(val);
|
||||
}
|
||||
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); }
|
||||
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
|
||||
if (len == 0 && !force)
|
||||
return;
|
||||
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
|
||||
this->encode_varint_raw(len);
|
||||
|
||||
// Using resize + memcpy instead of insert provides significant performance improvement:
|
||||
// ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings
|
||||
// as it avoids iterator checks and potential element moves that insert performs
|
||||
size_t old_size = this->buffer_->size();
|
||||
this->buffer_->resize(old_size + len);
|
||||
std::memcpy(this->buffer_->data() + old_size, string, len);
|
||||
// Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks
|
||||
// and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings.
|
||||
this->debug_check_bounds_(len);
|
||||
std::memcpy(this->pos_, string, len);
|
||||
this->pos_ += len;
|
||||
}
|
||||
void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
|
||||
this->encode_string(field_id, value.data(), value.size(), force);
|
||||
@@ -288,17 +273,26 @@ class ProtoWriteBuffer {
|
||||
if (!value && !force)
|
||||
return;
|
||||
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
|
||||
this->buffer_->push_back(value ? 0x01 : 0x00);
|
||||
this->debug_check_bounds_(1);
|
||||
*this->pos_++ = value ? 0x01 : 0x00;
|
||||
}
|
||||
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
|
||||
// noinline: 51 call sites; inlining causes net code growth vs a single out-of-line copy
|
||||
__attribute__((noinline)) void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
|
||||
if (value == 0 && !force)
|
||||
return;
|
||||
|
||||
this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32
|
||||
this->write((value >> 0) & 0xFF);
|
||||
this->write((value >> 8) & 0xFF);
|
||||
this->write((value >> 16) & 0xFF);
|
||||
this->write((value >> 24) & 0xFF);
|
||||
this->debug_check_bounds_(4);
|
||||
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
|
||||
// Protobuf fixed32 is little-endian, so direct copy works
|
||||
std::memcpy(this->pos_, &value, 4);
|
||||
this->pos_ += 4;
|
||||
#else
|
||||
*this->pos_++ = (value >> 0) & 0xFF;
|
||||
*this->pos_++ = (value >> 8) & 0xFF;
|
||||
*this->pos_++ = (value >> 16) & 0xFF;
|
||||
*this->pos_++ = (value >> 24) & 0xFF;
|
||||
#endif
|
||||
}
|
||||
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
|
||||
// not supported to reduce overhead on embedded systems. All ESPHome devices are
|
||||
@@ -334,11 +328,33 @@ class ProtoWriteBuffer {
|
||||
}
|
||||
/// Encode a packed repeated sint32 field (zero-copy from vector)
|
||||
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
|
||||
void encode_message(uint32_t field_id, const ProtoMessage &value);
|
||||
std::vector<uint8_t> *get_buffer() const { return buffer_; }
|
||||
/// Single-pass encode for repeated submessage elements.
|
||||
/// Thin template wrapper; all buffer work is in the non-template core.
|
||||
template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
|
||||
/// Encode an optional singular submessage field — skips if empty.
|
||||
/// Thin template wrapper; all buffer work is in the non-template core.
|
||||
template<typename T> void encode_optional_sub_message(uint32_t field_id, const T &value);
|
||||
|
||||
// Non-template core for encode_sub_message — backpatch approach.
|
||||
void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &));
|
||||
// Non-template core for encode_optional_sub_message.
|
||||
void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
|
||||
void (*encode_fn)(const void *, ProtoWriteBuffer &));
|
||||
APIBuffer *get_buffer() const { return buffer_; }
|
||||
|
||||
protected:
|
||||
std::vector<uint8_t> *buffer_;
|
||||
// Slow path for encode_varint_raw values >= 128, outlined to keep fast path small
|
||||
void encode_varint_raw_slow_(uint32_t value) __attribute__((noinline));
|
||||
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
void debug_check_bounds_(size_t bytes, const char *caller = __builtin_FUNCTION());
|
||||
void debug_check_encode_size_(uint32_t field_id, uint32_t expected, ptrdiff_t actual);
|
||||
#else
|
||||
void debug_check_bounds_([[maybe_unused]] size_t bytes) {}
|
||||
#endif
|
||||
|
||||
APIBuffer *buffer_;
|
||||
uint8_t *pos_;
|
||||
};
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -414,15 +430,25 @@ class DumpBuffer {
|
||||
|
||||
class ProtoMessage {
|
||||
public:
|
||||
virtual ~ProtoMessage() = default;
|
||||
// Default implementation for messages with no fields
|
||||
virtual void encode(ProtoWriteBuffer buffer) const {}
|
||||
// Default implementation for messages with no fields
|
||||
virtual void calculate_size(ProtoSize &size) const {}
|
||||
// Non-virtual defaults for messages with no fields.
|
||||
// Concrete message classes hide these with their own implementations.
|
||||
// All call sites use templates to preserve the concrete type, so virtual
|
||||
// dispatch is not needed. This eliminates per-message vtable entries for
|
||||
// encode/calculate_size, saving ~1.3 KB of flash across all message types.
|
||||
void encode(ProtoWriteBuffer &buffer) const {}
|
||||
uint32_t calculate_size() const { return 0; }
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
virtual const char *dump_to(DumpBuffer &out) const = 0;
|
||||
virtual const char *message_name() const { return "unknown"; }
|
||||
#endif
|
||||
|
||||
#ifndef USE_HOST
|
||||
protected:
|
||||
#endif
|
||||
// Non-virtual destructor is protected to prevent polymorphic deletion.
|
||||
// On host platform, made public to allow value-initialization of std::array
|
||||
// members (e.g. DeviceInfoResponse::devices) without clang errors.
|
||||
~ProtoMessage() = default;
|
||||
};
|
||||
|
||||
// Base class for messages that support decoding
|
||||
@@ -442,38 +468,20 @@ class ProtoDecodableMessage : public ProtoMessage {
|
||||
static uint32_t count_repeated_field(const uint8_t *buffer, size_t length, uint32_t target_field_id);
|
||||
|
||||
protected:
|
||||
virtual bool decode_varint(uint32_t field_id, ProtoVarInt value) { return false; }
|
||||
~ProtoDecodableMessage() = default;
|
||||
virtual bool decode_varint(uint32_t field_id, proto_varint_value_t value) { return false; }
|
||||
virtual bool decode_length(uint32_t field_id, ProtoLengthDelimited value) { return false; }
|
||||
virtual bool decode_32bit(uint32_t field_id, Proto32Bit value) { return false; }
|
||||
// NOTE: decode_64bit removed - wire type 1 not supported
|
||||
};
|
||||
|
||||
class ProtoSize {
|
||||
private:
|
||||
uint32_t total_size_ = 0;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief ProtoSize class for Protocol Buffer serialization size calculation
|
||||
*
|
||||
* This class provides methods to calculate the exact byte counts needed
|
||||
* for encoding various Protocol Buffer field types. The class now uses an
|
||||
* object-based approach to reduce parameter passing overhead while keeping
|
||||
* varint calculation methods static for external use.
|
||||
*
|
||||
* Implements Protocol Buffer encoding size calculation according to:
|
||||
* https://protobuf.dev/programming-guides/encoding/
|
||||
*
|
||||
* Key features:
|
||||
* - Object-based approach reduces flash usage by eliminating parameter passing
|
||||
* - Early-return optimization for zero/default values
|
||||
* - Static varint methods for external callers
|
||||
* - Specialized handling for different field types according to protobuf spec
|
||||
*/
|
||||
|
||||
ProtoSize() = default;
|
||||
|
||||
uint32_t get_size() const { return total_size_; }
|
||||
// Varint encoding thresholds: values below each threshold fit in N bytes
|
||||
static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128
|
||||
static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384
|
||||
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
|
||||
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
@@ -481,24 +489,29 @@ class ProtoSize {
|
||||
* @param value The uint32_t value to calculate size for
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static constexpr uint32_t varint(uint32_t value) {
|
||||
// Optimized varint size calculation using leading zeros
|
||||
// Each 7 bits requires one byte in the varint encoding
|
||||
if (value < 128)
|
||||
return 1; // 7 bits, common case for small values
|
||||
|
||||
// For larger values, count bytes needed based on the position of the highest bit set
|
||||
if (value < 16384) {
|
||||
return 2; // 14 bits
|
||||
} else if (value < 2097152) {
|
||||
return 3; // 21 bits
|
||||
} else if (value < 268435456) {
|
||||
return 4; // 28 bits
|
||||
} else {
|
||||
return 5; // 32 bits (maximum for uint32_t)
|
||||
}
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) {
|
||||
if (value < VARINT_THRESHOLD_1_BYTE) [[likely]]
|
||||
return 1; // Fast path: 7 bits, most common case
|
||||
if (__builtin_is_constant_evaluated())
|
||||
return varint_wide(value);
|
||||
return varint_slow(value);
|
||||
}
|
||||
|
||||
private:
|
||||
// Slow path for varint >= 128, outlined to keep fast path small
|
||||
static uint32_t varint_slow(uint32_t value) __attribute__((noinline));
|
||||
// Shared cascade for values >= 128 (used by both constexpr and noinline paths)
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) {
|
||||
if (value < VARINT_THRESHOLD_2_BYTE)
|
||||
return 2;
|
||||
if (value < VARINT_THRESHOLD_3_BYTE)
|
||||
return 3;
|
||||
if (value < VARINT_THRESHOLD_4_BYTE)
|
||||
return 4;
|
||||
return 5;
|
||||
}
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint64_t value as a varint
|
||||
*
|
||||
@@ -571,312 +584,78 @@ class ProtoSize {
|
||||
return varint(tag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Common parameters for all add_*_field methods
|
||||
*
|
||||
* All add_*_field methods follow these common patterns:
|
||||
* * @param field_id_size Pre-calculated size of the field ID in bytes
|
||||
* @param value The value to calculate size for (type varies)
|
||||
* @param force Whether to calculate size even if the value is default/zero/empty
|
||||
*
|
||||
* Each method follows this implementation pattern:
|
||||
* 1. Skip calculation if value is default (0, false, empty) and not forced
|
||||
* 2. Calculate the size based on the field's encoding rules
|
||||
* 3. Add the field_id_size + calculated value size to total_size
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int32 field to the total message size
|
||||
*/
|
||||
inline void add_int32(uint32_t field_id_size, int32_t value) {
|
||||
if (value != 0) {
|
||||
add_int32_force(field_id_size, value);
|
||||
}
|
||||
// Static methods that RETURN size contribution (no ProtoSize object needed).
|
||||
// Used by generated calculate_size() methods to accumulate into a plain uint32_t register.
|
||||
static constexpr uint32_t calc_int32(uint32_t field_id_size, int32_t value) {
|
||||
return value ? field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value))) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int32 field to the total message size (force version)
|
||||
*/
|
||||
inline void add_int32_force(uint32_t field_id_size, int32_t value) {
|
||||
// Always calculate size when forced
|
||||
// Negative values are encoded as 10-byte varints in protobuf
|
||||
total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value)));
|
||||
static constexpr uint32_t calc_int32_force(uint32_t field_id_size, int32_t value) {
|
||||
return field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint32 field to the total message size
|
||||
*/
|
||||
inline void add_uint32(uint32_t field_id_size, uint32_t value) {
|
||||
if (value != 0) {
|
||||
add_uint32_force(field_id_size, value);
|
||||
}
|
||||
static constexpr uint32_t calc_uint32(uint32_t field_id_size, uint32_t value) {
|
||||
return value ? field_id_size + varint(value) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint32 field to the total message size (force version)
|
||||
*/
|
||||
inline void add_uint32_force(uint32_t field_id_size, uint32_t value) {
|
||||
// Always calculate size when force is true
|
||||
total_size_ += field_id_size + varint(value);
|
||||
static constexpr uint32_t calc_uint32_force(uint32_t field_id_size, uint32_t value) {
|
||||
return field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a boolean field to the total message size
|
||||
*/
|
||||
inline void add_bool(uint32_t field_id_size, bool value) {
|
||||
if (value) {
|
||||
// Boolean fields always use 1 byte when true
|
||||
total_size_ += field_id_size + 1;
|
||||
}
|
||||
static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; }
|
||||
static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; }
|
||||
static constexpr uint32_t calc_float(uint32_t field_id_size, float value) {
|
||||
return value != 0.0f ? field_id_size + 4 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a boolean field to the total message size (force version)
|
||||
*/
|
||||
inline void add_bool_force(uint32_t field_id_size, bool value) {
|
||||
// Always calculate size when force is true
|
||||
// Boolean fields always use 1 byte
|
||||
total_size_ += field_id_size + 1;
|
||||
static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) {
|
||||
return value ? field_id_size + 4 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a float field to the total message size
|
||||
*/
|
||||
inline void add_float(uint32_t field_id_size, float value) {
|
||||
if (value != 0.0f) {
|
||||
total_size_ += field_id_size + 4;
|
||||
}
|
||||
static constexpr uint32_t calc_sfixed32(uint32_t field_id_size, int32_t value) {
|
||||
return value ? field_id_size + 4 : 0;
|
||||
}
|
||||
|
||||
// NOTE: add_double_field removed - wire type 1 (64-bit: double) not supported
|
||||
// to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a fixed32 field to the total message size
|
||||
*/
|
||||
inline void add_fixed32(uint32_t field_id_size, uint32_t value) {
|
||||
if (value != 0) {
|
||||
total_size_ += field_id_size + 4;
|
||||
}
|
||||
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
|
||||
return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
|
||||
}
|
||||
|
||||
// NOTE: add_fixed64_field removed - wire type 1 (64-bit: fixed64) not supported
|
||||
// to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sfixed32 field to the total message size
|
||||
*/
|
||||
inline void add_sfixed32(uint32_t field_id_size, int32_t value) {
|
||||
if (value != 0) {
|
||||
total_size_ += field_id_size + 4;
|
||||
}
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) {
|
||||
return field_id_size + varint(encode_zigzag32(value));
|
||||
}
|
||||
|
||||
// NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported
|
||||
// to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sint32 field to the total message size
|
||||
*
|
||||
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
|
||||
*/
|
||||
inline void add_sint32(uint32_t field_id_size, int32_t value) {
|
||||
if (value != 0) {
|
||||
add_sint32_force(field_id_size, value);
|
||||
}
|
||||
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
|
||||
return value ? field_id_size + varint(value) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sint32 field to the total message size (force version)
|
||||
*
|
||||
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
|
||||
*/
|
||||
inline void add_sint32_force(uint32_t field_id_size, int32_t value) {
|
||||
// Always calculate size when force is true
|
||||
// ZigZag encoding for sint32
|
||||
total_size_ += field_id_size + varint(encode_zigzag32(value));
|
||||
static constexpr uint32_t calc_int64_force(uint32_t field_id_size, int64_t value) {
|
||||
return field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int64 field to the total message size
|
||||
*/
|
||||
inline void add_int64(uint32_t field_id_size, int64_t value) {
|
||||
if (value != 0) {
|
||||
add_int64_force(field_id_size, value);
|
||||
}
|
||||
static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) {
|
||||
return value ? field_id_size + varint(value) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int64 field to the total message size (force version)
|
||||
*/
|
||||
inline void add_int64_force(uint32_t field_id_size, int64_t value) {
|
||||
// Always calculate size when force is true
|
||||
total_size_ += field_id_size + varint(value);
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
|
||||
return field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint64 field to the total message size
|
||||
*/
|
||||
inline void add_uint64(uint32_t field_id_size, uint64_t value) {
|
||||
if (value != 0) {
|
||||
add_uint64_force(field_id_size, value);
|
||||
}
|
||||
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
|
||||
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint64 field to the total message size (force version)
|
||||
*/
|
||||
inline void add_uint64_force(uint32_t field_id_size, uint64_t value) {
|
||||
// Always calculate size when force is true
|
||||
total_size_ += field_id_size + varint(value);
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_length_force(uint32_t field_id_size, size_t len) {
|
||||
return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
|
||||
}
|
||||
|
||||
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed
|
||||
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size
|
||||
*/
|
||||
inline void add_length(uint32_t field_id_size, size_t len) {
|
||||
if (len != 0) {
|
||||
add_length_force(field_id_size, len);
|
||||
}
|
||||
static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) {
|
||||
return value ? field_id_size + varint(encode_zigzag64(value)) : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated
|
||||
* field version)
|
||||
*/
|
||||
inline void add_length_force(uint32_t field_id_size, size_t len) {
|
||||
// Always calculate size when force is true
|
||||
// Field ID + length varint + data bytes
|
||||
total_size_ += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
|
||||
static constexpr uint32_t calc_sint64_force(uint32_t field_id_size, int64_t value) {
|
||||
return field_id_size + varint(encode_zigzag64(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Adds a pre-calculated size directly to the total
|
||||
*
|
||||
* This is used when we can calculate the total size by multiplying the number
|
||||
* of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.)
|
||||
*
|
||||
* @param size The pre-calculated total size to add
|
||||
*/
|
||||
inline void add_precalculated_size(uint32_t size) { total_size_ += size; }
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size
|
||||
*
|
||||
* This helper function directly updates the total_size reference if the nested size
|
||||
* is greater than zero.
|
||||
*
|
||||
* @param nested_size The pre-calculated size of the nested message
|
||||
*/
|
||||
inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) {
|
||||
if (nested_size != 0) {
|
||||
add_message_field_force(field_id_size, nested_size);
|
||||
}
|
||||
static constexpr uint32_t calc_fixed64(uint32_t field_id_size, uint64_t value) {
|
||||
return value ? field_id_size + 8 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
|
||||
*
|
||||
* @param nested_size The pre-calculated size of the nested message
|
||||
*/
|
||||
inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) {
|
||||
// Always calculate size when force is true
|
||||
// Field ID + length varint + nested message content
|
||||
total_size_ += field_id_size + varint(nested_size) + nested_size;
|
||||
static constexpr uint32_t calc_sfixed64(uint32_t field_id_size, int64_t value) {
|
||||
return value ? field_id_size + 8 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size
|
||||
*
|
||||
* This version takes a ProtoMessage object, calculates its size internally,
|
||||
* and updates the total_size reference. This eliminates the need for a temporary variable
|
||||
* at the call site.
|
||||
*
|
||||
* @param message The nested message object
|
||||
*/
|
||||
inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) {
|
||||
// Calculate nested message size by creating a temporary ProtoSize
|
||||
ProtoSize nested_calc;
|
||||
message.calculate_size(nested_calc);
|
||||
uint32_t nested_size = nested_calc.get_size();
|
||||
|
||||
// Use the base implementation with the calculated nested_size
|
||||
add_message_field(field_id_size, nested_size);
|
||||
static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) {
|
||||
return nested_size ? field_id_size + varint(nested_size) + nested_size : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
|
||||
*
|
||||
* @param message The nested message object
|
||||
*/
|
||||
inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) {
|
||||
// Calculate nested message size by creating a temporary ProtoSize
|
||||
ProtoSize nested_calc;
|
||||
message.calculate_size(nested_calc);
|
||||
uint32_t nested_size = nested_calc.get_size();
|
||||
|
||||
// Use the base implementation with the calculated nested_size
|
||||
add_message_field_force(field_id_size, nested_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size
|
||||
*
|
||||
* This helper processes a vector of message objects, calculating the size for each message
|
||||
* and adding it to the total size.
|
||||
*
|
||||
* @tparam MessageType The type of the nested messages in the vector
|
||||
* @param messages Vector of message objects
|
||||
*/
|
||||
template<typename MessageType>
|
||||
inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
|
||||
// Skip if the vector is empty
|
||||
if (!messages.empty()) {
|
||||
// Use the force version for all messages in the repeated field
|
||||
for (const auto &message : messages) {
|
||||
add_message_object_force(field_id_size, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the sizes of all messages in a repeated field to the total message size (FixedVector
|
||||
* version)
|
||||
*
|
||||
* @tparam MessageType The type of the nested messages in the FixedVector
|
||||
* @param messages FixedVector of message objects
|
||||
*/
|
||||
template<typename MessageType>
|
||||
inline void add_repeated_message(uint32_t field_id_size, const FixedVector<MessageType> &messages) {
|
||||
// Skip if the fixed vector is empty
|
||||
if (!messages.empty()) {
|
||||
// Use the force version for all messages in the repeated field
|
||||
for (const auto &message : messages) {
|
||||
add_message_object_force(field_id_size, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculate size of a packed repeated sint32 field
|
||||
*/
|
||||
inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) {
|
||||
if (values.empty())
|
||||
return;
|
||||
|
||||
size_t packed_size = 0;
|
||||
for (int value : values) {
|
||||
packed_size += varint(encode_zigzag32(value));
|
||||
}
|
||||
|
||||
// field_id + length varint + packed data
|
||||
total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size);
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_message_force(uint32_t field_id_size,
|
||||
uint32_t nested_size) {
|
||||
return field_id_size + varint(nested_size) + nested_size;
|
||||
}
|
||||
};
|
||||
|
||||
// Implementation of methods that depend on ProtoSize being fully defined
|
||||
|
||||
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined
|
||||
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
|
||||
if (values.empty())
|
||||
@@ -896,32 +675,19 @@ inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std:
|
||||
}
|
||||
}
|
||||
|
||||
// Implementation of encode_message - must be after ProtoMessage is defined
|
||||
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
|
||||
// Encode thunk — converts void* back to concrete type for direct encode() call
|
||||
template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) {
|
||||
static_cast<const T *>(msg)->encode(buf);
|
||||
}
|
||||
|
||||
// Calculate the message size first
|
||||
ProtoSize msg_size;
|
||||
value.calculate_size(msg_size);
|
||||
uint32_t msg_length_bytes = msg_size.get_size();
|
||||
// Thin template wrapper; delegates to non-template core in proto.cpp.
|
||||
template<typename T> inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) {
|
||||
this->encode_sub_message(field_id, &value, &proto_encode_msg<T>);
|
||||
}
|
||||
|
||||
// Calculate how many bytes the length varint needs
|
||||
uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes);
|
||||
|
||||
// Reserve exact space for the length varint
|
||||
size_t begin = this->buffer_->size();
|
||||
this->buffer_->resize(this->buffer_->size() + varint_length_bytes);
|
||||
|
||||
// Write the length varint directly
|
||||
encode_varint_to_buffer(msg_length_bytes, this->buffer_->data() + begin);
|
||||
|
||||
// Now encode the message content - it will append to the buffer
|
||||
value.encode(*this);
|
||||
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
// Verify that the encoded size matches what we calculated
|
||||
assert(this->buffer_->size() == begin + varint_length_bytes + msg_length_bytes);
|
||||
#endif
|
||||
// Thin template wrapper; delegates to non-template core.
|
||||
template<typename T> inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) {
|
||||
this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg<T>);
|
||||
}
|
||||
|
||||
// Implementation of decode_to_message - must be after ProtoDecodableMessage is defined
|
||||
@@ -940,14 +706,6 @@ class ProtoService {
|
||||
virtual void on_no_setup_connection() = 0;
|
||||
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
|
||||
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0;
|
||||
/**
|
||||
* Send a protobuf message by calculating its size, allocating a buffer, encoding, and sending.
|
||||
* This is the implementation method - callers should use send_message() which adds logging.
|
||||
* @param msg The protobuf message to send.
|
||||
* @param message_type The message type identifier.
|
||||
* @return True if the message was sent successfully, false otherwise.
|
||||
*/
|
||||
virtual bool send_message_impl(const ProtoMessage &msg, uint8_t message_type) = 0;
|
||||
|
||||
// Authentication helper methods
|
||||
inline bool check_connection_setup_() {
|
||||
|
||||
@@ -16,7 +16,7 @@ class APIConnection;
|
||||
return this->client_->send_##entity_type##_state(entity); \
|
||||
}
|
||||
|
||||
class InitialStateIterator : public ComponentIterator {
|
||||
class InitialStateIterator final : public ComponentIterator {
|
||||
public:
|
||||
InitialStateIterator(APIConnection *client);
|
||||
#ifdef USE_BINARY_SENSOR
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "user_services.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -11,6 +12,8 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
|
||||
}
|
||||
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
|
||||
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
||||
// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode)
|
||||
template<> StringRef get_execute_arg_value<StringRef>(const ExecuteServiceArgument &arg) { return arg.string_; }
|
||||
|
||||
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
|
||||
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
|
||||
@@ -61,6 +64,8 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER
|
||||
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
|
||||
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
|
||||
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
||||
// Zero-copy StringRef version for YAML-generated services
|
||||
template<> enums::ServiceArgType to_service_arg_type<StringRef>() { return enums::SERVICE_ARG_TYPE_STRING; }
|
||||
|
||||
// Legacy std::vector versions for external components using custom_api_device.h
|
||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
|
||||
|
||||
@@ -230,7 +230,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; }
|
||||
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void set_data(std::function<void(Ts..., JsonObject)> func) {
|
||||
void set_data(std::function<void(Ts..., JsonObject)> &&func) {
|
||||
this->json_builder_ = std::move(func);
|
||||
this->has_data_ = true;
|
||||
}
|
||||
@@ -264,9 +264,9 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
// Build and send JSON response
|
||||
json::JsonBuilder builder;
|
||||
this->json_builder_(x..., builder.root());
|
||||
std::string json_str = builder.serialize();
|
||||
auto json_buf = builder.serialize();
|
||||
this->parent_->send_action_response(call_id, success, StringRef(error_message),
|
||||
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
|
||||
reinterpret_cast<const uint8_t *>(json_buf.data()), json_buf.size());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -307,9 +307,9 @@ void AS3935Component::tune_antenna() {
|
||||
uint8_t tune_val = this->read_capacitance();
|
||||
ESP_LOGI(TAG,
|
||||
"Starting antenna tuning\n"
|
||||
"Division Ratio is set to: %d\n"
|
||||
"Internal Capacitor is set to: %d\n"
|
||||
"Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio",
|
||||
" Division Ratio is set to: %d\n"
|
||||
" Internal Capacitor is set to: %d\n"
|
||||
" Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio",
|
||||
div_ratio, tune_val);
|
||||
this->display_oscillator(true, ANTFREQ);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ enum AS3935RegisterMasks {
|
||||
INT_MASK = 0xF0,
|
||||
THRESH_MASK = 0x0F,
|
||||
R_SPIKE_MASK = 0xF0,
|
||||
ENERGY_MASK = 0xF0,
|
||||
ENERGY_MASK = 0xE0,
|
||||
CAP_MASK = 0xF0,
|
||||
LIGHT_MASK = 0xCF,
|
||||
DISTURB_MASK = 0xDF,
|
||||
|
||||
@@ -9,6 +9,7 @@ from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_POWER_MODE,
|
||||
CONF_RANGE,
|
||||
CONF_WATCHDOG,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@ammmze"]
|
||||
@@ -57,7 +58,6 @@ FAST_FILTER = {
|
||||
|
||||
CONF_RAW_ANGLE = "raw_angle"
|
||||
CONF_RAW_POSITION = "raw_position"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
CONF_START_POSITION = "start_position"
|
||||
|
||||
@@ -23,7 +23,6 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone
|
||||
|
||||
CONF_RAW_ANGLE = "raw_angle"
|
||||
CONF_RAW_POSITION = "raw_position"
|
||||
CONF_WATCHDOG = "watchdog"
|
||||
CONF_SLOW_FILTER = "slow_filter"
|
||||
CONF_FAST_FILTER = "fast_filter"
|
||||
CONF_PWM_FREQUENCY = "pwm_frequency"
|
||||
|
||||
@@ -52,11 +52,12 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
|
||||
connect_cb_(connect_arg_, this);
|
||||
return true;
|
||||
}
|
||||
if (errno != EINPROGRESS) {
|
||||
ESP_LOGE(TAG, "Connect failed: %d", errno);
|
||||
const int saved_errno = errno;
|
||||
if (saved_errno != EINPROGRESS) {
|
||||
ESP_LOGE(TAG, "Connect failed: %d", saved_errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
error_cb_(error_arg_, this, saved_errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -79,11 +80,12 @@ size_t AsyncClient::write(const char *data, size_t len) {
|
||||
|
||||
ssize_t sent = socket_->write(data, len);
|
||||
if (sent < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
ESP_LOGE(TAG, "Write error: %d", errno);
|
||||
const int err = errno;
|
||||
if (err != EAGAIN && err != EWOULDBLOCK) {
|
||||
ESP_LOGE(TAG, "Write error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
error_cb_(error_arg_, this, err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -129,10 +131,11 @@ void AsyncClient::loop() {
|
||||
error_cb_(error_arg_, this, error);
|
||||
}
|
||||
} else if (ret < 0) {
|
||||
ESP_LOGE(TAG, "Select error: %d", errno);
|
||||
const int err = errno;
|
||||
ESP_LOGE(TAG, "Select error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
error_cb_(error_arg_, this, err);
|
||||
}
|
||||
} else if (connected_) {
|
||||
// For connected sockets, use the Application's select() results
|
||||
@@ -148,11 +151,14 @@ void AsyncClient::loop() {
|
||||
} else if (len > 0) {
|
||||
if (data_cb_)
|
||||
data_cb_(data_arg_, this, buf, len);
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
ESP_LOGW(TAG, "Read error: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
} else {
|
||||
const int err = errno;
|
||||
if (err != EAGAIN && err != EWOULDBLOCK) {
|
||||
ESP_LOGW(TAG, "Read error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ AT581XSettingsAction = at581x_ns.class_("AT581XSettingsAction", automation.Actio
|
||||
cv.Required(CONF_ID): cv.use_id(AT581XComponent),
|
||||
}
|
||||
),
|
||||
synchronous=True,
|
||||
)
|
||||
async def at581x_reset_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
@@ -160,6 +161,7 @@ RADAR_SETTINGS_SCHEMA = cv.Schema(
|
||||
"at581x.settings",
|
||||
AT581XSettingsAction,
|
||||
RADAR_SETTINGS_SCHEMA,
|
||||
synchronous=True,
|
||||
)
|
||||
async def at581x_settings_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
|
||||
@@ -77,14 +77,14 @@ void AT581XComponent::dump_config() { LOG_I2C_DEVICE(this); }
|
||||
bool AT581XComponent::i2c_write_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Writing new config for AT581X\n"
|
||||
"Frequency: %dMHz\n"
|
||||
"Sensing distance: %d\n"
|
||||
"Power: %dµA\n"
|
||||
"Gain: %d\n"
|
||||
"Trigger base time: %dms\n"
|
||||
"Trigger keep time: %dms\n"
|
||||
"Protect time: %dms\n"
|
||||
"Self check time: %dms",
|
||||
" Frequency: %dMHz\n"
|
||||
" Sensing distance: %d\n"
|
||||
" Power: %dµA\n"
|
||||
" Gain: %d\n"
|
||||
" Trigger base time: %dms\n"
|
||||
" Trigger keep time: %dms\n"
|
||||
" Protect time: %dms\n"
|
||||
" Self check time: %dms",
|
||||
this->freq_, this->delta_, this->power_, this->gain_, this->trigger_base_time_ms_,
|
||||
this->trigger_keep_time_ms_, this->protect_time_ms_, this->self_check_time_ms_);
|
||||
|
||||
@@ -135,6 +135,11 @@ bool AT581XComponent::i2c_write_config() {
|
||||
}
|
||||
|
||||
// Set gain
|
||||
if (this->gain_ < 0 || static_cast<size_t>(this->gain_) >= ARRAY_SIZE(GAIN5C_TABLE) ||
|
||||
static_cast<size_t>(this->gain_ >> 1) >= ARRAY_SIZE(GAIN63_TABLE)) {
|
||||
ESP_LOGE(TAG, "AT581X gain index out of range: %d", this->gain_);
|
||||
return false;
|
||||
}
|
||||
if (!this->i2c_write_reg(GAIN_ADDR_TABLE[0], GAIN5C_TABLE[this->gain_]) ||
|
||||
!this->i2c_write_reg(GAIN_ADDR_TABLE[1], GAIN63_TABLE[this->gain_ >> 1])) {
|
||||
ESP_LOGE(TAG, "Failed to write AT581X gain registers");
|
||||
|
||||
@@ -61,6 +61,10 @@ optional<ParseResult> ATCMiThermometer::parse_header_(const esp32_ble_tracker::S
|
||||
}
|
||||
|
||||
auto raw = service_data.data;
|
||||
if (raw.size() < 13) {
|
||||
ESP_LOGVV(TAG, "parse_header_(): service data too short (%zu).", raw.size());
|
||||
return {};
|
||||
}
|
||||
|
||||
static uint8_t last_frame_count = 0;
|
||||
if (last_frame_count == raw[12]) {
|
||||
|
||||
@@ -197,7 +197,7 @@ float ATM90E26Component::get_reactive_power_() {
|
||||
float ATM90E26Component::get_power_factor_() {
|
||||
const uint16_t val = this->read16_(ATM90E26_REGISTER_POWERF); // signed
|
||||
if (val & 0x8000) {
|
||||
return -(val & 0x7FF) / 1000.0f;
|
||||
return -(val & 0x7FFF) / 1000.0f;
|
||||
} else {
|
||||
return val / 1000.0f;
|
||||
}
|
||||
|
||||
@@ -619,7 +619,7 @@ void ATM90E32Component::run_gain_calibrations() {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs,
|
||||
phase_labels[phase]);
|
||||
} else {
|
||||
uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain);
|
||||
uint32_t new_voltage_gain = static_cast<uint32_t>((ref_voltage / measured_voltage) * current_voltage_gain);
|
||||
if (new_voltage_gain == 0) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs,
|
||||
phase_labels[phase]);
|
||||
@@ -644,7 +644,7 @@ void ATM90E32Component::run_gain_calibrations() {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs,
|
||||
phase_labels[phase]);
|
||||
} else {
|
||||
uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain);
|
||||
uint32_t new_current_gain = static_cast<uint32_t>((ref_current / measured_current) * current_current_gain);
|
||||
if (new_current_gain == 0) {
|
||||
ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs,
|
||||
phase_labels[phase]);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user