mirror of
https://github.com/esphome/esphome.git
synced 2026-01-19 17:46:23 -07:00
Compare commits
409 Commits
controller
...
scheduler_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d91cad1636 | ||
|
|
54d0328002 | ||
|
|
865312ff60 | ||
|
|
637cb3f04a | ||
|
|
80e881655f | ||
|
|
78b2ae8a35 | ||
|
|
8caaf53ef0 | ||
|
|
4db7748815 | ||
|
|
0da157ab98 | ||
|
|
cafa275579 | ||
|
|
a31fb223f3 | ||
|
|
37019231de | ||
|
|
2af66bd6fc | ||
|
|
951c5377c5 | ||
|
|
22803ef54b | ||
|
|
20f82a3820 | ||
|
|
fb331e1c5a | ||
|
|
a8518d3cea | ||
|
|
03aaa66f8e | ||
|
|
a24ba26068 | ||
|
|
623cdac689 | ||
|
|
1fbd91dc71 | ||
|
|
cfd88376b9 | ||
|
|
b3812b5811 | ||
|
|
577a6b2941 | ||
|
|
de68b56c4a | ||
|
|
ccd23e692b | ||
|
|
1f5a44be3d | ||
|
|
1d1e47c757 | ||
| 3fbed1fa79 | |||
|
|
5c71520635 | ||
|
|
9d6c81ec23 | ||
|
|
73fa9230e6 | ||
|
|
48caff13c9 | ||
|
|
71bb94524e | ||
|
|
a3199792c6 | ||
|
|
87ac4baf3a | ||
|
|
669bcad458 | ||
|
|
6f91c75f86 | ||
|
|
ab60ae092d | ||
|
|
708496c101 | ||
|
|
2f75962b19 | ||
|
|
a6a6f482e6 | ||
|
|
638c59e162 | ||
|
|
8f97f3b81f | ||
|
|
6ce2a45691 | ||
|
|
77477bd330 | ||
|
|
3f08cacf71 | ||
|
|
d1583456e9 | ||
|
|
101103c666 | ||
|
|
5142ff372b | ||
|
|
f9ad832e7b | ||
|
|
deda7a1bf3 | ||
|
|
29be1423f5 | ||
|
|
10ddebc737 | ||
|
|
9a0731437a | ||
|
|
82a06c697e | ||
|
|
c45cd44bb8 | ||
|
|
2903a4aa92 | ||
|
|
6943803176 | ||
|
|
6dafc5137e | ||
|
|
df58e832e5 | ||
|
|
e42cf9a4f4 | ||
|
|
96f28f0ab4 | ||
|
|
d332edfaca | ||
|
|
d4bd282bb4 | ||
|
|
78df884bb5 | ||
|
|
52fe3de78f | ||
|
|
6a79ce8eff | ||
|
|
2b7695ba3f | ||
|
|
6d336676a2 | ||
|
|
b322622ef1 | ||
|
|
065c1bfc6a | ||
|
|
664881bc13 | ||
|
|
dbc16ce468 | ||
|
|
161a18b326 | ||
|
|
4335fcdb72 | ||
| bf4ef36c3a | |||
|
|
2ca118f371 | ||
|
|
82e1238330 | ||
|
|
8308bc2911 | ||
|
|
47c767fa5e | ||
|
|
e95ceafc17 | ||
|
|
7317bf4a5d | ||
|
|
042a08887f | ||
|
|
77f5f2326f | ||
| d82a92b406 | |||
|
|
ec88bf0cb1 | ||
|
|
46567c4716 | ||
|
|
1f47797007 | ||
|
|
cf444fc3b8 | ||
|
|
c40e8e7f5c | ||
|
|
b71d8010d2 | ||
|
|
2174795b27 | ||
|
|
5fa4ff754c | ||
|
|
bc50be6053 | ||
|
|
ca599b25c2 | ||
|
|
2e55296640 | ||
|
|
d6ca01775e | ||
|
|
e15f3a08ae | ||
|
|
fb82362e9c | ||
|
|
26e979d3d5 | ||
|
|
60ffa0e52e | ||
|
|
e1ec6146c0 | ||
|
|
450065fdae | ||
|
|
71dc402a30 | ||
|
|
9bd148dfd1 | ||
|
|
50c1720c16 | ||
|
|
4c549798bc | ||
|
|
4115dd7222 | ||
|
|
d5e2543751 | ||
|
|
b4b34aee13 | ||
|
|
6645994700 | ||
|
|
ae140f52e3 | ||
|
|
46ae6d35a2 | ||
|
|
278f12fb99 | ||
|
|
acdcd56395 | ||
|
|
9289fc36f7 | ||
|
|
1fadd1227d | ||
|
|
91df0548ef | ||
|
|
a7a5a0b9a2 | ||
|
|
9c85ec9182 | ||
|
|
23e58c1c7b | ||
|
|
b3955cd151 | ||
|
|
927d3715c1 | ||
|
|
a2d9941c62 | ||
|
|
caaa08d678 | ||
|
|
eb970cf44e | ||
|
|
083886c4b0 | ||
|
|
12a51ff047 | ||
|
|
b328758634 | ||
|
|
1207b9e995 | ||
|
|
e071380532 | ||
|
|
f071b6232a | ||
|
|
d443dbbf34 | ||
|
|
03a8ef71ff | ||
|
|
bda17180df | ||
|
|
ffae3501ab | ||
|
|
50bdcdee0c | ||
|
|
ae60b5e6a1 | ||
|
|
70df4ecaa9 | ||
|
|
b6be5e3eda | ||
|
|
dec323e786 | ||
|
|
6ca0cd1e8b | ||
|
|
3106934678 | ||
|
|
8c5985f68a | ||
|
|
cf8c205644 | ||
|
|
a571033b43 | ||
|
|
cdf27f1447 | ||
|
|
c30b920193 | ||
|
|
697c5f424e | ||
|
|
18c97a08c3 | ||
|
|
66a871840e | ||
|
|
46a26560fd | ||
|
|
1c808a3375 | ||
|
|
2bc8a4a779 | ||
|
|
7f1a9a611f | ||
|
|
b51409ed5e | ||
|
|
3775b54554 | ||
|
|
88b898458b | ||
|
|
9186144dcd | ||
|
|
25bcd0ea25 | ||
|
|
50d08a2eba | ||
|
|
3a7a0c66ab | ||
|
|
83525b7a92 | ||
|
|
f31f023c89 | ||
|
|
f8efefffaa | ||
|
|
d698083ede | ||
|
|
11ba6440d7 | ||
|
|
89ee37a2d5 | ||
|
|
45b8c1e267 | ||
|
|
fbe091f167 | ||
|
|
e09656f20e | ||
|
|
eeb373fca9 | ||
|
|
97ba67f4ee | ||
|
|
909baf5e7a | ||
|
|
a0440603b7 | ||
|
|
e2cd0ccd0e | ||
|
|
378fc4120a | ||
|
|
0dd842744a | ||
|
|
7a73a524b9 | ||
|
|
d1a1bb446b | ||
|
|
c146d92425 | ||
|
|
c888becfa7 | ||
|
|
09f3f62194 | ||
|
|
b820e67616 | ||
|
|
d7da559885 | ||
|
|
d7a197b3a3 | ||
|
|
66cda04664 | ||
|
|
0764f4da86 | ||
|
|
06815fe177 | ||
|
|
04ec6a6999 | ||
|
|
737f23a0bd | ||
|
|
3c48e13c9f | ||
|
|
426734beef | ||
|
|
056b4375eb | ||
|
|
1f0a5e1eea | ||
|
|
8607a0881d | ||
|
|
b4b98505ba | ||
|
|
60d687c2c6 | ||
|
|
5750f7fccb | ||
|
|
c91a9495e6 | ||
|
|
f42b806889 | ||
|
|
a5751b294f | ||
|
|
3f6f2d7d65 | ||
|
|
782aee92a7 | ||
|
|
972b7e84fe | ||
|
|
150e26dc2b | ||
|
|
0dea7a23e3 | ||
|
|
01addeae08 | ||
|
|
a1e507baf8 | ||
|
|
1accb4ff34 | ||
|
|
59cd6dbf70 | ||
|
|
3c86f3894b | ||
|
|
06bef148f4 | ||
|
|
5d883c6e06 | ||
|
|
b62053812b | ||
|
|
a2321edf3c | ||
|
|
24a6ad148c | ||
|
|
5071473767 | ||
|
|
4825da8e9c | ||
|
|
b346666a52 | ||
|
|
83307684a3 | ||
|
|
da25951f6e | ||
|
|
4398fd84d2 | ||
|
|
bbd6d019e5 | ||
|
|
625172e07d | ||
|
|
1e9c7d3c6d | ||
|
|
4cdab4e2d8 | ||
|
|
c2bc7b3cdc | ||
|
|
2c3417062a | ||
|
|
c75abfb894 | ||
|
|
1157b4aee8 | ||
|
|
71dc2d374d | ||
|
|
0a224f919b | ||
|
|
7ef4b4f3d9 | ||
|
|
13b875c763 | ||
|
|
b02b07ffaf | ||
|
|
8804bc2815 | ||
|
|
61cef0a75c | ||
|
|
73bc5252a1 | ||
|
|
f2b10ad132 | ||
|
|
100ea46f03 | ||
|
|
b3ef05e5e1 | ||
|
|
45c994e4de | ||
|
|
a72545639d | ||
|
|
dfd614c00c | ||
|
|
29374837c6 | ||
|
|
2681a14d05 | ||
|
|
f436f6ee2e | ||
|
|
f18bc62690 | ||
|
|
6db73df649 | ||
|
|
93215f1737 | ||
|
|
70aa94b8a4 | ||
|
|
e8998a79c7 | ||
|
|
3b25fdbc5f | ||
|
|
6c8577678c | ||
|
|
70ed9c7c4d | ||
|
|
81fe5deaa9 | ||
|
|
72e4b16a5b | ||
|
|
fe2befcec2 | ||
|
|
1888f5ffd5 | ||
|
|
c59af22217 | ||
|
|
33983b051b | ||
|
|
11d0d4d128 | ||
|
|
a4242dee64 | ||
|
|
0d6c9623ce | ||
|
|
0923bcd2ca | ||
|
|
fdc7ae7760 | ||
|
|
1a73f49cd2 | ||
|
|
23f85162d0 | ||
|
|
7a238028a7 | ||
|
|
3d6c361037 | ||
|
|
9e1f8d83f8 | ||
|
|
fa0aa6defc | ||
|
|
70366d2124 | ||
|
|
10bdb47eae | ||
|
|
a38c4e0c6e | ||
|
|
6c6b03bda0 | ||
|
|
9e02e31917 | ||
|
|
3fd58f1a91 | ||
|
|
9151489481 | ||
|
|
f19296ac7f | ||
|
|
36868ee7b1 | ||
|
|
d559f9f52e | ||
|
|
6440b5fbf5 | ||
|
|
97c4914573 | ||
|
|
7ce94c27fe | ||
|
|
eb54c0026d | ||
|
|
fe00e209ff | ||
|
|
aed80732f9 | ||
|
|
aa097a2fe6 | ||
|
|
3b860e784c | ||
|
|
96ee38759d | ||
|
|
986d3c8f13 | ||
|
|
320120883c | ||
|
|
4fc4da6ed2 | ||
|
|
6f4042f401 | ||
|
|
ea2b4c3e25 | ||
|
|
fc546ca3f6 | ||
|
|
6b158e760d | ||
|
|
5710cab972 | ||
|
|
eb759efb3d | ||
|
|
1df996601d | ||
|
|
c32891ec02 | ||
|
|
2bf6d48fcf | ||
|
|
e49a943cf7 | ||
|
|
67524e14ee | ||
|
|
2290eb0dd2 | ||
|
|
0afcf67c32 | ||
|
|
952bdfaac2 | ||
|
|
ed7e5cd325 | ||
|
|
a15f46e741 | ||
|
|
050a27a409 | ||
|
|
382483b063 | ||
|
|
1675408161 | ||
|
|
1d8b08dcce | ||
|
|
afed581079 | ||
|
|
ff107a0674 | ||
|
|
72da3d0f1e | ||
|
|
5a2e6697e0 | ||
|
|
799cfe1de4 | ||
|
|
6df0264d51 | ||
|
|
a859ecaad1 | ||
|
|
4f088c93c9 | ||
|
|
a1ab19d127 | ||
|
|
d869108416 | ||
|
|
2d6618da3c | ||
|
|
47fe84e922 | ||
|
|
735bf9930a | ||
|
|
769137fc09 | ||
|
|
3a5b3ad77d | ||
|
|
859101ddc9 | ||
|
|
29a50da635 | ||
|
|
5f0fa68d73 | ||
|
|
2f39b10baa | ||
|
|
5a550cc579 | ||
|
|
4b58cb4ce6 | ||
|
|
3872a2fd91 | ||
|
|
5d613ada83 | ||
|
|
9de80b635a | ||
|
|
748aee584a | ||
|
|
3cbfddcc83 | ||
|
|
1d71b6b93e | ||
|
|
398dba4fc8 | ||
|
|
298813d4fa | ||
|
|
56d141c741 | ||
|
|
47a7f729dd | ||
|
|
7806eb980f | ||
|
|
a59888224c | ||
|
|
58ad4759f0 | ||
|
|
87f79290ba | ||
|
|
9326d78439 | ||
|
|
a93887a790 | ||
|
|
d7fa131a8a | ||
|
|
79a4444928 | ||
|
|
572fae5c7d | ||
|
|
5dafaaced4 | ||
|
|
65a303d48f | ||
|
|
00c71b7236 | ||
|
|
ef04903a7a | ||
|
|
a2ec7f622c | ||
|
|
2f91e7bd47 | ||
|
|
80a7c6d3c3 | ||
|
|
7a92565a0c | ||
|
|
661920c51e | ||
|
|
a6b905e148 | ||
|
|
a6b7c1f18c | ||
|
|
7a700ca077 | ||
|
|
1539b43074 | ||
|
|
463a00b1ac | ||
|
|
82692d7053 | ||
|
|
1cccfdd2b9 | ||
|
|
855aa32f54 | ||
|
|
0f8332fe3c | ||
|
|
40e2976ba2 | ||
|
|
e46300828e | ||
|
|
8c5b964722 | ||
|
|
43eafbccb3 | ||
|
|
f32b69b8f1 | ||
|
|
2a16653642 | ||
|
|
b47e89a7d5 | ||
|
|
c17a31a8f8 | ||
|
|
fbbdad75f6 | ||
|
|
7abb6d4998 | ||
|
|
1dabe83d04 | ||
|
|
0d735dc259 | ||
|
|
7b86e1feb0 | ||
|
|
d516627957 | ||
|
|
fb1c67490a | ||
|
|
8b9600b930 | ||
|
|
cbb98c4050 | ||
|
|
e7ff56f1cd | ||
|
|
7705a5de06 | ||
|
|
77ab096b59 | ||
|
|
26a3ec41d6 | ||
|
|
3bcbfe8d97 | ||
|
|
870b2c4f84 | ||
|
|
5f9c7a70ff | ||
|
|
f7179d4255 | ||
|
|
eb0558ca3f | ||
|
|
5585355263 | ||
|
|
e468ca4881 | ||
|
|
4c078dea2c | ||
|
|
783dbd1e6b | ||
|
|
b49619d9bf | ||
|
|
a290b88cd6 | ||
|
|
b61027607f | ||
|
|
f55c872180 |
@@ -172,8 +172,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
|
||||
* **C++ Class Pattern:**
|
||||
```cpp
|
||||
namespace esphome {
|
||||
namespace my_component {
|
||||
namespace esphome::my_component {
|
||||
|
||||
class MyComponent : public Component {
|
||||
public:
|
||||
@@ -189,8 +188,7 @@ This document provides essential context for AI models interacting with this pro
|
||||
int param_{0};
|
||||
};
|
||||
|
||||
} // namespace my_component
|
||||
} // namespace esphome
|
||||
} // namespace esphome::my_component
|
||||
```
|
||||
|
||||
* **Common Component Examples:**
|
||||
@@ -404,35 +402,45 @@ This document provides essential context for AI models interacting with this pro
|
||||
_use_feature = True
|
||||
```
|
||||
|
||||
**Good Pattern (CORE.data with Helpers):**
|
||||
**Bad Pattern (Flat Keys):**
|
||||
```python
|
||||
# Don't do this - keys should be namespaced under component domain
|
||||
MY_FEATURE_KEY = "my_component_feature"
|
||||
CORE.data[MY_FEATURE_KEY] = True
|
||||
```
|
||||
|
||||
**Good Pattern (dataclass):**
|
||||
```python
|
||||
from dataclasses import dataclass, field
|
||||
from esphome.core import CORE
|
||||
|
||||
# Keys for CORE.data storage
|
||||
COMPONENT_STATE_KEY = "my_component_state"
|
||||
USE_FEATURE_KEY = "my_component_use_feature"
|
||||
DOMAIN = "my_component"
|
||||
|
||||
def _get_component_state() -> list:
|
||||
"""Get component state from CORE.data."""
|
||||
return CORE.data.setdefault(COMPONENT_STATE_KEY, [])
|
||||
@dataclass
|
||||
class MyComponentData:
|
||||
feature_enabled: bool = False
|
||||
item_count: int = 0
|
||||
items: list[str] = field(default_factory=list)
|
||||
|
||||
def _get_use_feature() -> bool | None:
|
||||
"""Get feature flag from CORE.data."""
|
||||
return CORE.data.get(USE_FEATURE_KEY)
|
||||
def _get_data() -> MyComponentData:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = MyComponentData()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
def _set_use_feature(value: bool) -> None:
|
||||
"""Set feature flag in CORE.data."""
|
||||
CORE.data[USE_FEATURE_KEY] = value
|
||||
def request_feature() -> None:
|
||||
_get_data().feature_enabled = True
|
||||
|
||||
def enable_feature():
|
||||
_set_use_feature(True)
|
||||
def add_item(item: str) -> None:
|
||||
_get_data().items.append(item)
|
||||
```
|
||||
|
||||
If you need a real-world example, search for components that use `@dataclass` with `CORE.data` in the codebase. Note: Some components may use `TypedDict` for dictionary-based storage; both patterns are acceptable depending on your needs.
|
||||
|
||||
**Why this matters:**
|
||||
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
|
||||
- `CORE.data` automatically clears between runs
|
||||
- Typed helper functions provide better IDE support and maintainability
|
||||
- Encapsulation makes state management explicit and testable
|
||||
- Namespacing under `DOMAIN` prevents key collisions between components
|
||||
- `@dataclass` provides type safety and cleaner attribute access
|
||||
|
||||
* **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c
|
||||
29270eecb86ffa07b2b1d2a4ca56dd7f84762ddc89c6248dbf3f012eca8780b6
|
||||
|
||||
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
1
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -7,6 +7,7 @@
|
||||
- [ ] Bugfix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Developer breaking change (an API change that could break external components)
|
||||
- [ ] Code quality improvements to existing code or addition of tests
|
||||
- [ ] Other
|
||||
|
||||
|
||||
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -17,7 +17,7 @@ runs:
|
||||
steps:
|
||||
- name: Set up Python ${{ inputs.python-version }}
|
||||
id: python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
|
||||
6
.github/workflows/auto-label-pr.yml
vendored
6
.github/workflows/auto-label-pr.yml
vendored
@@ -22,11 +22,11 @@ jobs:
|
||||
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@67018539274d69449ef7c02e8e71183d1719ab42 # v2
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
@@ -68,6 +68,7 @@ jobs:
|
||||
'bugfix',
|
||||
'new-feature',
|
||||
'breaking-change',
|
||||
'developer-breaking-change',
|
||||
'code-quality'
|
||||
];
|
||||
|
||||
@@ -367,6 +368,7 @@ jobs:
|
||||
{ pattern: /- \[x\] Bugfix \(non-breaking change which fixes an issue\)/i, label: 'bugfix' },
|
||||
{ pattern: /- \[x\] New feature \(non-breaking change which adds functionality\)/i, label: 'new-feature' },
|
||||
{ pattern: /- \[x\] Breaking change \(fix or feature that would cause existing functionality to not work as expected\)/i, label: 'breaking-change' },
|
||||
{ pattern: /- \[x\] Developer breaking change \(an API change that could break external components\)/i, label: 'developer-breaking-change' },
|
||||
{ pattern: /- \[x\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||
];
|
||||
|
||||
|
||||
4
.github/workflows/ci-api-proto.yml
vendored
4
.github/workflows/ci-api-proto.yml
vendored
@@ -21,9 +21,9 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
4
.github/workflows/ci-clang-tidy-hash.yml
vendored
@@ -21,10 +21,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
|
||||
4
.github/workflows/ci-docker.yml
vendored
4
.github/workflows/ci-docker.yml
vendored
@@ -43,9 +43,9 @@ jobs:
|
||||
- "docker"
|
||||
# - "lint"
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Set up Docker Buildx
|
||||
|
||||
@@ -49,7 +49,7 @@ jobs:
|
||||
|
||||
- name: Check out code from base repository
|
||||
if: steps.pr.outputs.skip != 'true'
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Always check out from the base repository (esphome/esphome), never from forks
|
||||
# Use the PR's target branch to ensure we run trusted code from the main repo
|
||||
|
||||
36
.github/workflows/ci.yml
vendored
36
.github/workflows/ci.yml
vendored
@@ -36,13 +36,13 @@ jobs:
|
||||
cache-key: ${{ steps.cache-key.outputs.key }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Generate cache-key
|
||||
id: cache-key
|
||||
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
|
||||
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
|
||||
id: python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
@@ -70,7 +70,7 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.python-linters == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -91,7 +91,7 @@ jobs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -132,7 +132,7 @@ jobs:
|
||||
- common
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
id: restore-python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -183,7 +183,7 @@ jobs:
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Fetch enough history to find the merge base
|
||||
fetch-depth: 2
|
||||
@@ -237,10 +237,10 @@ jobs:
|
||||
if: needs.determine-jobs.outputs.integration-tests == 'true'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python 3.13
|
||||
id: python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
@@ -273,7 +273,7 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
@@ -321,7 +321,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -400,7 +400,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -489,7 +489,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
# Need history for HEAD~1 to work for checking changed files
|
||||
fetch-depth: 2
|
||||
@@ -577,7 +577,7 @@ jobs:
|
||||
version: 1.0
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -662,13 +662,13 @@ jobs:
|
||||
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
||||
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
||||
env:
|
||||
SKIP: pylint,clang-tidy-hash
|
||||
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
||||
@@ -688,7 +688,7 @@ jobs:
|
||||
skip: ${{ steps.check-script.outputs.skip }}
|
||||
steps:
|
||||
- name: Check out target branch
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
|
||||
@@ -840,7 +840,7 @@ jobs:
|
||||
flash_usage: ${{ steps.extract.outputs.flash_usage }}
|
||||
steps:
|
||||
- name: Check out PR branch
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
@@ -908,7 +908,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
|
||||
@@ -21,7 +21,7 @@ permissions:
|
||||
jobs:
|
||||
request-codeowner-reviews:
|
||||
name: Run
|
||||
if: ${{ !github.event.pull_request.draft }}
|
||||
if: ${{ github.repository == 'esphome/esphome' && !github.event.pull_request.draft }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Request reviews from component codeowners
|
||||
|
||||
6
.github/workflows/codeql.yml
vendored
6
.github/workflows/codeql.yml
vendored
@@ -54,11 +54,11 @@ jobs:
|
||||
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||
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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
|
||||
uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
65
.github/workflows/release.yml
vendored
65
.github/workflows/release.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
||||
branch_build: ${{ steps.tag.outputs.branch_build }}
|
||||
deploy_env: ${{ steps.tag.outputs.deploy_env }}
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Get tag
|
||||
id: tag
|
||||
# yamllint disable rule:line-length
|
||||
@@ -60,9 +60,9 @@ jobs:
|
||||
contents: read
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.x"
|
||||
- name: Build
|
||||
@@ -92,9 +92,9 @@ jobs:
|
||||
os: "ubuntu-24.04-arm"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
@@ -168,7 +168,7 @@ jobs:
|
||||
- ghcr
|
||||
- dockerhub
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
|
||||
@@ -219,10 +219,19 @@ jobs:
|
||||
- init
|
||||
- deploy-manifest
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: home-assistant-addon
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
let description = "ESPHome";
|
||||
if (context.eventName == "release") {
|
||||
@@ -245,10 +254,19 @@ jobs:
|
||||
needs: [init]
|
||||
environment: ${{ needs.init.outputs.deploy_env }}
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: esphome-schema
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: "esphome",
|
||||
@@ -259,3 +277,34 @@ jobs:
|
||||
version: "${{ needs.init.outputs.tag }}",
|
||||
}
|
||||
})
|
||||
|
||||
version-notifier:
|
||||
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- init
|
||||
- deploy-manifest
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2.2.0
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
owner: esphome
|
||||
repositories: version-notifier
|
||||
|
||||
- name: Trigger Workflow
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
github-token: ${{ steps.generate-token.outputs.token }}
|
||||
script: |
|
||||
github.rest.actions.createWorkflowDispatch({
|
||||
owner: "esphome",
|
||||
repo: "version-notifier",
|
||||
workflow_id: "notify.yml",
|
||||
ref: "main",
|
||||
inputs: {
|
||||
version: "${{ needs.init.outputs.tag }}",
|
||||
}
|
||||
})
|
||||
|
||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Stale
|
||||
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
||||
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||
with:
|
||||
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
||||
remove-stale-when-updated: true
|
||||
|
||||
8
.github/workflows/sync-device-classes.yml
vendored
8
.github/workflows/sync-device-classes.yml
vendored
@@ -13,16 +13,16 @@ jobs:
|
||||
if: github.repository == 'esphome/esphome'
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Checkout Home Assistant
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
with:
|
||||
repository: home-assistant/core
|
||||
path: lib/home-assistant
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
|
||||
with:
|
||||
python-version: 3.13
|
||||
|
||||
@@ -41,7 +41,7 @@ jobs:
|
||||
python script/run-in-env.py pre-commit run --all-files
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
|
||||
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
|
||||
with:
|
||||
commit-message: "Synchronise Device Classes from Home Assistant"
|
||||
committer: esphomebot <esphome@openhomefoundation.org>
|
||||
|
||||
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.14.4
|
||||
rev: v0.14.8
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
||||
@@ -21,6 +21,7 @@ esphome/components/adc128s102/* @DeerMaximum
|
||||
esphome/components/addressable_light/* @justfalter
|
||||
esphome/components/ade7880/* @kpfleming
|
||||
esphome/components/ade7953/* @angelnu
|
||||
esphome/components/ade7953_base/* @angelnu
|
||||
esphome/components/ade7953_i2c/* @angelnu
|
||||
esphome/components/ade7953_spi/* @angelnu
|
||||
esphome/components/ads1118/* @solomondg1
|
||||
@@ -72,6 +73,7 @@ esphome/components/bl0942/* @dbuezas @dwmw2
|
||||
esphome/components/ble_client/* @buxtronix @clydebarrow
|
||||
esphome/components/ble_nus/* @tomaszduda23
|
||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
|
||||
esphome/components/bm8563/* @abmantis
|
||||
esphome/components/bme280_base/* @esphome/core
|
||||
esphome/components/bme280_spi/* @apbodrov
|
||||
esphome/components/bme680_bsec/* @trvrnrth
|
||||
@@ -95,6 +97,7 @@ esphome/components/camera_encoder/* @DT-art1
|
||||
esphome/components/canbus/* @danielschramm @mvturnho
|
||||
esphome/components/cap1188/* @mreditor97
|
||||
esphome/components/captive_portal/* @esphome/core
|
||||
esphome/components/cc1101/* @gabest11 @lygris
|
||||
esphome/components/ccs811/* @habbie
|
||||
esphome/components/cd74hc4067/* @asoehlke
|
||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||
@@ -188,6 +191,7 @@ esphome/components/gps/* @coogle @ximex
|
||||
esphome/components/graph/* @synco
|
||||
esphome/components/graphical_display_menu/* @MrMDavidson
|
||||
esphome/components/gree/* @orestismers
|
||||
esphome/components/gree/switch/* @nagyrobi
|
||||
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
|
||||
esphome/components/grove_tb6612fng/* @max246
|
||||
esphome/components/growatt_solar/* @leeuwte
|
||||
@@ -202,10 +206,12 @@ esphome/components/havells_solar/* @sourabhjaiswal
|
||||
esphome/components/hbridge/fan/* @WeekendWarrior
|
||||
esphome/components/hbridge/light/* @DotNetDann
|
||||
esphome/components/hbridge/switch/* @dwmw2
|
||||
esphome/components/hc8/* @omartijn
|
||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
|
||||
esphome/components/he60r/* @clydebarrow
|
||||
esphome/components/heatpumpir/* @rob-deutsch
|
||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
|
||||
esphome/components/hlk_fm22x/* @OnFreund
|
||||
esphome/components/hm3301/* @freekode
|
||||
esphome/components/hmac_md5/* @dwmw2
|
||||
esphome/components/homeassistant/* @esphome/core @OttoWinter
|
||||
@@ -395,6 +401,7 @@ esphome/components/rpi_dpi_rgb/* @clydebarrow
|
||||
esphome/components/rtl87xx/* @kuba2k2
|
||||
esphome/components/rtttl/* @glmnet
|
||||
esphome/components/runtime_stats/* @bdraco
|
||||
esphome/components/rx8130/* @beormund
|
||||
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
|
||||
esphome/components/scd4x/* @martgras @sjtrny
|
||||
esphome/components/script/* @esphome/core
|
||||
@@ -458,6 +465,7 @@ esphome/components/st7735/* @SenexCrenshaw
|
||||
esphome/components/st7789v/* @kbx81
|
||||
esphome/components/st7920/* @marsjan155
|
||||
esphome/components/statsd/* @Links2004
|
||||
esphome/components/stts22h/* @B48D81EFCC
|
||||
esphome/components/substitutions/* @esphome/core
|
||||
esphome/components/sun/* @OttoWinter
|
||||
esphome/components/sun_gtil2/* @Mat931
|
||||
@@ -479,6 +487,7 @@ esphome/components/template/datetime/* @rfdarter
|
||||
esphome/components/template/event/* @nohat
|
||||
esphome/components/template/fan/* @ssieb
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/thermopro_ble/* @sittner
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @esphome/core
|
||||
esphome/components/tinyusb/* @kbx81
|
||||
|
||||
2
Doxyfile
2
Doxyfile
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2025.11.0-dev
|
||||
PROJECT_NUMBER = 2025.12.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -1,309 +0,0 @@
|
||||
# Sensor Callback Optimization - Zero-Cost Implementation
|
||||
|
||||
## The Perfect Optimization
|
||||
|
||||
By storing the partition count **in the Sensor class** alongside existing small fields, we achieve a **zero-cost optimization** with only wins and no losses!
|
||||
|
||||
## Implementation Design
|
||||
|
||||
### Key Insight: Reuse Available Padding
|
||||
|
||||
Sensor already has grouped small fields with 1 byte of available space:
|
||||
|
||||
```cpp
|
||||
class Sensor {
|
||||
protected:
|
||||
// Existing small members grouped together
|
||||
int8_t accuracy_decimals_{-1}; // 1 byte
|
||||
StateClass state_class_{STATE_CLASS_NONE}; // 1 byte (uint8_t enum)
|
||||
|
||||
struct SensorFlags {
|
||||
uint8_t has_accuracy_override : 1;
|
||||
uint8_t has_state_class_override : 1;
|
||||
uint8_t force_update : 1;
|
||||
uint8_t reserved : 5;
|
||||
} sensor_flags_{}; // 1 byte
|
||||
|
||||
uint8_t filtered_count_{0}; // 1 byte ← NEW! Perfect fit!
|
||||
// Total: 4 bytes (naturally aligned, no padding waste)
|
||||
};
|
||||
```
|
||||
|
||||
### Callbacks Structure (Heap-Allocated)
|
||||
|
||||
```cpp
|
||||
class Sensor {
|
||||
protected:
|
||||
std::unique_ptr<std::vector<std::function<void(float)>>> callbacks_;
|
||||
|
||||
// Partition layout: [filtered_0, ..., filtered_n-1, raw_0, ..., raw_m-1]
|
||||
// ^ ^
|
||||
// 0 filtered_count_
|
||||
};
|
||||
```
|
||||
|
||||
### Core Methods
|
||||
|
||||
```cpp
|
||||
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = std::make_unique<std::vector<std::function<void(float)>>>();
|
||||
}
|
||||
|
||||
// Add to filtered section: append + swap into position
|
||||
this->callbacks_->push_back(std::move(callback));
|
||||
if (this->filtered_count_ < this->callbacks_->size() - 1) {
|
||||
std::swap((*this->callbacks_)[this->filtered_count_],
|
||||
(*this->callbacks_)[this->callbacks_->size() - 1]);
|
||||
}
|
||||
this->filtered_count_++;
|
||||
}
|
||||
|
||||
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = std::make_unique<std::vector<std::function<void(float)>>>();
|
||||
}
|
||||
|
||||
// Add to raw section: just append (already at end)
|
||||
this->callbacks_->push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void Sensor::publish_state(float state) {
|
||||
this->raw_state = state;
|
||||
|
||||
// Call raw callbacks (before filters)
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = this->filtered_count_; i < this->callbacks_->size(); i++) {
|
||||
(*this->callbacks_)[i](state);
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
|
||||
|
||||
// ... apply filters ...
|
||||
}
|
||||
|
||||
void Sensor::internal_send_state_to_frontend(float state) {
|
||||
this->set_has_state(true);
|
||||
this->state = state;
|
||||
|
||||
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy",
|
||||
this->get_name().c_str(), state, this->get_unit_of_measurement_ref().c_str(),
|
||||
this->get_accuracy_decimals());
|
||||
|
||||
// Call filtered callbacks (after filters)
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = 0; i < this->filtered_count_; i++) {
|
||||
(*this->callbacks_)[i](state);
|
||||
}
|
||||
}
|
||||
|
||||
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
|
||||
ControllerRegistry::notify_sensor_update(this);
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
## Memory Analysis (ESP32 32-bit)
|
||||
|
||||
### Current Implementation
|
||||
```cpp
|
||||
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; // 4 bytes
|
||||
CallbackManager<void(float)> callback_; // 12 bytes
|
||||
```
|
||||
|
||||
### Partitioned Implementation
|
||||
```cpp
|
||||
std::unique_ptr<std::vector<std::function<void(float)>>> callbacks_; // 4 bytes
|
||||
uint8_t filtered_count_{0}; // 0 bytes (uses existing padding slot)
|
||||
```
|
||||
|
||||
## Memory Comparison
|
||||
|
||||
| Scenario | Current | Partitioned | Savings |
|
||||
|----------|---------|-------------|---------|
|
||||
| **No callbacks** | 16 bytes | 4 bytes | **+12 bytes** ✅ |
|
||||
| **1 filtered (MQTT)** | 32 bytes | 32 bytes | **±0 bytes** ✅ |
|
||||
| **1 raw only** | 44 bytes | 32 bytes | **+12 bytes** ✅ |
|
||||
| **1 raw + 1 filtered** | 60 bytes | 48 bytes | **+12 bytes** ✅ |
|
||||
| **2 filtered** | 48 bytes | 48 bytes | **±0 bytes** ✅ |
|
||||
|
||||
### Detailed Breakdown
|
||||
|
||||
**No callbacks:**
|
||||
- Current: 4 (raw ptr) + 12 (callback_ vec) = 16 bytes
|
||||
- Partitioned: 4 (callbacks_ ptr) + 0 (count uses existing padding) = **4 bytes**
|
||||
- **Saves: 12 bytes** ✅
|
||||
|
||||
**1 filtered callback (MQTT):**
|
||||
- Current: 4 + 12 + 16 (function) = 32 bytes
|
||||
- Partitioned: 4 (ptr) + 12 (vector on heap) + 16 (function) = **32 bytes**
|
||||
- **Saves: 0 bytes** (ZERO COST!) ✅
|
||||
|
||||
**1 raw + 1 filtered:**
|
||||
- Current: 4 + 12 + 12 (raw vec on heap) + 16 + 16 = 60 bytes
|
||||
- Partitioned: 4 + 12 + 16 + 16 = **48 bytes**
|
||||
- **Saves: 12 bytes** ✅
|
||||
|
||||
## Real-World Impact
|
||||
|
||||
### Typical IoT Device (15 sensors)
|
||||
**API-only (no MQTT, no automations):**
|
||||
- Current: 15 × 16 = 240 bytes
|
||||
- Optimized: 15 × 4 = 60 bytes
|
||||
- **Saves: 180 bytes** ✅
|
||||
|
||||
**With MQTT on all sensors:**
|
||||
- Current: 15 × 32 = 480 bytes
|
||||
- Optimized: 15 × 32 = 480 bytes
|
||||
- **Saves: 0 bytes** (ZERO COST!) ✅
|
||||
|
||||
**Mixed (10 API-only + 5 MQTT):**
|
||||
- Current: (10 × 16) + (5 × 32) = 320 bytes
|
||||
- Optimized: (10 × 4) + (5 × 32) = 200 bytes
|
||||
- **Saves: 120 bytes** ✅
|
||||
|
||||
### Large Dashboard (50 sensors)
|
||||
**API-only:**
|
||||
- Current: 50 × 16 = 800 bytes
|
||||
- Optimized: 50 × 4 = 200 bytes
|
||||
- **Saves: 600 bytes** ✅
|
||||
|
||||
**With MQTT on 20 sensors:**
|
||||
- Current: (30 × 16) + (20 × 32) = 1,120 bytes
|
||||
- Optimized: (30 × 4) + (20 × 32) = 760 bytes
|
||||
- **Saves: 360 bytes** ✅
|
||||
|
||||
## Performance Characteristics
|
||||
|
||||
### Time Complexity
|
||||
- `add_on_state_callback()`: **O(1)** - append + swap
|
||||
- `add_on_raw_state_callback()`: **O(1)** - append
|
||||
- `publish_state()` (call raw): **O(m)** - iterate raw section
|
||||
- `internal_send_state_to_frontend()` (call filtered): **O(n)** - iterate filtered section
|
||||
|
||||
### Hot Path Performance
|
||||
**Before:**
|
||||
```cpp
|
||||
if (this->raw_callback_) {
|
||||
this->raw_callback_->call(state); // Separate container
|
||||
}
|
||||
// ...
|
||||
this->callback_.call(state); // Separate container
|
||||
```
|
||||
|
||||
**After:**
|
||||
```cpp
|
||||
// Call raw callbacks
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = filtered_count_; i < callbacks_->size(); i++) {
|
||||
(*callbacks_)[i](state);
|
||||
}
|
||||
}
|
||||
// ...
|
||||
// Call filtered callbacks
|
||||
if (this->callbacks_) {
|
||||
for (size_t i = 0; i < filtered_count_; i++) {
|
||||
(*callbacks_)[i](state);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Performance impact:**
|
||||
- ✅ Better cache locality (single vector instead of two containers)
|
||||
- ✅ No branching inside loops (vs checking callback types)
|
||||
- ✅ Tight loops for typical 0-2 callbacks case
|
||||
- ⚠️ One extra nullptr check (negligible, likely free with branch prediction)
|
||||
|
||||
## Advantages
|
||||
|
||||
### Memory
|
||||
1. ✅ **12 bytes saved** per sensor without callbacks (most common after Controller Registry)
|
||||
2. ✅ **ZERO cost** for MQTT-enabled sensors (32 → 32 bytes)
|
||||
3. ✅ **12 bytes saved** for sensors with both raw + filtered callbacks
|
||||
4. ✅ **No padding waste** (reuses existing padding slot in Sensor class)
|
||||
|
||||
### Architecture
|
||||
1. ✅ **Cleaner:** ONE vector instead of TWO separate CallbackManager instances
|
||||
2. ✅ **Simpler:** Partitioned vector is more elegant than dual containers
|
||||
3. ✅ **Better cache locality:** Callbacks stored contiguously
|
||||
4. ✅ **O(1) insertion:** Both add operations use append (+ optional swap)
|
||||
|
||||
### Code Quality
|
||||
1. ✅ **No new fields in hot path:** filtered_count_ reuses padding
|
||||
2. ✅ **No branching in iteration:** Direct range iteration
|
||||
3. ✅ **Order preservation not needed:** Callbacks are independent
|
||||
|
||||
## Implementation Files
|
||||
|
||||
### Modified Files
|
||||
- `esphome/components/sensor/sensor.h`
|
||||
- `esphome/components/sensor/sensor.cpp`
|
||||
|
||||
### Changes Required
|
||||
1. Replace callback storage with partitioned vector
|
||||
2. Update `add_on_state_callback()` to use swap-based insertion
|
||||
3. Update `add_on_raw_state_callback()` to append
|
||||
4. Update `publish_state()` to iterate raw section
|
||||
5. Update `internal_send_state_to_frontend()` to iterate filtered section
|
||||
6. Add `filtered_count_` field (uses existing padding)
|
||||
|
||||
## TextSensor Implementation
|
||||
|
||||
TextSensor can use the **exact same pattern**:
|
||||
|
||||
```cpp
|
||||
class TextSensor {
|
||||
protected:
|
||||
std::unique_ptr<std::vector<std::function<void(std::string)>>> callbacks_;
|
||||
uint8_t filtered_count_{0}; // Store in class (check for available padding)
|
||||
};
|
||||
```
|
||||
|
||||
Same benefits apply!
|
||||
|
||||
## Migration Risk Assessment
|
||||
|
||||
### Low Risk
|
||||
- ✅ No API changes (public methods unchanged)
|
||||
- ✅ Callback behavior identical (same execution order within each type)
|
||||
- ✅ Only internal implementation changes
|
||||
- ✅ Well-tested pattern (partitioned vectors common in CS)
|
||||
|
||||
### Testing Strategy
|
||||
1. Unit tests: Verify callback execution order preserved
|
||||
2. Integration tests: Test with MQTT, automations, copy components
|
||||
3. Memory benchmarks: Confirm actual RAM savings on real devices
|
||||
4. Regression tests: Ensure no behavior changes for existing configs
|
||||
|
||||
## Recommendation
|
||||
|
||||
**IMPLEMENT IMMEDIATELY** ✅
|
||||
|
||||
This optimization has:
|
||||
- ✅ **Zero cost** for MQTT users (32 → 32 bytes)
|
||||
- ✅ **12-byte savings** for API-only sensors (most common)
|
||||
- ✅ **12-byte savings** for sensors with automations
|
||||
- ✅ **Better architecture** (one container vs two)
|
||||
- ✅ **No downsides** whatsoever
|
||||
|
||||
**Expected savings for typical device: 150-600 bytes**
|
||||
|
||||
This is a **pure win** optimization with no trade-offs!
|
||||
|
||||
## Implementation Priority
|
||||
|
||||
### Phase 1: Sensor ⭐⭐⭐ (HIGHEST PRIORITY)
|
||||
- Most common entity type
|
||||
- Biggest impact
|
||||
- Zero cost even for MQTT users
|
||||
- **Start here!**
|
||||
|
||||
### Phase 2: TextSensor ⭐⭐
|
||||
- Second most common entity with raw callbacks
|
||||
- Same pattern as Sensor
|
||||
|
||||
### Phase 3: Other entities (simple lazy vector) ⭐
|
||||
- BinarySensor, Switch, etc. don't have raw callbacks
|
||||
- Can use simpler lazy-allocated vector
|
||||
- Still save 12 bytes when no callbacks
|
||||
@@ -1,845 +0,0 @@
|
||||
# CallbackManager Optimization Plan
|
||||
|
||||
**Note:** ESPHome uses C++20 (gnu++20), so implementations leverage modern C++ features:
|
||||
- **Concepts** for type constraints and better error messages
|
||||
- **Designated initializers** for cleaner struct initialization
|
||||
- **consteval** for compile-time validation
|
||||
- **Requires clauses** for inline constraints
|
||||
|
||||
## Current State
|
||||
|
||||
### Memory Profile (ESP32 - 32-bit)
|
||||
|
||||
```cpp
|
||||
sizeof(std::function<void(T)>): 32 bytes
|
||||
sizeof(void*): 4 bytes
|
||||
sizeof(function pointer): 4 bytes
|
||||
```
|
||||
|
||||
### Current Implementation
|
||||
|
||||
```cpp
|
||||
template<typename... Ts> class CallbackManager<void(Ts...)> {
|
||||
public:
|
||||
void add(std::function<void(Ts...)> &&callback) {
|
||||
this->callbacks_.push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto &cb : this->callbacks_)
|
||||
cb(args...);
|
||||
}
|
||||
|
||||
size_t size() const { return this->callbacks_.size(); }
|
||||
|
||||
protected:
|
||||
std::vector<std::function<void(Ts...)>> callbacks_;
|
||||
};
|
||||
```
|
||||
|
||||
### Memory Cost Per Instance
|
||||
|
||||
- **Per callback:** 32 bytes (std::function storage)
|
||||
- **Vector reallocation code:** ~132 bytes (`_M_realloc_append` template instantiation)
|
||||
- **Example (1 callback):** 32 + 132 = 164 bytes
|
||||
|
||||
### Codebase Usage
|
||||
|
||||
- **Total CallbackManager instances:** ~67 files
|
||||
- **Estimated total callbacks:** 100-150 across all components
|
||||
- **Examples:**
|
||||
- `sensor.h`: `CallbackManager<void(float)>` - multiple callbacks per sensor
|
||||
- `esp32_ble_tracker.h`: `CallbackManager<void(ScannerState)>` - 1 callback (bluetooth_proxy)
|
||||
- `esp32_improv.h`: `CallbackManager<void(State, Error)>` - up to 5 callbacks (automation triggers)
|
||||
- `climate.h`: `CallbackManager<void()>` - multiple callbacks for state/control
|
||||
|
||||
### Current Usage Pattern
|
||||
|
||||
All callbacks currently use lambda captures:
|
||||
|
||||
```cpp
|
||||
// bluetooth_proxy.cpp
|
||||
parent_->add_scanner_state_callback([this](ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
});
|
||||
|
||||
// sensor.cpp (via automation)
|
||||
sensor->add_on_state_callback([this](float state) {
|
||||
this->trigger(state);
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Optimization Options
|
||||
|
||||
### Option 1: Function Pointer + Context (Recommended)
|
||||
|
||||
**C++20 Implementation (Type-Safe with Concepts):**
|
||||
|
||||
```cpp
|
||||
#include <concepts>
|
||||
#include <type_traits>
|
||||
|
||||
// Concept to validate callback signature
|
||||
template<typename F, typename Context, typename... Ts>
|
||||
concept CallbackFunction = requires(F func, Context* ctx, Ts... args) {
|
||||
{ func(ctx, args...) } -> std::same_as<void>;
|
||||
};
|
||||
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, Ts...); // 4 bytes - type-erased invoker
|
||||
void* context; // 4 bytes - captured context
|
||||
// Total: 8 bytes
|
||||
};
|
||||
|
||||
// Type-safe invoker template - knows real context type
|
||||
template<typename Context>
|
||||
static void invoke(void* ctx, Ts... args) {
|
||||
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(
|
||||
*static_cast<void**>(ctx)
|
||||
);
|
||||
auto typed_ctx = static_cast<Context*>(
|
||||
*reinterpret_cast<void**>(static_cast<char*>(ctx) + sizeof(void*))
|
||||
);
|
||||
typed_func(typed_ctx, args...);
|
||||
}
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
// Type-safe registration with concept constraint
|
||||
template<typename Context>
|
||||
requires CallbackFunction<void(*)(Context*, Ts...), Context, Ts...>
|
||||
void add(void (*func)(Context*, Ts...), Context* context) {
|
||||
// Use designated initializers (C++20)
|
||||
callbacks_.push_back({
|
||||
.invoker = [](void* storage, Ts... args) {
|
||||
// Extract function pointer and context from packed storage
|
||||
void* func_and_ctx[2];
|
||||
std::memcpy(func_and_ctx, storage, sizeof(func_and_ctx));
|
||||
|
||||
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(func_and_ctx[0]);
|
||||
auto typed_ctx = static_cast<Context*>(func_and_ctx[1]);
|
||||
typed_func(typed_ctx, args...);
|
||||
},
|
||||
.context = nullptr // Will store packed data
|
||||
});
|
||||
|
||||
// Pack function pointer and context into the callback storage
|
||||
void* func_and_ctx[2] = { reinterpret_cast<void*>(func), context };
|
||||
std::memcpy(&callbacks_.back(), func_and_ctx, sizeof(func_and_ctx));
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(&cb, args...);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t size() const { return callbacks_.size(); }
|
||||
};
|
||||
```
|
||||
|
||||
**Cleaner C++20 Implementation (12 bytes, simpler):**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, void*, Ts...); // 4 bytes - generic invoker
|
||||
void* func_ptr; // 4 bytes - actual function
|
||||
void* context; // 4 bytes - context
|
||||
// Total: 12 bytes (still 20 bytes saved vs std::function!)
|
||||
};
|
||||
|
||||
template<typename Context>
|
||||
static consteval auto make_invoker() {
|
||||
return +[](void* func, void* ctx, Ts... args) {
|
||||
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(func);
|
||||
typed_func(static_cast<Context*>(ctx), args...);
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
// C++20 concepts for type safety
|
||||
template<typename Context>
|
||||
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
|
||||
void add(void (*func)(Context*, Ts...), Context* context) {
|
||||
// C++20 designated initializers
|
||||
callbacks_.push_back({
|
||||
.invoker = make_invoker<Context>(),
|
||||
.func_ptr = reinterpret_cast<void*>(func),
|
||||
.context = context
|
||||
});
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(cb.func_ptr, cb.context, args...);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t size() const { return callbacks_.size(); }
|
||||
constexpr bool empty() const { return callbacks_.empty(); }
|
||||
};
|
||||
```
|
||||
|
||||
**Most Efficient C++20 Implementation (8 bytes):**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, Ts...); // 4 bytes
|
||||
void* context; // 4 bytes
|
||||
// Total: 8 bytes - maximum savings!
|
||||
};
|
||||
|
||||
// C++20: consteval ensures compile-time evaluation
|
||||
template<typename Context>
|
||||
static consteval auto make_invoker() {
|
||||
// The + forces decay to function pointer
|
||||
return +[](void* ctx, Ts... args) {
|
||||
// Unpack the storage struct
|
||||
struct Storage {
|
||||
void (*func)(Context*, Ts...);
|
||||
Context* context;
|
||||
};
|
||||
auto* storage = static_cast<Storage*>(ctx);
|
||||
storage->func(storage->context, args...);
|
||||
};
|
||||
}
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
template<typename Context>
|
||||
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
|
||||
void add(void (*func)(Context*, Ts...), Context* context) {
|
||||
// Allocate storage for function + context
|
||||
struct Storage {
|
||||
void (*func)(Context*, Ts...);
|
||||
Context* context;
|
||||
};
|
||||
|
||||
auto* storage = new Storage{func, context};
|
||||
|
||||
callbacks_.push_back({
|
||||
.invoker = make_invoker<Context>(),
|
||||
.context = storage
|
||||
});
|
||||
}
|
||||
|
||||
~CallbackManager() {
|
||||
// Clean up storage
|
||||
for (auto& cb : callbacks_) {
|
||||
delete static_cast<void*>(cb.context);
|
||||
}
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(cb.context, args...);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t size() const { return callbacks_.size(); }
|
||||
};
|
||||
```
|
||||
|
||||
**Simplest C++20 Implementation (Recommended):**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, void*, Ts...); // 4 bytes
|
||||
void* func_ptr; // 4 bytes
|
||||
void* context; // 4 bytes
|
||||
// Total: 12 bytes
|
||||
};
|
||||
|
||||
template<typename Context>
|
||||
static void invoke(void* func, void* ctx, Ts... args) {
|
||||
reinterpret_cast<void(*)(Context*, Ts...)>(func)(static_cast<Context*>(ctx), args...);
|
||||
}
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
template<typename Context>
|
||||
requires std::invocable<void(*)(Context*, Ts...), Context*, Ts...>
|
||||
void add(void (*func)(Context*, Ts...), Context* context) {
|
||||
callbacks_.push_back({
|
||||
.invoker = &invoke<Context>,
|
||||
.func_ptr = reinterpret_cast<void*>(func),
|
||||
.context = context
|
||||
});
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(cb.func_ptr, cb.context, args...);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t size() const { return callbacks_.size(); }
|
||||
};
|
||||
```
|
||||
|
||||
**C++20 Benefits:**
|
||||
- ✅ **Concepts** provide clear compile errors
|
||||
- ✅ **Designated initializers** make code more readable
|
||||
- ✅ **consteval** ensures compile-time evaluation
|
||||
- ✅ **constexpr** improvements allow more compile-time validation
|
||||
- ✅ **Requires clauses** document constraints inline
|
||||
|
||||
**Usage Changes:**
|
||||
|
||||
```cpp
|
||||
// OLD (lambda):
|
||||
parent_->add_scanner_state_callback([this](ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
});
|
||||
|
||||
// NEW (static function + context):
|
||||
static void scanner_state_callback(BluetoothProxy* proxy, ScannerState state) {
|
||||
if (proxy->api_connection_ != nullptr) {
|
||||
proxy->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
}
|
||||
|
||||
// Registration
|
||||
parent_->add_scanner_state_callback(scanner_state_callback, this);
|
||||
```
|
||||
|
||||
**Savings:**
|
||||
- **Per callback:** 24 bytes (32 → 8) or 20 bytes (32 → 12 for simpler version)
|
||||
- **RAM saved (100-150 callbacks):** 2.4 - 3.6 KB
|
||||
- **Flash saved:** ~5-10 KB (eliminates std::function template instantiations)
|
||||
|
||||
**Pros:**
|
||||
- ✅ Maximum memory savings (75% reduction)
|
||||
- ✅ Type-safe at registration time
|
||||
- ✅ No virtual function overhead
|
||||
- ✅ Works with all capture patterns
|
||||
- ✅ Simple implementation
|
||||
|
||||
**Cons:**
|
||||
- ❌ Requires converting lambdas to static functions
|
||||
- ❌ Changes API for all 67 CallbackManager users
|
||||
- ❌ More verbose at call site
|
||||
|
||||
---
|
||||
|
||||
### Option 2: Member Function Pointers
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, Ts...); // 4 bytes
|
||||
void* obj; // 4 bytes
|
||||
// Total: 8 bytes
|
||||
};
|
||||
|
||||
template<typename T, void (T::*Method)(Ts...)>
|
||||
static void invoke_member(void* obj, Ts... args) {
|
||||
(static_cast<T*>(obj)->*Method)(args...);
|
||||
}
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
// Register a member function
|
||||
template<typename T, void (T::*Method)(Ts...)>
|
||||
void add(T* obj) {
|
||||
callbacks_.push_back({
|
||||
&invoke_member<T, Method>,
|
||||
obj
|
||||
});
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(cb.obj, args...);
|
||||
}
|
||||
}
|
||||
|
||||
size_t size() const { return callbacks_.size(); }
|
||||
};
|
||||
```
|
||||
|
||||
**Usage Changes:**
|
||||
|
||||
```cpp
|
||||
// Add a method to BluetoothProxy
|
||||
void BluetoothProxy::on_scanner_state_changed(ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
}
|
||||
|
||||
// Register it
|
||||
parent_->add_scanner_state_callback<BluetoothProxy,
|
||||
&BluetoothProxy::on_scanner_state_changed>(this);
|
||||
```
|
||||
|
||||
**Savings:**
|
||||
- **Per callback:** 24 bytes (32 → 8)
|
||||
- **RAM saved:** 2.4 - 3.6 KB
|
||||
- **Flash saved:** ~5-10 KB
|
||||
|
||||
**Pros:**
|
||||
- ✅ Same memory savings as Option 1
|
||||
- ✅ Most type-safe (member function pointers)
|
||||
- ✅ No static functions needed
|
||||
- ✅ Clean separation of callback logic
|
||||
|
||||
**Cons:**
|
||||
- ❌ Verbose syntax at registration: `add<Type, &Type::method>(this)`
|
||||
- ❌ Requires adding methods to classes
|
||||
- ❌ Can't capture additional state beyond `this`
|
||||
- ❌ Template parameters at call site are ugly
|
||||
|
||||
---
|
||||
|
||||
### Option 3: Hybrid (Backward Compatible)
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*invoker)(void*, Ts...); // 4 bytes
|
||||
void* data; // 4 bytes
|
||||
bool is_std_function; // 1 byte + 3 padding = 4 bytes
|
||||
// Total: 12 bytes
|
||||
};
|
||||
|
||||
std::vector<Callback> callbacks_;
|
||||
|
||||
public:
|
||||
// Optimized: function pointer + context
|
||||
template<typename Context>
|
||||
void add(void (*func)(Context*, Ts...), Context* context) {
|
||||
callbacks_.push_back({
|
||||
[](void* ctx, Ts... args) {
|
||||
auto cb = static_cast<Callback*>(ctx);
|
||||
auto typed_func = reinterpret_cast<void(*)(Context*, Ts...)>(cb->data);
|
||||
auto typed_ctx = static_cast<Context*>(*reinterpret_cast<void**>(
|
||||
static_cast<char*>(cb) + offsetof(Callback, data)
|
||||
));
|
||||
typed_func(typed_ctx, args...);
|
||||
},
|
||||
reinterpret_cast<void*>(func),
|
||||
false
|
||||
});
|
||||
}
|
||||
|
||||
// Legacy: std::function support (for gradual migration)
|
||||
void add(std::function<void(Ts...)>&& func) {
|
||||
auto* stored = new std::function<void(Ts...)>(std::move(func));
|
||||
callbacks_.push_back({
|
||||
[](void* ctx, Ts... args) {
|
||||
(*static_cast<std::function<void(Ts...)>*>(ctx))(args...);
|
||||
},
|
||||
stored,
|
||||
true
|
||||
});
|
||||
}
|
||||
|
||||
~CallbackManager() {
|
||||
for (auto& cb : callbacks_) {
|
||||
if (cb.is_std_function) {
|
||||
delete static_cast<std::function<void(Ts...)>*>(cb.data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto& cb : callbacks_) {
|
||||
cb.invoker(&cb, args...);
|
||||
}
|
||||
}
|
||||
|
||||
size_t size() const { return callbacks_.size(); }
|
||||
};
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```cpp
|
||||
// NEW (optimized):
|
||||
parent_->add_scanner_state_callback(scanner_state_callback, this);
|
||||
|
||||
// OLD (still works - gradual migration):
|
||||
parent_->add_scanner_state_callback([this](ScannerState state) {
|
||||
// ... lambda still works
|
||||
});
|
||||
```
|
||||
|
||||
**Savings:**
|
||||
- **Per optimized callback:** 20 bytes (32 → 12)
|
||||
- **Per legacy callback:** 0 bytes (still uses std::function)
|
||||
- **Allows gradual migration**
|
||||
|
||||
**Pros:**
|
||||
- ✅ Backward compatible
|
||||
- ✅ Gradual migration path
|
||||
- ✅ Mix optimized and legacy in same codebase
|
||||
- ✅ No breaking changes
|
||||
|
||||
**Cons:**
|
||||
- ❌ More complex implementation
|
||||
- ❌ Need to track which callbacks need cleanup
|
||||
- ❌ Extra bool field (padding makes it 12 bytes instead of 8)
|
||||
- ❌ std::function still compiled in
|
||||
|
||||
---
|
||||
|
||||
### Option 4: FixedVector (Keep std::function, Optimize Vector)
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```cpp
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
public:
|
||||
void add(std::function<void(Ts...)> &&callback) {
|
||||
if (this->callbacks_.empty()) {
|
||||
// Most CallbackManagers have 1-5 callbacks
|
||||
this->callbacks_.init(5);
|
||||
}
|
||||
this->callbacks_.push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void call(Ts... args) {
|
||||
for (auto &cb : this->callbacks_)
|
||||
cb(args...);
|
||||
}
|
||||
|
||||
size_t size() const { return this->callbacks_.size(); }
|
||||
|
||||
protected:
|
||||
FixedVector<std::function<void(Ts...)>> callbacks_; // Changed from std::vector
|
||||
};
|
||||
```
|
||||
|
||||
**Savings:**
|
||||
- **Per callback:** 0 bytes (still 32 bytes)
|
||||
- **Per instance:** ~132 bytes (eliminates `_M_realloc_append`)
|
||||
- **Flash saved:** ~5-10 KB (one less vector template instantiation per type)
|
||||
- **Total:** ~132 bytes × ~20 unique callback types = ~2.6 KB
|
||||
|
||||
**Pros:**
|
||||
- ✅ No API changes
|
||||
- ✅ Drop-in replacement
|
||||
- ✅ Eliminates vector reallocation machinery
|
||||
- ✅ Zero migration cost
|
||||
|
||||
**Cons:**
|
||||
- ❌ No per-callback savings
|
||||
- ❌ std::function still 32 bytes each
|
||||
- ❌ Must guess max size at runtime
|
||||
- ❌ Can still overflow if guess is wrong
|
||||
|
||||
---
|
||||
|
||||
### Option 5: Template Parameter for Storage (Advanced)
|
||||
|
||||
**Implementation:**
|
||||
|
||||
```cpp
|
||||
enum class CallbackStorage {
|
||||
FUNCTION, // Use std::function (default, most flexible)
|
||||
FUNCTION_PTR // Use function pointer + context (optimal)
|
||||
};
|
||||
|
||||
template<typename... Ts, CallbackStorage Storage = CallbackStorage::FUNCTION>
|
||||
class CallbackManager<void(Ts...)> {
|
||||
// Specialize implementation based on Storage parameter
|
||||
};
|
||||
|
||||
// Default: std::function (backward compatible)
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...), CallbackStorage::FUNCTION> {
|
||||
protected:
|
||||
std::vector<std::function<void(Ts...)>> callbacks_;
|
||||
// ... current implementation
|
||||
};
|
||||
|
||||
// Optimized: function pointer + context
|
||||
template<typename... Ts>
|
||||
class CallbackManager<void(Ts...), CallbackStorage::FUNCTION_PTR> {
|
||||
private:
|
||||
struct Callback {
|
||||
void (*func)(void*, Ts...);
|
||||
void* context;
|
||||
};
|
||||
std::vector<Callback> callbacks_;
|
||||
// ... Option 1 implementation
|
||||
};
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
|
||||
```cpp
|
||||
// Old components (no changes):
|
||||
CallbackManager<void(float)> callback_; // Uses std::function by default
|
||||
|
||||
// Optimized components:
|
||||
CallbackManager<void(ScannerState), CallbackStorage::FUNCTION_PTR> scanner_state_callbacks_;
|
||||
```
|
||||
|
||||
**Savings:**
|
||||
- **Opt-in per component**
|
||||
- **Same as Option 1 for optimized components**
|
||||
|
||||
**Pros:**
|
||||
- ✅ Gradual migration
|
||||
- ✅ No breaking changes
|
||||
- ✅ Explicit opt-in per component
|
||||
- ✅ Clear which components are optimized
|
||||
|
||||
**Cons:**
|
||||
- ❌ Complex template metaprogramming
|
||||
- ❌ Two implementations to maintain
|
||||
- ❌ Template parameter pollution
|
||||
- ❌ Harder to understand codebase
|
||||
|
||||
---
|
||||
|
||||
## Comparison Matrix
|
||||
|
||||
| Option | Per-Callback Savings | Flash Savings | API Changes | Complexity | Migration Cost |
|
||||
|--------|---------------------|---------------|-------------|------------|----------------|
|
||||
| **1. Function Ptr + Context** | **24 bytes** (75%) | **~10 KB** | Yes | Low | High (67 files) |
|
||||
| **2. Member Function Ptrs** | **24 bytes** (75%) | **~10 KB** | Yes | Medium | High + class changes |
|
||||
| **3. Hybrid** | **20 bytes** (opt-in) | **~8 KB** | No | High | Low (gradual) |
|
||||
| **4. FixedVector** | **0 bytes** | **~3 KB** | No | Low | None |
|
||||
| **5. Template Parameter** | **24 bytes** (opt-in) | **~10 KB** | Optional | High | Medium |
|
||||
|
||||
---
|
||||
|
||||
## Migration Effort Estimate
|
||||
|
||||
### Option 1 (Function Pointer + Context)
|
||||
|
||||
**Files to change:** ~67 files with CallbackManager usage
|
||||
|
||||
**Per-file changes:**
|
||||
1. Convert lambda to static function (5 min)
|
||||
2. Update registration call (1 min)
|
||||
3. Test (5 min)
|
||||
|
||||
**Estimate:** ~11 min × 67 files = **~12 hours** (assuming some files have multiple callbacks)
|
||||
|
||||
**High-impact components to prioritize:**
|
||||
- `sensor.h` / `sensor.cpp` - many sensor callbacks
|
||||
- `esp32_ble_tracker.h` - BLE callbacks
|
||||
- `climate.h` - climate callbacks
|
||||
- `binary_sensor.h` - binary sensor callbacks
|
||||
|
||||
### Option 4 (FixedVector)
|
||||
|
||||
**Files to change:** 1 file (`esphome/core/helpers.h`)
|
||||
|
||||
**Changes:**
|
||||
1. Change `std::vector` to `FixedVector` in CallbackManager
|
||||
2. Initialize with reasonable default size (e.g., 5)
|
||||
3. Test across codebase
|
||||
|
||||
**Estimate:** **~1 hour**
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate Action: Option 4 (FixedVector)
|
||||
|
||||
**Why:**
|
||||
- Zero migration cost
|
||||
- Immediate ~3 KB flash savings
|
||||
- No API changes
|
||||
- Low risk
|
||||
|
||||
**Implementation:**
|
||||
```cpp
|
||||
template<typename... Ts> class CallbackManager<void(Ts...)> {
|
||||
public:
|
||||
void add(std::function<void(Ts...)> &&callback) {
|
||||
if (this->callbacks_.empty()) {
|
||||
this->callbacks_.init(8); // Most have < 8 callbacks
|
||||
}
|
||||
this->callbacks_.push_back(std::move(callback));
|
||||
}
|
||||
// ... rest unchanged
|
||||
protected:
|
||||
FixedVector<std::function<void(Ts...)>> callbacks_;
|
||||
};
|
||||
```
|
||||
|
||||
### Long-term: Option 1 (Function Pointer + Context)
|
||||
|
||||
**Why:**
|
||||
- Maximum savings (2.4-3.6 KB RAM + 10 KB flash)
|
||||
- Clean, simple implementation
|
||||
- Type-safe
|
||||
- Well-tested pattern
|
||||
|
||||
**Migration Strategy:**
|
||||
1. Implement new `CallbackManager` in `helpers.h`
|
||||
2. Migrate high-impact components first:
|
||||
- Core components (sensor, binary_sensor, climate)
|
||||
- BLE components (esp32_ble_tracker, bluetooth_proxy)
|
||||
- Network components (api, mqtt)
|
||||
3. Create helper macros to reduce boilerplate
|
||||
4. Migrate remaining components over 2-3 releases
|
||||
|
||||
**Helper Macro Example:**
|
||||
```cpp
|
||||
// Define a callback wrapper
|
||||
#define CALLBACK_WRAPPER(Class, Method, ...) \
|
||||
static void Method##_callback(Class* self, ##__VA_ARGS__) { \
|
||||
self->Method(__VA_ARGS__); \
|
||||
}
|
||||
|
||||
// In class:
|
||||
class BluetoothProxy {
|
||||
CALLBACK_WRAPPER(BluetoothProxy, on_scanner_state, ScannerState state)
|
||||
|
||||
void on_scanner_state(ScannerState state) {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
void setup() {
|
||||
parent_->add_scanner_state_callback(on_scanner_state_callback, this);
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Plan
|
||||
|
||||
### Phase 1: Unit Tests
|
||||
- Test CallbackManager with various signatures
|
||||
- Test multiple callbacks (1, 5, 10, 50)
|
||||
- Test callback removal/cancellation
|
||||
- Test edge cases (empty, nullptr, etc.)
|
||||
|
||||
### Phase 2: Integration Tests
|
||||
- Create test YAML with heavily-used callbacks
|
||||
- Run on ESP32, ESP8266, RP2040
|
||||
- Measure before/after memory usage
|
||||
- Verify no functional regressions
|
||||
|
||||
### Phase 3: Component Tests
|
||||
- Test high-impact components:
|
||||
- sensor with multiple state callbacks
|
||||
- esp32_improv with all automation triggers
|
||||
- climate with state/control callbacks
|
||||
- Measure memory with `esphome analyze-memory`
|
||||
|
||||
---
|
||||
|
||||
## Risk Analysis
|
||||
|
||||
### Option 1 Risks
|
||||
|
||||
**Risk: Breaking change across 67 files**
|
||||
- **Mitigation:** Gradual rollout over 2-3 releases
|
||||
- **Mitigation:** Extensive testing on real hardware
|
||||
|
||||
**Risk: Static function verbosity**
|
||||
- **Mitigation:** Helper macros (see above)
|
||||
- **Mitigation:** Code generation from Python
|
||||
|
||||
**Risk: Missing captures**
|
||||
- **Mitigation:** Static analysis to find lambda captures
|
||||
- **Mitigation:** Compile-time errors for incorrect usage
|
||||
|
||||
### Option 4 Risks
|
||||
|
||||
**Risk: Buffer overflow if size guess is wrong**
|
||||
- **Mitigation:** Choose conservative default (8)
|
||||
- **Mitigation:** Add runtime warning on resize
|
||||
- **Mitigation:** Monitor in CI/testing
|
||||
|
||||
**Risk: Still uses std::function (32 bytes each)**
|
||||
- **Mitigation:** Follow up with Option 1 migration
|
||||
- **Mitigation:** This is a stepping stone, not final solution
|
||||
|
||||
---
|
||||
|
||||
## Implementation Timeline
|
||||
|
||||
### Week 1: Option 4 (Quick Win)
|
||||
- Implement FixedVector in CallbackManager
|
||||
- Test across codebase
|
||||
- Create PR with memory analysis
|
||||
- **Expected savings:** ~3 KB flash
|
||||
|
||||
### Month 1-2: Option 1 (Core Components)
|
||||
- Implement function pointer CallbackManager
|
||||
- Migrate sensor, binary_sensor, climate
|
||||
- Create helper macros
|
||||
- **Expected savings:** ~1 KB RAM + 5 KB flash
|
||||
|
||||
### Month 3-4: Option 1 (Remaining Components)
|
||||
- Migrate BLE components
|
||||
- Migrate network components (api, mqtt)
|
||||
- Migrate automation components
|
||||
- **Expected savings:** ~2 KB RAM + 10 KB flash total
|
||||
|
||||
### Month 5: Cleanup
|
||||
- Remove std::function CallbackManager
|
||||
- Update documentation
|
||||
- Blog post about optimization
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Recommended Approach:**
|
||||
|
||||
1. **Immediate (Week 1):** Implement Option 4 (FixedVector)
|
||||
- Low risk, zero migration cost
|
||||
- ~3 KB flash savings
|
||||
- Sets foundation for Option 1
|
||||
|
||||
2. **Short-term (Month 1-2):** Begin Option 1 migration
|
||||
- Start with high-impact components
|
||||
- ~1-2 KB RAM + 5 KB flash savings
|
||||
- Validate approach
|
||||
|
||||
3. **Long-term (Month 3-6):** Complete Option 1 migration
|
||||
- Migrate all components
|
||||
- ~3-4 KB total RAM + 10 KB flash savings
|
||||
- Remove std::function variant
|
||||
|
||||
**Total Expected Savings:**
|
||||
- **RAM:** 2.4 - 3.6 KB (75% reduction per callback)
|
||||
- **Flash:** 8 - 13 KB (vector overhead + template instantiations)
|
||||
- **Performance:** Slightly faster (no std::function indirection)
|
||||
|
||||
This is significant for ESP8266 (80 KB RAM, 1 MB flash) and beneficial for all platforms.
|
||||
@@ -1,75 +0,0 @@
|
||||
# Callback Optimization Analysis - Why It Failed
|
||||
|
||||
## Goal
|
||||
Convert stateful lambdas in CallbackManager to stateless function pointers to reduce flash usage.
|
||||
|
||||
## Approach Tested
|
||||
|
||||
### Attempt 1: Discriminated Union in CallbackManager
|
||||
**Changed:** `CallbackManager` to use union with discriminator (like `TemplatableValue`)
|
||||
- Stateless lambdas → function pointer (8 bytes)
|
||||
- Stateful lambdas → heap-allocated `std::function*` (8 bytes struct + 32 bytes heap)
|
||||
|
||||
**Result:**
|
||||
- ❌ **+300 bytes heap usage** (37-38 callbacks × 8 bytes overhead)
|
||||
- ✅ Flash savings potential: ~200-400 bytes per stateless callback
|
||||
- **Verdict:** RAM is more precious than flash on ESP8266 - rejected
|
||||
|
||||
### Attempt 2: Convert Individual Callbacks to Stateless
|
||||
**Changed:** API logger callback from `[this]` lambda to static member function
|
||||
- Used existing `global_api_server` pointer
|
||||
- Made callback stateless (convertible to function pointer)
|
||||
|
||||
**Result:**
|
||||
```
|
||||
Removed:
|
||||
- Lambda _M_invoke: 103 bytes
|
||||
- Lambda _M_manager: 20 bytes
|
||||
|
||||
Added:
|
||||
- log_callback function: 104 bytes
|
||||
- Function pointer _M_invoke: 20 bytes
|
||||
- Function pointer _M_manager: 20 bytes
|
||||
- Larger setup(): 7 bytes
|
||||
|
||||
Net: +32 bytes flash ❌
|
||||
```
|
||||
|
||||
**Why it failed:**
|
||||
Even though the callback became stateless, `CallbackManager` still uses `std::vector<std::function<void(Ts...)>>`. The function pointer STILL gets wrapped in `std::function`, generating the same template instantiation overhead. We just moved the code from a lambda to a static function.
|
||||
|
||||
## Root Cause
|
||||
|
||||
The optimization **requires BOTH**:
|
||||
1. ✅ Stateless callback (function pointer)
|
||||
2. ❌ Modified `CallbackManager` to store function pointers directly without `std::function` wrapper
|
||||
|
||||
Without modifying `CallbackManager`, converting individual callbacks to function pointers provides **no benefit** and actually **increases** code size slightly due to the extra function definition.
|
||||
|
||||
## Conclusion
|
||||
|
||||
This optimization path is a **dead end** for ESPHome because:
|
||||
1. **Discriminated union approach**: Increases heap by 300 bytes (unacceptable for ESP8266)
|
||||
2. **Individual callback conversion**: Increases flash by 32+ bytes (no benefit without CallbackManager changes)
|
||||
|
||||
The current `std::vector<std::function<...>>` approach is already optimal for the use case where most callbacks capture state.
|
||||
|
||||
## Alternative Approaches Considered
|
||||
|
||||
1. **Create separate `StatelessCallbackManager`**: Would require changing all call sites, not worth the complexity
|
||||
2. **Template parameter to select storage type**: Same issue - requires modifying many components
|
||||
3. **Hand-pick specific callbacks**: Provides no benefit as shown in Attempt 2
|
||||
|
||||
## Recommendation
|
||||
|
||||
**Do not pursue this optimization.** The RAM/flash trade-offs are unfavorable for embedded systems where RAM is typically more constrained than flash.
|
||||
|
||||
---
|
||||
|
||||
**Test Results:**
|
||||
- Platform: ESP8266-Arduino
|
||||
- Component: API
|
||||
- Result: +32 bytes flash (0.01% increase)
|
||||
- Status: Reverted
|
||||
|
||||
🤖 Analysis by Claude Code
|
||||
@@ -1,256 +0,0 @@
|
||||
# Callback Optimization Implementation Plan
|
||||
|
||||
## Analysis Summary
|
||||
|
||||
After Controller Registry (PR #11772), callback infrastructure can be further optimized:
|
||||
|
||||
**Current overhead per entity (ESP32 32-bit):**
|
||||
- No callbacks: 16 bytes (4-byte ptr + 12-byte empty vector)
|
||||
- With callbacks: 32+ bytes (16 baseline + 16+ per callback)
|
||||
|
||||
**Opportunity:** After Controller Registry, most entities have **zero callbacks** (API/WebServer use registry instead). We can save 12 bytes per entity by lazy allocation.
|
||||
|
||||
## Entity Types by Callback Needs
|
||||
|
||||
### Entities with ONLY filtered callbacks (most)
|
||||
- Climate, Fan, Light, Cover
|
||||
- Switch, Lock, Valve
|
||||
- Number, Select, Text, Button
|
||||
- AlarmControlPanel, MediaPlayer
|
||||
- BinarySensor, Event, Update, DateTime
|
||||
|
||||
**Optimization:** Simple lazy-allocated vector
|
||||
|
||||
### Entities with raw AND filtered callbacks
|
||||
- **Sensor** - has raw callbacks for automation triggers
|
||||
- **TextSensor** - has raw callbacks for automation triggers
|
||||
|
||||
**Optimization:** Partitioned vector (filtered | raw)
|
||||
|
||||
## Proposed Implementations
|
||||
|
||||
### Option 1: Simple Lazy Vector (for entities without raw callbacks)
|
||||
|
||||
```cpp
|
||||
class Climate {
|
||||
protected:
|
||||
std::unique_ptr<std::vector<std::function<void(Climate&)>>> state_callback_;
|
||||
};
|
||||
|
||||
void Climate::add_on_state_callback(std::function<void(Climate&)> &&callback) {
|
||||
if (!this->state_callback_) {
|
||||
this->state_callback_ = std::make_unique<std::vector<std::function<void(Climate&)>>>();
|
||||
}
|
||||
this->state_callback_->push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void Climate::publish_state() {
|
||||
if (this->state_callback_) {
|
||||
for (auto &cb : *this->state_callback_) {
|
||||
cb(*this);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Memory (ESP32):**
|
||||
- No callbacks: 4 bytes (saves 12 vs current)
|
||||
- 1 callback: 36 bytes (costs 4 vs current)
|
||||
- Net: Positive for API-only devices
|
||||
|
||||
### Option 2: Partitioned Vector (for Sensor & TextSensor)
|
||||
|
||||
```cpp
|
||||
class Sensor {
|
||||
protected:
|
||||
struct Callbacks {
|
||||
std::vector<std::function<void(float)>> callbacks_;
|
||||
uint8_t filtered_count_{0}; // Partition point: [filtered | raw]
|
||||
|
||||
void add_filtered(std::function<void(float)> &&fn) {
|
||||
callbacks_.push_back(std::move(fn));
|
||||
if (filtered_count_ < callbacks_.size() - 1) {
|
||||
std::swap(callbacks_[filtered_count_], callbacks_[callbacks_.size() - 1]);
|
||||
}
|
||||
filtered_count_++;
|
||||
}
|
||||
|
||||
void add_raw(std::function<void(float)> &&fn) {
|
||||
callbacks_.push_back(std::move(fn)); // Append to raw section
|
||||
}
|
||||
|
||||
void call_filtered(float value) {
|
||||
for (size_t i = 0; i < filtered_count_; i++) {
|
||||
callbacks_[i](value);
|
||||
}
|
||||
}
|
||||
|
||||
void call_raw(float value) {
|
||||
for (size_t i = filtered_count_; i < callbacks_.size(); i++) {
|
||||
callbacks_[i](value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
std::unique_ptr<Callbacks> callbacks_;
|
||||
};
|
||||
```
|
||||
|
||||
**Why partitioned:**
|
||||
- Maintains separation of raw (pre-filter) vs filtered (post-filter) callbacks
|
||||
- O(1) insertion via swap (order doesn't matter)
|
||||
- No branching in hot path
|
||||
- Saves 12 bytes when no callbacks
|
||||
|
||||
## Memory Impact Analysis
|
||||
|
||||
### Scenario 1: API-only device (10 sensors, no MQTT, no automations)
|
||||
**Current:** 10 × 16 = 160 bytes
|
||||
**Optimized:** 10 × 4 = 40 bytes
|
||||
**Saves: 120 bytes** ✅
|
||||
|
||||
### Scenario 2: MQTT-enabled device (10 sensors with MQTT)
|
||||
**Current:** 10 × 32 = 320 bytes
|
||||
**Optimized:** 10 × 36 = 360 bytes
|
||||
**Costs: 40 bytes** ⚠️
|
||||
|
||||
### Scenario 3: Mixed device (5 API-only + 5 MQTT)
|
||||
**Current:** (5 × 16) + (5 × 32) = 240 bytes
|
||||
**Optimized:** (5 × 4) + (5 × 36) = 200 bytes
|
||||
**Saves: 40 bytes** ✅
|
||||
|
||||
### Scenario 4: Sensor with automation (1 raw + 1 filtered)
|
||||
**Current:** 16 + 12 + 16 + 16 = 60 bytes
|
||||
**Optimized:** 4 + 16 + 32 = 52 bytes
|
||||
**Saves: 8 bytes** ✅
|
||||
|
||||
## Implementation Strategy
|
||||
|
||||
### Phase 1: Simple Entities (high impact, low complexity)
|
||||
1. **Climate** (common, no raw callbacks)
|
||||
2. **Fan** (common, no raw callbacks)
|
||||
3. **Cover** (common, no raw callbacks)
|
||||
4. **Switch** (very common, no raw callbacks)
|
||||
5. **Lock** (no raw callbacks)
|
||||
|
||||
**Change:** Replace `CallbackManager<void(...)> callback_` with `std::unique_ptr<std::vector<std::function<...>>>`
|
||||
|
||||
### Phase 2: Sensor & TextSensor (more complex)
|
||||
1. **Sensor** (most common entity, has raw callbacks)
|
||||
2. **TextSensor** (common, has raw callbacks)
|
||||
|
||||
**Change:** Implement partitioned vector approach
|
||||
|
||||
### Phase 3: Remaining Entities
|
||||
- BinarySensor, Number, Select, Text
|
||||
- Light, Valve, AlarmControlPanel
|
||||
- MediaPlayer, Button, Event, Update, DateTime
|
||||
|
||||
**Change:** Simple lazy vector
|
||||
|
||||
## Code Template for Simple Entities
|
||||
|
||||
```cpp
|
||||
// Header (.h)
|
||||
class EntityType {
|
||||
public:
|
||||
void add_on_state_callback(std::function<void(Args...)> &&callback);
|
||||
|
||||
protected:
|
||||
std::unique_ptr<std::vector<std::function<void(Args...)>>> state_callback_;
|
||||
};
|
||||
|
||||
// Implementation (.cpp)
|
||||
void EntityType::add_on_state_callback(std::function<void(Args...)> &&callback) {
|
||||
if (!this->state_callback_) {
|
||||
this->state_callback_ = std::make_unique<std::vector<std::function<void(Args...)>>>();
|
||||
}
|
||||
this->state_callback_->push_back(std::move(callback));
|
||||
}
|
||||
|
||||
void EntityType::publish_state(...) {
|
||||
// ... state update logic ...
|
||||
|
||||
if (this->state_callback_) {
|
||||
for (auto &cb : *this->state_callback_) {
|
||||
cb(...);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef USE_CONTROLLER_REGISTRY
|
||||
ControllerRegistry::notify_entity_update(this);
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit tests:** Verify callback ordering/execution unchanged
|
||||
2. **Integration tests:** Test with MQTT, automations, copy components
|
||||
3. **Memory benchmarks:** Measure actual flash/RAM impact
|
||||
4. **Compatibility:** Ensure no API breakage
|
||||
|
||||
## Expected Results
|
||||
|
||||
**For typical ESPHome devices after Controller Registry:**
|
||||
- Most entities: API/WebServer only (no callbacks)
|
||||
- Some entities: MQTT (1 callback)
|
||||
- Few entities: Automations (1-2 callbacks)
|
||||
|
||||
**Memory savings:**
|
||||
- Device with 20 entities, 5 with MQTT: ~180 bytes saved
|
||||
- Device with 50 entities, 10 with MQTT: ~480 bytes saved
|
||||
|
||||
**Trade-off:**
|
||||
- Entities without callbacks: Save 12 bytes ✅
|
||||
- Entities with callbacks: Cost 4 bytes ⚠️
|
||||
- Net benefit: Positive for most devices
|
||||
|
||||
## Risks & Mitigation
|
||||
|
||||
**Risk 1:** Increased complexity
|
||||
- **Mitigation:** Start with simple entities first, template for reuse
|
||||
|
||||
**Risk 2:** Performance regression
|
||||
- **Mitigation:** Minimal - just nullptr check (likely free with branch prediction)
|
||||
|
||||
**Risk 3:** Edge cases with callback order
|
||||
- **Mitigation:** Order already undefined within same callback type
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. Should we template the Callbacks struct for reuse across entity types?
|
||||
2. Should Phase 1 include a memory benchmark before expanding?
|
||||
3. Should we make this configurable (compile-time flag)?
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Phase 1 (Simple Entities)
|
||||
- `esphome/components/climate/climate.h`
|
||||
- `esphome/components/climate/climate.cpp`
|
||||
- `esphome/components/fan/fan.h`
|
||||
- `esphome/components/fan/fan.cpp`
|
||||
- `esphome/components/cover/cover.h`
|
||||
- `esphome/components/cover/cover.cpp`
|
||||
- (etc. for switch, lock)
|
||||
|
||||
### Phase 2 (Partitioned)
|
||||
- `esphome/components/sensor/sensor.h`
|
||||
- `esphome/components/sensor/sensor.cpp`
|
||||
- `esphome/components/text_sensor/text_sensor.h`
|
||||
- `esphome/components/text_sensor/text_sensor.cpp`
|
||||
|
||||
### Phase 3 (Remaining)
|
||||
- All other entity types
|
||||
|
||||
## Conclusion
|
||||
|
||||
**Recommendation: Implement in phases**
|
||||
|
||||
1. Start with Climate (common entity, simple change)
|
||||
2. Measure impact on real device
|
||||
3. If positive, proceed with other simple entities
|
||||
4. Implement partitioned approach for Sensor/TextSensor
|
||||
5. Complete remaining entity types
|
||||
|
||||
Expected net savings: **50-500 bytes per typical device**, depending on entity count and MQTT usage.
|
||||
@@ -1,118 +0,0 @@
|
||||
# add_on_state_callback Usage Analysis
|
||||
|
||||
## Summary
|
||||
|
||||
After the Controller Registry migration (PR #11772), `add_on_state_callback` is still widely used in the codebase, but for **legitimate reasons** - components that genuinely need per-entity state tracking.
|
||||
|
||||
## Usage Breakdown
|
||||
|
||||
### 1. **MQTT Components** (~17 uses)
|
||||
**Purpose:** Per-entity MQTT configuration requires callbacks
|
||||
- Each MQTT component instance needs to publish to custom topics with custom QoS/retain settings
|
||||
- Cannot use Controller pattern due to per-entity configuration overhead
|
||||
- Examples: `mqtt_sensor.cpp`, `mqtt_climate.cpp`, `mqtt_number.cpp`, etc.
|
||||
|
||||
```cpp
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
this->publish_state(state);
|
||||
});
|
||||
```
|
||||
|
||||
### 2. **Copy Components** (~10 uses)
|
||||
**Purpose:** Mirror state from one entity to another
|
||||
- Each copy instance tracks a different source entity
|
||||
- Legitimate use of callbacks for entity-to-entity synchronization
|
||||
- Examples: `copy_sensor.cpp`, `copy_fan.cpp`, `copy_select.cpp`, etc.
|
||||
|
||||
```cpp
|
||||
source_->add_on_state_callback([this](const std::string &value) {
|
||||
this->publish_state(value);
|
||||
});
|
||||
```
|
||||
|
||||
### 3. **Derivative Sensors** (~5-7 uses)
|
||||
**Purpose:** Compute derived values from source sensors
|
||||
- **integration_sensor:** Integrates sensor values over time
|
||||
- **total_daily_energy:** Tracks cumulative energy
|
||||
- **combination:** Combines multiple sensor values
|
||||
- **graph:** Samples sensor data for display
|
||||
- **duty_time:** Tracks on-time duration
|
||||
- **ntc/absolute_humidity/resistance:** Mathematical transformations
|
||||
|
||||
```cpp
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
this->process_sensor_value_(state);
|
||||
});
|
||||
```
|
||||
|
||||
### 4. **Climate/Cover with Sensors** (~10-15 uses)
|
||||
**Purpose:** External sensors providing feedback to control loops
|
||||
- **feedback_cover:** Binary sensors for open/close/obstacle detection
|
||||
- **bang_bang/pid/thermostat:** External temperature sensors for climate control
|
||||
- **climate_ir (toshiba/yashima/heatpumpir):** Temperature sensors for IR climate
|
||||
|
||||
```cpp
|
||||
this->sensor_->add_on_state_callback([this](float state) {
|
||||
this->current_temperature = state;
|
||||
// Trigger control loop update
|
||||
});
|
||||
```
|
||||
|
||||
### 5. **Entity Base Classes** (~10-15 definitions)
|
||||
**Purpose:** Provide the callback interface for all entities
|
||||
- Not actual usage, just the method definitions
|
||||
- Examples: `sensor.cpp::add_on_state_callback()`, `climate.cpp::add_on_state_callback()`, etc.
|
||||
|
||||
### 6. **Automation Trigger Classes** (~15-20 definitions)
|
||||
**Purpose:** User-defined YAML automations need callbacks
|
||||
- Files like `sensor/automation.h`, `climate/automation.h`
|
||||
- Implement triggers like `on_value:`, `on_state:`
|
||||
- Cannot be migrated - this is user-facing automation functionality
|
||||
|
||||
### 7. **Miscellaneous** (~5-10 uses)
|
||||
- **voice_assistant/micro_wake_word:** State coordination
|
||||
- **esp32_improv:** Provisioning state tracking
|
||||
- **http_request/update:** Update status monitoring
|
||||
- **switch/binary_sensor:** Cross-component dependencies
|
||||
- **OTA callbacks:** OTA state monitoring
|
||||
|
||||
## Key Insights
|
||||
|
||||
### What's NOT Using Callbacks Anymore ✅
|
||||
**API Server and WebServer** - migrated to Controller Registry
|
||||
- **Before:** Each entity had 2 callbacks (API + WebServer) = ~32 bytes overhead
|
||||
- **After:** Zero per-entity overhead = saves ~32 bytes per entity
|
||||
|
||||
### What SHOULD Keep Using Callbacks ✅
|
||||
All the above categories have legitimate reasons:
|
||||
|
||||
1. **Per-entity configuration:** MQTT needs custom topics/QoS per entity
|
||||
2. **Entity-to-entity relationships:** Copy components, derivative sensors
|
||||
3. **Control loop feedback:** Climate/cover with external sensors
|
||||
4. **User-defined automations:** YAML triggers configured by users
|
||||
5. **Component dependencies:** Components that genuinely depend on other entities
|
||||
|
||||
## Memory Impact
|
||||
|
||||
**Per Sensor (ESP32):**
|
||||
- Empty callback infrastructure: **~16 bytes** (unique_ptr + empty vector)
|
||||
- With one callback (e.g., MQTT): **~32 bytes** (16 + std::function)
|
||||
- With multiple callbacks: **~32 + 16n bytes** (where n = additional callbacks)
|
||||
|
||||
**Typical scenarios:**
|
||||
- Sensor with **only API/WebServer:** ~16 bytes (no callbacks registered)
|
||||
- Sensor with **MQTT:** ~32 bytes (one callback)
|
||||
- Sensor with **MQTT + automation:** ~48 bytes (two callbacks)
|
||||
- Sensor with **copy + total_daily_energy + graph:** ~64 bytes (three callbacks)
|
||||
|
||||
## Conclusion
|
||||
|
||||
The callback system is still heavily used (~103 occurrences) but for **appropriate reasons**:
|
||||
- Components with per-entity state/configuration (MQTT, Copy)
|
||||
- Sensor processing chains (derivatives, transformations)
|
||||
- Control loops with external feedback (climate, covers)
|
||||
- User-defined automations (cannot be removed)
|
||||
|
||||
The Controller Registry successfully eliminated wasteful callbacks for **stateless global handlers** (API/WebServer), saving ~32 bytes per entity for those use cases.
|
||||
|
||||
**No further callback elimination opportunities** exist without fundamentally changing ESPHome's architecture or breaking user-facing features.
|
||||
@@ -944,6 +944,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
"""
|
||||
from esphome import platformio_api
|
||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
|
||||
|
||||
# Always compile to ensure fresh data (fast if no changes - just relinks)
|
||||
exit_code = write_cpp(config)
|
||||
@@ -966,7 +967,7 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
external_components = detect_external_components(config)
|
||||
_LOGGER.debug("Detected external components: %s", external_components)
|
||||
|
||||
# Perform memory analysis
|
||||
# Perform component memory analysis
|
||||
_LOGGER.info("Analyzing memory usage...")
|
||||
analyzer = MemoryAnalyzerCLI(
|
||||
str(firmware_elf),
|
||||
@@ -976,11 +977,28 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
)
|
||||
analyzer.analyze()
|
||||
|
||||
# Generate and display report
|
||||
# Generate and display component report
|
||||
report = analyzer.generate_report()
|
||||
print()
|
||||
print(report)
|
||||
|
||||
# Perform RAM strings analysis
|
||||
_LOGGER.info("Analyzing RAM strings...")
|
||||
try:
|
||||
ram_analyzer = RamStringsAnalyzer(
|
||||
str(firmware_elf),
|
||||
objdump_path=idedata.objdump_path,
|
||||
platform=CORE.target_platform,
|
||||
)
|
||||
ram_analyzer.analyze()
|
||||
|
||||
# Generate and display RAM strings report
|
||||
ram_report = ram_analyzer.generate_report()
|
||||
print()
|
||||
print(ram_report)
|
||||
except Exception as e: # pylint: disable=broad-except
|
||||
_LOGGER.warning("RAM strings analysis failed: %s", e)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
@@ -1319,7 +1337,7 @@ def parse_args(argv):
|
||||
"clean-all", help="Clean all build and platform files."
|
||||
)
|
||||
parser_clean_all.add_argument(
|
||||
"configuration", help="Your YAML configuration directory.", nargs="*"
|
||||
"configuration", help="Your YAML file or configuration directory.", nargs="*"
|
||||
)
|
||||
|
||||
parser_dashboard = subparsers.add_parser(
|
||||
|
||||
@@ -15,6 +15,7 @@ from .const import (
|
||||
SECTION_TO_ATTR,
|
||||
SYMBOL_PATTERNS,
|
||||
)
|
||||
from .demangle import batch_demangle
|
||||
from .helpers import (
|
||||
get_component_class_patterns,
|
||||
get_esphome_components,
|
||||
@@ -27,15 +28,6 @@ if TYPE_CHECKING:
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# GCC global constructor/destructor prefix annotations
|
||||
_GCC_PREFIX_ANNOTATIONS = {
|
||||
"_GLOBAL__sub_I_": "global constructor for",
|
||||
"_GLOBAL__sub_D_": "global destructor for",
|
||||
}
|
||||
|
||||
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
|
||||
_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
|
||||
|
||||
# C++ runtime patterns for categorization
|
||||
_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"])
|
||||
|
||||
@@ -312,168 +304,9 @@ class MemoryAnalyzer:
|
||||
if not symbols:
|
||||
return
|
||||
|
||||
# Try to find the appropriate c++filt for the platform
|
||||
cppfilt_cmd = "c++filt"
|
||||
|
||||
_LOGGER.info("Demangling %d symbols", len(symbols))
|
||||
_LOGGER.debug("objdump_path = %s", self.objdump_path)
|
||||
|
||||
# Check if we have a toolchain-specific c++filt
|
||||
if self.objdump_path and self.objdump_path != "objdump":
|
||||
# Replace objdump with c++filt in the path
|
||||
potential_cppfilt = self.objdump_path.replace("objdump", "c++filt")
|
||||
_LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt)
|
||||
if Path(potential_cppfilt).exists():
|
||||
cppfilt_cmd = potential_cppfilt
|
||||
_LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"✗ Toolchain c++filt not found at %s, using system c++filt",
|
||||
potential_cppfilt,
|
||||
)
|
||||
else:
|
||||
_LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path)
|
||||
|
||||
# Strip GCC optimization suffixes and prefixes before demangling
|
||||
# Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt
|
||||
# Prefixes like _GLOBAL__sub_I_ need to be removed and tracked
|
||||
symbols_stripped: list[str] = []
|
||||
symbols_prefixes: list[str] = [] # Track removed prefixes
|
||||
for symbol in symbols:
|
||||
# Remove GCC optimization markers
|
||||
stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
|
||||
|
||||
# Handle GCC global constructor/initializer prefixes
|
||||
# _GLOBAL__sub_I_<mangled> -> extract <mangled> for demangling
|
||||
prefix = ""
|
||||
for gcc_prefix in _GCC_PREFIX_ANNOTATIONS:
|
||||
if stripped.startswith(gcc_prefix):
|
||||
prefix = gcc_prefix
|
||||
stripped = stripped[len(prefix) :]
|
||||
break
|
||||
|
||||
symbols_stripped.append(stripped)
|
||||
symbols_prefixes.append(prefix)
|
||||
|
||||
try:
|
||||
# Send all symbols to c++filt at once
|
||||
result = subprocess.run(
|
||||
[cppfilt_cmd],
|
||||
input="\n".join(symbols_stripped),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
|
||||
# On error, cache originals
|
||||
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
|
||||
for symbol in symbols:
|
||||
self._demangle_cache[symbol] = symbol
|
||||
return
|
||||
|
||||
if result.returncode != 0:
|
||||
_LOGGER.warning(
|
||||
"c++filt exited with code %d: %s",
|
||||
result.returncode,
|
||||
result.stderr[:200] if result.stderr else "(no error output)",
|
||||
)
|
||||
# Cache originals on failure
|
||||
for symbol in symbols:
|
||||
self._demangle_cache[symbol] = symbol
|
||||
return
|
||||
|
||||
# Process demangled output
|
||||
self._process_demangled_output(
|
||||
symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd
|
||||
)
|
||||
|
||||
def _process_demangled_output(
|
||||
self,
|
||||
symbols: list[str],
|
||||
symbols_stripped: list[str],
|
||||
symbols_prefixes: list[str],
|
||||
demangled_output: str,
|
||||
cppfilt_cmd: str,
|
||||
) -> None:
|
||||
"""Process demangled symbol output and populate cache.
|
||||
|
||||
Args:
|
||||
symbols: Original symbol names
|
||||
symbols_stripped: Stripped symbol names sent to c++filt
|
||||
symbols_prefixes: Removed prefixes to restore
|
||||
demangled_output: Output from c++filt
|
||||
cppfilt_cmd: Path to c++filt command (for logging)
|
||||
"""
|
||||
demangled_lines = demangled_output.strip().split("\n")
|
||||
failed_count = 0
|
||||
|
||||
for original, stripped, prefix, demangled in zip(
|
||||
symbols, symbols_stripped, symbols_prefixes, demangled_lines
|
||||
):
|
||||
# Add back any prefix that was removed
|
||||
demangled = self._restore_symbol_prefix(prefix, stripped, demangled)
|
||||
|
||||
# If we stripped a suffix, add it back to the demangled name for clarity
|
||||
if original != stripped and not prefix:
|
||||
demangled = self._restore_symbol_suffix(original, demangled)
|
||||
|
||||
self._demangle_cache[original] = demangled
|
||||
|
||||
# Log symbols that failed to demangle (stayed the same as stripped version)
|
||||
if stripped == demangled and stripped.startswith("_Z"):
|
||||
failed_count += 1
|
||||
if failed_count <= 5: # Only log first 5 failures
|
||||
_LOGGER.warning("Failed to demangle: %s", original)
|
||||
|
||||
if failed_count == 0:
|
||||
_LOGGER.info("Successfully demangled all %d symbols", len(symbols))
|
||||
return
|
||||
|
||||
_LOGGER.warning(
|
||||
"Failed to demangle %d/%d symbols using %s",
|
||||
failed_count,
|
||||
len(symbols),
|
||||
cppfilt_cmd,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
|
||||
"""Restore prefix that was removed before demangling.
|
||||
|
||||
Args:
|
||||
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
|
||||
stripped: Stripped symbol name
|
||||
demangled: Demangled symbol name
|
||||
|
||||
Returns:
|
||||
Demangled name with prefix restored/annotated
|
||||
"""
|
||||
if not prefix:
|
||||
return demangled
|
||||
|
||||
# Successfully demangled - add descriptive prefix
|
||||
if demangled != stripped and (
|
||||
annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix)
|
||||
):
|
||||
return f"[{annotation}: {demangled}]"
|
||||
|
||||
# Failed to demangle - restore original prefix
|
||||
return prefix + demangled
|
||||
|
||||
@staticmethod
|
||||
def _restore_symbol_suffix(original: str, demangled: str) -> str:
|
||||
"""Restore GCC optimization suffix that was removed before demangling.
|
||||
|
||||
Args:
|
||||
original: Original symbol name with suffix
|
||||
demangled: Demangled symbol name without suffix
|
||||
|
||||
Returns:
|
||||
Demangled name with suffix annotation
|
||||
"""
|
||||
if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
|
||||
return f"{demangled} [{suffix_match.group(1)}]"
|
||||
return demangled
|
||||
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path)
|
||||
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
|
||||
|
||||
def _demangle_symbol(self, symbol: str) -> str:
|
||||
"""Get demangled C++ symbol name from cache."""
|
||||
|
||||
@@ -15,6 +15,11 @@ from . import (
|
||||
class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
"""Memory analyzer with CLI-specific report generation."""
|
||||
|
||||
# Symbol size threshold for detailed analysis
|
||||
SYMBOL_SIZE_THRESHOLD: int = (
|
||||
100 # Show symbols larger than this in detailed analysis
|
||||
)
|
||||
|
||||
# Column width constants
|
||||
COL_COMPONENT: int = 29
|
||||
COL_FLASH_TEXT: int = 14
|
||||
@@ -191,14 +196,21 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%"
|
||||
)
|
||||
|
||||
# Top 15 largest core symbols
|
||||
# All core symbols above threshold
|
||||
lines.append("")
|
||||
lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:")
|
||||
sorted_core_symbols = sorted(
|
||||
self._esphome_core_symbols, key=lambda x: x[2], reverse=True
|
||||
)
|
||||
large_core_symbols = [
|
||||
(symbol, demangled, size)
|
||||
for symbol, demangled, size in sorted_core_symbols
|
||||
if size > self.SYMBOL_SIZE_THRESHOLD
|
||||
]
|
||||
|
||||
for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]):
|
||||
lines.append(
|
||||
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)")
|
||||
|
||||
lines.append("=" * self.TABLE_WIDTH)
|
||||
@@ -268,13 +280,15 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(f"Total size: {comp_mem.flash_total:,} B")
|
||||
lines.append("")
|
||||
|
||||
# Show all symbols > 100 bytes for better visibility
|
||||
# Show all symbols above threshold for better visibility
|
||||
large_symbols = [
|
||||
(sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
|
||||
(sym, dem, size)
|
||||
for sym, dem, size in sorted_symbols
|
||||
if size > self.SYMBOL_SIZE_THRESHOLD
|
||||
]
|
||||
|
||||
lines.append(
|
||||
f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):"
|
||||
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)")
|
||||
|
||||
182
esphome/analyze_memory/demangle.py
Normal file
182
esphome/analyze_memory/demangle.py
Normal file
@@ -0,0 +1,182 @@
|
||||
"""Symbol demangling utilities for memory analysis.
|
||||
|
||||
This module provides functions for demangling C++ symbol names using c++filt.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from .toolchain import find_tool
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# GCC global constructor/destructor prefix annotations
|
||||
GCC_PREFIX_ANNOTATIONS = {
|
||||
"_GLOBAL__sub_I_": "global constructor for",
|
||||
"_GLOBAL__sub_D_": "global destructor for",
|
||||
}
|
||||
|
||||
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
|
||||
GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
|
||||
|
||||
|
||||
def _strip_gcc_annotations(symbol: str) -> tuple[str, str]:
|
||||
"""Strip GCC optimization suffixes and prefixes from a symbol.
|
||||
|
||||
Args:
|
||||
symbol: The mangled symbol name
|
||||
|
||||
Returns:
|
||||
Tuple of (stripped_symbol, removed_prefix)
|
||||
"""
|
||||
# Remove GCC optimization markers
|
||||
stripped = GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
|
||||
|
||||
# Handle GCC global constructor/initializer prefixes
|
||||
prefix = ""
|
||||
for gcc_prefix in GCC_PREFIX_ANNOTATIONS:
|
||||
if stripped.startswith(gcc_prefix):
|
||||
prefix = gcc_prefix
|
||||
stripped = stripped[len(prefix) :]
|
||||
break
|
||||
|
||||
return stripped, prefix
|
||||
|
||||
|
||||
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
|
||||
"""Restore prefix that was removed before demangling.
|
||||
|
||||
Args:
|
||||
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
|
||||
stripped: Stripped symbol name
|
||||
demangled: Demangled symbol name
|
||||
|
||||
Returns:
|
||||
Demangled name with prefix restored/annotated
|
||||
"""
|
||||
if not prefix:
|
||||
return demangled
|
||||
|
||||
# Successfully demangled - add descriptive prefix
|
||||
if demangled != stripped and (annotation := GCC_PREFIX_ANNOTATIONS.get(prefix)):
|
||||
return f"[{annotation}: {demangled}]"
|
||||
|
||||
# Failed to demangle - restore original prefix
|
||||
return prefix + demangled
|
||||
|
||||
|
||||
def _restore_symbol_suffix(original: str, demangled: str) -> str:
|
||||
"""Restore GCC optimization suffix that was removed before demangling.
|
||||
|
||||
Args:
|
||||
original: Original symbol name with suffix
|
||||
demangled: Demangled symbol name without suffix
|
||||
|
||||
Returns:
|
||||
Demangled name with suffix annotation
|
||||
"""
|
||||
if suffix_match := GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
|
||||
return f"{demangled} [{suffix_match.group(1)}]"
|
||||
return demangled
|
||||
|
||||
|
||||
def batch_demangle(
|
||||
symbols: list[str],
|
||||
cppfilt_path: str | None = None,
|
||||
objdump_path: str | None = None,
|
||||
) -> dict[str, str]:
|
||||
"""Batch demangle C++ symbol names.
|
||||
|
||||
Args:
|
||||
symbols: List of symbol names to demangle
|
||||
cppfilt_path: Path to c++filt binary (auto-detected if not provided)
|
||||
objdump_path: Path to objdump binary to derive c++filt path from
|
||||
|
||||
Returns:
|
||||
Dictionary mapping original symbol names to demangled names
|
||||
"""
|
||||
cache: dict[str, str] = {}
|
||||
|
||||
if not symbols:
|
||||
return cache
|
||||
|
||||
# Find c++filt tool
|
||||
cppfilt_cmd = cppfilt_path or find_tool("c++filt", objdump_path)
|
||||
if not cppfilt_cmd:
|
||||
_LOGGER.warning("Could not find c++filt, symbols will not be demangled")
|
||||
return {s: s for s in symbols}
|
||||
|
||||
_LOGGER.debug("Demangling %d symbols using %s", len(symbols), cppfilt_cmd)
|
||||
|
||||
# Strip GCC optimization suffixes and prefixes before demangling
|
||||
symbols_stripped: list[str] = []
|
||||
symbols_prefixes: list[str] = []
|
||||
for symbol in symbols:
|
||||
stripped, prefix = _strip_gcc_annotations(symbol)
|
||||
symbols_stripped.append(stripped)
|
||||
symbols_prefixes.append(prefix)
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[cppfilt_cmd],
|
||||
input="\n".join(symbols_stripped),
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
|
||||
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
|
||||
return {s: s for s in symbols}
|
||||
|
||||
if result.returncode != 0:
|
||||
_LOGGER.warning(
|
||||
"c++filt exited with code %d: %s",
|
||||
result.returncode,
|
||||
result.stderr[:200] if result.stderr else "(no error output)",
|
||||
)
|
||||
return {s: s for s in symbols}
|
||||
|
||||
# Process demangled output
|
||||
demangled_lines = result.stdout.strip().split("\n")
|
||||
|
||||
# Check for output length mismatch
|
||||
if len(demangled_lines) != len(symbols):
|
||||
_LOGGER.warning(
|
||||
"c++filt output mismatch: expected %d lines, got %d",
|
||||
len(symbols),
|
||||
len(demangled_lines),
|
||||
)
|
||||
return {s: s for s in symbols}
|
||||
|
||||
failed_count = 0
|
||||
|
||||
for original, stripped, prefix, demangled in zip(
|
||||
symbols, symbols_stripped, symbols_prefixes, demangled_lines
|
||||
):
|
||||
# Add back any prefix that was removed
|
||||
demangled = _restore_symbol_prefix(prefix, stripped, demangled)
|
||||
|
||||
# If we stripped a suffix, add it back to the demangled name for clarity
|
||||
if original != stripped and not prefix:
|
||||
demangled = _restore_symbol_suffix(original, demangled)
|
||||
|
||||
cache[original] = demangled
|
||||
|
||||
# Count symbols that failed to demangle
|
||||
if stripped == demangled and stripped.startswith("_Z"):
|
||||
failed_count += 1
|
||||
if failed_count <= 5:
|
||||
_LOGGER.debug("Failed to demangle: %s", original)
|
||||
|
||||
if failed_count > 0:
|
||||
_LOGGER.debug(
|
||||
"Failed to demangle %d/%d symbols using %s",
|
||||
failed_count,
|
||||
len(symbols),
|
||||
cppfilt_cmd,
|
||||
)
|
||||
|
||||
return cache
|
||||
493
esphome/analyze_memory/ram_strings.py
Normal file
493
esphome/analyze_memory/ram_strings.py
Normal file
@@ -0,0 +1,493 @@
|
||||
"""Analyzer for RAM-stored strings in ESP8266/ESP32 firmware ELF files.
|
||||
|
||||
This module identifies strings that are stored in RAM sections (.data, .bss, .rodata)
|
||||
rather than in flash sections (.irom0.text, .irom.text), which is important for
|
||||
memory-constrained platforms like ESP8266.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
from .demangle import batch_demangle
|
||||
from .toolchain import find_tool
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# ESP8266: .rodata is in RAM (DRAM), not flash
|
||||
# ESP32: .rodata is in flash, mapped to data bus
|
||||
ESP8266_RAM_SECTIONS = frozenset([".data", ".rodata", ".bss"])
|
||||
ESP8266_FLASH_SECTIONS = frozenset([".irom0.text", ".irom.text", ".text"])
|
||||
|
||||
# ESP32: .rodata is memory-mapped from flash
|
||||
ESP32_RAM_SECTIONS = frozenset([".data", ".bss", ".dram0.data", ".dram0.bss"])
|
||||
ESP32_FLASH_SECTIONS = frozenset([".text", ".rodata", ".flash.text", ".flash.rodata"])
|
||||
|
||||
# nm symbol types for data symbols (D=global data, d=local data, R=rodata, B=bss)
|
||||
DATA_SYMBOL_TYPES = frozenset(["D", "d", "R", "r", "B", "b"])
|
||||
|
||||
|
||||
@dataclass
|
||||
class SectionInfo:
|
||||
"""Information about an ELF section."""
|
||||
|
||||
name: str
|
||||
address: int
|
||||
size: int
|
||||
|
||||
|
||||
@dataclass
|
||||
class RamString:
|
||||
"""A string found in RAM."""
|
||||
|
||||
section: str
|
||||
address: int
|
||||
content: str
|
||||
|
||||
@property
|
||||
def size(self) -> int:
|
||||
"""Size in bytes including null terminator."""
|
||||
return len(self.content) + 1
|
||||
|
||||
|
||||
@dataclass
|
||||
class RamSymbol:
|
||||
"""A symbol found in RAM."""
|
||||
|
||||
name: str
|
||||
sym_type: str
|
||||
address: int
|
||||
size: int
|
||||
section: str
|
||||
demangled: str = "" # Demangled name, set after batch demangling
|
||||
|
||||
|
||||
class RamStringsAnalyzer:
|
||||
"""Analyzes ELF files to find strings stored in RAM."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
elf_path: str,
|
||||
objdump_path: str | None = None,
|
||||
min_length: int = 8,
|
||||
platform: str = "esp32",
|
||||
) -> None:
|
||||
"""Initialize the RAM strings analyzer.
|
||||
|
||||
Args:
|
||||
elf_path: Path to the ELF file to analyze
|
||||
objdump_path: Path to objdump binary (used to find other tools)
|
||||
min_length: Minimum string length to report (default: 8)
|
||||
platform: Platform name ("esp8266", "esp32", etc.) for section mapping
|
||||
"""
|
||||
self.elf_path = Path(elf_path)
|
||||
if not self.elf_path.exists():
|
||||
raise FileNotFoundError(f"ELF file not found: {elf_path}")
|
||||
|
||||
self.objdump_path = objdump_path
|
||||
self.min_length = min_length
|
||||
self.platform = platform
|
||||
|
||||
# Set RAM/flash sections based on platform
|
||||
if self.platform == "esp8266":
|
||||
self.ram_sections = ESP8266_RAM_SECTIONS
|
||||
self.flash_sections = ESP8266_FLASH_SECTIONS
|
||||
else:
|
||||
# ESP32 and other platforms
|
||||
self.ram_sections = ESP32_RAM_SECTIONS
|
||||
self.flash_sections = ESP32_FLASH_SECTIONS
|
||||
|
||||
self.sections: dict[str, SectionInfo] = {}
|
||||
self.ram_strings: list[RamString] = []
|
||||
self.ram_symbols: list[RamSymbol] = []
|
||||
|
||||
def _run_command(self, cmd: list[str]) -> str:
|
||||
"""Run a command and return its output."""
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
return result.stdout
|
||||
except subprocess.CalledProcessError as e:
|
||||
_LOGGER.debug("Command failed: %s - %s", " ".join(cmd), e.stderr)
|
||||
raise
|
||||
except FileNotFoundError:
|
||||
_LOGGER.warning("Command not found: %s", cmd[0])
|
||||
raise
|
||||
|
||||
def analyze(self) -> None:
|
||||
"""Perform the full RAM analysis."""
|
||||
self._parse_sections()
|
||||
self._extract_strings()
|
||||
self._analyze_symbols()
|
||||
self._demangle_symbols()
|
||||
|
||||
def _parse_sections(self) -> None:
|
||||
"""Parse section headers from ELF file."""
|
||||
objdump = find_tool("objdump", self.objdump_path)
|
||||
if not objdump:
|
||||
_LOGGER.error("Could not find objdump command")
|
||||
return
|
||||
|
||||
try:
|
||||
output = self._run_command([objdump, "-h", str(self.elf_path)])
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return
|
||||
|
||||
# Parse section headers
|
||||
# Format: Idx Name Size VMA LMA File off Algn
|
||||
section_pattern = re.compile(
|
||||
r"^\s*\d+\s+(\S+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)"
|
||||
)
|
||||
|
||||
for line in output.split("\n"):
|
||||
if match := section_pattern.match(line):
|
||||
name = match.group(1)
|
||||
size = int(match.group(2), 16)
|
||||
vma = int(match.group(3), 16)
|
||||
self.sections[name] = SectionInfo(name, vma, size)
|
||||
|
||||
def _extract_strings(self) -> None:
|
||||
"""Extract strings from RAM sections."""
|
||||
objdump = find_tool("objdump", self.objdump_path)
|
||||
if not objdump:
|
||||
return
|
||||
|
||||
for section_name in self.ram_sections:
|
||||
if section_name not in self.sections:
|
||||
continue
|
||||
|
||||
try:
|
||||
output = self._run_command(
|
||||
[objdump, "-s", "-j", section_name, str(self.elf_path)]
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
# Section may exist but have no content (e.g., .bss)
|
||||
continue
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
|
||||
strings = self._parse_hex_dump(output, section_name)
|
||||
self.ram_strings.extend(strings)
|
||||
|
||||
def _parse_hex_dump(self, output: str, section_name: str) -> list[RamString]:
|
||||
"""Parse hex dump output to extract strings.
|
||||
|
||||
Args:
|
||||
output: Output from objdump -s
|
||||
section_name: Name of the section being parsed
|
||||
|
||||
Returns:
|
||||
List of RamString objects
|
||||
"""
|
||||
strings: list[RamString] = []
|
||||
current_string = bytearray()
|
||||
string_start_addr = 0
|
||||
|
||||
for line in output.split("\n"):
|
||||
# Lines look like: " 3ffef8a0 00000000 00000000 00000000 00000000 ................"
|
||||
match = re.match(r"^\s+([0-9a-fA-F]+)\s+((?:[0-9a-fA-F]{2,8}\s*)+)", line)
|
||||
if not match:
|
||||
continue
|
||||
|
||||
addr = int(match.group(1), 16)
|
||||
hex_data = match.group(2).strip()
|
||||
|
||||
# Convert hex to bytes
|
||||
hex_bytes = hex_data.split()
|
||||
byte_offset = 0
|
||||
for hex_chunk in hex_bytes:
|
||||
# Handle both byte-by-byte and word formats
|
||||
for i in range(0, len(hex_chunk), 2):
|
||||
byte_val = int(hex_chunk[i : i + 2], 16)
|
||||
if 0x20 <= byte_val <= 0x7E: # Printable ASCII
|
||||
if not current_string:
|
||||
string_start_addr = addr + byte_offset
|
||||
current_string.append(byte_val)
|
||||
else:
|
||||
if byte_val == 0 and len(current_string) >= self.min_length:
|
||||
# Found null terminator
|
||||
strings.append(
|
||||
RamString(
|
||||
section=section_name,
|
||||
address=string_start_addr,
|
||||
content=current_string.decode(
|
||||
"ascii", errors="ignore"
|
||||
),
|
||||
)
|
||||
)
|
||||
current_string = bytearray()
|
||||
byte_offset += 1
|
||||
|
||||
return strings
|
||||
|
||||
def _analyze_symbols(self) -> None:
|
||||
"""Analyze symbols in RAM sections."""
|
||||
nm = find_tool("nm", self.objdump_path)
|
||||
if not nm:
|
||||
return
|
||||
|
||||
try:
|
||||
output = self._run_command([nm, "-S", "--size-sort", str(self.elf_path)])
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
return
|
||||
|
||||
for line in output.split("\n"):
|
||||
parts = line.split()
|
||||
if len(parts) < 4:
|
||||
continue
|
||||
|
||||
try:
|
||||
addr = int(parts[0], 16)
|
||||
size = int(parts[1], 16) if parts[1] != "?" else 0
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
sym_type = parts[2]
|
||||
name = " ".join(parts[3:])
|
||||
|
||||
# Filter for data symbols
|
||||
if sym_type not in DATA_SYMBOL_TYPES:
|
||||
continue
|
||||
|
||||
# Check if symbol is in a RAM section
|
||||
for section_name in self.ram_sections:
|
||||
if section_name not in self.sections:
|
||||
continue
|
||||
|
||||
section = self.sections[section_name]
|
||||
if section.address <= addr < section.address + section.size:
|
||||
self.ram_symbols.append(
|
||||
RamSymbol(
|
||||
name=name,
|
||||
sym_type=sym_type,
|
||||
address=addr,
|
||||
size=size,
|
||||
section=section_name,
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
def _demangle_symbols(self) -> None:
|
||||
"""Batch demangle all RAM symbol names."""
|
||||
if not self.ram_symbols:
|
||||
return
|
||||
|
||||
# Collect all symbol names and demangle them
|
||||
symbol_names = [s.name for s in self.ram_symbols]
|
||||
demangle_cache = batch_demangle(symbol_names, objdump_path=self.objdump_path)
|
||||
|
||||
# Assign demangled names to symbols
|
||||
for symbol in self.ram_symbols:
|
||||
symbol.demangled = demangle_cache.get(symbol.name, symbol.name)
|
||||
|
||||
def _get_sections_size(self, section_names: frozenset[str]) -> int:
|
||||
"""Get total size of specified sections."""
|
||||
return sum(
|
||||
section.size
|
||||
for name, section in self.sections.items()
|
||||
if name in section_names
|
||||
)
|
||||
|
||||
def get_total_ram_usage(self) -> int:
|
||||
"""Get total RAM usage from RAM sections."""
|
||||
return self._get_sections_size(self.ram_sections)
|
||||
|
||||
def get_total_flash_usage(self) -> int:
|
||||
"""Get total flash usage from flash sections."""
|
||||
return self._get_sections_size(self.flash_sections)
|
||||
|
||||
def get_total_string_bytes(self) -> int:
|
||||
"""Get total bytes used by strings in RAM."""
|
||||
return sum(s.size for s in self.ram_strings)
|
||||
|
||||
def get_repeated_strings(self) -> list[tuple[str, int]]:
|
||||
"""Find strings that appear multiple times.
|
||||
|
||||
Returns:
|
||||
List of (string, count) tuples sorted by potential savings
|
||||
"""
|
||||
string_counts: dict[str, int] = defaultdict(int)
|
||||
for ram_string in self.ram_strings:
|
||||
string_counts[ram_string.content] += 1
|
||||
|
||||
return sorted(
|
||||
[(s, c) for s, c in string_counts.items() if c > 1],
|
||||
key=lambda x: x[1] * (len(x[0]) + 1),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def get_long_strings(self, min_len: int = 20) -> list[RamString]:
|
||||
"""Get strings longer than the specified length.
|
||||
|
||||
Args:
|
||||
min_len: Minimum string length
|
||||
|
||||
Returns:
|
||||
List of RamString objects sorted by length
|
||||
"""
|
||||
return sorted(
|
||||
[s for s in self.ram_strings if len(s.content) >= min_len],
|
||||
key=lambda x: len(x.content),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def get_largest_symbols(self, min_size: int = 100) -> list[RamSymbol]:
|
||||
"""Get RAM symbols larger than the specified size.
|
||||
|
||||
Args:
|
||||
min_size: Minimum symbol size in bytes
|
||||
|
||||
Returns:
|
||||
List of RamSymbol objects sorted by size
|
||||
"""
|
||||
return sorted(
|
||||
[s for s in self.ram_symbols if s.size >= min_size],
|
||||
key=lambda x: x.size,
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
def generate_report(self, show_all_sections: bool = False) -> str:
|
||||
"""Generate a formatted RAM strings analysis report.
|
||||
|
||||
Args:
|
||||
show_all_sections: If True, show all sections, not just RAM
|
||||
|
||||
Returns:
|
||||
Formatted report string
|
||||
"""
|
||||
lines: list[str] = []
|
||||
table_width = 80
|
||||
|
||||
lines.append("=" * table_width)
|
||||
lines.append(
|
||||
f"RAM Strings Analysis ({self.platform.upper()})".center(table_width)
|
||||
)
|
||||
lines.append("=" * table_width)
|
||||
lines.append("")
|
||||
|
||||
# Section Analysis
|
||||
lines.append("SECTION ANALYSIS")
|
||||
lines.append("-" * table_width)
|
||||
lines.append(f"{'Section':<20} {'Address':<12} {'Size':<12} {'Location'}")
|
||||
lines.append("-" * table_width)
|
||||
|
||||
total_ram_usage = 0
|
||||
total_flash_usage = 0
|
||||
|
||||
for name, section in sorted(self.sections.items(), key=lambda x: x[1].address):
|
||||
if name in self.ram_sections:
|
||||
location = "RAM"
|
||||
total_ram_usage += section.size
|
||||
elif name in self.flash_sections:
|
||||
location = "FLASH"
|
||||
total_flash_usage += section.size
|
||||
else:
|
||||
location = "OTHER"
|
||||
|
||||
if show_all_sections or name in self.ram_sections:
|
||||
lines.append(
|
||||
f"{name:<20} 0x{section.address:08x} {section.size:>8} B {location}"
|
||||
)
|
||||
|
||||
lines.append("-" * table_width)
|
||||
lines.append(f"Total RAM sections size: {total_ram_usage:,} bytes")
|
||||
lines.append(f"Total Flash sections size: {total_flash_usage:,} bytes")
|
||||
|
||||
# Strings in RAM
|
||||
lines.append("")
|
||||
lines.append("=" * table_width)
|
||||
lines.append("STRINGS IN RAM SECTIONS")
|
||||
lines.append("=" * table_width)
|
||||
lines.append(
|
||||
"Note: .bss sections contain uninitialized data (no strings to extract)"
|
||||
)
|
||||
|
||||
# Group strings by section
|
||||
strings_by_section: dict[str, list[RamString]] = defaultdict(list)
|
||||
for ram_string in self.ram_strings:
|
||||
strings_by_section[ram_string.section].append(ram_string)
|
||||
|
||||
for section_name in sorted(strings_by_section.keys()):
|
||||
section_strings = strings_by_section[section_name]
|
||||
lines.append(f"\nSection: {section_name}")
|
||||
lines.append("-" * 40)
|
||||
for ram_string in sorted(section_strings, key=lambda x: x.address):
|
||||
clean_string = ram_string.content[:100] + (
|
||||
"..." if len(ram_string.content) > 100 else ""
|
||||
)
|
||||
lines.append(
|
||||
f' 0x{ram_string.address:08x}: "{clean_string}" (len={len(ram_string.content)})'
|
||||
)
|
||||
|
||||
# Large RAM symbols
|
||||
lines.append("")
|
||||
lines.append("=" * table_width)
|
||||
lines.append("LARGE DATA SYMBOLS IN RAM (>= 50 bytes)")
|
||||
lines.append("=" * table_width)
|
||||
|
||||
largest_symbols = self.get_largest_symbols(50)
|
||||
lines.append(f"\n{'Symbol':<50} {'Type':<6} {'Size':<10} {'Section'}")
|
||||
lines.append("-" * table_width)
|
||||
|
||||
for symbol in largest_symbols:
|
||||
# Use demangled name if available, otherwise raw name
|
||||
display_name = symbol.demangled or symbol.name
|
||||
name_display = display_name[:49] if len(display_name) > 49 else display_name
|
||||
lines.append(
|
||||
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
|
||||
)
|
||||
|
||||
# Summary
|
||||
lines.append("")
|
||||
lines.append("=" * table_width)
|
||||
lines.append("SUMMARY")
|
||||
lines.append("=" * table_width)
|
||||
lines.append(f"Total strings found in RAM: {len(self.ram_strings)}")
|
||||
total_string_bytes = self.get_total_string_bytes()
|
||||
lines.append(f"Total bytes used by strings: {total_string_bytes:,}")
|
||||
|
||||
# Optimization targets
|
||||
lines.append("")
|
||||
lines.append("=" * table_width)
|
||||
lines.append("POTENTIAL OPTIMIZATION TARGETS")
|
||||
lines.append("=" * table_width)
|
||||
|
||||
# Repeated strings
|
||||
repeated = self.get_repeated_strings()[:10]
|
||||
if repeated:
|
||||
lines.append("\nRepeated strings (could be deduplicated):")
|
||||
for string, count in repeated:
|
||||
savings = (count - 1) * (len(string) + 1)
|
||||
clean_string = string[:50] + ("..." if len(string) > 50 else "")
|
||||
lines.append(
|
||||
f' "{clean_string}" - appears {count} times (potential savings: {savings} bytes)'
|
||||
)
|
||||
|
||||
# Long strings - platform-specific advice
|
||||
long_strings = self.get_long_strings(20)[:10]
|
||||
if long_strings:
|
||||
if self.platform == "esp8266":
|
||||
lines.append(
|
||||
"\nLong strings that could be moved to PROGMEM (>= 20 chars):"
|
||||
)
|
||||
else:
|
||||
# ESP32: strings in DRAM are typically there for a reason
|
||||
# (interrupt handlers, pre-flash-init code, etc.)
|
||||
lines.append("\nLong strings in DRAM (>= 20 chars):")
|
||||
lines.append(
|
||||
"Note: ESP32 DRAM strings may be required for interrupt/early-boot contexts"
|
||||
)
|
||||
for ram_string in long_strings:
|
||||
clean_string = ram_string.content[:60] + (
|
||||
"..." if len(ram_string.content) > 60 else ""
|
||||
)
|
||||
lines.append(
|
||||
f' {ram_string.section} @ 0x{ram_string.address:08x}: "{clean_string}" ({len(ram_string.content)} bytes)'
|
||||
)
|
||||
|
||||
lines.append("")
|
||||
return "\n".join(lines)
|
||||
57
esphome/analyze_memory/toolchain.py
Normal file
57
esphome/analyze_memory/toolchain.py
Normal file
@@ -0,0 +1,57 @@
|
||||
"""Toolchain utilities for memory analysis."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# Platform-specific toolchain prefixes
|
||||
TOOLCHAIN_PREFIXES = [
|
||||
"xtensa-lx106-elf-", # ESP8266
|
||||
"xtensa-esp32-elf-", # ESP32
|
||||
"xtensa-esp-elf-", # ESP32 (newer IDF)
|
||||
"", # System default (no prefix)
|
||||
]
|
||||
|
||||
|
||||
def find_tool(
|
||||
tool_name: str,
|
||||
objdump_path: str | None = None,
|
||||
) -> str | None:
|
||||
"""Find a toolchain tool by name.
|
||||
|
||||
First tries to derive the tool path from objdump_path (if provided),
|
||||
then falls back to searching for platform-specific tools.
|
||||
|
||||
Args:
|
||||
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
|
||||
objdump_path: Path to objdump binary to derive other tool paths from
|
||||
|
||||
Returns:
|
||||
Path to the tool or None if not found
|
||||
"""
|
||||
# Try to derive from objdump path first (most reliable)
|
||||
if objdump_path and objdump_path != "objdump":
|
||||
objdump_file = Path(objdump_path)
|
||||
# Replace just the filename portion, preserving any prefix (e.g., xtensa-esp32-elf-)
|
||||
new_name = objdump_file.name.replace("objdump", tool_name)
|
||||
potential_path = str(objdump_file.with_name(new_name))
|
||||
if Path(potential_path).exists():
|
||||
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
|
||||
return potential_path
|
||||
|
||||
# Try platform-specific tools
|
||||
for prefix in TOOLCHAIN_PREFIXES:
|
||||
cmd = f"{prefix}{tool_name}"
|
||||
try:
|
||||
subprocess.run([cmd, "--version"], capture_output=True, check=True)
|
||||
_LOGGER.debug("Found %s: %s", tool_name, cmd)
|
||||
return cmd
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
continue
|
||||
|
||||
_LOGGER.warning("Could not find %s tool", tool_name)
|
||||
return None
|
||||
@@ -87,7 +87,7 @@ void AbsoluteHumidityComponent::loop() {
|
||||
break;
|
||||
default:
|
||||
this->publish_state(NAN);
|
||||
this->status_set_error("Invalid saturation vapor pressure equation selection!");
|
||||
this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!"));
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
|
||||
|
||||
@@ -107,6 +107,17 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
4: adc_channel_t.ADC_CHANNEL_3,
|
||||
5: adc_channel_t.ADC_CHANNEL_4,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
|
||||
VARIANT_ESP32P4: {
|
||||
16: adc_channel_t.ADC_CHANNEL_0,
|
||||
17: adc_channel_t.ADC_CHANNEL_1,
|
||||
18: adc_channel_t.ADC_CHANNEL_2,
|
||||
19: adc_channel_t.ADC_CHANNEL_3,
|
||||
20: adc_channel_t.ADC_CHANNEL_4,
|
||||
21: adc_channel_t.ADC_CHANNEL_5,
|
||||
22: adc_channel_t.ADC_CHANNEL_6,
|
||||
23: adc_channel_t.ADC_CHANNEL_7,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32S2: {
|
||||
1: adc_channel_t.ADC_CHANNEL_0,
|
||||
@@ -133,16 +144,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
9: adc_channel_t.ADC_CHANNEL_8,
|
||||
10: adc_channel_t.ADC_CHANNEL_9,
|
||||
},
|
||||
VARIANT_ESP32P4: {
|
||||
16: adc_channel_t.ADC_CHANNEL_0,
|
||||
17: adc_channel_t.ADC_CHANNEL_1,
|
||||
18: adc_channel_t.ADC_CHANNEL_2,
|
||||
19: adc_channel_t.ADC_CHANNEL_3,
|
||||
20: adc_channel_t.ADC_CHANNEL_4,
|
||||
21: adc_channel_t.ADC_CHANNEL_5,
|
||||
22: adc_channel_t.ADC_CHANNEL_6,
|
||||
23: adc_channel_t.ADC_CHANNEL_7,
|
||||
},
|
||||
}
|
||||
|
||||
# pin to adc2 channel mapping
|
||||
@@ -175,6 +176,15 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
VARIANT_ESP32C6: {}, # no ADC2
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32H2: {}, # no ADC2
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
|
||||
VARIANT_ESP32P4: {
|
||||
49: adc_channel_t.ADC_CHANNEL_0,
|
||||
50: adc_channel_t.ADC_CHANNEL_1,
|
||||
51: adc_channel_t.ADC_CHANNEL_2,
|
||||
52: adc_channel_t.ADC_CHANNEL_3,
|
||||
53: adc_channel_t.ADC_CHANNEL_4,
|
||||
54: adc_channel_t.ADC_CHANNEL_5,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
|
||||
VARIANT_ESP32S2: {
|
||||
11: adc_channel_t.ADC_CHANNEL_0,
|
||||
@@ -201,14 +211,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
19: adc_channel_t.ADC_CHANNEL_8,
|
||||
20: adc_channel_t.ADC_CHANNEL_9,
|
||||
},
|
||||
VARIANT_ESP32P4: {
|
||||
49: adc_channel_t.ADC_CHANNEL_0,
|
||||
50: adc_channel_t.ADC_CHANNEL_1,
|
||||
51: adc_channel_t.ADC_CHANNEL_2,
|
||||
52: adc_channel_t.ADC_CHANNEL_3,
|
||||
53: adc_channel_t.ADC_CHANNEL_4,
|
||||
54: adc_channel_t.ADC_CHANNEL_5,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ void ADCSensor::setup() {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
// RISC-V variants and S3 use curve fitting calibration
|
||||
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
@@ -111,7 +111,7 @@ void ADCSensor::setup() {
|
||||
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
|
||||
this->setup_flags_.calibration_complete = false;
|
||||
}
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
}
|
||||
|
||||
this->setup_flags_.init_complete = true;
|
||||
@@ -186,11 +186,11 @@ float ADCSensor::sample_fixed_attenuation_() {
|
||||
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else // Other ESP32 variants use line fitting calibration
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
|
||||
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
this->calibration_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -219,7 +219,7 @@ float ADCSensor::sample_autorange_() {
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
// Delete old calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
@@ -231,7 +231,7 @@ float ADCSensor::sample_autorange_() {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_curve_fitting_config_t cali_config = {};
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
@@ -266,7 +266,7 @@ float ADCSensor::sample_autorange_() {
|
||||
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
|
||||
if (handle != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
@@ -288,7 +288,7 @@ float ADCSensor::sample_autorange_() {
|
||||
}
|
||||
// Clean up calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
|
||||
@@ -24,6 +24,8 @@ from esphome.const import (
|
||||
UNIT_WATT,
|
||||
)
|
||||
|
||||
CODEOWNERS = ["@angelnu"]
|
||||
|
||||
CONF_CURRENT_A = "current_a"
|
||||
CONF_CURRENT_B = "current_b"
|
||||
CONF_ACTIVE_POWER_A = "active_power_a"
|
||||
|
||||
@@ -25,7 +25,8 @@ void ADE7953::setup() {
|
||||
this->ade_write_8(PGA_V_8, pga_v_);
|
||||
this->ade_write_8(PGA_IA_8, pga_ia_);
|
||||
this->ade_write_8(PGA_IB_8, pga_ib_);
|
||||
this->ade_write_32(AVGAIN_32, vgain_);
|
||||
this->ade_write_32(AVGAIN_32, avgain_);
|
||||
this->ade_write_32(BVGAIN_32, bvgain_);
|
||||
this->ade_write_32(AIGAIN_32, aigain_);
|
||||
this->ade_write_32(BIGAIN_32, bigain_);
|
||||
this->ade_write_32(AWGAIN_32, awgain_);
|
||||
@@ -34,7 +35,8 @@ void ADE7953::setup() {
|
||||
this->ade_read_8(PGA_V_8, &pga_v_);
|
||||
this->ade_read_8(PGA_IA_8, &pga_ia_);
|
||||
this->ade_read_8(PGA_IB_8, &pga_ib_);
|
||||
this->ade_read_32(AVGAIN_32, &vgain_);
|
||||
this->ade_read_32(AVGAIN_32, &avgain_);
|
||||
this->ade_read_32(BVGAIN_32, &bvgain_);
|
||||
this->ade_read_32(AIGAIN_32, &aigain_);
|
||||
this->ade_read_32(BIGAIN_32, &bigain_);
|
||||
this->ade_read_32(AWGAIN_32, &awgain_);
|
||||
@@ -63,13 +65,14 @@ void ADE7953::dump_config() {
|
||||
" PGA_V_8: 0x%X\n"
|
||||
" PGA_IA_8: 0x%X\n"
|
||||
" PGA_IB_8: 0x%X\n"
|
||||
" VGAIN_32: 0x%08jX\n"
|
||||
" AVGAIN_32: 0x%08jX\n"
|
||||
" BVGAIN_32: 0x%08jX\n"
|
||||
" AIGAIN_32: 0x%08jX\n"
|
||||
" BIGAIN_32: 0x%08jX\n"
|
||||
" AWGAIN_32: 0x%08jX\n"
|
||||
" BWGAIN_32: 0x%08jX",
|
||||
this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) vgain_, (uintmax_t) aigain_,
|
||||
(uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_);
|
||||
this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) avgain_, (uintmax_t) bvgain_,
|
||||
(uintmax_t) aigain_, (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_);
|
||||
}
|
||||
|
||||
#define ADE_PUBLISH_(name, val, factor) \
|
||||
|
||||
@@ -46,7 +46,12 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
|
||||
void set_pga_ib(uint8_t pga_ib) { pga_ib_ = pga_ib; }
|
||||
|
||||
// Set input gains
|
||||
void set_vgain(uint32_t vgain) { vgain_ = vgain; }
|
||||
void set_vgain(uint32_t vgain) {
|
||||
// Datasheet says: "to avoid discrepancies in other registers,
|
||||
// if AVGAIN is set then BVGAIN should be set to the same value."
|
||||
avgain_ = vgain;
|
||||
bvgain_ = vgain;
|
||||
}
|
||||
void set_aigain(uint32_t aigain) { aigain_ = aigain; }
|
||||
void set_bigain(uint32_t bigain) { bigain_ = bigain; }
|
||||
void set_awgain(uint32_t awgain) { awgain_ = awgain; }
|
||||
@@ -100,7 +105,8 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
|
||||
uint8_t pga_v_;
|
||||
uint8_t pga_ia_;
|
||||
uint8_t pga_ib_;
|
||||
uint32_t vgain_;
|
||||
uint32_t avgain_;
|
||||
uint32_t bvgain_;
|
||||
uint32_t aigain_;
|
||||
uint32_t bigain_;
|
||||
uint32_t awgain_;
|
||||
|
||||
@@ -83,7 +83,7 @@ void AHT10Component::setup() {
|
||||
void AHT10Component::restart_read_() {
|
||||
if (this->read_count_ == AHT10_ATTEMPTS) {
|
||||
this->read_count_ = 0;
|
||||
this->status_set_error("Reading timed out");
|
||||
this->status_set_error(LOG_STR("Reading timed out"));
|
||||
return;
|
||||
}
|
||||
this->read_count_++;
|
||||
|
||||
@@ -56,13 +56,13 @@ bool Alpha3::is_current_response_type_(const uint8_t *response_type) {
|
||||
|
||||
void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
|
||||
if (this->response_offset_ >= this->response_length_) {
|
||||
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str().c_str());
|
||||
ESP_LOGD(TAG, "[%s] GENI response begin", this->parent_->address_str());
|
||||
if (length < GENI_RESPONSE_HEADER_LENGTH) {
|
||||
ESP_LOGW(TAG, "[%s] response to short", this->parent_->address_str().c_str());
|
||||
ESP_LOGW(TAG, "[%s] response too short", this->parent_->address_str());
|
||||
return;
|
||||
}
|
||||
if (response[0] != 36 || response[2] != 248 || response[3] != 231 || response[4] != 10) {
|
||||
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str().c_str(),
|
||||
ESP_LOGW(TAG, "[%s] response bytes %d %d %d %d %d don't match GENI HEADER", this->parent_->address_str(),
|
||||
response[0], response[1], response[2], response[3], response[4]);
|
||||
return;
|
||||
}
|
||||
@@ -77,11 +77,11 @@ void Alpha3::handle_geni_response_(const uint8_t *response, uint16_t length) {
|
||||
};
|
||||
|
||||
if (this->is_current_response_type_(GENI_RESPONSE_TYPE_FLOW_HEAD)) {
|
||||
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str().c_str());
|
||||
ESP_LOGD(TAG, "[%s] FLOW HEAD Response", this->parent_->address_str());
|
||||
extract_publish_sensor_value(GENI_RESPONSE_FLOW_OFFSET, this->flow_sensor_, 3600.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_HEAD_OFFSET, this->head_sensor_, .0001F);
|
||||
} else if (this->is_current_response_type_(GENI_RESPONSE_TYPE_POWER)) {
|
||||
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str().c_str());
|
||||
ESP_LOGD(TAG, "[%s] POWER Response", this->parent_->address_str());
|
||||
extract_publish_sensor_value(GENI_RESPONSE_POWER_OFFSET, this->power_sensor_, 1.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_CURRENT_OFFSET, this->current_sensor_, 1.0F);
|
||||
extract_publish_sensor_value(GENI_RESPONSE_MOTOR_SPEED_OFFSET, this->speed_sensor_, 1.0F);
|
||||
@@ -100,7 +100,7 @@ void Alpha3::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc
|
||||
if (param->open.status == ESP_GATT_OK) {
|
||||
this->response_offset_ = 0;
|
||||
this->response_length_ = 0;
|
||||
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str().c_str());
|
||||
ESP_LOGI(TAG, "[%s] connection open", this->parent_->address_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -132,7 +132,7 @@ void Alpha3::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(ALPHA3_GENI_SERVICE_UUID, ALPHA3_GENI_CHARACTERISTIC_UUID);
|
||||
if (chr == nullptr) {
|
||||
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str().c_str());
|
||||
ESP_LOGE(TAG, "[%s] No GENI service found at device, not an Alpha3..?", this->parent_->address_str());
|
||||
break;
|
||||
}
|
||||
auto status = esp_ble_gattc_register_for_notify(this->parent_->get_gattc_if(), this->parent_->get_remote_bda(),
|
||||
@@ -164,12 +164,12 @@ void Alpha3::send_request_(uint8_t *request, size_t len) {
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
|
||||
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status)
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
|
||||
void Alpha3::update() {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,11 +44,9 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
|
||||
auto *chr = this->parent_->get_characteristic(AM43_SERVICE_UUID, AM43_CHARACTERISTIC_UUID);
|
||||
if (chr == nullptr) {
|
||||
if (this->parent_->get_characteristic(AM43_TUYA_SERVICE_UUID, AM43_TUYA_CHARACTERISTIC_UUID) != nullptr) {
|
||||
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.",
|
||||
this->parent_->address_str().c_str());
|
||||
ESP_LOGE(TAG, "[%s] Detected a Tuya AM43 which is not supported, sorry.", this->parent_->address_str());
|
||||
} else {
|
||||
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?",
|
||||
this->parent_->address_str().c_str());
|
||||
ESP_LOGE(TAG, "[%s] No control service found at device, not an AM43..?", this->parent_->address_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -82,8 +80,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
|
||||
this->char_handle_, packet->length, packet->data,
|
||||
ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
|
||||
status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
this->current_sensor_ = 0;
|
||||
@@ -97,7 +94,7 @@ void Am43::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_i
|
||||
|
||||
void Am43::update() {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str().c_str());
|
||||
ESP_LOGW(TAG, "[%s] Cannot poll, not connected", this->parent_->address_str());
|
||||
return;
|
||||
}
|
||||
if (this->current_sensor_ == 0) {
|
||||
@@ -107,7 +104,7 @@ void Am43::update() {
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
packet->length, packet->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
this->current_sensor_++;
|
||||
|
||||
@@ -12,10 +12,11 @@ void AnalogThresholdBinarySensor::setup() {
|
||||
// TRUE state is defined to be when sensor is >= threshold
|
||||
// so when undefined sensor value initialize to FALSE
|
||||
if (std::isnan(sensor_value)) {
|
||||
this->raw_state_ = false;
|
||||
this->publish_initial_state(false);
|
||||
} else {
|
||||
this->publish_initial_state(sensor_value >=
|
||||
(this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f);
|
||||
this->raw_state_ = sensor_value >= (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f;
|
||||
this->publish_initial_state(this->raw_state_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +26,10 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
|
||||
this->sensor_->add_on_state_callback([this](float sensor_value) {
|
||||
// if there is an invalid sensor reading, ignore the change and keep the current state
|
||||
if (!std::isnan(sensor_value)) {
|
||||
this->publish_state(sensor_value >=
|
||||
(this->state ? this->lower_threshold_.value() : this->upper_threshold_.value()));
|
||||
// Use raw_state_ for hysteresis logic, not this->state which is post-filter
|
||||
this->raw_state_ =
|
||||
sensor_value >= (this->raw_state_ ? this->lower_threshold_.value() : this->upper_threshold_.value());
|
||||
this->publish_state(this->raw_state_);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
|
||||
sensor::Sensor *sensor_{nullptr};
|
||||
TemplatableValue<float> upper_threshold_{};
|
||||
TemplatableValue<float> lower_threshold_{};
|
||||
bool raw_state_{false}; // Pre-filter state for hysteresis logic
|
||||
};
|
||||
|
||||
} // namespace analog_threshold
|
||||
|
||||
@@ -42,7 +42,7 @@ void Anova::control(const ClimateCall &call) {
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
if (call.get_target_temperature().has_value()) {
|
||||
@@ -51,7 +51,7 @@ void Anova::control(const ClimateCall &call) {
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -124,8 +124,7 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(),
|
||||
status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +149,7 @@ void Anova::update() {
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_,
|
||||
pkt->length, pkt->data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
this->current_request_++;
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||
CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
|
||||
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
@@ -227,6 +228,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
esp32=8, # More RAM, can buffer more
|
||||
rp2040=5, # Limited RAM
|
||||
bk72xx=8, # Moderate RAM
|
||||
nrf52=8, # Moderate RAM
|
||||
rtl87xx=8, # Moderate RAM
|
||||
host=16, # Abundant resources
|
||||
ln882x=8, # Moderate RAM
|
||||
@@ -259,9 +261,9 @@ async def to_code(config):
|
||||
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
|
||||
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
|
||||
|
||||
# Set USE_API_SERVICES if any services are enabled
|
||||
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
|
||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||
cg.add_define("USE_API_SERVICES")
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTIONS")
|
||||
|
||||
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
|
||||
if config[CONF_CUSTOM_SERVICES]:
|
||||
@@ -536,9 +538,24 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_condition("api.connected", APIConnectedCondition, {})
|
||||
API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(APIServer),
|
||||
cv.Optional(CONF_STATE_SUBSCRIPTION_ONLY, default=False): cv.templatable(
|
||||
cv.boolean
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"api.connected", APIConnectedCondition, API_CONNECTED_CONDITION_SCHEMA
|
||||
)
|
||||
async def api_connected_to_code(config, condition_id, template_arg, args):
|
||||
return cg.new_Pvariable(condition_id, template_arg)
|
||||
var = cg.new_Pvariable(condition_id, template_arg)
|
||||
templ = await cg.templatable(config[CONF_STATE_SUBSCRIPTION_ONLY], args, cg.bool_)
|
||||
cg.add(var.set_state_subscription_only(templ))
|
||||
return var
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
|
||||
@@ -518,7 +518,7 @@ message ListEntitiesLightResponse {
|
||||
bool legacy_supports_color_temperature = 8 [deprecated=true];
|
||||
float min_mireds = 9;
|
||||
float max_mireds = 10;
|
||||
repeated string effects = 11;
|
||||
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"];
|
||||
bool disabled_by_default = 13;
|
||||
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
EntityCategory entity_category = 15;
|
||||
@@ -589,6 +589,7 @@ enum SensorStateClass {
|
||||
STATE_CLASS_MEASUREMENT = 1;
|
||||
STATE_CLASS_TOTAL_INCREASING = 2;
|
||||
STATE_CLASS_TOTAL = 3;
|
||||
STATE_CLASS_MEASUREMENT_ANGLE = 4;
|
||||
}
|
||||
|
||||
// Deprecated in API version 1.5
|
||||
@@ -855,21 +856,21 @@ enum ServiceArgType {
|
||||
SERVICE_ARG_TYPE_STRING_ARRAY = 7;
|
||||
}
|
||||
message ListEntitiesServicesArgument {
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
string name = 1;
|
||||
ServiceArgType type = 2;
|
||||
}
|
||||
message ListEntitiesServicesResponse {
|
||||
option (id) = 41;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
|
||||
string name = 1;
|
||||
fixed32 key = 2;
|
||||
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
||||
}
|
||||
message ExecuteServiceArgument {
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
bool bool_ = 1;
|
||||
int32 legacy_int = 2;
|
||||
float float_ = 3;
|
||||
@@ -885,7 +886,7 @@ message ExecuteServiceRequest {
|
||||
option (id) = 42;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_SERVICES";
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
|
||||
fixed32 key = 1;
|
||||
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
|
||||
|
||||
@@ -90,8 +90,8 @@ static const int CAMERA_STOP_STREAM = 5000;
|
||||
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent)
|
||||
: parent_(parent), initial_state_iterator_(this), list_entities_iterator_(this) {
|
||||
#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
|
||||
auto noise_ctx = parent->get_noise_ctx();
|
||||
if (noise_ctx->has_psk()) {
|
||||
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_)};
|
||||
} else {
|
||||
@@ -169,8 +169,7 @@ void APIConnection::loop() {
|
||||
} else {
|
||||
this->last_traffic_ = now;
|
||||
// read a packet
|
||||
this->read_message(buffer.data_len, buffer.type,
|
||||
buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
|
||||
this->read_message(buffer.data_len, buffer.type, buffer.data);
|
||||
if (this->flags_.remove)
|
||||
return;
|
||||
}
|
||||
@@ -195,6 +194,9 @@ void APIConnection::loop() {
|
||||
}
|
||||
// Now that everything is sent, enable immediate sending for future state changes
|
||||
this->flags_.should_try_send_immediately = true;
|
||||
// Release excess memory from buffers that grew during initial sync
|
||||
this->deferred_batch_.release_buffer();
|
||||
this->helper_->release_buffers();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -476,19 +478,24 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
|
||||
auto *light = static_cast<light::LightState *>(entity);
|
||||
ListEntitiesLightResponse msg;
|
||||
auto traits = light->get_traits();
|
||||
auto supported_modes = traits.get_supported_color_modes();
|
||||
// Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
|
||||
msg.supported_color_modes = &traits.get_supported_color_modes();
|
||||
msg.supported_color_modes = &supported_modes;
|
||||
if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
|
||||
traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
|
||||
msg.min_mireds = traits.get_min_mireds();
|
||||
msg.max_mireds = traits.get_max_mireds();
|
||||
}
|
||||
FixedVector<const char *> effects_list;
|
||||
if (light->supports_effects()) {
|
||||
msg.effects.emplace_back("None");
|
||||
for (auto *effect : light->get_effects()) {
|
||||
msg.effects.emplace_back(effect->get_name());
|
||||
auto &light_effects = light->get_effects();
|
||||
effects_list.init(light_effects.size() + 1);
|
||||
effects_list.push_back("None");
|
||||
for (auto *effect : light_effects) {
|
||||
effects_list.push_back(effect->get_name());
|
||||
}
|
||||
}
|
||||
msg.effects = &effects_list;
|
||||
return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -1294,11 +1301,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void APIConnection::send_event(event::Event *event, const std::string &event_type) {
|
||||
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
||||
EventResponse::ESTIMATED_SIZE);
|
||||
void APIConnection::send_event(event::Event *event, const char *event_type) {
|
||||
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
|
||||
EventResponse::ESTIMATED_SIZE);
|
||||
}
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
||||
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single) {
|
||||
EventResponse resp;
|
||||
resp.set_event_type(StringRef(event_type));
|
||||
@@ -1450,8 +1457,11 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
#ifdef USE_AREAS
|
||||
resp.set_suggested_area(StringRef(App.get_area()));
|
||||
#endif
|
||||
// mac_address must store temporary string - will be valid during send_message call
|
||||
std::string mac_address = get_mac_address_pretty();
|
||||
// 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.set_esphome_version(ESPHOME_VERSION_REF);
|
||||
@@ -1467,6 +1477,8 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Beken");
|
||||
#elif defined(USE_LN882X)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Lightning");
|
||||
#elif defined(USE_NRF52)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Nordic Semiconductor");
|
||||
#elif defined(USE_RTL87XX)
|
||||
static constexpr auto MANUFACTURER = StringRef::from_lit("Realtek");
|
||||
#elif defined(USE_HOST)
|
||||
@@ -1490,8 +1502,9 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
|
||||
// bt_mac must store temporary string - will be valid during send_message call
|
||||
std::string bluetooth_mac = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_mac_address_pretty();
|
||||
// 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));
|
||||
#endif
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
@@ -1538,7 +1551,7 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
bool found = false;
|
||||
for (auto *service : this->parent_->get_user_services()) {
|
||||
@@ -1648,9 +1661,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
|
||||
// O(n) but optimized for RAM and not performance.
|
||||
for (auto &item : items) {
|
||||
if (item.entity == entity && item.message_type == message_type) {
|
||||
// Clean up old creator before replacing
|
||||
item.creator.cleanup(message_type);
|
||||
// Move assign the new creator
|
||||
// Replace with new creator
|
||||
item.creator = std::move(creator);
|
||||
return;
|
||||
}
|
||||
@@ -1820,7 +1831,7 @@ void APIConnection::process_batch_() {
|
||||
|
||||
// Handle remaining items more efficiently
|
||||
if (items_processed < this->deferred_batch_.size()) {
|
||||
// Remove processed items from the beginning with proper cleanup
|
||||
// Remove processed items from the beginning
|
||||
this->deferred_batch_.remove_front(items_processed);
|
||||
// Reschedule for remaining items
|
||||
this->schedule_batch_();
|
||||
@@ -1833,10 +1844,10 @@ void APIConnection::process_batch_() {
|
||||
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
|
||||
bool is_single, uint8_t message_type) const {
|
||||
#ifdef USE_EVENT
|
||||
// Special case: EventResponse uses string pointer
|
||||
// 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_.string_ptr, conn, remaining_size, is_single);
|
||||
return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -177,7 +177,7 @@ class APIConnection final : public APIServerConnection {
|
||||
#endif
|
||||
|
||||
#ifdef USE_EVENT
|
||||
void send_event(event::Event *event, const std::string &event_type);
|
||||
void send_event(event::Event *event, const char *event_type);
|
||||
#endif
|
||||
|
||||
#ifdef USE_UPDATE
|
||||
@@ -221,7 +221,7 @@ class APIConnection final : public APIServerConnection {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void execute_service(const ExecuteServiceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -450,7 +450,7 @@ class APIConnection final : public APIServerConnection {
|
||||
bool is_single);
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
|
||||
static uint16_t try_send_event_response(event::Event *event, const char *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
|
||||
@@ -508,10 +508,8 @@ class APIConnection final : public APIServerConnection {
|
||||
// Constructor for function pointer
|
||||
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
|
||||
|
||||
// Constructor for string state capture
|
||||
explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
|
||||
|
||||
// No destructor - cleanup must be called explicitly with message_type
|
||||
// Constructor for const char * (Event types - no allocation needed)
|
||||
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
|
||||
|
||||
// Delete copy operations - MessageCreator should only be moved
|
||||
MessageCreator(const MessageCreator &other) = delete;
|
||||
@@ -523,8 +521,6 @@ class APIConnection final : public APIServerConnection {
|
||||
// Move assignment
|
||||
MessageCreator &operator=(MessageCreator &&other) noexcept {
|
||||
if (this != &other) {
|
||||
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
|
||||
// In our usage, this happens in add_item() deduplication and vector::erase()
|
||||
data_ = other.data_;
|
||||
other.data_.function_ptr = nullptr;
|
||||
}
|
||||
@@ -535,20 +531,10 @@ class APIConnection final : public APIServerConnection {
|
||||
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
|
||||
uint8_t message_type) const;
|
||||
|
||||
// Manual cleanup method - must be called before destruction for string types
|
||||
void cleanup(uint8_t message_type) {
|
||||
#ifdef USE_EVENT
|
||||
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
|
||||
delete data_.string_ptr;
|
||||
data_.string_ptr = nullptr;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
private:
|
||||
union Data {
|
||||
MessageCreatorPtr function_ptr;
|
||||
std::string *string_ptr;
|
||||
const char *const_char_ptr;
|
||||
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
|
||||
};
|
||||
|
||||
@@ -568,46 +554,35 @@ class APIConnection final : public APIServerConnection {
|
||||
std::vector<BatchItem> items;
|
||||
uint32_t batch_start_time{0};
|
||||
|
||||
private:
|
||||
// Helper to cleanup items from the beginning
|
||||
void cleanup_items_(size_t count) {
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
items[i].creator.cleanup(items[i].message_type);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
DeferredBatch() {
|
||||
// Pre-allocate capacity for typical batch sizes to avoid reallocation
|
||||
items.reserve(8);
|
||||
}
|
||||
|
||||
~DeferredBatch() {
|
||||
// Ensure cleanup of any remaining items
|
||||
clear();
|
||||
}
|
||||
// No pre-allocation - log connections never use batching, and for
|
||||
// connections that do, buffers are released after initial sync anyway
|
||||
|
||||
// Add item to the batch
|
||||
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||
// Add item to the front of the batch (for high priority messages like ping)
|
||||
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
|
||||
|
||||
// Clear all items with proper cleanup
|
||||
// Clear all items
|
||||
void clear() {
|
||||
cleanup_items_(items.size());
|
||||
items.clear();
|
||||
batch_start_time = 0;
|
||||
}
|
||||
|
||||
// Remove processed items from the front with proper cleanup
|
||||
void remove_front(size_t count) {
|
||||
cleanup_items_(count);
|
||||
items.erase(items.begin(), items.begin() + count);
|
||||
}
|
||||
// Remove processed items from the front
|
||||
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
|
||||
|
||||
bool empty() const { return items.empty(); }
|
||||
size_t size() const { return items.size(); }
|
||||
const BatchItem &operator[](size_t index) const { return items[index]; }
|
||||
// Release excess capacity - only releases if items already empty
|
||||
void release_buffer() {
|
||||
// Safe to call: batch is processed before release_buffer is called,
|
||||
// and if any items remain (partial processing), we must not clear them.
|
||||
// Use swap trick since shrink_to_fit() is non-binding and may be ignored.
|
||||
if (items.empty()) {
|
||||
std::vector<BatchItem>().swap(items);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// DeferredBatch here (16 bytes, 4-byte aligned)
|
||||
@@ -682,21 +657,30 @@ class APIConnection final : public APIServerConnection {
|
||||
}
|
||||
#endif
|
||||
|
||||
// Helper to check if a message type should bypass batching
|
||||
// Returns true if:
|
||||
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
|
||||
// the main loop is blocked, e.g., during OTA updates)
|
||||
// 2. It's an EventResponse (events are edge-triggered - every occurrence matters)
|
||||
// 3. OR: User has opted into immediate sending (should_try_send_immediately = true
|
||||
// AND batch_delay = 0)
|
||||
inline bool should_send_immediately_(uint8_t message_type) const {
|
||||
return (
|
||||
#ifdef USE_UPDATE
|
||||
message_type == UpdateStateResponse::MESSAGE_TYPE ||
|
||||
#endif
|
||||
#ifdef USE_EVENT
|
||||
message_type == EventResponse::MESSAGE_TYPE ||
|
||||
#endif
|
||||
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0));
|
||||
}
|
||||
|
||||
// Helper method to send a message either immediately or via batching
|
||||
// Tries immediate send if should_send_immediately_() returns true and buffer has space
|
||||
// Falls back to batching if immediate send fails or isn't applicable
|
||||
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
|
||||
uint8_t estimated_size) {
|
||||
// Try to send immediately if:
|
||||
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
|
||||
// the main loop is blocked, e.g., during OTA updates)
|
||||
// 2. OR: We should try to send immediately (should_try_send_immediately = true)
|
||||
// AND Batch delay is 0 (user has opted in to immediate sending)
|
||||
// 3. AND: Buffer has space available
|
||||
if ((
|
||||
#ifdef USE_UPDATE
|
||||
message_type == UpdateStateResponse::MESSAGE_TYPE ||
|
||||
#endif
|
||||
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
|
||||
this->helper_->can_write_without_blocking()) {
|
||||
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
||||
// Now actually encode and send
|
||||
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
|
||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
||||
@@ -714,6 +698,27 @@ class APIConnection final : public APIServerConnection {
|
||||
return this->schedule_message_(entity, creator, message_type, estimated_size);
|
||||
}
|
||||
|
||||
// Overload for MessageCreator (used by events which need to capture event_type)
|
||||
bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
|
||||
// Try to send immediately if message type should bypass batching and buffer has space
|
||||
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
||||
// Now actually encode and send
|
||||
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) &&
|
||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Log the message in verbose mode
|
||||
this->log_proto_message_(entity, creator, message_type);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
// If immediate send failed, fall through to batching
|
||||
}
|
||||
|
||||
// Fall back to scheduled batching
|
||||
return this->schedule_message_(entity, std::move(creator), message_type, estimated_size);
|
||||
}
|
||||
|
||||
// Helper function to schedule a deferred message with known message type
|
||||
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
|
||||
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
|
||||
|
||||
@@ -35,10 +35,9 @@ struct ClientInfo;
|
||||
class ProtoWriteBuffer;
|
||||
|
||||
struct ReadPacketBuffer {
|
||||
std::vector<uint8_t> container;
|
||||
uint16_t type;
|
||||
uint16_t data_offset;
|
||||
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
|
||||
@@ -84,9 +83,7 @@ class APIFrameHelper {
|
||||
public:
|
||||
APIFrameHelper() = default;
|
||||
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
|
||||
: socket_owned_(std::move(socket)), client_info_(client_info) {
|
||||
socket_ = socket_owned_.get();
|
||||
}
|
||||
: socket_(std::move(socket)), client_info_(client_info) {}
|
||||
virtual ~APIFrameHelper() = default;
|
||||
virtual APIError init() = 0;
|
||||
virtual APIError loop();
|
||||
@@ -121,6 +118,22 @@ class APIFrameHelper {
|
||||
uint8_t frame_footer_size() const { return frame_footer_size_; }
|
||||
// Check if socket has data ready to read
|
||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||
// Release excess memory from internal buffers after initial sync
|
||||
void release_buffers() {
|
||||
// rx_buf_: Safe to clear only if no partial read in progress.
|
||||
// rx_buf_len_ tracks bytes read so far; if non-zero, we're mid-frame
|
||||
// and clearing would lose partially received data.
|
||||
if (this->rx_buf_len_ == 0) {
|
||||
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
|
||||
std::vector<uint8_t>().swap(this->rx_buf_);
|
||||
}
|
||||
// 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:
|
||||
// Buffer containing data to be sent
|
||||
@@ -149,9 +162,8 @@ class APIFrameHelper {
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
|
||||
const std::string &info, StateEnum &state, StateEnum failed_state);
|
||||
|
||||
// Pointers first (4 bytes each)
|
||||
socket::Socket *socket_{nullptr};
|
||||
std::unique_ptr<socket::Socket> socket_owned_;
|
||||
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
|
||||
// Common state enum for all frame helpers
|
||||
// Note: Not all states are used by all implementations
|
||||
|
||||
@@ -239,12 +239,13 @@ APIError APINoiseFrameHelper::state_action_() {
|
||||
}
|
||||
if (state_ == State::SERVER_HELLO) {
|
||||
// send server hello
|
||||
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
|
||||
const std::string &name = App.get_name();
|
||||
const std::string &mac = get_mac_address();
|
||||
char mac[mac_len];
|
||||
get_mac_address_into_buffer(mac);
|
||||
|
||||
// Calculate positions and sizes
|
||||
size_t name_len = name.size() + 1; // including null terminator
|
||||
size_t mac_len = mac.size() + 1; // including null terminator
|
||||
size_t name_offset = 1;
|
||||
size_t mac_offset = name_offset + name_len;
|
||||
size_t total_size = 1 + name_len + mac_len;
|
||||
@@ -257,7 +258,7 @@ APIError APINoiseFrameHelper::state_action_() {
|
||||
// node name, terminated by null byte
|
||||
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
|
||||
// node mac, terminated by null byte
|
||||
std::memcpy(msg.get() + mac_offset, mac.c_str(), mac_len);
|
||||
std::memcpy(msg.get() + mac_offset, mac, mac_len);
|
||||
|
||||
aerr = write_frame_(msg.get(), total_size);
|
||||
if (aerr != APIError::OK)
|
||||
@@ -406,8 +407,7 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
return APIError::BAD_DATA_PACKET;
|
||||
}
|
||||
|
||||
buffer->container = std::move(this->rx_buf_);
|
||||
buffer->data_offset = 4;
|
||||
buffer->data = msg_data + 4; // Skip 4-byte header (type + length)
|
||||
buffer->data_len = data_len;
|
||||
buffer->type = type;
|
||||
return APIError::OK;
|
||||
@@ -527,7 +527,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
const auto &psk = ctx_->get_psk();
|
||||
const auto &psk = this->ctx_.get_psk();
|
||||
err = noise_handshakestate_set_pre_shared_key(handshake_, psk.data(), psk.size());
|
||||
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_set_pre_shared_key"),
|
||||
APIError::HANDSHAKESTATE_SETUP_FAILED);
|
||||
|
||||
@@ -9,9 +9,8 @@ namespace esphome::api {
|
||||
|
||||
class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
public:
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx,
|
||||
const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info), ctx_(std::move(ctx)) {
|
||||
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info)
|
||||
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) {
|
||||
// Noise header structure:
|
||||
// Pos 0: indicator (0x01)
|
||||
// Pos 1-2: encrypted payload size (16-bit big-endian)
|
||||
@@ -41,8 +40,8 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
NoiseCipherState *send_cipher_{nullptr};
|
||||
NoiseCipherState *recv_cipher_{nullptr};
|
||||
|
||||
// Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer)
|
||||
std::shared_ptr<APINoiseContext> ctx_;
|
||||
// Reference to noise context (4 bytes on 32-bit)
|
||||
APINoiseContext &ctx_;
|
||||
|
||||
// Vector (12 bytes on 32-bit)
|
||||
std::vector<uint8_t> prologue_;
|
||||
|
||||
@@ -210,8 +210,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
return aerr;
|
||||
}
|
||||
|
||||
buffer->container = std::move(this->rx_buf_);
|
||||
buffer->data_offset = 0;
|
||||
buffer->data = this->rx_buf_.data();
|
||||
buffer->data_len = this->rx_header_parsed_len_;
|
||||
buffer->type = this->rx_header_parsed_type_;
|
||||
return APIError::OK;
|
||||
|
||||
@@ -476,8 +476,8 @@ void ListEntitiesLightResponse::encode(ProtoWriteBuffer buffer) const {
|
||||
}
|
||||
buffer.encode_float(9, this->min_mireds);
|
||||
buffer.encode_float(10, this->max_mireds);
|
||||
for (auto &it : this->effects) {
|
||||
buffer.encode_string(11, it, true);
|
||||
for (const char *it : *this->effects) {
|
||||
buffer.encode_string(11, it, strlen(it), true);
|
||||
}
|
||||
buffer.encode_bool(13, this->disabled_by_default);
|
||||
#ifdef USE_ENTITY_ICON
|
||||
@@ -499,9 +499,9 @@ void ListEntitiesLightResponse::calculate_size(ProtoSize &size) const {
|
||||
}
|
||||
size.add_float(1, this->min_mireds);
|
||||
size.add_float(1, this->max_mireds);
|
||||
if (!this->effects.empty()) {
|
||||
for (const auto &it : this->effects) {
|
||||
size.add_length_force(1, it.size());
|
||||
if (!this->effects->empty()) {
|
||||
for (const char *it : *this->effects) {
|
||||
size.add_length_force(1, strlen(it));
|
||||
}
|
||||
}
|
||||
size.add_bool(1, this->disabled_by_default);
|
||||
@@ -995,7 +995,7 @@ bool GetTimeResponse::decode_32bit(uint32_t field_id, Proto32Bit value) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void ListEntitiesServicesArgument::encode(ProtoWriteBuffer buffer) const {
|
||||
buffer.encode_string(1, this->name_ref_);
|
||||
buffer.encode_uint32(2, static_cast<uint32_t>(this->type));
|
||||
|
||||
@@ -51,6 +51,7 @@ enum SensorStateClass : uint32_t {
|
||||
STATE_CLASS_MEASUREMENT = 1,
|
||||
STATE_CLASS_TOTAL_INCREASING = 2,
|
||||
STATE_CLASS_TOTAL = 3,
|
||||
STATE_CLASS_MEASUREMENT_ANGLE = 4,
|
||||
};
|
||||
#endif
|
||||
enum LogLevel : uint32_t {
|
||||
@@ -63,7 +64,7 @@ enum LogLevel : uint32_t {
|
||||
LOG_LEVEL_VERBOSE = 6,
|
||||
LOG_LEVEL_VERY_VERBOSE = 7,
|
||||
};
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
enum ServiceArgType : uint32_t {
|
||||
SERVICE_ARG_TYPE_BOOL = 0,
|
||||
SERVICE_ARG_TYPE_INT = 1,
|
||||
@@ -793,7 +794,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
|
||||
const light::ColorModeMask *supported_color_modes{};
|
||||
float min_mireds{0.0f};
|
||||
float max_mireds{0.0f};
|
||||
std::vector<std::string> effects{};
|
||||
const FixedVector<const char *> *effects{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1239,7 +1240,7 @@ class GetTimeResponse final : public ProtoDecodableMessage {
|
||||
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
};
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
public:
|
||||
StringRef name_ref_{};
|
||||
|
||||
@@ -66,7 +66,7 @@ static void dump_field(std::string &out, const char *field_name, float value, in
|
||||
static void dump_field(std::string &out, const char *field_name, uint64_t value, int indent = 2) {
|
||||
char buffer[64];
|
||||
append_field_prefix(out, field_name, indent);
|
||||
snprintf(buffer, 64, "%llu", value);
|
||||
snprintf(buffer, 64, "%" PRIu64, value);
|
||||
append_with_newline(out, buffer);
|
||||
}
|
||||
|
||||
@@ -179,6 +179,8 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::Sens
|
||||
return "STATE_CLASS_TOTAL_INCREASING";
|
||||
case enums::STATE_CLASS_TOTAL:
|
||||
return "STATE_CLASS_TOTAL";
|
||||
case enums::STATE_CLASS_MEASUREMENT_ANGLE:
|
||||
return "STATE_CLASS_MEASUREMENT_ANGLE";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -206,7 +208,7 @@ template<> const char *proto_enum_to_string<enums::LogLevel>(enums::LogLevel val
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::ServiceArgType value) {
|
||||
switch (value) {
|
||||
case enums::SERVICE_ARG_TYPE_BOOL:
|
||||
@@ -924,7 +926,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
|
||||
}
|
||||
dump_field(out, "min_mireds", this->min_mireds);
|
||||
dump_field(out, "max_mireds", this->max_mireds);
|
||||
for (const auto &it : this->effects) {
|
||||
for (const auto &it : *this->effects) {
|
||||
dump_field(out, "effects", it, 4);
|
||||
}
|
||||
dump_field(out, "disabled_by_default", this->disabled_by_default);
|
||||
@@ -1177,7 +1179,7 @@ void GetTimeResponse::dump_to(std::string &out) const {
|
||||
out.append(format_hex_pretty(this->timezone, this->timezone_len));
|
||||
out.append("\n");
|
||||
}
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "ListEntitiesServicesArgument");
|
||||
dump_field(out, "name", this->name_ref_);
|
||||
|
||||
@@ -13,7 +13,7 @@ void APIServerConnectionBase::log_send_message_(const char *name, const std::str
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
|
||||
void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
|
||||
switch (msg_type) {
|
||||
case HelloRequest::MESSAGE_TYPE: {
|
||||
HelloRequest msg;
|
||||
@@ -193,7 +193,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
case ExecuteServiceRequest::MESSAGE_TYPE: {
|
||||
ExecuteServiceRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
@@ -670,7 +670,7 @@ void APIServerConnection::on_subscribe_home_assistant_states_request(const Subsc
|
||||
this->subscribe_home_assistant_states(msg);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { this->execute_service(msg); }
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -827,7 +827,7 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th
|
||||
void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); }
|
||||
#endif
|
||||
|
||||
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) {
|
||||
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
|
||||
|
||||
@@ -79,7 +79,7 @@ class APIServerConnectionBase : public ProtoService {
|
||||
|
||||
virtual void on_get_time_response(const GetTimeResponse &value){};
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
virtual void on_execute_service_request(const ExecuteServiceRequest &value){};
|
||||
#endif
|
||||
|
||||
@@ -218,7 +218,7 @@ class APIServerConnectionBase : public ProtoService {
|
||||
virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
|
||||
#endif
|
||||
protected:
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
class APIServerConnection : public APIServerConnectionBase {
|
||||
@@ -239,7 +239,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
virtual void execute_service(const ExecuteServiceRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -368,7 +368,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void on_execute_service_request(const ExecuteServiceRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
@@ -480,7 +480,7 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override;
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -52,11 +52,6 @@ void APIServer::setup() {
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// Schedule reboot if no clients connect within timeout
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
this->schedule_reboot_timeout_();
|
||||
}
|
||||
|
||||
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
|
||||
if (this->socket_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Could not create socket");
|
||||
@@ -101,42 +96,22 @@ void APIServer::setup() {
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
if (logger::global_logger != nullptr) {
|
||||
logger::global_logger->add_on_log_callback(
|
||||
[this](int level, const char *tag, const char *message, size_t message_len) {
|
||||
if (this->shutting_down_) {
|
||||
// Don't try to send logs during shutdown
|
||||
// as it could result in a recursion and
|
||||
// we would be filling a buffer we are trying to clear
|
||||
return;
|
||||
}
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
|
||||
c->try_send_log_message(level, tag, message, message_len);
|
||||
}
|
||||
});
|
||||
logger::global_logger->add_log_listener(this);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
|
||||
camera::Camera::instance()->add_image_callback([this](const std::shared_ptr<camera::CameraImage> &image) {
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->flags_.remove)
|
||||
c->set_camera_state(image);
|
||||
}
|
||||
});
|
||||
camera::Camera::instance()->add_listener(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void APIServer::schedule_reboot_timeout_() {
|
||||
this->status_set_warning();
|
||||
this->set_timeout("api_reboot", this->reboot_timeout_, []() {
|
||||
if (!global_api_server->is_connected()) {
|
||||
ESP_LOGE(TAG, "No clients; rebooting");
|
||||
App.reboot();
|
||||
}
|
||||
});
|
||||
// Initialize last_connected_ for reboot timeout tracking
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
// Set warning status if reboot timeout is enabled
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
this->status_set_warning();
|
||||
}
|
||||
}
|
||||
|
||||
void APIServer::loop() {
|
||||
@@ -164,15 +139,24 @@ void APIServer::loop() {
|
||||
this->clients_.emplace_back(conn);
|
||||
conn->start();
|
||||
|
||||
// Clear warning status and cancel reboot when first client connects
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
|
||||
this->status_clear_warning();
|
||||
this->cancel_timeout("api_reboot");
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this->clients_.empty()) {
|
||||
// Check reboot timeout - done in loop to avoid scheduler heap churn
|
||||
// (cancelled scheduler items sit in heap memory until their scheduled time)
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_connected_ > this->reboot_timeout_) {
|
||||
ESP_LOGE(TAG, "No clients; rebooting");
|
||||
App.reboot();
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -211,9 +195,10 @@ void APIServer::loop() {
|
||||
}
|
||||
this->clients_.pop_back();
|
||||
|
||||
// Schedule reboot when last client disconnects
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
|
||||
this->schedule_reboot_timeout_();
|
||||
this->status_set_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
// Don't increment client_index since we need to process the swapped element
|
||||
}
|
||||
@@ -227,8 +212,8 @@ void APIServer::dump_config() {
|
||||
" Max connections: %u",
|
||||
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
|
||||
#ifdef USE_API_NOISE
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_->has_psk()));
|
||||
if (!this->noise_ctx_->has_psk()) {
|
||||
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
|
||||
if (!this->noise_ctx_.has_psk()) {
|
||||
ESP_LOGCONFIG(TAG, " Supports encryption: YES");
|
||||
}
|
||||
#else
|
||||
@@ -493,7 +478,7 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
ESP_LOGW(TAG, "Key set in YAML");
|
||||
return false;
|
||||
#else
|
||||
auto &old_psk = this->noise_ctx_->get_psk();
|
||||
auto &old_psk = this->noise_ctx_.get_psk();
|
||||
if (std::equal(old_psk.begin(), old_psk.end(), psk.begin())) {
|
||||
ESP_LOGW(TAG, "New PSK matches old");
|
||||
return true;
|
||||
@@ -528,7 +513,42 @@ void APIServer::request_time() {
|
||||
}
|
||||
#endif
|
||||
|
||||
bool APIServer::is_connected() const { return !this->clients_.empty(); }
|
||||
bool APIServer::is_connected(bool state_subscription_only) const {
|
||||
if (!state_subscription_only) {
|
||||
return !this->clients_.empty();
|
||||
}
|
||||
|
||||
for (const auto &client : this->clients_) {
|
||||
if (client->flags_.state_subscription) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
void APIServer::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
if (this->shutting_down_) {
|
||||
// Don't try to send logs during shutdown
|
||||
// as it could result in a recursion and
|
||||
// we would be filling a buffer we are trying to clear
|
||||
return;
|
||||
}
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
|
||||
c->try_send_log_message(level, tag, message, message_len);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_CAMERA
|
||||
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
|
||||
for (auto &c : this->clients_) {
|
||||
if (!c->flags_.remove)
|
||||
c->set_camera_state(image);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
void APIServer::on_shutdown() {
|
||||
this->shutting_down_ = true;
|
||||
|
||||
@@ -12,9 +12,15 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "list_entities.h"
|
||||
#include "subscribe_state.h"
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
#include "esphome/components/camera/camera.h"
|
||||
#endif
|
||||
|
||||
#include <map>
|
||||
#include <vector>
|
||||
@@ -27,7 +33,17 @@ struct SavedNoisePsk {
|
||||
} PACKED; // NOLINT
|
||||
#endif
|
||||
|
||||
class APIServer : public Component, public Controller {
|
||||
class APIServer : public Component,
|
||||
public Controller
|
||||
#ifdef USE_LOGGER
|
||||
,
|
||||
public logger::LogListener
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
,
|
||||
public camera::CameraListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
APIServer();
|
||||
void setup() override;
|
||||
@@ -37,6 +53,12 @@ class APIServer : public Component, public Controller {
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
bool teardown() override;
|
||||
#ifdef USE_LOGGER
|
||||
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override;
|
||||
#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);
|
||||
@@ -54,8 +76,8 @@ class APIServer : public Component, public Controller {
|
||||
#ifdef USE_API_NOISE
|
||||
bool save_noise_psk(psk_t psk, bool make_active = true);
|
||||
bool clear_noise_psk(bool make_active = true);
|
||||
void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
|
||||
std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
|
||||
void set_noise_psk(psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
APINoiseContext &get_noise_ctx() { return this->noise_ctx_; }
|
||||
#endif // USE_API_NOISE
|
||||
|
||||
void handle_disconnect(APIConnection *conn);
|
||||
@@ -124,7 +146,7 @@ class APIServer : public Component, public Controller {
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
|
||||
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
#endif // USE_API_HOMEASSISTANT_SERVICES
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) {
|
||||
this->user_services_.assign(services);
|
||||
}
|
||||
@@ -150,7 +172,7 @@ class APIServer : public Component, public Controller {
|
||||
void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg);
|
||||
#endif
|
||||
|
||||
bool is_connected() const;
|
||||
bool is_connected(bool state_subscription_only = false) const;
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
struct HomeAssistantStateSubscription {
|
||||
@@ -166,7 +188,7 @@ class APIServer : public Component, public Controller {
|
||||
std::function<void(std::string)> f);
|
||||
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
const std::vector<UserServiceDescriptor *> &get_user_services() const { return this->user_services_; }
|
||||
#endif
|
||||
|
||||
@@ -180,7 +202,6 @@ class APIServer : public Component, public Controller {
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void schedule_reboot_timeout_();
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
const psk_t &active_psk, bool make_active);
|
||||
@@ -196,6 +217,7 @@ class APIServer : public Component, public Controller {
|
||||
|
||||
// 4-byte aligned types
|
||||
uint32_t reboot_timeout_{300000};
|
||||
uint32_t last_connected_{0};
|
||||
|
||||
// Vectors and strings (12 bytes each on 32-bit)
|
||||
std::vector<std::unique_ptr<APIConnection>> clients_;
|
||||
@@ -206,7 +228,7 @@ class APIServer : public Component, public Controller {
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
std::vector<HomeAssistantStateSubscription> state_subs_;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
std::vector<UserServiceDescriptor *> user_services_;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
@@ -228,7 +250,7 @@ class APIServer : public Component, public Controller {
|
||||
// 7 bytes used, 1 byte padding
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>();
|
||||
APINoiseContext noise_ctx_;
|
||||
ESPPreferenceObject noise_pref_;
|
||||
#endif // USE_API_NOISE
|
||||
};
|
||||
@@ -236,8 +258,11 @@ class APIServer : public Component, public Controller {
|
||||
extern APIServer *global_api_server; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
template<typename... Ts> class APIConnectedCondition : public Condition<Ts...> {
|
||||
TEMPLATABLE_VALUE(bool, state_subscription_only)
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_api_server->is_connected(); }
|
||||
bool check(const Ts &...x) override {
|
||||
return global_api_server->is_connected(this->state_subscription_only_.value(x...));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
#include <map>
|
||||
#include "api_server.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
#include "user_services.h"
|
||||
#endif
|
||||
namespace esphome::api {
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
template<typename T, typename... Ts> class CustomAPIDeviceService : public UserServiceDynamic<Ts...> {
|
||||
public:
|
||||
CustomAPIDeviceService(const std::string &name, const std::array<std::string, sizeof...(Ts)> &arg_names, T *obj,
|
||||
@@ -21,7 +21,7 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
|
||||
T *obj_;
|
||||
void (T::*callback_)(Ts...);
|
||||
};
|
||||
#endif // USE_API_SERVICES
|
||||
#endif // USE_API_USER_DEFINED_ACTIONS
|
||||
|
||||
class CustomAPIDevice {
|
||||
public:
|
||||
@@ -49,7 +49,7 @@ class CustomAPIDevice {
|
||||
* @param name The name of the service to register.
|
||||
* @param arg_names The name of the arguments for the service, must match the arguments of the function.
|
||||
*/
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
template<typename T, typename... Ts>
|
||||
void register_service(void (T::*callback)(Ts...), const std::string &name,
|
||||
const std::array<std::string, sizeof...(Ts)> &arg_names) {
|
||||
@@ -90,7 +90,7 @@ class CustomAPIDevice {
|
||||
* @param callback The member function to call when the service is triggered.
|
||||
* @param name The name of the arguments for the service, must match the arguments of the function.
|
||||
*/
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||
#ifdef USE_API_CUSTOM_SERVICES
|
||||
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||
|
||||
@@ -82,7 +82,7 @@ bool ListEntitiesIterator::on_end() { return this->client_->send_list_info_done(
|
||||
|
||||
ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(client) {}
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
return this->client_->send_message(resp, ListEntitiesServicesResponse::MESSAGE_TYPE);
|
||||
|
||||
@@ -43,7 +43,7 @@ class ListEntitiesIterator : public ComponentIterator {
|
||||
#ifdef USE_TEXT_SENSOR
|
||||
bool on_text_sensor(text_sensor::TextSensor *entity) override;
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
bool on_service(UserServiceDescriptor *service) override;
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
|
||||
@@ -846,7 +846,7 @@ class ProtoService {
|
||||
*/
|
||||
virtual ProtoWriteBuffer create_buffer(uint32_t reserve_size) = 0;
|
||||
virtual bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) = 0;
|
||||
virtual void read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) = 0;
|
||||
virtual void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) = 0;
|
||||
|
||||
// Optimized method that pre-allocates buffer based on message size
|
||||
bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "api_pb2.h"
|
||||
|
||||
#ifdef USE_API_SERVICES
|
||||
#ifdef USE_API_USER_DEFINED_ACTIONS
|
||||
namespace esphome::api {
|
||||
|
||||
class UserServiceDescriptor {
|
||||
@@ -51,13 +51,14 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
@@ -95,13 +96,14 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
return false;
|
||||
if (req.args.size() != sizeof...(Ts))
|
||||
return false;
|
||||
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
|
||||
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
virtual void execute(Ts... x) = 0;
|
||||
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
|
||||
template<typename ArgsContainer, size_t... S>
|
||||
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
|
||||
this->execute((get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
@@ -122,4 +124,4 @@ template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif // USE_API_SERVICES
|
||||
#endif // USE_API_USER_DEFINED_ACTIONS
|
||||
|
||||
@@ -23,7 +23,7 @@ void BH1900NUXSensor::setup() {
|
||||
i2c::ErrorCode result_code =
|
||||
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
|
||||
if (result_code != i2c::ERROR_OK) {
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#include "automation.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor.automation";
|
||||
|
||||
void binary_sensor::MultiClickTrigger::on_state_(bool state) {
|
||||
void MultiClickTrigger::on_state_(bool state) {
|
||||
// Handle duplicate events
|
||||
if (state == this->last_state_) {
|
||||
return;
|
||||
@@ -67,7 +66,7 @@ void binary_sensor::MultiClickTrigger::on_state_(bool state) {
|
||||
|
||||
*this->at_index_ = *this->at_index_ + 1;
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
|
||||
void MultiClickTrigger::schedule_cooldown_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
|
||||
this->is_in_cooldown_ = true;
|
||||
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
|
||||
@@ -79,7 +78,7 @@ void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
|
||||
this->cancel_timeout("is_valid");
|
||||
this->cancel_timeout("is_not_valid");
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
if (min_length == 0) {
|
||||
this->is_valid_ = true;
|
||||
return;
|
||||
@@ -90,19 +89,19 @@ void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
|
||||
this->is_valid_ = true;
|
||||
});
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->set_timeout("is_not_valid", max_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
});
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::cancel() {
|
||||
void MultiClickTrigger::cancel() {
|
||||
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
}
|
||||
void binary_sensor::MultiClickTrigger::trigger_() {
|
||||
void MultiClickTrigger::trigger_() {
|
||||
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
|
||||
this->at_index_.reset();
|
||||
this->cancel_timeout("trigger");
|
||||
@@ -118,5 +117,4 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
|
||||
return length >= min_length && length <= max_length;
|
||||
}
|
||||
}
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
struct MultiClickTriggerEvent {
|
||||
bool state;
|
||||
@@ -172,5 +171,4 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
|
||||
BinarySensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -3,9 +3,7 @@
|
||||
#include "esphome/core/controller_registry.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "binary_sensor";
|
||||
|
||||
@@ -63,6 +61,4 @@ void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
|
||||
}
|
||||
bool BinarySensor::is_status_binary_sensor() const { return false; }
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -6,9 +6,7 @@
|
||||
|
||||
#include <initializer_list>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
class BinarySensor;
|
||||
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
|
||||
@@ -70,5 +68,4 @@ class BinarySensorInitiallyOff : public BinarySensor {
|
||||
bool has_state() const override { return true; }
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
#include "binary_sensor.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
static const char *const TAG = "sensor.filter";
|
||||
|
||||
@@ -132,6 +130,4 @@ optional<bool> SettleFilter::new_value(bool value) {
|
||||
|
||||
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome {
|
||||
|
||||
namespace binary_sensor {
|
||||
namespace esphome::binary_sensor {
|
||||
|
||||
class BinarySensor;
|
||||
|
||||
@@ -139,6 +137,4 @@ class SettleFilter : public Filter, public Component {
|
||||
bool steady_{true};
|
||||
};
|
||||
|
||||
} // namespace binary_sensor
|
||||
|
||||
} // namespace esphome
|
||||
} // namespace esphome::binary_sensor
|
||||
|
||||
@@ -9,7 +9,7 @@ static const char *const TAG = "bl0940.number";
|
||||
void CalibrationNumber::setup() {
|
||||
float value = 0.0f;
|
||||
if (this->restore_value_) {
|
||||
this->pref_ = global_preferences->make_preference<float>(this->get_object_id_hash());
|
||||
this->pref_ = global_preferences->make_preference<float>(this->get_preference_hash());
|
||||
if (!this->pref_.load(&value)) {
|
||||
value = 0.0f;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ from esphome.const import (
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_VALUE,
|
||||
)
|
||||
from esphome.core import ID
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_client"]
|
||||
CODEOWNERS = ["@buxtronix", "@clydebarrow"]
|
||||
@@ -198,7 +199,12 @@ async def ble_write_to_code(config, action_id, template_arg, args):
|
||||
templ = await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
|
||||
cg.add(var.set_value_template(templ))
|
||||
else:
|
||||
cg.add(var.set_value_simple(value))
|
||||
# Generate static array in flash to avoid RAM copy
|
||||
if isinstance(value, bytes):
|
||||
value = list(value)
|
||||
arr_id = ID(f"{action_id}_data", is_declaration=True, type=cg.uint8)
|
||||
arr = cg.static_const_array(arr_id, cg.ArrayInitializer(*value))
|
||||
cg.add(var.set_value_simple(arr, len(value)))
|
||||
|
||||
if len(config[CONF_SERVICE_UUID]) == len(esp32_ble_tracker.bt_uuid16_format):
|
||||
cg.add(
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
#include "automation.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
const char *const Automation::TAG = "ble_client.automation";
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -9,8 +9,7 @@
|
||||
#include "esphome/components/ble_client/ble_client.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
// placeholder class for static TAG .
|
||||
class Automation {
|
||||
@@ -96,11 +95,8 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
BLEClientWriteAction(BLEClient *ble_client) {
|
||||
ble_client->register_ble_node(this);
|
||||
ble_client_ = ble_client;
|
||||
this->construct_simple_value_();
|
||||
}
|
||||
|
||||
~BLEClientWriteAction() { this->destroy_simple_value_(); }
|
||||
|
||||
void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
|
||||
void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
|
||||
void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
@@ -110,17 +106,14 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
|
||||
|
||||
void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
|
||||
this->destroy_simple_value_();
|
||||
this->value_.template_func = func;
|
||||
this->has_simple_value_ = false;
|
||||
this->value_.func = func;
|
||||
this->len_ = -1; // Sentinel value indicates template mode
|
||||
}
|
||||
|
||||
void set_value_simple(const std::vector<uint8_t> &value) {
|
||||
if (!this->has_simple_value_) {
|
||||
this->construct_simple_value_();
|
||||
}
|
||||
this->value_.simple = value;
|
||||
this->has_simple_value_ = true;
|
||||
// Store pointer to static data in flash (no RAM copy)
|
||||
void set_value_simple(const uint8_t *data, size_t len) {
|
||||
this->value_.data = data;
|
||||
this->len_ = len; // Length >= 0 indicates static mode
|
||||
}
|
||||
|
||||
void play(const Ts &...x) override {}
|
||||
@@ -128,9 +121,19 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
void play_complex(const Ts &...x) override {
|
||||
this->num_running_++;
|
||||
this->var_ = std::make_tuple(x...);
|
||||
auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
|
||||
|
||||
bool result;
|
||||
if (this->len_ >= 0) {
|
||||
// Static mode: write directly from flash pointer
|
||||
result = this->write(this->value_.data, this->len_);
|
||||
} else {
|
||||
// Template mode: call function and write the vector
|
||||
std::vector<uint8_t> value = this->value_.func(x...);
|
||||
result = this->write(value);
|
||||
}
|
||||
|
||||
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
|
||||
if (!write(value))
|
||||
if (!result)
|
||||
this->play_next_(x...);
|
||||
}
|
||||
|
||||
@@ -143,15 +146,15 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
* errors.
|
||||
*/
|
||||
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
|
||||
bool write(const std::vector<uint8_t> &value) {
|
||||
bool write(const uint8_t *data, size_t len) {
|
||||
if (this->node_state != espbt::ClientState::ESTABLISHED) {
|
||||
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
|
||||
return false;
|
||||
}
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
|
||||
esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
|
||||
this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
|
||||
this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str());
|
||||
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);
|
||||
if (err != ESP_OK) {
|
||||
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
|
||||
return false;
|
||||
@@ -159,6 +162,8 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
return true;
|
||||
}
|
||||
|
||||
bool write(const std::vector<uint8_t> &value) { return this->write(value.data(), value.size()); }
|
||||
|
||||
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) override {
|
||||
switch (event) {
|
||||
@@ -192,7 +197,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
|
||||
ble_client_->address_str().c_str());
|
||||
ble_client_->address_str());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -201,21 +206,11 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
|
||||
}
|
||||
|
||||
private:
|
||||
void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); }
|
||||
|
||||
void destroy_simple_value_() {
|
||||
if (this->has_simple_value_) {
|
||||
this->value_.simple.~vector();
|
||||
}
|
||||
}
|
||||
|
||||
BLEClient *ble_client_;
|
||||
bool has_simple_value_ = true;
|
||||
ssize_t len_{-1}; // -1 = template mode, >=0 = static mode with length
|
||||
union Value {
|
||||
std::vector<uint8_t> simple;
|
||||
std::vector<uint8_t> (*template_func)(Ts...);
|
||||
Value() {} // trivial constructor
|
||||
~Value() {} // trivial destructor - we manage lifetime via discriminator
|
||||
std::vector<uint8_t> (*func)(Ts...); // Function pointer (stateless lambdas)
|
||||
const uint8_t *data; // Pointer to static data in flash
|
||||
} value_;
|
||||
espbt::ESPBTUUID service_uuid_;
|
||||
espbt::ESPBTUUID char_uuid_;
|
||||
@@ -395,7 +390,6 @@ template<typename... Ts> class BLEClientDisconnectAction : public Action<Ts...>,
|
||||
BLEClient *ble_client_;
|
||||
std::tuple<Ts...> var_{};
|
||||
};
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_client";
|
||||
|
||||
@@ -39,7 +38,7 @@ void BLEClient::set_enabled(bool enabled) {
|
||||
return;
|
||||
this->enabled = enabled;
|
||||
if (!enabled) {
|
||||
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
|
||||
ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str());
|
||||
this->disconnect();
|
||||
}
|
||||
}
|
||||
@@ -82,7 +81,6 @@ bool BLEClient::all_nodes_established_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -75,7 +74,6 @@ class BLEClient : public BLEClientBase {
|
||||
std::vector<BLEClientNode *> nodes_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_binary_output";
|
||||
|
||||
@@ -14,7 +13,7 @@ void BLEBinaryOutput::dump_config() {
|
||||
" MAC address : %s\n"
|
||||
" Service UUID : %s\n"
|
||||
" Characteristic UUID: %s",
|
||||
this->parent_->address_str().c_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->parent_->address_str(), this->service_uuid_.to_string().c_str(),
|
||||
this->char_uuid_.to_string().c_str());
|
||||
LOG_BINARY_OUTPUT(this);
|
||||
}
|
||||
@@ -44,7 +43,7 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
}
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
|
||||
this->parent()->address_str().c_str());
|
||||
this->parent()->address_str());
|
||||
this->node_state = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
@@ -75,6 +74,5 @@ void BLEBinaryOutput::write_state(bool state) {
|
||||
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err);
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -36,7 +35,6 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi
|
||||
esp_gatt_write_type_t write_type_{};
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
class BLESensorNotifyTrigger : public Trigger<float>, public BLESensor {
|
||||
public:
|
||||
@@ -35,7 +34,6 @@ class BLESensorNotifyTrigger : public Trigger<float>, public BLESensor {
|
||||
BLESensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_rssi_sensor";
|
||||
|
||||
@@ -19,7 +18,7 @@ void BLEClientRSSISensor::loop() {
|
||||
|
||||
void BLEClientRSSISensor::dump_config() {
|
||||
LOG_SENSOR("", "BLE Client RSSI Sensor", this);
|
||||
ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str().c_str());
|
||||
ESP_LOGCONFIG(TAG, " MAC address : %s", this->parent()->address_str());
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
@@ -69,15 +68,14 @@ void BLEClientRSSISensor::update() {
|
||||
this->get_rssi_();
|
||||
}
|
||||
void BLEClientRSSISensor::get_rssi_() {
|
||||
ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str().c_str());
|
||||
ESP_LOGV(TAG, "requesting rssi from %s", this->parent()->address_str());
|
||||
auto status = esp_ble_gap_read_rssi(this->parent()->get_remote_bda());
|
||||
if (status != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str().c_str(), status);
|
||||
ESP_LOGW(TAG, "esp_ble_gap_read_rssi error, address=%s, status=%d", this->parent()->address_str(), status);
|
||||
this->status_set_warning();
|
||||
this->publish_state(NAN);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -29,6 +28,5 @@ class BLEClientRSSISensor : public sensor::Sensor, public PollingComponent, publ
|
||||
bool should_update_{false};
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_sensor";
|
||||
|
||||
@@ -25,7 +24,7 @@ void BLESensor::dump_config() {
|
||||
" Characteristic UUID: %s\n"
|
||||
" Descriptor UUID : %s\n"
|
||||
" Notifications : %s",
|
||||
this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(),
|
||||
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_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
@@ -147,6 +146,5 @@ void BLESensor::update() {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -10,8 +10,7 @@
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -48,6 +47,5 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
|
||||
espbt::ESPBTUUID descr_uuid_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_switch";
|
||||
|
||||
@@ -31,6 +30,5 @@ void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
|
||||
void BLEClientSwitch::dump_config() { LOG_SWITCH("", "BLE Client Switch", this); }
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -24,6 +23,5 @@ class BLEClientSwitch : public switch_::Switch, public Component, public BLEClie
|
||||
void write_state(bool state) override;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -5,8 +5,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
class BLETextSensorNotifyTrigger : public Trigger<std::string>, public BLETextSensor {
|
||||
public:
|
||||
@@ -33,7 +32,6 @@ class BLETextSensorNotifyTrigger : public Trigger<std::string>, public BLETextSe
|
||||
BLETextSensor *sensor_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
#endif
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
static const char *const TAG = "ble_text_sensor";
|
||||
|
||||
@@ -28,7 +27,7 @@ void BLETextSensor::dump_config() {
|
||||
" Characteristic UUID: %s\n"
|
||||
" Descriptor UUID : %s\n"
|
||||
" Notifications : %s",
|
||||
this->parent()->address_str().c_str(), this->service_uuid_.to_string().c_str(),
|
||||
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_));
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
@@ -138,6 +137,5 @@ void BLETextSensor::update() {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
#ifdef USE_ESP32
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace ble_client {
|
||||
namespace esphome::ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -40,6 +39,5 @@ class BLETextSensor : public text_sensor::TextSensor, public PollingComponent, p
|
||||
espbt::ESPBTUUID descr_uuid_;
|
||||
};
|
||||
|
||||
} // namespace ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::ble_client
|
||||
#endif
|
||||
|
||||
@@ -87,17 +87,21 @@ void BLENUS::setup() {
|
||||
global_ble_nus = this;
|
||||
#ifdef USE_LOGGER
|
||||
if (logger::global_logger != nullptr && this->expose_log_) {
|
||||
logger::global_logger->add_on_log_callback(
|
||||
[this](int level, const char *tag, const char *message, size_t message_len) {
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
|
||||
const char c = '\n';
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
|
||||
});
|
||||
logger::global_logger->add_log_listener(this);
|
||||
}
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef USE_LOGGER
|
||||
void BLENUS::on_log(uint8_t level, const char *tag, const char *message, size_t message_len) {
|
||||
(void) level;
|
||||
(void) tag;
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
|
||||
const char c = '\n';
|
||||
this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
void BLENUS::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ble nus:");
|
||||
ESP_LOGCONFIG(TAG, " log: %s", YESNO(this->expose_log_));
|
||||
|
||||
@@ -2,12 +2,20 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/component.h"
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
#include <shell/shell_bt_nus.h>
|
||||
#include <atomic>
|
||||
|
||||
namespace esphome::ble_nus {
|
||||
|
||||
class BLENUS : public Component {
|
||||
class BLENUS : public Component
|
||||
#ifdef USE_LOGGER
|
||||
,
|
||||
public logger::LogListener
|
||||
#endif
|
||||
{
|
||||
enum TxStatus {
|
||||
TX_DISABLED,
|
||||
TX_ENABLED,
|
||||
@@ -20,6 +28,9 @@ class BLENUS : public Component {
|
||||
void loop() override;
|
||||
size_t write_array(const uint8_t *data, size_t len);
|
||||
void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
|
||||
#ifdef USE_LOGGER
|
||||
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
static void send_enabled_callback(bt_nus_send_status status);
|
||||
|
||||
@@ -196,8 +196,8 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
|
||||
if (service_status != ESP_GATT_OK || service_count == 0) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service %s, status=%d, service_count=%d, offset=%d",
|
||||
this->connection_index_, this->address_str().c_str(),
|
||||
service_status != ESP_GATT_OK ? "error" : "missing", service_status, service_count, this->send_service_);
|
||||
this->connection_index_, this->address_str(), service_status != ESP_GATT_OK ? "error" : "missing",
|
||||
service_status, service_count, this->send_service_);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
@@ -312,13 +312,13 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
if (resp.services.size() > 1) {
|
||||
resp.services.pop_back();
|
||||
ESP_LOGD(TAG, "[%d] [%s] Service %d would exceed limit (current: %d + service: %d > %d), sending current batch",
|
||||
this->connection_index_, this->address_str().c_str(), this->send_service_, current_size, service_size,
|
||||
this->connection_index_, this->address_str(), this->send_service_, current_size, service_size,
|
||||
MAX_PACKET_SIZE);
|
||||
// Don't increment send_service_ - we'll retry this service in next batch
|
||||
} else {
|
||||
// This single service is too large, but we have to send it anyway
|
||||
ESP_LOGV(TAG, "[%d] [%s] Service %d is too large (%d bytes) but sending anyway", this->connection_index_,
|
||||
this->address_str().c_str(), this->send_service_, service_size);
|
||||
this->address_str(), this->send_service_, service_size);
|
||||
// Increment so we don't get stuck
|
||||
this->send_service_++;
|
||||
}
|
||||
@@ -337,21 +337,20 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation,
|
||||
status);
|
||||
ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str(), operation, status);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err);
|
||||
ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str(), operation, err);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(),
|
||||
action, type);
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str(), action,
|
||||
type);
|
||||
}
|
||||
|
||||
void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(),
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str(),
|
||||
operation, handle, status);
|
||||
}
|
||||
|
||||
@@ -372,14 +371,14 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
// Don't reset connection yet - wait for CLOSE_EVT to ensure controller has freed resources
|
||||
// This prevents race condition where we mark slot as free before controller cleanup is complete
|
||||
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_.c_str(),
|
||||
ESP_LOGD(TAG, "[%d] [%s] Disconnect, reason=0x%02x", this->connection_index_, this->address_str_,
|
||||
param->disconnect.reason);
|
||||
// Send disconnection notification but don't free the slot yet
|
||||
this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_.c_str(),
|
||||
ESP_LOGD(TAG, "[%d] [%s] Close, reason=0x%02x, freeing slot", this->connection_index_, this->address_str_,
|
||||
param->close.reason);
|
||||
// Now the GATT connection is fully closed and controller resources are freed
|
||||
// Safe to mark the connection slot as available
|
||||
@@ -463,7 +462,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_.c_str(),
|
||||
ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_NOTIFY_EVT: handle=0x%2X", this->connection_index_, this->address_str_,
|
||||
param->notify.handle);
|
||||
api::BluetoothGATTNotifyDataResponse resp;
|
||||
resp.address = this->address_;
|
||||
@@ -502,8 +501,7 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
|
||||
@@ -515,8 +513,7 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const uint8
|
||||
this->log_gatt_not_connected_("write", "characteristic");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
|
||||
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
|
||||
@@ -532,8 +529,7 @@ esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
|
||||
this->log_gatt_not_connected_("read", "descriptor");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
|
||||
@@ -544,8 +540,7 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const uint8_t *
|
||||
this->log_gatt_not_connected_("write", "descriptor");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
|
||||
handle);
|
||||
ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_, handle);
|
||||
|
||||
// ESP-IDF's API requires a non-const uint8_t* but it doesn't modify the data
|
||||
// The BTC layer immediately copies the data to its own buffer (see btc_gattc.c)
|
||||
@@ -564,13 +559,13 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
|
||||
|
||||
if (enable) {
|
||||
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_.c_str(), handle);
|
||||
this->address_str_, handle);
|
||||
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_.c_str(), handle);
|
||||
this->address_str_, handle);
|
||||
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err);
|
||||
}
|
||||
|
||||
@@ -27,11 +27,13 @@ void BluetoothProxy::setup() {
|
||||
// Capture the configured scan mode from YAML before any API changes
|
||||
this->configured_scan_active_ = this->parent_->get_scan_active();
|
||||
|
||||
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
});
|
||||
this->parent_->add_scanner_state_listener(this);
|
||||
}
|
||||
|
||||
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state) {
|
||||
@@ -47,12 +49,11 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
|
||||
|
||||
void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(),
|
||||
connection->address_str().c_str(), espbt::client_state_to_string(state));
|
||||
connection->address_str(), espbt::client_state_to_string(state));
|
||||
}
|
||||
|
||||
void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(),
|
||||
message);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str(), message);
|
||||
}
|
||||
|
||||
void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) {
|
||||
@@ -186,7 +187,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
}
|
||||
if (!msg.has_address_type) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] Missing address type in connect request", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
connection->address_str());
|
||||
this->send_device_connection(msg.address, false);
|
||||
return;
|
||||
}
|
||||
@@ -199,7 +200,7 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
} else if (connection->state() == espbt::ClientState::CONNECTING) {
|
||||
if (connection->disconnect_pending()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
connection->get_connection_index(), connection->address_str());
|
||||
connection->cancel_pending_disconnect();
|
||||
return;
|
||||
}
|
||||
@@ -339,7 +340,7 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
|
||||
return;
|
||||
}
|
||||
if (!connection->service_count_) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str());
|
||||
ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str());
|
||||
this->send_gatt_services_done(msg.address);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
|
||||
SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0,
|
||||
};
|
||||
|
||||
class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
|
||||
public esp32_ble_tracker::BLEScannerStateListener,
|
||||
public Component {
|
||||
friend class BluetoothConnection; // Allow connection to update connections_free_response_
|
||||
public:
|
||||
BluetoothProxy();
|
||||
@@ -108,6 +110,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
|
||||
void set_active(bool active) { this->active_ = active; }
|
||||
bool has_active() { return this->active_; }
|
||||
|
||||
/// BLEScannerStateListener interface
|
||||
void on_scanner_state(esp32_ble_tracker::ScannerState state) override;
|
||||
|
||||
uint32_t get_legacy_version() const {
|
||||
if (this->active_) {
|
||||
return LEGACY_ACTIVE_CONNECTIONS_VERSION;
|
||||
@@ -130,11 +135,13 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, publ
|
||||
return flags;
|
||||
}
|
||||
|
||||
std::string get_bluetooth_mac_address_pretty() {
|
||||
void get_bluetooth_mac_address_pretty(std::span<char, 18> output) {
|
||||
const uint8_t *mac = esp_bt_dev_get_address();
|
||||
char buf[18];
|
||||
format_mac_addr_upper(mac, buf);
|
||||
return std::string(buf);
|
||||
if (mac != nullptr) {
|
||||
format_mac_addr_upper(mac, output.data());
|
||||
} else {
|
||||
output[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
1
esphome/components/bm8563/__init__.py
Normal file
1
esphome/components/bm8563/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@abmantis"]
|
||||
198
esphome/components/bm8563/bm8563.cpp
Normal file
198
esphome/components/bm8563/bm8563.cpp
Normal file
@@ -0,0 +1,198 @@
|
||||
#include "bm8563.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::bm8563 {
|
||||
|
||||
static const char *const TAG = "bm8563";
|
||||
|
||||
static constexpr uint8_t CONTROL_STATUS_1_REG = 0x00;
|
||||
static constexpr uint8_t CONTROL_STATUS_2_REG = 0x01;
|
||||
static constexpr uint8_t TIME_FIRST_REG = 0x02; // Time uses reg 2, 3, 4
|
||||
static constexpr uint8_t DATE_FIRST_REG = 0x05; // Date uses reg 5, 6, 7, 8
|
||||
static constexpr uint8_t TIMER_CONTROL_REG = 0x0E;
|
||||
static constexpr uint8_t TIMER_VALUE_REG = 0x0F;
|
||||
static constexpr uint8_t CLOCK_1_HZ = 0x82;
|
||||
static constexpr uint8_t CLOCK_1_60_HZ = 0x83;
|
||||
// Maximum duration: 255 minutes (at 1/60 Hz) = 15300 seconds
|
||||
static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60;
|
||||
|
||||
void BM8563::setup() {
|
||||
if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) {
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void BM8563::update() { this->read_time(); }
|
||||
|
||||
void BM8563::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BM8563:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
}
|
||||
|
||||
void BM8563::start_timer(uint32_t duration_s) {
|
||||
this->clear_irq_();
|
||||
this->set_timer_irq_(duration_s);
|
||||
}
|
||||
|
||||
void BM8563::write_time() {
|
||||
auto now = time::RealTimeClock::utcnow();
|
||||
if (!now.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid system time, not syncing to RTC.");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Writing time: %i-%i-%i %i, %i:%i:%i", now.year, now.month, now.day_of_month, now.day_of_week, now.hour,
|
||||
now.minute, now.second);
|
||||
|
||||
this->set_time_(now);
|
||||
this->set_date_(now);
|
||||
}
|
||||
|
||||
void BM8563::read_time() {
|
||||
ESPTime rtc_time;
|
||||
this->get_time_(rtc_time);
|
||||
this->get_date_(rtc_time);
|
||||
rtc_time.day_of_year = 1; // unused by recalc_timestamp_utc, but needs to be valid
|
||||
ESP_LOGD(TAG, "Read time: %i-%i-%i %i, %i:%i:%i", rtc_time.year, rtc_time.month, rtc_time.day_of_month,
|
||||
rtc_time.day_of_week, rtc_time.hour, rtc_time.minute, rtc_time.second);
|
||||
|
||||
rtc_time.recalc_timestamp_utc(false);
|
||||
if (!rtc_time.is_valid()) {
|
||||
ESP_LOGE(TAG, "Invalid RTC time, not syncing to system clock.");
|
||||
return;
|
||||
}
|
||||
time::RealTimeClock::synchronize_epoch_(rtc_time.timestamp);
|
||||
}
|
||||
|
||||
uint8_t BM8563::bcd2_to_byte_(uint8_t value) {
|
||||
const uint8_t tmp = ((value & 0xF0) >> 0x4) * 10;
|
||||
return tmp + (value & 0x0F);
|
||||
}
|
||||
|
||||
uint8_t BM8563::byte_to_bcd2_(uint8_t value) {
|
||||
const uint8_t bcdhigh = value / 10;
|
||||
value -= bcdhigh * 10;
|
||||
return (bcdhigh << 4) | value;
|
||||
}
|
||||
|
||||
void BM8563::get_time_(ESPTime &time) {
|
||||
uint8_t buf[3] = {0};
|
||||
this->read_register(TIME_FIRST_REG, buf, 3);
|
||||
|
||||
time.second = this->bcd2_to_byte_(buf[0] & 0x7f);
|
||||
time.minute = this->bcd2_to_byte_(buf[1] & 0x7f);
|
||||
time.hour = this->bcd2_to_byte_(buf[2] & 0x3f);
|
||||
}
|
||||
|
||||
void BM8563::set_time_(const ESPTime &time) {
|
||||
uint8_t buf[3] = {this->byte_to_bcd2_(time.second), this->byte_to_bcd2_(time.minute), this->byte_to_bcd2_(time.hour)};
|
||||
this->write_register_(TIME_FIRST_REG, buf, 3);
|
||||
}
|
||||
|
||||
void BM8563::get_date_(ESPTime &time) {
|
||||
uint8_t buf[4] = {0};
|
||||
this->read_register(DATE_FIRST_REG, buf, sizeof(buf));
|
||||
|
||||
time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f);
|
||||
time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07);
|
||||
time.month = this->bcd2_to_byte_(buf[2] & 0x1f);
|
||||
|
||||
uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff);
|
||||
|
||||
if (buf[2] & 0x80) {
|
||||
time.year = 1900 + year_byte;
|
||||
} else {
|
||||
time.year = 2000 + year_byte;
|
||||
}
|
||||
}
|
||||
|
||||
void BM8563::set_date_(const ESPTime &time) {
|
||||
uint8_t buf[4] = {
|
||||
this->byte_to_bcd2_(time.day_of_month),
|
||||
this->byte_to_bcd2_(time.day_of_week),
|
||||
this->byte_to_bcd2_(time.month),
|
||||
this->byte_to_bcd2_(time.year % 100),
|
||||
};
|
||||
|
||||
if (time.year < 2000) {
|
||||
buf[2] = buf[2] | 0x80;
|
||||
}
|
||||
|
||||
this->write_register_(DATE_FIRST_REG, buf, 4);
|
||||
}
|
||||
|
||||
void BM8563::write_byte_(uint8_t reg, uint8_t value) {
|
||||
if (!this->write_byte(reg, value)) {
|
||||
ESP_LOGE(TAG, "Failed to write byte 0x%02X with value 0x%02X", reg, value);
|
||||
}
|
||||
}
|
||||
|
||||
void BM8563::write_register_(uint8_t reg, const uint8_t *data, size_t len) {
|
||||
if (auto error = this->write_register(reg, data, len); error != i2c::ErrorCode::NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to write register 0x%02X with %zu bytes", reg, len);
|
||||
}
|
||||
}
|
||||
|
||||
optional<uint8_t> BM8563::read_register_(uint8_t reg) {
|
||||
uint8_t data;
|
||||
if (auto error = this->read_register(reg, &data, 1); error != i2c::ErrorCode::NO_ERROR) {
|
||||
ESP_LOGE(TAG, "Failed to read register 0x%02X", reg);
|
||||
return {};
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
void BM8563::set_timer_irq_(uint32_t duration_s) {
|
||||
ESP_LOGI(TAG, "Timer Duration: %u s", duration_s);
|
||||
|
||||
if (duration_s > MAX_TIMER_DURATION_S) {
|
||||
ESP_LOGW(TAG, "Timer duration %u s exceeds maximum %u s", duration_s, MAX_TIMER_DURATION_S);
|
||||
return;
|
||||
}
|
||||
|
||||
if (duration_s > 255) {
|
||||
uint8_t duration_minutes = duration_s / 60;
|
||||
this->write_byte_(TIMER_VALUE_REG, duration_minutes);
|
||||
this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_60_HZ);
|
||||
} else {
|
||||
this->write_byte_(TIMER_VALUE_REG, duration_s);
|
||||
this->write_byte_(TIMER_CONTROL_REG, CLOCK_1_HZ);
|
||||
}
|
||||
|
||||
auto maybe_ctrl_status_2 = this->read_register_(CONTROL_STATUS_2_REG);
|
||||
if (!maybe_ctrl_status_2.has_value()) {
|
||||
ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG");
|
||||
return;
|
||||
}
|
||||
uint8_t ctrl_status_2_reg_value = maybe_ctrl_status_2.value();
|
||||
ctrl_status_2_reg_value |= (1 << 0);
|
||||
ctrl_status_2_reg_value &= ~(1 << 7);
|
||||
this->write_byte_(CONTROL_STATUS_2_REG, ctrl_status_2_reg_value);
|
||||
}
|
||||
|
||||
void BM8563::clear_irq_() {
|
||||
auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG);
|
||||
if (!maybe_data.has_value()) {
|
||||
ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG");
|
||||
return;
|
||||
}
|
||||
uint8_t data = maybe_data.value();
|
||||
this->write_byte_(CONTROL_STATUS_2_REG, data & 0xf3);
|
||||
}
|
||||
|
||||
void BM8563::disable_irq_() {
|
||||
this->clear_irq_();
|
||||
auto maybe_data = this->read_register_(CONTROL_STATUS_2_REG);
|
||||
if (!maybe_data.has_value()) {
|
||||
ESP_LOGE(TAG, "Failed to read CONTROL_STATUS_2_REG");
|
||||
return;
|
||||
}
|
||||
uint8_t data = maybe_data.value();
|
||||
this->write_byte_(CONTROL_STATUS_2_REG, data & 0xfc);
|
||||
}
|
||||
|
||||
} // namespace esphome::bm8563
|
||||
57
esphome/components/bm8563/bm8563.h
Normal file
57
esphome/components/bm8563/bm8563.h
Normal file
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/time/real_time_clock.h"
|
||||
|
||||
namespace esphome::bm8563 {
|
||||
|
||||
class BM8563 : public time::RealTimeClock, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void update() override;
|
||||
void dump_config() override;
|
||||
|
||||
void write_time();
|
||||
void read_time();
|
||||
void start_timer(uint32_t duration_s);
|
||||
|
||||
private:
|
||||
void get_time_(ESPTime &time);
|
||||
void get_date_(ESPTime &time);
|
||||
|
||||
void set_time_(const ESPTime &time);
|
||||
void set_date_(const ESPTime &time);
|
||||
|
||||
void set_timer_irq_(uint32_t duration_s);
|
||||
void clear_irq_();
|
||||
void disable_irq_();
|
||||
|
||||
void write_byte_(uint8_t reg, uint8_t value);
|
||||
void write_register_(uint8_t reg, const uint8_t *data, size_t len);
|
||||
optional<uint8_t> read_register_(uint8_t reg);
|
||||
|
||||
uint8_t bcd2_to_byte_(uint8_t value);
|
||||
uint8_t byte_to_bcd2_(uint8_t value);
|
||||
};
|
||||
|
||||
template<typename... Ts> class WriteAction : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->write_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class ReadAction : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
void play(const Ts &...x) override { this->parent_->read_time(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class TimerAction : public Action<Ts...>, public Parented<BM8563> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint32_t, duration)
|
||||
|
||||
void play(const Ts &...x) override {
|
||||
auto duration = this->duration_.value(x...);
|
||||
this->parent_->start_timer(duration);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::bm8563
|
||||
80
esphome/components/bm8563/time.py
Normal file
80
esphome/components/bm8563/time.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, time
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_DURATION, CONF_ID
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
I2C_ADDR = 0x51
|
||||
|
||||
bm8563_ns = cg.esphome_ns.namespace("bm8563")
|
||||
BM8563 = bm8563_ns.class_("BM8563", time.RealTimeClock, i2c.I2CDevice)
|
||||
WriteAction = bm8563_ns.class_("WriteAction", automation.Action)
|
||||
ReadAction = bm8563_ns.class_("ReadAction", automation.Action)
|
||||
TimerAction = bm8563_ns.class_("TimerAction", automation.Action)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
time.TIME_SCHEMA.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(BM8563),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(i2c.i2c_device_schema(I2C_ADDR))
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bm8563.write_time",
|
||||
WriteAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def bm8563_write_time_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bm8563.start_timer",
|
||||
TimerAction,
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
cv.Required(CONF_DURATION): cv.templatable(cv.positive_time_period_seconds),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def bm8563_start_timer_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_DURATION], args, cg.uint32)
|
||||
cg.add(var.set_duration(template_))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"bm8563.read_time",
|
||||
ReadAction,
|
||||
automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(BM8563),
|
||||
}
|
||||
),
|
||||
)
|
||||
async def bm8563_read_time_to_code(config, action_id, template_arg, args):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
await time.register_time(var, config)
|
||||
@@ -100,18 +100,18 @@ void BME280Component::setup() {
|
||||
|
||||
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != 0x60) {
|
||||
this->error_code_ = WRONG_CHIP_ID;
|
||||
this->mark_failed(BME280_ERROR_WRONG_CHIP_ID);
|
||||
this->mark_failed(LOG_STR(BME280_ERROR_WRONG_CHIP_ID));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a soft reset.
|
||||
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
|
||||
this->mark_failed("Reset failed");
|
||||
this->mark_failed(LOG_STR("Reset failed"));
|
||||
return;
|
||||
}
|
||||
// Wait until the NVM data has finished loading.
|
||||
@@ -120,12 +120,12 @@ void BME280Component::setup() {
|
||||
do { // NOLINT
|
||||
delay(2);
|
||||
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
|
||||
this->mark_failed("Error reading status register");
|
||||
this->mark_failed(LOG_STR("Error reading status register"));
|
||||
return;
|
||||
}
|
||||
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
|
||||
if (status & BME280_STATUS_IM_UPDATE) {
|
||||
this->mark_failed("Timeout loading NVM");
|
||||
this->mark_failed(LOG_STR("Timeout loading NVM"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -153,26 +153,26 @@ void BME280Component::setup() {
|
||||
|
||||
uint8_t humid_control_val = 0;
|
||||
if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
|
||||
this->mark_failed("Read humidity control");
|
||||
this->mark_failed(LOG_STR("Read humidity control"));
|
||||
return;
|
||||
}
|
||||
humid_control_val &= ~0b00000111;
|
||||
humid_control_val |= this->humidity_oversampling_ & 0b111;
|
||||
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
|
||||
this->mark_failed("Write humidity control");
|
||||
this->mark_failed(LOG_STR("Write humidity control"));
|
||||
return;
|
||||
}
|
||||
|
||||
uint8_t config_register = 0;
|
||||
if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
|
||||
this->mark_failed("Read config");
|
||||
this->mark_failed(LOG_STR("Read config"));
|
||||
return;
|
||||
}
|
||||
config_register &= ~0b11111100;
|
||||
config_register |= 0b101 << 5; // 1000 ms standby time
|
||||
config_register |= (this->iir_filter_ & 0b111) << 2;
|
||||
if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
|
||||
this->mark_failed("Write config");
|
||||
this->mark_failed(LOG_STR("Write config"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,9 @@ void BME68xBSEC2Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, "Communication failed (BSEC2 status: %d, BME68X status: %d)", this->bsec_status_,
|
||||
this->bme68x_status_);
|
||||
if (this->bsec_status_ == BSEC_I_SU_SUBSCRIBEDOUTPUTGATES) {
|
||||
ESP_LOGE(TAG, "No sensors, add at least one sensor to the config");
|
||||
}
|
||||
}
|
||||
|
||||
if (this->algorithm_output_ != ALGORITHM_OUTPUT_IAQ) {
|
||||
|
||||
@@ -65,23 +65,23 @@ void BMP280Component::setup() {
|
||||
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
|
||||
if (!this->bmp_read_byte(0xD0, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (!this->bmp_read_byte(0xD0, &chip_id)) {
|
||||
this->error_code_ = COMMUNICATION_FAILED;
|
||||
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
|
||||
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
|
||||
return;
|
||||
}
|
||||
if (chip_id != 0x58) {
|
||||
this->error_code_ = WRONG_CHIP_ID;
|
||||
this->mark_failed(BMP280_ERROR_WRONG_CHIP_ID);
|
||||
this->mark_failed(LOG_STR(BMP280_ERROR_WRONG_CHIP_ID));
|
||||
return;
|
||||
}
|
||||
|
||||
// Send a soft reset.
|
||||
if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
|
||||
this->mark_failed("Reset failed");
|
||||
this->mark_failed(LOG_STR("Reset failed"));
|
||||
return;
|
||||
}
|
||||
// Wait until the NVM data has finished loading.
|
||||
@@ -90,12 +90,12 @@ void BMP280Component::setup() {
|
||||
do {
|
||||
delay(2);
|
||||
if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) {
|
||||
this->mark_failed("Error reading status register");
|
||||
this->mark_failed(LOG_STR("Error reading status register"));
|
||||
return;
|
||||
}
|
||||
} while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
|
||||
if (status & BMP280_STATUS_IM_UPDATE) {
|
||||
this->mark_failed("Timeout loading NVM");
|
||||
this->mark_failed(LOG_STR("Timeout loading NVM"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -116,14 +116,14 @@ void BMP280Component::setup() {
|
||||
|
||||
uint8_t config_register = 0;
|
||||
if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
|
||||
this->mark_failed("Read config");
|
||||
this->mark_failed(LOG_STR("Read config"));
|
||||
return;
|
||||
}
|
||||
config_register &= ~0b11111100;
|
||||
config_register |= 0b000 << 5; // 0.5 ms standby time
|
||||
config_register |= (this->iir_filter_ & 0b111) << 2;
|
||||
if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) {
|
||||
this->mark_failed("Write config");
|
||||
this->mark_failed(LOG_STR("Write config"));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace button {
|
||||
namespace esphome::button {
|
||||
|
||||
template<typename... Ts> class PressAction : public Action<Ts...> {
|
||||
public:
|
||||
@@ -24,5 +23,4 @@ class ButtonPressTrigger : public Trigger<> {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace button
|
||||
} // namespace esphome
|
||||
} // namespace esphome::button
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
#include "button.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace button {
|
||||
namespace esphome::button {
|
||||
|
||||
static const char *const TAG = "button";
|
||||
|
||||
@@ -26,5 +25,4 @@ void Button::press() {
|
||||
}
|
||||
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
|
||||
|
||||
} // namespace button
|
||||
} // namespace esphome
|
||||
} // namespace esphome::button
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user