mirror of
https://github.com/esphome/esphome.git
synced 2026-01-13 05:27:53 -07:00
Compare commits
410 Commits
ha_state_n
...
api_debug_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3c97a00bd8 | ||
|
|
6ac8df7cbd | ||
|
|
b24a1a9e25 | ||
|
|
5e911e20bc | ||
|
|
b5f6a6e24d | ||
|
|
5890cdf69a | ||
|
|
297f05d600 | ||
|
|
54fc10714d | ||
|
|
889886909b | ||
|
|
655e2b43cb | ||
|
|
81e639a6ba | ||
|
|
f9ffd134df | ||
|
|
c50bf45496 | ||
|
|
9f9341a700 | ||
|
|
71d532a349 | ||
|
|
61a89a97d7 | ||
|
|
0c3433d056 | ||
|
|
7e1cda8f9f | ||
|
|
7f0e4eaa84 | ||
|
|
8cccfa5369 | ||
|
|
7ea6bcef88 | ||
|
|
353daa97d0 | ||
|
|
6c68ebe86e | ||
|
|
29cef3bc5d | ||
|
|
83eebdf15d | ||
|
|
595217786c | ||
|
|
912f94d1e8 | ||
|
|
ea8ae2ae60 | ||
|
|
e1aac7601d | ||
|
|
f1b11b1855 | ||
|
|
23f9f70b71 | ||
|
|
eeeae53f76 | ||
|
|
45c0796e40 | ||
|
|
38e2e4a56d | ||
|
|
52132ea3bc | ||
|
|
ace3ff2170 | ||
|
|
26e90b4ca6 | ||
|
|
684790c2ab | ||
|
|
6a3737bac3 | ||
|
|
723ca57617 | ||
|
|
909bd1074a | ||
|
|
68064dc974 | ||
|
|
742d724e65 | ||
|
|
5ae46a4369 | ||
|
|
a1395af763 | ||
|
|
6222fae907 | ||
|
|
e34532f283 | ||
|
|
f2eb61a767 | ||
|
|
5725a4840e | ||
|
|
de82f96ccb | ||
|
|
6c981d8b71 | ||
|
|
c03faf2d9a | ||
|
|
da7680f7d9 | ||
|
|
cea2878b55 | ||
|
|
e0ff7fdaa1 | ||
|
|
3c9b300c46 | ||
|
|
32f90b2855 | ||
|
|
2fb7c0d453 | ||
|
|
7935fba4b1 | ||
|
|
ab32b93928 | ||
|
|
3d54ccac65 | ||
|
|
c40f44f4bd | ||
|
|
62cb08c3dc | ||
|
|
7576e032f8 | ||
|
|
cd43b4114e | ||
|
|
2c165e4817 | ||
|
|
5afe4b7b12 | ||
|
|
dcb8c994cc | ||
|
|
012a1e2afd | ||
|
|
d4969f581a | ||
|
|
40f108116b | ||
|
|
52459d1bc7 | ||
|
|
325c938074 | ||
|
|
423a617b15 | ||
|
|
eb5c4f34e2 | ||
|
|
c9ab4ca018 | ||
|
|
da0b01f4d0 | ||
|
|
e301b8d0e0 | ||
|
|
738678e87b | ||
|
|
0ce3ac438b | ||
|
|
afa4fe9820 | ||
|
|
a66df9ab0f | ||
|
|
1339f3e77e | ||
|
|
e29523e248 | ||
|
|
44eac36e05 | ||
|
|
050e9b0d4a | ||
|
|
25ac89e9b5 | ||
|
|
d86d1f9f52 | ||
|
|
fd19280df9 | ||
|
|
b7dbda497a | ||
|
|
815543b77e | ||
|
|
0948e0359f | ||
|
|
2830c7dab8 | ||
|
|
a03c13f304 | ||
|
|
ef64226ed0 | ||
|
|
ed39a130a8 | ||
|
|
21687a1f58 | ||
|
|
bf75f77eee | ||
|
|
39526e5360 | ||
|
|
8e40a55d5d | ||
|
|
20927674da | ||
|
|
8464307a43 | ||
|
|
546cdbde0d | ||
|
|
ada4e6d5e9 | ||
|
|
d6554702d8 | ||
|
|
b083c33857 | ||
|
|
f8309b007c | ||
|
|
ac672e4b8f | ||
|
|
c387c03944 | ||
|
|
fb47bfe92a | ||
|
|
5b9be7c169 | ||
|
|
6d1f6a1084 | ||
|
|
f9ed2aa17f | ||
|
|
35118da606 | ||
|
|
498477c5a2 | ||
|
|
3a84e4a0b4 | ||
|
|
4391457a96 | ||
|
|
68b4bc9d9e | ||
|
|
b052c9f562 | ||
|
|
a19597626b | ||
|
|
2147ddf8c7 | ||
|
|
412ab5dbbf | ||
|
|
4419bf02b1 | ||
|
|
8eb28a7724 | ||
|
|
d6c2dd3c26 | ||
|
|
ac42102320 | ||
|
|
2c6584baf5 | ||
|
|
c1ad39a072 | ||
|
|
d3e193cd71 | ||
|
|
11aed601b8 | ||
|
|
e0981323bd | ||
|
|
1e56325b33 | ||
|
|
a8a26f4ea8 | ||
|
|
a94eef3a60 | ||
|
|
b6f3a5d8b7 | ||
|
|
3322b04e00 | ||
|
|
47d0d3cfeb | ||
|
|
8255c02d5d | ||
|
|
8b4ba8dfe6 | ||
|
|
178a61b6fd | ||
|
|
b5df4cdf1d | ||
|
|
484f4b3aad | ||
|
|
22cb0da903 | ||
|
|
28cf3b7a9b | ||
|
|
84dd17187d | ||
|
|
110c892c3c | ||
|
|
8518424a88 | ||
|
|
7ba4dc0f1a | ||
|
|
95573bc106 | ||
|
|
a6adc29b14 | ||
|
|
8251513556 | ||
|
|
e6e0be3345 | ||
|
|
64da6d46e9 | ||
|
|
9b9a341db0 | ||
|
|
18217fbe10 | ||
|
|
c3e6a4178c | ||
|
|
2d4cd4ce7e | ||
|
|
0290ed5d23 | ||
|
|
b402e403a0 | ||
|
|
b2c22a02b1 | ||
|
|
7ed4922d28 | ||
|
|
21aa245cff | ||
|
|
94bedd83be | ||
|
|
c8f5a97cef | ||
|
|
fc7e55bfdc | ||
|
|
6aaaae5d0e | ||
|
|
e87a3b3916 | ||
|
|
3fb5b28930 | ||
|
|
1bb4be435c | ||
|
|
0990a9c2b0 | ||
|
|
086eb4b930 | ||
|
|
d107b37d3b | ||
|
|
80ab9485e0 | ||
|
|
28d30fdddb | ||
|
|
0bd8a7e1a0 | ||
|
|
2381ea7ff5 | ||
|
|
06101c54a5 | ||
|
|
ed332a034b | ||
|
|
a2bb9468ff | ||
|
|
d8387799d9 | ||
|
|
ae3cdeda99 | ||
|
|
9cd003034c | ||
|
|
f67a8d0d1f | ||
|
|
47223965b6 | ||
|
|
9128fc3120 | ||
|
|
c742db48b8 | ||
|
|
4bc1a02fc2 | ||
|
|
1d0f36ba35 | ||
|
|
405b26426c | ||
|
|
2295f57dec | ||
|
|
a011d5ea96 | ||
|
|
12027569d3 | ||
|
|
ab0e15e4bb | ||
|
|
9f7925c1d5 | ||
|
|
7449421cea | ||
|
|
4f20c1ceb1 | ||
|
|
452fcd56dd | ||
|
|
44fc156ef6 | ||
|
|
3ec05a5a13 | ||
|
|
e6a630ae64 | ||
|
|
3c8fd5c5c0 | ||
|
|
a635c82830 | ||
|
|
0b9fcf9ed3 | ||
|
|
2d8abbb2ac | ||
|
|
6d8142c539 | ||
|
|
50f27cdd77 | ||
|
|
6c809583d3 | ||
|
|
f41f0506c1 | ||
|
|
850f189225 | ||
|
|
a37d4b17eb | ||
|
|
7309a65167 | ||
|
|
7fde110ac5 | ||
|
|
9ed107bc33 | ||
|
|
b291f359ae | ||
|
|
161545584d | ||
|
|
3ea11d4e59 | ||
|
|
5713d69efe | ||
|
|
9d9f9c3c84 | ||
|
|
29d332af92 | ||
|
|
c44d095f8a | ||
|
|
0b996616b8 | ||
|
|
557b6a9ef0 | ||
|
|
6e633f7f3b | ||
|
|
c59455e445 | ||
|
|
32b3d27c7c | ||
|
|
88cb5d9671 | ||
|
|
56d1d928f9 | ||
|
|
e9cab96cb7 | ||
|
|
022c42f9ca | ||
|
|
25ef9aff04 | ||
|
|
71940acc49 | ||
|
|
05695affff | ||
|
|
f2308c77c6 | ||
|
|
a5368d1d95 | ||
|
|
b8d93f2150 | ||
|
|
ca574a1550 | ||
|
|
b0855b4a0e | ||
|
|
1fccddf67f | ||
|
|
548600b47a | ||
|
|
9bbfad4a08 | ||
|
|
8ae1f26b6a | ||
|
|
9b2a36a313 | ||
|
|
aa4b274b3c | ||
|
|
d1d5c942ec | ||
|
|
ccc9d95c9d | ||
|
|
6d9d593e12 | ||
|
|
fc9683f024 | ||
|
|
61ecfb5f2b | ||
|
|
7e75826064 | ||
|
|
8287484a36 | ||
|
|
dd8259b2ce | ||
|
|
449e478bec | ||
|
|
9ae19d53dc | ||
|
|
77b3ffee00 | ||
|
|
dff8dc0ed1 | ||
|
|
5a8b0f59b8 | ||
|
|
25a325da61 | ||
|
|
a6db5a2ed8 | ||
|
|
9e5dbb073a | ||
|
|
cf513975f3 | ||
|
|
1e70091a27 | ||
|
|
766826cc9c | ||
|
|
8a4ee19c0b | ||
|
|
b1f9c08f51 | ||
|
|
facf4777a4 | ||
|
|
096de869b6 | ||
|
|
c59314ec09 | ||
|
|
e94158a12f | ||
|
|
cb598c43e8 | ||
|
|
6e8817cbc4 | ||
|
|
9f06f046d6 | ||
|
|
44fa6bae95 | ||
|
|
bc9093127e | ||
|
|
cf93b66306 | ||
|
|
16ada4d477 | ||
|
|
c96d0015a0 | ||
|
|
12c6f5749e | ||
|
|
5f1eacf4ec | ||
|
|
5d384c77c5 | ||
|
|
32562ca991 | ||
|
|
6b4b1272db | ||
|
|
7b74f94360 | ||
|
|
997ab553c1 | ||
|
|
8b80fe9c6b | ||
|
|
ee65f2f0cd | ||
|
|
723ccd7547 | ||
|
|
102862e99d | ||
|
|
9cb265347c | ||
|
|
d84562f878 | ||
|
|
6bbee3cfc6 | ||
|
|
41e7ecb29f | ||
|
|
0196d6ee55 | ||
|
|
ea848db683 | ||
|
|
41a188ac35 | ||
|
|
8ddfeb2d38 | ||
|
|
d364432e3a | ||
|
|
2a6b192af8 | ||
|
|
07a581e13a | ||
|
|
5f5edf90e9 | ||
|
|
5e24469ce3 | ||
|
|
d7a1ac83ca | ||
|
|
f11abc7dbf | ||
|
|
ec05692f0d | ||
|
|
2e2e54811a | ||
|
|
c29aa61e2a | ||
|
|
cb3edfc654 | ||
|
|
6685fa1da9 | ||
|
|
d505f0316b | ||
|
|
9781073f2a | ||
|
|
0a0501c140 | ||
|
|
a6e9aa7876 | ||
|
|
ede7391582 | ||
|
|
5cfcf8d104 | ||
|
|
c34665f650 | ||
|
|
69867bf818 | ||
|
|
1d323c2d71 | ||
|
|
95a7356ea0 | ||
|
|
89b550b74a | ||
|
|
538c6544a0 | ||
|
|
98e3695c89 | ||
|
|
00fd4f2fdd | ||
|
|
2a5be725c8 | ||
|
|
c4d339a4c9 | ||
|
|
6409970f6e | ||
|
|
bc1af007b4 | ||
|
|
c3ffc1635d | ||
|
|
016eeef04a | ||
|
|
ace48464a8 | ||
|
|
64ba376330 | ||
|
|
d946ddabfd | ||
|
|
a57011b50b | ||
|
|
1240e7907e | ||
|
|
f0391f0213 | ||
|
|
3cc6810be5 | ||
|
|
916370a943 | ||
|
|
e2f45c590e | ||
|
|
7d21411ca4 | ||
|
|
56ed5af27d | ||
|
|
c8241b0122 | ||
|
|
30efd7fb07 | ||
|
|
1703343694 | ||
|
|
7fa04b6c25 | ||
|
|
61b6476de4 | ||
|
|
b4e5e0bc9b | ||
|
|
f9b4e0e489 | ||
|
|
9ccb100cca | ||
|
|
20b66cba23 | ||
|
|
b711172b33 | ||
|
|
0c4184b129 | ||
|
|
0e108c2178 | ||
|
|
2230e56347 | ||
|
|
2ff9535f5f | ||
|
|
ddb6c6cfd4 | ||
|
|
00ab64a3c7 | ||
|
|
e732f8469e | ||
|
|
023be88a87 | ||
|
|
25e60d62cf | ||
|
|
167a42aa27 | ||
|
|
0ef49a8b73 | ||
|
|
51259888bf | ||
|
|
0b7ff09657 | ||
|
|
f394cf3f4d | ||
|
|
4cb066bcbf | ||
|
|
e7001c5eea | ||
|
|
5bb9ffa0cb | ||
|
|
c6713eaccb | ||
|
|
087f521b19 | ||
|
|
763515d3a1 | ||
|
|
6d4f4d8d23 | ||
|
|
d7fd85e610 | ||
|
|
8acaa16987 | ||
|
|
4e8c02b396 | ||
|
|
a828abf53d | ||
|
|
ebfa0149cc | ||
|
|
3a4cca0027 | ||
|
|
7702a9ae85 | ||
|
|
2e8baa0493 | ||
|
|
69ec311d21 | ||
|
|
1cc18055ef | ||
|
|
bcc6bbbf5f | ||
|
|
71c3d4ca27 | ||
|
|
c6f3860f90 | ||
|
|
0049c8ad38 | ||
|
|
e1788bba45 | ||
|
|
4fcd263ea8 | ||
|
|
c81ce243cc | ||
|
|
7df41124b2 | ||
|
|
b5188731f8 | ||
|
|
0924281545 | ||
|
|
14e97642f7 | ||
|
|
544aaeaa66 | ||
|
|
7483bbd6ea | ||
|
|
2841b5fe44 | ||
|
|
ed435241b1 | ||
|
|
9847e51fbc | ||
|
|
dc320f455a | ||
|
|
1945e85ddc | ||
|
|
4313130f2e | ||
|
|
3c9ed126a6 | ||
|
|
d8c23d4fc9 | ||
|
|
1d96de986e | ||
|
|
e9e0712959 | ||
|
|
062840dd7b | ||
|
|
f0f01c081a | ||
|
|
dd855985be | ||
|
|
4633803d5d | ||
|
|
476d00d0e5 | ||
|
|
98cdef2568 | ||
|
|
bd3ecad3a1 | ||
|
|
dae7ba604a |
@@ -293,6 +293,12 @@ This document provides essential context for AI models interacting with this pro
|
||||
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
|
||||
* **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage.
|
||||
|
||||
**Why Heap Allocation Matters:**
|
||||
|
||||
ESP devices run for months with small heaps shared between Wi-Fi, BLE, LWIP, and application code. Over time, repeated allocations of different sizes fragment the heap. Failures happen when the largest contiguous block shrinks, even if total free heap is still large. We have seen field crashes caused by this.
|
||||
|
||||
**Heap allocation after `setup()` should be avoided unless absolutely unavoidable.** Every allocation/deallocation cycle contributes to fragmentation. ESPHome treats runtime heap allocation as a long-term reliability bug, not a performance issue. Helpers that hide allocation (`std::string`, `std::to_string`, string-returning helpers) are being deprecated and replaced with buffer and view based APIs.
|
||||
|
||||
**STL Container Guidelines:**
|
||||
|
||||
ESPHome runs on embedded systems with limited resources. Choose containers carefully:
|
||||
@@ -322,15 +328,15 @@ This document provides essential context for AI models interacting with this pro
|
||||
std::array<uint8_t, 256> buffer;
|
||||
```
|
||||
|
||||
2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for fixed-size stack allocation with `push_back()` interface.
|
||||
2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for compile-time fixed size with `push_back()` interface (no dynamic allocation).
|
||||
```cpp
|
||||
// Bad - generates STL realloc code (_M_realloc_insert)
|
||||
std::vector<ServiceRecord> services;
|
||||
services.reserve(5); // Still includes reallocation machinery
|
||||
|
||||
// Good - compile-time fixed size, stack allocated, no reallocation machinery
|
||||
StaticVector<ServiceRecord, MAX_SERVICES> services; // Allocates all MAX_SERVICES on stack
|
||||
services.push_back(record1); // Tracks count but all slots allocated
|
||||
// Good - compile-time fixed size, no dynamic allocation
|
||||
StaticVector<ServiceRecord, MAX_SERVICES> services;
|
||||
services.push_back(record1);
|
||||
```
|
||||
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
|
||||
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
|
||||
@@ -372,22 +378,21 @@ This document provides essential context for AI models interacting with this pro
|
||||
```
|
||||
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
|
||||
|
||||
5. **Detection:** Look for these patterns in compiler output:
|
||||
5. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
|
||||
|
||||
6. **Detection:** Look for these patterns in compiler output:
|
||||
- Large code sections with STL symbols (vector, map, set)
|
||||
- `alloc`, `realloc`, `dealloc` in symbol names
|
||||
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
|
||||
- Red-black tree code (`rb_tree`, `_Rb_tree`)
|
||||
- Hash table infrastructure (`unordered_map`, `hash`)
|
||||
|
||||
**When to optimize:**
|
||||
**Prioritize optimization effort for:**
|
||||
- Core components (API, network, logger)
|
||||
- Widely-used components (mdns, wifi, ble)
|
||||
- Components causing flash size complaints
|
||||
|
||||
**When not to optimize:**
|
||||
- Single-use niche components
|
||||
- Code where readability matters more than bytes
|
||||
- Already using appropriate containers
|
||||
Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`).
|
||||
|
||||
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
94557f94be073390342833aff12ef8676a8b597db5fa770a5a1232e9425cb48f
|
||||
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
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@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
|
||||
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.10
|
||||
rev: v0.14.11
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -91,6 +91,7 @@ esphome/components/bmp3xx_spi/* @latonita
|
||||
esphome/components/bmp581/* @kahrendt
|
||||
esphome/components/bp1658cj/* @Cossid
|
||||
esphome/components/bp5758d/* @Cossid
|
||||
esphome/components/bthome_mithermometer/* @nagyrobi
|
||||
esphome/components/button/* @esphome/core
|
||||
esphome/components/bytebuffer/* @clydebarrow
|
||||
esphome/components/camera/* @bdraco @DT-art1
|
||||
@@ -134,7 +135,7 @@ esphome/components/display_menu_base/* @numo68
|
||||
esphome/components/dps310/* @kbx81
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/ds2484/* @mrk-its
|
||||
esphome/components/dsmr/* @glmnet @zuidwijk
|
||||
esphome/components/dsmr/* @glmnet @PolarGoose @zuidwijk
|
||||
esphome/components/duty_time/* @dudanov
|
||||
esphome/components/ee895/* @Stock-M
|
||||
esphome/components/ektf2232/touchscreen/* @jesserockz
|
||||
@@ -248,6 +249,7 @@ esphome/components/ina260/* @mreditor97
|
||||
esphome/components/ina2xx_base/* @latonita
|
||||
esphome/components/ina2xx_i2c/* @latonita
|
||||
esphome/components/ina2xx_spi/* @latonita
|
||||
esphome/components/infrared/* @kbx81
|
||||
esphome/components/inkbird_ibsth1_mini/* @fkirill
|
||||
esphome/components/inkplate/* @jesserockz @JosipKuci
|
||||
esphome/components/integration/* @OttoWinter
|
||||
@@ -394,6 +396,7 @@ esphome/components/radon_eye_rd200/* @jeffeb3
|
||||
esphome/components/rc522/* @glmnet
|
||||
esphome/components/rc522_i2c/* @glmnet
|
||||
esphome/components/rc522_spi/* @glmnet
|
||||
esphome/components/rd03d/* @jasstrong
|
||||
esphome/components/resampler/speaker/* @kahrendt
|
||||
esphome/components/restart/* @esphome/core
|
||||
esphome/components/rf_bridge/* @jesserockz
|
||||
@@ -574,5 +577,6 @@ esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
|
||||
esphome/components/xxtea/* @clydebarrow
|
||||
esphome/components/zephyr/* @tomaszduda23
|
||||
esphome/components/zhlt01/* @cfeenstra1024
|
||||
esphome/components/zigbee/* @tomaszduda23
|
||||
esphome/components/zio_ultrasonic/* @kahrendt
|
||||
esphome/components/zwave_proxy/* @kbx81
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
include LICENSE
|
||||
include README.md
|
||||
include requirements.txt
|
||||
recursive-include esphome *.yaml
|
||||
recursive-include esphome *.cpp *.h *.tcc *.c
|
||||
recursive-include esphome *.py.script
|
||||
recursive-include esphome LICENSE.txt
|
||||
|
||||
@@ -11,6 +11,16 @@ FROM base-source-${BUILD_TYPE} AS base
|
||||
|
||||
RUN git config --system --add safe.directory "*"
|
||||
|
||||
# Install build tools for Python packages that require compilation
|
||||
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
|
||||
RUN if command -v apk > /dev/null; then \
|
||||
apk add --no-cache build-base; \
|
||||
else \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
RUN pip install --no-cache-dir -U pip uv==0.6.14
|
||||
|
||||
@@ -62,6 +62,9 @@ from esphome.util import (
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Maximum buffer size for serial log reading to prevent unbounded memory growth
|
||||
SERIAL_BUFFER_MAX_SIZE = 65536
|
||||
|
||||
# Special non-component keys that appear in configs
|
||||
_NON_COMPONENT_KEYS = frozenset(
|
||||
{
|
||||
@@ -431,25 +434,37 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
while tries < 5:
|
||||
try:
|
||||
with ser:
|
||||
buffer = b""
|
||||
ser.timeout = 0.1 # 100ms timeout for non-blocking reads
|
||||
while True:
|
||||
try:
|
||||
raw = ser.readline()
|
||||
# Read all available data and timestamp it
|
||||
chunk = ser.read(ser.in_waiting or 1)
|
||||
if not chunk:
|
||||
continue
|
||||
time_ = datetime.now()
|
||||
milliseconds = time_.microsecond // 1000
|
||||
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{milliseconds:03}]"
|
||||
|
||||
# Add to buffer and process complete lines
|
||||
# Limit buffer size to prevent unbounded memory growth
|
||||
# if device sends data without newlines
|
||||
buffer += chunk
|
||||
if len(buffer) > SERIAL_BUFFER_MAX_SIZE:
|
||||
buffer = buffer[-SERIAL_BUFFER_MAX_SIZE:]
|
||||
while b"\n" in buffer:
|
||||
raw_line, buffer = buffer.split(b"\n", 1)
|
||||
line = raw_line.replace(b"\r", b"").decode(
|
||||
"utf8", "backslashreplace"
|
||||
)
|
||||
safe_print(parser.parse_line(line, time_str))
|
||||
|
||||
backtrace_state = platformio_api.process_stacktrace(
|
||||
config, line, backtrace_state=backtrace_state
|
||||
)
|
||||
except serial.SerialException:
|
||||
_LOGGER.error("Serial port closed!")
|
||||
return 0
|
||||
line = (
|
||||
raw.replace(b"\r", b"")
|
||||
.replace(b"\n", b"")
|
||||
.decode("utf8", "backslashreplace")
|
||||
)
|
||||
time_ = datetime.now()
|
||||
nanoseconds = time_.microsecond // 1000
|
||||
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
|
||||
safe_print(parser.parse_line(line, time_str))
|
||||
|
||||
backtrace_state = platformio_api.process_stacktrace(
|
||||
config, line, backtrace_state=backtrace_state
|
||||
)
|
||||
except serial.SerialException:
|
||||
tries += 1
|
||||
time.sleep(1)
|
||||
@@ -1017,6 +1032,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
idedata.objdump_path,
|
||||
idedata.readelf_path,
|
||||
external_components,
|
||||
idedata=idedata,
|
||||
)
|
||||
analyzer.analyze()
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ from .helpers import (
|
||||
map_section_name,
|
||||
parse_symbol_line,
|
||||
)
|
||||
from .toolchain import find_tool, run_tool
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from esphome.platformio_api import IDEData
|
||||
@@ -53,6 +54,9 @@ _NAMESPACE_STD = "std::"
|
||||
# Type alias for symbol information: (symbol_name, size, component)
|
||||
SymbolInfoType = tuple[str, int, str]
|
||||
|
||||
# RAM sections - symbols in these sections consume RAM
|
||||
RAM_SECTIONS = frozenset([".data", ".bss"])
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemorySection:
|
||||
@@ -60,7 +64,20 @@ class MemorySection:
|
||||
|
||||
name: str
|
||||
symbols: list[SymbolInfoType] = field(default_factory=list)
|
||||
total_size: int = 0
|
||||
total_size: int = 0 # Actual section size from ELF headers
|
||||
symbol_size: int = 0 # Sum of symbol sizes (may be less than total_size)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SDKSymbol:
|
||||
"""Represents a symbol from an SDK library that's not in the ELF symbol table."""
|
||||
|
||||
name: str
|
||||
size: int
|
||||
library: str # Name of the .a file (e.g., "libpp.a")
|
||||
section: str # ".bss" or ".data"
|
||||
is_local: bool # True if static/local symbol (lowercase in nm output)
|
||||
demangled: str = "" # Demangled name (populated after analysis)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -118,6 +135,10 @@ class MemoryAnalyzer:
|
||||
self.objdump_path = objdump_path or "objdump"
|
||||
self.readelf_path = readelf_path or "readelf"
|
||||
self.external_components = external_components or set()
|
||||
self._idedata = idedata
|
||||
|
||||
# Derive nm path from objdump path using shared toolchain utility
|
||||
self.nm_path = find_tool("nm", self.objdump_path)
|
||||
|
||||
self.sections: dict[str, MemorySection] = {}
|
||||
self.components: dict[str, ComponentMemory] = defaultdict(
|
||||
@@ -128,15 +149,25 @@ class MemoryAnalyzer:
|
||||
self._esphome_core_symbols: list[
|
||||
tuple[str, str, int]
|
||||
] = [] # Track core symbols
|
||||
self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict(
|
||||
# Track symbols for all components: (symbol_name, demangled, size, section)
|
||||
self._component_symbols: dict[str, list[tuple[str, str, int, str]]] = (
|
||||
defaultdict(list)
|
||||
)
|
||||
# Track RAM symbols separately for detailed analysis: (symbol_name, demangled, size, section)
|
||||
self._ram_symbols: dict[str, list[tuple[str, str, int, str]]] = defaultdict(
|
||||
list
|
||||
) # Track symbols for all components
|
||||
)
|
||||
# Track ELF symbol names for SDK cross-reference
|
||||
self._elf_symbol_names: set[str] = set()
|
||||
# SDK symbols not in ELF (static/local symbols from closed-source libs)
|
||||
self._sdk_symbols: list[SDKSymbol] = []
|
||||
|
||||
def analyze(self) -> dict[str, ComponentMemory]:
|
||||
"""Analyze the ELF file and return component memory usage."""
|
||||
self._parse_sections()
|
||||
self._parse_symbols()
|
||||
self._categorize_symbols()
|
||||
self._analyze_sdk_libraries()
|
||||
return dict(self.components)
|
||||
|
||||
def _parse_sections(self) -> None:
|
||||
@@ -190,6 +221,8 @@ class MemoryAnalyzer:
|
||||
continue
|
||||
|
||||
self.sections[section].symbols.append((name, size, ""))
|
||||
self.sections[section].symbol_size += size
|
||||
self._elf_symbol_names.add(name)
|
||||
seen_addresses.add(address)
|
||||
|
||||
def _categorize_symbols(self) -> None:
|
||||
@@ -233,8 +266,13 @@ class MemoryAnalyzer:
|
||||
if size > 0:
|
||||
demangled = self._demangle_symbol(symbol_name)
|
||||
self._component_symbols[component].append(
|
||||
(symbol_name, demangled, size)
|
||||
(symbol_name, demangled, size, section_name)
|
||||
)
|
||||
# Track RAM symbols separately for detailed RAM analysis
|
||||
if section_name in RAM_SECTIONS:
|
||||
self._ram_symbols[component].append(
|
||||
(symbol_name, demangled, size, section_name)
|
||||
)
|
||||
|
||||
def _identify_component(self, symbol_name: str) -> str:
|
||||
"""Identify which component a symbol belongs to."""
|
||||
@@ -328,6 +366,247 @@ class MemoryAnalyzer:
|
||||
|
||||
return "Other Core"
|
||||
|
||||
def get_unattributed_ram(self) -> tuple[int, int, int]:
|
||||
"""Get unattributed RAM sizes (SDK/framework overhead).
|
||||
|
||||
Returns:
|
||||
Tuple of (unattributed_bss, unattributed_data, total_unattributed)
|
||||
These are bytes in RAM sections that have no corresponding symbols.
|
||||
"""
|
||||
bss_section = self.sections.get(".bss")
|
||||
data_section = self.sections.get(".data")
|
||||
|
||||
unattributed_bss = 0
|
||||
unattributed_data = 0
|
||||
|
||||
if bss_section:
|
||||
unattributed_bss = max(0, bss_section.total_size - bss_section.symbol_size)
|
||||
if data_section:
|
||||
unattributed_data = max(
|
||||
0, data_section.total_size - data_section.symbol_size
|
||||
)
|
||||
|
||||
return unattributed_bss, unattributed_data, unattributed_bss + unattributed_data
|
||||
|
||||
def _find_sdk_library_dirs(self) -> list[Path]:
|
||||
"""Find SDK library directories based on platform.
|
||||
|
||||
Returns:
|
||||
List of paths to SDK library directories containing .a files.
|
||||
"""
|
||||
sdk_dirs: list[Path] = []
|
||||
|
||||
if self._idedata is None:
|
||||
return sdk_dirs
|
||||
|
||||
# Get the CC path to determine the framework location
|
||||
cc_path = getattr(self._idedata, "cc_path", None)
|
||||
if not cc_path:
|
||||
return sdk_dirs
|
||||
|
||||
cc_path = Path(cc_path)
|
||||
|
||||
# For ESP8266 Arduino framework
|
||||
# CC is like: ~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gcc
|
||||
# SDK libs are in: ~/.platformio/packages/framework-arduinoespressif8266/tools/sdk/lib/
|
||||
if "xtensa-lx106" in str(cc_path):
|
||||
platformio_dir = cc_path.parent.parent.parent
|
||||
esp8266_sdk = (
|
||||
platformio_dir
|
||||
/ "framework-arduinoespressif8266"
|
||||
/ "tools"
|
||||
/ "sdk"
|
||||
/ "lib"
|
||||
)
|
||||
if esp8266_sdk.exists():
|
||||
sdk_dirs.append(esp8266_sdk)
|
||||
# Also check for NONOSDK subdirectories (closed-source libs)
|
||||
sdk_dirs.extend(
|
||||
subdir
|
||||
for subdir in esp8266_sdk.iterdir()
|
||||
if subdir.is_dir() and subdir.name.startswith("NONOSDK")
|
||||
)
|
||||
|
||||
# For ESP32 IDF framework
|
||||
# CC is like: ~/.platformio/packages/toolchain-xtensa-esp-elf/bin/xtensa-esp32-elf-gcc
|
||||
# or: ~/.platformio/packages/toolchain-riscv32-esp/bin/riscv32-esp-elf-gcc
|
||||
elif "xtensa-esp" in str(cc_path) or "riscv32-esp" in str(cc_path):
|
||||
# Detect ESP32 variant from CC path or defines
|
||||
variant = self._detect_esp32_variant()
|
||||
if variant:
|
||||
platformio_dir = cc_path.parent.parent.parent
|
||||
espidf_dir = platformio_dir / "framework-espidf" / "components"
|
||||
if espidf_dir.exists():
|
||||
# Find all directories named after the variant that contain .a files
|
||||
# This handles various ESP-IDF library layouts:
|
||||
# - components/*/lib/<variant>/
|
||||
# - components/*/<variant>/
|
||||
# - components/*/lib/lib/<variant>/
|
||||
# - components/*/*/lib_*/<variant>/
|
||||
sdk_dirs.extend(
|
||||
variant_dir
|
||||
for variant_dir in espidf_dir.rglob(variant)
|
||||
if variant_dir.is_dir() and any(variant_dir.glob("*.a"))
|
||||
)
|
||||
|
||||
return sdk_dirs
|
||||
|
||||
def _detect_esp32_variant(self) -> str | None:
|
||||
"""Detect ESP32 variant from idedata defines.
|
||||
|
||||
Returns:
|
||||
Variant string like 'esp32', 'esp32s2', 'esp32c3', etc. or None.
|
||||
"""
|
||||
if self._idedata is None:
|
||||
return None
|
||||
|
||||
defines = getattr(self._idedata, "defines", [])
|
||||
if not defines:
|
||||
return None
|
||||
|
||||
# ESPHome always adds USE_ESP32_VARIANT_xxx defines
|
||||
variant_prefix = "USE_ESP32_VARIANT_"
|
||||
for define in defines:
|
||||
if define.startswith(variant_prefix):
|
||||
# Extract variant name and convert to lowercase
|
||||
# USE_ESP32_VARIANT_ESP32 -> esp32
|
||||
# USE_ESP32_VARIANT_ESP32S3 -> esp32s3
|
||||
return define[len(variant_prefix) :].lower()
|
||||
|
||||
return None
|
||||
|
||||
def _parse_sdk_library(
|
||||
self, lib_path: Path
|
||||
) -> tuple[list[tuple[str, int, str, bool]], set[str]]:
|
||||
"""Parse a single SDK library for symbols.
|
||||
|
||||
Args:
|
||||
lib_path: Path to the .a library file
|
||||
|
||||
Returns:
|
||||
Tuple of:
|
||||
- List of BSS/DATA symbols: (symbol_name, size, section, is_local)
|
||||
- Set of global BSS/DATA symbol names (for checking if RAM is linked)
|
||||
"""
|
||||
ram_symbols: list[tuple[str, int, str, bool]] = []
|
||||
global_ram_symbols: set[str] = set()
|
||||
|
||||
result = run_tool([self.nm_path, "--size-sort", str(lib_path)], timeout=10)
|
||||
if result is None:
|
||||
return ram_symbols, global_ram_symbols
|
||||
|
||||
for line in result.stdout.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
|
||||
try:
|
||||
size = int(parts[0], 16)
|
||||
sym_type = parts[1]
|
||||
name = parts[2]
|
||||
|
||||
# Only collect BSS (b/B) and DATA (d/D) for RAM analysis
|
||||
if sym_type in ("b", "B"):
|
||||
section = ".bss"
|
||||
is_local = sym_type == "b"
|
||||
ram_symbols.append((name, size, section, is_local))
|
||||
# Track global RAM symbols (B/D) for linking check
|
||||
if sym_type == "B":
|
||||
global_ram_symbols.add(name)
|
||||
elif sym_type in ("d", "D"):
|
||||
section = ".data"
|
||||
is_local = sym_type == "d"
|
||||
ram_symbols.append((name, size, section, is_local))
|
||||
if sym_type == "D":
|
||||
global_ram_symbols.add(name)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
return ram_symbols, global_ram_symbols
|
||||
|
||||
def _analyze_sdk_libraries(self) -> None:
|
||||
"""Analyze SDK libraries to find symbols not in the ELF.
|
||||
|
||||
This finds static/local symbols from closed-source SDK libraries
|
||||
that consume RAM but don't appear in the final ELF symbol table.
|
||||
Only includes symbols from libraries that have RAM actually linked
|
||||
(at least one global BSS/DATA symbol in the ELF).
|
||||
"""
|
||||
sdk_dirs = self._find_sdk_library_dirs()
|
||||
if not sdk_dirs:
|
||||
_LOGGER.debug("No SDK library directories found")
|
||||
return
|
||||
|
||||
_LOGGER.debug("Analyzing SDK libraries in %d directories", len(sdk_dirs))
|
||||
|
||||
# Track seen symbols to avoid duplicates from multiple SDK versions
|
||||
seen_symbols: set[str] = set()
|
||||
|
||||
for sdk_dir in sdk_dirs:
|
||||
for lib_path in sorted(sdk_dir.glob("*.a")):
|
||||
lib_name = lib_path.name
|
||||
ram_symbols, global_ram_symbols = self._parse_sdk_library(lib_path)
|
||||
|
||||
# Check if this library's RAM is actually linked by seeing if any
|
||||
# of its global BSS/DATA symbols appear in the ELF
|
||||
if not global_ram_symbols & self._elf_symbol_names:
|
||||
# No RAM from this library is in the ELF - skip it
|
||||
continue
|
||||
|
||||
for name, size, section, is_local in ram_symbols:
|
||||
# Skip if already in ELF or already seen from another lib
|
||||
if name in self._elf_symbol_names or name in seen_symbols:
|
||||
continue
|
||||
|
||||
# Only track symbols with non-zero size
|
||||
if size > 0:
|
||||
self._sdk_symbols.append(
|
||||
SDKSymbol(
|
||||
name=name,
|
||||
size=size,
|
||||
library=lib_name,
|
||||
section=section,
|
||||
is_local=is_local,
|
||||
)
|
||||
)
|
||||
seen_symbols.add(name)
|
||||
|
||||
# Demangle SDK symbols for better readability
|
||||
if self._sdk_symbols:
|
||||
sdk_names = [sym.name for sym in self._sdk_symbols]
|
||||
demangled_map = batch_demangle(sdk_names, objdump_path=self.objdump_path)
|
||||
for sym in self._sdk_symbols:
|
||||
sym.demangled = demangled_map.get(sym.name, sym.name)
|
||||
|
||||
# Sort by size descending for reporting
|
||||
self._sdk_symbols.sort(key=lambda s: s.size, reverse=True)
|
||||
|
||||
total_sdk_ram = sum(s.size for s in self._sdk_symbols)
|
||||
_LOGGER.debug(
|
||||
"Found %d SDK symbols not in ELF, totaling %d bytes",
|
||||
len(self._sdk_symbols),
|
||||
total_sdk_ram,
|
||||
)
|
||||
|
||||
def get_sdk_ram_symbols(self) -> list[SDKSymbol]:
|
||||
"""Get SDK symbols that consume RAM but aren't in the ELF symbol table.
|
||||
|
||||
Returns:
|
||||
List of SDKSymbol objects sorted by size descending.
|
||||
"""
|
||||
return self._sdk_symbols
|
||||
|
||||
def get_sdk_ram_by_library(self) -> dict[str, list[SDKSymbol]]:
|
||||
"""Get SDK RAM symbols grouped by library.
|
||||
|
||||
Returns:
|
||||
Dictionary mapping library name to list of symbols.
|
||||
"""
|
||||
by_lib: dict[str, list[SDKSymbol]] = defaultdict(list)
|
||||
for sym in self._sdk_symbols:
|
||||
by_lib[sym.library].append(sym)
|
||||
return dict(by_lib)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from .cli import main
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
"""CLI interface for memory analysis with report generation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import (
|
||||
_COMPONENT_API,
|
||||
_COMPONENT_CORE,
|
||||
_COMPONENT_PREFIX_ESPHOME,
|
||||
_COMPONENT_PREFIX_EXTERNAL,
|
||||
RAM_SECTIONS,
|
||||
MemoryAnalyzer,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ComponentMemory
|
||||
|
||||
|
||||
class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
"""Memory analyzer with CLI-specific report generation."""
|
||||
@@ -19,6 +27,8 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
SYMBOL_SIZE_THRESHOLD: int = (
|
||||
100 # Show symbols larger than this in detailed analysis
|
||||
)
|
||||
# Lower threshold for RAM symbols (RAM is more constrained)
|
||||
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
|
||||
|
||||
# Column width constants
|
||||
COL_COMPONENT: int = 29
|
||||
@@ -83,6 +93,60 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
COL_CORE_PERCENT,
|
||||
)
|
||||
|
||||
def _add_section_header(self, lines: list[str], title: str) -> None:
|
||||
"""Add a section header with title centered between separator lines."""
|
||||
lines.append("")
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append(title.center(self.TABLE_WIDTH))
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append("")
|
||||
|
||||
def _add_top_consumers(
|
||||
self,
|
||||
lines: list[str],
|
||||
title: str,
|
||||
components: list[tuple[str, ComponentMemory]],
|
||||
get_size: Callable[[ComponentMemory], int],
|
||||
total: int,
|
||||
memory_type: str,
|
||||
limit: int = 25,
|
||||
) -> None:
|
||||
"""Add a formatted list of top memory consumers to the report.
|
||||
|
||||
Args:
|
||||
lines: List of report lines to append the output to.
|
||||
title: Section title to print before the list.
|
||||
components: Sequence of (name, ComponentMemory) tuples to analyze.
|
||||
get_size: Callable that takes a ComponentMemory and returns the
|
||||
size in bytes to use for ranking and display.
|
||||
total: Total size in bytes for computing percentage usage.
|
||||
memory_type: Label for the memory region (e.g., "flash" or "RAM").
|
||||
limit: Maximum number of components to include in the list.
|
||||
"""
|
||||
lines.append("")
|
||||
lines.append(f"{title}:")
|
||||
for i, (name, mem) in enumerate(components[:limit]):
|
||||
size = get_size(mem)
|
||||
if size > 0:
|
||||
percentage = (size / total * 100) if total > 0 else 0
|
||||
lines.append(
|
||||
f"{i + 1}. {name} ({size:,} B) - {percentage:.1f}% of analyzed {memory_type}"
|
||||
)
|
||||
|
||||
def _format_symbol_with_section(
|
||||
self, demangled: str, size: int, section: str | None = None
|
||||
) -> str:
|
||||
"""Format a symbol entry, optionally adding a RAM section label.
|
||||
|
||||
If section is one of the RAM sections (.data or .bss), a label like
|
||||
" [data]" or " [bss]" is appended. For non-RAM sections or when
|
||||
section is None, no section label is added.
|
||||
"""
|
||||
section_label = ""
|
||||
if section in RAM_SECTIONS:
|
||||
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
|
||||
return f"{demangled} ({size:,} B){section_label}"
|
||||
|
||||
def generate_report(self, detailed: bool = False) -> str:
|
||||
"""Generate a formatted memory report."""
|
||||
components = sorted(
|
||||
@@ -123,43 +187,70 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B"
|
||||
)
|
||||
|
||||
# Top consumers
|
||||
lines.append("")
|
||||
lines.append("Top Flash Consumers:")
|
||||
for i, (name, mem) in enumerate(components[:25]):
|
||||
if mem.flash_total > 0:
|
||||
percentage = (
|
||||
(mem.flash_total / total_flash * 100) if total_flash > 0 else 0
|
||||
)
|
||||
lines.append(
|
||||
f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append("Top RAM Consumers:")
|
||||
ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True)
|
||||
for i, (name, mem) in enumerate(ram_components[:25]):
|
||||
if mem.ram_total > 0:
|
||||
percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0
|
||||
lines.append(
|
||||
f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM"
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
lines.append(
|
||||
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
|
||||
# Show unattributed RAM (SDK/framework overhead)
|
||||
unattributed_bss, unattributed_data, unattributed_total = (
|
||||
self.get_unattributed_ram()
|
||||
)
|
||||
if unattributed_total > 0:
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"Unattributed RAM: {unattributed_total:,} B (SDK/framework overhead)"
|
||||
)
|
||||
if unattributed_bss > 0 and unattributed_data > 0:
|
||||
lines.append(
|
||||
f" .bss: {unattributed_bss:,} B | .data: {unattributed_data:,} B"
|
||||
)
|
||||
|
||||
# Show SDK symbol breakdown if available
|
||||
sdk_by_lib = self.get_sdk_ram_by_library()
|
||||
if sdk_by_lib:
|
||||
lines.append("")
|
||||
lines.append("SDK library breakdown (static symbols not in ELF):")
|
||||
# Sort libraries by total size
|
||||
lib_totals = [
|
||||
(lib, sum(s.size for s in syms), syms)
|
||||
for lib, syms in sdk_by_lib.items()
|
||||
]
|
||||
lib_totals.sort(key=lambda x: x[1], reverse=True)
|
||||
|
||||
for lib_name, lib_total, syms in lib_totals:
|
||||
if lib_total == 0:
|
||||
continue
|
||||
lines.append(f" {lib_name}: {lib_total:,} B")
|
||||
# Show top symbols from this library
|
||||
for sym in sorted(syms, key=lambda s: s.size, reverse=True)[:3]:
|
||||
section_label = sym.section.lstrip(".")
|
||||
# Use demangled name (falls back to original if not demangled)
|
||||
display_name = sym.demangled or sym.name
|
||||
if len(display_name) > 50:
|
||||
display_name = f"{display_name[:47]}..."
|
||||
lines.append(
|
||||
f" {sym.size:>6,} B [{section_label}] {display_name}"
|
||||
)
|
||||
|
||||
# Top consumers
|
||||
self._add_top_consumers(
|
||||
lines,
|
||||
"Top Flash Consumers",
|
||||
components,
|
||||
lambda m: m.flash_total,
|
||||
total_flash,
|
||||
"flash",
|
||||
)
|
||||
|
||||
ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True)
|
||||
self._add_top_consumers(
|
||||
lines,
|
||||
"Top RAM Consumers",
|
||||
ram_components,
|
||||
lambda m: m.ram_total,
|
||||
total_ram,
|
||||
"RAM",
|
||||
)
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
|
||||
# Add ESPHome core detailed analysis if there are core symbols
|
||||
if self._esphome_core_symbols:
|
||||
lines.append("")
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append(
|
||||
f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH)
|
||||
)
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append("")
|
||||
self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis")
|
||||
|
||||
# Group core symbols by subcategory
|
||||
core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict(
|
||||
@@ -211,7 +302,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
|
||||
)
|
||||
for i, (symbol, demangled, size) in enumerate(large_core_symbols):
|
||||
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
|
||||
# Core symbols only track (symbol, demangled, size) without section info,
|
||||
# so we don't show section labels here
|
||||
lines.append(
|
||||
f"{i + 1}. {self._format_symbol_with_section(demangled, size)}"
|
||||
)
|
||||
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
|
||||
@@ -267,11 +362,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
for comp_name, comp_mem in components_to_analyze:
|
||||
if not (comp_symbols := self._component_symbols.get(comp_name, [])):
|
||||
continue
|
||||
lines.append("")
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH))
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
lines.append("")
|
||||
self._add_section_header(lines, f"{comp_name} Detailed Analysis")
|
||||
|
||||
# Sort symbols by size
|
||||
sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True)
|
||||
@@ -282,19 +373,69 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
|
||||
# Show all symbols above threshold for better visibility
|
||||
large_symbols = [
|
||||
(sym, dem, size)
|
||||
for sym, dem, size in sorted_symbols
|
||||
(sym, dem, size, sec)
|
||||
for sym, dem, size, sec in sorted_symbols
|
||||
if size > self.SYMBOL_SIZE_THRESHOLD
|
||||
]
|
||||
|
||||
lines.append(
|
||||
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):"
|
||||
)
|
||||
for i, (symbol, demangled, size) in enumerate(large_symbols):
|
||||
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
|
||||
for i, (symbol, demangled, size, section) in enumerate(large_symbols):
|
||||
lines.append(
|
||||
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
|
||||
)
|
||||
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
|
||||
# Detailed RAM analysis by component (at end, before RAM strings analysis)
|
||||
self._add_section_header(lines, "RAM Symbol Analysis by Component")
|
||||
|
||||
# Show top 15 RAM consumers with their large symbols
|
||||
for name, mem in ram_components[:15]:
|
||||
if mem.ram_total == 0:
|
||||
continue
|
||||
ram_syms = self._ram_symbols.get(name, [])
|
||||
if not ram_syms:
|
||||
continue
|
||||
|
||||
# Sort by size descending
|
||||
sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True)
|
||||
large_ram_syms = [
|
||||
s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD
|
||||
]
|
||||
|
||||
lines.append(f"{name} ({mem.ram_total:,} B total RAM):")
|
||||
|
||||
# Show breakdown by section type
|
||||
data_size = sum(s[2] for s in ram_syms if s[3] == ".data")
|
||||
bss_size = sum(s[2] for s in ram_syms if s[3] == ".bss")
|
||||
lines.append(f" .data (initialized): {data_size:,} B")
|
||||
lines.append(f" .bss (uninitialized): {bss_size:,} B")
|
||||
|
||||
if large_ram_syms:
|
||||
lines.append(
|
||||
f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):"
|
||||
)
|
||||
for symbol, demangled, size, section in large_ram_syms[:10]:
|
||||
# Format section label consistently by stripping leading dot
|
||||
section_label = section.lstrip(".") if section else ""
|
||||
# Add ellipsis if name is truncated
|
||||
demangled_display = (
|
||||
f"{demangled[:70]}..." if len(demangled) > 70 else demangled
|
||||
)
|
||||
lines.append(
|
||||
f" {size:>6,} B [{section_label}] {demangled_display}"
|
||||
)
|
||||
if len(large_ram_syms) > 10:
|
||||
lines.append(f" ... and {len(large_ram_syms) - 10} more")
|
||||
lines.append("")
|
||||
|
||||
lines.append(
|
||||
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
|
||||
)
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
|
||||
|
||||
@@ -7,11 +7,13 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
|
||||
|
||||
# Section mapping for ELF file sections
|
||||
# Maps standard section names to their various platform-specific variants
|
||||
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
|
||||
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
|
||||
SECTION_MAPPING = {
|
||||
".text": frozenset([".text", ".iram"]),
|
||||
".rodata": frozenset([".rodata"]),
|
||||
".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss"
|
||||
".data": frozenset([".data", ".dram"]),
|
||||
".bss": frozenset([".bss"]),
|
||||
}
|
||||
|
||||
# Section to ComponentMemory attribute mapping
|
||||
@@ -88,6 +90,77 @@ SYMBOL_PATTERNS = {
|
||||
"sys_mbox_new",
|
||||
"sys_arch_mbox_tryfetch",
|
||||
],
|
||||
# LibreTiny/Beken BK7231 radio calibration
|
||||
"bk_radio_cal": [
|
||||
"bk7011_",
|
||||
"calibration_main",
|
||||
"gcali_",
|
||||
"rwnx_cal",
|
||||
],
|
||||
# LibreTiny/Beken WiFi MAC layer
|
||||
"bk_wifi_mac": [
|
||||
"rxu_", # RX upper layer
|
||||
"txu_", # TX upper layer
|
||||
"txl_", # TX lower layer
|
||||
"rxl_", # RX lower layer
|
||||
"scanu_", # Scan unit
|
||||
"mm_hw_", # MAC management hardware
|
||||
"mm_bcn", # MAC management beacon
|
||||
"mm_tim", # MAC management TIM
|
||||
"mm_check", # MAC management checks
|
||||
"sm_connect", # Station management
|
||||
"me_beacon", # Management entity beacon
|
||||
"me_build", # Management entity build
|
||||
"hapd_", # Host AP daemon
|
||||
"chan_pre_", # Channel management
|
||||
"handle_probe_", # Probe handling
|
||||
],
|
||||
# LibreTiny/Beken system control
|
||||
"bk_system": [
|
||||
"sctrl_", # System control
|
||||
"icu_ctrl", # Interrupt control unit
|
||||
"gdma_ctrl", # DMA control
|
||||
"mpb_ctrl", # MPB control
|
||||
"uf2_", # UF2 OTA
|
||||
"bkreg_", # Beken registers
|
||||
],
|
||||
# LibreTiny/Beken BLE stack
|
||||
"bk_ble": [
|
||||
"gapc_", # GAP client
|
||||
"gattc_", # GATT client
|
||||
"attc_", # ATT client
|
||||
"attmdb_", # ATT database
|
||||
"atts_", # ATT server
|
||||
"l2cc_", # L2CAP
|
||||
"prf_env", # Profile environment
|
||||
],
|
||||
# LibreTiny/Beken scheduler
|
||||
"bk_scheduler": [
|
||||
"sch_plan_", # Scheduler plan
|
||||
"sch_prog_", # Scheduler program
|
||||
"sch_arb_", # Scheduler arbiter
|
||||
],
|
||||
# LibreTiny/Beken DMA descriptors
|
||||
"bk_dma": [
|
||||
"rx_payload_desc",
|
||||
"rx_dma_hdrdesc",
|
||||
"tx_hw_desc",
|
||||
"host_event_data",
|
||||
"host_cmd_data",
|
||||
],
|
||||
# ARM EABI compiler runtime (LibreTiny uses ARM Cortex-M)
|
||||
"arm_runtime": [
|
||||
"__aeabi_",
|
||||
"__adddf3",
|
||||
"__subdf3",
|
||||
"__muldf3",
|
||||
"__divdf3",
|
||||
"__addsf3",
|
||||
"__subsf3",
|
||||
"__mulsf3",
|
||||
"__divsf3",
|
||||
"__gnu_unwind",
|
||||
],
|
||||
"xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"],
|
||||
"heap": ["heap_", "multi_heap"],
|
||||
"spi_flash": ["spi_flash"],
|
||||
@@ -782,7 +855,22 @@ SYMBOL_PATTERNS = {
|
||||
"math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"],
|
||||
"character_class": ["__chclass"],
|
||||
"camellia": ["camellia_", "camellia_feistel"],
|
||||
"crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"],
|
||||
"crypto_tables": [
|
||||
"FSb",
|
||||
"FSb2",
|
||||
"FSb3",
|
||||
"FSb4",
|
||||
"Te0", # AES encryption table
|
||||
"Td0", # AES decryption table
|
||||
"crc32_table", # CRC32 lookup table
|
||||
"crc_tab", # CRC lookup table
|
||||
],
|
||||
"crypto_hash": [
|
||||
"SHA1Transform", # SHA1 hash function
|
||||
"MD5Transform", # MD5 hash function
|
||||
"SHA256",
|
||||
"SHA512",
|
||||
],
|
||||
"event_buffer": ["g_eb_list_desc", "eb_space"],
|
||||
"base_node": ["base_node_", "base_node_add_handler"],
|
||||
"file_descriptor": ["s_fd_table"],
|
||||
|
||||
@@ -5,6 +5,10 @@ from __future__ import annotations
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -55,3 +59,35 @@ def find_tool(
|
||||
|
||||
_LOGGER.warning("Could not find %s tool", tool_name)
|
||||
return None
|
||||
|
||||
|
||||
def run_tool(
|
||||
cmd: Sequence[str],
|
||||
timeout: int = 30,
|
||||
) -> subprocess.CompletedProcess[str] | None:
|
||||
"""Run a toolchain command and return the result.
|
||||
|
||||
Args:
|
||||
cmd: Command and arguments to run
|
||||
timeout: Timeout in seconds
|
||||
|
||||
Returns:
|
||||
CompletedProcess on success, None on failure
|
||||
"""
|
||||
try:
|
||||
return subprocess.run(
|
||||
cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=timeout,
|
||||
check=False,
|
||||
)
|
||||
except subprocess.TimeoutExpired:
|
||||
_LOGGER.warning("Command timed out: %s", " ".join(cmd))
|
||||
return None
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning("Command not found: %s", cmd[0])
|
||||
return None
|
||||
except OSError as e:
|
||||
_LOGGER.warning("Failed to run command %s: %s", cmd[0], e)
|
||||
return None
|
||||
|
||||
@@ -30,7 +30,9 @@ void A01nyubComponent::check_buffer_() {
|
||||
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
|
||||
this->publish_state(meters);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
|
||||
char hex_buf[format_hex_pretty_size(4)];
|
||||
ESP_LOGW(TAG, "Invalid data read from sensor: %s",
|
||||
format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size()));
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);
|
||||
|
||||
@@ -29,7 +29,9 @@ void A02yyuwComponent::check_buffer_() {
|
||||
ESP_LOGV(TAG, "Distance from sensor: %f mm", distance);
|
||||
this->publish_state(distance);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
|
||||
char hex_buf[format_hex_pretty_size(4)];
|
||||
ESP_LOGW(TAG, "Invalid data read from sensor: %s",
|
||||
format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size()));
|
||||
}
|
||||
} else {
|
||||
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);
|
||||
|
||||
@@ -90,13 +90,16 @@ void AbsoluteHumidityComponent::loop() {
|
||||
this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
|
||||
|
||||
// Calculate absolute humidity
|
||||
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);
|
||||
|
||||
// Publish absolute humidity
|
||||
ESP_LOGD(TAG, "Publishing absolute humidity %f g/m³", absolute_humidity);
|
||||
this->status_clear_warning();
|
||||
this->publish_state(absolute_humidity);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "ac_dimmer.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -9,12 +7,12 @@
|
||||
#ifdef USE_ESP8266
|
||||
#include <core_esp8266_waveform.h>
|
||||
#endif
|
||||
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
|
||||
#include <esp32-hal-timer.h>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include "hw_timer_esp_idf.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace ac_dimmer {
|
||||
namespace esphome::ac_dimmer {
|
||||
|
||||
static const char *const TAG = "ac_dimmer";
|
||||
|
||||
@@ -27,7 +25,14 @@ static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-no
|
||||
/// However other factors like gate driver propagation time
|
||||
/// are also considered and a really low value is not important
|
||||
/// See also: https://github.com/esphome/issues/issues/1632
|
||||
static const uint32_t GATE_ENABLE_TIME = 50;
|
||||
static constexpr uint32_t GATE_ENABLE_TIME = 50;
|
||||
|
||||
#ifdef USE_ESP32
|
||||
/// Timer frequency in Hz (1 MHz = 1µs resolution)
|
||||
static constexpr uint32_t TIMER_FREQUENCY_HZ = 1000000;
|
||||
/// Timer interrupt interval in microseconds
|
||||
static constexpr uint64_t TIMER_INTERVAL_US = 50;
|
||||
#endif
|
||||
|
||||
/// Function called from timer interrupt
|
||||
/// Input is current time in microseconds (micros())
|
||||
@@ -154,7 +159,7 @@ void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) {
|
||||
#ifdef USE_ESP32
|
||||
// ESP32 implementation, uses basically the same code but needs to wrap
|
||||
// timer_interrupt() function to auto-reschedule
|
||||
static hw_timer_t *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static HWTimer *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); }
|
||||
#endif
|
||||
|
||||
@@ -194,15 +199,15 @@ void AcDimmer::setup() {
|
||||
setTimer1Callback(&timer_interrupt);
|
||||
#endif
|
||||
#ifdef USE_ESP32
|
||||
// timer frequency of 1mhz
|
||||
dimmer_timer = timerBegin(1000000);
|
||||
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
|
||||
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.
|
||||
timerAlarm(dimmer_timer, 50, true, 0);
|
||||
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
|
||||
#endif
|
||||
}
|
||||
|
||||
void AcDimmer::write_state(float state) {
|
||||
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
|
||||
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
|
||||
@@ -210,14 +215,15 @@ void AcDimmer::write_state(float state) {
|
||||
this->store_.init_cycle = this->init_with_half_cycle_;
|
||||
this->store_.value = new_value;
|
||||
}
|
||||
|
||||
void AcDimmer::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AcDimmer:");
|
||||
LOG_PIN(" Output Pin: ", this->gate_pin_);
|
||||
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"AcDimmer:\n"
|
||||
" Min Power: %.1f%%\n"
|
||||
" Init with half cycle: %s",
|
||||
this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_));
|
||||
LOG_PIN(" Output Pin: ", this->gate_pin_);
|
||||
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
|
||||
if (method_ == DIM_METHOD_LEADING_PULSE) {
|
||||
ESP_LOGCONFIG(TAG, " Method: leading pulse");
|
||||
} else if (method_ == DIM_METHOD_LEADING) {
|
||||
@@ -230,7 +236,4 @@ void AcDimmer::dump_config() {
|
||||
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
|
||||
}
|
||||
|
||||
} // namespace ac_dimmer
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
||||
} // namespace esphome::ac_dimmer
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ac_dimmer {
|
||||
namespace esphome::ac_dimmer {
|
||||
|
||||
enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING };
|
||||
|
||||
@@ -64,7 +61,4 @@ class AcDimmer : public output::FloatOutput, public Component {
|
||||
DimMethod method_;
|
||||
};
|
||||
|
||||
} // namespace ac_dimmer
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ARDUINO
|
||||
} // namespace esphome::ac_dimmer
|
||||
|
||||
152
esphome/components/ac_dimmer/hw_timer_esp_idf.cpp
Normal file
152
esphome/components/ac_dimmer/hw_timer_esp_idf.cpp
Normal file
@@ -0,0 +1,152 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "hw_timer_esp_idf.h"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "driver/gptimer.h"
|
||||
#include "esp_clk_tree.h"
|
||||
#include "soc/clk_tree_defs.h"
|
||||
|
||||
static const char *const TAG = "hw_timer_esp_idf";
|
||||
|
||||
namespace esphome::ac_dimmer {
|
||||
|
||||
// GPTimer divider constraints from ESP-IDF documentation
|
||||
static constexpr uint32_t GPTIMER_DIVIDER_MIN = 2;
|
||||
static constexpr uint32_t GPTIMER_DIVIDER_MAX = 65536;
|
||||
|
||||
using voidFuncPtr = void (*)();
|
||||
using voidFuncPtrArg = void (*)(void *);
|
||||
|
||||
struct InterruptConfigT {
|
||||
voidFuncPtr fn{nullptr};
|
||||
void *arg{nullptr};
|
||||
};
|
||||
|
||||
struct HWTimer {
|
||||
gptimer_handle_t timer_handle{nullptr};
|
||||
InterruptConfigT interrupt_handle{};
|
||||
bool timer_started{false};
|
||||
};
|
||||
|
||||
HWTimer *timer_begin(uint32_t frequency) {
|
||||
esp_err_t err = ESP_OK;
|
||||
uint32_t counter_src_hz = 0;
|
||||
uint32_t divider = 0;
|
||||
soc_module_clk_t clk;
|
||||
for (auto clk_candidate : SOC_GPTIMER_CLKS) {
|
||||
clk = clk_candidate;
|
||||
esp_clk_tree_src_get_freq_hz(clk, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &counter_src_hz);
|
||||
divider = counter_src_hz / frequency;
|
||||
if ((divider >= GPTIMER_DIVIDER_MIN) && (divider <= GPTIMER_DIVIDER_MAX)) {
|
||||
break;
|
||||
} else {
|
||||
divider = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (divider == 0) {
|
||||
ESP_LOGE(TAG, "Resolution not possible; aborting");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
gptimer_config_t config = {
|
||||
.clk_src = static_cast<gptimer_clock_source_t>(clk),
|
||||
.direction = GPTIMER_COUNT_UP,
|
||||
.resolution_hz = frequency,
|
||||
.flags = {.intr_shared = true},
|
||||
};
|
||||
|
||||
HWTimer *timer = new HWTimer();
|
||||
|
||||
err = gptimer_new_timer(&config, &timer->timer_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "GPTimer creation failed; error %d", err);
|
||||
delete timer;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
err = gptimer_enable(timer->timer_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "GPTimer enable failed; error %d", err);
|
||||
gptimer_del_timer(timer->timer_handle);
|
||||
delete timer;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
err = gptimer_start(timer->timer_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "GPTimer start failed; error %d", err);
|
||||
gptimer_disable(timer->timer_handle);
|
||||
gptimer_del_timer(timer->timer_handle);
|
||||
delete timer;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
timer->timer_started = true;
|
||||
return timer;
|
||||
}
|
||||
|
||||
bool IRAM_ATTR timer_fn_wrapper(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *args) {
|
||||
auto *isr = static_cast<InterruptConfigT *>(args);
|
||||
if (isr->fn) {
|
||||
if (isr->arg) {
|
||||
reinterpret_cast<voidFuncPtrArg>(isr->fn)(isr->arg);
|
||||
} else {
|
||||
isr->fn();
|
||||
}
|
||||
}
|
||||
// Return false to indicate that no higher-priority task was woken and no context switch is requested.
|
||||
return false;
|
||||
}
|
||||
|
||||
static void timer_attach_interrupt_functional_arg(HWTimer *timer, void (*user_func)(void *), void *arg) {
|
||||
if (timer == nullptr) {
|
||||
ESP_LOGE(TAG, "Timer handle is nullptr");
|
||||
return;
|
||||
}
|
||||
gptimer_event_callbacks_t cbs = {
|
||||
.on_alarm = timer_fn_wrapper,
|
||||
};
|
||||
|
||||
timer->interrupt_handle.fn = reinterpret_cast<voidFuncPtr>(user_func);
|
||||
timer->interrupt_handle.arg = arg;
|
||||
|
||||
if (timer->timer_started) {
|
||||
gptimer_stop(timer->timer_handle);
|
||||
}
|
||||
gptimer_disable(timer->timer_handle);
|
||||
esp_err_t err = gptimer_register_event_callbacks(timer->timer_handle, &cbs, &timer->interrupt_handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Timer Attach Interrupt failed; error %d", err);
|
||||
}
|
||||
gptimer_enable(timer->timer_handle);
|
||||
if (timer->timer_started) {
|
||||
gptimer_start(timer->timer_handle);
|
||||
}
|
||||
}
|
||||
|
||||
void timer_attach_interrupt(HWTimer *timer, voidFuncPtr user_func) {
|
||||
timer_attach_interrupt_functional_arg(timer, reinterpret_cast<voidFuncPtrArg>(user_func), nullptr);
|
||||
}
|
||||
|
||||
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count) {
|
||||
if (timer == nullptr) {
|
||||
ESP_LOGE(TAG, "Timer handle is nullptr");
|
||||
return;
|
||||
}
|
||||
gptimer_alarm_config_t alarm_cfg = {
|
||||
.alarm_count = alarm_value,
|
||||
.reload_count = reload_count,
|
||||
.flags = {.auto_reload_on_alarm = autoreload},
|
||||
};
|
||||
esp_err_t err = gptimer_set_alarm_action(timer->timer_handle, &alarm_cfg);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Timer Alarm Write failed; error %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ac_dimmer
|
||||
#endif
|
||||
17
esphome/components/ac_dimmer/hw_timer_esp_idf.h
Normal file
17
esphome/components/ac_dimmer/hw_timer_esp_idf.h
Normal file
@@ -0,0 +1,17 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "driver/gptimer_types.h"
|
||||
|
||||
namespace esphome::ac_dimmer {
|
||||
|
||||
struct HWTimer;
|
||||
|
||||
HWTimer *timer_begin(uint32_t frequency);
|
||||
|
||||
void timer_attach_interrupt(HWTimer *timer, void (*user_func)());
|
||||
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count);
|
||||
|
||||
} // namespace esphome::ac_dimmer
|
||||
|
||||
#endif
|
||||
@@ -3,6 +3,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import output
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@glmnet"]
|
||||
|
||||
@@ -31,11 +32,16 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_with_arduino,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
if CORE.is_esp8266:
|
||||
# ac_dimmer uses setTimer1Callback which requires the waveform generator
|
||||
from esphome.components.esp8266.const import require_waveform
|
||||
|
||||
require_waveform()
|
||||
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
|
||||
@@ -121,23 +121,21 @@ void ADCSensor::setup() {
|
||||
void ADCSensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC Sensor", this);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Channel: %d\n"
|
||||
" Unit: %s\n"
|
||||
" Attenuation: %s\n"
|
||||
" Samples: %i\n"
|
||||
" Sampling mode: %s",
|
||||
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
|
||||
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
|
||||
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
|
||||
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
" Channel: %d\n"
|
||||
" Unit: %s\n"
|
||||
" Attenuation: %s\n"
|
||||
" Samples: %i\n"
|
||||
" Sampling mode: %s\n"
|
||||
" Setup Status:\n"
|
||||
" Handle Init: %s\n"
|
||||
" Config: %s\n"
|
||||
" Calibration: %s\n"
|
||||
" Overall Init: %s",
|
||||
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
|
||||
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
|
||||
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
|
||||
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
|
||||
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
|
||||
|
||||
|
||||
@@ -25,11 +25,13 @@ class AddressableLightDisplay : public display::DisplayBuffer {
|
||||
if (enabled_ && !enabled) { // enabled -> disabled
|
||||
// - Tell the parent light to refresh, effectively wiping the display. Also
|
||||
// restores the previous effect (if any).
|
||||
light_state_->make_call().set_effect(this->last_effect_).perform();
|
||||
if (this->last_effect_index_.has_value()) {
|
||||
light_state_->make_call().set_effect(*this->last_effect_index_).perform();
|
||||
}
|
||||
|
||||
} else if (!enabled_ && enabled) { // disabled -> enabled
|
||||
// - Save the current effect.
|
||||
this->last_effect_ = light_state_->get_effect_name();
|
||||
// - 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();
|
||||
}
|
||||
@@ -56,7 +58,7 @@ class AddressableLightDisplay : public display::DisplayBuffer {
|
||||
int32_t width_;
|
||||
int32_t height_;
|
||||
std::vector<Color> addressable_light_buffer_;
|
||||
optional<std::string> last_effect_;
|
||||
optional<uint32_t> last_effect_index_;
|
||||
optional<std::function<int(int, int)>> pixel_mapper_f_;
|
||||
};
|
||||
} // namespace addressable_light
|
||||
|
||||
@@ -162,11 +162,13 @@ void ADE7880::update() {
|
||||
}
|
||||
|
||||
void ADE7880::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ADE7880:");
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ADE7880:\n"
|
||||
" Frequency: %.0f Hz",
|
||||
this->frequency_);
|
||||
LOG_PIN(" IRQ0 Pin: ", this->irq0_pin_);
|
||||
LOG_PIN(" IRQ1 Pin: ", this->irq1_pin_);
|
||||
LOG_PIN(" RESET Pin: ", this->reset_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Frequency: %.0f Hz", this->frequency_);
|
||||
|
||||
if (this->channel_a_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " Phase A:");
|
||||
|
||||
@@ -21,10 +21,12 @@ void ADS1115Sensor::update() {
|
||||
|
||||
void ADS1115Sensor::dump_config() {
|
||||
LOG_SENSOR(" ", "ADS1115 Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_);
|
||||
ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_);
|
||||
ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_);
|
||||
ESP_LOGCONFIG(TAG, " Sample rate: %u", this->samplerate_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Multiplexer: %u\n"
|
||||
" Gain: %u\n"
|
||||
" Resolution: %u\n"
|
||||
" Sample rate: %u",
|
||||
this->multiplexer_, this->gain_, this->resolution_, this->samplerate_);
|
||||
}
|
||||
|
||||
} // namespace ads1115
|
||||
|
||||
@@ -9,8 +9,10 @@ static const char *const TAG = "ads1118.sensor";
|
||||
|
||||
void ADS1118Sensor::dump_config() {
|
||||
LOG_SENSOR(" ", "ADS1118 Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_);
|
||||
ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Multiplexer: %u\n"
|
||||
" Gain: %u",
|
||||
this->multiplexer_, this->gain_);
|
||||
}
|
||||
|
||||
float ADS1118Sensor::sample() {
|
||||
|
||||
@@ -20,7 +20,8 @@ bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &devic
|
||||
sn |= ((uint32_t) it.data[2] << 16);
|
||||
sn |= ((uint32_t) it.data[3] << 24);
|
||||
|
||||
ESP_LOGD(TAG, "Found AirThings device Serial:%" PRIu32 " (MAC: %s)", sn, device.address_str().c_str());
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGD(TAG, "Found AirThings device Serial:%" PRIu32 " (MAC: %s)", sn, device.address_str_to(addr_buf));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#include "airthings_wave_base.h"
|
||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
||||
|
||||
// All information related to reading battery information came from the sensors.airthings_wave
|
||||
// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave)
|
||||
@@ -93,8 +94,10 @@ void AirthingsWaveBase::update() {
|
||||
bool AirthingsWaveBase::request_read_values_() {
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
|
||||
this->sensors_data_characteristic_uuid_.to_string().c_str());
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_str(service_buf),
|
||||
this->sensors_data_characteristic_uuid_.to_str(char_buf));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -117,17 +120,20 @@ bool AirthingsWaveBase::request_battery_() {
|
||||
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_);
|
||||
if (chr == nullptr) {
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "No access control point characteristic found at service %s char %s",
|
||||
this->service_uuid_.to_string().c_str(),
|
||||
this->access_control_point_characteristic_uuid_.to_string().c_str());
|
||||
this->service_uuid_.to_str(service_buf), this->access_control_point_characteristic_uuid_.to_str(char_buf));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto *descr = this->parent()->get_descriptor(this->service_uuid_, this->access_control_point_characteristic_uuid_,
|
||||
CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID);
|
||||
if (descr == nullptr) {
|
||||
ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_string().c_str(),
|
||||
this->access_control_point_characteristic_uuid_.to_string().c_str());
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_str(service_buf),
|
||||
this->access_control_point_characteristic_uuid_.to_str(char_buf));
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,8 @@ namespace alpha3 {
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d);
|
||||
static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID =
|
||||
espbt::ESPBTUUID::from_raw({static_cast<char>(0xa9), 0x7b, static_cast<char>(0xb8), static_cast<char>(0x85), 0x0,
|
||||
0x1a, 0x28, static_cast<char>(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast<char>(0xd1),
|
||||
static_cast<char>(0xff), static_cast<char>(0x9c), static_cast<char>(0x85)});
|
||||
static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID = espbt::ESPBTUUID::from_raw(
|
||||
{0xa9, 0x7b, 0xb8, 0x85, 0x00, 0x1a, 0x28, 0xaa, 0x2a, 0x43, 0x6e, 0x03, 0xd1, 0xff, 0x9c, 0x85});
|
||||
static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13;
|
||||
static const size_t GENI_RESPONSE_TYPE_LENGTH = 8;
|
||||
|
||||
|
||||
@@ -67,8 +67,10 @@ 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..?", this->get_name().c_str());
|
||||
ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str());
|
||||
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());
|
||||
break;
|
||||
}
|
||||
this->char_handle_ = chr->handle;
|
||||
|
||||
@@ -226,32 +226,6 @@ def _encryption_schema(config):
|
||||
return ENCRYPTION_SCHEMA(config)
|
||||
|
||||
|
||||
def _validate_api_config(config: ConfigType) -> ConfigType:
|
||||
"""Validate API configuration with mutual exclusivity check and deprecation warning."""
|
||||
# Check if both password and encryption are configured
|
||||
has_password = CONF_PASSWORD in config and config[CONF_PASSWORD]
|
||||
has_encryption = CONF_ENCRYPTION in config
|
||||
|
||||
if has_password and has_encryption:
|
||||
raise cv.Invalid(
|
||||
"The 'password' and 'encryption' options are mutually exclusive. "
|
||||
"The API client only supports one authentication method at a time. "
|
||||
"Please remove one of them. "
|
||||
"Note: 'password' authentication is deprecated and will be removed in version 2026.1.0. "
|
||||
"We strongly recommend using 'encryption' instead for better security."
|
||||
)
|
||||
|
||||
# Warn about password deprecation
|
||||
if has_password:
|
||||
_LOGGER.warning(
|
||||
"API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
|
||||
"Please migrate to the 'encryption' configuration. "
|
||||
"See https://esphome.io/components/api/#configuration-variables"
|
||||
)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def _consume_api_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for API component."""
|
||||
from esphome.components import socket
|
||||
@@ -268,7 +242,17 @@ CONFIG_SCHEMA = cv.All(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(APIServer),
|
||||
cv.Optional(CONF_PORT, default=6053): cv.port,
|
||||
cv.Optional(CONF_PASSWORD, default=""): cv.string_strict,
|
||||
# Removed in 2026.1.0 - kept to provide helpful error message
|
||||
cv.Optional(CONF_PASSWORD): cv.invalid(
|
||||
"The 'password' option has been removed in ESPHome 2026.1.0.\n"
|
||||
"Password authentication was deprecated in May 2022.\n"
|
||||
"Please migrate to encryption for secure API communication:\n\n"
|
||||
"api:\n"
|
||||
" encryption:\n"
|
||||
" key: !secret api_encryption_key\n\n"
|
||||
"Generate a key with: openssl rand -base64 32\n"
|
||||
"Or visit https://esphome.io/components/api/#configuration-variables"
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_REBOOT_TIMEOUT, default="15min"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
@@ -330,7 +314,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
||||
_validate_api_config,
|
||||
_consume_api_sockets,
|
||||
)
|
||||
|
||||
@@ -344,9 +327,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
CORE.register_controller()
|
||||
|
||||
cg.add(var.set_port(config[CONF_PORT]))
|
||||
if config[CONF_PASSWORD]:
|
||||
cg.add_define("USE_API_PASSWORD")
|
||||
cg.add(var.set_password(config[CONF_PASSWORD]))
|
||||
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
|
||||
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
|
||||
if CONF_LISTEN_BACKLOG in config:
|
||||
|
||||
@@ -7,10 +7,7 @@ service APIConnection {
|
||||
option (needs_setup_connection) = false;
|
||||
option (needs_authentication) = false;
|
||||
}
|
||||
rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) {
|
||||
option (needs_setup_connection) = false;
|
||||
option (needs_authentication) = false;
|
||||
}
|
||||
// REMOVED in ESPHome 2026.1.0: rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse)
|
||||
rpc disconnect (DisconnectRequest) returns (DisconnectResponse) {
|
||||
option (needs_setup_connection) = false;
|
||||
option (needs_authentication) = false;
|
||||
@@ -69,6 +66,8 @@ service APIConnection {
|
||||
|
||||
rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
|
||||
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
|
||||
|
||||
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,14 +81,13 @@ service APIConnection {
|
||||
// * VarInt denoting the type of message.
|
||||
// * The message object encoded as a ProtoBuf message
|
||||
|
||||
// The connection is established in 4 steps:
|
||||
// The connection is established in 2 steps:
|
||||
// * First, the client connects to the server and sends a "Hello Request" identifying itself
|
||||
// * The server responds with a "Hello Response" and selects the protocol version
|
||||
// * After receiving this message, the client attempts to authenticate itself using
|
||||
// the password and a "Connect Request"
|
||||
// * The server responds with a "Connect Response" and notifies of invalid password.
|
||||
// * The server responds with a "Hello Response" and the connection is authenticated
|
||||
// If anything in this initial process fails, the connection must immediately closed
|
||||
// by both sides and _no_ disconnection message is to be sent.
|
||||
// Note: Password authentication via AuthenticationRequest/AuthenticationResponse (message IDs 3, 4)
|
||||
// was removed in ESPHome 2026.1.0. Those message IDs are reserved and should not be reused.
|
||||
|
||||
// Message sent at the beginning of each connection
|
||||
// Can only be sent by the client and only at the beginning of the connection
|
||||
@@ -102,7 +100,7 @@ message HelloRequest {
|
||||
// For example "Home Assistant"
|
||||
// Not strictly necessary to send but nice for debugging
|
||||
// purposes.
|
||||
string client_info = 1 [(pointer_to_buffer) = true];
|
||||
string client_info = 1;
|
||||
uint32 api_version_major = 2;
|
||||
uint32 api_version_minor = 3;
|
||||
}
|
||||
@@ -130,25 +128,23 @@ message HelloResponse {
|
||||
string name = 4;
|
||||
}
|
||||
|
||||
// Message sent at the beginning of each connection to authenticate the client
|
||||
// Can only be sent by the client and only at the beginning of the connection
|
||||
// DEPRECATED in ESPHome 2026.1.0 - Password authentication is no longer supported.
|
||||
// These messages are kept for protocol documentation but are not processed by the server.
|
||||
// Use noise encryption instead: https://esphome.io/components/api/#configuration-variables
|
||||
message AuthenticationRequest {
|
||||
option (id) = 3;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_PASSWORD";
|
||||
option deprecated = true;
|
||||
|
||||
// The password to log in with
|
||||
string password = 1 [(pointer_to_buffer) = true];
|
||||
string password = 1;
|
||||
}
|
||||
|
||||
// Confirmation of successful connection. After this the connection is available for all traffic.
|
||||
// Can only be sent by the server and only at the beginning of the connection
|
||||
message AuthenticationResponse {
|
||||
option (id) = 4;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_PASSWORD";
|
||||
option deprecated = true;
|
||||
|
||||
bool invalid_password = 1;
|
||||
}
|
||||
@@ -205,7 +201,9 @@ message DeviceInfoResponse {
|
||||
option (id) = 10;
|
||||
option (source) = SOURCE_SERVER;
|
||||
|
||||
bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"];
|
||||
// Deprecated in ESPHome 2026.1.0, but kept for backward compatibility
|
||||
// with older ESPHome versions that still send this field.
|
||||
bool uses_password = 1 [deprecated = true];
|
||||
|
||||
// The name of the node, given by "App.set_name()"
|
||||
string name = 2;
|
||||
@@ -477,7 +475,7 @@ message FanCommandRequest {
|
||||
bool has_speed_level = 10;
|
||||
int32 speed_level = 11;
|
||||
bool has_preset_mode = 12;
|
||||
string preset_mode = 13 [(pointer_to_buffer) = true];
|
||||
string preset_mode = 13;
|
||||
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
@@ -579,7 +577,7 @@ message LightCommandRequest {
|
||||
bool has_flash_length = 16;
|
||||
uint32 flash_length = 17;
|
||||
bool has_effect = 18;
|
||||
string effect = 19 [(pointer_to_buffer) = true];
|
||||
string effect = 19;
|
||||
uint32 device_id = 28 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
@@ -747,7 +745,7 @@ message NoiseEncryptionSetKeyRequest {
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_API_NOISE";
|
||||
|
||||
bytes key = 1 [(pointer_to_buffer) = true];
|
||||
bytes key = 1;
|
||||
}
|
||||
|
||||
message NoiseEncryptionSetKeyResponse {
|
||||
@@ -767,7 +765,7 @@ message SubscribeHomeassistantServicesRequest {
|
||||
|
||||
message HomeassistantServiceMap {
|
||||
string key = 1;
|
||||
string value = 2 [(no_zero_copy) = true];
|
||||
string value = 2;
|
||||
}
|
||||
|
||||
message HomeassistantActionRequest {
|
||||
@@ -783,7 +781,7 @@ message HomeassistantActionRequest {
|
||||
bool is_event = 5;
|
||||
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
|
||||
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||
string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||
string response_template = 8 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||
}
|
||||
|
||||
// Message sent by Home Assistant to ESPHome with service call response data
|
||||
@@ -796,7 +794,7 @@ message HomeassistantActionResponse {
|
||||
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
|
||||
bool success = 2; // Whether the service call succeeded
|
||||
string error_message = 3; // Error message if success = false
|
||||
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||
bytes response_data = 4 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
|
||||
}
|
||||
|
||||
// ==================== IMPORT HOME ASSISTANT STATES ====================
|
||||
@@ -824,9 +822,9 @@ message HomeAssistantStateResponse {
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
|
||||
|
||||
string entity_id = 1 [(pointer_to_buffer) = true];
|
||||
string state = 2 [(pointer_to_buffer) = true];
|
||||
string attribute = 3 [(pointer_to_buffer) = true];
|
||||
string entity_id = 1;
|
||||
string state = 2;
|
||||
string attribute = 3;
|
||||
}
|
||||
|
||||
// ==================== IMPORT TIME ====================
|
||||
@@ -841,7 +839,7 @@ message GetTimeResponse {
|
||||
option (no_delay) = true;
|
||||
|
||||
fixed32 epoch_seconds = 1;
|
||||
string timezone = 2 [(pointer_to_buffer) = true];
|
||||
string timezone = 2;
|
||||
}
|
||||
|
||||
// ==================== USER-DEFINES SERVICES ====================
|
||||
@@ -1091,11 +1089,11 @@ message ClimateCommandRequest {
|
||||
bool has_swing_mode = 14;
|
||||
ClimateSwingMode swing_mode = 15;
|
||||
bool has_custom_fan_mode = 16;
|
||||
string custom_fan_mode = 17 [(pointer_to_buffer) = true];
|
||||
string custom_fan_mode = 17;
|
||||
bool has_preset = 18;
|
||||
ClimatePreset preset = 19;
|
||||
bool has_custom_preset = 20;
|
||||
string custom_preset = 21 [(pointer_to_buffer) = true];
|
||||
string custom_preset = 21;
|
||||
bool has_target_humidity = 22;
|
||||
float target_humidity = 23;
|
||||
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -1274,7 +1272,7 @@ message SelectCommandRequest {
|
||||
option (base_class) = "CommandProtoMessage";
|
||||
|
||||
fixed32 key = 1;
|
||||
string state = 2 [(pointer_to_buffer) = true];
|
||||
string state = 2;
|
||||
uint32 device_id = 3 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
|
||||
@@ -1292,7 +1290,7 @@ message ListEntitiesSirenResponse {
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
bool disabled_by_default = 6;
|
||||
repeated string tones = 7;
|
||||
repeated string tones = 7 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
bool supports_duration = 8;
|
||||
bool supports_volume = 9;
|
||||
EntityCategory entity_category = 10;
|
||||
@@ -1692,7 +1690,7 @@ message BluetoothGATTWriteRequest {
|
||||
uint32 handle = 2;
|
||||
bool response = 3;
|
||||
|
||||
bytes data = 4 [(pointer_to_buffer) = true];
|
||||
bytes data = 4;
|
||||
}
|
||||
|
||||
message BluetoothGATTReadDescriptorRequest {
|
||||
@@ -1712,7 +1710,7 @@ message BluetoothGATTWriteDescriptorRequest {
|
||||
uint64 address = 1;
|
||||
uint32 handle = 2;
|
||||
|
||||
bytes data = 3 [(pointer_to_buffer) = true];
|
||||
bytes data = 3;
|
||||
}
|
||||
|
||||
message BluetoothGATTNotifyRequest {
|
||||
@@ -1937,7 +1935,7 @@ message VoiceAssistantAudio {
|
||||
option (source) = SOURCE_BOTH;
|
||||
option (ifdef) = "USE_VOICE_ASSISTANT";
|
||||
|
||||
bytes data = 1;
|
||||
bytes data = 1 [(pointer_to_buffer) = true];
|
||||
bool end = 2;
|
||||
}
|
||||
|
||||
@@ -2425,7 +2423,7 @@ message ZWaveProxyFrame {
|
||||
option (ifdef) = "USE_ZWAVE_PROXY";
|
||||
option (no_delay) = true;
|
||||
|
||||
bytes data = 1 [(pointer_to_buffer) = true];
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
enum ZWaveProxyRequestType {
|
||||
@@ -2439,5 +2437,51 @@ message ZWaveProxyRequest {
|
||||
option (ifdef) = "USE_ZWAVE_PROXY";
|
||||
|
||||
ZWaveProxyRequestType type = 1;
|
||||
bytes data = 2 [(pointer_to_buffer) = true];
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
// ==================== INFRARED ====================
|
||||
// Note: Feature and capability flag enums are defined in
|
||||
// esphome/components/infrared/infrared.h
|
||||
|
||||
// Listing of infrared instances
|
||||
message ListEntitiesInfraredResponse {
|
||||
option (id) = 135;
|
||||
option (base_class) = "InfoResponseProtoMessage";
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_INFRARED";
|
||||
|
||||
string object_id = 1;
|
||||
fixed32 key = 2;
|
||||
string name = 3;
|
||||
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
bool disabled_by_default = 5;
|
||||
EntityCategory entity_category = 6;
|
||||
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
|
||||
uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags
|
||||
}
|
||||
|
||||
// Command to transmit infrared/RF data using raw timings
|
||||
message InfraredRFTransmitRawTimingsRequest {
|
||||
option (id) = 136;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_IR_RF";
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
fixed32 key = 2; // Key identifying the transmitter instance
|
||||
uint32 carrier_frequency = 3; // Carrier frequency in Hz
|
||||
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
|
||||
repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off)
|
||||
}
|
||||
|
||||
// Event message for received infrared/RF data
|
||||
message InfraredRFReceiveEvent {
|
||||
option (id) = 137;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_IR_RF";
|
||||
option (no_delay) = true;
|
||||
|
||||
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
|
||||
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
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
#ifdef USE_WATER_HEATER
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
#include "esphome/components/infrared/infrared.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -101,16 +104,14 @@ APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *pa
|
||||
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
|
||||
auto &noise_ctx = parent->get_noise_ctx();
|
||||
if (noise_ctx.has_psk()) {
|
||||
this->helper_ =
|
||||
std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx, &this->client_info_)};
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx)};
|
||||
} else {
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)};
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
|
||||
}
|
||||
#elif defined(USE_API_PLAINTEXT)
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock), &this->client_info_)};
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
|
||||
#elif defined(USE_API_NOISE)
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{
|
||||
new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx(), &this->client_info_)};
|
||||
this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
|
||||
#else
|
||||
#error "No frame helper defined"
|
||||
#endif
|
||||
@@ -131,8 +132,9 @@ void APIConnection::start() {
|
||||
this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
|
||||
return;
|
||||
}
|
||||
this->client_info_.peername = helper_->getpeername();
|
||||
this->client_info_.name = this->client_info_.peername;
|
||||
// Initialize client name with peername (IP address) until Hello message provides actual name
|
||||
const char *peername = this->helper_->get_client_peername();
|
||||
this->helper_->set_client_name(peername, strlen(peername));
|
||||
}
|
||||
|
||||
APIConnection::~APIConnection() {
|
||||
@@ -252,8 +254,7 @@ void APIConnection::loop() {
|
||||
// Disconnect if not responded within 2.5*keepalive
|
||||
if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(),
|
||||
this->client_info_.peername.c_str());
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting"));
|
||||
}
|
||||
} else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
|
||||
// Only send ping if we're not disconnecting
|
||||
@@ -287,7 +288,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
|
||||
// remote initiated disconnect_client
|
||||
// don't close yet, we still need to send the disconnect response
|
||||
// close will happen on next loop
|
||||
ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected"));
|
||||
this->flags_.next_close = true;
|
||||
DisconnectResponse resp;
|
||||
return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
|
||||
@@ -304,7 +305,8 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// If in log-only mode, just log and return
|
||||
if (conn->flags_.log_only_mode) {
|
||||
conn->log_send_message_(msg.message_name(), msg.dump());
|
||||
DumpBuffer dump_buf;
|
||||
conn->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
|
||||
return 1; // Return non-zero to indicate "success" for logging
|
||||
}
|
||||
#endif
|
||||
@@ -378,7 +380,7 @@ uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConne
|
||||
bool is_single) {
|
||||
auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity);
|
||||
ListEntitiesBinarySensorResponse msg;
|
||||
msg.set_device_class(binary_sensor->get_device_class_ref());
|
||||
msg.device_class = binary_sensor->get_device_class_ref();
|
||||
msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
|
||||
return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size, is_single);
|
||||
@@ -410,7 +412,7 @@ uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *c
|
||||
msg.supports_position = traits.get_supports_position();
|
||||
msg.supports_tilt = traits.get_supports_tilt();
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
msg.set_device_class(cover->get_device_class_ref());
|
||||
msg.device_class = cover->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -445,7 +447,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
|
||||
if (traits.supports_direction())
|
||||
msg.direction = static_cast<enums::FanDirection>(fan->direction);
|
||||
if (traits.supports_preset_modes() && fan->has_preset_mode())
|
||||
msg.set_preset_mode(StringRef(fan->get_preset_mode()));
|
||||
msg.preset_mode = fan->get_preset_mode();
|
||||
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
@@ -473,7 +475,7 @@ void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||
if (msg.has_direction)
|
||||
call.set_direction(static_cast<fan::FanDirection>(msg.direction));
|
||||
if (msg.has_preset_mode)
|
||||
call.set_preset_mode(reinterpret_cast<const char *>(msg.preset_mode), msg.preset_mode_len);
|
||||
call.set_preset_mode(msg.preset_mode.c_str(), msg.preset_mode.size());
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -501,7 +503,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
|
||||
resp.cold_white = values.get_cold_white();
|
||||
resp.warm_white = values.get_warm_white();
|
||||
if (light->supports_effects()) {
|
||||
resp.set_effect(light->get_effect_name_ref());
|
||||
resp.effect = light->get_effect_name();
|
||||
}
|
||||
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@@ -524,7 +526,8 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
effects_list.init(light_effects.size() + 1);
|
||||
effects_list.push_back("None");
|
||||
for (auto *effect : light_effects) {
|
||||
effects_list.push_back(effect->get_name());
|
||||
// c_str() is safe as effect names are null-terminated strings from codegen
|
||||
effects_list.push_back(effect->get_name().c_str());
|
||||
}
|
||||
}
|
||||
msg.effects = &effects_list;
|
||||
@@ -559,7 +562,7 @@ void APIConnection::light_command(const LightCommandRequest &msg) {
|
||||
if (msg.has_flash_length)
|
||||
call.set_flash_length(msg.flash_length);
|
||||
if (msg.has_effect)
|
||||
call.set_effect(reinterpret_cast<const char *>(msg.effect), msg.effect_len);
|
||||
call.set_effect(msg.effect.c_str(), msg.effect.size());
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -583,10 +586,10 @@ uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *sensor = static_cast<sensor::Sensor *>(entity);
|
||||
ListEntitiesSensorResponse msg;
|
||||
msg.set_unit_of_measurement(sensor->get_unit_of_measurement_ref());
|
||||
msg.unit_of_measurement = sensor->get_unit_of_measurement_ref();
|
||||
msg.accuracy_decimals = sensor->get_accuracy_decimals();
|
||||
msg.force_update = sensor->get_force_update();
|
||||
msg.set_device_class(sensor->get_device_class_ref());
|
||||
msg.device_class = sensor->get_device_class_ref();
|
||||
msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
|
||||
return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
@@ -613,7 +616,7 @@ uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *
|
||||
auto *a_switch = static_cast<switch_::Switch *>(entity);
|
||||
ListEntitiesSwitchResponse msg;
|
||||
msg.assumed_state = a_switch->assumed_state();
|
||||
msg.set_device_class(a_switch->get_device_class_ref());
|
||||
msg.device_class = a_switch->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -638,7 +641,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec
|
||||
bool is_single) {
|
||||
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
|
||||
TextSensorStateResponse resp;
|
||||
resp.set_state(StringRef(text_sensor->state));
|
||||
resp.state = StringRef(text_sensor->state);
|
||||
resp.missing_state = !text_sensor->has_state();
|
||||
return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
@@ -647,7 +650,7 @@ uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnect
|
||||
bool is_single) {
|
||||
auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
|
||||
ListEntitiesTextSensorResponse msg;
|
||||
msg.set_device_class(text_sensor->get_device_class_ref());
|
||||
msg.device_class = text_sensor->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn,
|
||||
remaining_size, is_single);
|
||||
}
|
||||
@@ -677,13 +680,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
||||
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
|
||||
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) {
|
||||
resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode()));
|
||||
resp.custom_fan_mode = climate->get_custom_fan_mode();
|
||||
}
|
||||
if (traits.get_supports_presets() && climate->preset.has_value()) {
|
||||
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) {
|
||||
resp.set_custom_preset(StringRef(climate->get_custom_preset()));
|
||||
resp.custom_preset = climate->get_custom_preset();
|
||||
}
|
||||
if (traits.get_supports_swing_modes())
|
||||
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
||||
@@ -738,11 +741,11 @@ void APIConnection::climate_command(const ClimateCommandRequest &msg) {
|
||||
if (msg.has_fan_mode)
|
||||
call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
|
||||
if (msg.has_custom_fan_mode)
|
||||
call.set_fan_mode(reinterpret_cast<const char *>(msg.custom_fan_mode), msg.custom_fan_mode_len);
|
||||
call.set_fan_mode(msg.custom_fan_mode.c_str(), msg.custom_fan_mode.size());
|
||||
if (msg.has_preset)
|
||||
call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
|
||||
if (msg.has_custom_preset)
|
||||
call.set_preset(reinterpret_cast<const char *>(msg.custom_preset), msg.custom_preset_len);
|
||||
call.set_preset(msg.custom_preset.c_str(), msg.custom_preset.size());
|
||||
if (msg.has_swing_mode)
|
||||
call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
|
||||
call.perform();
|
||||
@@ -768,9 +771,9 @@ uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *number = static_cast<number::Number *>(entity);
|
||||
ListEntitiesNumberResponse msg;
|
||||
msg.set_unit_of_measurement(number->traits.get_unit_of_measurement_ref());
|
||||
msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref();
|
||||
msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode());
|
||||
msg.set_device_class(number->traits.get_device_class_ref());
|
||||
msg.device_class = number->traits.get_device_class_ref();
|
||||
msg.min_value = number->traits.get_min_value();
|
||||
msg.max_value = number->traits.get_max_value();
|
||||
msg.step = number->traits.get_step();
|
||||
@@ -883,7 +886,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c
|
||||
bool is_single) {
|
||||
auto *text = static_cast<text::Text *>(entity);
|
||||
TextStateResponse resp;
|
||||
resp.set_state(StringRef(text->state));
|
||||
resp.state = StringRef(text->state);
|
||||
resp.missing_state = !text->has_state();
|
||||
return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@@ -895,7 +898,7 @@ uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *co
|
||||
msg.mode = static_cast<enums::TextMode>(text->traits.get_mode());
|
||||
msg.min_length = text->traits.get_min_length();
|
||||
msg.max_length = text->traits.get_max_length();
|
||||
msg.set_pattern(text->traits.get_pattern_ref());
|
||||
msg.pattern = text->traits.get_pattern_ref();
|
||||
return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -916,7 +919,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
|
||||
bool is_single) {
|
||||
auto *select = static_cast<select::Select *>(entity);
|
||||
SelectStateResponse resp;
|
||||
resp.set_state(StringRef(select->current_option()));
|
||||
resp.state = select->current_option();
|
||||
resp.missing_state = !select->has_state();
|
||||
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@@ -931,7 +934,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
|
||||
}
|
||||
void APIConnection::select_command(const SelectCommandRequest &msg) {
|
||||
ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
|
||||
call.set_option(reinterpret_cast<const char *>(msg.state), msg.state_len);
|
||||
call.set_option(msg.state.c_str(), msg.state.size());
|
||||
call.perform();
|
||||
}
|
||||
#endif
|
||||
@@ -941,7 +944,7 @@ uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *button = static_cast<button::Button *>(entity);
|
||||
ListEntitiesButtonResponse msg;
|
||||
msg.set_device_class(button->get_device_class_ref());
|
||||
msg.device_class = button->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -1010,7 +1013,7 @@ uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *c
|
||||
auto *valve = static_cast<valve::Valve *>(entity);
|
||||
ListEntitiesValveResponse msg;
|
||||
auto traits = valve->get_traits();
|
||||
msg.set_device_class(valve->get_device_class_ref());
|
||||
msg.device_class = valve->get_device_class_ref();
|
||||
msg.assumed_state = traits.get_is_assumed_state();
|
||||
msg.supports_position = traits.get_supports_position();
|
||||
msg.supports_stop = traits.get_supports_stop();
|
||||
@@ -1055,7 +1058,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
for (auto &supported_format : traits.get_supported_formats()) {
|
||||
msg.supported_formats.emplace_back();
|
||||
auto &media_format = msg.supported_formats.back();
|
||||
media_format.set_format(StringRef(supported_format.format));
|
||||
media_format.format = StringRef(supported_format.format);
|
||||
media_format.sample_rate = supported_format.sample_rate;
|
||||
media_format.num_channels = supported_format.num_channels;
|
||||
media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
|
||||
@@ -1153,9 +1156,8 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
|
||||
#ifdef USE_TIME_TIMEZONE
|
||||
if (value.timezone_len > 0) {
|
||||
homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
|
||||
value.timezone_len);
|
||||
if (!value.timezone.empty()) {
|
||||
homeassistant::global_homeassistant_time->set_timezone(value.timezone.c_str(), value.timezone.size());
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -1266,8 +1268,8 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
|
||||
for (auto &wake_word : config.available_wake_words) {
|
||||
resp.available_wake_words.emplace_back();
|
||||
auto &resp_wake_word = resp.available_wake_words.back();
|
||||
resp_wake_word.set_id(StringRef(wake_word.id));
|
||||
resp_wake_word.set_wake_word(StringRef(wake_word.wake_word));
|
||||
resp_wake_word.id = StringRef(wake_word.id);
|
||||
resp_wake_word.wake_word = StringRef(wake_word.wake_word);
|
||||
for (const auto &lang : wake_word.trained_languages) {
|
||||
resp_wake_word.trained_languages.push_back(lang);
|
||||
}
|
||||
@@ -1282,8 +1284,8 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
|
||||
|
||||
resp.available_wake_words.emplace_back();
|
||||
auto &resp_wake_word = resp.available_wake_words.back();
|
||||
resp_wake_word.set_id(StringRef(wake_word.id));
|
||||
resp_wake_word.set_wake_word(StringRef(wake_word.wake_word));
|
||||
resp_wake_word.id = StringRef(wake_word.id);
|
||||
resp_wake_word.wake_word = StringRef(wake_word.wake_word);
|
||||
for (const auto &lang : wake_word.trained_languages) {
|
||||
resp_wake_word.trained_languages.push_back(lang);
|
||||
}
|
||||
@@ -1417,14 +1419,15 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void APIConnection::send_event(event::Event *event, const char *event_type) {
|
||||
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
||||
void APIConnection::send_event(event::Event *event, StringRef event_type) {
|
||||
// get_last_event_type() returns StringRef pointing to null-terminated string literals from codegen
|
||||
this->send_message_smart_(event, MessageCreator(event_type.c_str()), EventResponse::MESSAGE_TYPE,
|
||||
EventResponse::ESTIMATED_SIZE);
|
||||
}
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single) {
|
||||
EventResponse resp;
|
||||
resp.set_event_type(StringRef(event_type));
|
||||
resp.event_type = event_type;
|
||||
return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
|
||||
@@ -1432,13 +1435,42 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
|
||||
bool is_single) {
|
||||
auto *event = static_cast<event::Event *>(entity);
|
||||
ListEntitiesEventResponse msg;
|
||||
msg.set_device_class(event->get_device_class_ref());
|
||||
msg.device_class = event->get_device_class_ref();
|
||||
msg.event_types = &event->get_event_types();
|
||||
return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
// TODO: When RF is implemented, add a field to the message to distinguish IR vs RF
|
||||
// and dispatch to the appropriate entity type based on that field.
|
||||
#ifdef USE_INFRARED
|
||||
ENTITY_COMMAND_MAKE_CALL(infrared::Infrared, infrared, infrared)
|
||||
call.set_carrier_frequency(msg.carrier_frequency);
|
||||
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
|
||||
call.set_repeat_count(msg.repeat_count);
|
||||
call.perform();
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) {
|
||||
this->send_message(msg, InfraredRFReceiveEvent::MESSAGE_TYPE);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_INFRARED
|
||||
uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single) {
|
||||
auto *infrared = static_cast<infrared::Infrared *>(entity);
|
||||
ListEntitiesInfraredResponse msg;
|
||||
msg.capabilities = infrared->get_capability_flags();
|
||||
return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
bool APIConnection::send_update_state(update::UpdateEntity *update) {
|
||||
return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE,
|
||||
@@ -1455,11 +1487,11 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection
|
||||
resp.has_progress = true;
|
||||
resp.progress = update->update_info.progress;
|
||||
}
|
||||
resp.set_current_version(StringRef(update->update_info.current_version));
|
||||
resp.set_latest_version(StringRef(update->update_info.latest_version));
|
||||
resp.set_title(StringRef(update->update_info.title));
|
||||
resp.set_release_summary(StringRef(update->update_info.summary));
|
||||
resp.set_release_url(StringRef(update->update_info.release_url));
|
||||
resp.current_version = StringRef(update->update_info.current_version);
|
||||
resp.latest_version = StringRef(update->update_info.latest_version);
|
||||
resp.title = StringRef(update->update_info.title);
|
||||
resp.release_summary = StringRef(update->update_info.summary);
|
||||
resp.release_url = StringRef(update->update_info.release_url);
|
||||
}
|
||||
return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
@@ -1467,7 +1499,7 @@ uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *update = static_cast<update::UpdateEntity *>(entity);
|
||||
ListEntitiesUpdateResponse msg;
|
||||
msg.set_device_class(update->get_device_class_ref());
|
||||
msg.device_class = update->get_device_class_ref();
|
||||
return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -1505,9 +1537,10 @@ void APIConnection::complete_authentication_() {
|
||||
}
|
||||
|
||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
|
||||
ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
|
||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
|
||||
this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
|
||||
this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
|
||||
std::string(this->helper_->get_client_peername()));
|
||||
#endif
|
||||
#ifdef USE_HOMEASSISTANT_TIME
|
||||
if (homeassistant::global_homeassistant_time != nullptr) {
|
||||
@@ -1522,41 +1555,25 @@ void APIConnection::complete_authentication_() {
|
||||
}
|
||||
|
||||
bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
this->client_info_.name.assign(reinterpret_cast<const char *>(msg.client_info), msg.client_info_len);
|
||||
this->client_info_.peername = this->helper_->getpeername();
|
||||
// Copy client name with truncation if needed (set_client_name handles truncation)
|
||||
this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
|
||||
this->client_api_version_major_ = msg.api_version_major;
|
||||
this->client_api_version_minor_ = msg.api_version_minor;
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.name.c_str(),
|
||||
this->client_info_.peername.c_str(), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->helper_->get_client_name(),
|
||||
this->helper_->get_client_peername(), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 13;
|
||||
resp.api_version_minor = 14;
|
||||
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
|
||||
resp.set_server_info(ESPHOME_VERSION_REF);
|
||||
resp.set_name(StringRef(App.get_name()));
|
||||
resp.server_info = ESPHOME_VERSION_REF;
|
||||
resp.name = StringRef(App.get_name());
|
||||
|
||||
#ifdef USE_API_PASSWORD
|
||||
// Password required - wait for authentication
|
||||
this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::CONNECTED);
|
||||
#else
|
||||
// No password configured - auto-authenticate
|
||||
// Auto-authenticate - password auth was removed in ESPHome 2026.1.0
|
||||
this->complete_authentication_();
|
||||
#endif
|
||||
|
||||
return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#ifdef USE_API_PASSWORD
|
||||
bool APIConnection::send_authenticate_response(const AuthenticationRequest &msg) {
|
||||
AuthenticationResponse resp;
|
||||
// bool invalid_password = 1;
|
||||
resp.invalid_password = !this->parent_->check_password(msg.password, msg.password_len);
|
||||
if (!resp.invalid_password) {
|
||||
this->complete_authentication_();
|
||||
}
|
||||
return this->send_message(resp, AuthenticationResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#endif // USE_API_PASSWORD
|
||||
|
||||
bool APIConnection::send_ping_response(const PingRequest &msg) {
|
||||
PingResponse resp;
|
||||
@@ -1565,27 +1582,24 @@ bool APIConnection::send_ping_response(const PingRequest &msg) {
|
||||
|
||||
bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
DeviceInfoResponse resp{};
|
||||
#ifdef USE_API_PASSWORD
|
||||
resp.uses_password = true;
|
||||
#endif
|
||||
resp.set_name(StringRef(App.get_name()));
|
||||
resp.set_friendly_name(StringRef(App.get_friendly_name()));
|
||||
resp.name = StringRef(App.get_name());
|
||||
resp.friendly_name = StringRef(App.get_friendly_name());
|
||||
#ifdef USE_AREAS
|
||||
resp.set_suggested_area(StringRef(App.get_area()));
|
||||
resp.suggested_area = StringRef(App.get_area());
|
||||
#endif
|
||||
// Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
|
||||
char mac_address[18];
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
format_mac_addr_upper(mac, mac_address);
|
||||
resp.set_mac_address(StringRef(mac_address));
|
||||
resp.mac_address = StringRef(mac_address);
|
||||
|
||||
resp.set_esphome_version(ESPHOME_VERSION_REF);
|
||||
resp.esphome_version = ESPHOME_VERSION_REF;
|
||||
|
||||
// Stack buffer for build time string
|
||||
char build_time_str[Application::BUILD_TIME_STR_SIZE];
|
||||
App.get_build_time_string(build_time_str);
|
||||
resp.set_compilation_time(StringRef(build_time_str));
|
||||
resp.compilation_time = StringRef(build_time_str);
|
||||
|
||||
// Manufacturer string - define once, handle ESP8266 PROGMEM separately
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
@@ -1609,10 +1623,10 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
static const char MANUFACTURER_PROGMEM[] PROGMEM = ESPHOME_MANUFACTURER;
|
||||
char manufacturer_buf[sizeof(MANUFACTURER_PROGMEM)];
|
||||
memcpy_P(manufacturer_buf, MANUFACTURER_PROGMEM, sizeof(MANUFACTURER_PROGMEM));
|
||||
resp.set_manufacturer(StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1));
|
||||
resp.manufacturer = StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1);
|
||||
#else
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
|
||||
resp.set_manufacturer(MANUFACTURER);
|
||||
resp.manufacturer = MANUFACTURER;
|
||||
#endif
|
||||
#undef ESPHOME_MANUFACTURER
|
||||
|
||||
@@ -1620,10 +1634,10 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
static const char MODEL_PROGMEM[] PROGMEM = ESPHOME_BOARD;
|
||||
char model_buf[sizeof(MODEL_PROGMEM)];
|
||||
memcpy_P(model_buf, MODEL_PROGMEM, sizeof(MODEL_PROGMEM));
|
||||
resp.set_model(StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1));
|
||||
resp.model = StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1);
|
||||
#else
|
||||
static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
|
||||
resp.set_model(MODEL);
|
||||
resp.model = MODEL;
|
||||
#endif
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
|
||||
@@ -1636,13 +1650,13 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
char project_version_buf[sizeof(PROJECT_VERSION_PROGMEM)];
|
||||
memcpy_P(project_name_buf, PROJECT_NAME_PROGMEM, sizeof(PROJECT_NAME_PROGMEM));
|
||||
memcpy_P(project_version_buf, PROJECT_VERSION_PROGMEM, sizeof(PROJECT_VERSION_PROGMEM));
|
||||
resp.set_project_name(StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1));
|
||||
resp.set_project_version(StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1));
|
||||
resp.project_name = StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1);
|
||||
resp.project_version = StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1);
|
||||
#else
|
||||
static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
|
||||
static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
|
||||
resp.set_project_name(PROJECT_NAME);
|
||||
resp.set_project_version(PROJECT_VERSION);
|
||||
resp.project_name = PROJECT_NAME;
|
||||
resp.project_version = PROJECT_VERSION;
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
@@ -1653,7 +1667,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
// Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
|
||||
char bluetooth_mac[18];
|
||||
bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty(bluetooth_mac);
|
||||
resp.set_bluetooth_mac_address(StringRef(bluetooth_mac));
|
||||
resp.bluetooth_mac_address = StringRef(bluetooth_mac);
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
|
||||
@@ -1672,7 +1686,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
break;
|
||||
auto &device_info = resp.devices[device_index++];
|
||||
device_info.device_id = device->get_device_id();
|
||||
device_info.set_name(StringRef(device->get_name()));
|
||||
device_info.name = StringRef(device->get_name());
|
||||
device_info.area_id = device->get_area_id();
|
||||
}
|
||||
#endif
|
||||
@@ -1683,7 +1697,7 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
break;
|
||||
auto &area_info = resp.areas[area_index++];
|
||||
area_info.area_id = area->get_area_id();
|
||||
area_info.set_name(StringRef(area->get_name()));
|
||||
area_info.name = StringRef(area->get_name());
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1693,28 +1707,37 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
|
||||
// Skip if entity_id is empty (invalid message)
|
||||
if (msg.entity_id_len == 0) {
|
||||
if (msg.entity_id.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto &it : this->parent_->get_state_subs()) {
|
||||
// Compare entity_id: check length matches and content matches
|
||||
size_t entity_id_len = strlen(it.entity_id);
|
||||
if (entity_id_len != msg.entity_id_len || memcmp(it.entity_id, msg.entity_id, msg.entity_id_len) != 0) {
|
||||
if (entity_id_len != msg.entity_id.size() ||
|
||||
memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Compare attribute: either both have matching attribute, or both have none
|
||||
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
|
||||
if (sub_attr_len != msg.attribute_len ||
|
||||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute, sub_attr_len) != 0)) {
|
||||
if (sub_attr_len != msg.attribute.size() ||
|
||||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create temporary string for callback (callback takes const std::string &)
|
||||
// Handle empty state (nullptr with len=0)
|
||||
std::string state(msg.state_len > 0 ? reinterpret_cast<const char *>(msg.state) : "", msg.state_len);
|
||||
it.callback(state);
|
||||
// Create null-terminated state for callback (parse_number needs null-termination)
|
||||
// HA state max length is 255, so 256 byte buffer covers all cases
|
||||
char state_buf[256];
|
||||
size_t copy_len = msg.state.size();
|
||||
if (copy_len >= sizeof(state_buf)) {
|
||||
copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator
|
||||
}
|
||||
if (copy_len > 0) {
|
||||
memcpy(state_buf, msg.state.c_str(), copy_len);
|
||||
}
|
||||
state_buf[copy_len] = '\0';
|
||||
it.callback(StringRef(state_buf, copy_len));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -1749,20 +1772,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
// the action list. This ensures async actions (delays, waits) complete first.
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message) {
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message) {
|
||||
ExecuteServiceResponse resp;
|
||||
resp.call_id = call_id;
|
||||
resp.success = success;
|
||||
resp.set_error_message(StringRef(error_message));
|
||||
resp.error_message = error_message;
|
||||
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||
void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message,
|
||||
const uint8_t *response_data, size_t response_data_len) {
|
||||
ExecuteServiceResponse resp;
|
||||
resp.call_id = call_id;
|
||||
resp.success = success;
|
||||
resp.set_error_message(StringRef(error_message));
|
||||
resp.error_message = error_message;
|
||||
resp.response_data = response_data;
|
||||
resp.response_data_len = response_data_len;
|
||||
this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
|
||||
@@ -1831,10 +1854,30 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
||||
return false;
|
||||
}
|
||||
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse
|
||||
const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
|
||||
|
||||
if (!this->try_to_clear_buffer(!is_log_message)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Toggle Nagle's algorithm based on message type to prevent log messages from
|
||||
// filling the TCP send buffer and crowding out important state updates.
|
||||
//
|
||||
// This honors the `no_delay` proto option - SubscribeLogsResponse is the only
|
||||
// message with `option (no_delay) = false;` in api.proto, indicating it should
|
||||
// allow Nagle coalescing. This option existed since 2019 but was never implemented.
|
||||
//
|
||||
// - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce
|
||||
// into fewer, larger packets. They flush naturally via TCP delayed ACK timer
|
||||
// (~200ms), buffer filling, or when a state update triggers a flush.
|
||||
//
|
||||
// - All other messages (state updates, responses): Disable Nagle (NODELAY=true)
|
||||
// for immediate delivery. These are time-sensitive and should not be delayed.
|
||||
//
|
||||
// This must be done proactively BEFORE the buffer fills up - checking buffer
|
||||
// state here would be too late since we'd already be in a degraded state.
|
||||
this->helper_->set_nodelay(!is_log_message);
|
||||
|
||||
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
|
||||
if (err == APIError::WOULD_BLOCK)
|
||||
return false;
|
||||
@@ -1845,15 +1888,9 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
// Do not set last_traffic_ on send
|
||||
return true;
|
||||
}
|
||||
#ifdef USE_API_PASSWORD
|
||||
void APIConnection::on_unauthenticated_access() {
|
||||
this->on_fatal_error();
|
||||
ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
|
||||
}
|
||||
#endif
|
||||
void APIConnection::on_no_setup_connection() {
|
||||
this->on_fatal_error();
|
||||
ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
|
||||
}
|
||||
void APIConnection::on_fatal_error() {
|
||||
this->helper_->close();
|
||||
@@ -1899,9 +1936,9 @@ bool APIConnection::schedule_batch_() {
|
||||
}
|
||||
|
||||
void APIConnection::process_batch_() {
|
||||
// Ensure PacketInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<PacketInfo>::value,
|
||||
"PacketInfo must remain trivially destructible with this placement-new approach");
|
||||
// Ensure MessageInfo remains trivially destructible for our placement new approach
|
||||
static_assert(std::is_trivially_destructible<MessageInfo>::value,
|
||||
"MessageInfo must remain trivially destructible with this placement-new approach");
|
||||
|
||||
if (this->deferred_batch_.empty()) {
|
||||
this->flags_.batch_scheduled = false;
|
||||
@@ -1941,12 +1978,12 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t packets_to_process = std::min(num_items, MAX_PACKETS_PER_BATCH);
|
||||
size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
|
||||
|
||||
// Stack-allocated array for packet info
|
||||
alignas(PacketInfo) char packet_info_storage[MAX_PACKETS_PER_BATCH * sizeof(PacketInfo)];
|
||||
PacketInfo *packet_info = reinterpret_cast<PacketInfo *>(packet_info_storage);
|
||||
size_t packet_count = 0;
|
||||
// Stack-allocated array for message info
|
||||
alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
|
||||
MessageInfo *message_info = reinterpret_cast<MessageInfo *>(message_info_storage);
|
||||
size_t message_count = 0;
|
||||
|
||||
// Cache these values to avoid repeated virtual calls
|
||||
const uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
@@ -1977,7 +2014,7 @@ void APIConnection::process_batch_() {
|
||||
uint32_t current_offset = 0;
|
||||
|
||||
// Process items and encode directly to buffer (up to our limit)
|
||||
for (size_t i = 0; i < packets_to_process; i++) {
|
||||
for (size_t i = 0; i < messages_to_process; i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
// Try to encode message
|
||||
// The creator will calculate overhead to determine if the message fits
|
||||
@@ -1991,11 +2028,11 @@ void APIConnection::process_batch_() {
|
||||
// Message was encoded successfully
|
||||
// payload_size is header_padding + actual payload size + footer_size
|
||||
uint16_t proto_payload_size = payload_size - header_padding - footer_size;
|
||||
// Use placement new to construct PacketInfo in pre-allocated stack array
|
||||
// This avoids default-constructing all MAX_PACKETS_PER_BATCH elements
|
||||
// Explicit destruction is not needed because PacketInfo is trivially destructible,
|
||||
// Use placement new to construct MessageInfo in pre-allocated stack array
|
||||
// This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
|
||||
// Explicit destruction is not needed because MessageInfo is trivially destructible,
|
||||
// as ensured by the static_assert in its definition.
|
||||
new (&packet_info[packet_count++]) PacketInfo(item.message_type, current_offset, proto_payload_size);
|
||||
new (&message_info[message_count++]) MessageInfo(item.message_type, current_offset, proto_payload_size);
|
||||
|
||||
// Update tracking variables
|
||||
items_processed++;
|
||||
@@ -2019,9 +2056,9 @@ void APIConnection::process_batch_() {
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
}
|
||||
|
||||
// Send all collected packets
|
||||
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const PacketInfo>(packet_info, packet_count));
|
||||
// Send all collected messages
|
||||
APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const MessageInfo>(message_info, message_count));
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
|
||||
}
|
||||
@@ -2053,7 +2090,7 @@ uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnec
|
||||
// Special case: EventResponse uses const char * pointer
|
||||
if (message_type == EventResponse::MESSAGE_TYPE) {
|
||||
auto *e = static_cast<event::Event *>(entity);
|
||||
return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single);
|
||||
return APIConnection::try_send_event_response(e, StringRef(data_.const_char_ptr), conn, remaining_size, is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2089,10 +2126,10 @@ void APIConnection::process_state_subscriptions_() {
|
||||
|
||||
const auto &it = subs[this->state_subs_at_];
|
||||
SubscribeHomeAssistantStateResponse resp;
|
||||
resp.set_entity_id(StringRef(it.entity_id));
|
||||
resp.entity_id = StringRef(it.entity_id);
|
||||
|
||||
// Avoid string copy by using the const char* pointer if it exists
|
||||
resp.set_attribute(it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""));
|
||||
resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef("");
|
||||
|
||||
resp.once = it.once;
|
||||
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
|
||||
@@ -2101,8 +2138,13 @@ void APIConnection::process_state_subscriptions_() {
|
||||
}
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
|
||||
void APIConnection::log_client_(int level, const LogString *message) {
|
||||
esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
|
||||
this->helper_->get_client_peername(), LOG_STR_ARG(message));
|
||||
}
|
||||
|
||||
void APIConnection::log_warning_(const LogString *message, APIError err) {
|
||||
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(),
|
||||
ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_client_peername(),
|
||||
LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,32 +9,23 @@
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// Client information structure
|
||||
struct ClientInfo {
|
||||
std::string name; // Client name from Hello message
|
||||
std::string peername; // IP:port from socket
|
||||
};
|
||||
|
||||
// Keepalive timeout in milliseconds
|
||||
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
|
||||
// Maximum number of entities to process in a single batch during initial state/info sending
|
||||
// This was increased from 20 to 24 after removing the unique_id field from entity info messages,
|
||||
// which reduced message sizes allowing more entities per batch without exceeding packet limits
|
||||
static constexpr size_t MAX_INITIAL_PER_BATCH = 24;
|
||||
// Maximum number of packets to process in a single batch (platform-dependent)
|
||||
// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_
|
||||
// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes
|
||||
#if defined(USE_ESP32) || defined(USE_HOST)
|
||||
static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty
|
||||
#else
|
||||
static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks
|
||||
#endif
|
||||
// API 1.14+ clients compute object_id client-side, so messages are smaller and we can fit more per batch
|
||||
// TODO: Remove MAX_INITIAL_PER_BATCH_LEGACY before 2026.7.0 - all clients should support API 1.14 by then
|
||||
static constexpr size_t MAX_INITIAL_PER_BATCH_LEGACY = 24; // For clients < API 1.14 (includes object_id)
|
||||
static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= API 1.14 (no object_id)
|
||||
// Verify MAX_MESSAGES_PER_BATCH (defined in api_frame_helper.h) can hold the initial batch
|
||||
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
|
||||
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
|
||||
|
||||
class APIConnection final : public APIServerConnection {
|
||||
public:
|
||||
@@ -181,8 +172,13 @@ class APIConnection final : public APIServerConnection {
|
||||
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void send_event(event::Event *event, const char *event_type);
|
||||
void send_event(event::Event *event, StringRef event_type);
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
@@ -202,9 +198,6 @@ class APIConnection final : public APIServerConnection {
|
||||
void on_get_time_response(const GetTimeResponse &value) override;
|
||||
#endif
|
||||
bool send_hello_response(const HelloRequest &msg) override;
|
||||
#ifdef USE_API_PASSWORD
|
||||
bool send_authenticate_response(const AuthenticationRequest &msg) override;
|
||||
#endif
|
||||
bool send_disconnect_response(const DisconnectRequest &msg) override;
|
||||
bool send_ping_response(const PingRequest &msg) override;
|
||||
bool send_device_info_response(const DeviceInfoRequest &msg) override;
|
||||
@@ -233,9 +226,9 @@ class APIConnection final : public APIServerConnection {
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message);
|
||||
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void send_execute_service_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message,
|
||||
const uint8_t *response_data, size_t response_data_len);
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
@@ -260,9 +253,6 @@ class APIConnection final : public APIServerConnection {
|
||||
}
|
||||
|
||||
void on_fatal_error() override;
|
||||
#ifdef USE_API_PASSWORD
|
||||
void on_unauthenticated_access() override;
|
||||
#endif
|
||||
void on_no_setup_connection() override;
|
||||
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
|
||||
// FIXME: ensure no recursive writes can happen
|
||||
@@ -289,8 +279,9 @@ class APIConnection final : public APIServerConnection {
|
||||
bool try_to_clear_buffer(bool log_out_of_space);
|
||||
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
|
||||
|
||||
const std::string &get_name() const { return this->client_info_.name; }
|
||||
const std::string &get_peername() const { return this->client_info_.peername; }
|
||||
const char *get_name() const { return this->helper_->get_client_name(); }
|
||||
/// Get peer name (IP address) - cached at connection init time
|
||||
const char *get_peername() const { return this->helper_->get_client_peername(); }
|
||||
|
||||
protected:
|
||||
// Helper function to handle authentication completion
|
||||
@@ -323,18 +314,24 @@ class APIConnection final : public APIServerConnection {
|
||||
APIConnection *conn, uint32_t remaining_size, bool is_single) {
|
||||
// Set common fields that are shared by all entity types
|
||||
msg.key = entity->get_object_id_hash();
|
||||
// Get object_id with zero heap allocation
|
||||
// Static case returns direct reference, dynamic case uses buffer
|
||||
|
||||
// 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];
|
||||
msg.set_object_id(entity->get_object_id_to(object_id_buf));
|
||||
if (!conn->client_supports_api_version(1, 14)) {
|
||||
msg.object_id = entity->get_object_id_to(object_id_buf);
|
||||
}
|
||||
|
||||
if (entity->has_own_name()) {
|
||||
msg.set_name(entity->get_name());
|
||||
msg.name = entity->get_name();
|
||||
}
|
||||
|
||||
// Set common EntityBase properties
|
||||
#ifdef USE_ENTITY_ICON
|
||||
msg.set_icon(entity->get_icon_ref());
|
||||
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());
|
||||
@@ -349,16 +346,24 @@ class APIConnection final : public APIServerConnection {
|
||||
inline bool check_voice_assistant_api_connection_() const;
|
||||
#endif
|
||||
|
||||
// Get the max batch size based on client API version
|
||||
// API 1.14+ clients don't receive object_id, so messages are smaller and more fit per batch
|
||||
// TODO: Remove this method before 2026.7.0 and use MAX_INITIAL_PER_BATCH directly
|
||||
size_t get_max_batch_size_() const {
|
||||
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
|
||||
}
|
||||
|
||||
// Helper method to process multiple entities from an iterator in a batch
|
||||
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
|
||||
size_t initial_size = this->deferred_batch_.size();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) {
|
||||
size_t max_batch = this->get_max_batch_size_();
|
||||
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
|
||||
iterator.advance();
|
||||
}
|
||||
|
||||
// If the batch is full, process it immediately
|
||||
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
|
||||
if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) {
|
||||
if (this->deferred_batch_.size() >= max_batch) {
|
||||
this->process_batch_();
|
||||
}
|
||||
}
|
||||
@@ -468,8 +473,12 @@ class APIConnection final : public APIServerConnection {
|
||||
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||
static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
|
||||
#endif
|
||||
@@ -522,10 +531,7 @@ class APIConnection final : public APIServerConnection {
|
||||
std::unique_ptr<camera::CameraImageReader> image_reader_;
|
||||
#endif
|
||||
|
||||
// Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each)
|
||||
ClientInfo client_info_;
|
||||
|
||||
// Group 4: 4-byte types
|
||||
// Group 3: 4-byte types
|
||||
uint32_t last_traffic_;
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
int state_subs_at_ = -1;
|
||||
@@ -752,6 +758,8 @@ class APIConnection final : public APIServerConnection {
|
||||
return this->schedule_batch_();
|
||||
}
|
||||
|
||||
// Helper function to log client messages with name and peername
|
||||
void log_client_(int level, const LogString *message);
|
||||
// Helper function to log API errors with errno
|
||||
void log_warning_(const LogString *message, APIError err);
|
||||
// Helper to handle fatal errors with logging
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "api_frame_helper.h"
|
||||
#ifdef USE_API
|
||||
#include "api_connection.h" // For ClientInfo struct
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -13,12 +12,29 @@ namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.frame_helper";
|
||||
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
|
||||
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
|
||||
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
|
||||
#define LOG_PACKET_RECEIVED(buffer) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Received frame: %s", \
|
||||
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
||||
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#define LOG_PACKET_SENDING(data, len) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Sending raw: %s", \
|
||||
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
@@ -229,6 +245,8 @@ APIError APIFrameHelper::init_common_() {
|
||||
HELPER_LOG("Bad state for init %d", (int) state_);
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
// Cache peername now while socket is valid - needed for error logging after socket failure
|
||||
this->socket_->getpeername_to(this->client_peername_);
|
||||
int err = this->socket_->setblocking(false);
|
||||
if (err != 0) {
|
||||
state_ = State::FAILED;
|
||||
|
||||
@@ -29,24 +29,28 @@ 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
|
||||
|
||||
// Forward declaration
|
||||
struct ClientInfo;
|
||||
// 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;
|
||||
|
||||
struct ReadPacketBuffer {
|
||||
const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call)
|
||||
uint16_t data_len;
|
||||
uint16_t type;
|
||||
};
|
||||
|
||||
// Packed packet info structure to minimize memory usage
|
||||
struct PacketInfo {
|
||||
// Packed message info structure to minimize memory usage
|
||||
struct MessageInfo {
|
||||
uint16_t offset; // Offset in buffer where message starts
|
||||
uint16_t payload_size; // Size of the message payload
|
||||
uint8_t message_type; // Message type (0-255)
|
||||
|
||||
PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
|
||||
MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
|
||||
};
|
||||
|
||||
enum class APIError : uint16_t {
|
||||
@@ -82,14 +86,23 @@ const LogString *api_error_to_logstr(APIError err);
|
||||
class APIFrameHelper {
|
||||
public:
|
||||
APIFrameHelper() = default;
|
||||
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
|
||||
: socket_(std::move(socket)), client_info_(client_info) {}
|
||||
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
|
||||
|
||||
// Get client name (null-terminated)
|
||||
const char *get_client_name() const { return this->client_name_; }
|
||||
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
|
||||
const char *get_client_peername() const { return this->client_peername_; }
|
||||
// Set client name from buffer with length (truncates if needed)
|
||||
void set_client_name(const char *name, size_t len) {
|
||||
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
|
||||
memcpy(this->client_name_, name, copy_len);
|
||||
this->client_name_[copy_len] = '\0';
|
||||
}
|
||||
virtual ~APIFrameHelper() = default;
|
||||
virtual APIError init() = 0;
|
||||
virtual APIError loop();
|
||||
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
|
||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
|
||||
std::string getpeername() { return socket_->getpeername(); }
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
|
||||
APIError close() {
|
||||
state_ = State::CLOSED;
|
||||
@@ -107,11 +120,32 @@ class APIFrameHelper {
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
|
||||
///
|
||||
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
|
||||
/// state updates low-latency (NODELAY enabled). Without this, many small log
|
||||
/// packets fill the TCP send buffer, crowding out important state updates.
|
||||
///
|
||||
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
|
||||
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
|
||||
///
|
||||
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
|
||||
/// @return true if successful or already in desired state
|
||||
bool set_nodelay(bool enable) {
|
||||
if (this->nodelay_enabled_ == enable)
|
||||
return true;
|
||||
int val = enable ? 1 : 0;
|
||||
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
|
||||
if (err == 0) {
|
||||
this->nodelay_enabled_ = enable;
|
||||
}
|
||||
return err == 0;
|
||||
}
|
||||
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
|
||||
// Write multiple protobuf packets in a single operation
|
||||
// packets contains (message_type, offset, length) for each message in the buffer
|
||||
// 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
|
||||
virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0;
|
||||
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
|
||||
// Get the frame header padding required by this protocol
|
||||
uint8_t frame_header_padding() const { return frame_header_padding_; }
|
||||
// Get the frame footer size required by this protocol
|
||||
@@ -127,12 +161,6 @@ class APIFrameHelper {
|
||||
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
|
||||
std::vector<uint8_t>().swap(this->rx_buf_);
|
||||
}
|
||||
// reusable_iovs_: Safe to release unconditionally.
|
||||
// Only used within write_protobuf_packets() calls - cleared at start,
|
||||
// populated with pointers, used for writev(), then function returns.
|
||||
// The iovecs contain stale pointers after the call (data was either sent
|
||||
// or copied to tx_buf_), and are cleared on next write_protobuf_packets().
|
||||
std::vector<struct iovec>().swap(this->reusable_iovs_);
|
||||
}
|
||||
|
||||
protected:
|
||||
@@ -186,12 +214,12 @@ class APIFrameHelper {
|
||||
|
||||
// Containers (size varies, but typically 12+ bytes on 32-bit)
|
||||
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
|
||||
std::vector<struct iovec> reusable_iovs_;
|
||||
std::vector<uint8_t> rx_buf_;
|
||||
|
||||
// Pointer to client info (4 bytes on 32-bit)
|
||||
// Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance.
|
||||
const ClientInfo *client_info_{nullptr};
|
||||
// Client name buffer - stores name from Hello message or initial peername
|
||||
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
|
||||
// Cached peername/IP address - captured at init time for availability after socket failure
|
||||
char client_peername_[socket::SOCKADDR_STR_LEN]{};
|
||||
|
||||
// Group smaller types together
|
||||
uint16_t rx_buf_len_ = 0;
|
||||
@@ -201,7 +229,10 @@ class APIFrameHelper {
|
||||
uint8_t tx_buf_head_{0};
|
||||
uint8_t tx_buf_tail_{0};
|
||||
uint8_t tx_buf_count_{0};
|
||||
// 8 bytes total, 0 bytes padding
|
||||
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
|
||||
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
|
||||
// messages to coalesce while keeping state updates low-latency.
|
||||
bool nodelay_enabled_{true};
|
||||
|
||||
// Common initialization for both plaintext and noise protocols
|
||||
APIError init_common_();
|
||||
|
||||
@@ -24,12 +24,29 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
|
||||
#endif
|
||||
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
|
||||
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
|
||||
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
|
||||
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
|
||||
#define LOG_PACKET_RECEIVED(buffer) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Received frame: %s", \
|
||||
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
||||
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#define LOG_PACKET_SENDING(data, len) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Sending raw: %s", \
|
||||
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
@@ -415,12 +432,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
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_);
|
||||
PacketInfo packet{type, 0,
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
||||
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_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
|
||||
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
|
||||
APIError aerr = state_action_();
|
||||
if (aerr != APIError::OK) {
|
||||
return aerr;
|
||||
@@ -430,20 +447,20 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
if (packets.empty()) {
|
||||
if (messages.empty()) {
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||
|
||||
this->reusable_iovs_.clear();
|
||||
this->reusable_iovs_.reserve(packets.size());
|
||||
// Stack-allocated iovec array - no heap allocation
|
||||
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
|
||||
uint16_t total_write_len = 0;
|
||||
|
||||
// We need to encrypt each packet in place
|
||||
for (const auto &packet : packets) {
|
||||
// We need to encrypt each message in place
|
||||
for (const auto &msg : messages) {
|
||||
// The buffer already has padding at offset
|
||||
uint8_t *buf_start = buffer_data + packet.offset;
|
||||
uint8_t *buf_start = buffer_data + msg.offset;
|
||||
|
||||
// Write noise header
|
||||
buf_start[0] = 0x01; // indicator
|
||||
@@ -451,10 +468,10 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
||||
|
||||
// Write message header (to be encrypted)
|
||||
const uint8_t msg_offset = 3;
|
||||
buf_start[msg_offset] = static_cast<uint8_t>(packet.message_type >> 8); // type high byte
|
||||
buf_start[msg_offset + 1] = static_cast<uint8_t>(packet.message_type); // type low byte
|
||||
buf_start[msg_offset + 2] = static_cast<uint8_t>(packet.payload_size >> 8); // data_len high byte
|
||||
buf_start[msg_offset + 3] = static_cast<uint8_t>(packet.payload_size); // data_len low byte
|
||||
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
|
||||
buf_start[msg_offset + 3] = static_cast<uint8_t>(msg.payload_size); // data_len low byte
|
||||
// payload data is already in the buffer starting at offset + 7
|
||||
|
||||
// Make sure we have space for MAC
|
||||
@@ -463,8 +480,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
||||
// Encrypt the message in place
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size,
|
||||
4 + packet.payload_size + frame_footer_size_);
|
||||
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + msg.payload_size,
|
||||
4 + msg.payload_size + frame_footer_size_);
|
||||
|
||||
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
|
||||
APIError aerr =
|
||||
@@ -476,14 +493,14 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
|
||||
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
|
||||
buf_start[2] = static_cast<uint8_t>(mbuf.size);
|
||||
|
||||
// Add iovec for this encrypted packet
|
||||
size_t packet_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||
this->reusable_iovs_.push_back({buf_start, packet_len});
|
||||
total_write_len += packet_len;
|
||||
// Add iovec for this encrypted message
|
||||
size_t msg_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
|
||||
iovs.push_back({buf_start, msg_len});
|
||||
total_write_len += msg_len;
|
||||
}
|
||||
|
||||
// Send all encrypted packets in one writev call
|
||||
return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
|
||||
// Send all encrypted messages in one writev call
|
||||
return this->write_raw_(iovs.data(), iovs.size(), total_write_len);
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
|
||||
|
||||
@@ -9,8 +9,8 @@ namespace esphome::api {
|
||||
|
||||
class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) {
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
|
||||
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
|
||||
// Noise header structure:
|
||||
// Pos 0: indicator (0x01)
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
@@ -23,7 +23,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
APIError state_action_();
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#include "api_frame_helper_plaintext.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
#include "api_connection.h" // For ClientInfo struct
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -18,12 +17,29 @@ namespace esphome::api {
|
||||
|
||||
static const char *const TAG = "api.plaintext";
|
||||
|
||||
#define HELPER_LOG(msg, ...) \
|
||||
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
|
||||
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
|
||||
static constexpr size_t API_MAX_LOG_BYTES = 168;
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
|
||||
#else
|
||||
#define HELPER_LOG(msg, ...) ((void) 0)
|
||||
#endif
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
|
||||
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
|
||||
#define LOG_PACKET_RECEIVED(buffer) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Received frame: %s", \
|
||||
format_hex_pretty_to(hex_buf_, (buffer).data(), \
|
||||
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#define LOG_PACKET_SENDING(data, len) \
|
||||
do { \
|
||||
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
|
||||
ESP_LOGVV(TAG, "Sending raw: %s", \
|
||||
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
|
||||
} while (0)
|
||||
#else
|
||||
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
|
||||
#define LOG_PACKET_SENDING(data, len) ((void) 0)
|
||||
@@ -216,29 +232,30 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
|
||||
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
|
||||
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_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
|
||||
std::span<const MessageInfo> messages) {
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
|
||||
if (packets.empty()) {
|
||||
if (messages.empty()) {
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
uint8_t *buffer_data = buffer.get_buffer()->data();
|
||||
|
||||
this->reusable_iovs_.clear();
|
||||
this->reusable_iovs_.reserve(packets.size());
|
||||
// Stack-allocated iovec array - no heap allocation
|
||||
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
|
||||
uint16_t total_write_len = 0;
|
||||
|
||||
for (const auto &packet : packets) {
|
||||
for (const auto &msg : messages) {
|
||||
// Calculate varint sizes for header layout
|
||||
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size));
|
||||
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type));
|
||||
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));
|
||||
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// Calculate where to start writing the header
|
||||
@@ -266,25 +283,25 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
|
||||
//
|
||||
// The message starts at offset + frame_header_padding_
|
||||
// So we write the header starting at offset + frame_header_padding_ - total_header_len
|
||||
uint8_t *buf_start = buffer_data + packet.offset;
|
||||
uint8_t *buf_start = buffer_data + msg.offset;
|
||||
uint32_t header_offset = frame_header_padding_ - total_header_len;
|
||||
|
||||
// Write the plaintext header
|
||||
buf_start[header_offset] = 0x00; // indicator
|
||||
|
||||
// Encode varints directly into buffer
|
||||
ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
|
||||
ProtoVarInt(packet.message_type)
|
||||
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
|
||||
ProtoVarInt(msg.message_type)
|
||||
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
|
||||
|
||||
// Add iovec for this packet (header + payload)
|
||||
size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size);
|
||||
this->reusable_iovs_.push_back({buf_start + header_offset, packet_len});
|
||||
total_write_len += packet_len;
|
||||
// Add iovec for this message (header + payload)
|
||||
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
|
||||
iovs.push_back({buf_start + header_offset, msg_len});
|
||||
total_write_len += msg_len;
|
||||
}
|
||||
|
||||
// Send all packets in one writev call
|
||||
return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
|
||||
// Send all messages in one writev call
|
||||
return write_raw_(iovs.data(), iovs.size(), total_write_len);
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -7,8 +7,7 @@ namespace esphome::api {
|
||||
|
||||
class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info) {
|
||||
explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
|
||||
// Plaintext header structure (worst case):
|
||||
// Pos 0: indicator (0x00)
|
||||
// Pos 1-3: payload size varint (up to 3 bytes)
|
||||
@@ -21,7 +20,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
APIError try_read_frame_();
|
||||
|
||||
@@ -27,7 +27,6 @@ extend google.protobuf.MessageOptions {
|
||||
extend google.protobuf.FieldOptions {
|
||||
optional string field_ifdef = 1042;
|
||||
optional uint32 fixed_array_size = 50007;
|
||||
optional bool no_zero_copy = 50008 [default=false];
|
||||
optional bool fixed_array_skip_zero = 50009 [default=false];
|
||||
optional string fixed_array_size_define = 50010;
|
||||
optional string fixed_array_with_length_define = 50011;
|
||||
@@ -80,4 +79,15 @@ extend google.protobuf.FieldOptions {
|
||||
// Example: [(container_pointer_no_template) = "light::ColorModeMask"]
|
||||
// generates: const light::ColorModeMask *supported_color_modes{};
|
||||
optional string container_pointer_no_template = 50014;
|
||||
|
||||
// packed_buffer: Expose raw packed buffer instead of decoding into container
|
||||
// When set on a packed repeated field, the generated code stores a pointer
|
||||
// to the raw protobuf buffer instead of decoding values. This enables
|
||||
// zero-copy passthrough when the consumer can decode on-demand.
|
||||
// The field must be a packed repeated field (packed=true).
|
||||
// Generates three fields:
|
||||
// - const uint8_t *<field>_data_{nullptr};
|
||||
// - uint16_t <field>_length_{0};
|
||||
// - uint16_t <field>_count_{0};
|
||||
optional bool packed_buffer = 50015 [default=false];
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,8 +8,12 @@ namespace esphome::api {
|
||||
static const char *const TAG = "api.service";
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void APIServerConnectionBase::log_send_message_(const char *name, const std::string &dump) {
|
||||
ESP_LOGVV(TAG, "send_message %s: %s", name, dump.c_str());
|
||||
void APIServerConnectionBase::log_send_message_(const char *name, const char *dump) {
|
||||
ESP_LOGVV(TAG, "send_message %s: %s", name, dump);
|
||||
}
|
||||
void APIServerConnectionBase::log_receive_message_(const LogString *name, const ProtoMessage &msg) {
|
||||
DumpBuffer dump_buf;
|
||||
ESP_LOGVV(TAG, "%s: %s", LOG_STR_ARG(name), msg.dump_to(dump_buf));
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -19,27 +23,16 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
HelloRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_hello_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_hello_request"), msg);
|
||||
#endif
|
||||
this->on_hello_request(msg);
|
||||
break;
|
||||
}
|
||||
#ifdef USE_API_PASSWORD
|
||||
case AuthenticationRequest::MESSAGE_TYPE: {
|
||||
AuthenticationRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_authentication_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_authentication_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case DisconnectRequest::MESSAGE_TYPE: {
|
||||
DisconnectRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_request"), msg);
|
||||
#endif
|
||||
this->on_disconnect_request(msg);
|
||||
break;
|
||||
@@ -48,7 +41,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
DisconnectResponse msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_response"), msg);
|
||||
#endif
|
||||
this->on_disconnect_response(msg);
|
||||
break;
|
||||
@@ -57,7 +50,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
PingRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_ping_request"), msg);
|
||||
#endif
|
||||
this->on_ping_request(msg);
|
||||
break;
|
||||
@@ -66,7 +59,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
PingResponse msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_ping_response"), msg);
|
||||
#endif
|
||||
this->on_ping_response(msg);
|
||||
break;
|
||||
@@ -75,7 +68,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
DeviceInfoRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_device_info_request"), msg);
|
||||
#endif
|
||||
this->on_device_info_request(msg);
|
||||
break;
|
||||
@@ -84,7 +77,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ListEntitiesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_list_entities_request"), msg);
|
||||
#endif
|
||||
this->on_list_entities_request(msg);
|
||||
break;
|
||||
@@ -93,7 +86,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
SubscribeStatesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_states_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_states_request(msg);
|
||||
break;
|
||||
@@ -102,7 +95,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
SubscribeLogsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_logs_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_logs_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_logs_request(msg);
|
||||
break;
|
||||
@@ -112,7 +105,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
CoverCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_cover_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_cover_command_request"), msg);
|
||||
#endif
|
||||
this->on_cover_command_request(msg);
|
||||
break;
|
||||
@@ -123,7 +116,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
FanCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_fan_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_fan_command_request"), msg);
|
||||
#endif
|
||||
this->on_fan_command_request(msg);
|
||||
break;
|
||||
@@ -134,7 +127,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
LightCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_light_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_light_command_request"), msg);
|
||||
#endif
|
||||
this->on_light_command_request(msg);
|
||||
break;
|
||||
@@ -145,7 +138,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
SwitchCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_switch_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_switch_command_request"), msg);
|
||||
#endif
|
||||
this->on_switch_command_request(msg);
|
||||
break;
|
||||
@@ -156,7 +149,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
SubscribeHomeassistantServicesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_homeassistant_services_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_homeassistant_services_request(msg);
|
||||
break;
|
||||
@@ -166,7 +159,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
GetTimeResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_get_time_response: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_get_time_response"), msg);
|
||||
#endif
|
||||
this->on_get_time_response(msg);
|
||||
break;
|
||||
@@ -176,7 +169,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
SubscribeHomeAssistantStatesRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_home_assistant_states_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_home_assistant_states_request(msg);
|
||||
break;
|
||||
@@ -187,7 +180,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
HomeAssistantStateResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_home_assistant_state_response: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_home_assistant_state_response"), msg);
|
||||
#endif
|
||||
this->on_home_assistant_state_response(msg);
|
||||
break;
|
||||
@@ -198,7 +191,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ExecuteServiceRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_execute_service_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_execute_service_request"), msg);
|
||||
#endif
|
||||
this->on_execute_service_request(msg);
|
||||
break;
|
||||
@@ -209,7 +202,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
CameraImageRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_camera_image_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_camera_image_request"), msg);
|
||||
#endif
|
||||
this->on_camera_image_request(msg);
|
||||
break;
|
||||
@@ -220,7 +213,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ClimateCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_climate_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_climate_command_request"), msg);
|
||||
#endif
|
||||
this->on_climate_command_request(msg);
|
||||
break;
|
||||
@@ -231,7 +224,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
NumberCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_number_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_number_command_request"), msg);
|
||||
#endif
|
||||
this->on_number_command_request(msg);
|
||||
break;
|
||||
@@ -242,7 +235,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
SelectCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_select_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_select_command_request"), msg);
|
||||
#endif
|
||||
this->on_select_command_request(msg);
|
||||
break;
|
||||
@@ -253,7 +246,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
SirenCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_siren_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_siren_command_request"), msg);
|
||||
#endif
|
||||
this->on_siren_command_request(msg);
|
||||
break;
|
||||
@@ -264,7 +257,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
LockCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_lock_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_lock_command_request"), msg);
|
||||
#endif
|
||||
this->on_lock_command_request(msg);
|
||||
break;
|
||||
@@ -275,7 +268,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ButtonCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_button_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_button_command_request"), msg);
|
||||
#endif
|
||||
this->on_button_command_request(msg);
|
||||
break;
|
||||
@@ -286,7 +279,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
MediaPlayerCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_media_player_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_media_player_command_request"), msg);
|
||||
#endif
|
||||
this->on_media_player_command_request(msg);
|
||||
break;
|
||||
@@ -297,7 +290,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
SubscribeBluetoothLEAdvertisementsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_le_advertisements_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_bluetooth_le_advertisements_request(msg);
|
||||
break;
|
||||
@@ -308,7 +301,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
BluetoothDeviceRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_bluetooth_device_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_bluetooth_device_request"), msg);
|
||||
#endif
|
||||
this->on_bluetooth_device_request(msg);
|
||||
break;
|
||||
@@ -319,7 +312,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
BluetoothGATTGetServicesRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_bluetooth_gatt_get_services_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_get_services_request"), msg);
|
||||
#endif
|
||||
this->on_bluetooth_gatt_get_services_request(msg);
|
||||
break;
|
||||
@@ -330,7 +323,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
BluetoothGATTReadRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_bluetooth_gatt_read_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_read_request"), msg);
|
||||
#endif
|
||||
this->on_bluetooth_gatt_read_request(msg);
|
||||
break;
|
||||
@@ -341,7 +334,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
BluetoothGATTWriteRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_bluetooth_gatt_write_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_write_request"), msg);
|
||||
#endif
|
||||
this->on_bluetooth_gatt_write_request(msg);
|
||||
break;
|
||||
@@ -352,7 +345,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
BluetoothGATTReadDescriptorRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_bluetooth_gatt_read_descriptor_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_read_descriptor_request"), msg);
|
||||
#endif
|
||||
this->on_bluetooth_gatt_read_descriptor_request(msg);
|
||||
break;
|
||||
@@ -363,7 +356,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
BluetoothGATTWriteDescriptorRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_bluetooth_gatt_write_descriptor_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_write_descriptor_request"), msg);
|
||||
#endif
|
||||
this->on_bluetooth_gatt_write_descriptor_request(msg);
|
||||
break;
|
||||
@@ -374,7 +367,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
BluetoothGATTNotifyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_bluetooth_gatt_notify_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_bluetooth_gatt_notify_request"), msg);
|
||||
#endif
|
||||
this->on_bluetooth_gatt_notify_request(msg);
|
||||
break;
|
||||
@@ -385,7 +378,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
SubscribeBluetoothConnectionsFreeRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_bluetooth_connections_free_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_bluetooth_connections_free_request(msg);
|
||||
break;
|
||||
@@ -396,7 +389,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
UnsubscribeBluetoothLEAdvertisementsRequest msg;
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_unsubscribe_bluetooth_le_advertisements_request"), msg);
|
||||
#endif
|
||||
this->on_unsubscribe_bluetooth_le_advertisements_request(msg);
|
||||
break;
|
||||
@@ -407,7 +400,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
SubscribeVoiceAssistantRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_voice_assistant_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_subscribe_voice_assistant_request"), msg);
|
||||
#endif
|
||||
this->on_subscribe_voice_assistant_request(msg);
|
||||
break;
|
||||
@@ -418,7 +411,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
VoiceAssistantResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_voice_assistant_response: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_voice_assistant_response"), msg);
|
||||
#endif
|
||||
this->on_voice_assistant_response(msg);
|
||||
break;
|
||||
@@ -429,7 +422,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
VoiceAssistantEventResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_voice_assistant_event_response"), msg);
|
||||
#endif
|
||||
this->on_voice_assistant_event_response(msg);
|
||||
break;
|
||||
@@ -440,7 +433,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
AlarmControlPanelCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_alarm_control_panel_command_request"), msg);
|
||||
#endif
|
||||
this->on_alarm_control_panel_command_request(msg);
|
||||
break;
|
||||
@@ -451,7 +444,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
TextCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_text_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_text_command_request"), msg);
|
||||
#endif
|
||||
this->on_text_command_request(msg);
|
||||
break;
|
||||
@@ -462,7 +455,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
DateCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_date_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_date_command_request"), msg);
|
||||
#endif
|
||||
this->on_date_command_request(msg);
|
||||
break;
|
||||
@@ -473,7 +466,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
TimeCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_time_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_time_command_request"), msg);
|
||||
#endif
|
||||
this->on_time_command_request(msg);
|
||||
break;
|
||||
@@ -484,7 +477,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
VoiceAssistantAudio msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_voice_assistant_audio: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_voice_assistant_audio"), msg);
|
||||
#endif
|
||||
this->on_voice_assistant_audio(msg);
|
||||
break;
|
||||
@@ -495,7 +488,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ValveCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_valve_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_valve_command_request"), msg);
|
||||
#endif
|
||||
this->on_valve_command_request(msg);
|
||||
break;
|
||||
@@ -506,7 +499,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
DateTimeCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_date_time_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_date_time_command_request"), msg);
|
||||
#endif
|
||||
this->on_date_time_command_request(msg);
|
||||
break;
|
||||
@@ -517,7 +510,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
VoiceAssistantTimerEventResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_voice_assistant_timer_event_response: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_voice_assistant_timer_event_response"), msg);
|
||||
#endif
|
||||
this->on_voice_assistant_timer_event_response(msg);
|
||||
break;
|
||||
@@ -528,7 +521,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
UpdateCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_update_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_update_command_request"), msg);
|
||||
#endif
|
||||
this->on_update_command_request(msg);
|
||||
break;
|
||||
@@ -539,7 +532,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
VoiceAssistantAnnounceRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_voice_assistant_announce_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_voice_assistant_announce_request"), msg);
|
||||
#endif
|
||||
this->on_voice_assistant_announce_request(msg);
|
||||
break;
|
||||
@@ -550,7 +543,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
VoiceAssistantConfigurationRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_voice_assistant_configuration_request"), msg);
|
||||
#endif
|
||||
this->on_voice_assistant_configuration_request(msg);
|
||||
break;
|
||||
@@ -561,7 +554,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
VoiceAssistantSetConfiguration msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_voice_assistant_set_configuration: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_voice_assistant_set_configuration"), msg);
|
||||
#endif
|
||||
this->on_voice_assistant_set_configuration(msg);
|
||||
break;
|
||||
@@ -572,7 +565,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
NoiseEncryptionSetKeyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_noise_encryption_set_key_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_noise_encryption_set_key_request"), msg);
|
||||
#endif
|
||||
this->on_noise_encryption_set_key_request(msg);
|
||||
break;
|
||||
@@ -583,7 +576,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
BluetoothScannerSetModeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_bluetooth_scanner_set_mode_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_bluetooth_scanner_set_mode_request"), msg);
|
||||
#endif
|
||||
this->on_bluetooth_scanner_set_mode_request(msg);
|
||||
break;
|
||||
@@ -594,7 +587,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ZWaveProxyFrame msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_z_wave_proxy_frame: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_z_wave_proxy_frame"), msg);
|
||||
#endif
|
||||
this->on_z_wave_proxy_frame(msg);
|
||||
break;
|
||||
@@ -605,7 +598,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
ZWaveProxyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_z_wave_proxy_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_z_wave_proxy_request"), msg);
|
||||
#endif
|
||||
this->on_z_wave_proxy_request(msg);
|
||||
break;
|
||||
@@ -616,7 +609,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
HomeassistantActionResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_homeassistant_action_response"), msg);
|
||||
#endif
|
||||
this->on_homeassistant_action_response(msg);
|
||||
break;
|
||||
@@ -627,11 +620,22 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
WaterHeaterCommandRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_water_heater_command_request: %s", msg.dump().c_str());
|
||||
this->log_receive_message_(LOG_STR("on_water_heater_command_request"), msg);
|
||||
#endif
|
||||
this->on_water_heater_command_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: {
|
||||
InfraredRFTransmitRawTimingsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_infrared_rf_transmit_raw_timings_request"), msg);
|
||||
#endif
|
||||
this->on_infrared_rf_transmit_raw_timings_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
@@ -643,13 +647,6 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_PASSWORD
|
||||
void APIServerConnection::on_authentication_request(const AuthenticationRequest &msg) {
|
||||
if (!this->send_authenticate_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
|
||||
if (!this->send_disconnect_response(msg)) {
|
||||
this->on_fatal_error();
|
||||
@@ -837,14 +834,16 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
void APIServerConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
|
||||
this->infrared_rf_transmit_raw_timings(msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
// Check authentication/connection requirements for messages
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||
#ifdef USE_API_PASSWORD
|
||||
case AuthenticationRequest::MESSAGE_TYPE: // No setup required
|
||||
#endif
|
||||
case HelloRequest::MESSAGE_TYPE: // No setup required
|
||||
case DisconnectRequest::MESSAGE_TYPE: // No setup required
|
||||
case PingRequest::MESSAGE_TYPE: // No setup required
|
||||
break; // Skip all checks for these messages
|
||||
|
||||
@@ -12,24 +12,22 @@ class APIServerConnectionBase : public ProtoService {
|
||||
public:
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
protected:
|
||||
void log_send_message_(const char *name, const std::string &dump);
|
||||
void log_send_message_(const char *name, const char *dump);
|
||||
void log_receive_message_(const LogString *name, const ProtoMessage &msg);
|
||||
|
||||
public:
|
||||
#endif
|
||||
|
||||
bool send_message(const ProtoMessage &msg, uint8_t message_type) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_send_message_(msg.message_name(), msg.dump());
|
||||
DumpBuffer dump_buf;
|
||||
this->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
|
||||
#endif
|
||||
return this->send_message_(msg, message_type);
|
||||
}
|
||||
|
||||
virtual void on_hello_request(const HelloRequest &value){};
|
||||
|
||||
#ifdef USE_API_PASSWORD
|
||||
virtual void on_authentication_request(const AuthenticationRequest &value){};
|
||||
#endif
|
||||
|
||||
virtual void on_disconnect_request(const DisconnectRequest &value){};
|
||||
virtual void on_disconnect_response(const DisconnectResponse &value){};
|
||||
virtual void on_ping_request(const PingRequest &value){};
|
||||
@@ -221,6 +219,11 @@ class APIServerConnectionBase : public ProtoService {
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
@@ -228,9 +231,6 @@ class APIServerConnectionBase : public ProtoService {
|
||||
class APIServerConnection : public APIServerConnectionBase {
|
||||
public:
|
||||
virtual bool send_hello_response(const HelloRequest &msg) = 0;
|
||||
#ifdef USE_API_PASSWORD
|
||||
virtual bool send_authenticate_response(const AuthenticationRequest &msg) = 0;
|
||||
#endif
|
||||
virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
|
||||
virtual bool send_ping_response(const PingRequest &msg) = 0;
|
||||
virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
|
||||
@@ -354,12 +354,12 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
virtual void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) = 0;
|
||||
#endif
|
||||
protected:
|
||||
void on_hello_request(const HelloRequest &msg) override;
|
||||
#ifdef USE_API_PASSWORD
|
||||
void on_authentication_request(const AuthenticationRequest &msg) override;
|
||||
#endif
|
||||
void on_disconnect_request(const DisconnectRequest &msg) override;
|
||||
void on_ping_request(const PingRequest &msg) override;
|
||||
void on_device_info_request(const DeviceInfoRequest &msg) override;
|
||||
@@ -483,6 +483,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#endif
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_IR_RF
|
||||
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override;
|
||||
#endif
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
@@ -125,15 +125,18 @@ void APIServer::loop() {
|
||||
if (!sock)
|
||||
break;
|
||||
|
||||
char peername[socket::SOCKADDR_STR_LEN];
|
||||
sock->getpeername_to(peername);
|
||||
|
||||
// Check if we're at the connection limit
|
||||
if (this->clients_.size() >= this->max_connections_) {
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str());
|
||||
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
|
||||
// Immediately close - socket destructor will handle cleanup
|
||||
sock.reset();
|
||||
continue;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
|
||||
ESP_LOGD(TAG, "Accept %s", peername);
|
||||
|
||||
auto *conn = new APIConnection(std::move(sock), this);
|
||||
this->clients_.emplace_back(conn);
|
||||
@@ -166,8 +169,7 @@ void APIServer::loop() {
|
||||
// Network is down - disconnect all clients
|
||||
for (auto &client : this->clients_) {
|
||||
client->on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(),
|
||||
client->client_info_.peername.c_str());
|
||||
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
|
||||
}
|
||||
// Continue to process and clean up the clients below
|
||||
}
|
||||
@@ -184,13 +186,16 @@ void APIServer::loop() {
|
||||
}
|
||||
|
||||
// Rare case: handle disconnection
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
this->unregister_active_action_calls_for_connection(client.get());
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
|
||||
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Save client info before removal for the trigger
|
||||
std::string client_name(client->get_name());
|
||||
std::string client_peername(client->get_peername());
|
||||
#endif
|
||||
|
||||
// Swap with the last element and pop (avoids expensive vector shifts)
|
||||
if (client_index < this->clients_.size() - 1) {
|
||||
@@ -203,6 +208,11 @@ void APIServer::loop() {
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
|
||||
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
|
||||
// Fire trigger after client is removed so api.connected reflects the true state
|
||||
this->client_disconnected_trigger_->trigger(client_name, client_peername);
|
||||
#endif
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
}
|
||||
@@ -224,38 +234,6 @@ void APIServer::dump_config() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_API_PASSWORD
|
||||
bool APIServer::check_password(const uint8_t *password_data, size_t password_len) const {
|
||||
// depend only on input password length
|
||||
const char *a = this->password_.c_str();
|
||||
uint32_t len_a = this->password_.length();
|
||||
const char *b = reinterpret_cast<const char *>(password_data);
|
||||
uint32_t len_b = password_len;
|
||||
|
||||
// disable optimization with volatile
|
||||
volatile uint32_t length = len_b;
|
||||
volatile const char *left = nullptr;
|
||||
volatile const char *right = b;
|
||||
uint8_t result = 0;
|
||||
|
||||
if (len_a == length) {
|
||||
left = *((volatile const char **) &a);
|
||||
result = 0;
|
||||
}
|
||||
if (len_a != length) {
|
||||
left = b;
|
||||
result = 1;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < length; i++) {
|
||||
result |= *left++ ^ *right++; // NOLINT
|
||||
}
|
||||
|
||||
return result == 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
void APIServer::handle_disconnect(APIConnection *conn) {}
|
||||
|
||||
// Macro for controller update dispatch
|
||||
@@ -369,6 +347,21 @@ void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IR_RF
|
||||
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
|
||||
const std::vector<int32_t> *timings) {
|
||||
InfraredRFReceiveEvent resp{};
|
||||
#ifdef USE_DEVICES
|
||||
resp.device_id = device_id;
|
||||
#endif
|
||||
resp.key = key;
|
||||
resp.timings = timings;
|
||||
|
||||
for (auto &c : this->clients_)
|
||||
c->send_infrared_rf_receive_event(resp);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
|
||||
#endif
|
||||
@@ -377,10 +370,6 @@ float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI;
|
||||
|
||||
void APIServer::set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
#ifdef USE_API_PASSWORD
|
||||
void APIServer::set_password(const std::string &password) { this->password_ = password; }
|
||||
#endif
|
||||
|
||||
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
@@ -394,7 +383,7 @@ void APIServer::register_action_response_callback(uint32_t call_id, ActionRespon
|
||||
this->action_response_callbacks_.push_back({call_id, std::move(callback)});
|
||||
}
|
||||
|
||||
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
|
||||
void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message) {
|
||||
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
|
||||
if (it->call_id == call_id) {
|
||||
auto callback = std::move(it->callback);
|
||||
@@ -406,7 +395,7 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||
void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message,
|
||||
const uint8_t *response_data, size_t response_data_len) {
|
||||
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
|
||||
if (it->call_id == call_id) {
|
||||
@@ -424,8 +413,8 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
|
||||
|
||||
#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(std::string)> 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)
|
||||
@@ -434,7 +423,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(std::string)> 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));
|
||||
@@ -454,23 +443,43 @@ 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(std::string)> 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(std::string)> f) {
|
||||
std::function<void(StringRef)> f) {
|
||||
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
|
||||
}
|
||||
|
||||
// Existing std::string overload (for custom_api_device.h - heap allocation)
|
||||
// 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(std::string)> 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(std::string)> 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) {
|
||||
// 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()); }),
|
||||
once);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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) {
|
||||
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
|
||||
}
|
||||
|
||||
@@ -678,7 +687,7 @@ void APIServer::unregister_active_action_calls_for_connection(APIConnection *con
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message) {
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message) {
|
||||
for (auto &call : this->active_action_calls_) {
|
||||
if (call.action_call_id == action_call_id) {
|
||||
call.connection->send_execute_service_response(call.client_call_id, success, error_message);
|
||||
@@ -688,7 +697,7 @@ void APIServer::send_action_response(uint32_t action_call_id, bool success, cons
|
||||
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
|
||||
}
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, const std::string &error_message,
|
||||
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
|
||||
const uint8_t *response_data, size_t response_data_len) {
|
||||
for (auto &call : this->active_action_calls_) {
|
||||
if (call.action_call_id == action_call_id) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/controller.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "list_entities.h"
|
||||
#include "subscribe_state.h"
|
||||
#ifdef USE_LOGGER
|
||||
@@ -59,10 +60,6 @@ class APIServer : public Component,
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
|
||||
#endif
|
||||
#ifdef USE_API_PASSWORD
|
||||
bool check_password(const uint8_t *password_data, size_t password_len) const;
|
||||
void set_password(const std::string &password);
|
||||
#endif
|
||||
void set_port(uint16_t port);
|
||||
void set_reboot_timeout(uint32_t reboot_timeout);
|
||||
@@ -143,10 +140,10 @@ class APIServer : public Component,
|
||||
// Action response handling
|
||||
using ActionResponseCallback = std::function<void(const class ActionResponse &)>;
|
||||
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
|
||||
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
|
||||
void handle_action_response(uint32_t call_id, bool success, StringRef error_message);
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
|
||||
const uint8_t *response_data, size_t response_data_len);
|
||||
void handle_action_response(uint32_t call_id, bool success, StringRef error_message, const uint8_t *response_data,
|
||||
size_t response_data_len);
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
@@ -165,9 +162,9 @@ class APIServer : public Component,
|
||||
void unregister_active_action_call(uint32_t action_call_id);
|
||||
void unregister_active_action_calls_for_connection(APIConnection *conn);
|
||||
// Send response for a specific action call (uses action_call_id, sends client_call_id in response)
|
||||
void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message);
|
||||
void send_action_response(uint32_t action_call_id, bool success, StringRef error_message);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
void send_action_response(uint32_t action_call_id, bool success, const std::string &error_message,
|
||||
void send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
|
||||
const uint8_t *response_data, size_t response_data_len);
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
@@ -188,6 +185,9 @@ class APIServer : public Component,
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_zwave_proxy_request(const esphome::api::ProtoMessage &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;
|
||||
|
||||
@@ -195,7 +195,7 @@ class APIServer : public Component,
|
||||
struct HomeAssistantStateSubscription {
|
||||
const char *entity_id; // Pointer to flash (internal) or heap (external)
|
||||
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute)
|
||||
std::function<void(std::string)> callback;
|
||||
std::function<void(StringRef)> callback;
|
||||
bool once;
|
||||
|
||||
// Dynamic storage for external components using std::string API (custom_api_device.h)
|
||||
@@ -205,14 +205,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(std::string)> f);
|
||||
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> 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);
|
||||
|
||||
// Existing std::string overload (for custom_api_device.h - heap allocation)
|
||||
// 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(std::string)> f);
|
||||
std::function<void(StringRef)> f);
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(std::string)> 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);
|
||||
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
std::function<void(const std::string &)> f);
|
||||
|
||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||
#endif
|
||||
@@ -236,10 +242,13 @@ 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(std::string)> 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(std::string)> f, bool once);
|
||||
std::function<void(const std::string &)> f, bool once);
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
// Pointers and pointer-like types first (4 bytes each)
|
||||
std::unique_ptr<socket::Socket> socket_ = nullptr;
|
||||
@@ -256,9 +265,6 @@ class APIServer : public Component,
|
||||
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
std::vector<std::unique_ptr<APIConnection>> clients_;
|
||||
#ifdef USE_API_PASSWORD
|
||||
std::string password_;
|
||||
#endif
|
||||
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||
|
||||
@@ -16,7 +16,7 @@ with warnings.catch_warnings():
|
||||
|
||||
import contextlib
|
||||
|
||||
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
|
||||
from esphome.const import CONF_KEY, CONF_PORT, __version__
|
||||
from esphome.core import CORE
|
||||
|
||||
from . import CONF_ENCRYPTION
|
||||
@@ -35,7 +35,6 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
conf = config["api"]
|
||||
name = config["esphome"]["name"]
|
||||
port: int = int(conf[CONF_PORT])
|
||||
password: str = conf[CONF_PASSWORD]
|
||||
noise_psk: str | None = None
|
||||
if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
|
||||
noise_psk = key
|
||||
@@ -50,7 +49,7 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
cli = APIClient(
|
||||
addresses[0], # Primary address for compatibility
|
||||
port,
|
||||
password,
|
||||
"", # Password auth removed in 2026.1.0
|
||||
client_info=f"ESPHome Logs {__version__}",
|
||||
noise_psk=noise_psk,
|
||||
addresses=addresses, # Pass all addresses for automatic retry
|
||||
|
||||
@@ -122,21 +122,36 @@ class CustomAPIDevice {
|
||||
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature");
|
||||
* }
|
||||
*
|
||||
* void on_state_changed(std::string state) {
|
||||
* // State of sensor.weather_forecast is `state`
|
||||
* void on_state_changed(StringRef state) {
|
||||
* // State of climate.kitchen current_temperature is `state`
|
||||
* // Use state.c_str() for C string, state.str() for std::string
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @tparam T The class type creating the service, automatically deduced from the function pointer.
|
||||
* @param callback The member function to call when the entity state changes.
|
||||
* @param callback The member function to call when the entity state changes (zero-allocation).
|
||||
* @param entity_id The entity_id to track.
|
||||
* @param attribute The entity state attribute to track.
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
|
||||
*
|
||||
* @deprecated Use the StringRef overload for zero-allocation callbacks. Will be removed in 2027.1.0.
|
||||
*/
|
||||
template<typename T>
|
||||
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);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
|
||||
// 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));
|
||||
}
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
|
||||
@@ -148,23 +163,45 @@ class CustomAPIDevice {
|
||||
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
|
||||
* }
|
||||
*
|
||||
* void on_state_changed(std::string entity_id, std::string state) {
|
||||
* void on_state_changed(const std::string &entity_id, StringRef state) {
|
||||
* // State of `entity_id` is `state`
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @tparam T The class type creating the service, automatically deduced from the function pointer.
|
||||
* @param callback The member function to call when the entity state changes.
|
||||
* @param callback The member function to call when the entity state changes (zero-allocation for state).
|
||||
* @param entity_id The entity_id to track.
|
||||
* @param attribute The entity state attribute to track.
|
||||
*/
|
||||
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));
|
||||
}
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
|
||||
*
|
||||
* @deprecated Use the StringRef overload for zero-allocation callbacks. Will be removed in 2027.1.0.
|
||||
*/
|
||||
template<typename T>
|
||||
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);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
|
||||
// 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));
|
||||
}
|
||||
#else
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
|
||||
"of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
@@ -173,6 +210,14 @@ class CustomAPIDevice {
|
||||
"of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
|
||||
"of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
@@ -195,7 +240,7 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -215,12 +260,12 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
kv.key = StringRef(it.first);
|
||||
kv.value = StringRef(it.second); // data map lives until send completes
|
||||
}
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
@@ -237,7 +282,7 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &event_name) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(event_name));
|
||||
resp.service = StringRef(event_name);
|
||||
resp.is_event = true;
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
@@ -257,13 +302,13 @@ class CustomAPIDevice {
|
||||
*/
|
||||
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
HomeassistantActionRequest resp;
|
||||
resp.set_service(StringRef(service_name));
|
||||
resp.service = StringRef(service_name);
|
||||
resp.is_event = true;
|
||||
resp.data.init(data.size());
|
||||
for (auto &it : data) {
|
||||
auto &kv = resp.data.emplace_back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.value = it.second;
|
||||
kv.key = StringRef(it.first);
|
||||
kv.value = StringRef(it.second); // data map lives until send completes
|
||||
}
|
||||
global_api_server->send_homeassistant_action(resp);
|
||||
}
|
||||
|
||||
@@ -67,10 +67,10 @@ template<typename... Ts> class TemplatableKeyValuePair {
|
||||
// the callback is invoked synchronously while the message is on the stack).
|
||||
class ActionResponse {
|
||||
public:
|
||||
ActionResponse(bool success, const std::string &error_message) : success_(success), error_message_(error_message) {}
|
||||
ActionResponse(bool success, StringRef error_message) : success_(success), error_message_(error_message) {}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
ActionResponse(bool success, const std::string &error_message, const uint8_t *data, size_t data_len)
|
||||
ActionResponse(bool success, StringRef error_message, const uint8_t *data, size_t data_len)
|
||||
: success_(success), error_message_(error_message) {
|
||||
if (data == nullptr || data_len == 0)
|
||||
return;
|
||||
@@ -147,13 +147,23 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
void play(const Ts &...x) override {
|
||||
HomeassistantActionRequest resp;
|
||||
std::string service_value = this->service_.value(x...);
|
||||
resp.set_service(StringRef(service_value));
|
||||
resp.service = StringRef(service_value);
|
||||
resp.is_event = this->flags_.is_event;
|
||||
this->populate_service_map(resp.data, this->data_, x...);
|
||||
this->populate_service_map(resp.data_template, this->data_template_, x...);
|
||||
this->populate_service_map(resp.variables, this->variables_, x...);
|
||||
|
||||
// Local storage for lambda-evaluated strings - lives until after send
|
||||
FixedVector<std::string> data_storage;
|
||||
FixedVector<std::string> data_template_storage;
|
||||
FixedVector<std::string> variables_storage;
|
||||
|
||||
this->populate_service_map(resp.data, this->data_, data_storage, x...);
|
||||
this->populate_service_map(resp.data_template, this->data_template_, data_template_storage, x...);
|
||||
this->populate_service_map(resp.variables, this->variables_, variables_storage, x...);
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
// IMPORTANT: Declare at outer scope so it lives until send_homeassistant_action returns.
|
||||
std::string response_template_value;
|
||||
#endif
|
||||
if (this->flags_.wants_status) {
|
||||
// Generate a unique call ID for this service call
|
||||
static uint32_t call_id_counter = 1;
|
||||
@@ -164,8 +174,8 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
resp.wants_response = true;
|
||||
// Set response template if provided
|
||||
if (this->flags_.has_response_template) {
|
||||
std::string response_template_value = this->response_template_.value(x...);
|
||||
resp.response_template = response_template_value;
|
||||
response_template_value = this->response_template_.value(x...);
|
||||
resp.response_template = StringRef(response_template_value);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -205,12 +215,31 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
}
|
||||
|
||||
template<typename VectorType, typename SourceType>
|
||||
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
|
||||
static void populate_service_map(VectorType &dest, SourceType &source, FixedVector<std::string> &value_storage,
|
||||
Ts... x) {
|
||||
dest.init(source.size());
|
||||
|
||||
// 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()) {
|
||||
lambda_count++;
|
||||
}
|
||||
}
|
||||
value_storage.init(lambda_count);
|
||||
|
||||
for (auto &it : source) {
|
||||
auto &kv = dest.emplace_back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
kv.value = it.value.value(x...);
|
||||
kv.key = StringRef(it.key);
|
||||
|
||||
if (it.value.is_static_string()) {
|
||||
// Static string from YAML - zero allocation
|
||||
kv.value = StringRef(it.value.get_static_string());
|
||||
} else {
|
||||
// Lambda evaluation - store result, reference it
|
||||
value_storage.push_back(it.value.value(x...));
|
||||
kv.value = StringRef(value_storage.back());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,9 @@ LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPane
|
||||
#ifdef USE_WATER_HEATER
|
||||
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
LIST_ENTITIES_HANDLER(infrared, infrared::Infrared, ListEntitiesInfraredResponse)
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
|
||||
#endif
|
||||
|
||||
@@ -85,6 +85,9 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool on_water_heater(water_heater::WaterHeater *entity) override;
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
bool on_infrared(infrared::Infrared *entity) override;
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *entity) override;
|
||||
#endif
|
||||
|
||||
@@ -139,12 +139,4 @@ void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
std::string ProtoMessage::dump() const {
|
||||
std::string out;
|
||||
this->dump_to(out);
|
||||
return out;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -39,6 +39,24 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) {
|
||||
return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1);
|
||||
}
|
||||
|
||||
/// Count number of varints in a packed buffer
|
||||
inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
|
||||
uint16_t count = 0;
|
||||
while (len > 0) {
|
||||
// Skip varint bytes until we find one without continuation bit
|
||||
while (len > 0 && (*data & 0x80)) {
|
||||
data++;
|
||||
len--;
|
||||
}
|
||||
if (len > 0) {
|
||||
data++;
|
||||
len--;
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/*
|
||||
* StringRef Ownership Model for API Protocol Messages
|
||||
* ===================================================
|
||||
@@ -54,16 +72,16 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) {
|
||||
* 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe
|
||||
* 4. Local variables: Safe ONLY if encoding happens before function returns:
|
||||
* std::string temp = compute_value();
|
||||
* msg.set_field(StringRef(temp));
|
||||
* msg.field = StringRef(temp);
|
||||
* return this->send_message(msg); // temp is valid during encoding
|
||||
*
|
||||
* Unsafe Patterns (WILL cause crashes/corruption):
|
||||
* 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value
|
||||
* 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
|
||||
* 1. Temporaries: msg.field = StringRef(obj.get_string()) // get_string() returns by value
|
||||
* 2. Concatenation: msg.field = StringRef(str1 + str2) // Result is temporary
|
||||
*
|
||||
* For unsafe patterns, store in a local variable first:
|
||||
* std::string temp = get_string(); // or str1 + str2
|
||||
* msg.set_field(StringRef(temp));
|
||||
* msg.field = StringRef(temp);
|
||||
*
|
||||
* The send_*_response pattern ensures proper lifetime management by encoding
|
||||
* within the same function scope where temporaries are created.
|
||||
@@ -180,9 +198,10 @@ class ProtoVarInt {
|
||||
uint64_t value_;
|
||||
};
|
||||
|
||||
// Forward declaration for decode_to_message and encode_to_writer
|
||||
class ProtoMessage;
|
||||
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
|
||||
class ProtoDecodableMessage;
|
||||
class ProtoMessage;
|
||||
class ProtoSize;
|
||||
|
||||
class ProtoLengthDelimited {
|
||||
public:
|
||||
@@ -334,6 +353,8 @@ class ProtoWriteBuffer {
|
||||
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
|
||||
this->encode_uint64(field_id, encode_zigzag64(value), force);
|
||||
}
|
||||
/// 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_; }
|
||||
|
||||
@@ -341,8 +362,62 @@ class ProtoWriteBuffer {
|
||||
std::vector<uint8_t> *buffer_;
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class ProtoSize;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
/**
|
||||
* Fixed-size buffer for message dumps - avoids heap allocation.
|
||||
* Sized to match the logger's default tx_buffer_size (512 bytes)
|
||||
* since anything larger gets truncated anyway.
|
||||
*/
|
||||
class DumpBuffer {
|
||||
public:
|
||||
// Matches default tx_buffer_size in logger component
|
||||
static constexpr size_t CAPACITY = 512;
|
||||
|
||||
DumpBuffer() : pos_(0) { buf_[0] = '\0'; }
|
||||
|
||||
DumpBuffer &append(const char *str) {
|
||||
if (str) {
|
||||
append_impl_(str, strlen(str));
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
DumpBuffer &append(const char *str, size_t len) {
|
||||
append_impl_(str, len);
|
||||
return *this;
|
||||
}
|
||||
|
||||
DumpBuffer &append(size_t n, char c) {
|
||||
size_t space = CAPACITY - 1 - pos_;
|
||||
if (n > space)
|
||||
n = space;
|
||||
if (n > 0) {
|
||||
memset(buf_ + pos_, c, n);
|
||||
pos_ += n;
|
||||
buf_[pos_] = '\0';
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
const char *c_str() const { return buf_; }
|
||||
size_t size() const { return pos_; }
|
||||
|
||||
private:
|
||||
void append_impl_(const char *str, size_t len) {
|
||||
size_t space = CAPACITY - 1 - pos_;
|
||||
if (len > space)
|
||||
len = space;
|
||||
if (len > 0) {
|
||||
memcpy(buf_ + pos_, str, len);
|
||||
pos_ += len;
|
||||
buf_[pos_] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
char buf_[CAPACITY];
|
||||
size_t pos_;
|
||||
};
|
||||
#endif
|
||||
|
||||
class ProtoMessage {
|
||||
public:
|
||||
@@ -352,8 +427,7 @@ class ProtoMessage {
|
||||
// Default implementation for messages with no fields
|
||||
virtual void calculate_size(ProtoSize &size) const {}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
std::string dump() const;
|
||||
virtual void dump_to(std::string &out) const = 0;
|
||||
virtual const char *dump_to(DumpBuffer &out) const = 0;
|
||||
virtual const char *message_name() const { return "unknown"; }
|
||||
#endif
|
||||
};
|
||||
@@ -792,8 +866,43 @@ class ProtoSize {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
};
|
||||
|
||||
// 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())
|
||||
return;
|
||||
|
||||
// Calculate packed size
|
||||
size_t packed_size = 0;
|
||||
for (int value : values) {
|
||||
packed_size += ProtoSize::varint(encode_zigzag32(value));
|
||||
}
|
||||
|
||||
// Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values
|
||||
this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED);
|
||||
this->encode_varint_raw(packed_size);
|
||||
for (int value : values) {
|
||||
this->encode_varint_raw(encode_zigzag32(value));
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -833,9 +942,6 @@ class ProtoService {
|
||||
virtual bool is_authenticated() = 0;
|
||||
virtual bool is_connection_setup() = 0;
|
||||
virtual void on_fatal_error() = 0;
|
||||
#ifdef USE_API_PASSWORD
|
||||
virtual void on_unauthenticated_access() = 0;
|
||||
#endif
|
||||
virtual void on_no_setup_connection() = 0;
|
||||
/**
|
||||
* Create a buffer with a reserved size.
|
||||
@@ -873,20 +979,7 @@ class ProtoService {
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool check_authenticated_() {
|
||||
#ifdef USE_API_PASSWORD
|
||||
if (!this->check_connection_setup_()) {
|
||||
return false;
|
||||
}
|
||||
if (!this->is_authenticated()) {
|
||||
this->on_unauthenticated_access();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
#else
|
||||
return this->check_connection_setup_();
|
||||
#endif
|
||||
}
|
||||
inline bool check_authenticated_() { return this->check_connection_setup_(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -79,6 +79,9 @@ class InitialStateIterator : public ComponentIterator {
|
||||
#ifdef USE_WATER_HEATER
|
||||
bool on_water_heater(water_heater::WaterHeater *entity) override;
|
||||
#endif
|
||||
#ifdef USE_INFRARED
|
||||
bool on_infrared(infrared::Infrared *infrared) override { return true; };
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
bool on_event(event::Event *event) override { return true; };
|
||||
#endif
|
||||
|
||||
@@ -46,7 +46,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.name = StringRef(this->name_);
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = this->supports_response_;
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
@@ -54,7 +54,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.set_name(StringRef(this->arg_names_[i]));
|
||||
arg.name = StringRef(this->arg_names_[i]);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.set_name(StringRef(this->name_));
|
||||
msg.name = StringRef(this->name_);
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
@@ -116,7 +116,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.set_name(StringRef(this->arg_names_[i]));
|
||||
arg.name = StringRef(this->arg_names_[i]);
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
@@ -255,7 +255,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
bool return_response = std::get<1>(args);
|
||||
if (!return_response) {
|
||||
// Client doesn't want response data, just send success/error
|
||||
this->parent_->send_action_response(call_id, success, error_message);
|
||||
this->parent_->send_action_response(call_id, success, StringRef(error_message));
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -265,12 +265,12 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
|
||||
json::JsonBuilder builder;
|
||||
this->json_builder_(x..., builder.root());
|
||||
std::string json_str = builder.serialize();
|
||||
this->parent_->send_action_response(call_id, success, error_message,
|
||||
this->parent_->send_action_response(call_id, success, StringRef(error_message),
|
||||
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->parent_->send_action_response(call_id, success, error_message);
|
||||
this->parent_->send_action_response(call_id, success, StringRef(error_message));
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace esphome::aqi {
|
||||
|
||||
class AbstractAQICalculator {
|
||||
public:
|
||||
virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
|
||||
virtual uint16_t get_aqi(float pm2_5_value, float pm10_0_value) = 0;
|
||||
};
|
||||
|
||||
} // namespace esphome::aqi
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <climits>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
|
||||
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
|
||||
@@ -9,39 +10,41 @@ namespace esphome::aqi {
|
||||
|
||||
class AQICalculator : public AbstractAQICalculator {
|
||||
public:
|
||||
uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
|
||||
int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_);
|
||||
int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_);
|
||||
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index;
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
}
|
||||
|
||||
protected:
|
||||
static const int AMOUNT_OF_LEVELS = 6;
|
||||
static constexpr int NUM_LEVELS = 6;
|
||||
|
||||
int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
|
||||
|
||||
int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55},
|
||||
{56, 125}, {126, 225}, {226, INT_MAX}};
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f},
|
||||
{35.5f, 55.4f}, {55.5f, 125.4f},
|
||||
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}};
|
||||
|
||||
int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254},
|
||||
{255, 354}, {355, 424}, {425, INT_MAX}};
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f},
|
||||
{155.0f, 254.0f}, {255.0f, 354.0f},
|
||||
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}};
|
||||
|
||||
int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
|
||||
int grid_index = get_grid_index_(value, array);
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
if (grid_index == -1) {
|
||||
return -1;
|
||||
return -1.0f;
|
||||
}
|
||||
int aqi_lo = index_grid_[grid_index][0];
|
||||
int aqi_hi = index_grid_[grid_index][1];
|
||||
int conc_lo = array[grid_index][0];
|
||||
int conc_hi = array[grid_index][1];
|
||||
float aqi_lo = INDEX_GRID[grid_index][0];
|
||||
float aqi_hi = INDEX_GRID[grid_index][1];
|
||||
float conc_lo = array[grid_index][0];
|
||||
float conc_hi = array[grid_index][1];
|
||||
|
||||
return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
|
||||
}
|
||||
|
||||
int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
|
||||
for (int i = 0; i < AMOUNT_OF_LEVELS; i++) {
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
return i;
|
||||
}
|
||||
|
||||
51
esphome/components/aqi/aqi_sensor.cpp
Normal file
51
esphome/components/aqi/aqi_sensor.cpp
Normal file
@@ -0,0 +1,51 @@
|
||||
#include "aqi_sensor.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::aqi {
|
||||
|
||||
static const char *const TAG = "aqi";
|
||||
|
||||
void AQISensor::setup() {
|
||||
if (this->pm_2_5_sensor_ != nullptr) {
|
||||
this->pm_2_5_sensor_->add_on_state_callback([this](float value) {
|
||||
this->pm_2_5_value_ = value;
|
||||
// Defer calculation to avoid double-publishing if both sensors update in the same loop
|
||||
this->defer("update", [this]() { this->calculate_aqi_(); });
|
||||
});
|
||||
}
|
||||
if (this->pm_10_0_sensor_ != nullptr) {
|
||||
this->pm_10_0_sensor_->add_on_state_callback([this](float value) {
|
||||
this->pm_10_0_value_ = value;
|
||||
this->defer("update", [this]() { this->calculate_aqi_(); });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void AQISensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AQI Sensor:");
|
||||
ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI");
|
||||
if (this->pm_2_5_sensor_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str());
|
||||
}
|
||||
if (this->pm_10_0_sensor_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " PM10 Sensor: '%s'", this->pm_10_0_sensor_->get_name().c_str());
|
||||
}
|
||||
LOG_SENSOR(" ", "AQI", this);
|
||||
}
|
||||
|
||||
void AQISensor::calculate_aqi_() {
|
||||
if (std::isnan(this->pm_2_5_value_) || std::isnan(this->pm_10_0_value_)) {
|
||||
return;
|
||||
}
|
||||
|
||||
AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
|
||||
if (calculator == nullptr) {
|
||||
ESP_LOGW(TAG, "Unknown AQI calculator type");
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t aqi = calculator->get_aqi(this->pm_2_5_value_, this->pm_10_0_value_);
|
||||
this->publish_state(aqi);
|
||||
}
|
||||
|
||||
} // namespace esphome::aqi
|
||||
31
esphome/components/aqi/aqi_sensor.h
Normal file
31
esphome/components/aqi/aqi_sensor.h
Normal file
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "aqi_calculator_factory.h"
|
||||
|
||||
namespace esphome::aqi {
|
||||
|
||||
class AQISensor : public sensor::Sensor, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
|
||||
void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; }
|
||||
void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; }
|
||||
void set_aqi_calculation_type(AQICalculatorType type) { this->aqi_calc_type_ = type; }
|
||||
|
||||
protected:
|
||||
void calculate_aqi_();
|
||||
|
||||
sensor::Sensor *pm_2_5_sensor_{nullptr};
|
||||
sensor::Sensor *pm_10_0_sensor_{nullptr};
|
||||
AQICalculatorType aqi_calc_type_{AQI_TYPE};
|
||||
AQICalculatorFactory aqi_calculator_factory_;
|
||||
|
||||
float pm_2_5_value_{NAN};
|
||||
float pm_10_0_value_{NAN};
|
||||
};
|
||||
|
||||
} // namespace esphome::aqi
|
||||
@@ -1,44 +1,47 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include "abstract_aqi_calculator.h"
|
||||
|
||||
namespace esphome::aqi {
|
||||
|
||||
class CAQICalculator : public AbstractAQICalculator {
|
||||
public:
|
||||
uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
|
||||
int pm2_5_index = calculate_index_(pm2_5_value, pm2_5_calculation_grid_);
|
||||
int pm10_0_index = calculate_index_(pm10_0_value, pm10_0_calculation_grid_);
|
||||
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
|
||||
return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index;
|
||||
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
|
||||
}
|
||||
|
||||
protected:
|
||||
static const int AMOUNT_OF_LEVELS = 5;
|
||||
static constexpr int NUM_LEVELS = 5;
|
||||
|
||||
int index_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
|
||||
|
||||
int pm2_5_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}};
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
{0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}};
|
||||
|
||||
int pm10_0_calculation_grid_[AMOUNT_OF_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}};
|
||||
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
|
||||
{0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}};
|
||||
|
||||
int calculate_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
|
||||
int grid_index = get_grid_index_(value, array);
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
if (grid_index == -1) {
|
||||
return -1;
|
||||
return -1.0f;
|
||||
}
|
||||
|
||||
int aqi_lo = index_grid_[grid_index][0];
|
||||
int aqi_hi = index_grid_[grid_index][1];
|
||||
int conc_lo = array[grid_index][0];
|
||||
int conc_hi = array[grid_index][1];
|
||||
float aqi_lo = INDEX_GRID[grid_index][0];
|
||||
float aqi_hi = INDEX_GRID[grid_index][1];
|
||||
float conc_lo = array[grid_index][0];
|
||||
float conc_hi = array[grid_index][1];
|
||||
|
||||
return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
|
||||
}
|
||||
|
||||
int get_grid_index_(uint16_t value, int array[AMOUNT_OF_LEVELS][2]) {
|
||||
for (int i = 0; i < AMOUNT_OF_LEVELS; i++) {
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
if (value >= array[i][0] && value <= array[i][1]) {
|
||||
return i;
|
||||
}
|
||||
|
||||
51
esphome/components/aqi/sensor.py
Normal file
51
esphome/components/aqi/sensor.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_PM_2_5,
|
||||
CONF_PM_10_0,
|
||||
DEVICE_CLASS_AQI,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
|
||||
|
||||
CODEOWNERS = ["@jasstrong"]
|
||||
DEPENDENCIES = ["sensor"]
|
||||
|
||||
UNIT_INDEX = "index"
|
||||
|
||||
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
AQISensor,
|
||||
unit_of_measurement=UNIT_INDEX,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_AQI,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
cv.Required(CONF_PM_2_5): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_PM_10_0): cv.use_id(sensor.Sensor),
|
||||
cv.Required(CONF_CALCULATION_TYPE): cv.enum(
|
||||
AQI_CALCULATION_TYPE, upper=True
|
||||
),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
pm_2_5_sensor = await cg.get_variable(config[CONF_PM_2_5])
|
||||
cg.add(var.set_pm_2_5_sensor(pm_2_5_sensor))
|
||||
|
||||
pm_10_0_sensor = await cg.get_variable(config[CONF_PM_10_0])
|
||||
cg.add(var.set_pm_10_0_sensor(pm_10_0_sensor))
|
||||
|
||||
cg.add(var.set_aqi_calculation_type(config[CONF_CALCULATION_TYPE]))
|
||||
@@ -305,12 +305,14 @@ bool AS3935Component::calibrate_oscillator() {
|
||||
}
|
||||
|
||||
void AS3935Component::tune_antenna() {
|
||||
ESP_LOGI(TAG, "Starting antenna tuning");
|
||||
uint8_t div_ratio = this->read_div_ratio();
|
||||
uint8_t tune_val = this->read_capacitance();
|
||||
ESP_LOGI(TAG, "Division Ratio is set to: %d", div_ratio);
|
||||
ESP_LOGI(TAG, "Internal Capacitor is set to: %d", tune_val);
|
||||
ESP_LOGI(TAG, "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio");
|
||||
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",
|
||||
div_ratio, tune_val);
|
||||
this->display_oscillator(true, ANTFREQ);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,37 +1,50 @@
|
||||
# Dummy integration to allow relying on AsyncTCP
|
||||
# Async TCP client support for all platforms
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema({}),
|
||||
cv.only_with_arduino,
|
||||
cv.only_on(
|
||||
[
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_LN882X,
|
||||
PLATFORM_RTL87XX,
|
||||
]
|
||||
),
|
||||
)
|
||||
|
||||
def AUTO_LOAD() -> list[str]:
|
||||
# Socket component needed for platforms using socket-based implementation
|
||||
# ESP32, ESP8266, RP2040, and LibreTiny use AsyncTCP libraries, others use sockets
|
||||
if (
|
||||
not CORE.is_esp32
|
||||
and not CORE.is_esp8266
|
||||
and not CORE.is_rp2040
|
||||
and not CORE.is_libretiny
|
||||
):
|
||||
return ["socket"]
|
||||
return []
|
||||
|
||||
|
||||
# Support all platforms - Arduino/ESP-IDF get libraries, other platforms use socket implementation
|
||||
CONFIG_SCHEMA = cv.Schema({})
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT)
|
||||
async def to_code(config):
|
||||
if CORE.is_esp32 or CORE.is_libretiny:
|
||||
if CORE.is_esp32:
|
||||
# https://github.com/ESP32Async/AsyncTCP
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
add_idf_component(name="esp32async/asynctcp", ref="3.4.91")
|
||||
elif CORE.is_libretiny:
|
||||
# https://github.com/ESP32Async/AsyncTCP
|
||||
cg.add_library("ESP32Async/AsyncTCP", "3.4.5")
|
||||
elif CORE.is_esp8266:
|
||||
# https://github.com/ESP32Async/ESPAsyncTCP
|
||||
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
|
||||
elif CORE.is_rp2040:
|
||||
# https://github.com/khoih-prog/AsyncTCP_RP2040W
|
||||
cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0")
|
||||
# Other platforms (host, etc) use socket-based implementation
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
# Exclude socket implementation for platforms that use AsyncTCP libraries
|
||||
if CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny:
|
||||
return ["async_tcp_socket.cpp"]
|
||||
return []
|
||||
|
||||
16
esphome/components/async_tcp/async_tcp.h
Normal file
16
esphome/components/async_tcp/async_tcp.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
// Use AsyncTCP library for ESP32 (Arduino or ESP-IDF) and LibreTiny
|
||||
#include <AsyncTCP.h>
|
||||
#elif defined(USE_ESP8266)
|
||||
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
|
||||
#include <ESPAsyncTCP.h>
|
||||
#elif defined(USE_RP2040)
|
||||
// Use AsyncTCP_RP2040W library for RP2040
|
||||
#include <AsyncTCP_RP2040W.h>
|
||||
#else
|
||||
// Use socket-based implementation for other platforms
|
||||
#include "async_tcp_socket.h"
|
||||
#endif
|
||||
162
esphome/components/async_tcp/async_tcp_socket.cpp
Normal file
162
esphome/components/async_tcp/async_tcp_socket.cpp
Normal file
@@ -0,0 +1,162 @@
|
||||
#include "async_tcp_socket.h"
|
||||
|
||||
#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \
|
||||
(defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS))
|
||||
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <cerrno>
|
||||
#include <sys/select.h>
|
||||
|
||||
namespace esphome::async_tcp {
|
||||
|
||||
static const char *const TAG = "async_tcp";
|
||||
|
||||
// Read buffer size matches TCP MSS (1500 MTU - 40 bytes IP/TCP headers).
|
||||
// This implementation only runs on ESP-IDF and host which have ample stack.
|
||||
static constexpr size_t READ_BUFFER_SIZE = 1460;
|
||||
|
||||
bool AsyncClient::connect(const char *host, uint16_t port) {
|
||||
if (connected_ || connecting_) {
|
||||
ESP_LOGW(TAG, "Already connected/connecting");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Resolve address
|
||||
struct sockaddr_storage addr;
|
||||
socklen_t addrlen = esphome::socket::set_sockaddr((struct sockaddr *) &addr, sizeof(addr), host, port);
|
||||
if (addrlen == 0) {
|
||||
ESP_LOGE(TAG, "Invalid address: %s", host);
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Create socket with loop monitoring
|
||||
int family = ((struct sockaddr *) &addr)->sa_family;
|
||||
socket_ = esphome::socket::socket_loop_monitored(family, SOCK_STREAM, IPPROTO_TCP);
|
||||
if (!socket_) {
|
||||
ESP_LOGE(TAG, "Failed to create socket");
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, -1);
|
||||
return false;
|
||||
}
|
||||
|
||||
socket_->setblocking(false);
|
||||
|
||||
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
|
||||
if (err == 0) {
|
||||
// Connection succeeded immediately (rare, but possible for localhost)
|
||||
connected_ = true;
|
||||
if (connect_cb_)
|
||||
connect_cb_(connect_arg_, this);
|
||||
return true;
|
||||
}
|
||||
if (errno != EINPROGRESS) {
|
||||
ESP_LOGE(TAG, "Connect failed: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
connecting_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AsyncClient::close() {
|
||||
socket_.reset();
|
||||
bool was_connected = connected_;
|
||||
connected_ = false;
|
||||
connecting_ = false;
|
||||
if (was_connected && disconnect_cb_)
|
||||
disconnect_cb_(disconnect_arg_, this);
|
||||
}
|
||||
|
||||
size_t AsyncClient::write(const char *data, size_t len) {
|
||||
if (!socket_ || !connected_)
|
||||
return 0;
|
||||
|
||||
ssize_t sent = socket_->write(data, len);
|
||||
if (sent < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
ESP_LOGE(TAG, "Write error: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
void AsyncClient::loop() {
|
||||
if (!socket_)
|
||||
return;
|
||||
|
||||
if (connecting_) {
|
||||
// For connecting, we need to check writability, not readability
|
||||
// The Application's select() only monitors read FDs, so we do our own check here
|
||||
// For ESP platforms lwip_select() might be faster, but this code isn't used
|
||||
// on those platforms anyway. If it was, we'd fix the Application select()
|
||||
// to report writability instead of doing it this way.
|
||||
int fd = socket_->get_fd();
|
||||
if (fd < 0) {
|
||||
ESP_LOGW(TAG, "Invalid socket fd");
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(fd, &writefds);
|
||||
|
||||
struct timeval tv = {0, 0};
|
||||
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
|
||||
|
||||
if (ret > 0 && FD_ISSET(fd, &writefds)) {
|
||||
int error = 0;
|
||||
socklen_t len = sizeof(error);
|
||||
if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
|
||||
connecting_ = false;
|
||||
connected_ = true;
|
||||
if (connect_cb_)
|
||||
connect_cb_(connect_arg_, this);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Connection failed: %d", error);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, error);
|
||||
}
|
||||
} else if (ret < 0) {
|
||||
ESP_LOGE(TAG, "Select error: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
}
|
||||
} else if (connected_) {
|
||||
// For connected sockets, use the Application's select() results
|
||||
if (!socket_->ready())
|
||||
return;
|
||||
|
||||
uint8_t buf[READ_BUFFER_SIZE];
|
||||
ssize_t len = socket_->read(buf, READ_BUFFER_SIZE);
|
||||
|
||||
if (len == 0) {
|
||||
ESP_LOGI(TAG, "Connection closed by peer");
|
||||
close();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::async_tcp
|
||||
|
||||
#endif
|
||||
73
esphome/components/async_tcp/async_tcp_socket.h
Normal file
73
esphome/components/async_tcp/async_tcp_socket.h
Normal file
@@ -0,0 +1,73 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \
|
||||
(defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS))
|
||||
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace esphome::async_tcp {
|
||||
|
||||
/// AsyncClient API for platforms using sockets (ESP-IDF, host, etc.)
|
||||
/// NOTE: This class is NOT thread-safe. All methods must be called from the main loop.
|
||||
class AsyncClient {
|
||||
public:
|
||||
using AcConnectHandler = std::function<void(void *, AsyncClient *)>;
|
||||
using AcDataHandler = std::function<void(void *, AsyncClient *, void *data, size_t len)>;
|
||||
using AcErrorHandler = std::function<void(void *, AsyncClient *, int8_t error)>;
|
||||
|
||||
AsyncClient() = default;
|
||||
~AsyncClient() = default;
|
||||
|
||||
[[nodiscard]] bool connect(const char *host, uint16_t port);
|
||||
void close();
|
||||
[[nodiscard]] bool connected() const { return connected_; }
|
||||
size_t write(const char *data, size_t len);
|
||||
|
||||
void onConnect(AcConnectHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
|
||||
connect_cb_ = std::move(cb);
|
||||
connect_arg_ = arg;
|
||||
}
|
||||
void onDisconnect(AcConnectHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
|
||||
disconnect_cb_ = std::move(cb);
|
||||
disconnect_arg_ = arg;
|
||||
}
|
||||
/// Set data callback. NOTE: data pointer is only valid during callback execution.
|
||||
void onData(AcDataHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
|
||||
data_cb_ = std::move(cb);
|
||||
data_arg_ = arg;
|
||||
}
|
||||
void onError(AcErrorHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
|
||||
error_cb_ = std::move(cb);
|
||||
error_arg_ = arg;
|
||||
}
|
||||
|
||||
// Must be called from loop()
|
||||
void loop();
|
||||
|
||||
private:
|
||||
std::unique_ptr<esphome::socket::Socket> socket_;
|
||||
|
||||
AcConnectHandler connect_cb_{nullptr};
|
||||
void *connect_arg_{nullptr};
|
||||
AcConnectHandler disconnect_cb_{nullptr};
|
||||
void *disconnect_arg_{nullptr};
|
||||
AcDataHandler data_cb_{nullptr};
|
||||
void *data_arg_{nullptr};
|
||||
AcErrorHandler error_cb_{nullptr};
|
||||
void *error_arg_{nullptr};
|
||||
|
||||
bool connected_{false};
|
||||
bool connecting_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::async_tcp
|
||||
|
||||
// Expose AsyncClient in global namespace to match library behavior
|
||||
using esphome::async_tcp::AsyncClient; // NOLINT(google-global-names-in-headers)
|
||||
#endif
|
||||
@@ -21,7 +21,9 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device
|
||||
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
const char *addr_str = device.address_str_to(addr_buf);
|
||||
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str);
|
||||
|
||||
bool success = false;
|
||||
for (auto &service_data : device.get_service_datas()) {
|
||||
@@ -32,7 +34,7 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device
|
||||
if (!(parse_message_(service_data.data, *res))) {
|
||||
continue;
|
||||
}
|
||||
if (!(report_results_(res, device.address_str()))) {
|
||||
if (!(report_results_(res, addr_str))) {
|
||||
continue;
|
||||
}
|
||||
if (res->temperature.has_value() && this->temperature_ != nullptr)
|
||||
@@ -103,13 +105,13 @@ bool ATCMiThermometer::parse_message_(const std::vector<uint8_t> &message, Parse
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ATCMiThermometer::report_results_(const optional<ParseResult> &result, const std::string &address) {
|
||||
bool ATCMiThermometer::report_results_(const optional<ParseResult> &result, const char *address) {
|
||||
if (!result.has_value()) {
|
||||
ESP_LOGVV(TAG, "report_results(): no results available.");
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Got ATC MiThermometer (%s):", address.c_str());
|
||||
ESP_LOGD(TAG, "Got ATC MiThermometer (%s):", address);
|
||||
|
||||
if (result->temperature.has_value()) {
|
||||
ESP_LOGD(TAG, " Temperature: %.1f °C", *result->temperature);
|
||||
|
||||
@@ -41,7 +41,7 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice
|
||||
|
||||
optional<ParseResult> parse_header_(const esp32_ble_tracker::ServiceData &service_data);
|
||||
bool parse_message_(const std::vector<uint8_t> &message, ParseResult &result);
|
||||
bool report_results_(const optional<ParseResult> &result, const std::string &address);
|
||||
bool report_results_(const optional<ParseResult> &result, const char *address);
|
||||
};
|
||||
|
||||
} // namespace atc_mithermometer
|
||||
|
||||
@@ -22,7 +22,8 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str_to(addr_buf));
|
||||
const auto &service_datas = device.get_service_datas();
|
||||
if (service_datas.size() != 1) {
|
||||
ESP_LOGE(TAG, "Unexpected service_datas size (%d)", service_datas.size());
|
||||
|
||||
@@ -193,8 +193,9 @@ bool BedJetHub::discover_characteristics_() {
|
||||
result = false;
|
||||
} else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 ||
|
||||
descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
|
||||
char uuid_buf[espbt::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_,
|
||||
descr->uuid.to_string().c_str());
|
||||
descr->uuid.to_str(uuid_buf));
|
||||
result = false;
|
||||
} else {
|
||||
this->config_descr_status_ = descr->handle;
|
||||
@@ -216,11 +217,14 @@ bool BedJetHub::discover_characteristics_() {
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "[%s] Discovered service characteristics: ", this->get_name().c_str());
|
||||
ESP_LOGI(TAG, " - Command char: 0x%x", this->char_handle_cmd_);
|
||||
ESP_LOGI(TAG, " - Status char: 0x%x", this->char_handle_status_);
|
||||
ESP_LOGI(TAG, " - config descriptor: 0x%x", this->config_descr_status_);
|
||||
ESP_LOGI(TAG, " - Name char: 0x%x", this->char_handle_name_);
|
||||
ESP_LOGI(TAG,
|
||||
"[%s] Discovered service characteristics:\n"
|
||||
" - Command char: 0x%x\n"
|
||||
" - Status char: 0x%x\n"
|
||||
" - config descriptor: 0x%x\n"
|
||||
" - Name char: 0x%x",
|
||||
this->get_name().c_str(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_,
|
||||
this->char_handle_name_);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -164,21 +164,21 @@ void BedJetClimate::control(const ClimateCall &call) {
|
||||
return;
|
||||
}
|
||||
} else if (call.has_custom_preset()) {
|
||||
const char *preset = call.get_custom_preset();
|
||||
auto preset = call.get_custom_preset();
|
||||
bool result;
|
||||
|
||||
if (strcmp(preset, "M1") == 0) {
|
||||
if (preset == "M1") {
|
||||
result = this->parent_->button_memory1();
|
||||
} else if (strcmp(preset, "M2") == 0) {
|
||||
} else if (preset == "M2") {
|
||||
result = this->parent_->button_memory2();
|
||||
} else if (strcmp(preset, "M3") == 0) {
|
||||
} else if (preset == "M3") {
|
||||
result = this->parent_->button_memory3();
|
||||
} else if (strcmp(preset, "LTD HT") == 0) {
|
||||
} else if (preset == "LTD HT") {
|
||||
result = this->parent_->button_heat();
|
||||
} else if (strcmp(preset, "EXT HT") == 0) {
|
||||
} else if (preset == "EXT HT") {
|
||||
result = this->parent_->button_ext_heat();
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Unsupported preset: %s", preset);
|
||||
ESP_LOGW(TAG, "Unsupported preset: %.*s", (int) preset.size(), preset.c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -208,10 +208,11 @@ void BedJetClimate::control(const ClimateCall &call) {
|
||||
this->set_fan_mode_(fan_mode);
|
||||
}
|
||||
} else if (call.has_custom_fan_mode()) {
|
||||
const char *fan_mode = call.get_custom_fan_mode();
|
||||
auto fan_index = bedjet_fan_speed_to_step(fan_mode);
|
||||
auto fan_mode = call.get_custom_fan_mode();
|
||||
auto fan_index = bedjet_fan_speed_to_step(fan_mode.c_str());
|
||||
if (fan_index <= 19) {
|
||||
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index);
|
||||
ESP_LOGV(TAG, "[%s] Converted fan mode %.*s to bedjet fan step %d", this->get_name().c_str(),
|
||||
(int) fan_mode.size(), fan_mode.c_str(), fan_index);
|
||||
bool result = this->parent_->set_fan_index(fan_index);
|
||||
if (result) {
|
||||
this->set_custom_fan_mode_(fan_mode);
|
||||
|
||||
@@ -3,7 +3,7 @@ from logging import getLogger
|
||||
from esphome import automation, core
|
||||
from esphome.automation import Condition, maybe_simple_id
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import mqtt, web_server
|
||||
from esphome.components import mqtt, web_server, zigbee
|
||||
from esphome.components.const import CONF_ON_STATE_CHANGE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -439,6 +439,7 @@ def validate_publish_initial_state(value):
|
||||
_BINARY_SENSOR_SCHEMA = (
|
||||
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
|
||||
.extend(cv.MQTT_COMPONENT_SCHEMA)
|
||||
.extend(zigbee.BINARY_SENSOR_SCHEMA)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BinarySensor),
|
||||
@@ -520,6 +521,7 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
|
||||
|
||||
_BINARY_SENSOR_SCHEMA.add_extra(entity_duplicate_validator("binary_sensor"))
|
||||
_BINARY_SENSOR_SCHEMA.add_extra(zigbee.validate_binary_sensor)
|
||||
|
||||
|
||||
def binary_sensor_schema(
|
||||
@@ -621,6 +623,8 @@ async def setup_binary_sensor_core_(var, config):
|
||||
if web_server_config := config.get(CONF_WEB_SERVER):
|
||||
await web_server.add_entity_config(var, web_server_config)
|
||||
|
||||
await zigbee.setup_binary_sensor(var, config)
|
||||
|
||||
|
||||
async def register_binary_sensor(var, config):
|
||||
if not CORE.has_id(config[CONF_ID]):
|
||||
|
||||
@@ -21,8 +21,10 @@ void MultiClickTrigger::on_state_(bool state) {
|
||||
// Start matching
|
||||
MultiClickTriggerEvent evt = this->timing_[0];
|
||||
if (evt.state == state) {
|
||||
ESP_LOGV(TAG, "START min=%" PRIu32 " max=%" PRIu32, evt.min_length, evt.max_length);
|
||||
ESP_LOGV(TAG, "Multi Click: Starting multi click action!");
|
||||
ESP_LOGV(TAG,
|
||||
"START min=%" PRIu32 " max=%" PRIu32 "\n"
|
||||
"Multi Click: Starting multi click action!",
|
||||
evt.min_length, evt.max_length);
|
||||
this->at_index_ = 1;
|
||||
if (this->timing_.size() == 1 && evt.max_length == 4294967294UL) {
|
||||
this->set_timeout("trigger", evt.min_length, [this]() { this->trigger_(); });
|
||||
|
||||
@@ -7,8 +7,12 @@
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// Maximum bytes to log in hex format for BLE writes (many logging buffers are 256 chars)
|
||||
static constexpr size_t BLE_WRITE_MAX_LOG_BYTES = 64;
|
||||
|
||||
namespace esphome::ble_client {
|
||||
|
||||
// placeholder class for static TAG .
|
||||
@@ -151,7 +155,10 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
|
||||
return false;
|
||||
}
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(BLE_WRITE_MAX_LOG_BYTES)];
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty_to(hex_buf, data, len));
|
||||
#endif
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
|
||||
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
@@ -179,8 +186,10 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
esph_log_w("ble_write_action", "Characteristic %s was not found in service %s",
|
||||
this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str());
|
||||
this->char_uuid_.to_str(char_buf), this->service_uuid_.to_str(service_buf));
|
||||
break;
|
||||
}
|
||||
this->char_handle_ = chr->handle;
|
||||
@@ -192,11 +201,13 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP;
|
||||
esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
|
||||
} else {
|
||||
esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str());
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_str(char_buf));
|
||||
break;
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf),
|
||||
ble_client_->address_str());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -9,12 +9,15 @@ static const char *const TAG = "ble_binary_output";
|
||||
|
||||
void BLEBinaryOutput::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BLE Binary Output:");
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
this->service_uuid_.to_str(service_buf);
|
||||
this->char_uuid_.to_str(char_buf);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" MAC address : %s\n"
|
||||
" Service UUID : %s\n"
|
||||
" Characteristic UUID: %s",
|
||||
this->parent_->address_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->char_uuid_.to_string().c_str());
|
||||
this->parent_->address_str(), service_buf, char_buf);
|
||||
LOG_BINARY_OUTPUT(this);
|
||||
}
|
||||
|
||||
@@ -24,8 +27,10 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGW(TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_string().c_str(),
|
||||
this->service_uuid_.to_string().c_str());
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_str(char_buf),
|
||||
this->service_uuid_.to_str(service_buf));
|
||||
break;
|
||||
}
|
||||
this->char_handle_ = chr->handle;
|
||||
@@ -37,20 +42,24 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP;
|
||||
ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
|
||||
} else {
|
||||
ESP_LOGE(TAG, "Characteristic %s does not allow writing with%s response", this->char_uuid_.to_string().c_str(),
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGE(TAG, "Characteristic %s does not allow writing with%s response", this->char_uuid_.to_str(char_buf),
|
||||
this->require_response_ ? "" : "out");
|
||||
break;
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_str(char_buf),
|
||||
this->parent()->address_str());
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_CHAR_EVT: {
|
||||
if (param->write.handle == this->char_handle_) {
|
||||
if (param->write.status != 0)
|
||||
ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status);
|
||||
if (param->write.status != 0) {
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_str(char_buf), param->write.status);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -60,18 +69,19 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
}
|
||||
|
||||
void BLEBinaryOutput::write_state(bool state) {
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%s] Not connected to BLE client. State update can not be written.",
|
||||
this->char_uuid_.to_string().c_str());
|
||||
this->char_uuid_.to_str(char_buf));
|
||||
return;
|
||||
}
|
||||
uint8_t state_as_uint = (uint8_t) state;
|
||||
ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_string().c_str(), state_as_uint);
|
||||
ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_str(char_buf), state_as_uint);
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_,
|
||||
sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ESP_GATT_OK)
|
||||
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err);
|
||||
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_str(char_buf), err);
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
@@ -18,14 +18,17 @@ void BLESensor::loop() {
|
||||
|
||||
void BLESensor::dump_config() {
|
||||
LOG_SENSOR("", "BLE Sensor", this);
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
char descr_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" MAC address : %s\n"
|
||||
" Service UUID : %s\n"
|
||||
" Characteristic UUID: %s\n"
|
||||
" Descriptor UUID : %s\n"
|
||||
" Notifications : %s",
|
||||
this->parent()->address_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_));
|
||||
this->parent()->address_str(), this->service_uuid_.to_str(service_buf),
|
||||
this->char_uuid_.to_str(char_buf), this->descr_uuid_.to_str(descr_buf), YESNO(this->notify_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
@@ -51,8 +54,10 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
if (chr == nullptr) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
|
||||
this->char_uuid_.to_string().c_str());
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_str(service_buf),
|
||||
this->char_uuid_.to_str(char_buf));
|
||||
break;
|
||||
}
|
||||
this->handle = chr->handle;
|
||||
@@ -61,9 +66,12 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
if (descr == nullptr) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
char descr_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s",
|
||||
this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(),
|
||||
this->descr_uuid_.to_string().c_str());
|
||||
this->service_uuid_.to_str(service_buf), this->char_uuid_.to_str(char_buf),
|
||||
this->descr_uuid_.to_str(descr_buf));
|
||||
break;
|
||||
}
|
||||
this->handle = descr->handle;
|
||||
@@ -109,7 +117,8 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
break;
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
ESP_LOGD(TAG, "Register for notify on %s complete", this->char_uuid_.to_string().c_str());
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGD(TAG, "Register for notify on %s complete", this->char_uuid_.to_str(char_buf));
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class BLETextSensorNotifyTrigger : public Trigger<std::string>, public BLETextSe
|
||||
if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() ||
|
||||
param->notify.handle != this->sensor_->handle)
|
||||
break;
|
||||
this->trigger(this->sensor_->parse_data(param->notify.value, param->notify.value_len));
|
||||
this->trigger(std::string(reinterpret_cast<const char *>(param->notify.value), param->notify.value_len));
|
||||
}
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -11,8 +11,6 @@ namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_text_sensor";
|
||||
|
||||
static const std::string EMPTY = "";
|
||||
|
||||
void BLETextSensor::loop() {
|
||||
// Parent BLEClientNode has a loop() method, but this component uses
|
||||
// polling via update() and BLE callbacks so loop isn't needed
|
||||
@@ -21,14 +19,17 @@ void BLETextSensor::loop() {
|
||||
|
||||
void BLETextSensor::dump_config() {
|
||||
LOG_TEXT_SENSOR("", "BLE Text Sensor", this);
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
char descr_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" MAC address : %s\n"
|
||||
" Service UUID : %s\n"
|
||||
" Characteristic UUID: %s\n"
|
||||
" Descriptor UUID : %s\n"
|
||||
" Notifications : %s",
|
||||
this->parent()->address_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->char_uuid_.to_string().c_str(), this->descr_uuid_.to_string().c_str(), YESNO(this->notify_));
|
||||
this->parent()->address_str(), this->service_uuid_.to_str(service_buf),
|
||||
this->char_uuid_.to_str(char_buf), this->descr_uuid_.to_str(descr_buf), YESNO(this->notify_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
@@ -44,7 +45,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
this->status_set_warning();
|
||||
this->publish_state(EMPTY);
|
||||
this->publish_state("");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
@@ -52,9 +53,11 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
|
||||
if (chr == nullptr) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(EMPTY);
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
|
||||
this->char_uuid_.to_string().c_str());
|
||||
this->publish_state("");
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_str(service_buf),
|
||||
this->char_uuid_.to_str(char_buf));
|
||||
break;
|
||||
}
|
||||
this->handle = chr->handle;
|
||||
@@ -62,10 +65,13 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
auto *descr = chr->get_descriptor(this->descr_uuid_);
|
||||
if (descr == nullptr) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(EMPTY);
|
||||
this->publish_state("");
|
||||
char service_buf[esp32_ble::UUID_STR_LEN];
|
||||
char char_buf[esp32_ble::UUID_STR_LEN];
|
||||
char descr_buf[esp32_ble::UUID_STR_LEN];
|
||||
ESP_LOGW(TAG, "No sensor descriptor found at service %s char %s descr %s",
|
||||
this->service_uuid_.to_string().c_str(), this->char_uuid_.to_string().c_str(),
|
||||
this->descr_uuid_.to_string().c_str());
|
||||
this->service_uuid_.to_str(service_buf), this->char_uuid_.to_str(char_buf),
|
||||
this->descr_uuid_.to_str(descr_buf));
|
||||
break;
|
||||
}
|
||||
this->handle = descr->handle;
|
||||
@@ -91,7 +97,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
break;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
this->publish_state(this->parse_data(param->read.value, param->read.value_len));
|
||||
this->publish_state(reinterpret_cast<const char *>(param->read.value), param->read.value_len);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -100,7 +106,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
break;
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
|
||||
param->notify.handle, param->notify.value[0]);
|
||||
this->publish_state(this->parse_data(param->notify.value, param->notify.value_len));
|
||||
this->publish_state(reinterpret_cast<const char *>(param->notify.value), param->notify.value_len);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
@@ -113,11 +119,6 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
}
|
||||
}
|
||||
|
||||
std::string BLETextSensor::parse_data(uint8_t *value, uint16_t value_len) {
|
||||
std::string text(value, value + value_len);
|
||||
return text;
|
||||
}
|
||||
|
||||
void BLETextSensor::update() {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->get_name().c_str());
|
||||
@@ -132,7 +133,7 @@ void BLETextSensor::update() {
|
||||
ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
this->status_set_warning();
|
||||
this->publish_state(EMPTY);
|
||||
this->publish_state("");
|
||||
ESP_LOGW(TAG, "[%s] Error sending read request for sensor, status=%d", this->get_name().c_str(), status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p
|
||||
void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
void set_enable_notify(bool notify) { this->notify_ = notify; }
|
||||
std::string parse_data(uint8_t *value, uint16_t value_len);
|
||||
uint16_t handle;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -103,8 +103,10 @@ void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t
|
||||
#endif
|
||||
|
||||
void BLENUS::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ble nus:");
|
||||
ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_));
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"ble nus:\n"
|
||||
" log: %s",
|
||||
YESNO(this->expose_log_));
|
||||
uint32_t mtu = 0;
|
||||
bt_conn *conn = this->conn_.load();
|
||||
if (conn) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include <cinttypes>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
@@ -15,17 +16,13 @@ namespace ble_scanner {
|
||||
class BLEScanner : public text_sensor::TextSensor, public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
public:
|
||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override {
|
||||
this->publish_state("{\"timestamp\":" + to_string(::time(nullptr)) +
|
||||
","
|
||||
"\"address\":\"" +
|
||||
device.address_str() +
|
||||
"\","
|
||||
"\"rssi\":" +
|
||||
to_string(device.get_rssi()) +
|
||||
","
|
||||
"\"name\":\"" +
|
||||
device.get_name() + "\"}");
|
||||
|
||||
// Format JSON using stack buffer to avoid heap allocations from string concatenation
|
||||
char buf[128];
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
snprintf(buf, sizeof(buf), "{\"timestamp\":%" PRId64 ",\"address\":\"%s\",\"rssi\":%d,\"name\":\"%s\"}",
|
||||
static_cast<int64_t>(::time(nullptr)), device.address_str_to(addr_buf), device.get_rssi(),
|
||||
device.get_name().c_str());
|
||||
this->publish_state(buf);
|
||||
return true;
|
||||
}
|
||||
void dump_config() override;
|
||||
|
||||
@@ -31,7 +31,11 @@ void BME68xBSEC2I2CComponent::dump_config() {
|
||||
BME68xBSEC2Component::dump_config();
|
||||
}
|
||||
|
||||
uint32_t BME68xBSEC2I2CComponent::get_hash() { return fnv1_hash("bme68x_bsec_state_" + to_string(this->address_)); }
|
||||
uint32_t BME68xBSEC2I2CComponent::get_hash() {
|
||||
char buf[22]; // "bme68x_bsec_state_" (18) + uint8_t max (3) + null
|
||||
snprintf(buf, sizeof(buf), "bme68x_bsec_state_%u", this->address_);
|
||||
return fnv1_hash(buf);
|
||||
}
|
||||
|
||||
int8_t BME68xBSEC2I2CComponent::read_bytes_wrapper(uint8_t a_register, uint8_t *data, uint32_t len, void *intfPtr) {
|
||||
ESP_LOGVV(TAG, "read_bytes_wrapper: reg = %u", a_register);
|
||||
|
||||
@@ -22,13 +22,13 @@ void BP1658CJ::setup() {
|
||||
this->pwm_amounts_.resize(5, 0);
|
||||
}
|
||||
void BP1658CJ::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BP1658CJ:");
|
||||
LOG_PIN(" Data Pin: ", this->data_pin_);
|
||||
LOG_PIN(" Clock Pin: ", this->clock_pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"BP1658CJ:\n"
|
||||
" Color Channels Max Power: %u\n"
|
||||
" White Channels Max Power: %u",
|
||||
this->max_power_color_channels_, this->max_power_white_channels_);
|
||||
LOG_PIN(" Data Pin: ", this->data_pin_);
|
||||
LOG_PIN(" Clock Pin: ", this->clock_pin_);
|
||||
}
|
||||
|
||||
void BP1658CJ::loop() {
|
||||
|
||||
36
esphome/components/bthome_mithermometer/__init__.py
Normal file
36
esphome/components/bthome_mithermometer/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble_tracker
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_MAC_ADDRESS
|
||||
|
||||
CODEOWNERS = ["@nagyrobi"]
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
|
||||
BLE_DEVICE_SCHEMA = esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA
|
||||
|
||||
bthome_mithermometer_ns = cg.esphome_ns.namespace("bthome_mithermometer")
|
||||
BTHomeMiThermometer = bthome_mithermometer_ns.class_(
|
||||
"BTHomeMiThermometer", esp32_ble_tracker.ESPBTDeviceListener, cg.Component
|
||||
)
|
||||
|
||||
|
||||
def bthome_mithermometer_base_schema(extra_schema=None):
|
||||
if extra_schema is None:
|
||||
extra_schema = {}
|
||||
return (
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
|
||||
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
|
||||
}
|
||||
)
|
||||
.extend(BLE_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(extra_schema)
|
||||
)
|
||||
|
||||
|
||||
async def setup_bthome_mithermometer(var, config):
|
||||
await cg.register_component(var, config)
|
||||
await esp32_ble_tracker.register_ble_device(var, config)
|
||||
cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
|
||||
300
esphome/components/bthome_mithermometer/bthome_ble.cpp
Normal file
300
esphome/components/bthome_mithermometer/bthome_ble.cpp
Normal file
@@ -0,0 +1,300 @@
|
||||
#include "bthome_ble.h"
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <array>
|
||||
#include <span>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace bthome_mithermometer {
|
||||
|
||||
static const char *const TAG = "bthome_mithermometer";
|
||||
|
||||
static const char *format_mac_address(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buffer, uint64_t address) {
|
||||
std::array<uint8_t, MAC_ADDRESS_SIZE> mac{};
|
||||
for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) {
|
||||
mac[i] = (address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF;
|
||||
}
|
||||
|
||||
format_mac_addr_upper(mac.data(), buffer.data());
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
static bool get_bthome_value_length(uint8_t obj_type, size_t &value_length) {
|
||||
switch (obj_type) {
|
||||
case 0x00: // packet id
|
||||
case 0x01: // battery
|
||||
case 0x09: // count (uint8)
|
||||
case 0x0F: // generic boolean
|
||||
case 0x10: // power (bool)
|
||||
case 0x11: // opening
|
||||
case 0x15: // battery low
|
||||
case 0x16: // battery charging
|
||||
case 0x17: // carbon monoxide
|
||||
case 0x18: // cold
|
||||
case 0x19: // connectivity
|
||||
case 0x1A: // door
|
||||
case 0x1B: // garage door
|
||||
case 0x1C: // gas
|
||||
case 0x1D: // heat
|
||||
case 0x1E: // light
|
||||
case 0x1F: // lock
|
||||
case 0x20: // moisture
|
||||
case 0x21: // motion
|
||||
case 0x22: // moving
|
||||
case 0x23: // occupancy
|
||||
case 0x24: // plug
|
||||
case 0x25: // presence
|
||||
case 0x26: // problem
|
||||
case 0x27: // running
|
||||
case 0x28: // safety
|
||||
case 0x29: // smoke
|
||||
case 0x2A: // sound
|
||||
case 0x2B: // tamper
|
||||
case 0x2C: // vibration
|
||||
case 0x2D: // water leak
|
||||
case 0x2E: // humidity (uint8)
|
||||
case 0x2F: // moisture (uint8)
|
||||
case 0x46: // UV index
|
||||
case 0x57: // temperature (sint8)
|
||||
case 0x58: // temperature (0.35C step)
|
||||
case 0x59: // count (sint8)
|
||||
case 0x60: // channel
|
||||
value_length = 1;
|
||||
return true;
|
||||
case 0x02: // temperature (0.01C)
|
||||
case 0x03: // humidity
|
||||
case 0x06: // mass (kg)
|
||||
case 0x07: // mass (lb)
|
||||
case 0x08: // dewpoint
|
||||
case 0x0C: // voltage (mV)
|
||||
case 0x0D: // pm2.5
|
||||
case 0x0E: // pm10
|
||||
case 0x12: // CO2
|
||||
case 0x13: // TVOC
|
||||
case 0x14: // moisture
|
||||
case 0x3D: // count (uint16)
|
||||
case 0x3F: // rotation
|
||||
case 0x40: // distance (mm)
|
||||
case 0x41: // distance (m)
|
||||
case 0x43: // current (A)
|
||||
case 0x44: // speed
|
||||
case 0x45: // temperature (0.1C)
|
||||
case 0x47: // volume (L)
|
||||
case 0x48: // volume (mL)
|
||||
case 0x49: // volume flow rate
|
||||
case 0x4A: // voltage (0.1V)
|
||||
case 0x51: // acceleration
|
||||
case 0x52: // gyroscope
|
||||
case 0x56: // conductivity
|
||||
case 0x5A: // count (sint16)
|
||||
case 0x5D: // current (sint16)
|
||||
case 0x5E: // direction
|
||||
case 0x5F: // precipitation
|
||||
case 0x61: // rotational speed
|
||||
case 0xF0: // button event
|
||||
value_length = 2;
|
||||
return true;
|
||||
case 0x04: // pressure
|
||||
case 0x05: // illuminance
|
||||
case 0x0A: // energy
|
||||
case 0x0B: // power
|
||||
case 0x42: // duration
|
||||
case 0x4B: // gas (uint24)
|
||||
case 0xF2: // firmware version (uint24)
|
||||
value_length = 3;
|
||||
return true;
|
||||
case 0x3E: // count (uint32)
|
||||
case 0x4C: // gas (uint32)
|
||||
case 0x4D: // energy (uint32)
|
||||
case 0x4E: // volume (uint32)
|
||||
case 0x4F: // water (uint32)
|
||||
case 0x50: // timestamp
|
||||
case 0x55: // volume storage
|
||||
case 0x5B: // count (sint32)
|
||||
case 0x5C: // power (sint32)
|
||||
case 0x62: // speed (sint32)
|
||||
case 0x63: // acceleration (sint32)
|
||||
case 0xF1: // firmware version (uint32)
|
||||
value_length = 4;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void BTHomeMiThermometer::dump_config() {
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
ESP_LOGCONFIG(TAG, "BTHome MiThermometer");
|
||||
ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_));
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_);
|
||||
LOG_SENSOR(" ", "Humidity", this->humidity_);
|
||||
LOG_SENSOR(" ", "Battery Level", this->battery_level_);
|
||||
LOG_SENSOR(" ", "Battery Voltage", this->battery_voltage_);
|
||||
LOG_SENSOR(" ", "Signal Strength", this->signal_strength_);
|
||||
}
|
||||
|
||||
bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
bool matched = false;
|
||||
for (auto &service_data : device.get_service_datas()) {
|
||||
if (this->handle_service_data_(service_data, device)) {
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
if (matched && this->signal_strength_ != nullptr) {
|
||||
this->signal_strength_->publish_state(device.get_rssi());
|
||||
}
|
||||
return matched;
|
||||
}
|
||||
|
||||
bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||
const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
if (!service_data.uuid.contains(0xD2, 0xFC)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto &data = service_data.data;
|
||||
if (data.size() < 2) {
|
||||
ESP_LOGVV(TAG, "BTHome data too short: %zu", data.size());
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint8_t adv_info = data[0];
|
||||
const bool is_encrypted = adv_info & 0x01;
|
||||
const bool mac_included = adv_info & 0x02;
|
||||
const bool is_trigger_based = adv_info & 0x04;
|
||||
const uint8_t version = (adv_info >> 5) & 0x07;
|
||||
|
||||
if (version != 0x02) {
|
||||
ESP_LOGVV(TAG, "Unsupported BTHome version %u", version);
|
||||
return false;
|
||||
}
|
||||
|
||||
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
if (is_encrypted) {
|
||||
ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf));
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t payload_index = 1;
|
||||
uint64_t source_address = device.address_uint64();
|
||||
|
||||
if (mac_included) {
|
||||
if (data.size() < 7) {
|
||||
ESP_LOGVV(TAG, "BTHome payload missing MAC address");
|
||||
return false;
|
||||
}
|
||||
source_address = 0;
|
||||
for (int i = 5; i >= 0; i--) {
|
||||
source_address = (source_address << 8) | data[1 + i];
|
||||
}
|
||||
payload_index = 7;
|
||||
}
|
||||
|
||||
if (source_address != this->address_) {
|
||||
ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (payload_index >= data.size()) {
|
||||
ESP_LOGVV(TAG, "BTHome payload empty after header");
|
||||
return false;
|
||||
}
|
||||
|
||||
bool reported = false;
|
||||
size_t offset = payload_index;
|
||||
uint8_t last_type = 0;
|
||||
|
||||
while (offset < data.size()) {
|
||||
const uint8_t obj_type = data[offset++];
|
||||
size_t value_length = 0;
|
||||
bool has_length_byte = obj_type == 0x53; // text objects include explicit length
|
||||
|
||||
if (has_length_byte) {
|
||||
if (offset >= data.size()) {
|
||||
break;
|
||||
}
|
||||
value_length = data[offset++];
|
||||
} else {
|
||||
if (!get_bthome_value_length(obj_type, value_length)) {
|
||||
ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (value_length == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (offset + value_length > data.size()) {
|
||||
ESP_LOGVV(TAG, "BTHome object length exceeds payload");
|
||||
break;
|
||||
}
|
||||
|
||||
const uint8_t *value = &data[offset];
|
||||
offset += value_length;
|
||||
|
||||
if (obj_type < last_type) {
|
||||
ESP_LOGVV(TAG, "BTHome objects not in ascending order");
|
||||
}
|
||||
last_type = obj_type;
|
||||
|
||||
switch (obj_type) {
|
||||
case 0x00: { // packet id
|
||||
const uint8_t packet_id = value[0];
|
||||
if (this->last_packet_id_.has_value() && *this->last_packet_id_ == packet_id) {
|
||||
return reported;
|
||||
}
|
||||
this->last_packet_id_ = packet_id;
|
||||
break;
|
||||
}
|
||||
case 0x01: { // battery percentage
|
||||
if (this->battery_level_ != nullptr) {
|
||||
this->battery_level_->publish_state(value[0]);
|
||||
reported = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x0C: { // battery voltage (mV)
|
||||
if (this->battery_voltage_ != nullptr) {
|
||||
const uint16_t raw = encode_uint16(value[1], value[0]);
|
||||
this->battery_voltage_->publish_state(raw * 0.001f);
|
||||
reported = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x02: { // temperature
|
||||
if (this->temperature_ != nullptr) {
|
||||
const int16_t raw = encode_uint16(value[1], value[0]);
|
||||
this->temperature_->publish_state(raw * 0.01f);
|
||||
reported = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 0x03: { // humidity
|
||||
if (this->humidity_ != nullptr) {
|
||||
const uint16_t raw = encode_uint16(value[1], value[0]);
|
||||
this->humidity_->publish_state(raw * 0.01f);
|
||||
reported = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (reported) {
|
||||
ESP_LOGD(TAG, "BTHome data%sfrom %s", is_trigger_based ? " (triggered) " : " ", device.address_str_to(addr_buf));
|
||||
}
|
||||
|
||||
return reported;
|
||||
}
|
||||
|
||||
} // namespace bthome_mithermometer
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
44
esphome/components/bthome_mithermometer/bthome_ble.h
Normal file
44
esphome/components/bthome_mithermometer/bthome_ble.h
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace bthome_mithermometer {
|
||||
|
||||
class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
public:
|
||||
void set_address(uint64_t address) { this->address_ = address; }
|
||||
|
||||
void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; }
|
||||
void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; }
|
||||
void set_battery_level(sensor::Sensor *battery_level) { this->battery_level_ = battery_level; }
|
||||
void set_battery_voltage(sensor::Sensor *battery_voltage) { this->battery_voltage_ = battery_voltage; }
|
||||
void set_signal_strength(sensor::Sensor *signal_strength) { this->signal_strength_ = signal_strength; }
|
||||
|
||||
void dump_config() override;
|
||||
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
|
||||
|
||||
protected:
|
||||
bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data,
|
||||
const esp32_ble_tracker::ESPBTDevice &device);
|
||||
|
||||
uint64_t address_{0};
|
||||
optional<uint8_t> last_packet_id_{};
|
||||
|
||||
sensor::Sensor *temperature_{nullptr};
|
||||
sensor::Sensor *humidity_{nullptr};
|
||||
sensor::Sensor *battery_level_{nullptr};
|
||||
sensor::Sensor *battery_voltage_{nullptr};
|
||||
sensor::Sensor *signal_strength_{nullptr};
|
||||
};
|
||||
|
||||
} // namespace bthome_mithermometer
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
88
esphome/components/bthome_mithermometer/sensor.py
Normal file
88
esphome/components/bthome_mithermometer/sensor.py
Normal file
@@ -0,0 +1,88 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BATTERY_LEVEL,
|
||||
CONF_BATTERY_VOLTAGE,
|
||||
CONF_HUMIDITY,
|
||||
CONF_ID,
|
||||
CONF_SIGNAL_STRENGTH,
|
||||
CONF_TEMPERATURE,
|
||||
DEVICE_CLASS_BATTERY,
|
||||
DEVICE_CLASS_HUMIDITY,
|
||||
DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
DEVICE_CLASS_TEMPERATURE,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_CELSIUS,
|
||||
UNIT_DECIBEL_MILLIWATT,
|
||||
UNIT_PERCENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
|
||||
from . import bthome_mithermometer_base_schema, setup_bthome_mithermometer
|
||||
|
||||
CODEOWNERS = ["@nagyrobi"]
|
||||
|
||||
DEPENDENCIES = ["esp32_ble_tracker"]
|
||||
|
||||
CONFIG_SCHEMA = bthome_mithermometer_base_schema(
|
||||
{
|
||||
cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_CELSIUS,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_TEMPERATURE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_LEVEL): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_PERCENT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_BATTERY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_BATTERY_VOLTAGE): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_VOLT,
|
||||
accuracy_decimals=3,
|
||||
device_class=DEVICE_CLASS_VOLTAGE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
icon="mdi:battery-plus",
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
cv.Optional(CONF_SIGNAL_STRENGTH): sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_DECIBEL_MILLIWATT,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await setup_bthome_mithermometer(var, config)
|
||||
|
||||
if temp_sens := config.get(CONF_TEMPERATURE):
|
||||
sens = await sensor.new_sensor(temp_sens)
|
||||
cg.add(var.set_temperature(sens))
|
||||
if humi_sens := config.get(CONF_HUMIDITY):
|
||||
sens = await sensor.new_sensor(humi_sens)
|
||||
cg.add(var.set_humidity(sens))
|
||||
if batl_sens := config.get(CONF_BATTERY_LEVEL):
|
||||
sens = await sensor.new_sensor(batl_sens)
|
||||
cg.add(var.set_battery_level(sens))
|
||||
if batv_sens := config.get(CONF_BATTERY_VOLTAGE):
|
||||
sens = await sensor.new_sensor(batv_sens)
|
||||
cg.add(var.set_battery_voltage(sens))
|
||||
if sgnl_sens := config.get(CONF_SIGNAL_STRENGTH):
|
||||
sens = await sensor.new_sensor(sgnl_sens)
|
||||
cg.add(var.set_signal_strength(sens))
|
||||
@@ -63,14 +63,14 @@ void CAP1188Component::finish_setup_() {
|
||||
}
|
||||
|
||||
void CAP1188Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CAP1188:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"CAP1188:\n"
|
||||
" Product ID: 0x%x\n"
|
||||
" Manufacture ID: 0x%x\n"
|
||||
" Revision ID: 0x%x",
|
||||
this->cap1188_product_id_, this->cap1188_manufacture_id_, this->cap1188_revision_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
|
||||
switch (this->error_code_) {
|
||||
case COMMUNICATION_FAILED:
|
||||
|
||||
@@ -7,6 +7,7 @@ from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_AP,
|
||||
CONF_COMPRESSION,
|
||||
CONF_ID,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
@@ -43,6 +44,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
|
||||
web_server_base.WebServerBase
|
||||
),
|
||||
cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on(
|
||||
@@ -96,6 +98,9 @@ async def to_code(config):
|
||||
await cg.register_component(var, config)
|
||||
cg.add_define("USE_CAPTIVE_PORTAL")
|
||||
|
||||
if config[CONF_COMPRESSION] == "gzip":
|
||||
cg.add_define("USE_CAPTIVE_PORTAL_GZIP")
|
||||
|
||||
if CORE.using_arduino:
|
||||
if CORE.is_esp8266:
|
||||
cg.add_library("DNSServer", None)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace captive_portal {
|
||||
namespace esphome::captive_portal {
|
||||
|
||||
#ifdef USE_CAPTIVE_PORTAL_GZIP
|
||||
const uint8_t INDEX_GZ[] PROGMEM = {
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e,
|
||||
0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36,
|
||||
@@ -85,5 +85,71 @@ const uint8_t INDEX_GZ[] PROGMEM = {
|
||||
0xa7, 0x18, 0x06, 0x7a, 0xcd, 0x05, 0x8c, 0x61, 0x8c, 0x82, 0x25, 0x3a, 0x79, 0x75, 0xb2, 0xf6, 0xc4, 0xaf, 0x68,
|
||||
0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00};
|
||||
|
||||
} // namespace captive_portal
|
||||
} // namespace esphome
|
||||
#else // Brotli (default, smaller)
|
||||
const uint8_t INDEX_BR[] PROGMEM = {
|
||||
0x1b, 0xf8, 0x0a, 0x00, 0x64, 0x5a, 0xd3, 0xfa, 0xe7, 0xf3, 0x62, 0xd8, 0x06, 0x1b, 0xe9, 0x6a, 0x8a, 0x81, 0x2b,
|
||||
0xb5, 0x49, 0x14, 0x37, 0xdc, 0x9e, 0x1a, 0xcb, 0x56, 0x87, 0xfb, 0xff, 0xf7, 0x73, 0x75, 0x12, 0x0a, 0xd6, 0x48,
|
||||
0x84, 0xc6, 0x21, 0xa4, 0x6d, 0xb5, 0x71, 0xef, 0x13, 0xbe, 0x4e, 0x54, 0xf1, 0x64, 0x8f, 0x3f, 0xcc, 0x9a, 0x78,
|
||||
0xa5, 0x89, 0x25, 0xb3, 0xda, 0x2c, 0xa2, 0x32, 0x9c, 0x57, 0x07, 0x56, 0xbc, 0x34, 0x13, 0xff, 0x5c, 0x0a, 0xa1,
|
||||
0x67, 0x82, 0xb8, 0x6b, 0x4c, 0x76, 0x31, 0x6c, 0xe3, 0x40, 0x46, 0xea, 0xb0, 0xd4, 0xf4, 0x3b, 0x02, 0x65, 0x18,
|
||||
0xa4, 0xaf, 0xac, 0x6d, 0x55, 0xd6, 0xbe, 0x59, 0x66, 0x7a, 0x7c, 0x60, 0xb2, 0x83, 0x33, 0x23, 0xc9, 0x79, 0x82,
|
||||
0x47, 0xb4, 0x28, 0xf4, 0x24, 0xb5, 0x23, 0x5a, 0x44, 0xe1, 0xc3, 0x27, 0x04, 0xe8, 0x0c, 0xdd, 0xb4, 0xd0, 0x8c,
|
||||
0xfb, 0x10, 0x39, 0x93, 0x04, 0x2a, 0x66, 0x18, 0x4b, 0x74, 0xca, 0x31, 0x7f, 0xb2, 0xe5, 0x45, 0xc1, 0xdd, 0x72,
|
||||
0x49, 0xff, 0x0e, 0xb3, 0xf0, 0x93, 0x18, 0xab, 0x68, 0xad, 0xe1, 0x9d, 0xe4, 0x29, 0xc0, 0xe3, 0x63, 0x54, 0x61,
|
||||
0x1b, 0x45, 0xb9, 0x6c, 0x23, 0x0f, 0x99, 0x7f, 0x8e, 0x69, 0xaa, 0xc1, 0xb8, 0x4e, 0x42, 0x9c, 0xc5, 0x6e, 0x69,
|
||||
0x40, 0x0e, 0x4f, 0x97, 0xd3, 0x23, 0x18, 0xf5, 0xc8, 0x75, 0x73, 0xb5, 0xbd, 0x46, 0x8a, 0x97, 0x7d, 0x83, 0xe4,
|
||||
0x29, 0x72, 0x73, 0xc1, 0x39, 0x8e, 0x7e, 0x84, 0x39, 0x66, 0x57, 0xc6, 0x85, 0x19, 0x8b, 0xf2, 0x4d, 0xd9, 0xfe,
|
||||
0x75, 0xa9, 0xe1, 0x2b, 0x21, 0x81, 0x58, 0x51, 0x99, 0xbc, 0xa4, 0x0b, 0x10, 0x6f, 0x86, 0x17, 0x0b, 0x92, 0x00,
|
||||
0x11, 0x6f, 0x3b, 0xa4, 0xa4, 0x11, 0x7e, 0x0b, 0x97, 0x85, 0x23, 0x0c, 0x01, 0x6f, 0x2a, 0x18, 0xc6, 0xbe, 0x3d,
|
||||
0x77, 0x1a, 0xe6, 0x00, 0x5c, 0x1a, 0x14, 0x47, 0xc6, 0xcc, 0xcc, 0x52, 0xbe, 0x04, 0x19, 0x31, 0x05, 0x46, 0xa0,
|
||||
0xc3, 0x69, 0x0c, 0x60, 0xb7, 0x14, 0x57, 0xa0, 0x92, 0xbf, 0xb7, 0x0c, 0xd8, 0x3a, 0x79, 0x09, 0x99, 0xc9, 0x71,
|
||||
0x88, 0x01, 0x8b, 0xa5, 0x61, 0x0a, 0xb5, 0xe8, 0xc7, 0x71, 0xe7, 0x70, 0x79, 0xb6, 0xe4, 0x01, 0xfc, 0x1a, 0x4a,
|
||||
0x7b, 0x60, 0x6e, 0xef, 0x95, 0x62, 0x59, 0x28, 0xb5, 0x25, 0x56, 0x15, 0xe7, 0xca, 0xad, 0x32, 0xe6, 0xf7, 0x01,
|
||||
0x31, 0x34, 0x87, 0x93, 0x0b, 0x9b, 0x9d, 0x26, 0xff, 0xe5, 0x92, 0xad, 0x6f, 0xb8, 0x3b, 0x16, 0xc1, 0xa0, 0x5a,
|
||||
0x4f, 0x52, 0x0b, 0x2b, 0xc1, 0xa7, 0x95, 0x7b, 0x24, 0x51, 0xd3, 0xb3, 0x23, 0x62, 0x0b, 0xcc, 0xa0, 0x58, 0xa7,
|
||||
0x64, 0x45, 0x2f, 0x0b, 0xdd, 0x1d, 0x97, 0x82, 0x1f, 0xcc, 0x64, 0xdb, 0xd3, 0xf4, 0xb0, 0x8b, 0xc8, 0xcf, 0x15,
|
||||
0x81, 0x8b, 0xa1, 0x9d, 0xf8, 0xfc, 0xec, 0x49, 0x40, 0x12, 0x01, 0x09, 0x51, 0xf3, 0x73, 0x18, 0x24, 0x97, 0x55,
|
||||
0x85, 0x6a, 0x92, 0x1a, 0xf5, 0x5a, 0x05, 0x54, 0x1f, 0x27, 0x0a, 0xa8, 0xa1, 0x94, 0x58, 0x78, 0x7d, 0x87, 0xa8,
|
||||
0xdb, 0x13, 0x66, 0x20, 0x5e, 0x43, 0x18, 0x7a, 0xbb, 0x16, 0x16, 0x07, 0xc8, 0xab, 0x10, 0xe2, 0x50, 0xb9, 0xb1,
|
||||
0xd8, 0x21, 0xc8, 0x4a, 0x2e, 0x99, 0x0e, 0x23, 0x52, 0xc6, 0xcb, 0x29, 0x84, 0x91, 0x03, 0xb1, 0xe2, 0x4c, 0x1d,
|
||||
0x22, 0xd3, 0xc8, 0x79, 0x00, 0x8b, 0x8b, 0x88, 0x1e, 0x29, 0xd3, 0xae, 0x10, 0x15, 0x22, 0x6d, 0xb0, 0x87, 0x6f,
|
||||
0x27, 0x2e, 0x7c, 0xc2, 0x7a, 0x61, 0xbd, 0x22, 0xe5, 0x5f, 0xdd, 0x7b, 0x00, 0x04, 0xf2, 0x7d, 0x5a, 0x03, 0x38,
|
||||
0x1f, 0x69, 0x6d, 0x0b, 0xfb, 0xec, 0x45, 0xfe, 0x8b, 0x7f, 0xec, 0x7b, 0xad, 0xc2, 0x33, 0xf1, 0x9e, 0x9c, 0x71,
|
||||
0xd9, 0xe8, 0x5e, 0x8f, 0xd4, 0xee, 0x87, 0x45, 0x6c, 0xe2, 0x12, 0xf8, 0xb8, 0xc5, 0xee, 0x43, 0xa6, 0x37, 0x91,
|
||||
0xb5, 0x2c, 0x2f, 0xe9, 0xe8, 0x24, 0xd0, 0x45, 0xc1, 0x0c, 0x7c, 0xf0, 0xb2, 0xb5, 0x2d, 0x10, 0x36, 0x7e, 0x18,
|
||||
0x7c, 0x79, 0x82, 0x69, 0x3d, 0x35, 0xca, 0x52, 0xee, 0xc9, 0xb5, 0x65, 0xa4, 0xa1, 0xfd, 0x70, 0x7e, 0xe0, 0x7d,
|
||||
0x67, 0xf9, 0xa1, 0x71, 0xd2, 0x08, 0x74, 0x33, 0x5f, 0x69, 0xa4, 0x59, 0x03, 0xfd, 0xf8, 0xf0, 0x70, 0x1a, 0x50,
|
||||
0x43, 0xfb, 0x61, 0xf0, 0x38, 0x18, 0x88, 0x85, 0x36, 0x23, 0x06, 0x4f, 0x02, 0xbb, 0x78, 0x1a, 0xaa, 0xd2, 0x02,
|
||||
0x5e, 0xa0, 0x74, 0x30, 0xc8, 0x7a, 0x66, 0xab, 0xd9, 0x43, 0x99, 0x45, 0xb7, 0x0c, 0x5c, 0xec, 0xc8, 0x03, 0x0e,
|
||||
0x0b, 0xca, 0x4a, 0x22, 0x48, 0xfb, 0xb7, 0x3d, 0x82, 0x07, 0x8d, 0x1b, 0x21, 0x87, 0x4d, 0x57, 0xa4, 0x5b, 0xd4,
|
||||
0xe3, 0x88, 0x02, 0xc4, 0x81, 0xf9, 0x47, 0xe4, 0xbf, 0x3e, 0x39, 0xbb, 0x4f, 0x7e, 0x91, 0x63, 0x98, 0x97, 0xe4,
|
||||
0x52, 0x01, 0x58, 0xba, 0x32, 0xbf, 0xae, 0xff, 0x45, 0xa1, 0xbc, 0x9b, 0xa4, 0x09, 0x0e, 0x79, 0xc0, 0x41, 0x86,
|
||||
0x52, 0x88, 0x55, 0x39, 0x9d, 0xb6, 0xed, 0x35, 0x68, 0x29, 0xfa, 0xe6, 0x6c, 0x3d, 0x0a, 0xcd, 0x6a, 0x28, 0xfd,
|
||||
0x65, 0x24, 0xce, 0x38, 0x98, 0x01, 0xd9, 0x3f, 0x1b, 0x4c, 0xc4, 0x5c, 0x1d, 0xaa, 0x21, 0x78, 0x67, 0xaf, 0x55,
|
||||
0x72, 0x34, 0xf8, 0x1b, 0x03, 0x21, 0x27, 0x08, 0xbd, 0x59, 0x60, 0x48, 0x0d, 0xe2, 0x56, 0x9b, 0x30, 0x92, 0x8f,
|
||||
0x67, 0x8a, 0x7f, 0x20, 0xbd, 0x2d, 0xfd, 0xc5, 0xb0, 0xa6, 0xaa, 0x77, 0x75, 0x26, 0x33, 0x2f, 0x20, 0x2a, 0xab,
|
||||
0x5c, 0xd1, 0x3b, 0xda, 0xb2, 0x4c, 0xa4, 0x86, 0x25, 0x8d, 0x49, 0x05, 0xaf, 0x7a, 0xa8, 0xd4, 0x9c, 0x0d, 0xd3,
|
||||
0x38, 0xa6, 0x5c, 0x29, 0x6b, 0x16, 0x27, 0x07, 0xf1, 0xbe, 0xe2, 0x24, 0xc1, 0x8d, 0x25, 0x76, 0xbc, 0xf6, 0x0d,
|
||||
0xc2, 0x94, 0x25, 0xb8, 0xf3, 0x07, 0x9a, 0x49, 0xf4, 0x89, 0x82, 0x4d, 0x51, 0xb1, 0x96, 0x61, 0x62, 0x8d, 0xc8,
|
||||
0x61, 0x65, 0x0d, 0x14, 0x34, 0x02, 0x65, 0x94, 0xcc, 0x1d, 0x85, 0x00, 0x0f, 0x1a, 0x57, 0x68, 0x15, 0xcf, 0xa4,
|
||||
0xa2, 0x7d, 0x6d, 0x53, 0x60, 0xce, 0x5c, 0x61, 0x82, 0x17, 0x32, 0xc1, 0x87, 0x02, 0x0c, 0x91, 0x85, 0x57, 0x51,
|
||||
0xbe, 0xb2, 0x38, 0x9f, 0x3d, 0x2a, 0x52, 0x5a, 0xad, 0xba, 0x46, 0x9e, 0x3c, 0x8a, 0xa0, 0x46, 0x15, 0xf4, 0x59,
|
||||
0x74, 0x5f, 0x2a, 0xae, 0x96, 0x56, 0xf0, 0x54, 0x39, 0xaf, 0xac, 0x2a, 0xb9, 0xad, 0x32, 0x50, 0xc9, 0xc1, 0xee,
|
||||
0xd2, 0x0d, 0x34, 0xaa, 0x98, 0x4d, 0x6d, 0x3d, 0xc6, 0xb9, 0x5b, 0x00, 0x5f, 0xea, 0xda, 0x16, 0xa6, 0x08, 0x43,
|
||||
0x58, 0x4d, 0x8d, 0x07, 0x55, 0x62, 0x81, 0x44, 0xcc, 0x31, 0x04, 0x4b, 0x4c, 0x8b, 0x3e, 0xff, 0xd8, 0xf6, 0x65,
|
||||
0x19, 0xa1, 0x94, 0x62, 0x65, 0x0a, 0xdd, 0x60, 0x38, 0xd3, 0xbe, 0x0d, 0xa3, 0x99, 0xd5, 0x37, 0x68, 0xa1, 0x71,
|
||||
0xa3, 0x41, 0xe7, 0xbe, 0x9d, 0x72, 0x84, 0x75, 0xb6, 0x8d, 0x98, 0xd6, 0xb8, 0x2d, 0x43, 0x85, 0x5d, 0xf9, 0xca,
|
||||
0xc3, 0x96, 0xa5, 0xa6, 0xe7, 0x50, 0x88, 0x6b, 0x84, 0x58, 0x44, 0x45, 0x20, 0xdf, 0x1e, 0x5a, 0xc9, 0xce, 0x42,
|
||||
0x2a, 0x1f, 0x3e, 0x3c, 0x7b, 0x68, 0x3c, 0x34, 0x8b, 0x36, 0xba, 0x1f, 0xce, 0x0f, 0xa0, 0x60, 0x37, 0x5f, 0x1a,
|
||||
0x03, 0x2b, 0x86, 0x29, 0x45, 0x7b, 0xb4, 0xb7, 0x06, 0x68, 0x17, 0x7e, 0x13, 0x76, 0x91, 0x4d, 0x27, 0xee, 0xbc,
|
||||
0x7e, 0x80, 0xc2, 0x66, 0xac, 0xc6, 0xbf, 0xeb, 0x7f, 0xd7, 0x84, 0x79, 0xf3, 0xf1, 0xde, 0xec, 0xa6, 0x93, 0xa8,
|
||||
0x13, 0x3b, 0x4a, 0x81, 0xfa, 0x11, 0x1e, 0x4a, 0xd2, 0x50, 0x2a, 0xea, 0x9a, 0xc2, 0x37, 0x08, 0xed, 0x01, 0xf5,
|
||||
0xa2, 0xd5, 0x32, 0x29, 0x49, 0xc4, 0x1a, 0x11, 0xc0, 0xda, 0x24, 0x28, 0x84, 0x38, 0x60, 0x80, 0xcf, 0xd0, 0x45,
|
||||
0x83, 0xa7, 0xca, 0x52, 0x5c, 0xac, 0x23, 0x01};
|
||||
|
||||
// Backwards compatibility alias
|
||||
#define INDEX_GZ INDEX_BR
|
||||
|
||||
#endif // USE_CAPTIVE_PORTAL_GZIP
|
||||
|
||||
} // namespace esphome::captive_portal
|
||||
|
||||
@@ -49,11 +49,18 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
|
||||
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr)
|
||||
ESP_LOGI(TAG, "Requested WiFi Settings Change:");
|
||||
ESP_LOGI(TAG, " SSID='%s'", ssid.c_str());
|
||||
ESP_LOGI(TAG, " Password=" LOG_SECRET("'%s'"), psk.c_str());
|
||||
ESP_LOGI(TAG,
|
||||
"Requested WiFi Settings Change:\n"
|
||||
" SSID='%s'\n"
|
||||
" Password=" LOG_SECRET("'%s'"),
|
||||
ssid.c_str(), psk.c_str());
|
||||
#ifdef USE_ESP8266
|
||||
// ESP8266 is single-threaded, call directly
|
||||
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
|
||||
#else
|
||||
// Defer save to main loop thread to avoid NVS operations from HTTP thread
|
||||
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); });
|
||||
#endif
|
||||
request->redirect(ESPHOME_F("/?save"));
|
||||
}
|
||||
|
||||
@@ -105,7 +112,11 @@ void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
|
||||
#else
|
||||
auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
|
||||
#endif
|
||||
#ifdef USE_CAPTIVE_PORTAL_GZIP
|
||||
response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
|
||||
#else
|
||||
response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("br"));
|
||||
#endif
|
||||
req->send(response);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,10 @@ struct DNSAnswer {
|
||||
|
||||
void DNSServer::start(const network::IPAddress &ip) {
|
||||
this->server_ip_ = ip;
|
||||
ESP_LOGV(TAG, "Starting DNS server on %s", ip.str().c_str());
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ESP_LOGV(TAG, "Starting DNS server on %s", ip.str_to(ip_buf));
|
||||
#endif
|
||||
|
||||
// Create loop-monitored UDP socket
|
||||
this->socket_ = socket::socket_ip_loop_monitored(SOCK_DGRAM, IPPROTO_UDP);
|
||||
|
||||
@@ -264,6 +264,7 @@ async def to_code(config):
|
||||
var.get_packet_trigger(),
|
||||
[
|
||||
(cg.std_vector.template(cg.uint8), "x"),
|
||||
(cg.float_, "freq_offset"),
|
||||
(cg.float_, "rssi"),
|
||||
(cg.uint8, "lqi"),
|
||||
],
|
||||
|
||||
@@ -140,7 +140,10 @@ void CC1101Component::setup() {
|
||||
this->write_(static_cast<Register>(i));
|
||||
}
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
this->strobe_(Command::RX);
|
||||
if (!this->enter_rx_()) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Defer pin mode setup until after all components have completed setup()
|
||||
// This handles the case where remote_transmitter runs after CC1101 and changes pin mode
|
||||
@@ -163,8 +166,7 @@ void CC1101Component::loop() {
|
||||
ESP_LOGW(TAG, "RX FIFO overflow, flushing");
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
this->enter_rx_();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -181,28 +183,28 @@ void CC1101Component::loop() {
|
||||
ESP_LOGW(TAG, "Invalid packet: rx_bytes %u, payload_length %u", rx_bytes, payload_length);
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
this->enter_rx_();
|
||||
return;
|
||||
}
|
||||
this->packet_.resize(payload_length);
|
||||
this->read_(Register::FIFO, this->packet_.data(), payload_length);
|
||||
|
||||
// Read status from registers (more reliable than FIFO status bytes due to timing issues)
|
||||
this->read_(Register::FREQEST);
|
||||
this->read_(Register::RSSI);
|
||||
this->read_(Register::LQI);
|
||||
float freq_offset = static_cast<int8_t>(this->state_.FREQEST) * (XTAL_FREQUENCY / (1 << 14));
|
||||
float rssi = (this->state_.RSSI * RSSI_STEP) - RSSI_OFFSET;
|
||||
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
|
||||
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
|
||||
if (this->state_.CRC_EN == 0 || crc_ok) {
|
||||
this->packet_trigger_->trigger(this->packet_, rssi, lqi);
|
||||
this->packet_trigger_->trigger(this->packet_, freq_offset, rssi, lqi);
|
||||
}
|
||||
|
||||
// Return to rx
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::FRX);
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
this->enter_rx_();
|
||||
}
|
||||
|
||||
void CC1101Component::dump_config() {
|
||||
@@ -212,9 +214,8 @@ void CC1101Component::dump_config() {
|
||||
XTAL_FREQUENCY / (1 << 16);
|
||||
float symbol_rate = (((256.0f + this->state_.DRATE_M) * (1 << this->state_.DRATE_E)) / (1 << 28)) * XTAL_FREQUENCY;
|
||||
float bw = XTAL_FREQUENCY / (8.0f * (4 + this->state_.CHANBW_M) * (1 << this->state_.CHANBW_E));
|
||||
ESP_LOGCONFIG(TAG, "CC1101:");
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"CC1101:\n"
|
||||
" Chip ID: 0x%04X\n"
|
||||
" Frequency: %" PRId32 " Hz\n"
|
||||
" Channel: %u\n"
|
||||
@@ -224,6 +225,7 @@ void CC1101Component::dump_config() {
|
||||
" Output Power: %.1f dBm",
|
||||
this->chip_id_, freq, this->state_.CHANNR, MODULATION_NAMES[this->state_.MOD_FORMAT & 0x07],
|
||||
symbol_rate, bw, this->output_power_effective_);
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
}
|
||||
|
||||
void CC1101Component::begin_tx() {
|
||||
@@ -233,9 +235,8 @@ void CC1101Component::begin_tx() {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_OUTPUT);
|
||||
}
|
||||
this->strobe_(Command::TX);
|
||||
if (!this->wait_for_state_(State::TX, 50)) {
|
||||
ESP_LOGW(TAG, "Timed out waiting for TX state!");
|
||||
if (!this->enter_tx_()) {
|
||||
ESP_LOGW(TAG, "Failed to enter TX state!");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +245,9 @@ void CC1101Component::begin_rx() {
|
||||
if (this->gdo0_pin_ != nullptr) {
|
||||
this->gdo0_pin_->pin_mode(gpio::FLAG_INPUT);
|
||||
}
|
||||
this->strobe_(Command::RX);
|
||||
if (!this->enter_rx_()) {
|
||||
ESP_LOGW(TAG, "Failed to enter RX state!");
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::reset() {
|
||||
@@ -270,11 +273,33 @@ bool CC1101Component::wait_for_state_(State target_state, uint32_t timeout_ms) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CC1101Component::enter_calibrated_(State target_state, Command cmd) {
|
||||
// The PLL must be recalibrated until PLL lock is achieved
|
||||
for (uint8_t retries = PLL_LOCK_RETRIES; retries > 0; retries--) {
|
||||
this->strobe_(cmd);
|
||||
if (!this->wait_for_state_(target_state)) {
|
||||
return false;
|
||||
}
|
||||
this->read_(Register::FSCAL1);
|
||||
if (this->state_.FSCAL1 != FSCAL1_PLL_NOT_LOCKED) {
|
||||
return true;
|
||||
}
|
||||
ESP_LOGW(TAG, "PLL lock failed, retrying calibration");
|
||||
this->enter_idle_();
|
||||
}
|
||||
ESP_LOGE(TAG, "PLL lock failed after retries");
|
||||
return false;
|
||||
}
|
||||
|
||||
void CC1101Component::enter_idle_() {
|
||||
this->strobe_(Command::IDLE);
|
||||
this->wait_for_state_(State::IDLE);
|
||||
}
|
||||
|
||||
bool CC1101Component::enter_rx_() { return this->enter_calibrated_(State::RX, Command::RX); }
|
||||
|
||||
bool CC1101Component::enter_tx_() { return this->enter_calibrated_(State::TX, Command::TX); }
|
||||
|
||||
uint8_t CC1101Component::strobe_(Command cmd) {
|
||||
uint8_t index = static_cast<uint8_t>(cmd);
|
||||
if (cmd < Command::RES || cmd > Command::NOP) {
|
||||
@@ -336,18 +361,26 @@ CC1101Error CC1101Component::transmit_packet(const std::vector<uint8_t> &packet)
|
||||
this->write_(Register::FIFO, static_cast<uint8_t>(packet.size()));
|
||||
}
|
||||
this->write_(Register::FIFO, packet.data(), packet.size());
|
||||
|
||||
// Calibrate PLL
|
||||
if (!this->enter_calibrated_(State::FSTXON, Command::FSTXON)) {
|
||||
ESP_LOGW(TAG, "PLL lock failed during TX");
|
||||
this->enter_idle_();
|
||||
this->enter_rx_();
|
||||
return CC1101Error::PLL_LOCK;
|
||||
}
|
||||
|
||||
// Transmit packet
|
||||
this->strobe_(Command::TX);
|
||||
if (!this->wait_for_state_(State::IDLE, 1000)) {
|
||||
ESP_LOGW(TAG, "TX timeout");
|
||||
this->enter_idle_();
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
this->enter_rx_();
|
||||
return CC1101Error::TIMEOUT;
|
||||
}
|
||||
|
||||
// Return to rx
|
||||
this->strobe_(Command::RX);
|
||||
this->wait_for_state_(State::RX);
|
||||
this->enter_rx_();
|
||||
return CC1101Error::NONE;
|
||||
}
|
||||
|
||||
@@ -404,7 +437,7 @@ void CC1101Component::set_frequency(float value) {
|
||||
this->write_(Register::FREQ2);
|
||||
this->write_(Register::FREQ1);
|
||||
this->write_(Register::FREQ0);
|
||||
this->strobe_(Command::RX);
|
||||
this->enter_rx_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,7 +464,7 @@ void CC1101Component::set_channel(uint8_t value) {
|
||||
if (this->initialized_) {
|
||||
this->enter_idle_();
|
||||
this->write_(Register::CHANNR);
|
||||
this->strobe_(Command::RX);
|
||||
this->enter_rx_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,7 +533,7 @@ void CC1101Component::set_modulation_type(Modulation value) {
|
||||
this->set_output_power(this->output_power_requested_);
|
||||
this->write_(Register::MDMCFG2);
|
||||
this->write_(Register::FREND0);
|
||||
this->strobe_(Command::RX);
|
||||
this->enter_rx_();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace esphome::cc1101 {
|
||||
|
||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW };
|
||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
|
||||
|
||||
class CC1101Component : public Component,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
@@ -73,7 +73,7 @@ class CC1101Component : public Component,
|
||||
|
||||
// Packet mode operations
|
||||
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
|
||||
Trigger<std::vector<uint8_t>, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
uint16_t chip_id_{0};
|
||||
@@ -89,7 +89,8 @@ class CC1101Component : public Component,
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
|
||||
// Packet handling
|
||||
Trigger<std::vector<uint8_t>, float, uint8_t> *packet_trigger_{new Trigger<std::vector<uint8_t>, float, uint8_t>()};
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
|
||||
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
|
||||
std::vector<uint8_t> packet_;
|
||||
|
||||
// Low-level Helpers
|
||||
@@ -102,7 +103,10 @@ class CC1101Component : public Component,
|
||||
|
||||
// State Management
|
||||
bool wait_for_state_(State target_state, uint32_t timeout_ms = 100);
|
||||
bool enter_calibrated_(State target_state, Command cmd);
|
||||
void enter_idle_();
|
||||
bool enter_rx_();
|
||||
bool enter_tx_();
|
||||
};
|
||||
|
||||
// Action Wrappers
|
||||
|
||||
@@ -9,6 +9,9 @@ static constexpr float XTAL_FREQUENCY = 26000000;
|
||||
static constexpr float RSSI_OFFSET = 74.0f;
|
||||
static constexpr float RSSI_STEP = 0.5f;
|
||||
|
||||
static constexpr uint8_t FSCAL1_PLL_NOT_LOCKED = 0x3F;
|
||||
static constexpr uint8_t PLL_LOCK_RETRIES = 3;
|
||||
|
||||
static constexpr uint8_t STATUS_CRC_OK_MASK = 0x80;
|
||||
static constexpr uint8_t STATUS_LQI_MASK = 0x7F;
|
||||
|
||||
|
||||
@@ -21,12 +21,14 @@ void CD74HC4067Component::setup() {
|
||||
}
|
||||
|
||||
void CD74HC4067Component::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "CD74HC4067 Multiplexer:");
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"CD74HC4067 Multiplexer:\n"
|
||||
" switch delay: %" PRIu32,
|
||||
this->switch_delay_);
|
||||
LOG_PIN(" S0 Pin: ", this->pin_s0_);
|
||||
LOG_PIN(" S1 Pin: ", this->pin_s1_);
|
||||
LOG_PIN(" S2 Pin: ", this->pin_s2_);
|
||||
LOG_PIN(" S3 Pin: ", this->pin_s3_);
|
||||
ESP_LOGCONFIG(TAG, "switch delay: %" PRIu32, this->switch_delay_);
|
||||
}
|
||||
|
||||
void CD74HC4067Component::activate_pin(uint8_t pin) {
|
||||
|
||||
@@ -93,7 +93,9 @@ bool CH422GComponent::read_inputs_() {
|
||||
bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) {
|
||||
auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str());
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "write failed for register 0x%X, error %d", reg, err);
|
||||
this->status_set_warning(buf);
|
||||
return false;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
@@ -104,7 +106,9 @@ uint8_t CH422GComponent::read_reg_(uint8_t reg) {
|
||||
uint8_t value;
|
||||
auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str());
|
||||
char buf[64];
|
||||
snprintf(buf, sizeof(buf), "read failed for register 0x%X, error %d", reg, err);
|
||||
this->status_set_warning(buf);
|
||||
return 0;
|
||||
}
|
||||
this->status_clear_warning();
|
||||
@@ -128,7 +132,9 @@ void CH422GGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->
|
||||
bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) ^ this->inverted_; }
|
||||
|
||||
void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); }
|
||||
std::string CH422GGPIOPin::dump_summary() const { return str_sprintf("EXIO%u via CH422G", pin_); }
|
||||
size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
return snprintf(buffer, len, "EXIO%u via CH422G", this->pin_);
|
||||
}
|
||||
void CH422GGPIOPin::set_flags(gpio::Flags flags) {
|
||||
flags_ = flags;
|
||||
this->parent_->pin_mode(this->pin_, flags);
|
||||
|
||||
@@ -50,7 +50,7 @@ class CH422GGPIOPin : public GPIOPin {
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
size_t dump_summary(char *buffer, size_t len) const override;
|
||||
|
||||
void set_parent(CH422GComponent *parent) { parent_ = parent; }
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user