mirror of
https://github.com/esphome/esphome.git
synced 2025-08-10 12:27:46 +00:00
Compare commits
185 Commits
logger_no_
...
dev
Author | SHA1 | Date | |
---|---|---|---|
![]() |
cef39e7c59 | ||
![]() |
2b9e1ce315 | ||
![]() |
ff9ddb9d68 | ||
![]() |
676c51ffa0 | ||
![]() |
7e4d09dbd8 | ||
![]() |
58504662d8 | ||
![]() |
83b69519dd | ||
![]() |
d4d1a96f9b | ||
![]() |
76fd104fb6 | ||
![]() |
c4d1b1317a | ||
![]() |
14bc83342f | ||
![]() |
a1461c5293 | ||
![]() |
73b2db8af5 | ||
![]() |
a7a119f576 | ||
![]() |
1ba76f5f2e | ||
![]() |
37a9ad6a0d | ||
![]() |
c0a62c0be1 | ||
![]() |
bfb14e1cf9 | ||
![]() |
1415e02e40 | ||
![]() |
81f907e994 | ||
![]() |
61008bc8a9 | ||
![]() |
6d66ddd68d | ||
![]() |
fc180251be | ||
![]() |
ee1d4f27ef | ||
![]() |
325ec0a0ae | ||
![]() |
6071f4b02c | ||
![]() |
083ac8ce8e | ||
![]() |
4ceda31f32 | ||
![]() |
5021cc6d5f | ||
![]() |
2b3e546203 | ||
![]() |
1642d34d29 | ||
![]() |
8ceb1b9d60 | ||
![]() |
d872c8a999 | ||
![]() |
99125c045f | ||
![]() |
860a5ef5c0 | ||
![]() |
b01f03cc24 | ||
![]() |
cfb22e33c9 | ||
![]() |
96bbb58f34 | ||
![]() |
3edd746c6c | ||
![]() |
c308e03e92 | ||
![]() |
bd2b3b9da5 | ||
![]() |
d443a97dd8 | ||
![]() |
58a088e06b | ||
![]() |
49a46883ed | ||
![]() |
bc03538e25 | ||
![]() |
969034b61a | ||
![]() |
06eb1b6014 | ||
![]() |
589d00f17f | ||
![]() |
68c0aa4d6d | ||
![]() |
2fddb061e1 | ||
![]() |
c85eb448e4 | ||
![]() |
396c02c6de | ||
![]() |
52c4509208 | ||
![]() |
d29cae9c3b | ||
![]() |
532e3e370f | ||
![]() |
da573a217d | ||
![]() |
a9b27d1966 | ||
![]() |
0aa3c9685e | ||
![]() |
93b28447ee | ||
![]() |
52634dac2a | ||
![]() |
64c94c1440 | ||
![]() |
f7bf1ef52c | ||
![]() |
fa8c5e880c | ||
![]() |
27ba90ea95 | ||
![]() |
469246b8d8 | ||
![]() |
50f15735dc | ||
![]() |
83d9c02a1b | ||
![]() |
701e6099aa | ||
![]() |
d59476d0e1 | ||
![]() |
fbbb791b0d | ||
![]() |
36c4430317 | ||
![]() |
6be22a5ea9 | ||
![]() |
989058e6a9 | ||
![]() |
7c297366c7 | ||
![]() |
bb3ebaf955 | ||
![]() |
3007ca4d57 | ||
![]() |
a5f1661643 | ||
![]() |
4d683d5a69 | ||
![]() |
c0c0a42362 | ||
![]() |
6a5eb460ef | ||
![]() |
ef372eeeb7 | ||
![]() |
9aad0733ef | ||
![]() |
494a1a216c | ||
![]() |
a75f73dbf0 | ||
![]() |
c9d865a061 | ||
![]() |
3fbbdb4589 | ||
![]() |
cd6cf074d9 | ||
![]() |
d86e1e29a9 | ||
![]() |
dbaf2cdd50 | ||
![]() |
b44d2183aa | ||
![]() |
0f13af0076 | ||
![]() |
339c26c815 | ||
![]() |
d69e98e15d | ||
![]() |
b1b0638fab | ||
![]() |
296442d8f1 | ||
![]() |
fd442cc485 | ||
![]() |
4f58e1c8b9 | ||
![]() |
00d9baed11 | ||
![]() |
f1877ca084 | ||
![]() |
1f7c59f88d | ||
![]() |
b5f42bc493 | ||
![]() |
d8a46c7482 | ||
![]() |
20ad1ab4eb | ||
![]() |
940a8b43fa | ||
![]() |
f761404bf6 | ||
![]() |
e4dc62ea74 | ||
![]() |
c42c5dd946 | ||
![]() |
291215909a | ||
![]() |
0954a6185c | ||
![]() |
f13e742bd5 | ||
![]() |
7a4738ec4e | ||
![]() |
549b0d12b6 | ||
![]() |
412f4ac341 | ||
![]() |
d4ff1bcf5c | ||
![]() |
161f51e1f4 | ||
![]() |
da0c47629a | ||
![]() |
28b277c1c4 | ||
![]() |
936a090aaa | ||
![]() |
1be6d27012 | ||
![]() |
71557c9f58 | ||
![]() |
88cfcc1967 | ||
![]() |
fb379bbb88 | ||
![]() |
88d8cfe6a2 | ||
![]() |
f25abc3248 | ||
![]() |
5b6e152d6c | ||
![]() |
1d0a38446f | ||
![]() |
853dca6c5c | ||
![]() |
97560fd9ef | ||
![]() |
4b7f3355ea | ||
![]() |
110eac4f09 | ||
![]() |
79533cb0d7 | ||
![]() |
f4f69e827b | ||
![]() |
48a4dde824 | ||
![]() |
9b4fe54f45 | ||
![]() |
913c58cd2c | ||
![]() |
374858efeb | ||
![]() |
14dd48f9c3 | ||
![]() |
76d33308d9 | ||
![]() |
daccaf36a7 | ||
![]() |
56c88807ee | ||
![]() |
9c6dbbd8ea | ||
![]() |
a7dd849a8e | ||
![]() |
1f0c606be4 | ||
![]() |
ace375944c | ||
![]() |
5f7c2f771f | ||
![]() |
3d5b602288 | ||
![]() |
6d30269565 | ||
![]() |
4ff3137c0d | ||
![]() |
9d43ddd6f1 | ||
![]() |
f733c43dec | ||
![]() |
f5f0a01a85 | ||
![]() |
908891a096 | ||
![]() |
7657316a92 | ||
![]() |
4f425c700a | ||
![]() |
2c9987869e | ||
![]() |
68f388f78e | ||
![]() |
189d20a822 | ||
![]() |
08defd7360 | ||
![]() |
59d466a6c8 | ||
![]() |
85435e6b5f | ||
![]() |
f9453f9642 | ||
![]() |
f6cdbe37f9 | ||
![]() |
d6b222c370 | ||
![]() |
eecdaa5163 | ||
![]() |
4933ef780b | ||
![]() |
1702356fc8 | ||
![]() |
05f6d01cbe | ||
![]() |
573dad1736 | ||
![]() |
3a6cc0ea3d | ||
![]() |
2f9475a927 | ||
![]() |
8dce7b0905 | ||
![]() |
8b0ad3072f | ||
![]() |
93028a4d90 | ||
![]() |
c9793f3741 | ||
![]() |
5029e248eb | ||
![]() |
087970bca8 | ||
![]() |
7f0c66f835 | ||
![]() |
84ed1bcf34 | ||
![]() |
6ed9214465 | ||
![]() |
a3690422bf | ||
![]() |
20b61d4bdb | ||
![]() |
a2ed209542 | ||
![]() |
14862904ac | ||
![]() |
bcc56648c0 | ||
![]() |
e00839a608 |
@@ -168,6 +168,8 @@ This document provides essential context for AI models interacting with this pro
|
||||
* `platformio.ini`: Configures the PlatformIO build environments for different microcontrollers.
|
||||
* `.pre-commit-config.yaml`: Configures the pre-commit hooks for linting and formatting.
|
||||
* **CI/CD Pipeline:** Defined in `.github/workflows`.
|
||||
* **Static Analysis & Development:**
|
||||
* `esphome/core/defines.h`: A comprehensive header file containing all `#define` directives that can be added by components using `cg.add_define()` in Python. This file is used exclusively for development, static analysis tools, and CI testing - it is not used during runtime compilation. When developing components that add new defines, they must be added to this file to ensure proper IDE support and static analysis coverage. The file includes feature flags, build configurations, and platform-specific defines that help static analyzers understand the complete codebase without needing to compile for specific platforms.
|
||||
|
||||
## 6. Development & Testing Workflow
|
||||
|
||||
|
@@ -1 +1 @@
|
||||
32b0db73b3ae01ba18c9cbb1dabbd8156bc14dded500471919bd0a3dc33916e0
|
||||
6af8b429b94191fe8e239fcb3b73f7982d0266cb5b05ffbc81edaeac1bc8c273
|
||||
|
2
.github/actions/restore-python/action.yml
vendored
2
.github/actions/restore-python/action.yml
vendored
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
42
.github/workflows/auto-label-pr.yml
vendored
42
.github/workflows/auto-label-pr.yml
vendored
@@ -63,7 +63,11 @@ jobs:
|
||||
'needs-docs',
|
||||
'needs-codeowners',
|
||||
'too-big',
|
||||
'labeller-recheck'
|
||||
'labeller-recheck',
|
||||
'bugfix',
|
||||
'new-feature',
|
||||
'breaking-change',
|
||||
'code-quality'
|
||||
];
|
||||
|
||||
const DOCS_PR_PATTERNS = [
|
||||
@@ -341,6 +345,31 @@ jobs:
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: PR Template Checkbox detection
|
||||
async function detectPRTemplateCheckboxes() {
|
||||
const labels = new Set();
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
|
||||
console.log('Checking PR template checkboxes...');
|
||||
|
||||
// Check for checked checkboxes in the "Types of changes" section
|
||||
const checkboxPatterns = [
|
||||
{ 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\] Code quality improvements to existing code or addition of tests/i, label: 'code-quality' }
|
||||
];
|
||||
|
||||
for (const { pattern, label } of checkboxPatterns) {
|
||||
if (pattern.test(prBody)) {
|
||||
console.log(`Found checked checkbox for: ${label}`);
|
||||
labels.add(label);
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
// Strategy: Requirements detection
|
||||
async function detectRequirements(allLabels) {
|
||||
const labels = new Set();
|
||||
@@ -351,7 +380,7 @@ jobs:
|
||||
}
|
||||
|
||||
// Check for missing docs
|
||||
if (allLabels.has('new-component') || allLabels.has('new-platform')) {
|
||||
if (allLabels.has('new-component') || allLabels.has('new-platform') || allLabels.has('new-feature')) {
|
||||
const prBody = context.payload.pull_request.body || '';
|
||||
const hasDocsLink = DOCS_PR_PATTERNS.some(pattern => pattern.test(prBody));
|
||||
|
||||
@@ -535,7 +564,8 @@ jobs:
|
||||
dashboardLabels,
|
||||
actionsLabels,
|
||||
codeOwnerLabels,
|
||||
testLabels
|
||||
testLabels,
|
||||
checkboxLabels
|
||||
] = await Promise.all([
|
||||
detectMergeBranch(),
|
||||
detectComponentPlatforms(apiData),
|
||||
@@ -546,7 +576,8 @@ jobs:
|
||||
detectDashboardChanges(),
|
||||
detectGitHubActionsChanges(),
|
||||
detectCodeOwner(),
|
||||
detectTests()
|
||||
detectTests(),
|
||||
detectPRTemplateCheckboxes()
|
||||
]);
|
||||
|
||||
// Combine all labels
|
||||
@@ -560,7 +591,8 @@ jobs:
|
||||
...dashboardLabels,
|
||||
...actionsLabels,
|
||||
...codeOwnerLabels,
|
||||
...testLabels
|
||||
...testLabels,
|
||||
...checkboxLabels
|
||||
]);
|
||||
|
||||
// Detect requirements based on all other labels
|
||||
|
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -161,7 +161,7 @@ jobs:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@v4.2.3
|
||||
uses: actions/cache/save@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -222,7 +222,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -281,7 +281,7 @@ jobs:
|
||||
pio_cache_key: tidyesp32-idf
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ZEPHYR
|
||||
options: --environment nrf52-tidy --grep USE_ZEPHYR
|
||||
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
|
||||
pio_cache_key: tidy-zephyr
|
||||
ignore_errors: false
|
||||
|
||||
@@ -300,14 +300,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@v4.2.3
|
||||
uses: actions/cache@v4.2.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@v4.2.3
|
||||
uses: actions/cache/restore@v4.2.4
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
|
10
.github/workflows/release.yml
vendored
10
.github/workflows/release.yml
vendored
@@ -102,12 +102,12 @@ jobs:
|
||||
uses: docker/setup-buildx-action@v3.11.1
|
||||
|
||||
- name: Log in to docker hub
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
@@ -171,7 +171,7 @@ jobs:
|
||||
- uses: actions/checkout@v4.2.2
|
||||
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4.3.0
|
||||
uses: actions/download-artifact@v5.0.0
|
||||
with:
|
||||
pattern: digests-*
|
||||
path: /tmp/digests
|
||||
@@ -182,13 +182,13 @@ jobs:
|
||||
|
||||
- name: Log in to docker hub
|
||||
if: matrix.registry == 'dockerhub'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USER }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
- name: Log in to the GitHub container registry
|
||||
if: matrix.registry == 'ghcr'
|
||||
uses: docker/login-action@v3.4.0
|
||||
uses: docker/login-action@v3.5.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
|
@@ -11,7 +11,7 @@ ci:
|
||||
repos:
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.12.5
|
||||
rev: v0.12.8
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
|
24
CODEOWNERS
24
CODEOWNERS
@@ -40,11 +40,11 @@ esphome/components/analog_threshold/* @ianchi
|
||||
esphome/components/animation/* @syndlex
|
||||
esphome/components/anova/* @buxtronix
|
||||
esphome/components/apds9306/* @aodrenah
|
||||
esphome/components/api/* @OttoWinter
|
||||
esphome/components/api/* @esphome/core
|
||||
esphome/components/as5600/* @ammmze
|
||||
esphome/components/as5600/sensor/* @ammmze
|
||||
esphome/components/as7341/* @mrgnr
|
||||
esphome/components/async_tcp/* @OttoWinter
|
||||
esphome/components/async_tcp/* @esphome/core
|
||||
esphome/components/at581x/* @X-Ryl669
|
||||
esphome/components/atc_mithermometer/* @ahpohl
|
||||
esphome/components/atm90e26/* @danieltwagner
|
||||
@@ -69,7 +69,7 @@ esphome/components/bl0939/* @ziceva
|
||||
esphome/components/bl0940/* @tobias-
|
||||
esphome/components/bl0942/* @dbuezas @dwmw2
|
||||
esphome/components/ble_client/* @buxtronix @clydebarrow
|
||||
esphome/components/bluetooth_proxy/* @jesserockz
|
||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
|
||||
esphome/components/bme280_base/* @esphome/core
|
||||
esphome/components/bme280_spi/* @apbodrov
|
||||
esphome/components/bme680_bsec/* @trvrnrth
|
||||
@@ -91,7 +91,7 @@ esphome/components/bytebuffer/* @clydebarrow
|
||||
esphome/components/camera/* @DT-art1 @bdraco
|
||||
esphome/components/canbus/* @danielschramm @mvturnho
|
||||
esphome/components/cap1188/* @mreditor97
|
||||
esphome/components/captive_portal/* @OttoWinter
|
||||
esphome/components/captive_portal/* @esphome/core
|
||||
esphome/components/ccs811/* @habbie
|
||||
esphome/components/cd74hc4067/* @asoehlke
|
||||
esphome/components/ch422g/* @clydebarrow @jesterret
|
||||
@@ -118,7 +118,7 @@ esphome/components/dallas_temp/* @ssieb
|
||||
esphome/components/daly_bms/* @s1lvi0
|
||||
esphome/components/dashboard_import/* @esphome/core
|
||||
esphome/components/datetime/* @jesserockz @rfdarter
|
||||
esphome/components/debug/* @OttoWinter
|
||||
esphome/components/debug/* @esphome/core
|
||||
esphome/components/delonghi/* @grob6000
|
||||
esphome/components/dfplayer/* @glmnet
|
||||
esphome/components/dfrobot_sen0395/* @niklasweber
|
||||
@@ -144,9 +144,10 @@ esphome/components/es8156/* @kbx81
|
||||
esphome/components/es8311/* @kahrendt @kroimon
|
||||
esphome/components/es8388/* @P4uLT
|
||||
esphome/components/esp32/* @esphome/core
|
||||
esphome/components/esp32_ble/* @Rapsssito @jesserockz
|
||||
esphome/components/esp32_ble_client/* @jesserockz
|
||||
esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz
|
||||
esphome/components/esp32_ble_client/* @bdraco @jesserockz
|
||||
esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz
|
||||
esphome/components/esp32_ble_tracker/* @bdraco
|
||||
esphome/components/esp32_camera_web_server/* @ayufan
|
||||
esphome/components/esp32_can/* @Sympatron
|
||||
esphome/components/esp32_hosted/* @swoboda1337
|
||||
@@ -155,6 +156,7 @@ esphome/components/esp32_rmt/* @jesserockz
|
||||
esphome/components/esp32_rmt_led_strip/* @jesserockz
|
||||
esphome/components/esp8266/* @esphome/core
|
||||
esphome/components/esp_ldo/* @clydebarrow
|
||||
esphome/components/espnow/* @jesserockz
|
||||
esphome/components/ethernet_info/* @gtjadsonsantos
|
||||
esphome/components/event/* @nohat
|
||||
esphome/components/event_emitter/* @Rapsssito
|
||||
@@ -236,7 +238,7 @@ esphome/components/integration/* @OttoWinter
|
||||
esphome/components/internal_temperature/* @Mat931
|
||||
esphome/components/interval/* @esphome/core
|
||||
esphome/components/jsn_sr04t/* @Mafus1
|
||||
esphome/components/json/* @OttoWinter
|
||||
esphome/components/json/* @esphome/core
|
||||
esphome/components/kamstrup_kmp/* @cfeenstra1024
|
||||
esphome/components/key_collector/* @ssieb
|
||||
esphome/components/key_provider/* @ssieb
|
||||
@@ -466,13 +468,13 @@ esphome/components/template/event/* @nohat
|
||||
esphome/components/template/fan/* @ssieb
|
||||
esphome/components/text/* @mauritskorse
|
||||
esphome/components/thermostat/* @kbx81
|
||||
esphome/components/time/* @OttoWinter
|
||||
esphome/components/time/* @esphome/core
|
||||
esphome/components/tlc5947/* @rnauber
|
||||
esphome/components/tlc5971/* @IJIJI
|
||||
esphome/components/tm1621/* @Philippe12
|
||||
esphome/components/tm1637/* @glmnet
|
||||
esphome/components/tm1638/* @skykingjwc
|
||||
esphome/components/tm1651/* @freekode
|
||||
esphome/components/tm1651/* @mrtoy-me
|
||||
esphome/components/tmp102/* @timsavage
|
||||
esphome/components/tmp1075/* @sybrenstuvel
|
||||
esphome/components/tmp117/* @Azimath
|
||||
@@ -510,7 +512,7 @@ esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
|
||||
esphome/components/watchdog/* @oarcher
|
||||
esphome/components/waveshare_epaper/* @clydebarrow
|
||||
esphome/components/web_server/ota/* @esphome/core
|
||||
esphome/components/web_server_base/* @OttoWinter
|
||||
esphome/components/web_server_base/* @esphome/core
|
||||
esphome/components/web_server_idf/* @dentra
|
||||
esphome/components/weikai/* @DrCoolZic
|
||||
esphome/components/weikai_i2c/* @DrCoolZic
|
||||
|
@@ -2,12 +2,14 @@
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import functools
|
||||
import getpass
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Protocol
|
||||
|
||||
import argcomplete
|
||||
|
||||
@@ -43,6 +45,7 @@ from esphome.const import (
|
||||
from esphome.core import CORE, EsphomeError, coroutine
|
||||
from esphome.helpers import get_bool_env, indent, is_ip_address
|
||||
from esphome.log import AnsiFore, color, setup_log
|
||||
from esphome.types import ConfigType
|
||||
from esphome.util import (
|
||||
get_serial_ports,
|
||||
list_yaml_files,
|
||||
@@ -54,6 +57,23 @@ from esphome.util import (
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ArgsProtocol(Protocol):
|
||||
device: list[str] | None
|
||||
reset: bool
|
||||
username: str | None
|
||||
password: str | None
|
||||
client_id: str | None
|
||||
topic: str | None
|
||||
file: str | None
|
||||
no_logs: bool
|
||||
only_generate: bool
|
||||
show_secrets: bool
|
||||
dashboard: bool
|
||||
configuration: str
|
||||
name: str
|
||||
upload_speed: str | None
|
||||
|
||||
|
||||
def choose_prompt(options, purpose: str = None):
|
||||
if not options:
|
||||
raise EsphomeError(
|
||||
@@ -87,30 +107,54 @@ def choose_prompt(options, purpose: str = None):
|
||||
|
||||
|
||||
def choose_upload_log_host(
|
||||
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
|
||||
):
|
||||
default: list[str] | str | None,
|
||||
check_default: str | None,
|
||||
show_ota: bool,
|
||||
show_mqtt: bool,
|
||||
show_api: bool,
|
||||
purpose: str | None = None,
|
||||
) -> list[str]:
|
||||
# Convert to list for uniform handling
|
||||
defaults = [default] if isinstance(default, str) else default or []
|
||||
|
||||
# If devices specified, resolve them
|
||||
if defaults:
|
||||
resolved: list[str] = []
|
||||
for device in defaults:
|
||||
if device == "SERIAL":
|
||||
serial_ports = get_serial_ports()
|
||||
if not serial_ports:
|
||||
_LOGGER.warning("No serial ports found, skipping SERIAL device")
|
||||
continue
|
||||
options = [
|
||||
(f"{port.path} ({port.description})", port.path)
|
||||
for port in serial_ports
|
||||
]
|
||||
resolved.append(choose_prompt(options, purpose=purpose))
|
||||
elif device == "OTA":
|
||||
if (show_ota and "ota" in CORE.config) or (
|
||||
show_api and "api" in CORE.config
|
||||
):
|
||||
resolved.append(CORE.address)
|
||||
elif show_mqtt and has_mqtt_logging():
|
||||
resolved.append("MQTT")
|
||||
else:
|
||||
resolved.append(device)
|
||||
return resolved
|
||||
|
||||
# No devices specified, show interactive chooser
|
||||
options = [
|
||||
(f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
|
||||
]
|
||||
if default == "SERIAL":
|
||||
return choose_prompt(options, purpose=purpose)
|
||||
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
|
||||
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||
if default == "OTA":
|
||||
return CORE.address
|
||||
if (
|
||||
show_mqtt
|
||||
and (mqtt_config := CORE.config.get(CONF_MQTT))
|
||||
and mqtt_logging_enabled(mqtt_config)
|
||||
):
|
||||
if show_mqtt and has_mqtt_logging():
|
||||
mqtt_config = CORE.config[CONF_MQTT]
|
||||
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
|
||||
if default == "OTA":
|
||||
return "MQTT"
|
||||
if default is not None:
|
||||
return default
|
||||
|
||||
if check_default is not None and check_default in [opt[1] for opt in options]:
|
||||
return check_default
|
||||
return choose_prompt(options, purpose=purpose)
|
||||
return [check_default]
|
||||
return [choose_prompt(options, purpose=purpose)]
|
||||
|
||||
|
||||
def mqtt_logging_enabled(mqtt_config):
|
||||
@@ -122,7 +166,14 @@ def mqtt_logging_enabled(mqtt_config):
|
||||
return log_topic.get(CONF_LEVEL, None) != "NONE"
|
||||
|
||||
|
||||
def get_port_type(port):
|
||||
def has_mqtt_logging() -> bool:
|
||||
"""Check if MQTT logging is available."""
|
||||
return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled(
|
||||
mqtt_config
|
||||
)
|
||||
|
||||
|
||||
def get_port_type(port: str) -> str:
|
||||
if port.startswith("/") or port.startswith("COM"):
|
||||
return "SERIAL"
|
||||
if port == "MQTT":
|
||||
@@ -130,7 +181,7 @@ def get_port_type(port):
|
||||
return "NETWORK"
|
||||
|
||||
|
||||
def run_miniterm(config, port, args):
|
||||
def run_miniterm(config: ConfigType, port: str, args) -> int:
|
||||
from aioesphomeapi import LogParser
|
||||
import serial
|
||||
|
||||
@@ -207,7 +258,7 @@ def wrap_to_code(name, comp):
|
||||
return wrapped
|
||||
|
||||
|
||||
def write_cpp(config):
|
||||
def write_cpp(config: ConfigType) -> int:
|
||||
if not get_bool_env(ENV_NOGITIGNORE):
|
||||
writer.write_gitignore()
|
||||
|
||||
@@ -215,7 +266,7 @@ def write_cpp(config):
|
||||
return write_cpp_file()
|
||||
|
||||
|
||||
def generate_cpp_contents(config):
|
||||
def generate_cpp_contents(config: ConfigType) -> None:
|
||||
_LOGGER.info("Generating C++ source...")
|
||||
|
||||
for name, component, conf in iter_component_configs(CORE.config):
|
||||
@@ -226,7 +277,7 @@ def generate_cpp_contents(config):
|
||||
CORE.flush_tasks()
|
||||
|
||||
|
||||
def write_cpp_file():
|
||||
def write_cpp_file() -> int:
|
||||
code_s = indent(CORE.cpp_main_section)
|
||||
writer.write_cpp(code_s)
|
||||
|
||||
@@ -237,7 +288,7 @@ def write_cpp_file():
|
||||
return 0
|
||||
|
||||
|
||||
def compile_program(args, config):
|
||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
from esphome import platformio_api
|
||||
|
||||
_LOGGER.info("Compiling app...")
|
||||
@@ -248,7 +299,9 @@ def compile_program(args, config):
|
||||
return 0 if idedata is not None else 1
|
||||
|
||||
|
||||
def upload_using_esptool(config, port, file, speed):
|
||||
def upload_using_esptool(
|
||||
config: ConfigType, port: str, file: str, speed: int
|
||||
) -> str | int:
|
||||
from esphome import platformio_api
|
||||
|
||||
first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
|
||||
@@ -276,20 +329,20 @@ def upload_using_esptool(config, port, file, speed):
|
||||
|
||||
def run_esptool(baud_rate):
|
||||
cmd = [
|
||||
"esptool.py",
|
||||
"esptool",
|
||||
"--before",
|
||||
"default_reset",
|
||||
"default-reset",
|
||||
"--after",
|
||||
"hard_reset",
|
||||
"hard-reset",
|
||||
"--baud",
|
||||
str(baud_rate),
|
||||
"--port",
|
||||
port,
|
||||
"--chip",
|
||||
mcu,
|
||||
"write_flash",
|
||||
"write-flash",
|
||||
"-z",
|
||||
"--flash_size",
|
||||
"--flash-size",
|
||||
"detect",
|
||||
]
|
||||
for img in flash_images:
|
||||
@@ -313,7 +366,7 @@ def upload_using_esptool(config, port, file, speed):
|
||||
return run_esptool(115200)
|
||||
|
||||
|
||||
def upload_using_platformio(config, port):
|
||||
def upload_using_platformio(config: ConfigType, port: str):
|
||||
from esphome import platformio_api
|
||||
|
||||
upload_args = ["-t", "upload", "-t", "nobuild"]
|
||||
@@ -322,7 +375,7 @@ def upload_using_platformio(config, port):
|
||||
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
|
||||
|
||||
|
||||
def check_permissions(port):
|
||||
def check_permissions(port: str):
|
||||
if os.name == "posix" and get_port_type(port) == "SERIAL":
|
||||
# Check if we can open selected serial port
|
||||
if not os.access(port, os.F_OK):
|
||||
@@ -335,12 +388,12 @@ def check_permissions(port):
|
||||
raise EsphomeError(
|
||||
"You do not have read or write permission on the selected serial port. "
|
||||
"To resolve this issue, you can add your user to the dialout group "
|
||||
f"by running the following command: sudo usermod -a -G dialout {os.getlogin()}. "
|
||||
f"by running the following command: sudo usermod -a -G dialout {getpass.getuser()}. "
|
||||
"You will need to log out & back in or reboot to activate the new group access."
|
||||
)
|
||||
|
||||
|
||||
def upload_program(config, args, host):
|
||||
def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str:
|
||||
try:
|
||||
module = importlib.import_module("esphome.components." + CORE.target_platform)
|
||||
if getattr(module, "upload_program")(config, args, host):
|
||||
@@ -355,7 +408,7 @@ def upload_program(config, args, host):
|
||||
return upload_using_esptool(config, host, file, args.upload_speed)
|
||||
|
||||
if CORE.target_platform in (PLATFORM_RP2040):
|
||||
return upload_using_platformio(config, args.device)
|
||||
return upload_using_platformio(config, host)
|
||||
|
||||
if CORE.is_libretiny:
|
||||
return upload_using_platformio(config, host)
|
||||
@@ -378,9 +431,12 @@ def upload_program(config, args, host):
|
||||
remote_port = int(ota_conf[CONF_PORT])
|
||||
password = ota_conf.get(CONF_PASSWORD, "")
|
||||
|
||||
# Check if we should use MQTT for address resolution
|
||||
# This happens when no device was specified, or the current host is "MQTT"/"OTA"
|
||||
devices: list[str] = args.device or []
|
||||
if (
|
||||
CONF_MQTT in config # pylint: disable=too-many-boolean-expressions
|
||||
and (not args.device or args.device in ("MQTT", "OTA"))
|
||||
and (not devices or host in ("MQTT", "OTA"))
|
||||
and (
|
||||
((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
|
||||
or get_port_type(host) == "MQTT"
|
||||
@@ -398,23 +454,28 @@ def upload_program(config, args, host):
|
||||
return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
|
||||
|
||||
|
||||
def show_logs(config, args, port):
|
||||
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
|
||||
if "logger" not in config:
|
||||
raise EsphomeError("Logger is not configured!")
|
||||
|
||||
port = devices[0]
|
||||
|
||||
if get_port_type(port) == "SERIAL":
|
||||
check_permissions(port)
|
||||
return run_miniterm(config, port, args)
|
||||
if get_port_type(port) == "NETWORK" and "api" in config:
|
||||
addresses_to_use = devices
|
||||
if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
|
||||
from esphome import mqtt
|
||||
|
||||
port = mqtt.get_esphome_device_ip(
|
||||
mqtt_address = mqtt.get_esphome_device_ip(
|
||||
config, args.username, args.password, args.client_id
|
||||
)[0]
|
||||
addresses_to_use = [mqtt_address]
|
||||
|
||||
from esphome.components.api.client import run_logs
|
||||
|
||||
return run_logs(config, port)
|
||||
return run_logs(config, addresses_to_use)
|
||||
if get_port_type(port) == "MQTT" and "mqtt" in config:
|
||||
from esphome import mqtt
|
||||
|
||||
@@ -425,7 +486,7 @@ def show_logs(config, args, port):
|
||||
raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
|
||||
|
||||
|
||||
def clean_mqtt(config, args):
|
||||
def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.clear_topic(
|
||||
@@ -433,13 +494,13 @@ def clean_mqtt(config, args):
|
||||
)
|
||||
|
||||
|
||||
def command_wizard(args):
|
||||
def command_wizard(args: ArgsProtocol) -> int | None:
|
||||
from esphome import wizard
|
||||
|
||||
return wizard.wizard(args.configuration)
|
||||
|
||||
|
||||
def command_config(args, config):
|
||||
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if not CORE.verbose:
|
||||
config = strip_default_ids(config)
|
||||
output = yaml_util.dump(config, args.show_secrets)
|
||||
@@ -454,7 +515,7 @@ def command_config(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_vscode(args):
|
||||
def command_vscode(args: ArgsProtocol) -> int | None:
|
||||
from esphome import vscode
|
||||
|
||||
logging.disable(logging.INFO)
|
||||
@@ -462,7 +523,7 @@ def command_vscode(args):
|
||||
vscode.read_config(args)
|
||||
|
||||
|
||||
def command_compile(args, config):
|
||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -476,8 +537,9 @@ def command_compile(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_upload(args, config):
|
||||
port = choose_upload_log_host(
|
||||
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
@@ -485,14 +547,22 @@ def command_upload(args, config):
|
||||
show_api=False,
|
||||
purpose="uploading",
|
||||
)
|
||||
exit_code = upload_program(config, args, port)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
return 0
|
||||
|
||||
# Try each device until one succeeds
|
||||
exit_code = 1
|
||||
for device in devices:
|
||||
_LOGGER.info("Uploading to %s", device)
|
||||
exit_code = upload_program(config, args, device)
|
||||
if exit_code == 0:
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
return 0
|
||||
if len(devices) > 1:
|
||||
_LOGGER.warning("Failed to upload to %s", device)
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
def command_discover(args, config):
|
||||
def command_discover(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if "mqtt" in config:
|
||||
from esphome import mqtt
|
||||
|
||||
@@ -501,8 +571,9 @@ def command_discover(args, config):
|
||||
raise EsphomeError("No discover method configured (mqtt)")
|
||||
|
||||
|
||||
def command_logs(args, config):
|
||||
port = choose_upload_log_host(
|
||||
def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=None,
|
||||
show_ota=False,
|
||||
@@ -510,10 +581,10 @@ def command_logs(args, config):
|
||||
show_api=True,
|
||||
purpose="logging",
|
||||
)
|
||||
return show_logs(config, args, port)
|
||||
return show_logs(config, args, devices)
|
||||
|
||||
|
||||
def command_run(args, config):
|
||||
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
@@ -530,7 +601,8 @@ def command_run(args, config):
|
||||
program_path = idedata.raw["prog_path"]
|
||||
return run_external_process(program_path)
|
||||
|
||||
port = choose_upload_log_host(
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=None,
|
||||
show_ota=True,
|
||||
@@ -538,39 +610,53 @@ def command_run(args, config):
|
||||
show_api=True,
|
||||
purpose="uploading",
|
||||
)
|
||||
exit_code = upload_program(config, args, port)
|
||||
if exit_code != 0:
|
||||
|
||||
# Try each device for upload until one succeeds
|
||||
successful_device: str | None = None
|
||||
for device in devices:
|
||||
_LOGGER.info("Uploading to %s", device)
|
||||
exit_code = upload_program(config, args, device)
|
||||
if exit_code == 0:
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
successful_device = device
|
||||
break
|
||||
if len(devices) > 1:
|
||||
_LOGGER.warning("Failed to upload to %s", device)
|
||||
|
||||
if successful_device is None:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully uploaded program.")
|
||||
|
||||
if args.no_logs:
|
||||
return 0
|
||||
port = choose_upload_log_host(
|
||||
default=args.device,
|
||||
check_default=port,
|
||||
|
||||
# For logs, prefer the device we successfully uploaded to
|
||||
devices = choose_upload_log_host(
|
||||
default=successful_device,
|
||||
check_default=successful_device,
|
||||
show_ota=False,
|
||||
show_mqtt=True,
|
||||
show_api=True,
|
||||
purpose="logging",
|
||||
)
|
||||
return show_logs(config, args, port)
|
||||
return show_logs(config, args, devices)
|
||||
|
||||
|
||||
def command_clean_mqtt(args, config):
|
||||
def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
return clean_mqtt(config, args)
|
||||
|
||||
|
||||
def command_mqtt_fingerprint(args, config):
|
||||
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
from esphome import mqtt
|
||||
|
||||
return mqtt.get_fingerprint(config)
|
||||
|
||||
|
||||
def command_version(args):
|
||||
def command_version(args: ArgsProtocol) -> int | None:
|
||||
safe_print(f"Version: {const.__version__}")
|
||||
return 0
|
||||
|
||||
|
||||
def command_clean(args, config):
|
||||
def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
try:
|
||||
writer.clean_build()
|
||||
except OSError as err:
|
||||
@@ -580,13 +666,13 @@ def command_clean(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_dashboard(args):
|
||||
def command_dashboard(args: ArgsProtocol) -> int | None:
|
||||
from esphome.dashboard import dashboard
|
||||
|
||||
return dashboard.start_dashboard(args)
|
||||
|
||||
|
||||
def command_update_all(args):
|
||||
def command_update_all(args: ArgsProtocol) -> int | None:
|
||||
import click
|
||||
|
||||
success = {}
|
||||
@@ -633,7 +719,7 @@ def command_update_all(args):
|
||||
return failed
|
||||
|
||||
|
||||
def command_idedata(args, config):
|
||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
import json
|
||||
|
||||
from esphome import platformio_api
|
||||
@@ -649,7 +735,7 @@ def command_idedata(args, config):
|
||||
return 0
|
||||
|
||||
|
||||
def command_rename(args, config):
|
||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
for c in args.name:
|
||||
if c not in ALLOWED_NAME_CHARS:
|
||||
print(
|
||||
@@ -766,6 +852,12 @@ POST_CONFIG_ACTIONS = {
|
||||
"discover": command_discover,
|
||||
}
|
||||
|
||||
SIMPLE_CONFIG_ACTIONS = [
|
||||
"clean",
|
||||
"clean-mqtt",
|
||||
"config",
|
||||
]
|
||||
|
||||
|
||||
def parse_args(argv):
|
||||
options_parser = argparse.ArgumentParser(add_help=False)
|
||||
@@ -853,7 +945,8 @@ def parse_args(argv):
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--device",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--upload_speed",
|
||||
@@ -875,7 +968,8 @@ def parse_args(argv):
|
||||
)
|
||||
parser_logs.add_argument(
|
||||
"--device",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
)
|
||||
parser_logs.add_argument(
|
||||
"--reset",
|
||||
@@ -904,7 +998,8 @@ def parse_args(argv):
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--device",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
|
||||
action="append",
|
||||
help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--upload_speed",
|
||||
@@ -1031,6 +1126,13 @@ def parse_args(argv):
|
||||
arguments = argv[1:]
|
||||
|
||||
argcomplete.autocomplete(parser)
|
||||
|
||||
if len(arguments) > 0 and arguments[0] in SIMPLE_CONFIG_ACTIONS:
|
||||
args, unknown_args = parser.parse_known_args(arguments)
|
||||
if unknown_args:
|
||||
_LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
|
||||
return args
|
||||
|
||||
return parser.parse_args(arguments)
|
||||
|
||||
|
||||
|
@@ -391,8 +391,7 @@ async def build_action(full_config, template_arg, args):
|
||||
)
|
||||
action_id = full_config[CONF_TYPE_ID]
|
||||
builder = registry_entry.coroutine_fun
|
||||
ret = await builder(config, action_id, template_arg, args)
|
||||
return ret
|
||||
return await builder(config, action_id, template_arg, args)
|
||||
|
||||
|
||||
async def build_action_list(config, templ, arg_type):
|
||||
@@ -409,8 +408,7 @@ async def build_condition(full_config, template_arg, args):
|
||||
)
|
||||
action_id = full_config[CONF_TYPE_ID]
|
||||
builder = registry_entry.coroutine_fun
|
||||
ret = await builder(config, action_id, template_arg, args)
|
||||
return ret
|
||||
return await builder(config, action_id, template_arg, args)
|
||||
|
||||
|
||||
async def build_condition_list(config, templ, args):
|
||||
|
@@ -5,7 +5,7 @@ from esphome.const import (
|
||||
CONF_EQUATION,
|
||||
CONF_HUMIDITY,
|
||||
CONF_TEMPERATURE,
|
||||
ICON_WATER,
|
||||
DEVICE_CLASS_ABSOLUTE_HUMIDITY,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_GRAMS_PER_CUBIC_METER,
|
||||
)
|
||||
@@ -27,8 +27,8 @@ EQUATION = {
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_GRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_WATER,
|
||||
accuracy_decimals=2,
|
||||
device_class=DEVICE_CLASS_ABSOLUTE_HUMIDITY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
.extend(
|
||||
|
@@ -1,6 +1,6 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C2,
|
||||
@@ -140,6 +140,16 @@ 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
|
||||
@@ -198,6 +208,14 @@ 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,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -249,6 +267,11 @@ def validate_adc_pin(value):
|
||||
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
||||
)(value)
|
||||
|
||||
if CORE.is_nrf52:
|
||||
return pins.gpio_pin_schema(
|
||||
{CONF_ANALOG: True, CONF_INPUT: True}, internal=True
|
||||
)(value)
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -265,5 +288,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
PlatformFramework.RTL87XX_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
"adc_sensor_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
}
|
||||
)
|
||||
|
@@ -13,6 +13,10 @@
|
||||
#include "hal/adc_types.h" // This defines ADC_CHANNEL_MAX
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <zephyr/drivers/adc.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
@@ -38,15 +42,15 @@ enum class SamplingMode : uint8_t {
|
||||
|
||||
const LogString *sampling_mode_to_str(SamplingMode mode);
|
||||
|
||||
class Aggregator {
|
||||
template<typename T> class Aggregator {
|
||||
public:
|
||||
Aggregator(SamplingMode mode);
|
||||
void add_sample(uint32_t value);
|
||||
uint32_t aggregate();
|
||||
void add_sample(T value);
|
||||
T aggregate();
|
||||
|
||||
protected:
|
||||
uint32_t aggr_{0};
|
||||
uint32_t samples_{0};
|
||||
T aggr_{0};
|
||||
uint8_t samples_{0};
|
||||
SamplingMode mode_{SamplingMode::AVG};
|
||||
};
|
||||
|
||||
@@ -69,6 +73,11 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
/// @return A float representing the setup priority.
|
||||
float get_setup_priority() const override;
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
/// Set the ADC channel to be used by the ADC sensor.
|
||||
/// @param channel Pointer to an adc_dt_spec structure representing the ADC channel.
|
||||
void set_adc_channel(const adc_dt_spec *channel) { this->channel_ = channel; }
|
||||
#endif
|
||||
/// Set the GPIO pin to be used by the ADC sensor.
|
||||
/// @param pin Pointer to an InternalGPIOPin representing the ADC input pin.
|
||||
void set_pin(InternalGPIOPin *pin) { this->pin_ = pin; }
|
||||
@@ -136,8 +145,8 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
adc_oneshot_unit_handle_t adc_handle_{nullptr};
|
||||
adc_cali_handle_t calibration_handle_{nullptr};
|
||||
adc_atten_t attenuation_{ADC_ATTEN_DB_0};
|
||||
adc_channel_t channel_;
|
||||
adc_unit_t adc_unit_;
|
||||
adc_channel_t channel_{};
|
||||
adc_unit_t adc_unit_{};
|
||||
struct SetupFlags {
|
||||
uint8_t init_complete : 1;
|
||||
uint8_t config_complete : 1;
|
||||
@@ -151,6 +160,10 @@ class ADCSensor : public sensor::Sensor, public PollingComponent, public voltage
|
||||
#ifdef USE_RP2040
|
||||
bool is_temperature_{false};
|
||||
#endif // USE_RP2040
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
const struct adc_dt_spec *channel_ = nullptr;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace adc
|
||||
|
@@ -18,15 +18,15 @@ const LogString *sampling_mode_to_str(SamplingMode mode) {
|
||||
return LOG_STR("unknown");
|
||||
}
|
||||
|
||||
Aggregator::Aggregator(SamplingMode mode) {
|
||||
template<typename T> Aggregator<T>::Aggregator(SamplingMode mode) {
|
||||
this->mode_ = mode;
|
||||
// set to max uint if mode is "min"
|
||||
if (mode == SamplingMode::MIN) {
|
||||
this->aggr_ = UINT32_MAX;
|
||||
this->aggr_ = std::numeric_limits<T>::max();
|
||||
}
|
||||
}
|
||||
|
||||
void Aggregator::add_sample(uint32_t value) {
|
||||
template<typename T> void Aggregator<T>::add_sample(T value) {
|
||||
this->samples_ += 1;
|
||||
|
||||
switch (this->mode_) {
|
||||
@@ -47,7 +47,7 @@ void Aggregator::add_sample(uint32_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t Aggregator::aggregate() {
|
||||
template<typename T> T Aggregator<T>::aggregate() {
|
||||
if (this->mode_ == SamplingMode::AVG) {
|
||||
if (this->samples_ == 0) {
|
||||
return this->aggr_;
|
||||
@@ -59,6 +59,12 @@ uint32_t Aggregator::aggregate() {
|
||||
return this->aggr_;
|
||||
}
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
template class Aggregator<int32_t>;
|
||||
#else
|
||||
template class Aggregator<uint32_t>;
|
||||
#endif
|
||||
|
||||
void ADCSensor::update() {
|
||||
float value_v = this->sample();
|
||||
ESP_LOGV(TAG, "'%s': Voltage=%.4fV", this->get_name().c_str(), value_v);
|
||||
|
@@ -72,10 +72,9 @@ void ADCSensor::setup() {
|
||||
// Initialize ADC calibration
|
||||
if (this->calibration_handle_ == nullptr) {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
esp_err_t err;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2
|
||||
USE_ESP32_VARIANT_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
// 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)
|
||||
@@ -153,7 +152,7 @@ float ADCSensor::sample() {
|
||||
}
|
||||
|
||||
float ADCSensor::sample_fixed_attenuation_() {
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
int raw;
|
||||
@@ -187,7 +186,7 @@ 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_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
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_);
|
||||
@@ -220,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_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
@@ -232,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_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
adc_cali_curve_fitting_config_t cali_config = {};
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
@@ -261,7 +260,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_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
@@ -281,7 +280,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_ESP32S3 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
|
@@ -37,7 +37,7 @@ void ADCSensor::dump_config() {
|
||||
}
|
||||
|
||||
float ADCSensor::sample() {
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
uint32_t raw = 0;
|
||||
|
@@ -30,7 +30,7 @@ void ADCSensor::dump_config() {
|
||||
|
||||
float ADCSensor::sample() {
|
||||
uint32_t raw = 0;
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
if (this->output_raw_) {
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
|
@@ -41,7 +41,7 @@ void ADCSensor::dump_config() {
|
||||
|
||||
float ADCSensor::sample() {
|
||||
uint32_t raw = 0;
|
||||
auto aggr = Aggregator(this->sampling_mode_);
|
||||
auto aggr = Aggregator<uint32_t>(this->sampling_mode_);
|
||||
|
||||
if (this->is_temperature_) {
|
||||
adc_set_temp_sensor_enabled(true);
|
||||
|
207
esphome/components/adc/adc_sensor_zephyr.cpp
Normal file
207
esphome/components/adc/adc_sensor_zephyr.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
|
||||
#include "adc_sensor.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include "hal/nrf_saadc.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace adc {
|
||||
|
||||
static const char *const TAG = "adc.zephyr";
|
||||
|
||||
void ADCSensor::setup() {
|
||||
if (!adc_is_ready_dt(this->channel_)) {
|
||||
ESP_LOGE(TAG, "ADC controller device %s not ready", this->channel_->dev->name);
|
||||
return;
|
||||
}
|
||||
|
||||
auto err = adc_channel_setup_dt(this->channel_);
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Could not setup channel %s (%d)", this->channel_->dev->name, err);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
static const LogString *gain_to_str(enum adc_gain gain) {
|
||||
switch (gain) {
|
||||
case ADC_GAIN_1_6:
|
||||
return LOG_STR("1/6");
|
||||
case ADC_GAIN_1_5:
|
||||
return LOG_STR("1/5");
|
||||
case ADC_GAIN_1_4:
|
||||
return LOG_STR("1/4");
|
||||
case ADC_GAIN_1_3:
|
||||
return LOG_STR("1/3");
|
||||
case ADC_GAIN_2_5:
|
||||
return LOG_STR("2/5");
|
||||
case ADC_GAIN_1_2:
|
||||
return LOG_STR("1/2");
|
||||
case ADC_GAIN_2_3:
|
||||
return LOG_STR("2/3");
|
||||
case ADC_GAIN_4_5:
|
||||
return LOG_STR("4/5");
|
||||
case ADC_GAIN_1:
|
||||
return LOG_STR("1");
|
||||
case ADC_GAIN_2:
|
||||
return LOG_STR("2");
|
||||
case ADC_GAIN_3:
|
||||
return LOG_STR("3");
|
||||
case ADC_GAIN_4:
|
||||
return LOG_STR("4");
|
||||
case ADC_GAIN_6:
|
||||
return LOG_STR("6");
|
||||
case ADC_GAIN_8:
|
||||
return LOG_STR("8");
|
||||
case ADC_GAIN_12:
|
||||
return LOG_STR("12");
|
||||
case ADC_GAIN_16:
|
||||
return LOG_STR("16");
|
||||
case ADC_GAIN_24:
|
||||
return LOG_STR("24");
|
||||
case ADC_GAIN_32:
|
||||
return LOG_STR("32");
|
||||
case ADC_GAIN_64:
|
||||
return LOG_STR("64");
|
||||
case ADC_GAIN_128:
|
||||
return LOG_STR("128");
|
||||
}
|
||||
return LOG_STR("undefined gain");
|
||||
}
|
||||
|
||||
static const LogString *reference_to_str(enum adc_reference reference) {
|
||||
switch (reference) {
|
||||
case ADC_REF_VDD_1:
|
||||
return LOG_STR("VDD");
|
||||
case ADC_REF_VDD_1_2:
|
||||
return LOG_STR("VDD/2");
|
||||
case ADC_REF_VDD_1_3:
|
||||
return LOG_STR("VDD/3");
|
||||
case ADC_REF_VDD_1_4:
|
||||
return LOG_STR("VDD/4");
|
||||
case ADC_REF_INTERNAL:
|
||||
return LOG_STR("INTERNAL");
|
||||
case ADC_REF_EXTERNAL0:
|
||||
return LOG_STR("External, input 0");
|
||||
case ADC_REF_EXTERNAL1:
|
||||
return LOG_STR("External, input 1");
|
||||
}
|
||||
return LOG_STR("undefined reference");
|
||||
}
|
||||
|
||||
static const LogString *input_to_str(uint8_t input) {
|
||||
switch (input) {
|
||||
case NRF_SAADC_INPUT_AIN0:
|
||||
return LOG_STR("AIN0");
|
||||
case NRF_SAADC_INPUT_AIN1:
|
||||
return LOG_STR("AIN1");
|
||||
case NRF_SAADC_INPUT_AIN2:
|
||||
return LOG_STR("AIN2");
|
||||
case NRF_SAADC_INPUT_AIN3:
|
||||
return LOG_STR("AIN3");
|
||||
case NRF_SAADC_INPUT_AIN4:
|
||||
return LOG_STR("AIN4");
|
||||
case NRF_SAADC_INPUT_AIN5:
|
||||
return LOG_STR("AIN5");
|
||||
case NRF_SAADC_INPUT_AIN6:
|
||||
return LOG_STR("AIN6");
|
||||
case NRF_SAADC_INPUT_AIN7:
|
||||
return LOG_STR("AIN7");
|
||||
case NRF_SAADC_INPUT_VDD:
|
||||
return LOG_STR("VDD");
|
||||
case NRF_SAADC_INPUT_VDDHDIV5:
|
||||
return LOG_STR("VDDHDIV5");
|
||||
}
|
||||
return LOG_STR("undefined input");
|
||||
}
|
||||
#endif // ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
|
||||
void ADCSensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC Sensor", this);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG,
|
||||
" Name: %s\n"
|
||||
" Channel: %d\n"
|
||||
" vref_mv: %d\n"
|
||||
" Resolution %d\n"
|
||||
" Oversampling %d",
|
||||
this->channel_->dev->name, this->channel_->channel_id, this->channel_->vref_mv, this->channel_->resolution,
|
||||
this->channel_->oversampling);
|
||||
|
||||
ESP_LOGV(TAG,
|
||||
" Gain: %s\n"
|
||||
" reference: %s\n"
|
||||
" acquisition_time: %d\n"
|
||||
" differential %s",
|
||||
LOG_STR_ARG(gain_to_str(this->channel_->channel_cfg.gain)),
|
||||
LOG_STR_ARG(reference_to_str(this->channel_->channel_cfg.reference)),
|
||||
this->channel_->channel_cfg.acquisition_time, YESNO(this->channel_->channel_cfg.differential));
|
||||
if (this->channel_->channel_cfg.differential) {
|
||||
ESP_LOGV(TAG,
|
||||
" Positive: %s\n"
|
||||
" Negative: %s",
|
||||
LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_positive)),
|
||||
LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_negative)));
|
||||
} else {
|
||||
ESP_LOGV(TAG, " Positive: %s", LOG_STR_ARG(input_to_str(this->channel_->channel_cfg.input_positive)));
|
||||
}
|
||||
#endif
|
||||
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
|
||||
float ADCSensor::sample() {
|
||||
auto aggr = Aggregator<int32_t>(this->sampling_mode_);
|
||||
int err;
|
||||
for (uint8_t sample = 0; sample < this->sample_count_; sample++) {
|
||||
int16_t buf = 0;
|
||||
struct adc_sequence sequence = {
|
||||
.buffer = &buf,
|
||||
/* buffer size in bytes, not number of samples */
|
||||
.buffer_size = sizeof(buf),
|
||||
};
|
||||
int32_t val_raw;
|
||||
|
||||
err = adc_sequence_init_dt(this->channel_, &sequence);
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Could sequence init %s (%d)", this->channel_->dev->name, err);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
err = adc_read(this->channel_->dev, &sequence);
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Could not read %s (%d)", this->channel_->dev->name, err);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
val_raw = (int32_t) buf;
|
||||
if (!this->channel_->channel_cfg.differential) {
|
||||
// https://github.com/adafruit/Adafruit_nRF52_Arduino/blob/0ed4d9ffc674ae407be7cacf5696a02f5e789861/cores/nRF5/wiring_analog_nRF52.c#L222
|
||||
if (val_raw < 0) {
|
||||
val_raw = 0;
|
||||
}
|
||||
}
|
||||
aggr.add_sample(val_raw);
|
||||
}
|
||||
|
||||
int32_t val_mv = aggr.aggregate();
|
||||
|
||||
if (this->output_raw_) {
|
||||
return val_mv;
|
||||
}
|
||||
|
||||
err = adc_raw_to_millivolts_dt(this->channel_, &val_mv);
|
||||
/* conversion to mV may not be supported, skip if not */
|
||||
if (err < 0) {
|
||||
ESP_LOGE(TAG, "Value in mV not available %s (%d)", this->channel_->dev->name, err);
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
return val_mv / 1000.0f;
|
||||
}
|
||||
|
||||
} // namespace adc
|
||||
} // namespace esphome
|
||||
#endif
|
@@ -3,6 +3,12 @@ import logging
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.components.nrf52.const import AIN_TO_GPIO, EXTRA_ADC
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_add_overlay,
|
||||
zephyr_add_prj_conf,
|
||||
zephyr_add_user,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ATTENUATION,
|
||||
@@ -11,6 +17,7 @@ from esphome.const import (
|
||||
CONF_PIN,
|
||||
CONF_RAW,
|
||||
DEVICE_CLASS_VOLTAGE,
|
||||
PLATFORM_NRF52,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_VOLT,
|
||||
)
|
||||
@@ -60,6 +67,10 @@ ADCSensor = adc_ns.class_(
|
||||
"ADCSensor", sensor.Sensor, cg.PollingComponent, voltage_sampler.VoltageSampler
|
||||
)
|
||||
|
||||
CONF_NRF_SAADC = "nrf_saadc"
|
||||
|
||||
adc_dt_spec = cg.global_ns.class_("adc_dt_spec")
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(
|
||||
ADCSensor,
|
||||
@@ -75,6 +86,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.SplitDefault(CONF_ATTENUATION, esp32="0db"): cv.All(
|
||||
cv.only_on_esp32, _attenuation
|
||||
),
|
||||
cv.OnlyWith(CONF_NRF_SAADC, PLATFORM_NRF52): cv.declare_id(adc_dt_spec),
|
||||
cv.Optional(CONF_SAMPLES, default=1): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SAMPLING_MODE, default="avg"): _sampling_mode,
|
||||
}
|
||||
@@ -83,6 +95,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
validate_config,
|
||||
)
|
||||
|
||||
CONF_ADC_CHANNEL_ID = "adc_channel_id"
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -93,7 +107,7 @@ async def to_code(config):
|
||||
cg.add_define("USE_ADC_SENSOR_VCC")
|
||||
elif config[CONF_PIN] == "TEMPERATURE":
|
||||
cg.add(var.set_is_temperature())
|
||||
else:
|
||||
elif not CORE.is_nrf52 or config[CONF_PIN][CONF_NUMBER] not in EXTRA_ADC:
|
||||
pin = await cg.gpio_pin_expression(config[CONF_PIN])
|
||||
cg.add(var.set_pin(pin))
|
||||
|
||||
@@ -122,3 +136,41 @@ async def to_code(config):
|
||||
):
|
||||
chan = ESP32_VARIANT_ADC2_PIN_TO_CHANNEL[variant][pin_num]
|
||||
cg.add(var.set_channel(adc_unit_t.ADC_UNIT_2, chan))
|
||||
|
||||
elif CORE.is_nrf52:
|
||||
CORE.data.setdefault(CONF_ADC_CHANNEL_ID, 0)
|
||||
channel_id = CORE.data[CONF_ADC_CHANNEL_ID]
|
||||
CORE.data[CONF_ADC_CHANNEL_ID] = channel_id + 1
|
||||
zephyr_add_prj_conf("ADC", True)
|
||||
nrf_saadc = config[CONF_NRF_SAADC]
|
||||
rhs = cg.RawExpression(
|
||||
f"ADC_DT_SPEC_GET_BY_IDX(DT_PATH(zephyr_user), {channel_id})"
|
||||
)
|
||||
adc = cg.new_Pvariable(nrf_saadc, rhs)
|
||||
cg.add(var.set_adc_channel(adc))
|
||||
gain = "ADC_GAIN_1_6"
|
||||
pin_number = config[CONF_PIN][CONF_NUMBER]
|
||||
if pin_number == "VDDHDIV5":
|
||||
gain = "ADC_GAIN_1_2"
|
||||
if isinstance(pin_number, int):
|
||||
GPIO_TO_AIN = {v: k for k, v in AIN_TO_GPIO.items()}
|
||||
pin_number = GPIO_TO_AIN[pin_number]
|
||||
zephyr_add_user("io-channels", f"<&adc {channel_id}>")
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
&adc {{
|
||||
#address-cells = <1>;
|
||||
#size-cells = <0>;
|
||||
|
||||
channel@{channel_id} {{
|
||||
reg = <{channel_id}>;
|
||||
zephyr,gain = "{gain}";
|
||||
zephyr,reference = "ADC_REF_INTERNAL";
|
||||
zephyr,acquisition-time = <ADC_ACQ_TIME_DEFAULT>;
|
||||
zephyr,input-positive = <NRF_SAADC_{pin_number}>;
|
||||
zephyr,resolution = <14>;
|
||||
zephyr,oversampling = <8>;
|
||||
}};
|
||||
}};
|
||||
"""
|
||||
)
|
||||
|
@@ -301,8 +301,7 @@ async def alarm_action_disarm_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -310,8 +309,7 @@ async def alarm_action_pending_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -319,8 +317,7 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -333,8 +330,7 @@ async def alarm_action_chime_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def alarm_action_ready_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
@@ -352,4 +348,3 @@ async def alarm_control_panel_is_armed_to_code(
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_global(alarm_control_panel_ns.using)
|
||||
cg.add_define("USE_ALARM_CONTROL_PANEL")
|
||||
|
@@ -34,17 +34,20 @@ SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = espImage.IMAGE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||
}
|
||||
),
|
||||
},
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
espImage.IMAGE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||
}
|
||||
),
|
||||
},
|
||||
),
|
||||
espImage.validate_settings,
|
||||
)
|
||||
|
||||
|
||||
|
@@ -29,7 +29,7 @@ from esphome.core import CORE, coroutine_with_priority
|
||||
DOMAIN = "api"
|
||||
DEPENDENCIES = ["network"]
|
||||
AUTO_LOAD = ["socket"]
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
api_ns = cg.esphome_ns.namespace("api")
|
||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
|
||||
@@ -53,6 +53,7 @@ SERVICE_ARG_NATIVE_TYPES = {
|
||||
CONF_ENCRYPTION = "encryption"
|
||||
CONF_BATCH_DELAY = "batch_delay"
|
||||
CONF_CUSTOM_SERVICES = "custom_services"
|
||||
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||
|
||||
|
||||
@@ -119,6 +120,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Range(max=cv.TimePeriod(milliseconds=65535)),
|
||||
),
|
||||
cv.Optional(CONF_CUSTOM_SERVICES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HOMEASSISTANT_SERVICES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HOMEASSISTANT_STATES, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ON_CLIENT_CONNECTED): automation.validate_automation(
|
||||
single=True
|
||||
@@ -148,6 +150,9 @@ async def to_code(config):
|
||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||
cg.add_define("USE_API_SERVICES")
|
||||
|
||||
if config[CONF_HOMEASSISTANT_SERVICES]:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
|
||||
if config[CONF_HOMEASSISTANT_STATES]:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||
|
||||
@@ -240,6 +245,7 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA,
|
||||
)
|
||||
async def homeassistant_service_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, False)
|
||||
templ = await cg.templatable(config[CONF_ACTION], args, None)
|
||||
@@ -283,6 +289,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
|
||||
HOMEASSISTANT_EVENT_ACTION_SCHEMA,
|
||||
)
|
||||
async def homeassistant_event_to_code(config, action_id, template_arg, args):
|
||||
cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
|
||||
serv = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, serv, True)
|
||||
templ = await cg.templatable(config[CONF_EVENT], args, None)
|
||||
|
@@ -250,8 +250,8 @@ message DeviceInfoResponse {
|
||||
// Supports receiving and saving api encryption key
|
||||
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
|
||||
|
||||
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES"];
|
||||
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS"];
|
||||
repeated DeviceInfo devices = 20 [(field_ifdef) = "USE_DEVICES", (fixed_array_size_define) = "ESPHOME_DEVICE_COUNT"];
|
||||
repeated AreaInfo areas = 21 [(field_ifdef) = "USE_AREAS", (fixed_array_size_define) = "ESPHOME_AREA_COUNT"];
|
||||
|
||||
// Top-level area info to phase out suggested_area
|
||||
AreaInfo area = 22 [(field_ifdef) = "USE_AREAS"];
|
||||
@@ -419,7 +419,7 @@ message ListEntitiesFanResponse {
|
||||
bool disabled_by_default = 9;
|
||||
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
EntityCategory entity_category = 11;
|
||||
repeated string supported_preset_modes = 12;
|
||||
repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"];
|
||||
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
|
||||
}
|
||||
// Deprecated in API version 1.6 - only used in deprecated fields
|
||||
@@ -500,7 +500,7 @@ message ListEntitiesLightResponse {
|
||||
string name = 3;
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
repeated ColorMode supported_color_modes = 12;
|
||||
repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
|
||||
// next four supports_* are for legacy clients, newer clients should use color modes
|
||||
// Deprecated in API version 1.6
|
||||
bool legacy_supports_brightness = 5 [deprecated=true];
|
||||
@@ -755,17 +755,19 @@ message NoiseEncryptionSetKeyResponse {
|
||||
message SubscribeHomeassistantServicesRequest {
|
||||
option (id) = 34;
|
||||
option (source) = SOURCE_CLIENT;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||
}
|
||||
|
||||
message HomeassistantServiceMap {
|
||||
string key = 1;
|
||||
string value = 2;
|
||||
string value = 2 [(no_zero_copy) = true];
|
||||
}
|
||||
|
||||
message HomeassistantServiceResponse {
|
||||
option (id) = 35;
|
||||
option (source) = SOURCE_SERVER;
|
||||
option (no_delay) = true;
|
||||
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
|
||||
|
||||
string service = 1;
|
||||
repeated HomeassistantServiceMap data = 2;
|
||||
@@ -964,7 +966,7 @@ message ListEntitiesClimateResponse {
|
||||
|
||||
bool supports_current_temperature = 5;
|
||||
bool supports_two_point_target_temperature = 6;
|
||||
repeated ClimateMode supported_modes = 7;
|
||||
repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
|
||||
float visual_min_temperature = 8;
|
||||
float visual_max_temperature = 9;
|
||||
float visual_target_temperature_step = 10;
|
||||
@@ -973,11 +975,11 @@ message ListEntitiesClimateResponse {
|
||||
// Deprecated in API version 1.5
|
||||
bool legacy_supports_away = 11 [deprecated=true];
|
||||
bool supports_action = 12;
|
||||
repeated ClimateFanMode supported_fan_modes = 13;
|
||||
repeated ClimateSwingMode supported_swing_modes = 14;
|
||||
repeated string supported_custom_fan_modes = 15;
|
||||
repeated ClimatePreset supported_presets = 16;
|
||||
repeated string supported_custom_presets = 17;
|
||||
repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
|
||||
repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
|
||||
repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
|
||||
repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"];
|
||||
repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"];
|
||||
bool disabled_by_default = 18;
|
||||
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
EntityCategory entity_category = 20;
|
||||
@@ -1117,7 +1119,7 @@ message ListEntitiesSelectResponse {
|
||||
reserved 4; // Deprecated: was string unique_id
|
||||
|
||||
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
|
||||
repeated string options = 6;
|
||||
repeated string options = 6 [(container_pointer) = "std::vector"];
|
||||
bool disabled_by_default = 7;
|
||||
EntityCategory entity_category = 8;
|
||||
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
|
||||
@@ -1295,6 +1297,9 @@ enum MediaPlayerState {
|
||||
MEDIA_PLAYER_STATE_IDLE = 1;
|
||||
MEDIA_PLAYER_STATE_PLAYING = 2;
|
||||
MEDIA_PLAYER_STATE_PAUSED = 3;
|
||||
MEDIA_PLAYER_STATE_ANNOUNCING = 4;
|
||||
MEDIA_PLAYER_STATE_OFF = 5;
|
||||
MEDIA_PLAYER_STATE_ON = 6;
|
||||
}
|
||||
enum MediaPlayerCommand {
|
||||
MEDIA_PLAYER_COMMAND_PLAY = 0;
|
||||
@@ -1302,6 +1307,15 @@ enum MediaPlayerCommand {
|
||||
MEDIA_PLAYER_COMMAND_STOP = 2;
|
||||
MEDIA_PLAYER_COMMAND_MUTE = 3;
|
||||
MEDIA_PLAYER_COMMAND_UNMUTE = 4;
|
||||
MEDIA_PLAYER_COMMAND_TOGGLE = 5;
|
||||
MEDIA_PLAYER_COMMAND_VOLUME_UP = 6;
|
||||
MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7;
|
||||
MEDIA_PLAYER_COMMAND_ENQUEUE = 8;
|
||||
MEDIA_PLAYER_COMMAND_REPEAT_ONE = 9;
|
||||
MEDIA_PLAYER_COMMAND_REPEAT_OFF = 10;
|
||||
MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11;
|
||||
MEDIA_PLAYER_COMMAND_TURN_ON = 12;
|
||||
MEDIA_PLAYER_COMMAND_TURN_OFF = 13;
|
||||
}
|
||||
enum MediaPlayerFormatPurpose {
|
||||
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0;
|
||||
@@ -1336,6 +1350,8 @@ message ListEntitiesMediaPlayerResponse {
|
||||
repeated MediaPlayerSupportedFormat supported_formats = 9;
|
||||
|
||||
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
|
||||
|
||||
uint32 feature_flags = 11;
|
||||
}
|
||||
message MediaPlayerStateResponse {
|
||||
option (id) = 64;
|
||||
@@ -1426,7 +1442,7 @@ message BluetoothLERawAdvertisementsResponse {
|
||||
}
|
||||
|
||||
enum BluetoothDeviceRequestType {
|
||||
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0;
|
||||
BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0 [deprecated = true]; // V1 removed, use V3 variants
|
||||
BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1;
|
||||
BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2;
|
||||
BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3;
|
||||
@@ -1466,21 +1482,39 @@ message BluetoothGATTGetServicesRequest {
|
||||
}
|
||||
|
||||
message BluetoothGATTDescriptor {
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
||||
uint32 handle = 2;
|
||||
|
||||
// New field for efficient UUID (v1.12+)
|
||||
// Only one of uuid or short_uuid will be set.
|
||||
// short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients.
|
||||
// 128-bit UUIDs always use the uuid field for backwards compatibility.
|
||||
uint32 short_uuid = 3; // 16-bit or 32-bit UUID
|
||||
}
|
||||
|
||||
message BluetoothGATTCharacteristic {
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
||||
uint32 handle = 2;
|
||||
uint32 properties = 3;
|
||||
repeated BluetoothGATTDescriptor descriptors = 4;
|
||||
|
||||
// New field for efficient UUID (v1.12+)
|
||||
// Only one of uuid or short_uuid will be set.
|
||||
// short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients.
|
||||
// 128-bit UUIDs always use the uuid field for backwards compatibility.
|
||||
uint32 short_uuid = 5; // 16-bit or 32-bit UUID
|
||||
}
|
||||
|
||||
message BluetoothGATTService {
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2];
|
||||
repeated uint64 uuid = 1 [(fixed_array_size) = 2, (fixed_array_skip_zero) = true];
|
||||
uint32 handle = 2;
|
||||
repeated BluetoothGATTCharacteristic characteristics = 3;
|
||||
|
||||
// New field for efficient UUID (v1.12+)
|
||||
// Only one of uuid or short_uuid will be set.
|
||||
// short_uuid is used for both 16-bit and 32-bit UUIDs with v1.12+ clients.
|
||||
// 128-bit UUIDs always use the uuid field for backwards compatibility.
|
||||
uint32 short_uuid = 4; // 16-bit or 32-bit UUID
|
||||
}
|
||||
|
||||
message BluetoothGATTGetServicesResponse {
|
||||
@@ -1489,7 +1523,7 @@ message BluetoothGATTGetServicesResponse {
|
||||
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||
|
||||
uint64 address = 1;
|
||||
repeated BluetoothGATTService services = 2 [(fixed_array_size) = 1];
|
||||
repeated BluetoothGATTService services = 2;
|
||||
}
|
||||
|
||||
message BluetoothGATTGetServicesDoneResponse {
|
||||
@@ -1587,7 +1621,10 @@ message BluetoothConnectionsFreeResponse {
|
||||
|
||||
uint32 free = 1;
|
||||
uint32 limit = 2;
|
||||
repeated uint64 allocated = 3;
|
||||
repeated uint64 allocated = 3 [
|
||||
(fixed_array_size_define) = "BLUETOOTH_PROXY_MAX_CONNECTIONS",
|
||||
(fixed_array_skip_zero) = true
|
||||
];
|
||||
}
|
||||
|
||||
message BluetoothGATTErrorResponse {
|
||||
@@ -1832,7 +1869,7 @@ message VoiceAssistantConfigurationResponse {
|
||||
option (ifdef) = "USE_VOICE_ASSISTANT";
|
||||
|
||||
repeated VoiceAssistantWakeWord available_wake_words = 1;
|
||||
repeated string active_wake_words = 2;
|
||||
repeated string active_wake_words = 2 [(container_pointer) = "std::vector"];
|
||||
uint32 max_active_wake_words = 3;
|
||||
}
|
||||
|
||||
|
@@ -112,8 +112,7 @@ void APIConnection::start() {
|
||||
APIError err = this->helper_->init();
|
||||
if (err != APIError::OK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Helper init failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||
errno);
|
||||
this->log_warning_("Helper init failed", err);
|
||||
return;
|
||||
}
|
||||
this->client_info_.peername = helper_->getpeername();
|
||||
@@ -144,8 +143,7 @@ void APIConnection::loop() {
|
||||
APIError err = this->helper_->loop();
|
||||
if (err != APIError::OK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(),
|
||||
api_error_to_str(err), errno);
|
||||
this->log_socket_operation_failed_(err);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -161,8 +159,7 @@ void APIConnection::loop() {
|
||||
break;
|
||||
} else if (err != APIError::OK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Reading failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||
errno);
|
||||
this->log_warning_("Reading failed", err);
|
||||
return;
|
||||
} else {
|
||||
this->last_traffic_ = now;
|
||||
@@ -244,21 +241,7 @@ void APIConnection::loop() {
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
if (state_subs_at_ >= 0) {
|
||||
const auto &subs = this->parent_->get_state_subs();
|
||||
if (state_subs_at_ < static_cast<int>(subs.size())) {
|
||||
auto &it = subs[state_subs_at_];
|
||||
SubscribeHomeAssistantStateResponse resp;
|
||||
resp.set_entity_id(StringRef(it.entity_id));
|
||||
// attribute.value() returns temporary - must store it
|
||||
std::string attribute_value = it.attribute.value();
|
||||
resp.set_attribute(StringRef(attribute_value));
|
||||
resp.once = it.once;
|
||||
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
|
||||
state_subs_at_++;
|
||||
}
|
||||
} else {
|
||||
state_subs_at_ = -1;
|
||||
}
|
||||
this->process_state_subscriptions_();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
@@ -290,8 +273,9 @@ uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t mess
|
||||
#endif
|
||||
|
||||
// Calculate size
|
||||
uint32_t calculated_size = 0;
|
||||
msg.calculate_size(calculated_size);
|
||||
ProtoSize size_calc;
|
||||
msg.calculate_size(size_calc);
|
||||
uint32_t calculated_size = size_calc.get_size();
|
||||
|
||||
// Cache frame sizes to avoid repeated virtual calls
|
||||
const uint8_t header_padding = conn->helper_->frame_header_padding();
|
||||
@@ -426,8 +410,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
|
||||
msg.supports_speed = traits.supports_speed();
|
||||
msg.supports_direction = traits.supports_direction();
|
||||
msg.supported_speed_count = traits.supported_speed_count();
|
||||
for (auto const &preset : traits.supported_preset_modes())
|
||||
msg.supported_preset_modes.push_back(preset);
|
||||
msg.supported_preset_modes = &traits.supported_preset_modes_for_api_();
|
||||
return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
|
||||
@@ -483,8 +466,7 @@ 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();
|
||||
for (auto mode : traits.get_supported_color_modes())
|
||||
msg.supported_color_modes.push_back(static_cast<enums::ColorMode>(mode));
|
||||
msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
|
||||
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();
|
||||
@@ -644,17 +626,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
|
||||
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
|
||||
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
|
||||
if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) {
|
||||
// custom_fan_mode.value() returns temporary - must store it
|
||||
std::string custom_fan_mode = climate->custom_fan_mode.value();
|
||||
resp.set_custom_fan_mode(StringRef(custom_fan_mode));
|
||||
resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value()));
|
||||
}
|
||||
if (traits.get_supports_presets() && climate->preset.has_value()) {
|
||||
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) {
|
||||
// custom_preset.value() returns temporary - must store it
|
||||
std::string custom_preset = climate->custom_preset.value();
|
||||
resp.set_custom_preset(StringRef(custom_preset));
|
||||
resp.set_custom_preset(StringRef(climate->custom_preset.value()));
|
||||
}
|
||||
if (traits.get_supports_swing_modes())
|
||||
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
|
||||
@@ -674,8 +652,7 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
||||
msg.supports_current_humidity = traits.get_supports_current_humidity();
|
||||
msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
|
||||
msg.supports_target_humidity = traits.get_supports_target_humidity();
|
||||
for (auto mode : traits.get_supported_modes())
|
||||
msg.supported_modes.push_back(static_cast<enums::ClimateMode>(mode));
|
||||
msg.supported_modes = &traits.get_supported_modes_for_api_();
|
||||
msg.visual_min_temperature = traits.get_visual_min_temperature();
|
||||
msg.visual_max_temperature = traits.get_visual_max_temperature();
|
||||
msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
|
||||
@@ -683,16 +660,11 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
|
||||
msg.visual_min_humidity = traits.get_visual_min_humidity();
|
||||
msg.visual_max_humidity = traits.get_visual_max_humidity();
|
||||
msg.supports_action = traits.get_supports_action();
|
||||
for (auto fan_mode : traits.get_supported_fan_modes())
|
||||
msg.supported_fan_modes.push_back(static_cast<enums::ClimateFanMode>(fan_mode));
|
||||
for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes())
|
||||
msg.supported_custom_fan_modes.push_back(custom_fan_mode);
|
||||
for (auto preset : traits.get_supported_presets())
|
||||
msg.supported_presets.push_back(static_cast<enums::ClimatePreset>(preset));
|
||||
for (auto const &custom_preset : traits.get_supported_custom_presets())
|
||||
msg.supported_custom_presets.push_back(custom_preset);
|
||||
for (auto swing_mode : traits.get_supported_swing_modes())
|
||||
msg.supported_swing_modes.push_back(static_cast<enums::ClimateSwingMode>(swing_mode));
|
||||
msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
|
||||
msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
|
||||
msg.supported_presets = &traits.get_supported_presets_for_api_();
|
||||
msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_();
|
||||
msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_();
|
||||
return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -898,8 +870,7 @@ uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *
|
||||
bool is_single) {
|
||||
auto *select = static_cast<select::Select *>(entity);
|
||||
ListEntitiesSelectResponse msg;
|
||||
for (const auto &option : select->traits.get_options())
|
||||
msg.options.push_back(option);
|
||||
msg.options = &select->traits.get_options();
|
||||
return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size,
|
||||
is_single);
|
||||
}
|
||||
@@ -1025,6 +996,7 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
ListEntitiesMediaPlayerResponse msg;
|
||||
auto traits = media_player->get_traits();
|
||||
msg.supports_pause = traits.get_supports_pause();
|
||||
msg.feature_flags = traits.get_feature_flags();
|
||||
for (auto &supported_format : traits.get_supported_formats()) {
|
||||
msg.supported_formats.emplace_back();
|
||||
auto &media_format = msg.supported_formats.back();
|
||||
@@ -1133,10 +1105,8 @@ void APIConnection::bluetooth_gatt_notify(const BluetoothGATTNotifyRequest &msg)
|
||||
|
||||
bool APIConnection::send_subscribe_bluetooth_connections_free_response(
|
||||
const SubscribeBluetoothConnectionsFreeRequest &msg) {
|
||||
BluetoothConnectionsFreeResponse resp;
|
||||
resp.free = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_free();
|
||||
resp.limit = bluetooth_proxy::global_bluetooth_proxy->get_bluetooth_connections_limit();
|
||||
return this->send_message(resp, BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||
bluetooth_proxy::global_bluetooth_proxy->send_connections_free(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void APIConnection::bluetooth_scanner_set_mode(const BluetoothScannerSetModeRequest &msg) {
|
||||
@@ -1213,9 +1183,7 @@ bool APIConnection::send_voice_assistant_get_configuration_response(const VoiceA
|
||||
resp_wake_word.trained_languages.push_back(lang);
|
||||
}
|
||||
}
|
||||
for (auto &wake_word_id : config.active_wake_words) {
|
||||
resp.active_wake_words.push_back(wake_word_id);
|
||||
}
|
||||
resp.active_wake_words = &config.active_wake_words;
|
||||
resp.max_active_wake_words = config.max_active_wake_words;
|
||||
return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
|
||||
}
|
||||
@@ -1393,7 +1361,7 @@ bool APIConnection::send_hello_response(const HelloRequest &msg) {
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 10;
|
||||
resp.api_version_minor = 12;
|
||||
// Temporary string for concatenation - will be valid during send_message call
|
||||
std::string server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")";
|
||||
resp.set_server_info(StringRef(server_info));
|
||||
@@ -1494,18 +1462,22 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
|
||||
resp.api_encryption_supported = true;
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
size_t device_index = 0;
|
||||
for (auto const &device : App.get_devices()) {
|
||||
resp.devices.emplace_back();
|
||||
auto &device_info = resp.devices.back();
|
||||
if (device_index >= ESPHOME_DEVICE_COUNT)
|
||||
break;
|
||||
auto &device_info = resp.devices[device_index++];
|
||||
device_info.device_id = device->get_device_id();
|
||||
device_info.set_name(StringRef(device->get_name()));
|
||||
device_info.area_id = device->get_area_id();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
size_t area_index = 0;
|
||||
for (auto const &area : App.get_areas()) {
|
||||
resp.areas.emplace_back();
|
||||
auto &area_info = resp.areas.back();
|
||||
if (area_index >= ESPHOME_AREA_COUNT)
|
||||
break;
|
||||
auto &area_info = resp.areas[area_index++];
|
||||
area_info.area_id = area->get_area_id();
|
||||
area_info.set_name(StringRef(area->get_name()));
|
||||
}
|
||||
@@ -1538,19 +1510,18 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
|
||||
psk_t psk{};
|
||||
NoiseEncryptionSetKeyResponse resp;
|
||||
resp.success = false;
|
||||
|
||||
psk_t psk{};
|
||||
if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
resp.success = false;
|
||||
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
|
||||
}
|
||||
if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
} else if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
ESP_LOGW(TAG, "Failed to save encryption key");
|
||||
resp.success = false;
|
||||
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
|
||||
} else {
|
||||
resp.success = true;
|
||||
}
|
||||
resp.success = true;
|
||||
|
||||
return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#endif
|
||||
@@ -1568,8 +1539,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
|
||||
APIError err = this->helper_->loop();
|
||||
if (err != APIError::OK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Socket operation failed %s errno=%d", this->get_client_combined_info().c_str(),
|
||||
api_error_to_str(err), errno);
|
||||
this->log_socket_operation_failed_(err);
|
||||
return false;
|
||||
}
|
||||
if (this->helper_->can_write_without_blocking())
|
||||
@@ -1589,8 +1559,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
|
||||
return false;
|
||||
if (err != APIError::OK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(),
|
||||
api_error_to_str(err), errno);
|
||||
this->log_warning_("Packet write failed", err);
|
||||
return false;
|
||||
}
|
||||
// Do not set last_traffic_ on send
|
||||
@@ -1675,6 +1644,8 @@ void APIConnection::process_batch_() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get shared buffer reference once to avoid multiple calls
|
||||
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
size_t num_items = this->deferred_batch_.size();
|
||||
|
||||
// Fast path for single message - allocate exact size needed
|
||||
@@ -1685,8 +1656,7 @@ void APIConnection::process_batch_() {
|
||||
uint16_t payload_size =
|
||||
item.creator(item.entity, this, std::numeric_limits<uint16_t>::max(), true, item.message_type);
|
||||
|
||||
if (payload_size > 0 &&
|
||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, item.message_type)) {
|
||||
if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&shared_buf}, item.message_type)) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
// Log messages after send attempt for VV debugging
|
||||
// It's safe to use the buffer for logging at this point regardless of send result
|
||||
@@ -1713,20 +1683,18 @@ void APIConnection::process_batch_() {
|
||||
const uint8_t footer_size = this->helper_->frame_footer_size();
|
||||
|
||||
// Initialize buffer and tracking variables
|
||||
this->parent_->get_shared_buffer_ref().clear();
|
||||
shared_buf.clear();
|
||||
|
||||
// Pre-calculate exact buffer size needed based on message types
|
||||
uint32_t total_estimated_size = 0;
|
||||
uint32_t total_estimated_size = num_items * (header_padding + footer_size);
|
||||
for (size_t i = 0; i < this->deferred_batch_.size(); i++) {
|
||||
const auto &item = this->deferred_batch_[i];
|
||||
total_estimated_size += item.estimated_size;
|
||||
}
|
||||
|
||||
// Calculate total overhead for all messages
|
||||
uint32_t total_overhead = (header_padding + footer_size) * num_items;
|
||||
|
||||
// Reserve based on estimated size (much more accurate than 24-byte worst-case)
|
||||
this->parent_->get_shared_buffer_ref().reserve(total_estimated_size + total_overhead);
|
||||
shared_buf.reserve(total_estimated_size);
|
||||
this->flags_.batch_first_message = true;
|
||||
|
||||
size_t items_processed = 0;
|
||||
@@ -1768,7 +1736,7 @@ void APIConnection::process_batch_() {
|
||||
remaining_size -= payload_size;
|
||||
// Calculate where the next message's header padding will start
|
||||
// Current buffer size + footer space (that prepare_message_buffer will add for this message)
|
||||
current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size;
|
||||
current_offset = shared_buf.size() + footer_size;
|
||||
}
|
||||
|
||||
if (items_processed == 0) {
|
||||
@@ -1778,17 +1746,15 @@ void APIConnection::process_batch_() {
|
||||
|
||||
// Add footer space for the last message (for Noise protocol MAC)
|
||||
if (footer_size > 0) {
|
||||
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
}
|
||||
|
||||
// Send all collected packets
|
||||
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()},
|
||||
APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
|
||||
std::span<const PacketInfo>(packet_info, packet_count));
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
on_fatal_error();
|
||||
ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), api_error_to_str(err),
|
||||
errno);
|
||||
this->log_warning_("Batch write failed", err);
|
||||
}
|
||||
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1844,5 +1810,33 @@ uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection
|
||||
return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size, is_single);
|
||||
}
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIConnection::process_state_subscriptions_() {
|
||||
const auto &subs = this->parent_->get_state_subs();
|
||||
if (this->state_subs_at_ >= static_cast<int>(subs.size())) {
|
||||
this->state_subs_at_ = -1;
|
||||
return;
|
||||
}
|
||||
|
||||
const auto &it = subs[this->state_subs_at_];
|
||||
SubscribeHomeAssistantStateResponse resp;
|
||||
resp.set_entity_id(StringRef(it.entity_id));
|
||||
|
||||
// Avoid string copy by directly using the optional's value if it exists
|
||||
resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef(""));
|
||||
|
||||
resp.once = it.once;
|
||||
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
|
||||
this->state_subs_at_++;
|
||||
}
|
||||
}
|
||||
#endif // USE_API_HOMEASSISTANT_STATES
|
||||
|
||||
void APIConnection::log_warning_(const char *message, APIError err) {
|
||||
ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), message, api_error_to_str(err), errno);
|
||||
}
|
||||
|
||||
void APIConnection::log_socket_operation_failed_(APIError err) { this->log_warning_("Socket operation failed", err); }
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
@@ -131,11 +131,13 @@ class APIConnection : public APIServerConnection {
|
||||
void media_player_command(const MediaPlayerCommandRequest &msg) override;
|
||||
#endif
|
||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||
if (!this->flags_.service_call_subscription)
|
||||
return;
|
||||
this->send_message(call, HomeassistantServiceResponse::MESSAGE_TYPE);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
|
||||
@@ -209,9 +211,11 @@ class APIConnection : public APIServerConnection {
|
||||
if (msg.dump_config)
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) override {
|
||||
this->flags_.service_call_subscription = true;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
#endif
|
||||
@@ -231,6 +235,13 @@ class APIConnection : public APIServerConnection {
|
||||
this->is_authenticated();
|
||||
}
|
||||
uint8_t get_log_subscription_level() const { return this->flags_.log_subscription; }
|
||||
|
||||
// Get client API version for feature detection
|
||||
bool client_supports_api_version(uint16_t major, uint16_t minor) const {
|
||||
return this->client_api_version_major_ > major ||
|
||||
(this->client_api_version_major_ == major && this->client_api_version_minor_ >= minor);
|
||||
}
|
||||
|
||||
void on_fatal_error() override;
|
||||
#ifdef USE_API_PASSWORD
|
||||
void on_unauthenticated_access() override;
|
||||
@@ -294,6 +305,10 @@ class APIConnection : public APIServerConnection {
|
||||
// Helper function to handle authentication completion
|
||||
void complete_authentication_();
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void process_state_subscriptions_();
|
||||
#endif
|
||||
|
||||
// Non-template helper to encode any ProtoMessage
|
||||
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
|
||||
uint32_t remaining_size, bool is_single);
|
||||
@@ -688,10 +703,16 @@ class APIConnection : public APIServerConnection {
|
||||
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
|
||||
uint8_t estimated_size) {
|
||||
// Try to send immediately if:
|
||||
// 1. We should try to send immediately (should_try_send_immediately = true)
|
||||
// 2. Batch delay is 0 (user has opted in to immediate sending)
|
||||
// 3. Buffer has space available
|
||||
if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
|
||||
// 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()) {
|
||||
// Now actually encode and send
|
||||
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
|
||||
@@ -728,6 +749,11 @@ class APIConnection : public APIServerConnection {
|
||||
this->deferred_batch_.add_item_front(entity, MessageCreator(function_ptr), message_type, estimated_size);
|
||||
return this->schedule_batch_();
|
||||
}
|
||||
|
||||
// Helper function to log API errors with errno
|
||||
void log_warning_(const char *message, APIError err);
|
||||
// Specific helper for duplicated error message
|
||||
void log_socket_operation_failed_(APIError err);
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
@@ -27,4 +27,33 @@ extend google.protobuf.MessageOptions {
|
||||
extend google.protobuf.FieldOptions {
|
||||
optional string field_ifdef = 1042;
|
||||
optional uint32 fixed_array_size = 50007;
|
||||
optional bool no_zero_copy = 50008 [default=false];
|
||||
optional bool fixed_array_skip_zero = 50009 [default=false];
|
||||
optional string fixed_array_size_define = 50010;
|
||||
|
||||
// container_pointer: Zero-copy optimization for repeated fields.
|
||||
//
|
||||
// When container_pointer is set on a repeated field, the generated message will
|
||||
// store a pointer to an existing container instead of copying the data into the
|
||||
// message's own repeated field. This eliminates heap allocations and improves performance.
|
||||
//
|
||||
// Requirements for safe usage:
|
||||
// 1. The source container must remain valid until the message is encoded
|
||||
// 2. Messages must be encoded immediately (which ESPHome does by default)
|
||||
// 3. The container type must match the field type exactly
|
||||
//
|
||||
// Supported container types:
|
||||
// - "std::vector<T>" for most repeated fields
|
||||
// - "std::set<T>" for unique/sorted data
|
||||
// - Full type specification required for enums (e.g., "std::set<climate::ClimateMode>")
|
||||
//
|
||||
// Example usage in .proto file:
|
||||
// repeated string supported_modes = 12 [(container_pointer) = "std::set"];
|
||||
// repeated ColorMode color_modes = 13 [(container_pointer) = "std::set<light::ColorMode>"];
|
||||
//
|
||||
// The corresponding C++ code must provide const reference access to a container
|
||||
// that matches the specified type and remains valid during message encoding.
|
||||
// This is typically done through methods returning const T& or special accessor
|
||||
// methods like get_options() or supported_modes_for_api_().
|
||||
optional string container_pointer = 50001;
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
#include "proto.h"
|
||||
#include "api_pb2_includes.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -149,6 +150,9 @@ enum MediaPlayerState : uint32_t {
|
||||
MEDIA_PLAYER_STATE_IDLE = 1,
|
||||
MEDIA_PLAYER_STATE_PLAYING = 2,
|
||||
MEDIA_PLAYER_STATE_PAUSED = 3,
|
||||
MEDIA_PLAYER_STATE_ANNOUNCING = 4,
|
||||
MEDIA_PLAYER_STATE_OFF = 5,
|
||||
MEDIA_PLAYER_STATE_ON = 6,
|
||||
};
|
||||
enum MediaPlayerCommand : uint32_t {
|
||||
MEDIA_PLAYER_COMMAND_PLAY = 0,
|
||||
@@ -156,6 +160,15 @@ enum MediaPlayerCommand : uint32_t {
|
||||
MEDIA_PLAYER_COMMAND_STOP = 2,
|
||||
MEDIA_PLAYER_COMMAND_MUTE = 3,
|
||||
MEDIA_PLAYER_COMMAND_UNMUTE = 4,
|
||||
MEDIA_PLAYER_COMMAND_TOGGLE = 5,
|
||||
MEDIA_PLAYER_COMMAND_VOLUME_UP = 6,
|
||||
MEDIA_PLAYER_COMMAND_VOLUME_DOWN = 7,
|
||||
MEDIA_PLAYER_COMMAND_ENQUEUE = 8,
|
||||
MEDIA_PLAYER_COMMAND_REPEAT_ONE = 9,
|
||||
MEDIA_PLAYER_COMMAND_REPEAT_OFF = 10,
|
||||
MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST = 11,
|
||||
MEDIA_PLAYER_COMMAND_TURN_ON = 12,
|
||||
MEDIA_PLAYER_COMMAND_TURN_OFF = 13,
|
||||
};
|
||||
enum MediaPlayerFormatPurpose : uint32_t {
|
||||
MEDIA_PLAYER_FORMAT_PURPOSE_DEFAULT = 0,
|
||||
@@ -340,7 +353,7 @@ class HelloResponse : public ProtoMessage {
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -371,14 +384,14 @@ class ConnectResponse : public ProtoMessage {
|
||||
#endif
|
||||
bool invalid_password{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class DisconnectRequest : public ProtoDecodableMessage {
|
||||
class DisconnectRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 5;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -391,7 +404,7 @@ class DisconnectRequest : public ProtoDecodableMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class DisconnectResponse : public ProtoDecodableMessage {
|
||||
class DisconnectResponse : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 6;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -404,7 +417,7 @@ class DisconnectResponse : public ProtoDecodableMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class PingRequest : public ProtoDecodableMessage {
|
||||
class PingRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 7;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -417,7 +430,7 @@ class PingRequest : public ProtoDecodableMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class PingResponse : public ProtoDecodableMessage {
|
||||
class PingResponse : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 8;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -430,7 +443,7 @@ class PingResponse : public ProtoDecodableMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class DeviceInfoRequest : public ProtoDecodableMessage {
|
||||
class DeviceInfoRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 9;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -450,7 +463,7 @@ class AreaInfo : public ProtoMessage {
|
||||
StringRef name_ref_{};
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -466,7 +479,7 @@ class DeviceInfo : public ProtoMessage {
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
uint32_t area_id{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -477,7 +490,7 @@ class DeviceInfo : public ProtoMessage {
|
||||
class DeviceInfoResponse : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 10;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 211;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 247;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "device_info_response"; }
|
||||
#endif
|
||||
@@ -530,23 +543,23 @@ class DeviceInfoResponse : public ProtoMessage {
|
||||
bool api_encryption_supported{false};
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
std::vector<DeviceInfo> devices{};
|
||||
std::array<DeviceInfo, ESPHOME_DEVICE_COUNT> devices{};
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
std::vector<AreaInfo> areas{};
|
||||
std::array<AreaInfo, ESPHOME_AREA_COUNT> areas{};
|
||||
#endif
|
||||
#ifdef USE_AREAS
|
||||
AreaInfo area{};
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class ListEntitiesRequest : public ProtoDecodableMessage {
|
||||
class ListEntitiesRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 11;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -572,7 +585,7 @@ class ListEntitiesDoneResponse : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class SubscribeStatesRequest : public ProtoDecodableMessage {
|
||||
class SubscribeStatesRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 20;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -597,7 +610,7 @@ class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
bool is_status_binary_sensor{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -614,7 +627,7 @@ class BinarySensorStateResponse : public StateResponseProtoMessage {
|
||||
bool state{false};
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -637,7 +650,7 @@ class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
bool supports_stop{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -655,7 +668,7 @@ class CoverStateResponse : public StateResponseProtoMessage {
|
||||
float tilt{0.0f};
|
||||
enums::CoverOperation current_operation{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -695,9 +708,9 @@ class ListEntitiesFanResponse : public InfoResponseProtoMessage {
|
||||
bool supports_speed{false};
|
||||
bool supports_direction{false};
|
||||
int32_t supported_speed_count{0};
|
||||
std::vector<std::string> supported_preset_modes{};
|
||||
const std::set<std::string> *supported_preset_modes{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -718,7 +731,7 @@ class FanStateResponse : public StateResponseProtoMessage {
|
||||
StringRef preset_mode_ref_{};
|
||||
void set_preset_mode(const StringRef &ref) { this->preset_mode_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -760,12 +773,12 @@ class ListEntitiesLightResponse : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_light_response"; }
|
||||
#endif
|
||||
std::vector<enums::ColorMode> supported_color_modes{};
|
||||
const std::set<light::ColorMode> *supported_color_modes{};
|
||||
float min_mireds{0.0f};
|
||||
float max_mireds{0.0f};
|
||||
std::vector<std::string> effects{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -793,7 +806,7 @@ class LightStateResponse : public StateResponseProtoMessage {
|
||||
StringRef effect_ref_{};
|
||||
void set_effect(const StringRef &ref) { this->effect_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -859,7 +872,7 @@ class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
enums::SensorStateClass state_class{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -876,7 +889,7 @@ class SensorStateResponse : public StateResponseProtoMessage {
|
||||
float state{0.0f};
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -896,7 +909,7 @@ class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -912,7 +925,7 @@ class SwitchStateResponse : public StateResponseProtoMessage {
|
||||
#endif
|
||||
bool state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -947,7 +960,7 @@ class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -965,7 +978,7 @@ class TextSensorStateResponse : public StateResponseProtoMessage {
|
||||
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1004,7 +1017,7 @@ class SubscribeLogsResponse : public ProtoMessage {
|
||||
this->message_len_ = len;
|
||||
}
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1036,7 +1049,7 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage {
|
||||
#endif
|
||||
bool success{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1044,7 +1057,8 @@ class NoiseEncryptionSetKeyResponse : public ProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
class SubscribeHomeassistantServicesRequest : public ProtoDecodableMessage {
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
class SubscribeHomeassistantServicesRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 34;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -1061,10 +1075,9 @@ class HomeassistantServiceMap : public ProtoMessage {
|
||||
public:
|
||||
StringRef key_ref_{};
|
||||
void set_key(const StringRef &ref) { this->key_ref_ = ref; }
|
||||
StringRef value_ref_{};
|
||||
void set_value(const StringRef &ref) { this->value_ref_ = ref; }
|
||||
std::string value{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1085,15 +1098,16 @@ class HomeassistantServiceResponse : public ProtoMessage {
|
||||
std::vector<HomeassistantServiceMap> variables{};
|
||||
bool is_event{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
class SubscribeHomeAssistantStatesRequest : public ProtoDecodableMessage {
|
||||
class SubscribeHomeAssistantStatesRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 38;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -1119,7 +1133,7 @@ class SubscribeHomeAssistantStateResponse : public ProtoMessage {
|
||||
void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; }
|
||||
bool once{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1144,7 +1158,7 @@ class HomeAssistantStateResponse : public ProtoDecodableMessage {
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
};
|
||||
#endif
|
||||
class GetTimeRequest : public ProtoDecodableMessage {
|
||||
class GetTimeRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 36;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -1166,7 +1180,7 @@ class GetTimeResponse : public ProtoDecodableMessage {
|
||||
#endif
|
||||
uint32_t epoch_seconds{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1181,7 +1195,7 @@ class ListEntitiesServicesArgument : public ProtoMessage {
|
||||
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
|
||||
enums::ServiceArgType type{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1200,7 +1214,7 @@ class ListEntitiesServicesResponse : public ProtoMessage {
|
||||
uint32_t key{0};
|
||||
std::vector<ListEntitiesServicesArgument> args{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1254,7 +1268,7 @@ class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
|
||||
const char *message_name() const override { return "list_entities_camera_response"; }
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1276,7 +1290,7 @@ class CameraImageResponse : public StateResponseProtoMessage {
|
||||
}
|
||||
bool done{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1310,23 +1324,23 @@ class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
|
||||
#endif
|
||||
bool supports_current_temperature{false};
|
||||
bool supports_two_point_target_temperature{false};
|
||||
std::vector<enums::ClimateMode> supported_modes{};
|
||||
const std::set<climate::ClimateMode> *supported_modes{};
|
||||
float visual_min_temperature{0.0f};
|
||||
float visual_max_temperature{0.0f};
|
||||
float visual_target_temperature_step{0.0f};
|
||||
bool supports_action{false};
|
||||
std::vector<enums::ClimateFanMode> supported_fan_modes{};
|
||||
std::vector<enums::ClimateSwingMode> supported_swing_modes{};
|
||||
std::vector<std::string> supported_custom_fan_modes{};
|
||||
std::vector<enums::ClimatePreset> supported_presets{};
|
||||
std::vector<std::string> supported_custom_presets{};
|
||||
const std::set<climate::ClimateFanMode> *supported_fan_modes{};
|
||||
const std::set<climate::ClimateSwingMode> *supported_swing_modes{};
|
||||
const std::set<std::string> *supported_custom_fan_modes{};
|
||||
const std::set<climate::ClimatePreset> *supported_presets{};
|
||||
const std::set<std::string> *supported_custom_presets{};
|
||||
float visual_current_temperature_step{0.0f};
|
||||
bool supports_current_humidity{false};
|
||||
bool supports_target_humidity{false};
|
||||
float visual_min_humidity{0.0f};
|
||||
float visual_max_humidity{0.0f};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1356,7 +1370,7 @@ class ClimateStateResponse : public StateResponseProtoMessage {
|
||||
float current_humidity{0.0f};
|
||||
float target_humidity{0.0f};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1417,7 +1431,7 @@ class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1434,7 +1448,7 @@ class NumberStateResponse : public StateResponseProtoMessage {
|
||||
float state{0.0f};
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1466,9 +1480,9 @@ class ListEntitiesSelectResponse : public InfoResponseProtoMessage {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_select_response"; }
|
||||
#endif
|
||||
std::vector<std::string> options{};
|
||||
const std::vector<std::string> *options{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1486,7 +1500,7 @@ class SelectStateResponse : public StateResponseProtoMessage {
|
||||
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1523,7 +1537,7 @@ class ListEntitiesSirenResponse : public InfoResponseProtoMessage {
|
||||
bool supports_duration{false};
|
||||
bool supports_volume{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1539,7 +1553,7 @@ class SirenStateResponse : public StateResponseProtoMessage {
|
||||
#endif
|
||||
bool state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1585,7 +1599,7 @@ class ListEntitiesLockResponse : public InfoResponseProtoMessage {
|
||||
StringRef code_format_ref_{};
|
||||
void set_code_format(const StringRef &ref) { this->code_format_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1601,7 +1615,7 @@ class LockStateResponse : public StateResponseProtoMessage {
|
||||
#endif
|
||||
enums::LockState state{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1639,7 +1653,7 @@ class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1672,7 +1686,7 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
|
||||
enums::MediaPlayerFormatPurpose purpose{};
|
||||
uint32_t sample_bytes{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1682,14 +1696,15 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
|
||||
class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 63;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 76;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 80;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "list_entities_media_player_response"; }
|
||||
#endif
|
||||
bool supports_pause{false};
|
||||
std::vector<MediaPlayerSupportedFormat> supported_formats{};
|
||||
uint32_t feature_flags{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1707,7 +1722,7 @@ class MediaPlayerStateResponse : public StateResponseProtoMessage {
|
||||
float volume{0.0f};
|
||||
bool muted{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1763,7 +1778,7 @@ class BluetoothLERawAdvertisement : public ProtoMessage {
|
||||
uint8_t data[62]{};
|
||||
uint8_t data_len{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1779,7 +1794,7 @@ class BluetoothLERawAdvertisementsResponse : public ProtoMessage {
|
||||
#endif
|
||||
std::vector<BluetoothLERawAdvertisement> advertisements{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1816,7 +1831,7 @@ class BluetoothDeviceConnectionResponse : public ProtoMessage {
|
||||
uint32_t mtu{0};
|
||||
int32_t error{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1842,8 +1857,9 @@ class BluetoothGATTDescriptor : public ProtoMessage {
|
||||
public:
|
||||
std::array<uint64_t, 2> uuid{};
|
||||
uint32_t handle{0};
|
||||
uint32_t short_uuid{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1856,8 +1872,9 @@ class BluetoothGATTCharacteristic : public ProtoMessage {
|
||||
uint32_t handle{0};
|
||||
uint32_t properties{0};
|
||||
std::vector<BluetoothGATTDescriptor> descriptors{};
|
||||
uint32_t short_uuid{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1869,8 +1886,9 @@ class BluetoothGATTService : public ProtoMessage {
|
||||
std::array<uint64_t, 2> uuid{};
|
||||
uint32_t handle{0};
|
||||
std::vector<BluetoothGATTCharacteristic> characteristics{};
|
||||
uint32_t short_uuid{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1880,14 +1898,14 @@ class BluetoothGATTService : public ProtoMessage {
|
||||
class BluetoothGATTGetServicesResponse : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 71;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 21;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 38;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_gatt_get_services_response"; }
|
||||
#endif
|
||||
uint64_t address{0};
|
||||
std::array<BluetoothGATTService, 1> services{};
|
||||
std::vector<BluetoothGATTService> services{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1903,7 +1921,7 @@ class BluetoothGATTGetServicesDoneResponse : public ProtoMessage {
|
||||
#endif
|
||||
uint64_t address{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -1942,7 +1960,7 @@ class BluetoothGATTReadResponse : public ProtoMessage {
|
||||
this->data_len_ = len;
|
||||
}
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2035,14 +2053,14 @@ class BluetoothGATTNotifyDataResponse : public ProtoMessage {
|
||||
this->data_len_ = len;
|
||||
}
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class SubscribeBluetoothConnectionsFreeRequest : public ProtoDecodableMessage {
|
||||
class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 80;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -2058,15 +2076,15 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoDecodableMessage {
|
||||
class BluetoothConnectionsFreeResponse : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 81;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 16;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 20;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "bluetooth_connections_free_response"; }
|
||||
#endif
|
||||
uint32_t free{0};
|
||||
uint32_t limit{0};
|
||||
std::vector<uint64_t> allocated{};
|
||||
std::array<uint64_t, BLUETOOTH_PROXY_MAX_CONNECTIONS> allocated{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2084,7 +2102,7 @@ class BluetoothGATTErrorResponse : public ProtoMessage {
|
||||
uint32_t handle{0};
|
||||
int32_t error{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2101,7 +2119,7 @@ class BluetoothGATTWriteResponse : public ProtoMessage {
|
||||
uint64_t address{0};
|
||||
uint32_t handle{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2118,7 +2136,7 @@ class BluetoothGATTNotifyResponse : public ProtoMessage {
|
||||
uint64_t address{0};
|
||||
uint32_t handle{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2136,7 +2154,7 @@ class BluetoothDevicePairingResponse : public ProtoMessage {
|
||||
bool paired{false};
|
||||
int32_t error{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2154,14 +2172,14 @@ class BluetoothDeviceUnpairingResponse : public ProtoMessage {
|
||||
bool success{false};
|
||||
int32_t error{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoDecodableMessage {
|
||||
class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 87;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -2185,7 +2203,7 @@ class BluetoothDeviceClearCacheResponse : public ProtoMessage {
|
||||
bool success{false};
|
||||
int32_t error{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2202,7 +2220,7 @@ class BluetoothScannerStateResponse : public ProtoMessage {
|
||||
enums::BluetoothScannerState state{};
|
||||
enums::BluetoothScannerMode mode{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2248,7 +2266,7 @@ class VoiceAssistantAudioSettings : public ProtoMessage {
|
||||
uint32_t auto_gain{0};
|
||||
float volume_multiplier{0.0f};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2270,7 +2288,7 @@ class VoiceAssistantRequest : public ProtoMessage {
|
||||
StringRef wake_word_phrase_ref_{};
|
||||
void set_wake_word_phrase(const StringRef &ref) { this->wake_word_phrase_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2337,7 +2355,7 @@ class VoiceAssistantAudio : public ProtoDecodableMessage {
|
||||
}
|
||||
bool end{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2395,7 +2413,7 @@ class VoiceAssistantAnnounceFinished : public ProtoMessage {
|
||||
#endif
|
||||
bool success{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2410,14 +2428,14 @@ class VoiceAssistantWakeWord : public ProtoMessage {
|
||||
void set_wake_word(const StringRef &ref) { this->wake_word_ref_ = ref; }
|
||||
std::vector<std::string> trained_languages{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
};
|
||||
class VoiceAssistantConfigurationRequest : public ProtoDecodableMessage {
|
||||
class VoiceAssistantConfigurationRequest : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 121;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
@@ -2438,10 +2456,10 @@ class VoiceAssistantConfigurationResponse : public ProtoMessage {
|
||||
const char *message_name() const override { return "voice_assistant_configuration_response"; }
|
||||
#endif
|
||||
std::vector<VoiceAssistantWakeWord> available_wake_words{};
|
||||
std::vector<std::string> active_wake_words{};
|
||||
const std::vector<std::string> *active_wake_words{};
|
||||
uint32_t max_active_wake_words{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2476,7 +2494,7 @@ class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage {
|
||||
bool requires_code{false};
|
||||
bool requires_code_to_arm{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2492,7 +2510,7 @@ class AlarmControlPanelStateResponse : public StateResponseProtoMessage {
|
||||
#endif
|
||||
enums::AlarmControlPanelState state{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2532,7 +2550,7 @@ class ListEntitiesTextResponse : public InfoResponseProtoMessage {
|
||||
void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; }
|
||||
enums::TextMode mode{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2550,7 +2568,7 @@ class TextStateResponse : public StateResponseProtoMessage {
|
||||
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
|
||||
bool missing_state{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2584,7 +2602,7 @@ class ListEntitiesDateResponse : public InfoResponseProtoMessage {
|
||||
const char *message_name() const override { return "list_entities_date_response"; }
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2603,7 +2621,7 @@ class DateStateResponse : public StateResponseProtoMessage {
|
||||
uint32_t month{0};
|
||||
uint32_t day{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2638,7 +2656,7 @@ class ListEntitiesTimeResponse : public InfoResponseProtoMessage {
|
||||
const char *message_name() const override { return "list_entities_time_response"; }
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2657,7 +2675,7 @@ class TimeStateResponse : public StateResponseProtoMessage {
|
||||
uint32_t minute{0};
|
||||
uint32_t second{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2695,7 +2713,7 @@ class ListEntitiesEventResponse : public InfoResponseProtoMessage {
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
std::vector<std::string> event_types{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2712,7 +2730,7 @@ class EventResponse : public StateResponseProtoMessage {
|
||||
StringRef event_type_ref_{};
|
||||
void set_event_type(const StringRef &ref) { this->event_type_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2734,7 +2752,7 @@ class ListEntitiesValveResponse : public InfoResponseProtoMessage {
|
||||
bool supports_position{false};
|
||||
bool supports_stop{false};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2751,7 +2769,7 @@ class ValveStateResponse : public StateResponseProtoMessage {
|
||||
float position{0.0f};
|
||||
enums::ValveOperation current_operation{};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2786,7 +2804,7 @@ class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage {
|
||||
const char *message_name() const override { return "list_entities_date_time_response"; }
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2803,7 +2821,7 @@ class DateTimeStateResponse : public StateResponseProtoMessage {
|
||||
bool missing_state{false};
|
||||
uint32_t epoch_seconds{0};
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2838,7 +2856,7 @@ class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
|
||||
StringRef device_class_ref_{};
|
||||
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
@@ -2867,7 +2885,7 @@ class UpdateStateResponse : public StateResponseProtoMessage {
|
||||
StringRef release_url_ref_{};
|
||||
void set_release_url(const StringRef &ref) { this->release_url_ref_ = ref; }
|
||||
void encode(ProtoWriteBuffer buffer) const override;
|
||||
void calculate_size(uint32_t &total_size) const override;
|
||||
void calculate_size(ProtoSize &size) const override;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
void dump_to(std::string &out) const override;
|
||||
#endif
|
||||
|
@@ -383,6 +383,12 @@ template<> const char *proto_enum_to_string<enums::MediaPlayerState>(enums::Medi
|
||||
return "MEDIA_PLAYER_STATE_PLAYING";
|
||||
case enums::MEDIA_PLAYER_STATE_PAUSED:
|
||||
return "MEDIA_PLAYER_STATE_PAUSED";
|
||||
case enums::MEDIA_PLAYER_STATE_ANNOUNCING:
|
||||
return "MEDIA_PLAYER_STATE_ANNOUNCING";
|
||||
case enums::MEDIA_PLAYER_STATE_OFF:
|
||||
return "MEDIA_PLAYER_STATE_OFF";
|
||||
case enums::MEDIA_PLAYER_STATE_ON:
|
||||
return "MEDIA_PLAYER_STATE_ON";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -399,6 +405,24 @@ template<> const char *proto_enum_to_string<enums::MediaPlayerCommand>(enums::Me
|
||||
return "MEDIA_PLAYER_COMMAND_MUTE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_UNMUTE:
|
||||
return "MEDIA_PLAYER_COMMAND_UNMUTE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_TOGGLE:
|
||||
return "MEDIA_PLAYER_COMMAND_TOGGLE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_VOLUME_UP:
|
||||
return "MEDIA_PLAYER_COMMAND_VOLUME_UP";
|
||||
case enums::MEDIA_PLAYER_COMMAND_VOLUME_DOWN:
|
||||
return "MEDIA_PLAYER_COMMAND_VOLUME_DOWN";
|
||||
case enums::MEDIA_PLAYER_COMMAND_ENQUEUE:
|
||||
return "MEDIA_PLAYER_COMMAND_ENQUEUE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_REPEAT_ONE:
|
||||
return "MEDIA_PLAYER_COMMAND_REPEAT_ONE";
|
||||
case enums::MEDIA_PLAYER_COMMAND_REPEAT_OFF:
|
||||
return "MEDIA_PLAYER_COMMAND_REPEAT_OFF";
|
||||
case enums::MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST:
|
||||
return "MEDIA_PLAYER_COMMAND_CLEAR_PLAYLIST";
|
||||
case enums::MEDIA_PLAYER_COMMAND_TURN_ON:
|
||||
return "MEDIA_PLAYER_COMMAND_TURN_ON";
|
||||
case enums::MEDIA_PLAYER_COMMAND_TURN_OFF:
|
||||
return "MEDIA_PLAYER_COMMAND_TURN_OFF";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
@@ -814,7 +838,7 @@ void ListEntitiesFanResponse::dump_to(std::string &out) const {
|
||||
dump_field(out, "icon", this->icon_ref_);
|
||||
#endif
|
||||
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
|
||||
for (const auto &it : this->supported_preset_modes) {
|
||||
for (const auto &it : *this->supported_preset_modes) {
|
||||
dump_field(out, "supported_preset_modes", it, 4);
|
||||
}
|
||||
#ifdef USE_DEVICES
|
||||
@@ -857,7 +881,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
|
||||
dump_field(out, "object_id", this->object_id_ref_);
|
||||
dump_field(out, "key", this->key);
|
||||
dump_field(out, "name", this->name_ref_);
|
||||
for (const auto &it : this->supported_color_modes) {
|
||||
for (const auto &it : *this->supported_color_modes) {
|
||||
dump_field(out, "supported_color_modes", static_cast<enums::ColorMode>(it), 4);
|
||||
}
|
||||
dump_field(out, "min_mireds", this->min_mireds);
|
||||
@@ -1038,13 +1062,14 @@ void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
|
||||
}
|
||||
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); }
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const {
|
||||
out.append("SubscribeHomeassistantServicesRequest {}");
|
||||
}
|
||||
void HomeassistantServiceMap::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "HomeassistantServiceMap");
|
||||
dump_field(out, "key", this->key_ref_);
|
||||
dump_field(out, "value", this->value_ref_);
|
||||
dump_field(out, "value", this->value);
|
||||
}
|
||||
void HomeassistantServiceResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "HomeassistantServiceResponse");
|
||||
@@ -1066,6 +1091,7 @@ void HomeassistantServiceResponse::dump_to(std::string &out) const {
|
||||
}
|
||||
dump_field(out, "is_event", this->is_event);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const {
|
||||
out.append("SubscribeHomeAssistantStatesRequest {}");
|
||||
@@ -1171,26 +1197,26 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
|
||||
dump_field(out, "name", this->name_ref_);
|
||||
dump_field(out, "supports_current_temperature", this->supports_current_temperature);
|
||||
dump_field(out, "supports_two_point_target_temperature", this->supports_two_point_target_temperature);
|
||||
for (const auto &it : this->supported_modes) {
|
||||
for (const auto &it : *this->supported_modes) {
|
||||
dump_field(out, "supported_modes", static_cast<enums::ClimateMode>(it), 4);
|
||||
}
|
||||
dump_field(out, "visual_min_temperature", this->visual_min_temperature);
|
||||
dump_field(out, "visual_max_temperature", this->visual_max_temperature);
|
||||
dump_field(out, "visual_target_temperature_step", this->visual_target_temperature_step);
|
||||
dump_field(out, "supports_action", this->supports_action);
|
||||
for (const auto &it : this->supported_fan_modes) {
|
||||
for (const auto &it : *this->supported_fan_modes) {
|
||||
dump_field(out, "supported_fan_modes", static_cast<enums::ClimateFanMode>(it), 4);
|
||||
}
|
||||
for (const auto &it : this->supported_swing_modes) {
|
||||
for (const auto &it : *this->supported_swing_modes) {
|
||||
dump_field(out, "supported_swing_modes", static_cast<enums::ClimateSwingMode>(it), 4);
|
||||
}
|
||||
for (const auto &it : this->supported_custom_fan_modes) {
|
||||
for (const auto &it : *this->supported_custom_fan_modes) {
|
||||
dump_field(out, "supported_custom_fan_modes", it, 4);
|
||||
}
|
||||
for (const auto &it : this->supported_presets) {
|
||||
for (const auto &it : *this->supported_presets) {
|
||||
dump_field(out, "supported_presets", static_cast<enums::ClimatePreset>(it), 4);
|
||||
}
|
||||
for (const auto &it : this->supported_custom_presets) {
|
||||
for (const auto &it : *this->supported_custom_presets) {
|
||||
dump_field(out, "supported_custom_presets", it, 4);
|
||||
}
|
||||
dump_field(out, "disabled_by_default", this->disabled_by_default);
|
||||
@@ -1303,7 +1329,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const {
|
||||
#ifdef USE_ENTITY_ICON
|
||||
dump_field(out, "icon", this->icon_ref_);
|
||||
#endif
|
||||
for (const auto &it : this->options) {
|
||||
for (const auto &it : *this->options) {
|
||||
dump_field(out, "options", it, 4);
|
||||
}
|
||||
dump_field(out, "disabled_by_default", this->disabled_by_default);
|
||||
@@ -1464,6 +1490,7 @@ void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
|
||||
#ifdef USE_DEVICES
|
||||
dump_field(out, "device_id", this->device_id);
|
||||
#endif
|
||||
dump_field(out, "feature_flags", this->feature_flags);
|
||||
}
|
||||
void MediaPlayerStateResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "MediaPlayerStateResponse");
|
||||
@@ -1534,6 +1561,7 @@ void BluetoothGATTDescriptor::dump_to(std::string &out) const {
|
||||
dump_field(out, "uuid", it, 4);
|
||||
}
|
||||
dump_field(out, "handle", this->handle);
|
||||
dump_field(out, "short_uuid", this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothGATTCharacteristic");
|
||||
@@ -1547,6 +1575,7 @@ void BluetoothGATTCharacteristic::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
dump_field(out, "short_uuid", this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTService::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothGATTService");
|
||||
@@ -1559,6 +1588,7 @@ void BluetoothGATTService::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
dump_field(out, "short_uuid", this->short_uuid);
|
||||
}
|
||||
void BluetoothGATTGetServicesResponse::dump_to(std::string &out) const {
|
||||
MessageDumpHelper helper(out, "BluetoothGATTGetServicesResponse");
|
||||
@@ -1767,7 +1797,7 @@ void VoiceAssistantConfigurationResponse::dump_to(std::string &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
for (const auto &it : this->active_wake_words) {
|
||||
for (const auto &it : *this->active_wake_words) {
|
||||
dump_field(out, "active_wake_words", it, 4);
|
||||
}
|
||||
dump_field(out, "max_active_wake_words", this->max_active_wake_words);
|
||||
|
34
esphome/components/api/api_pb2_includes.h
Normal file
34
esphome/components/api/api_pb2_includes.h
Normal file
@@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
// This file provides includes needed by the generated protobuf code
|
||||
// when using pointer optimizations for component-specific types
|
||||
|
||||
#ifdef USE_CLIMATE
|
||||
#include "esphome/components/climate/climate_mode.h"
|
||||
#include "esphome/components/climate/climate_traits.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_LIGHT
|
||||
#include "esphome/components/light/light_traits.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_FAN
|
||||
#include "esphome/components/fan/fan_traits.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_SELECT
|
||||
#include "esphome/components/select/select_traits.h"
|
||||
#endif
|
||||
|
||||
// Standard library includes that might be needed
|
||||
#include <set>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
// This file only provides includes, no actual code
|
||||
|
||||
} // namespace esphome::api
|
@@ -35,7 +35,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
case DisconnectRequest::MESSAGE_TYPE: {
|
||||
DisconnectRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_disconnect_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -44,7 +44,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
case DisconnectResponse::MESSAGE_TYPE: {
|
||||
DisconnectResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_disconnect_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -53,7 +53,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
case PingRequest::MESSAGE_TYPE: {
|
||||
PingRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_ping_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -62,7 +62,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
case PingResponse::MESSAGE_TYPE: {
|
||||
PingResponse msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_ping_response: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -71,7 +71,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
case DeviceInfoRequest::MESSAGE_TYPE: {
|
||||
DeviceInfoRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_device_info_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -80,7 +80,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
case ListEntitiesRequest::MESSAGE_TYPE: {
|
||||
ListEntitiesRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_list_entities_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -89,7 +89,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
}
|
||||
case SubscribeStatesRequest::MESSAGE_TYPE: {
|
||||
SubscribeStatesRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_states_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -149,18 +149,20 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
case SubscribeHomeassistantServicesRequest::MESSAGE_TYPE: {
|
||||
SubscribeHomeassistantServicesRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_homeassistant_services_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
this->on_subscribe_homeassistant_services_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case GetTimeRequest::MESSAGE_TYPE: {
|
||||
GetTimeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_get_time_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -179,7 +181,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
case SubscribeHomeAssistantStatesRequest::MESSAGE_TYPE: {
|
||||
SubscribeHomeAssistantStatesRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_home_assistant_states_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -388,7 +390,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case SubscribeBluetoothConnectionsFreeRequest::MESSAGE_TYPE: {
|
||||
SubscribeBluetoothConnectionsFreeRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_subscribe_bluetooth_connections_free_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -399,7 +401,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
case UnsubscribeBluetoothLEAdvertisementsRequest::MESSAGE_TYPE: {
|
||||
UnsubscribeBluetoothLEAdvertisementsRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_unsubscribe_bluetooth_le_advertisements_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -553,7 +555,7 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
#ifdef USE_VOICE_ASSISTANT
|
||||
case VoiceAssistantConfigurationRequest::MESSAGE_TYPE: {
|
||||
VoiceAssistantConfigurationRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
// Empty message: no decode needed
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
ESP_LOGVV(TAG, "on_voice_assistant_configuration_request: %s", msg.dump().c_str());
|
||||
#endif
|
||||
@@ -639,12 +641,14 @@ void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &
|
||||
this->subscribe_logs(msg);
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServerConnection::on_subscribe_homeassistant_services_request(
|
||||
const SubscribeHomeassistantServicesRequest &msg) {
|
||||
if (this->check_authenticated_()) {
|
||||
this->subscribe_homeassistant_services(msg);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) {
|
||||
if (this->check_authenticated_()) {
|
||||
|
@@ -60,7 +60,9 @@ class APIServerConnectionBase : public ProtoService {
|
||||
virtual void on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
|
||||
@@ -218,7 +220,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
virtual void list_entities(const ListEntitiesRequest &msg) = 0;
|
||||
virtual void subscribe_states(const SubscribeStatesRequest &msg) = 0;
|
||||
virtual void subscribe_logs(const SubscribeLogsRequest &msg) = 0;
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
virtual void subscribe_homeassistant_services(const SubscribeHomeassistantServicesRequest &msg) = 0;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
virtual void subscribe_home_assistant_states(const SubscribeHomeAssistantStatesRequest &msg) = 0;
|
||||
#endif
|
||||
@@ -338,7 +342,9 @@ class APIServerConnection : public APIServerConnectionBase {
|
||||
void on_list_entities_request(const ListEntitiesRequest &msg) override;
|
||||
void on_subscribe_states_request(const SubscribeStatesRequest &msg) override;
|
||||
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) override;
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &msg) override;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) override;
|
||||
#endif
|
||||
|
@@ -369,11 +369,13 @@ void APIServer::set_password(const std::string &password) { this->password_ = pa
|
||||
|
||||
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_service_call(const HomeassistantServiceResponse &call) {
|
||||
for (auto &client : this->clients_) {
|
||||
client->send_homeassistant_service_call(call);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
|
||||
|
@@ -106,7 +106,9 @@ class APIServer : public Component, public Controller {
|
||||
#ifdef USE_MEDIA_PLAYER
|
||||
void on_media_player_update(media_player::MediaPlayer *obj) override;
|
||||
#endif
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void send_homeassistant_service_call(const HomeassistantServiceResponse &call);
|
||||
#endif
|
||||
#ifdef USE_API_SERVICES
|
||||
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
|
||||
#endif
|
||||
|
@@ -30,7 +30,7 @@ if TYPE_CHECKING:
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
"""Run the logs command in the event loop."""
|
||||
conf = config["api"]
|
||||
name = config["esphome"]["name"]
|
||||
@@ -39,13 +39,21 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
noise_psk: str | None = None
|
||||
if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
|
||||
noise_psk = key
|
||||
_LOGGER.info("Starting log output from %s using esphome API", address)
|
||||
|
||||
if len(addresses) == 1:
|
||||
_LOGGER.info("Starting log output from %s using esphome API", addresses[0])
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Starting log output from %s using esphome API", " or ".join(addresses)
|
||||
)
|
||||
|
||||
cli = APIClient(
|
||||
address,
|
||||
addresses[0], # Primary address for compatibility
|
||||
port,
|
||||
password,
|
||||
client_info=f"ESPHome Logs {__version__}",
|
||||
noise_psk=noise_psk,
|
||||
addresses=addresses, # Pass all addresses for automatic retry
|
||||
)
|
||||
dashboard = CORE.dashboard
|
||||
|
||||
@@ -66,7 +74,7 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
|
||||
await stop()
|
||||
|
||||
|
||||
def run_logs(config: dict[str, Any], address: str) -> None:
|
||||
def run_logs(config: dict[str, Any], addresses: list[str]) -> None:
|
||||
"""Run the logs command."""
|
||||
with contextlib.suppress(KeyboardInterrupt):
|
||||
asyncio.run(async_run_logs(config, address))
|
||||
asyncio.run(async_run_logs(config, addresses))
|
||||
|
@@ -56,6 +56,14 @@ class CustomAPIDevice {
|
||||
auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
}
|
||||
#else
|
||||
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) {
|
||||
static_assert(
|
||||
sizeof(T) == 0,
|
||||
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
|
||||
/** Register a custom native API service that will show up in Home Assistant.
|
||||
@@ -81,6 +89,12 @@ class CustomAPIDevice {
|
||||
auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback); // NOLINT
|
||||
global_api_server->register_user_service(service);
|
||||
}
|
||||
#else
|
||||
template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
|
||||
static_assert(
|
||||
sizeof(T) == 0,
|
||||
"register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_STATES
|
||||
@@ -135,8 +149,25 @@ class CustomAPIDevice {
|
||||
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
|
||||
}
|
||||
#else
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
|
||||
"of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
static_assert(sizeof(T) == 0,
|
||||
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
|
||||
"of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
/** Call a Home Assistant service from ESPHome.
|
||||
*
|
||||
* Usage:
|
||||
@@ -174,7 +205,7 @@ class CustomAPIDevice {
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.set_value(StringRef(it.second));
|
||||
kv.value = it.second;
|
||||
}
|
||||
global_api_server->send_homeassistant_service_call(resp);
|
||||
}
|
||||
@@ -217,10 +248,33 @@ class CustomAPIDevice {
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
kv.set_key(StringRef(it.first));
|
||||
kv.set_value(StringRef(it.second));
|
||||
kv.value = it.second;
|
||||
}
|
||||
global_api_server->send_homeassistant_service_call(resp);
|
||||
}
|
||||
#else
|
||||
template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
|
||||
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T = void>
|
||||
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T = void> void fire_homeassistant_event(const std::string &event_name) {
|
||||
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
|
||||
template<typename T = void>
|
||||
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
|
||||
static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
|
||||
"section of your YAML configuration");
|
||||
}
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "api_server.h"
|
||||
#ifdef USE_API
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
#include "api_pb2.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
@@ -69,22 +70,19 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
resp.data.emplace_back();
|
||||
auto &kv = resp.data.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
std::string value = it.value.value(x...);
|
||||
kv.set_value(StringRef(value));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
for (auto &it : this->data_template_) {
|
||||
resp.data_template.emplace_back();
|
||||
auto &kv = resp.data_template.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
std::string value = it.value.value(x...);
|
||||
kv.set_value(StringRef(value));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
for (auto &it : this->variables_) {
|
||||
resp.variables.emplace_back();
|
||||
auto &kv = resp.variables.back();
|
||||
kv.set_key(StringRef(it.key));
|
||||
std::string value = it.value.value(x...);
|
||||
kv.set_value(StringRef(value));
|
||||
kv.value = it.value.value(x...);
|
||||
}
|
||||
this->parent_->send_homeassistant_service_call(resp);
|
||||
}
|
||||
@@ -100,3 +98,4 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
#endif
|
||||
|
@@ -35,11 +35,10 @@ namespace esphome::api {
|
||||
*
|
||||
* Unsafe Patterns (WILL cause crashes/corruption):
|
||||
* 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value
|
||||
* 2. Optional values: msg.set_field(StringRef(optional.value())) // value() returns a copy
|
||||
* 3. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
|
||||
* 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
|
||||
*
|
||||
* For unsafe patterns, store in a local variable first:
|
||||
* std::string temp = optional.value(); // or get_string() or str1 + str2
|
||||
* std::string temp = get_string(); // or str1 + str2
|
||||
* msg.set_field(StringRef(temp));
|
||||
*
|
||||
* The send_*_response pattern ensures proper lifetime management by encoding
|
||||
@@ -334,13 +333,16 @@ class ProtoWriteBuffer {
|
||||
std::vector<uint8_t> *buffer_;
|
||||
};
|
||||
|
||||
// Forward declaration
|
||||
class ProtoSize;
|
||||
|
||||
class ProtoMessage {
|
||||
public:
|
||||
virtual ~ProtoMessage() = default;
|
||||
// Default implementation for messages with no fields
|
||||
virtual void encode(ProtoWriteBuffer buffer) const {}
|
||||
// Default implementation for messages with no fields
|
||||
virtual void calculate_size(uint32_t &total_size) const {}
|
||||
virtual void calculate_size(ProtoSize &size) const {}
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
std::string dump() const;
|
||||
virtual void dump_to(std::string &out) const = 0;
|
||||
@@ -361,24 +363,32 @@ class ProtoDecodableMessage : public ProtoMessage {
|
||||
};
|
||||
|
||||
class ProtoSize {
|
||||
private:
|
||||
uint32_t total_size_ = 0;
|
||||
|
||||
public:
|
||||
/**
|
||||
* @brief ProtoSize class for Protocol Buffer serialization size calculation
|
||||
*
|
||||
* This class provides static methods to calculate the exact byte counts needed
|
||||
* for encoding various Protocol Buffer field types. All methods are designed to be
|
||||
* efficient for the common case where many fields have default values.
|
||||
* This class provides methods to calculate the exact byte counts needed
|
||||
* for encoding various Protocol Buffer field types. The class now uses an
|
||||
* object-based approach to reduce parameter passing overhead while keeping
|
||||
* varint calculation methods static for external use.
|
||||
*
|
||||
* Implements Protocol Buffer encoding size calculation according to:
|
||||
* https://protobuf.dev/programming-guides/encoding/
|
||||
*
|
||||
* Key features:
|
||||
* - Object-based approach reduces flash usage by eliminating parameter passing
|
||||
* - Early-return optimization for zero/default values
|
||||
* - Direct total_size updates to avoid unnecessary additions
|
||||
* - Static varint methods for external callers
|
||||
* - Specialized handling for different field types according to protobuf spec
|
||||
* - Templated helpers for repeated fields and messages
|
||||
*/
|
||||
|
||||
ProtoSize() = default;
|
||||
|
||||
uint32_t get_size() const { return total_size_; }
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
*
|
||||
@@ -479,9 +489,7 @@ class ProtoSize {
|
||||
* @brief Common parameters for all add_*_field methods
|
||||
*
|
||||
* All add_*_field methods follow these common patterns:
|
||||
*
|
||||
* @param total_size Reference to the total message size to update
|
||||
* @param field_id_size Pre-calculated size of the field ID in bytes
|
||||
* * @param field_id_size Pre-calculated size of the field ID in bytes
|
||||
* @param value The value to calculate size for (type varies)
|
||||
* @param force Whether to calculate size even if the value is default/zero/empty
|
||||
*
|
||||
@@ -494,85 +502,63 @@ class ProtoSize {
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int32 field to the total message size
|
||||
*/
|
||||
static inline void add_int32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
if (value < 0) {
|
||||
// Negative values are encoded as 10-byte varints in protobuf
|
||||
total_size += field_id_size + 10;
|
||||
} else {
|
||||
// For non-negative values, use the standard varint size
|
||||
total_size += field_id_size + varint(static_cast<uint32_t>(value));
|
||||
inline void add_int32(uint32_t field_id_size, int32_t value) {
|
||||
if (value != 0) {
|
||||
add_int32_force(field_id_size, value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int32 field to the total message size (repeated field version)
|
||||
* @brief Calculates and adds the size of an int32 field to the total message size (force version)
|
||||
*/
|
||||
static inline void add_int32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
if (value < 0) {
|
||||
// Negative values are encoded as 10-byte varints in protobuf
|
||||
total_size += field_id_size + 10;
|
||||
} else {
|
||||
// For non-negative values, use the standard varint size
|
||||
total_size += field_id_size + varint(static_cast<uint32_t>(value));
|
||||
}
|
||||
inline void add_int32_force(uint32_t field_id_size, int32_t value) {
|
||||
// Always calculate size when forced
|
||||
// Negative values are encoded as 10-byte varints in protobuf
|
||||
total_size_ += field_id_size + (value < 0 ? 10 : varint(static_cast<uint32_t>(value)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint32 field to the total message size
|
||||
*/
|
||||
static inline void add_uint32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
inline void add_uint32(uint32_t field_id_size, uint32_t value) {
|
||||
if (value != 0) {
|
||||
add_uint32_force(field_id_size, value);
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint32 field to the total message size (repeated field version)
|
||||
* @brief Calculates and adds the size of a uint32 field to the total message size (force version)
|
||||
*/
|
||||
static inline void add_uint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
total_size += field_id_size + varint(value);
|
||||
inline void add_uint32_force(uint32_t field_id_size, uint32_t value) {
|
||||
// Always calculate size when force is true
|
||||
total_size_ += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a boolean field to the total message size
|
||||
*/
|
||||
static inline void add_bool_field(uint32_t &total_size, uint32_t field_id_size, bool value) {
|
||||
// Skip calculation if value is false
|
||||
if (!value) {
|
||||
return; // No need to update total_size
|
||||
inline void add_bool(uint32_t field_id_size, bool value) {
|
||||
if (value) {
|
||||
// Boolean fields always use 1 byte when true
|
||||
total_size_ += field_id_size + 1;
|
||||
}
|
||||
|
||||
// Boolean fields always use 1 byte when true
|
||||
total_size += field_id_size + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a boolean field to the total message size (repeated field version)
|
||||
* @brief Calculates and adds the size of a boolean field to the total message size (force version)
|
||||
*/
|
||||
static inline void add_bool_field_repeated(uint32_t &total_size, uint32_t field_id_size, bool value) {
|
||||
// Always calculate size for repeated fields
|
||||
inline void add_bool_force(uint32_t field_id_size, bool value) {
|
||||
// Always calculate size when force is true
|
||||
// Boolean fields always use 1 byte
|
||||
total_size += field_id_size + 1;
|
||||
total_size_ += field_id_size + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a float field to the total message size
|
||||
*/
|
||||
static inline void add_float_field(uint32_t &total_size, uint32_t field_id_size, float value) {
|
||||
inline void add_float(uint32_t field_id_size, float value) {
|
||||
if (value != 0.0f) {
|
||||
total_size += field_id_size + 4;
|
||||
total_size_ += field_id_size + 4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -582,9 +568,9 @@ class ProtoSize {
|
||||
/**
|
||||
* @brief Calculates and adds the size of a fixed32 field to the total message size
|
||||
*/
|
||||
static inline void add_fixed32_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
|
||||
inline void add_fixed32(uint32_t field_id_size, uint32_t value) {
|
||||
if (value != 0) {
|
||||
total_size += field_id_size + 4;
|
||||
total_size_ += field_id_size + 4;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -594,149 +580,104 @@ class ProtoSize {
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sfixed32 field to the total message size
|
||||
*/
|
||||
static inline void add_sfixed32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
|
||||
inline void add_sfixed32(uint32_t field_id_size, int32_t value) {
|
||||
if (value != 0) {
|
||||
total_size += field_id_size + 4;
|
||||
total_size_ += field_id_size + 4;
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: add_sfixed64_field removed - wire type 1 (64-bit: sfixed64) not supported
|
||||
// to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an enum field to the total message size
|
||||
*
|
||||
* Enum fields are encoded as uint32 varints.
|
||||
*/
|
||||
static inline void add_enum_field(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
// Enums are encoded as uint32
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an enum field to the total message size (repeated field version)
|
||||
*
|
||||
* Enum fields are encoded as uint32 varints.
|
||||
*/
|
||||
static inline void add_enum_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
// Enums are encoded as uint32
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sint32 field to the total message size
|
||||
*
|
||||
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
|
||||
*/
|
||||
static inline void add_sint32_field(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
inline void add_sint32(uint32_t field_id_size, int32_t value) {
|
||||
if (value != 0) {
|
||||
add_sint32_force(field_id_size, value);
|
||||
}
|
||||
|
||||
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
|
||||
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||
total_size += field_id_size + varint(zigzag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a sint32 field to the total message size (repeated field version)
|
||||
* @brief Calculates and adds the size of a sint32 field to the total message size (force version)
|
||||
*
|
||||
* Sint32 fields use ZigZag encoding, which is more efficient for negative values.
|
||||
*/
|
||||
static inline void add_sint32_field_repeated(uint32_t &total_size, uint32_t field_id_size, int32_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
inline void add_sint32_force(uint32_t field_id_size, int32_t value) {
|
||||
// Always calculate size when force is true
|
||||
// ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
|
||||
uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
|
||||
total_size += field_id_size + varint(zigzag);
|
||||
total_size_ += field_id_size + varint(zigzag);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int64 field to the total message size
|
||||
*/
|
||||
static inline void add_int64_field(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
inline void add_int64(uint32_t field_id_size, int64_t value) {
|
||||
if (value != 0) {
|
||||
add_int64_force(field_id_size, value);
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of an int64 field to the total message size (repeated field version)
|
||||
* @brief Calculates and adds the size of an int64 field to the total message size (force version)
|
||||
*/
|
||||
static inline void add_int64_field_repeated(uint32_t &total_size, uint32_t field_id_size, int64_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
total_size += field_id_size + varint(value);
|
||||
inline void add_int64_force(uint32_t field_id_size, int64_t value) {
|
||||
// Always calculate size when force is true
|
||||
total_size_ += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint64 field to the total message size
|
||||
*/
|
||||
static inline void add_uint64_field(uint32_t &total_size, uint32_t field_id_size, uint64_t value) {
|
||||
// Skip calculation if value is zero
|
||||
if (value == 0) {
|
||||
return; // No need to update total_size
|
||||
inline void add_uint64(uint32_t field_id_size, uint64_t value) {
|
||||
if (value != 0) {
|
||||
add_uint64_force(field_id_size, value);
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
total_size += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a uint64 field to the total message size (repeated field version)
|
||||
* @brief Calculates and adds the size of a uint64 field to the total message size (force version)
|
||||
*/
|
||||
static inline void add_uint64_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint64_t value) {
|
||||
// Always calculate size for repeated fields
|
||||
total_size += field_id_size + varint(value);
|
||||
inline void add_uint64_force(uint32_t field_id_size, uint64_t value) {
|
||||
// Always calculate size when force is true
|
||||
total_size_ += field_id_size + varint(value);
|
||||
}
|
||||
|
||||
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_repeated) removed
|
||||
// NOTE: sint64 support functions (add_sint64_field, add_sint64_field_force) removed
|
||||
// sint64 type is not supported by ESPHome API to reduce overhead on embedded systems
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a string field using length
|
||||
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size
|
||||
*/
|
||||
static inline void add_string_field(uint32_t &total_size, uint32_t field_id_size, size_t len) {
|
||||
// Skip calculation if string is empty
|
||||
if (len == 0) {
|
||||
return; // No need to update total_size
|
||||
inline void add_length(uint32_t field_id_size, size_t len) {
|
||||
if (len != 0) {
|
||||
add_length_force(field_id_size, len);
|
||||
}
|
||||
|
||||
// Field ID + length varint + string bytes
|
||||
total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a string/bytes field to the total message size (repeated field version)
|
||||
* @brief Calculates and adds the size of a length-delimited field (string/bytes) to the total message size (repeated
|
||||
* field version)
|
||||
*/
|
||||
static inline void add_string_field_repeated(uint32_t &total_size, uint32_t field_id_size, const std::string &str) {
|
||||
// Always calculate size for repeated fields
|
||||
const uint32_t str_size = static_cast<uint32_t>(str.size());
|
||||
total_size += field_id_size + varint(str_size) + str_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a bytes field to the total message size
|
||||
*/
|
||||
static inline void add_bytes_field(uint32_t &total_size, uint32_t field_id_size, size_t len) {
|
||||
// Skip calculation if bytes is empty
|
||||
if (len == 0) {
|
||||
return; // No need to update total_size
|
||||
}
|
||||
|
||||
inline void add_length_force(uint32_t field_id_size, size_t len) {
|
||||
// Always calculate size when force is true
|
||||
// Field ID + length varint + data bytes
|
||||
total_size += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
|
||||
total_size_ += field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Adds a pre-calculated size directly to the total
|
||||
*
|
||||
* This is used when we can calculate the total size by multiplying the number
|
||||
* of elements by the bytes per element (for repeated fixed-size types like float, fixed32, etc.)
|
||||
*
|
||||
* @param size The pre-calculated total size to add
|
||||
*/
|
||||
inline void add_precalculated_size(uint32_t size) { total_size_ += size; }
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size
|
||||
*
|
||||
@@ -745,26 +686,21 @@ class ProtoSize {
|
||||
*
|
||||
* @param nested_size The pre-calculated size of the nested message
|
||||
*/
|
||||
static inline void add_message_field(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) {
|
||||
// Skip calculation if nested message is empty
|
||||
if (nested_size == 0) {
|
||||
return; // No need to update total_size
|
||||
inline void add_message_field(uint32_t field_id_size, uint32_t nested_size) {
|
||||
if (nested_size != 0) {
|
||||
add_message_field_force(field_id_size, nested_size);
|
||||
}
|
||||
|
||||
// Calculate and directly add to total_size
|
||||
// Field ID + length varint + nested message content
|
||||
total_size += field_id_size + varint(nested_size) + nested_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version)
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
|
||||
*
|
||||
* @param nested_size The pre-calculated size of the nested message
|
||||
*/
|
||||
static inline void add_message_field_repeated(uint32_t &total_size, uint32_t field_id_size, uint32_t nested_size) {
|
||||
// Always calculate size for repeated fields
|
||||
inline void add_message_field_force(uint32_t field_id_size, uint32_t nested_size) {
|
||||
// Always calculate size when force is true
|
||||
// Field ID + length varint + nested message content
|
||||
total_size += field_id_size + varint(nested_size) + nested_size;
|
||||
total_size_ += field_id_size + varint(nested_size) + nested_size;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -776,26 +712,29 @@ class ProtoSize {
|
||||
*
|
||||
* @param message The nested message object
|
||||
*/
|
||||
static inline void add_message_object(uint32_t &total_size, uint32_t field_id_size, const ProtoMessage &message) {
|
||||
uint32_t nested_size = 0;
|
||||
message.calculate_size(nested_size);
|
||||
inline void add_message_object(uint32_t field_id_size, const ProtoMessage &message) {
|
||||
// Calculate nested message size by creating a temporary ProtoSize
|
||||
ProtoSize nested_calc;
|
||||
message.calculate_size(nested_calc);
|
||||
uint32_t nested_size = nested_calc.get_size();
|
||||
|
||||
// Use the base implementation with the calculated nested_size
|
||||
add_message_field(total_size, field_id_size, nested_size);
|
||||
add_message_field(field_id_size, nested_size);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size (repeated field version)
|
||||
* @brief Calculates and adds the size of a nested message field to the total message size (force version)
|
||||
*
|
||||
* @param message The nested message object
|
||||
*/
|
||||
static inline void add_message_object_repeated(uint32_t &total_size, uint32_t field_id_size,
|
||||
const ProtoMessage &message) {
|
||||
uint32_t nested_size = 0;
|
||||
message.calculate_size(nested_size);
|
||||
inline void add_message_object_force(uint32_t field_id_size, const ProtoMessage &message) {
|
||||
// Calculate nested message size by creating a temporary ProtoSize
|
||||
ProtoSize nested_calc;
|
||||
message.calculate_size(nested_calc);
|
||||
uint32_t nested_size = nested_calc.get_size();
|
||||
|
||||
// Use the base implementation with the calculated nested_size
|
||||
add_message_field_repeated(total_size, field_id_size, nested_size);
|
||||
add_message_field_force(field_id_size, nested_size);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -808,16 +747,15 @@ class ProtoSize {
|
||||
* @param messages Vector of message objects
|
||||
*/
|
||||
template<typename MessageType>
|
||||
static inline void add_repeated_message(uint32_t &total_size, uint32_t field_id_size,
|
||||
const std::vector<MessageType> &messages) {
|
||||
inline void add_repeated_message(uint32_t field_id_size, const std::vector<MessageType> &messages) {
|
||||
// Skip if the vector is empty
|
||||
if (messages.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the repeated field version for all messages
|
||||
// Use the force version for all messages in the repeated field
|
||||
for (const auto &message : messages) {
|
||||
add_message_object_repeated(total_size, field_id_size, message);
|
||||
add_message_object_force(field_id_size, message);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -827,8 +765,9 @@ inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessa
|
||||
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
|
||||
|
||||
// Calculate the message size first
|
||||
uint32_t msg_length_bytes = 0;
|
||||
value.calculate_size(msg_length_bytes);
|
||||
ProtoSize msg_size;
|
||||
value.calculate_size(msg_size);
|
||||
uint32_t msg_length_bytes = msg_size.get_size();
|
||||
|
||||
// Calculate how many bytes the length varint needs
|
||||
uint32_t varint_length_bytes = ProtoSize::varint(msg_length_bytes);
|
||||
@@ -877,8 +816,9 @@ class ProtoService {
|
||||
|
||||
// Optimized method that pre-allocates buffer based on message size
|
||||
bool send_message_(const ProtoMessage &msg, uint8_t message_type) {
|
||||
uint32_t msg_size = 0;
|
||||
msg.calculate_size(msg_size);
|
||||
ProtoSize size;
|
||||
msg.calculate_size(size);
|
||||
uint32_t msg_size = size.get_size();
|
||||
|
||||
// Create a pre-sized buffer
|
||||
auto buffer = this->create_buffer(msg_size);
|
||||
|
@@ -10,7 +10,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema({}),
|
||||
|
@@ -516,6 +516,7 @@ def binary_sensor_schema(
|
||||
icon: str = cv.UNDEFINED,
|
||||
entity_category: str = cv.UNDEFINED,
|
||||
device_class: str = cv.UNDEFINED,
|
||||
filters: list = cv.UNDEFINED,
|
||||
) -> cv.Schema:
|
||||
schema = {}
|
||||
|
||||
@@ -527,6 +528,7 @@ def binary_sensor_schema(
|
||||
(CONF_ICON, icon, cv.icon),
|
||||
(CONF_ENTITY_CATEGORY, entity_category, cv.entity_category),
|
||||
(CONF_DEVICE_CLASS, device_class, validate_device_class),
|
||||
(CONF_FILTERS, filters, validate_filters),
|
||||
]:
|
||||
if default is not cv.UNDEFINED:
|
||||
schema[cv.Optional(key, default=default)] = validator
|
||||
@@ -652,7 +654,6 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args)
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_BINARY_SENSOR")
|
||||
cg.add_global(binary_sensor_ns.using)
|
||||
|
||||
|
||||
|
@@ -175,8 +175,7 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
|
||||
)
|
||||
async def ble_disconnect_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -184,8 +183,7 @@ async def ble_disconnect_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def ble_connect_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
@@ -282,9 +280,7 @@ async def passkey_reply_to_code(config, action_id, template_arg, args):
|
||||
)
|
||||
async def remove_bond_to_code(config, action_id, template_arg, args):
|
||||
parent = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, parent)
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
|
@@ -1,13 +1,19 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||
from esphome.components.esp32_ble import BTLoggers
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ACTIVE, CONF_ID
|
||||
from esphome.core import CORE
|
||||
from esphome.log import AnsiFore, color
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
|
||||
DEPENDENCIES = ["api", "esp32"]
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
CODEOWNERS = ["@jesserockz", "@bdraco"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_CONNECTION_SLOTS = "connection_slots"
|
||||
CONF_CACHE_SERVICES = "cache_services"
|
||||
@@ -41,6 +47,27 @@ def validate_connections(config):
|
||||
esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
|
||||
config
|
||||
)
|
||||
|
||||
# Warn about connection slot waste when using Arduino framework
|
||||
if CORE.using_arduino and connection_slots:
|
||||
_LOGGER.warning(
|
||||
"Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
|
||||
"If BLE connections fail, they can waste connection slots for 10 seconds because\n"
|
||||
"Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
|
||||
"ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
|
||||
"\n"
|
||||
"To switch to ESP-IDF, add this to your YAML:\n"
|
||||
" esp32:\n"
|
||||
" framework:\n"
|
||||
" type: esp-idf\n"
|
||||
"\n"
|
||||
"For detailed migration instructions, see:\n"
|
||||
"%s",
|
||||
color(
|
||||
AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
|
||||
),
|
||||
)
|
||||
|
||||
return {
|
||||
**config,
|
||||
CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
|
||||
@@ -87,6 +114,10 @@ async def to_code(config):
|
||||
cg.add(var.set_active(config[CONF_ACTIVE]))
|
||||
await esp32_ble_tracker.register_raw_ble_device(var, config)
|
||||
|
||||
# Define max connections for protobuf fixed array
|
||||
connection_count = len(config.get(CONF_CONNECTIONS, []))
|
||||
cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count)
|
||||
|
||||
for connection_conf in config.get(CONF_CONNECTIONS, []):
|
||||
connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
|
||||
await cg.register_component(connection_var, connection_conf)
|
||||
|
@@ -24,21 +24,107 @@ static void fill_128bit_uuid_array(std::array<uint64_t, 2> &out, esp_bt_uuid_t u
|
||||
((uint64_t) uuid.uuid.uuid128[1] << 8) | ((uint64_t) uuid.uuid.uuid128[0]);
|
||||
}
|
||||
|
||||
// Helper to fill UUID in the appropriate format based on client support and UUID type
|
||||
static void fill_gatt_uuid(std::array<uint64_t, 2> &uuid_128, uint32_t &short_uuid, const esp_bt_uuid_t &uuid,
|
||||
bool use_efficient_uuids) {
|
||||
if (!use_efficient_uuids || uuid.len == ESP_UUID_LEN_128) {
|
||||
// Use 128-bit format for old clients or when UUID is already 128-bit
|
||||
fill_128bit_uuid_array(uuid_128, uuid);
|
||||
} else if (uuid.len == ESP_UUID_LEN_16) {
|
||||
short_uuid = uuid.uuid.uuid16;
|
||||
} else if (uuid.len == ESP_UUID_LEN_32) {
|
||||
short_uuid = uuid.uuid.uuid32;
|
||||
}
|
||||
}
|
||||
|
||||
// Constants for size estimation
|
||||
static constexpr uint8_t SERVICE_OVERHEAD_LEGACY = 25; // UUID(20) + handle(4) + overhead(1)
|
||||
static constexpr uint8_t SERVICE_OVERHEAD_EFFICIENT = 10; // UUID(6) + handle(4)
|
||||
static constexpr uint8_t CHAR_SIZE_128BIT = 35; // UUID(20) + handle(4) + props(4) + overhead(7)
|
||||
static constexpr uint8_t DESC_SIZE_128BIT = 25; // UUID(20) + handle(4) + overhead(1)
|
||||
static constexpr uint8_t DESC_SIZE_16BIT = 10; // UUID(6) + handle(4)
|
||||
static constexpr uint8_t DESC_PER_CHAR = 1; // Assume 1 descriptor per characteristic
|
||||
|
||||
// Helper to estimate service size before fetching all data
|
||||
/**
|
||||
* Estimate the size of a Bluetooth service based on the number of characteristics and UUID format.
|
||||
*
|
||||
* @param char_count The number of characteristics in the service.
|
||||
* @param use_efficient_uuids Whether to use efficient UUIDs (16-bit or 32-bit) for newer APIVersions.
|
||||
* @return The estimated size of the service in bytes.
|
||||
*
|
||||
* This function calculates the size of a Bluetooth service by considering:
|
||||
* - A service overhead, which depends on whether efficient UUIDs are used.
|
||||
* - The size of each characteristic, assuming 128-bit UUIDs for safety.
|
||||
* - The size of descriptors, assuming one 128-bit descriptor per characteristic.
|
||||
*/
|
||||
static size_t estimate_service_size(uint16_t char_count, bool use_efficient_uuids) {
|
||||
size_t service_overhead = use_efficient_uuids ? SERVICE_OVERHEAD_EFFICIENT : SERVICE_OVERHEAD_LEGACY;
|
||||
// Always assume 128-bit UUIDs for characteristics to be safe
|
||||
size_t char_size = CHAR_SIZE_128BIT;
|
||||
// Assume one 128-bit descriptor per characteristic
|
||||
size_t desc_size = DESC_SIZE_128BIT * DESC_PER_CHAR;
|
||||
|
||||
return service_overhead + (char_size + desc_size) * char_count;
|
||||
}
|
||||
|
||||
bool BluetoothConnection::supports_efficient_uuids_() const {
|
||||
auto *api_conn = this->proxy_->get_api_connection();
|
||||
return api_conn && api_conn->client_supports_api_version(1, 12);
|
||||
}
|
||||
|
||||
void BluetoothConnection::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "BLE Connection:");
|
||||
BLEClientBase::dump_config();
|
||||
}
|
||||
|
||||
void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) {
|
||||
auto &allocated = this->proxy_->connections_free_response_.allocated;
|
||||
for (auto &slot : allocated) {
|
||||
if (slot == find_value) {
|
||||
slot = set_value;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothConnection::set_address(uint64_t address) {
|
||||
// If we're clearing an address (disconnecting), update the pre-allocated message
|
||||
if (address == 0 && this->address_ != 0) {
|
||||
this->proxy_->connections_free_response_.free++;
|
||||
this->update_allocated_slot_(this->address_, 0);
|
||||
}
|
||||
// If we're setting a new address (connecting), update the pre-allocated message
|
||||
else if (address != 0 && this->address_ == 0) {
|
||||
this->proxy_->connections_free_response_.free--;
|
||||
this->update_allocated_slot_(0, address);
|
||||
}
|
||||
|
||||
// Call parent implementation to actually set the address
|
||||
BLEClientBase::set_address(address);
|
||||
}
|
||||
|
||||
void BluetoothConnection::loop() {
|
||||
BLEClientBase::loop();
|
||||
|
||||
// Early return if no active connection or not in service discovery phase
|
||||
if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) {
|
||||
// Early return if no active connection
|
||||
if (this->address_ == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle service discovery
|
||||
this->send_service_for_discovery_();
|
||||
// Handle service discovery if in valid range
|
||||
if (this->send_service_ >= 0 && this->send_service_ <= this->service_count_) {
|
||||
this->send_service_for_discovery_();
|
||||
}
|
||||
|
||||
// Check if we should disable the loop
|
||||
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
|
||||
// - For other connections: Disable only after service discovery is complete
|
||||
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
|
||||
if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->send_service_ == DONE_SENDING_SERVICES)) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothConnection::reset_connection_(esp_err_t reason) {
|
||||
@@ -52,12 +138,12 @@ void BluetoothConnection::reset_connection_(esp_err_t reason) {
|
||||
// to detect incomplete service discovery rather than relying on us to
|
||||
// tell them about a partial list.
|
||||
this->set_address(0);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
this->send_service_ = INIT_SENDING_SERVICES;
|
||||
this->proxy_->send_connections_free();
|
||||
}
|
||||
|
||||
void BluetoothConnection::send_service_for_discovery_() {
|
||||
if (this->send_service_ == this->service_count_) {
|
||||
if (this->send_service_ >= this->service_count_) {
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
this->proxy_->send_gatt_services_done(this->address_);
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
@@ -70,120 +156,207 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
// Early return if no API connection
|
||||
auto *api_conn = this->proxy_->get_api_connection();
|
||||
if (api_conn == nullptr) {
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
|
||||
// Send next service
|
||||
esp_gattc_service_elem_t service_result;
|
||||
uint16_t service_count = 1;
|
||||
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
|
||||
&service_result, &service_count, this->send_service_);
|
||||
this->send_service_++;
|
||||
|
||||
if (service_status != ESP_GATT_OK) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service error at offset=%d, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), this->send_service_ - 1, service_status);
|
||||
return;
|
||||
}
|
||||
|
||||
if (service_count == 0) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_service missing, service_count=%d", this->connection_index_,
|
||||
this->address_str().c_str(), service_count);
|
||||
return;
|
||||
}
|
||||
// Check if client supports efficient UUIDs
|
||||
bool use_efficient_uuids = this->supports_efficient_uuids_();
|
||||
|
||||
// Prepare response
|
||||
api::BluetoothGATTGetServicesResponse resp;
|
||||
resp.address = this->address_;
|
||||
auto &service_resp = resp.services[0];
|
||||
fill_128bit_uuid_array(service_resp.uuid, service_result.uuid);
|
||||
service_resp.handle = service_result.start_handle;
|
||||
|
||||
// Get the number of characteristics directly with one call
|
||||
uint16_t total_char_count = 0;
|
||||
esp_gatt_status_t char_count_status =
|
||||
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
||||
// Dynamic batching based on actual size
|
||||
// Conservative MTU limit for API messages (accounts for WPA3 overhead)
|
||||
static constexpr size_t MAX_PACKET_SIZE = 1360;
|
||||
|
||||
if (char_count_status == ESP_GATT_OK && total_char_count > 0) {
|
||||
// Only reserve if we successfully got a count
|
||||
service_resp.characteristics.reserve(total_char_count);
|
||||
} else if (char_count_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), char_count_status);
|
||||
// Keep running total of actual message size
|
||||
size_t current_size = 0;
|
||||
api::ProtoSize size;
|
||||
resp.calculate_size(size);
|
||||
current_size = size.get_size();
|
||||
|
||||
while (this->send_service_ < this->service_count_) {
|
||||
esp_gattc_service_elem_t service_result;
|
||||
uint16_t service_count = 1;
|
||||
esp_gatt_status_t service_status = esp_ble_gattc_get_service(this->gattc_if_, this->conn_id_, nullptr,
|
||||
&service_result, &service_count, this->send_service_);
|
||||
|
||||
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->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the number of characteristics BEFORE adding to response
|
||||
uint16_t total_char_count = 0;
|
||||
esp_gatt_status_t char_count_status =
|
||||
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_CHARACTERISTIC,
|
||||
service_result.start_handle, service_result.end_handle, 0, &total_char_count);
|
||||
|
||||
if (char_count_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
|
||||
// If this service likely won't fit, send current batch (unless it's the first)
|
||||
size_t estimated_size = estimate_service_size(total_char_count, use_efficient_uuids);
|
||||
if (!resp.services.empty() && (current_size + estimated_size > MAX_PACKET_SIZE)) {
|
||||
// This service likely won't fit, send current batch
|
||||
break;
|
||||
}
|
||||
|
||||
// Now add the service since we know it will likely fit
|
||||
resp.services.emplace_back();
|
||||
auto &service_resp = resp.services.back();
|
||||
|
||||
fill_gatt_uuid(service_resp.uuid, service_resp.short_uuid, service_result.uuid, use_efficient_uuids);
|
||||
|
||||
service_resp.handle = service_result.start_handle;
|
||||
|
||||
if (total_char_count > 0) {
|
||||
// Reserve space and process characteristics
|
||||
service_resp.characteristics.reserve(total_char_count);
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
while (true) { // characteristics
|
||||
uint16_t char_count = 1;
|
||||
esp_gatt_status_t char_status =
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||
service_result.end_handle, &char_result, &char_count, char_offset);
|
||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (char_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_all_char", char_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (char_count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
service_resp.characteristics.emplace_back();
|
||||
auto &characteristic_resp = service_resp.characteristics.back();
|
||||
|
||||
fill_gatt_uuid(characteristic_resp.uuid, characteristic_resp.short_uuid, char_result.uuid, use_efficient_uuids);
|
||||
|
||||
characteristic_resp.handle = char_result.char_handle;
|
||||
characteristic_resp.properties = char_result.properties;
|
||||
char_offset++;
|
||||
|
||||
// Get the number of descriptors directly with one call
|
||||
uint16_t total_desc_count = 0;
|
||||
esp_gatt_status_t desc_count_status = esp_ble_gattc_get_attr_count(
|
||||
this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
|
||||
|
||||
if (desc_count_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (total_desc_count == 0) {
|
||||
// No descriptors, continue to next characteristic
|
||||
continue;
|
||||
}
|
||||
|
||||
// Reserve space and process descriptors
|
||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (true) { // descriptors
|
||||
uint16_t desc_count = 1;
|
||||
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
||||
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK) {
|
||||
this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status);
|
||||
this->send_service_ = DONE_SENDING_SERVICES;
|
||||
return;
|
||||
}
|
||||
if (desc_count == 0) {
|
||||
break; // No more descriptors
|
||||
}
|
||||
|
||||
characteristic_resp.descriptors.emplace_back();
|
||||
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
||||
|
||||
fill_gatt_uuid(descriptor_resp.uuid, descriptor_resp.short_uuid, desc_result.uuid, use_efficient_uuids);
|
||||
|
||||
descriptor_resp.handle = desc_result.handle;
|
||||
desc_offset++;
|
||||
}
|
||||
}
|
||||
} // end if (total_char_count > 0)
|
||||
|
||||
// Calculate the actual size of just this service
|
||||
api::ProtoSize service_sizer;
|
||||
service_resp.calculate_size(service_sizer);
|
||||
size_t service_size = service_sizer.get_size() + 1; // +1 for field tag
|
||||
|
||||
// Check if adding this service would exceed the limit
|
||||
if (current_size + service_size > MAX_PACKET_SIZE) {
|
||||
// We would go over - pop the last service if we have more than one
|
||||
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,
|
||||
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);
|
||||
// Increment so we don't get stuck
|
||||
this->send_service_++;
|
||||
}
|
||||
// Send what we have
|
||||
break;
|
||||
}
|
||||
|
||||
// Now we know we're keeping this service, add its size
|
||||
current_size += service_size;
|
||||
// Successfully added this service, increment counter
|
||||
this->send_service_++;
|
||||
}
|
||||
|
||||
// Now process characteristics
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
while (true) { // characteristics
|
||||
uint16_t char_count = 1;
|
||||
esp_gatt_status_t char_status =
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||
service_result.end_handle, &char_result, &char_count, char_offset);
|
||||
if (char_status == ESP_GATT_INVALID_OFFSET || char_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (char_status != ESP_GATT_OK) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), char_status);
|
||||
break;
|
||||
}
|
||||
if (char_count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
service_resp.characteristics.emplace_back();
|
||||
auto &characteristic_resp = service_resp.characteristics.back();
|
||||
fill_128bit_uuid_array(characteristic_resp.uuid, char_result.uuid);
|
||||
characteristic_resp.handle = char_result.char_handle;
|
||||
characteristic_resp.properties = char_result.properties;
|
||||
char_offset++;
|
||||
|
||||
// Get the number of descriptors directly with one call
|
||||
uint16_t total_desc_count = 0;
|
||||
esp_gatt_status_t desc_count_status =
|
||||
esp_ble_gattc_get_attr_count(this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, char_result.char_handle,
|
||||
service_result.end_handle, 0, &total_desc_count);
|
||||
|
||||
if (desc_count_status == ESP_GATT_OK && total_desc_count > 0) {
|
||||
// Only reserve if we successfully got a count
|
||||
characteristic_resp.descriptors.reserve(total_desc_count);
|
||||
} else if (desc_count_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), char_result.char_handle, desc_count_status);
|
||||
}
|
||||
|
||||
// Now process descriptors
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (true) { // descriptors
|
||||
uint16_t desc_count = 1;
|
||||
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
||||
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||
if (desc_status == ESP_GATT_INVALID_OFFSET || desc_status == ESP_GATT_NOT_FOUND) {
|
||||
break;
|
||||
}
|
||||
if (desc_status != ESP_GATT_OK) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
|
||||
this->address_str().c_str(), desc_status);
|
||||
break;
|
||||
}
|
||||
if (desc_count == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
characteristic_resp.descriptors.emplace_back();
|
||||
auto &descriptor_resp = characteristic_resp.descriptors.back();
|
||||
fill_128bit_uuid_array(descriptor_resp.uuid, desc_result.uuid);
|
||||
descriptor_resp.handle = desc_result.handle;
|
||||
desc_offset++;
|
||||
}
|
||||
}
|
||||
|
||||
// Send the message (we already checked api_conn is not null at the beginning)
|
||||
// Send the message with dynamically batched services
|
||||
api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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(),
|
||||
operation, handle, status);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::check_and_log_error_(const char *operation, esp_err_t err) {
|
||||
if (err != ESP_OK) {
|
||||
this->log_connection_warning_(operation, err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
|
||||
@@ -224,8 +397,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
case ESP_GATTC_READ_DESCR_EVT:
|
||||
case ESP_GATTC_READ_CHAR_EVT: {
|
||||
if (param->read.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->read.handle, param->read.status);
|
||||
this->log_gatt_operation_error_("reading char/descriptor", param->read.handle, param->read.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status);
|
||||
break;
|
||||
}
|
||||
@@ -239,8 +411,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
case ESP_GATTC_WRITE_CHAR_EVT:
|
||||
case ESP_GATTC_WRITE_DESCR_EVT: {
|
||||
if (param->write.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->write.handle, param->write.status);
|
||||
this->log_gatt_operation_error_("writing char/descriptor", param->write.handle, param->write.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status);
|
||||
break;
|
||||
}
|
||||
@@ -252,9 +423,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
}
|
||||
case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
|
||||
if (param->unreg_for_notify.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d",
|
||||
this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle,
|
||||
param->unreg_for_notify.status);
|
||||
this->log_gatt_operation_error_("unregistering notifications", param->unreg_for_notify.handle,
|
||||
param->unreg_for_notify.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
@@ -266,8 +436,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
if (param->reg_for_notify.status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status);
|
||||
this->log_gatt_operation_error_("registering notifications", param->reg_for_notify.handle,
|
||||
param->reg_for_notify.status);
|
||||
this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status);
|
||||
break;
|
||||
}
|
||||
@@ -313,8 +483,7 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
|
||||
|
||||
esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
|
||||
if (!this->connected()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT characteristic, not connected.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
this->log_gatt_not_connected_("read", "characteristic");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
@@ -322,18 +491,12 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
|
||||
handle);
|
||||
|
||||
esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char error, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) {
|
||||
if (!this->connected()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT characteristic, not connected.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
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(),
|
||||
@@ -342,36 +505,24 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
|
||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char error, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
return this->check_and_log_error_("esp_ble_gattc_write_char", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
|
||||
if (!this->connected()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT descriptor, not connected.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
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_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char_descr error, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) {
|
||||
if (!this->connected()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT descriptor, not connected.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
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(),
|
||||
@@ -380,18 +531,12 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri
|
||||
esp_err_t err = esp_ble_gattc_write_char_descr(
|
||||
this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
|
||||
response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ERR_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
return ESP_OK;
|
||||
return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
|
||||
}
|
||||
|
||||
esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) {
|
||||
if (!this->connected()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Cannot notify GATT characteristic, not connected.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
this->log_gatt_not_connected_("notify", "characteristic");
|
||||
return ESP_GATT_NOT_CONNECTED;
|
||||
}
|
||||
|
||||
@@ -399,22 +544,13 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
|
||||
ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_.c_str(), handle);
|
||||
esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_register_for_notify failed, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
} else {
|
||||
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_.c_str(), handle);
|
||||
esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_unregister_for_notify failed, err=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), err);
|
||||
return err;
|
||||
}
|
||||
return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
|
||||
}
|
||||
return ESP_OK;
|
||||
|
||||
ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
|
||||
this->address_str_.c_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);
|
||||
}
|
||||
|
||||
esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() {
|
||||
|
@@ -24,18 +24,27 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
|
||||
|
||||
esp_err_t notify_characteristic(uint16_t handle, bool enable);
|
||||
|
||||
void set_address(uint64_t address) override;
|
||||
|
||||
protected:
|
||||
friend class BluetoothProxy;
|
||||
|
||||
bool supports_efficient_uuids_() const;
|
||||
void send_service_for_discovery_();
|
||||
void reset_connection_(esp_err_t reason);
|
||||
void update_allocated_slot_(uint64_t find_value, uint64_t set_value);
|
||||
void log_connection_error_(const char *operation, esp_gatt_status_t status);
|
||||
void log_connection_warning_(const char *operation, esp_err_t err);
|
||||
void log_gatt_not_connected_(const char *action, const char *type);
|
||||
void log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status);
|
||||
esp_err_t check_and_log_error_(const char *operation, esp_err_t err);
|
||||
|
||||
// Memory optimized layout for 32-bit systems
|
||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||
BluetoothProxy *proxy_;
|
||||
|
||||
// Group 2: 2-byte types
|
||||
int16_t send_service_{-2}; // Needs to handle negative values and service count
|
||||
int16_t send_service_{-3}; // -3 = INIT_SENDING_SERVICES, -2 = DONE_SENDING_SERVICES, >=0 = service index
|
||||
|
||||
// Group 3: 1-byte types
|
||||
bool seen_mtu_or_services_{false};
|
||||
|
@@ -25,16 +25,16 @@ static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62
|
||||
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
|
||||
|
||||
void BluetoothProxy::setup() {
|
||||
// Pre-allocate response object
|
||||
this->response_ = std::make_unique<api::BluetoothLERawAdvertisementsResponse>();
|
||||
|
||||
// Reserve capacity but start with size 0
|
||||
// Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE
|
||||
this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2);
|
||||
this->response_.advertisements.reserve(FLUSH_BATCH_SIZE / 2);
|
||||
|
||||
// Don't pre-allocate pool - let it grow only if needed in busy environments
|
||||
// Many devices in quiet areas will never need the overflow pool
|
||||
|
||||
this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS;
|
||||
this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS;
|
||||
|
||||
this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_bluetooth_scanner_state_(state);
|
||||
@@ -50,6 +50,26 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
|
||||
this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) {
|
||||
ESP_LOGW(TAG, "Cannot %s GATT %s, not connected", action, type);
|
||||
}
|
||||
|
||||
void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action,
|
||||
const char *type) {
|
||||
this->log_not_connected_gatt_(action, type);
|
||||
this->send_gatt_error(address, handle, ESP_GATT_NOT_CONNECTED);
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
|
||||
// This method should never be called since bluetooth_proxy always uses raw advertisements
|
||||
@@ -62,7 +82,7 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results,
|
||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||||
return false;
|
||||
|
||||
auto &advertisements = this->response_->advertisements;
|
||||
auto &advertisements = this->response_.advertisements;
|
||||
|
||||
for (size_t i = 0; i < count; i++) {
|
||||
auto &result = scan_results[i];
|
||||
@@ -106,7 +126,7 @@ void BluetoothProxy::flush_pending_advertisements() {
|
||||
if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
|
||||
return;
|
||||
|
||||
auto &advertisements = this->response_->advertisements;
|
||||
auto &advertisements = this->response_.advertisements;
|
||||
|
||||
// Return any items beyond advertisement_count_ to the pool
|
||||
if (advertisements.size() > this->advertisement_count_) {
|
||||
@@ -120,37 +140,24 @@ void BluetoothProxy::flush_pending_advertisements() {
|
||||
}
|
||||
|
||||
// Send the message
|
||||
this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
|
||||
this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
|
||||
|
||||
// Reset count - existing items will be overwritten in next batch
|
||||
this->advertisement_count_ = 0;
|
||||
}
|
||||
|
||||
void BluetoothProxy::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Bluetooth Proxy:\n"
|
||||
" Active: %s\n"
|
||||
" Connections: %d",
|
||||
YESNO(this->active_), this->connections_.size());
|
||||
}
|
||||
|
||||
int BluetoothProxy::get_bluetooth_connections_free() {
|
||||
int free = 0;
|
||||
for (auto *connection : this->connections_) {
|
||||
if (connection->address_ == 0) {
|
||||
free++;
|
||||
ESP_LOGV(TAG, "[%d] Free connection", connection->get_connection_index());
|
||||
} else {
|
||||
ESP_LOGV(TAG, "[%d] Used connection by [%s]", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
}
|
||||
}
|
||||
return free;
|
||||
YESNO(this->active_), this->connection_count_);
|
||||
}
|
||||
|
||||
void BluetoothProxy::loop() {
|
||||
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
|
||||
for (auto *connection : this->connections_) {
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
|
||||
connection->disconnect();
|
||||
}
|
||||
@@ -173,7 +180,8 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
|
||||
}
|
||||
|
||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
|
||||
for (auto *connection : this->connections_) {
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() == address)
|
||||
return connection;
|
||||
}
|
||||
@@ -181,9 +189,10 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
|
||||
if (!reserve)
|
||||
return nullptr;
|
||||
|
||||
for (auto *connection : this->connections_) {
|
||||
for (uint8_t i = 0; i < this->connection_count_; i++) {
|
||||
auto *connection = this->connections_[i];
|
||||
if (connection->get_address() == 0) {
|
||||
connection->send_service_ = DONE_SENDING_SERVICES;
|
||||
connection->send_service_ = INIT_SENDING_SERVICES;
|
||||
connection->set_address(address);
|
||||
// All connections must start at INIT
|
||||
// We only set the state if we allocate the connection
|
||||
@@ -200,8 +209,7 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
|
||||
void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {
|
||||
switch (msg.request_type) {
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE:
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE:
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: {
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: {
|
||||
auto *connection = this->get_connection_(msg.address, true);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "No free connections available");
|
||||
@@ -210,23 +218,10 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
}
|
||||
if (connection->state() == espbt::ClientState::CONNECTED ||
|
||||
connection->state() == espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
this->log_connection_request_ignored_(connection, connection->state());
|
||||
this->send_device_connection(msg.address, true);
|
||||
this->send_connections_free();
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::SEARCHING) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::DISCOVERED) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device already discovered",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::READY_TO_CONNECT) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, waiting in line to connect",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::CONNECTING) {
|
||||
if (connection->disconnect_pending()) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
|
||||
@@ -234,29 +229,18 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
connection->cancel_pending_disconnect();
|
||||
return;
|
||||
}
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already connecting", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
return;
|
||||
} else if (connection->state() == espbt::ClientState::DISCONNECTING) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device is disconnecting",
|
||||
connection->get_connection_index(), connection->address_str().c_str());
|
||||
this->log_connection_request_ignored_(connection, connection->state());
|
||||
return;
|
||||
} else if (connection->state() != espbt::ClientState::INIT) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
this->log_connection_request_ignored_(connection, connection->state());
|
||||
return;
|
||||
}
|
||||
if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE) {
|
||||
connection->set_connection_type(espbt::ConnectionType::V3_WITH_CACHE);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting v3 with cache", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
} else if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE) {
|
||||
this->log_connection_info_(connection, "v3 with cache");
|
||||
} else { // BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE
|
||||
connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting v3 without cache", connection->get_connection_index(),
|
||||
connection->address_str().c_str());
|
||||
} else {
|
||||
connection->set_connection_type(espbt::ConnectionType::V1);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connecting v1", connection->get_connection_index(), connection->address_str().c_str());
|
||||
this->log_connection_info_(connection, "v3 without cache");
|
||||
}
|
||||
if (msg.has_address_type) {
|
||||
uint64_to_bd_addr(msg.address, connection->remote_bda_);
|
||||
@@ -318,14 +302,18 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
|
||||
|
||||
break;
|
||||
}
|
||||
case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: {
|
||||
ESP_LOGE(TAG, "V1 connections removed");
|
||||
this->send_device_connection(msg.address, false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected");
|
||||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "characteristic");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -338,8 +326,7 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms
|
||||
void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected");
|
||||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "characteristic");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -352,8 +339,7 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
|
||||
void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected");
|
||||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "descriptor");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -366,8 +352,7 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead
|
||||
void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected");
|
||||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "descriptor");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -380,8 +365,7 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
|
||||
void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr || !connection->connected()) {
|
||||
ESP_LOGW(TAG, "Cannot get GATT services, not connected");
|
||||
this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, 0, "get", "services");
|
||||
return;
|
||||
}
|
||||
if (!connection->service_count_) {
|
||||
@@ -389,16 +373,14 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
|
||||
this->send_gatt_services_done(msg.address);
|
||||
return;
|
||||
}
|
||||
if (connection->send_service_ ==
|
||||
DONE_SENDING_SERVICES) // Only start sending services if we're not already sending them
|
||||
if (connection->send_service_ == INIT_SENDING_SERVICES) // Start sending services if not started yet
|
||||
connection->send_service_ = 0;
|
||||
}
|
||||
|
||||
void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {
|
||||
auto *connection = this->get_connection_(msg.address, false);
|
||||
if (connection == nullptr) {
|
||||
ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected");
|
||||
this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
|
||||
this->handle_gatt_not_connected_(msg.address, msg.handle, "notify", "characteristic");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -439,17 +421,13 @@ void BluetoothProxy::send_device_connection(uint64_t address, bool connected, ui
|
||||
this->api_connection_->send_message(call, api::BluetoothDeviceConnectionResponse::MESSAGE_TYPE);
|
||||
}
|
||||
void BluetoothProxy::send_connections_free() {
|
||||
if (this->api_connection_ == nullptr)
|
||||
return;
|
||||
api::BluetoothConnectionsFreeResponse call;
|
||||
call.free = this->get_bluetooth_connections_free();
|
||||
call.limit = this->get_bluetooth_connections_limit();
|
||||
for (auto *connection : this->connections_) {
|
||||
if (connection->address_ != 0) {
|
||||
call.allocated.push_back(connection->address_);
|
||||
}
|
||||
if (this->api_connection_ != nullptr) {
|
||||
this->send_connections_free(this->api_connection_);
|
||||
}
|
||||
this->api_connection_->send_message(call, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_connections_free(api::APIConnection *api_connection) {
|
||||
api_connection->send_message(this->connections_free_response_, api::BluetoothConnectionsFreeResponse::MESSAGE_TYPE);
|
||||
}
|
||||
|
||||
void BluetoothProxy::send_gatt_services_done(uint64_t address) {
|
||||
|
@@ -2,6 +2,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
@@ -22,6 +23,7 @@ namespace esphome::bluetooth_proxy {
|
||||
|
||||
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
|
||||
static const int DONE_SENDING_SERVICES = -2;
|
||||
static const int INIT_SENDING_SERVICES = -3;
|
||||
|
||||
using namespace esp32_ble_client;
|
||||
|
||||
@@ -49,6 +51,7 @@ enum BluetoothProxySubscriptionFlag : uint32_t {
|
||||
};
|
||||
|
||||
class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component {
|
||||
friend class BluetoothConnection; // Allow connection to update connections_free_response_
|
||||
public:
|
||||
BluetoothProxy();
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
@@ -62,8 +65,10 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
|
||||
|
||||
void register_connection(BluetoothConnection *connection) {
|
||||
this->connections_.push_back(connection);
|
||||
connection->proxy_ = this;
|
||||
if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
|
||||
this->connections_[this->connection_count_++] = connection;
|
||||
connection->proxy_ = this;
|
||||
}
|
||||
}
|
||||
|
||||
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
|
||||
@@ -74,15 +79,13 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg);
|
||||
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg);
|
||||
|
||||
int get_bluetooth_connections_free();
|
||||
int get_bluetooth_connections_limit() { return this->connections_.size(); }
|
||||
|
||||
void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags);
|
||||
void unsubscribe_api_connection(api::APIConnection *api_connection);
|
||||
api::APIConnection *get_api_connection() { return this->api_connection_; }
|
||||
|
||||
void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK);
|
||||
void send_connections_free();
|
||||
void send_connections_free(api::APIConnection *api_connection);
|
||||
void send_gatt_services_done(uint64_t address);
|
||||
void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error);
|
||||
void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK);
|
||||
@@ -134,25 +137,33 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
|
||||
void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
|
||||
|
||||
BluetoothConnection *get_connection_(uint64_t address, bool reserve);
|
||||
void log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state);
|
||||
void log_connection_info_(BluetoothConnection *connection, const char *message);
|
||||
void log_not_connected_gatt_(const char *action, const char *type);
|
||||
void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type);
|
||||
|
||||
// Memory optimized layout for 32-bit systems
|
||||
// Group 1: Pointers (4 bytes each, naturally aligned)
|
||||
api::APIConnection *api_connection_{nullptr};
|
||||
|
||||
// Group 2: Container types (typically 12 bytes on 32-bit)
|
||||
std::vector<BluetoothConnection *> connections_{};
|
||||
// Group 2: Fixed-size array of connection pointers
|
||||
std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
|
||||
|
||||
// BLE advertisement batching
|
||||
std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
|
||||
std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
|
||||
api::BluetoothLERawAdvertisementsResponse response_;
|
||||
|
||||
// Group 3: 4-byte types
|
||||
uint32_t last_advertisement_flush_time_{0};
|
||||
|
||||
// Pre-allocated response message - always ready to send
|
||||
api::BluetoothConnectionsFreeResponse connections_free_response_;
|
||||
|
||||
// Group 4: 1-byte types grouped together
|
||||
bool active_;
|
||||
uint8_t advertisement_count_{0};
|
||||
// 2 bytes used, 2 bytes padding
|
||||
uint8_t connection_count_{0};
|
||||
// 3 bytes used, 1 byte padding
|
||||
};
|
||||
|
||||
extern BluetoothProxy *global_bluetooth_proxy; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
@@ -28,7 +28,7 @@ const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.0,
|
||||
const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0, 0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8,
|
||||
-0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
|
||||
|
||||
static const char *oversampling_to_str(BME680Oversampling oversampling) {
|
||||
[[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) {
|
||||
switch (oversampling) {
|
||||
case BME680_OVERSAMPLING_NONE:
|
||||
return "None";
|
||||
@@ -47,7 +47,7 @@ static const char *oversampling_to_str(BME680Oversampling oversampling) {
|
||||
}
|
||||
}
|
||||
|
||||
static const char *iir_filter_to_str(BME680IIRFilter filter) {
|
||||
[[maybe_unused]] static const char *iir_filter_to_str(BME680IIRFilter filter) {
|
||||
switch (filter) {
|
||||
case BME680_IIR_FILTER_OFF:
|
||||
return "OFF";
|
||||
|
@@ -137,4 +137,3 @@ async def button_press_to_code(config, action_id, template_arg, args):
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_global(button_ns.using)
|
||||
cg.add_define("USE_BUTTON")
|
||||
|
@@ -14,7 +14,7 @@ from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
AUTO_LOAD = ["web_server_base", "ota.web_server"]
|
||||
DEPENDENCIES = ["wifi"]
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
captive_portal_ns = cg.esphome_ns.namespace("captive_portal")
|
||||
CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component)
|
||||
|
@@ -519,5 +519,4 @@ async def climate_control_to_code(config, action_id, template_arg, args):
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_CLIMATE")
|
||||
cg.add_global(climate_ns.using)
|
||||
|
@@ -5,6 +5,13 @@
|
||||
#include <set>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
#ifdef USE_API
|
||||
namespace api {
|
||||
class APIConnection;
|
||||
} // namespace api
|
||||
#endif
|
||||
|
||||
namespace climate {
|
||||
|
||||
/** This class contains all static data for climate devices.
|
||||
@@ -173,6 +180,23 @@ class ClimateTraits {
|
||||
void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
|
||||
|
||||
protected:
|
||||
#ifdef USE_API
|
||||
// The API connection is a friend class to access internal methods
|
||||
friend class api::APIConnection;
|
||||
// These methods return references to internal data structures.
|
||||
// They are used by the API to avoid copying data when encoding messages.
|
||||
// Warning: Do not use these methods outside of the API connection code.
|
||||
// They return references to internal data that can be invalidated.
|
||||
const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; }
|
||||
const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; }
|
||||
const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const {
|
||||
return this->supported_custom_fan_modes_;
|
||||
}
|
||||
const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; }
|
||||
const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; }
|
||||
const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; }
|
||||
#endif
|
||||
|
||||
void set_mode_support_(climate::ClimateMode mode, bool supported) {
|
||||
if (supported) {
|
||||
this->supported_modes_.insert(mode);
|
||||
|
@@ -265,5 +265,4 @@ async def cover_control_to_code(config, action_id, template_arg, args):
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_COVER")
|
||||
cg.add_global(cover_ns.using)
|
||||
|
@@ -99,43 +99,39 @@ const optional<float> &CoverCall::get_tilt() const { return this->tilt_; }
|
||||
const optional<bool> &CoverCall::get_toggle() const { return this->toggle_; }
|
||||
void CoverCall::validate_() {
|
||||
auto traits = this->parent_->get_traits();
|
||||
const char *name = this->parent_->get_name().c_str();
|
||||
|
||||
if (this->position_.has_value()) {
|
||||
auto pos = *this->position_;
|
||||
if (!traits.get_supports_position() && pos != COVER_OPEN && pos != COVER_CLOSED) {
|
||||
ESP_LOGW(TAG, "'%s' - This cover device does not support setting position!", this->parent_->get_name().c_str());
|
||||
ESP_LOGW(TAG, "'%s': position unsupported", name);
|
||||
this->position_.reset();
|
||||
} else if (pos < 0.0f || pos > 1.0f) {
|
||||
ESP_LOGW(TAG, "'%s' - Position %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), pos);
|
||||
ESP_LOGW(TAG, "'%s': position %.2f out of range", name, pos);
|
||||
this->position_ = clamp(pos, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
if (this->tilt_.has_value()) {
|
||||
auto tilt = *this->tilt_;
|
||||
if (!traits.get_supports_tilt()) {
|
||||
ESP_LOGW(TAG, "'%s' - This cover device does not support tilt!", this->parent_->get_name().c_str());
|
||||
ESP_LOGW(TAG, "'%s': tilt unsupported", name);
|
||||
this->tilt_.reset();
|
||||
} else if (tilt < 0.0f || tilt > 1.0f) {
|
||||
ESP_LOGW(TAG, "'%s' - Tilt %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), tilt);
|
||||
ESP_LOGW(TAG, "'%s': tilt %.2f out of range", name, tilt);
|
||||
this->tilt_ = clamp(tilt, 0.0f, 1.0f);
|
||||
}
|
||||
}
|
||||
if (this->toggle_.has_value()) {
|
||||
if (!traits.get_supports_toggle()) {
|
||||
ESP_LOGW(TAG, "'%s' - This cover device does not support toggle!", this->parent_->get_name().c_str());
|
||||
ESP_LOGW(TAG, "'%s': toggle unsupported", name);
|
||||
this->toggle_.reset();
|
||||
}
|
||||
}
|
||||
if (this->stop_) {
|
||||
if (this->position_.has_value()) {
|
||||
ESP_LOGW(TAG, "Cannot set position when stopping a cover!");
|
||||
if (this->position_.has_value() || this->tilt_.has_value() || this->toggle_.has_value()) {
|
||||
ESP_LOGW(TAG, "'%s': cannot position/tilt/toggle when stopping", name);
|
||||
this->position_.reset();
|
||||
}
|
||||
if (this->tilt_.has_value()) {
|
||||
ESP_LOGW(TAG, "Cannot set tilt when stopping a cover!");
|
||||
this->tilt_.reset();
|
||||
}
|
||||
if (this->toggle_.has_value()) {
|
||||
ESP_LOGW(TAG, "Cannot set toggle when stopping a cover!");
|
||||
this->toggle_.reset();
|
||||
}
|
||||
}
|
||||
|
@@ -164,7 +164,6 @@ async def register_datetime(var, config):
|
||||
cg.add(getattr(cg.App, f"register_{entity_type}")(var))
|
||||
CORE.register_platform_component(entity_type, var)
|
||||
await setup_datetime_core_(var, config)
|
||||
cg.add_define(f"USE_DATETIME_{config[CONF_TYPE]}")
|
||||
|
||||
|
||||
async def new_datetime(config, *args):
|
||||
@@ -175,7 +174,6 @@ async def new_datetime(config, *args):
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_DATETIME")
|
||||
cg.add_global(datetime_ns.using)
|
||||
|
||||
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
@@ -10,8 +11,9 @@ from esphome.const import (
|
||||
CONF_LOOP_TIME,
|
||||
PlatformFramework,
|
||||
)
|
||||
from esphome.core import CORE
|
||||
|
||||
CODEOWNERS = ["@OttoWinter"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["logger"]
|
||||
|
||||
CONF_DEBUG_ID = "debug_id"
|
||||
@@ -44,6 +46,8 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("HWINFO", True)
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -62,5 +66,6 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
|
||||
PlatformFramework.RTL87XX_ARDUINO,
|
||||
PlatformFramework.LN882X_ARDUINO,
|
||||
},
|
||||
"debug_zephyr.cpp": {PlatformFramework.NRF52_ZEPHYR},
|
||||
}
|
||||
)
|
||||
|
281
esphome/components/debug/debug_zephyr.cpp
Normal file
281
esphome/components/debug/debug_zephyr.cpp
Normal file
@@ -0,0 +1,281 @@
|
||||
#include "debug_component.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <climits>
|
||||
#include "esphome/core/log.h"
|
||||
#include <zephyr/drivers/hwinfo.h>
|
||||
#include <hal/nrf_power.h>
|
||||
#include <cstdint>
|
||||
|
||||
#define BOOTLOADER_VERSION_REGISTER NRF_TIMER2->CC[0]
|
||||
|
||||
namespace esphome {
|
||||
namespace debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
|
||||
constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8;
|
||||
|
||||
static void show_reset_reason(std::string &reset_reason, bool set, const char *reason) {
|
||||
if (!set) {
|
||||
return;
|
||||
}
|
||||
if (!reset_reason.empty()) {
|
||||
reset_reason += ", ";
|
||||
}
|
||||
reset_reason += reason;
|
||||
}
|
||||
|
||||
inline uint32_t read_mem_u32(uintptr_t addr) {
|
||||
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
|
||||
}
|
||||
|
||||
std::string DebugComponent::get_reset_reason_() {
|
||||
uint32_t cause;
|
||||
auto ret = hwinfo_get_reset_cause(&cause);
|
||||
if (ret) {
|
||||
ESP_LOGE(TAG, "Unable to get reset cause: %d", ret);
|
||||
return "";
|
||||
}
|
||||
std::string reset_reason;
|
||||
|
||||
show_reset_reason(reset_reason, cause & RESET_PIN, "External pin");
|
||||
show_reset_reason(reset_reason, cause & RESET_SOFTWARE, "Software reset");
|
||||
show_reset_reason(reset_reason, cause & RESET_BROWNOUT, "Brownout (drop in voltage)");
|
||||
show_reset_reason(reset_reason, cause & RESET_POR, "Power-on reset (POR)");
|
||||
show_reset_reason(reset_reason, cause & RESET_WATCHDOG, "Watchdog timer expiration");
|
||||
show_reset_reason(reset_reason, cause & RESET_DEBUG, "Debug event");
|
||||
show_reset_reason(reset_reason, cause & RESET_SECURITY, "Security violation");
|
||||
show_reset_reason(reset_reason, cause & RESET_LOW_POWER_WAKE, "Waking up from low power mode");
|
||||
show_reset_reason(reset_reason, cause & RESET_CPU_LOCKUP, "CPU lock-up detected");
|
||||
show_reset_reason(reset_reason, cause & RESET_PARITY, "Parity error");
|
||||
show_reset_reason(reset_reason, cause & RESET_PLL, "PLL error");
|
||||
show_reset_reason(reset_reason, cause & RESET_CLOCK, "Clock error");
|
||||
show_reset_reason(reset_reason, cause & RESET_HARDWARE, "Hardware reset");
|
||||
show_reset_reason(reset_reason, cause & RESET_USER, "User reset");
|
||||
show_reset_reason(reset_reason, cause & RESET_TEMPERATURE, "Temperature reset");
|
||||
|
||||
ESP_LOGD(TAG, "Reset Reason: %s", reset_reason.c_str());
|
||||
return reset_reason;
|
||||
}
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
|
||||
|
||||
void DebugComponent::get_device_info_(std::string &device_info) {
|
||||
std::string supply = "Main supply status: ";
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) {
|
||||
supply += "Normal voltage.";
|
||||
} else {
|
||||
supply += "High voltage.";
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", supply.c_str());
|
||||
device_info += "|" + supply;
|
||||
|
||||
std::string reg0 = "Regulator stage 0: ";
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
|
||||
reg0 += nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
reg0 += ", ";
|
||||
switch (NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) {
|
||||
case (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "1.8V (default)";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_1V8 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "1.8V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V1 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "2.1V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V4 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "2.4V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_2V7 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "2.7V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "3.0V";
|
||||
break;
|
||||
case (UICR_REGOUT0_VOUT_3V3 << UICR_REGOUT0_VOUT_Pos):
|
||||
reg0 += "3.3V";
|
||||
break;
|
||||
default:
|
||||
reg0 += "???V";
|
||||
}
|
||||
} else {
|
||||
reg0 += "disabled";
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", reg0.c_str());
|
||||
device_info += "|" + reg0;
|
||||
|
||||
std::string reg1 = "Regulator stage 1: ";
|
||||
reg1 += nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
|
||||
ESP_LOGD(TAG, "%s", reg1.c_str());
|
||||
device_info += "|" + reg1;
|
||||
|
||||
std::string usb_power = "USB power state: ";
|
||||
if (nrf_power_usbregstatus_vbusdet_get(NRF_POWER)) {
|
||||
if (nrf_power_usbregstatus_outrdy_get(NRF_POWER)) {
|
||||
/**< From the power viewpoint, USB is ready for working. */
|
||||
usb_power += "ready";
|
||||
} else {
|
||||
/**< The USB power is detected, but USB power regulator is not ready. */
|
||||
usb_power += "connected (regulator is not ready)";
|
||||
}
|
||||
} else {
|
||||
/**< No power on USB lines detected. */
|
||||
usb_power += "disconected";
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", usb_power.c_str());
|
||||
device_info += "|" + usb_power;
|
||||
|
||||
bool enabled;
|
||||
nrf_power_pof_thr_t pof_thr;
|
||||
|
||||
pof_thr = nrf_power_pofcon_get(NRF_POWER, &enabled);
|
||||
std::string pof = "Power-fail comparator: ";
|
||||
if (enabled) {
|
||||
switch (pof_thr) {
|
||||
case POWER_POFCON_THRESHOLD_V17:
|
||||
pof += "1.7V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V18:
|
||||
pof += "1.8V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V19:
|
||||
pof += "1.9V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V20:
|
||||
pof += "2.0V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V21:
|
||||
pof += "2.1V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V22:
|
||||
pof += "2.2V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V23:
|
||||
pof += "2.3V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V24:
|
||||
pof += "2.4V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V25:
|
||||
pof += "2.5V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V26:
|
||||
pof += "2.6V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V27:
|
||||
pof += "2.7V";
|
||||
break;
|
||||
case POWER_POFCON_THRESHOLD_V28:
|
||||
pof += "2.8V";
|
||||
break;
|
||||
}
|
||||
|
||||
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
|
||||
pof += ", VDDH: ";
|
||||
switch (nrf_power_pofcon_vddh_get(NRF_POWER)) {
|
||||
case NRF_POWER_POFTHRVDDH_V27:
|
||||
pof += "2.7V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V28:
|
||||
pof += "2.8V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V29:
|
||||
pof += "2.9V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V30:
|
||||
pof += "3.0V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V31:
|
||||
pof += "3.1V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V32:
|
||||
pof += "3.2V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V33:
|
||||
pof += "3.3V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V34:
|
||||
pof += "3.4V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V35:
|
||||
pof += "3.5V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V36:
|
||||
pof += "3.6V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V37:
|
||||
pof += "3.7V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V38:
|
||||
pof += "3.8V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V39:
|
||||
pof += "3.9V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V40:
|
||||
pof += "4.0V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V41:
|
||||
pof += "4.1V";
|
||||
break;
|
||||
case NRF_POWER_POFTHRVDDH_V42:
|
||||
pof += "4.2V";
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
pof += "disabled";
|
||||
}
|
||||
ESP_LOGD(TAG, "%s", pof.c_str());
|
||||
device_info += "|" + pof;
|
||||
|
||||
auto package = [](uint32_t value) {
|
||||
switch (value) {
|
||||
case 0x2004:
|
||||
return "QIxx - 7x7 73-pin aQFN";
|
||||
case 0x2000:
|
||||
return "QFxx - 6x6 48-pin QFN";
|
||||
case 0x2005:
|
||||
return "CKxx - 3.544 x 3.607 WLCSP";
|
||||
}
|
||||
return "Unspecified";
|
||||
};
|
||||
|
||||
ESP_LOGD(TAG, "Code page size: %u, code size: %u, device id: 0x%08x%08x", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE,
|
||||
NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0]);
|
||||
ESP_LOGD(TAG, "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x", NRF_FICR->ER[0],
|
||||
NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2],
|
||||
NRF_FICR->IR[3]);
|
||||
ESP_LOGD(TAG, "Device address type: %s, address: %s", (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"),
|
||||
get_mac_address_pretty().c_str());
|
||||
ESP_LOGD(TAG, "Part code: nRF%x, version: %c%c%c%c, package: %s", NRF_FICR->INFO.PART,
|
||||
NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, NRF_FICR->INFO.VARIANT >> 8 & 0xFF,
|
||||
NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE));
|
||||
ESP_LOGD(TAG, "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH,
|
||||
(NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
|
||||
ESP_LOGD(
|
||||
TAG, "GPIO as NFC pins: %s, GPIO as nRESET pin: %s",
|
||||
YESNO((NRF_UICR->NFCPINS & UICR_NFCPINS_PROTECT_Msk) == (UICR_NFCPINS_PROTECT_NFC << UICR_NFCPINS_PROTECT_Pos)),
|
||||
YESNO(((NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) !=
|
||||
(UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos)) ||
|
||||
((NRF_UICR->PSELRESET[1] & UICR_PSELRESET_CONNECT_Msk) !=
|
||||
(UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos))));
|
||||
|
||||
#ifdef USE_BOOTLOADER_MCUBOOT
|
||||
ESP_LOGD(TAG, "bootloader: mcuboot");
|
||||
#else
|
||||
ESP_LOGD(TAG, "bootloader: Adafruit, version %u.%u.%u", (BOOTLOADER_VERSION_REGISTER >> 16) & 0xFF,
|
||||
(BOOTLOADER_VERSION_REGISTER >> 8) & 0xFF, BOOTLOADER_VERSION_REGISTER & 0xFF);
|
||||
ESP_LOGD(TAG, "MBR bootloader addr 0x%08x, UICR bootloader addr 0x%08x", read_mem_u32(MBR_BOOTLOADER_ADDR),
|
||||
NRF_UICR->NRFFW[0]);
|
||||
ESP_LOGD(TAG, "MBR param page addr 0x%08x, UICR param page addr 0x%08x", read_mem_u32(MBR_PARAM_PAGE_ADDR),
|
||||
NRF_UICR->NRFFW[1]);
|
||||
#endif
|
||||
}
|
||||
|
||||
void DebugComponent::update_platform_() {}
|
||||
|
||||
} // namespace debug
|
||||
} // namespace esphome
|
||||
#endif
|
@@ -1,6 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.components.esp32 import CONF_CPU_FREQUENCY
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BLOCK,
|
||||
@@ -54,7 +55,7 @@ CONFIG_SCHEMA = {
|
||||
),
|
||||
cv.Optional(CONF_PSRAM): cv.All(
|
||||
cv.only_on_esp32,
|
||||
cv.requires_component("psram"),
|
||||
cv.requires_component(PSRAM_DOMAIN),
|
||||
sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_BYTES,
|
||||
icon=ICON_COUNTER,
|
||||
|
@@ -1,4 +1,5 @@
|
||||
#ifdef USE_ESP32
|
||||
#include "driver/gpio.h"
|
||||
#include "deep_sleep_component.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -74,11 +75,20 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
if (this->sleep_duration_.has_value())
|
||||
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
|
||||
if (this->wakeup_pin_ != nullptr) {
|
||||
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
|
||||
if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
|
||||
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
|
||||
} else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) {
|
||||
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
|
||||
}
|
||||
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
|
||||
gpio_hold_en(gpio_pin);
|
||||
gpio_deep_sleep_hold_en();
|
||||
bool level = !this->wakeup_pin_->is_inverted();
|
||||
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
||||
level = !level;
|
||||
}
|
||||
esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level);
|
||||
esp_sleep_enable_ext0_wakeup(gpio_pin, level);
|
||||
}
|
||||
if (this->ext1_wakeup_.has_value()) {
|
||||
esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
|
||||
@@ -102,6 +112,15 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
if (this->sleep_duration_.has_value())
|
||||
esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
|
||||
if (this->wakeup_pin_ != nullptr) {
|
||||
const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
|
||||
if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLUP) {
|
||||
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
|
||||
} else if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLDOWN) {
|
||||
gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
|
||||
}
|
||||
gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
|
||||
gpio_hold_en(gpio_pin);
|
||||
gpio_deep_sleep_hold_en();
|
||||
bool level = !this->wakeup_pin_->is_inverted();
|
||||
if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
|
||||
level = !level;
|
||||
|
@@ -76,6 +76,7 @@ CONF_ASSERTION_LEVEL = "assertion_level"
|
||||
CONF_COMPILER_OPTIMIZATION = "compiler_optimization"
|
||||
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES = "enable_idf_experimental_features"
|
||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_RELEASE = "release"
|
||||
|
||||
ASSERTION_LEVELS = {
|
||||
@@ -313,7 +314,7 @@ def _format_framework_espidf_version(
|
||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
|
||||
# The platform-espressif32 version to use for arduino frameworks
|
||||
# - https://github.com/pioarduino/platform-espressif32/releases
|
||||
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21)
|
||||
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
|
||||
|
||||
# The default/recommended esp-idf framework version
|
||||
# - https://github.com/espressif/esp-idf/releases
|
||||
@@ -322,7 +323,7 @@ RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2)
|
||||
# The platformio/espressif32 version to use for esp-idf frameworks
|
||||
# - https://github.com/platformio/platform-espressif32/releases
|
||||
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
|
||||
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21)
|
||||
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
|
||||
|
||||
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
|
||||
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
|
||||
@@ -468,10 +469,10 @@ def _parse_platform_version(value):
|
||||
try:
|
||||
ver = cv.Version.parse(cv.version_number(value))
|
||||
if ver.major >= 50: # a pioarduino version
|
||||
if "-" in value:
|
||||
# maybe a release candidate?...definitely not our default, just use it as-is...
|
||||
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{value}/platform-espressif32.zip"
|
||||
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{ver.major}.{ver.minor:02d}.{ver.patch:02d}/platform-espressif32.zip"
|
||||
release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
|
||||
if ver.extra:
|
||||
release += f"-{ver.extra}"
|
||||
return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
|
||||
# if platform version is a valid version constraint, prefix the default package
|
||||
cv.platformio_version_constraint(value)
|
||||
return f"platformio/espressif32@{value}"
|
||||
@@ -519,32 +520,59 @@ def _detect_variant(value):
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
if not (
|
||||
pio_options := fv.full_config.get()[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS)
|
||||
):
|
||||
# Not specified or empty
|
||||
return config
|
||||
|
||||
pio_flash_size_key = "board_upload.flash_size"
|
||||
pio_partitions_key = "board_build.partitions"
|
||||
if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
|
||||
raise cv.Invalid(
|
||||
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
|
||||
)
|
||||
|
||||
if pio_flash_size_key in pio_options:
|
||||
raise cv.Invalid(
|
||||
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
|
||||
)
|
||||
# Imported locally to avoid circular import issues
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
|
||||
errs = []
|
||||
full_config = fv.full_config.get()
|
||||
if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS):
|
||||
pio_flash_size_key = "board_upload.flash_size"
|
||||
pio_partitions_key = "board_build.partitions"
|
||||
if CONF_PARTITIONS in config and pio_partitions_key in pio_options:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"Do not specify '{pio_partitions_key}' in '{CONF_PLATFORMIO_OPTIONS}' with '{CONF_PARTITIONS}' in esp32"
|
||||
)
|
||||
)
|
||||
if pio_flash_size_key in pio_options:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
|
||||
)
|
||||
)
|
||||
if (
|
||||
config[CONF_VARIANT] != VARIANT_ESP32
|
||||
and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK])
|
||||
and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"{CONF_IGNORE_EFUSE_MAC_CRC} is not supported on {config[CONF_VARIANT]}"
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC],
|
||||
)
|
||||
)
|
||||
if (
|
||||
config.get(CONF_FRAMEWORK, {})
|
||||
.get(CONF_ADVANCED, {})
|
||||
.get(CONF_EXECUTE_FROM_PSRAM)
|
||||
):
|
||||
if config[CONF_VARIANT] != VARIANT_ESP32S3:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_EXECUTE_FROM_PSRAM}' is only supported on {VARIANT_ESP32S3} variant",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
|
||||
)
|
||||
)
|
||||
if PSRAM_DOMAIN not in full_config:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_EXECUTE_FROM_PSRAM}' requires PSRAM to be configured",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_EXECUTE_FROM_PSRAM],
|
||||
)
|
||||
)
|
||||
|
||||
if errs:
|
||||
raise cv.MultipleInvalid(errs)
|
||||
|
||||
return config
|
||||
|
||||
@@ -571,6 +599,8 @@ CONF_SDKCONFIG_OPTIONS = "sdkconfig_options"
|
||||
CONF_ENABLE_LWIP_DHCP_SERVER = "enable_lwip_dhcp_server"
|
||||
CONF_ENABLE_LWIP_MDNS_QUERIES = "enable_lwip_mdns_queries"
|
||||
CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
|
||||
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
|
||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
|
||||
|
||||
|
||||
def _validate_idf_component(config: ConfigType) -> ConfigType:
|
||||
@@ -619,6 +649,13 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
|
||||
cv.Optional(
|
||||
CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True
|
||||
): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
|
||||
@@ -643,6 +680,64 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
class _FrameworkMigrationWarning:
|
||||
shown = False
|
||||
|
||||
|
||||
def _show_framework_migration_message(name: str, variant: str) -> None:
|
||||
"""Show a friendly message about framework migration when defaulting to Arduino."""
|
||||
if _FrameworkMigrationWarning.shown:
|
||||
return
|
||||
_FrameworkMigrationWarning.shown = True
|
||||
|
||||
from esphome.log import AnsiFore, color
|
||||
|
||||
message = (
|
||||
color(
|
||||
AnsiFore.BOLD_CYAN,
|
||||
f"💡 IMPORTANT: {name} doesn't have a framework specified!",
|
||||
)
|
||||
+ "\n\n"
|
||||
+ f"Currently, {variant} defaults to the Arduino framework.\n"
|
||||
+ color(AnsiFore.YELLOW, "This will change to ESP-IDF in ESPHome 2026.1.0.\n")
|
||||
+ "\n"
|
||||
+ "Note: Newer ESP32 variants (C6, H2, P4, etc.) already use ESP-IDF by default.\n"
|
||||
+ "\n"
|
||||
+ "Why change? ESP-IDF offers:\n"
|
||||
+ color(AnsiFore.GREEN, " ✨ Up to 40% smaller binaries\n")
|
||||
+ color(AnsiFore.GREEN, " 🚀 Better performance and optimization\n")
|
||||
+ color(AnsiFore.GREEN, " 📦 Custom-built firmware for your exact needs\n")
|
||||
+ color(
|
||||
AnsiFore.GREEN,
|
||||
" 🔧 Active development and testing by ESPHome developers\n",
|
||||
)
|
||||
+ "\n"
|
||||
+ "Trade-offs:\n"
|
||||
+ color(AnsiFore.YELLOW, " ⏱️ Compile times are ~25% longer\n")
|
||||
+ color(AnsiFore.YELLOW, " 🔄 Some components need migration\n")
|
||||
+ "\n"
|
||||
+ "What should I do?\n"
|
||||
+ color(AnsiFore.CYAN, " Option 1")
|
||||
+ ": Migrate to ESP-IDF (recommended)\n"
|
||||
+ " Add this to your YAML under 'esp32:':\n"
|
||||
+ color(AnsiFore.WHITE, " framework:\n")
|
||||
+ color(AnsiFore.WHITE, " type: esp-idf\n")
|
||||
+ "\n"
|
||||
+ color(AnsiFore.CYAN, " Option 2")
|
||||
+ ": Keep using Arduino (still supported)\n"
|
||||
+ " Add this to your YAML under 'esp32:':\n"
|
||||
+ color(AnsiFore.WHITE, " framework:\n")
|
||||
+ color(AnsiFore.WHITE, " type: arduino\n")
|
||||
+ "\n"
|
||||
+ "Need help? Check out the migration guide:\n"
|
||||
+ color(
|
||||
AnsiFore.BLUE,
|
||||
"https://esphome.io/guides/esp32_arduino_to_idf.html",
|
||||
)
|
||||
)
|
||||
_LOGGER.warning(message)
|
||||
|
||||
|
||||
def _set_default_framework(config):
|
||||
if CONF_FRAMEWORK not in config:
|
||||
config = config.copy()
|
||||
@@ -651,6 +746,10 @@ def _set_default_framework(config):
|
||||
if variant in ARDUINO_ALLOWED_VARIANTS:
|
||||
config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({})
|
||||
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
|
||||
# Show the migration message
|
||||
_show_framework_migration_message(
|
||||
config.get(CONF_NAME, "This device"), variant
|
||||
)
|
||||
else:
|
||||
config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({})
|
||||
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
|
||||
@@ -784,6 +883,21 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
|
||||
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
|
||||
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||
|
||||
# Apply LWIP core locking for better socket performance
|
||||
# This is already enabled by default in Arduino framework, where it provides
|
||||
# significant performance benefits. Our benchmarks show socket operations are
|
||||
# 24-200% faster with core locking enabled:
|
||||
# - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default)
|
||||
# - Up to 200% slower under load when all operations queue through tcpip_thread
|
||||
# Enabling this makes ESP-IDF socket performance match Arduino framework.
|
||||
if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True)
|
||||
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
|
||||
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
|
||||
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
if CONF_PARTITIONS in config:
|
||||
@@ -872,7 +986,7 @@ def get_arduino_partition_csv(flash_size):
|
||||
eeprom_partition_start = app1_partition_start + app_partition_size
|
||||
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
|
||||
|
||||
partition_csv = f"""\
|
||||
return f"""\
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xE000, 0x2000,
|
||||
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
|
||||
@@ -880,20 +994,18 @@ app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X},
|
||||
eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X},
|
||||
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
|
||||
"""
|
||||
return partition_csv
|
||||
|
||||
|
||||
def get_idf_partition_csv(flash_size):
|
||||
app_partition_size = APP_PARTITION_SIZES[flash_size]
|
||||
|
||||
partition_csv = f"""\
|
||||
return f"""\
|
||||
otadata, data, ota, , 0x2000,
|
||||
phy_init, data, phy, , 0x1000,
|
||||
app0, app, ota_0, , 0x{app_partition_size:X},
|
||||
app1, app, ota_1, , 0x{app_partition_size:X},
|
||||
nvs, data, nvs, , 0x6D000,
|
||||
"""
|
||||
return partition_csv
|
||||
|
||||
|
||||
def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
|
||||
|
@@ -187,8 +187,7 @@ def validate_supports(value):
|
||||
"Open-drain only works with output mode", [CONF_MODE, CONF_OPEN_DRAIN]
|
||||
)
|
||||
|
||||
value = _esp32_validations[variant].usage_validation(value)
|
||||
return value
|
||||
return _esp32_validations[variant].usage_validation(value)
|
||||
|
||||
|
||||
# https://docs.espressif.com/projects/esp-idf/en/v3.3.5/api-reference/peripherals/gpio.html#_CPPv416gpio_drive_cap_t
|
||||
|
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
_ESP32H2_SPI_FLASH_PINS = {6, 7, 15, 16, 17, 18, 19, 20, 21}
|
||||
|
||||
@@ -15,13 +16,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def esp32_h2_validate_gpio_pin(value):
|
||||
if value < 0 or value > 27:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-27)")
|
||||
if value in _ESP32H2_STRAPPING_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is a Strapping PIN and should be avoided.\n"
|
||||
"Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
|
||||
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
|
||||
value,
|
||||
)
|
||||
if value in _ESP32H2_SPI_FLASH_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is reserved for SPI Flash communication on some ESP32-H2 chip variants.\n"
|
||||
@@ -49,4 +43,5 @@ def esp32_h2_validate_supports(value):
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_strapping_pin(value, _ESP32H2_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
@@ -2,6 +2,7 @@ import logging
|
||||
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
_ESP32P4_USB_JTAG_PINS = {24, 25}
|
||||
|
||||
@@ -13,13 +14,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def esp32_p4_validate_gpio_pin(value):
|
||||
if value < 0 or value > 54:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
|
||||
if value in _ESP32P4_STRAPPING_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is a Strapping PIN and should be avoided.\n"
|
||||
"Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
|
||||
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
|
||||
value,
|
||||
)
|
||||
if value in _ESP32P4_USB_JTAG_PINS:
|
||||
_LOGGER.warning(
|
||||
"GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
|
||||
@@ -40,4 +34,5 @@ def esp32_p4_validate_supports(value):
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
check_strapping_pin(value, _ESP32P4_STRAPPING_PINS, _LOGGER)
|
||||
return value
|
||||
|
@@ -1,10 +1,11 @@
|
||||
Import("env")
|
||||
Import("env") # noqa: F821
|
||||
|
||||
import itertools # noqa: E402
|
||||
import json # noqa: E402
|
||||
import os # noqa: E402
|
||||
import pathlib # noqa: E402
|
||||
import shutil # noqa: E402
|
||||
|
||||
import os
|
||||
import json
|
||||
import shutil
|
||||
import pathlib
|
||||
import itertools
|
||||
|
||||
def merge_factory_bin(source, target, env):
|
||||
"""
|
||||
@@ -25,7 +26,9 @@ def merge_factory_bin(source, target, env):
|
||||
try:
|
||||
with flasher_args_path.open() as f:
|
||||
flash_data = json.load(f)
|
||||
for addr, fname in sorted(flash_data["flash_files"].items(), key=lambda kv: int(kv[0], 16)):
|
||||
for addr, fname in sorted(
|
||||
flash_data["flash_files"].items(), key=lambda kv: int(kv[0], 16)
|
||||
):
|
||||
file_path = pathlib.Path(fname)
|
||||
if file_path.exists():
|
||||
sections.append((addr, str(file_path)))
|
||||
@@ -40,20 +43,27 @@ def merge_factory_bin(source, target, env):
|
||||
if flash_images:
|
||||
print("Using FLASH_EXTRA_IMAGES from PlatformIO environment")
|
||||
# flatten any nested lists
|
||||
flat = list(itertools.chain.from_iterable(
|
||||
x if isinstance(x, (list, tuple)) else [x] for x in flash_images
|
||||
))
|
||||
flat = list(
|
||||
itertools.chain.from_iterable(
|
||||
x if isinstance(x, (list, tuple)) else [x] for x in flash_images
|
||||
)
|
||||
)
|
||||
entries = [env.subst(x) for x in flat]
|
||||
for i in range(0, len(entries) - 1, 2):
|
||||
addr, fname = entries[i], entries[i + 1]
|
||||
if isinstance(fname, (list, tuple)):
|
||||
print(f"Warning: Skipping malformed FLASH_EXTRA_IMAGES entry: {fname}")
|
||||
print(
|
||||
f"Warning: Skipping malformed FLASH_EXTRA_IMAGES entry: {fname}"
|
||||
)
|
||||
continue
|
||||
file_path = pathlib.Path(str(fname))
|
||||
if file_path.exists():
|
||||
sections.append((addr, str(file_path)))
|
||||
sections.append((addr, file_path))
|
||||
else:
|
||||
print(f"Info: {file_path.name} not found — skipping")
|
||||
if sections:
|
||||
# Append main firmware to sections
|
||||
sections.append(("0x10000", firmware_path))
|
||||
|
||||
# 3. Final fallback: guess standard image locations
|
||||
if not sections:
|
||||
@@ -62,11 +72,11 @@ def merge_factory_bin(source, target, env):
|
||||
("0x0", build_dir / "bootloader" / "bootloader.bin"),
|
||||
("0x8000", build_dir / "partition_table" / "partition-table.bin"),
|
||||
("0xe000", build_dir / "ota_data_initial.bin"),
|
||||
("0x10000", firmware_path)
|
||||
("0x10000", firmware_path),
|
||||
]
|
||||
for addr, file_path in guesses:
|
||||
if file_path.exists():
|
||||
sections.append((addr, str(file_path)))
|
||||
sections.append((addr, file_path))
|
||||
else:
|
||||
print(f"Info: {file_path.name} not found — skipping")
|
||||
|
||||
@@ -76,27 +86,32 @@ def merge_factory_bin(source, target, env):
|
||||
return
|
||||
|
||||
output_path = firmware_path.with_suffix(".factory.bin")
|
||||
python_exe = f'"{env.subst("$PYTHONEXE")}"'
|
||||
cmd = [
|
||||
"--chip", chip,
|
||||
"merge_bin",
|
||||
"--flash_size", flash_size,
|
||||
"--output", str(output_path)
|
||||
python_exe,
|
||||
"-m",
|
||||
"esptool",
|
||||
"--chip",
|
||||
chip,
|
||||
"merge-bin",
|
||||
"--flash-size",
|
||||
flash_size,
|
||||
"--output",
|
||||
str(output_path),
|
||||
]
|
||||
for addr, file_path in sections:
|
||||
cmd += [addr, file_path]
|
||||
cmd += [addr, str(file_path)]
|
||||
|
||||
print(f"Merging binaries into {output_path}")
|
||||
result = env.Execute(
|
||||
env.VerboseAction(
|
||||
f"{env.subst('$PYTHONEXE')} -m esptool " + " ".join(cmd),
|
||||
"Merging binaries with esptool"
|
||||
)
|
||||
env.VerboseAction(" ".join(cmd), "Merging binaries with esptool")
|
||||
)
|
||||
|
||||
if result == 0:
|
||||
print(f"Successfully created {output_path}")
|
||||
else:
|
||||
print(f"Error: esptool merge_bin failed with code {result}")
|
||||
print(f"Error: esptool merge-bin failed with code {result}")
|
||||
|
||||
|
||||
def esp32_copy_ota_bin(source, target, env):
|
||||
"""
|
||||
@@ -107,6 +122,7 @@ def esp32_copy_ota_bin(source, target, env):
|
||||
shutil.copyfile(firmware_name, new_file_name)
|
||||
print(f"Copied firmware to {new_file_name}")
|
||||
|
||||
|
||||
# Run merge first, then ota copy second
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin)
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin)
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
|
||||
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821
|
||||
|
@@ -6,12 +6,13 @@ import esphome.codegen as cg
|
||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ENABLE_ON_BOOT, CONF_ESPHOME, CONF_ID, CONF_NAME
|
||||
from esphome.core import CORE
|
||||
from esphome.core import CORE, TimePeriod
|
||||
from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX
|
||||
import esphome.final_validate as fv
|
||||
|
||||
DEPENDENCIES = ["esp32"]
|
||||
CODEOWNERS = ["@jesserockz", "@Rapsssito"]
|
||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
|
||||
DOMAIN = "esp32_ble"
|
||||
|
||||
|
||||
class BTLoggers(Enum):
|
||||
@@ -115,8 +116,11 @@ def register_bt_logger(*loggers: BTLoggers) -> None:
|
||||
|
||||
CONF_BLE_ID = "ble_id"
|
||||
CONF_IO_CAPABILITY = "io_capability"
|
||||
CONF_ADVERTISING = "advertising"
|
||||
CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time"
|
||||
CONF_DISABLE_BT_LOGS = "disable_bt_logs"
|
||||
CONF_CONNECTION_TIMEOUT = "connection_timeout"
|
||||
CONF_MAX_NOTIFICATIONS = "max_notifications"
|
||||
|
||||
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
|
||||
|
||||
@@ -161,12 +165,23 @@ CONFIG_SCHEMA = cv.Schema(
|
||||
IO_CAPABILITY, lower=True
|
||||
),
|
||||
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_ADVERTISING, default=False): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_ADVERTISING_CYCLE_TIME, default="10s"
|
||||
): cv.positive_time_period_milliseconds,
|
||||
cv.SplitDefault(CONF_DISABLE_BT_LOGS, esp32_idf=True): cv.All(
|
||||
cv.only_with_esp_idf, cv.boolean
|
||||
),
|
||||
cv.SplitDefault(CONF_CONNECTION_TIMEOUT, esp32_idf="20s"): cv.All(
|
||||
cv.only_with_esp_idf,
|
||||
cv.positive_time_period_seconds,
|
||||
cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)),
|
||||
),
|
||||
cv.SplitDefault(CONF_MAX_NOTIFICATIONS, esp32_idf=12): cv.All(
|
||||
cv.only_with_esp_idf,
|
||||
cv.positive_int,
|
||||
cv.Range(min=1, max=64),
|
||||
),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
@@ -255,8 +270,31 @@ async def to_code(config):
|
||||
if logger not in _required_loggers:
|
||||
add_idf_sdkconfig_option(f"{logger.value}_NONE", True)
|
||||
|
||||
# Set BLE connection establishment timeout to match aioesphomeapi/bleak-retry-connector
|
||||
# Default is 20 seconds instead of ESP-IDF's 30 seconds. Because there is no way to
|
||||
# cancel a BLE connection in progress, when aioesphomeapi times out at 20 seconds,
|
||||
# the connection slot remains occupied for the remaining time, preventing new connection
|
||||
# attempts and wasting valuable connection slots.
|
||||
if CONF_CONNECTION_TIMEOUT in config:
|
||||
timeout_seconds = int(config[CONF_CONNECTION_TIMEOUT].total_seconds)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
|
||||
)
|
||||
|
||||
# Set the maximum number of notification registrations
|
||||
# This controls how many BLE characteristics can have notifications enabled
|
||||
# across all connections for a single GATT client interface
|
||||
# https://github.com/esphome/issues/issues/6808
|
||||
if CONF_MAX_NOTIFICATIONS in config:
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_BT_GATTC_NOTIF_REG_MAX", config[CONF_MAX_NOTIFICATIONS]
|
||||
)
|
||||
|
||||
cg.add_define("USE_ESP32_BLE")
|
||||
|
||||
if config[CONF_ADVERTISING]:
|
||||
cg.add_define("USE_ESP32_BLE_ADVERTISING")
|
||||
|
||||
|
||||
@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({}))
|
||||
async def ble_enabled_to_code(config, condition_id, template_arg, args):
|
||||
|
@@ -1,7 +1,7 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "ble.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -53,6 +53,7 @@ void ESP32BLE::disable() {
|
||||
|
||||
bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void ESP32BLE::advertising_start() {
|
||||
this->advertising_init_();
|
||||
if (!this->is_active())
|
||||
@@ -88,6 +89,7 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
|
||||
this->advertising_->remove_service_uuid(uuid);
|
||||
this->advertising_start();
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ESP32BLE::ble_pre_setup_() {
|
||||
esp_err_t err = nvs_flash_init();
|
||||
@@ -98,6 +100,7 @@ bool ESP32BLE::ble_pre_setup_() {
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void ESP32BLE::advertising_init_() {
|
||||
if (this->advertising_ != nullptr)
|
||||
return;
|
||||
@@ -107,6 +110,7 @@ void ESP32BLE::advertising_init_() {
|
||||
this->advertising_->set_min_preferred_interval(0x06);
|
||||
this->advertising_->set_appearance(this->appearance_);
|
||||
}
|
||||
#endif
|
||||
|
||||
bool ESP32BLE::ble_setup_() {
|
||||
esp_err_t err;
|
||||
@@ -394,9 +398,11 @@ void ESP32BLE::loop() {
|
||||
this->ble_event_pool_.release(ble_event);
|
||||
ble_event = this->ble_events_.pop();
|
||||
}
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
if (this->advertising_ != nullptr) {
|
||||
this->advertising_->loop();
|
||||
}
|
||||
#endif
|
||||
|
||||
// Log dropped events periodically
|
||||
uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
|
||||
@@ -468,6 +474,8 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
|
||||
// Ignore these GAP events as they are not relevant for our use case
|
||||
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
|
||||
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
|
||||
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete
|
||||
case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm
|
||||
return;
|
||||
|
||||
default:
|
||||
|
@@ -1,14 +1,17 @@
|
||||
#pragma once
|
||||
|
||||
#include "ble_advertising.h"
|
||||
#include "esphome/core/defines.h" // Must be included before conditional includes
|
||||
|
||||
#include "ble_uuid.h"
|
||||
#include "ble_scan_result.h"
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
#include "ble_advertising.h"
|
||||
#endif
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include "ble_event.h"
|
||||
@@ -23,21 +26,14 @@
|
||||
|
||||
namespace esphome::esp32_ble {
|
||||
|
||||
// Maximum number of BLE scan results to buffer
|
||||
// Sized to handle bursts of advertisements while allowing for processing delays
|
||||
// With 16 advertisements per batch and some safety margin:
|
||||
// - Without PSRAM: 24 entries (1.5× batch size)
|
||||
// - With PSRAM: 36 entries (2.25× batch size)
|
||||
// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers
|
||||
// Maximum size of the BLE event queue
|
||||
// Increased to absorb the ring buffer capacity from esp32_ble_tracker
|
||||
#ifdef USE_PSRAM
|
||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36;
|
||||
static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100; // 64 + 36 (ring buffer size with PSRAM)
|
||||
#else
|
||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24;
|
||||
static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88; // 64 + 24 (ring buffer size without PSRAM)
|
||||
#endif
|
||||
|
||||
// Maximum size of the BLE event queue - must be power of 2 for lock-free queue
|
||||
static constexpr size_t MAX_BLE_QUEUE_SIZE = 64;
|
||||
|
||||
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
|
||||
|
||||
// NOLINTNEXTLINE(modernize-use-using)
|
||||
@@ -113,6 +109,7 @@ class ESP32BLE : public Component {
|
||||
float get_setup_priority() const override;
|
||||
void set_name(const std::string &name) { this->name_ = name; }
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void advertising_start();
|
||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
@@ -120,6 +117,7 @@ class ESP32BLE : public Component {
|
||||
void advertising_add_service_uuid(ESPBTUUID uuid);
|
||||
void advertising_remove_service_uuid(ESPBTUUID uuid);
|
||||
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
|
||||
#endif
|
||||
|
||||
void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
|
||||
void register_gap_scan_event_handler(GAPScanEventHandler *handler) {
|
||||
@@ -140,7 +138,9 @@ class ESP32BLE : public Component {
|
||||
bool ble_setup_();
|
||||
bool ble_dismantle_();
|
||||
bool ble_pre_setup_();
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void advertising_init_();
|
||||
#endif
|
||||
|
||||
private:
|
||||
template<typename... Args> friend void enqueue_ble_event(Args... args);
|
||||
@@ -160,7 +160,9 @@ class ESP32BLE : public Component {
|
||||
optional<std::string> name_;
|
||||
|
||||
// 4-byte aligned members
|
||||
BLEAdvertising *advertising_{}; // 4 bytes (pointer)
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
BLEAdvertising *advertising_{}; // 4 bytes (pointer)
|
||||
#endif
|
||||
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; // 4 bytes (enum)
|
||||
uint32_t advertising_cycle_time_{}; // 4 bytes
|
||||
|
||||
|
@@ -1,6 +1,7 @@
|
||||
#include "ble_advertising.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -161,4 +162,5 @@ void BLEAdvertising::register_raw_advertisement_callback(std::function<void(bool
|
||||
|
||||
} // namespace esphome::esp32_ble
|
||||
|
||||
#endif
|
||||
#endif // USE_ESP32_BLE_ADVERTISING
|
||||
#endif // USE_ESP32
|
||||
|
@@ -1,10 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#include <array>
|
||||
#include <functional>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
|
||||
#include <esp_bt.h>
|
||||
#include <esp_gap_ble_api.h>
|
||||
@@ -56,4 +59,5 @@ class BLEAdvertising {
|
||||
|
||||
} // namespace esphome::esp32_ble
|
||||
|
||||
#endif
|
||||
#endif // USE_ESP32_BLE_ADVERTISING
|
||||
#endif // USE_ESP32
|
||||
|
@@ -82,6 +82,8 @@ async def to_code(config):
|
||||
cg.add(var.set_measured_power(config[CONF_MEASURED_POWER]))
|
||||
cg.add(var.set_tx_power(config[CONF_TX_POWER]))
|
||||
|
||||
cg.add_define("USE_ESP32_BLE_ADVERTISING")
|
||||
|
||||
if CORE.using_esp_idf:
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
|
||||
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
|
||||
|
@@ -2,7 +2,7 @@ import esphome.codegen as cg
|
||||
from esphome.components import esp32_ble_tracker
|
||||
|
||||
AUTO_LOAD = ["esp32_ble_tracker"]
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
CODEOWNERS = ["@jesserockz", "@bdraco"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
|
||||
esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client")
|
||||
|
@@ -5,9 +5,9 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_client {
|
||||
namespace esphome::esp32_ble_client {
|
||||
|
||||
static const char *const TAG = "esp32_ble_client";
|
||||
|
||||
@@ -93,7 +93,7 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size)
|
||||
return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP);
|
||||
}
|
||||
|
||||
} // namespace esp32_ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::esp32_ble_client
|
||||
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
#endif // USE_ESP32
|
||||
|
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
|
||||
@@ -8,8 +11,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_client {
|
||||
namespace esphome::esp32_ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -33,7 +35,7 @@ class BLECharacteristic {
|
||||
BLEService *service;
|
||||
};
|
||||
|
||||
} // namespace esp32_ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::esp32_ble_client
|
||||
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
#endif // USE_ESP32
|
||||
|
@@ -5,10 +5,27 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_client {
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gatt_defs.h>
|
||||
|
||||
namespace esphome::esp32_ble_client {
|
||||
|
||||
static const char *const TAG = "esp32_ble_client";
|
||||
|
||||
// Intermediate connection parameters for standard operation
|
||||
// ESP-IDF defaults (12.5-15ms) are too slow for stable connections through WiFi-based BLE proxies,
|
||||
// causing disconnections. These medium parameters balance responsiveness with bandwidth usage.
|
||||
static const uint16_t MEDIUM_MIN_CONN_INTERVAL = 0x07; // 7 * 1.25ms = 8.75ms
|
||||
static const uint16_t MEDIUM_MAX_CONN_INTERVAL = 0x09; // 9 * 1.25ms = 11.25ms
|
||||
// The timeout value was increased from 6s to 8s to address stability issues observed
|
||||
// in certain BLE devices when operating through WiFi-based BLE proxies. The longer
|
||||
// timeout reduces the likelihood of disconnections during periods of high latency.
|
||||
static const uint16_t MEDIUM_CONN_TIMEOUT = 800; // 800 * 10ms = 8s
|
||||
|
||||
// Fastest connection parameters for devices with short discovery timeouts
|
||||
static const uint16_t FAST_MIN_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms (BLE minimum)
|
||||
static const uint16_t FAST_MAX_CONN_INTERVAL = 0x06; // 6 * 1.25ms = 7.5ms
|
||||
static const uint16_t FAST_CONN_TIMEOUT = 1000; // 1000 * 10ms = 10s
|
||||
static const esp_bt_uuid_t NOTIFY_DESC_UUID = {
|
||||
.len = ESP_UUID_LEN_16,
|
||||
.uuid =
|
||||
@@ -27,8 +44,10 @@ void BLEClientBase::set_state(espbt::ClientState st) {
|
||||
ESPBTClient::set_state(st);
|
||||
|
||||
if (st == espbt::ClientState::READY_TO_CONNECT) {
|
||||
// Enable loop when we need to connect
|
||||
// Enable loop for state processing
|
||||
this->enable_loop();
|
||||
// Connect immediately instead of waiting for next loop
|
||||
this->connect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,11 +64,6 @@ void BLEClientBase::loop() {
|
||||
}
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
}
|
||||
// READY_TO_CONNECT means we have discovered the device
|
||||
// and the scanner has been stopped by the tracker.
|
||||
else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) {
|
||||
this->connect();
|
||||
}
|
||||
// If its idle, we can disable the loop as set_state
|
||||
// will enable it again when we need to connect.
|
||||
else if (this->state_ == espbt::ClientState::IDLE) {
|
||||
@@ -64,40 +78,7 @@ void BLEClientBase::dump_config() {
|
||||
" Address: %s\n"
|
||||
" Auto-Connect: %s",
|
||||
this->address_str().c_str(), TRUEFALSE(this->auto_connect_));
|
||||
std::string state_name;
|
||||
switch (this->state()) {
|
||||
case espbt::ClientState::INIT:
|
||||
state_name = "INIT";
|
||||
break;
|
||||
case espbt::ClientState::DISCONNECTING:
|
||||
state_name = "DISCONNECTING";
|
||||
break;
|
||||
case espbt::ClientState::IDLE:
|
||||
state_name = "IDLE";
|
||||
break;
|
||||
case espbt::ClientState::SEARCHING:
|
||||
state_name = "SEARCHING";
|
||||
break;
|
||||
case espbt::ClientState::DISCOVERED:
|
||||
state_name = "DISCOVERED";
|
||||
break;
|
||||
case espbt::ClientState::READY_TO_CONNECT:
|
||||
state_name = "READY_TO_CONNECT";
|
||||
break;
|
||||
case espbt::ClientState::CONNECTING:
|
||||
state_name = "CONNECTING";
|
||||
break;
|
||||
case espbt::ClientState::CONNECTED:
|
||||
state_name = "CONNECTED";
|
||||
break;
|
||||
case espbt::ClientState::ESTABLISHED:
|
||||
state_name = "ESTABLISHED";
|
||||
break;
|
||||
default:
|
||||
state_name = "UNKNOWN_STATE";
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " State: %s", state_name.c_str());
|
||||
ESP_LOGCONFIG(TAG, " State: %s", espbt::client_state_to_string(this->state()));
|
||||
if (this->status_ == ESP_GATT_NO_RESOURCES) {
|
||||
ESP_LOGE(TAG, " Failed due to no resources. Try to reduce number of BLE clients in config.");
|
||||
} else if (this->status_ != ESP_GATT_OK) {
|
||||
@@ -126,13 +107,43 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
||||
#endif
|
||||
|
||||
void BLEClientBase::connect() {
|
||||
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(),
|
||||
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(),
|
||||
this->remote_addr_type_);
|
||||
this->paired_ = false;
|
||||
|
||||
// Set preferred connection parameters before connecting
|
||||
// Use FAST for all V3 connections (better latency and reliability)
|
||||
// Use MEDIUM for V1/legacy connections (balanced performance)
|
||||
uint16_t min_interval, max_interval, timeout;
|
||||
const char *param_type;
|
||||
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
min_interval = FAST_MIN_CONN_INTERVAL;
|
||||
max_interval = FAST_MAX_CONN_INTERVAL;
|
||||
timeout = FAST_CONN_TIMEOUT;
|
||||
param_type = "fast";
|
||||
} else {
|
||||
min_interval = MEDIUM_MIN_CONN_INTERVAL;
|
||||
max_interval = MEDIUM_MAX_CONN_INTERVAL;
|
||||
timeout = MEDIUM_CONN_TIMEOUT;
|
||||
param_type = "medium";
|
||||
}
|
||||
|
||||
auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
|
||||
0, // latency: 0
|
||||
timeout);
|
||||
if (param_ret != ESP_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
|
||||
this->address_str_.c_str(), param_ret);
|
||||
} else {
|
||||
this->log_connection_params_(param_type);
|
||||
}
|
||||
|
||||
// Now open the connection
|
||||
auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(),
|
||||
ret);
|
||||
this->log_gattc_warning_("esp_ble_gattc_open", ret);
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
} else {
|
||||
this->set_state(espbt::ClientState::CONNECTING);
|
||||
@@ -142,14 +153,9 @@ void BLEClientBase::connect() {
|
||||
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
|
||||
|
||||
void BLEClientBase::disconnect() {
|
||||
if (this->state_ == espbt::ClientState::IDLE) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already idle.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
return;
|
||||
}
|
||||
if (this->state_ == espbt::ClientState::DISCONNECTING) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already disconnecting.", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_.c_str(),
|
||||
espbt::client_state_to_string(this->state_));
|
||||
return;
|
||||
}
|
||||
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
|
||||
@@ -184,8 +190,7 @@ void BLEClientBase::unconditional_disconnect() {
|
||||
// In the future we might consider App.reboot() here since
|
||||
// the BLE stack is in an indeterminate state.
|
||||
//
|
||||
ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(),
|
||||
err);
|
||||
this->log_gattc_warning_("esp_ble_gattc_close", err);
|
||||
}
|
||||
|
||||
if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT ||
|
||||
@@ -198,9 +203,11 @@ void BLEClientBase::unconditional_disconnect() {
|
||||
}
|
||||
|
||||
void BLEClientBase::release_services() {
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
for (auto &svc : this->services_)
|
||||
delete svc; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
this->services_.clear();
|
||||
#endif
|
||||
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
|
||||
esp_ble_gattc_cache_clean(this->remote_bda_);
|
||||
#endif
|
||||
@@ -210,6 +217,36 @@ void BLEClientBase::log_event_(const char *name) {
|
||||
ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
|
||||
}
|
||||
|
||||
void BLEClientBase::log_gattc_event_(const char *name) {
|
||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_.c_str(), name);
|
||||
}
|
||||
|
||||
void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation,
|
||||
status);
|
||||
}
|
||||
|
||||
void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, err);
|
||||
}
|
||||
|
||||
void BLEClientBase::log_connection_params_(const char *param_type) {
|
||||
ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
|
||||
}
|
||||
|
||||
void BLEClientBase::restore_medium_conn_params_() {
|
||||
// Restore to medium connection parameters after initial connection phase
|
||||
// This balances performance with bandwidth usage for normal operation
|
||||
esp_ble_conn_update_params_t conn_params = {{0}};
|
||||
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
|
||||
conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
|
||||
conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
|
||||
conn_params.latency = 0;
|
||||
conn_params.timeout = MEDIUM_CONN_TIMEOUT;
|
||||
this->log_connection_params_("medium");
|
||||
esp_ble_gap_update_conn_params(&conn_params);
|
||||
}
|
||||
|
||||
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
|
||||
esp_ble_gattc_cb_param_t *param) {
|
||||
if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
|
||||
@@ -237,30 +274,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
case ESP_GATTC_OPEN_EVT: {
|
||||
if (!this->check_addr(param->open.remote_bda))
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_OPEN_EVT");
|
||||
this->conn_id_ = param->open.conn_id;
|
||||
this->log_gattc_event_("OPEN");
|
||||
// conn_id was already set in ESP_GATTC_CONNECT_EVT
|
||||
this->service_count_ = 0;
|
||||
if (this->state_ != espbt::ClientState::CONNECTING) {
|
||||
// This should not happen but lets log it in case it does
|
||||
// because it means we have a bad assumption about how the
|
||||
// ESP BT stack works.
|
||||
if (this->state_ == espbt::ClientState::CONNECTED) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already connected, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->open.status);
|
||||
} else if (this->state_ == espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already established, status=%d",
|
||||
this->connection_index_, this->address_str_.c_str(), param->open.status);
|
||||
} else if (this->state_ == espbt::ClientState::DISCONNECTING) {
|
||||
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while disconnecting, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->open.status);
|
||||
} else {
|
||||
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while not in connecting state, status=%d",
|
||||
this->connection_index_, this->address_str_.c_str(), param->open.status);
|
||||
}
|
||||
ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while in %s state, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status);
|
||||
}
|
||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(),
|
||||
param->open.status);
|
||||
this->log_gattc_warning_("Connection open", param->open.status);
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
break;
|
||||
}
|
||||
@@ -272,32 +297,47 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
this->conn_id_ = UNSET_CONN_ID;
|
||||
break;
|
||||
}
|
||||
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id);
|
||||
if (ret) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
|
||||
this->address_str_.c_str(), ret);
|
||||
}
|
||||
// MTU negotiation already started in ESP_GATTC_CONNECT_EVT
|
||||
this->set_state(espbt::ClientState::CONNECTED);
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
|
||||
// Restore to medium connection parameters for cached connections too
|
||||
this->restore_medium_conn_params_();
|
||||
// only set our state, subclients might have more stuff to do yet.
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%d] [%s] Searching for services", this->connection_index_, this->address_str_.c_str());
|
||||
esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_CONNECT_EVT: {
|
||||
if (!this->check_addr(param->connect.remote_bda))
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_CONNECT_EVT");
|
||||
this->log_gattc_event_("CONNECT");
|
||||
this->conn_id_ = param->connect.conn_id;
|
||||
// Start MTU negotiation immediately as recommended by ESP-IDF examples
|
||||
// (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
|
||||
// ESP_GATTC_CONNECT_EVT instead of waiting for ESP_GATTC_OPEN_EVT.
|
||||
// This saves ~3ms in the connection process.
|
||||
auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
|
||||
if (ret) {
|
||||
this->log_gattc_warning_("esp_ble_gattc_send_mtu_req", ret);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_DISCONNECT_EVT: {
|
||||
if (!this->check_addr(param->disconnect.remote_bda))
|
||||
return false;
|
||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_,
|
||||
this->address_str_.c_str(), param->disconnect.reason);
|
||||
// Check if we were disconnected while waiting for service discovery
|
||||
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
|
||||
this->state_ == espbt::ClientState::CONNECTED) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Disconnected by remote during service discovery", this->connection_index_,
|
||||
this->address_str_.c_str());
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_,
|
||||
this->address_str_.c_str(), param->disconnect.reason);
|
||||
}
|
||||
this->release_services();
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
break;
|
||||
@@ -320,7 +360,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
case ESP_GATTC_CLOSE_EVT: {
|
||||
if (this->conn_id_ != param->close.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_CLOSE_EVT");
|
||||
this->log_gattc_event_("CLOSE");
|
||||
this->release_services();
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
this->conn_id_ = UNSET_CONN_ID;
|
||||
@@ -332,63 +372,74 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
this->service_count_++;
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
// V3 clients don't need services initialized since
|
||||
// they only request by handle after receiving the services.
|
||||
// as they use the ESP APIs to get services.
|
||||
break;
|
||||
}
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
BLEService *ble_service = new BLEService(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid);
|
||||
ble_service->start_handle = param->search_res.start_handle;
|
||||
ble_service->end_handle = param->search_res.end_handle;
|
||||
ble_service->client = this;
|
||||
this->services_.push_back(ble_service);
|
||||
#endif
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_SEARCH_CMPL_EVT: {
|
||||
if (this->conn_id_ != param->search_cmpl.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_SEARCH_CMPL_EVT");
|
||||
for (auto &svc : this->services_) {
|
||||
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
|
||||
svc->uuid.to_string().c_str());
|
||||
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_,
|
||||
this->address_str_.c_str(), svc->start_handle, svc->end_handle);
|
||||
this->log_gattc_event_("SEARCH_CMPL");
|
||||
// For V3 connections, restore to medium connection parameters after service discovery
|
||||
// This balances performance with bandwidth usage after the critical discovery phase
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
this->restore_medium_conn_params_();
|
||||
} else {
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
for (auto &svc : this->services_) {
|
||||
ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
|
||||
svc->uuid.to_string().c_str());
|
||||
ESP_LOGV(TAG, "[%d] [%s] start_handle: 0x%x end_handle: 0x%x", this->connection_index_,
|
||||
this->address_str_.c_str(), svc->start_handle, svc->end_handle);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
|
||||
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str());
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_READ_DESCR_EVT: {
|
||||
if (this->conn_id_ != param->write.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_READ_DESCR_EVT");
|
||||
this->log_gattc_event_("READ_DESCR");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_DESCR_EVT: {
|
||||
if (this->conn_id_ != param->write.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_WRITE_DESCR_EVT");
|
||||
this->log_gattc_event_("WRITE_DESCR");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_WRITE_CHAR_EVT: {
|
||||
if (this->conn_id_ != param->write.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_WRITE_CHAR_EVT");
|
||||
this->log_gattc_event_("WRITE_CHAR");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_READ_CHAR_EVT: {
|
||||
if (this->conn_id_ != param->read.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_READ_CHAR_EVT");
|
||||
this->log_gattc_event_("READ_CHAR");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (this->conn_id_ != param->notify.conn_id)
|
||||
return false;
|
||||
this->log_event_("ESP_GATTC_NOTIFY_EVT");
|
||||
this->log_gattc_event_("NOTIFY");
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
|
||||
this->log_event_("ESP_GATTC_REG_FOR_NOTIFY_EVT");
|
||||
this->log_gattc_event_("REG_FOR_NOTIFY");
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
|
||||
// Client is responsible for flipping the descriptor value
|
||||
@@ -400,8 +451,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle(
|
||||
this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count);
|
||||
if (descr_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), descr_status);
|
||||
this->log_gattc_warning_("esp_ble_gattc_get_descr_by_char_handle", descr_status);
|
||||
break;
|
||||
}
|
||||
esp_gattc_char_elem_t char_result;
|
||||
@@ -409,8 +459,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle,
|
||||
param->reg_for_notify.handle, &char_result, &count, 0);
|
||||
if (char_status != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), char_status);
|
||||
this->log_gattc_warning_("esp_ble_gattc_get_all_char", char_status);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -424,8 +473,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
(uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_,
|
||||
this->address_str_.c_str(), status);
|
||||
this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -533,6 +581,7 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) {
|
||||
return NAN;
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
BLEService *BLEClientBase::get_service(espbt::ESPBTUUID uuid) {
|
||||
for (auto *svc : this->services_) {
|
||||
if (svc->uuid == uuid)
|
||||
@@ -609,8 +658,8 @@ BLEDescriptor *BLEClientBase::get_descriptor(uint16_t handle) {
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
|
||||
} // namespace esp32_ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::esp32_ble_client
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
@@ -5,7 +5,9 @@
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
#include "ble_service.h"
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
#include <string>
|
||||
@@ -16,8 +18,7 @@
|
||||
#include <esp_gatt_common_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_client {
|
||||
namespace esphome::esp32_ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -48,7 +49,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
|
||||
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
|
||||
|
||||
void set_address(uint64_t address) {
|
||||
virtual void set_address(uint64_t address) {
|
||||
this->address_ = address;
|
||||
this->remote_bda_[0] = (address >> 40) & 0xFF;
|
||||
this->remote_bda_[1] = (address >> 32) & 0xFF;
|
||||
@@ -66,8 +67,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
(uint8_t) (this->address_ >> 0) & 0xff);
|
||||
}
|
||||
}
|
||||
std::string address_str() const { return this->address_str_; }
|
||||
const std::string &address_str() const { return this->address_str_; }
|
||||
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
BLEService *get_service(espbt::ESPBTUUID uuid);
|
||||
BLEService *get_service(uint16_t uuid);
|
||||
BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr);
|
||||
@@ -78,6 +80,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
BLEDescriptor *get_descriptor(uint16_t handle);
|
||||
// Get the configuration descriptor for the given characteristic handle.
|
||||
BLEDescriptor *get_config_descriptor(uint16_t handle);
|
||||
#endif
|
||||
|
||||
float parse_char_value(uint8_t *value, uint16_t length);
|
||||
|
||||
@@ -104,7 +107,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
|
||||
// Group 2: Container types (grouped for memory optimization)
|
||||
std::string address_str_{};
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
std::vector<BLEService *> services_;
|
||||
#endif
|
||||
|
||||
// Group 3: 4-byte types
|
||||
int gattc_if_;
|
||||
@@ -127,9 +132,13 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
// 6 bytes used, 2 bytes padding
|
||||
|
||||
void log_event_(const char *name);
|
||||
void log_gattc_event_(const char *name);
|
||||
void restore_medium_conn_params_();
|
||||
void log_gattc_warning_(const char *operation, esp_gatt_status_t status);
|
||||
void log_gattc_warning_(const char *operation, esp_err_t err);
|
||||
void log_connection_params_(const char *param_type);
|
||||
};
|
||||
|
||||
} // namespace esp32_ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::esp32_ble_client
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
@@ -1,11 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_client {
|
||||
namespace esphome::esp32_ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -19,7 +21,7 @@ class BLEDescriptor {
|
||||
BLECharacteristic *characteristic;
|
||||
};
|
||||
|
||||
} // namespace esp32_ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::esp32_ble_client
|
||||
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
#endif // USE_ESP32
|
||||
|
@@ -4,9 +4,9 @@
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_client {
|
||||
namespace esphome::esp32_ble_client {
|
||||
|
||||
static const char *const TAG = "esp32_ble_client";
|
||||
|
||||
@@ -71,7 +71,7 @@ void BLEService::parse_characteristics() {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esp32_ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::esp32_ble_client
|
||||
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
#endif // USE_ESP32
|
||||
|
@@ -1,6 +1,9 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
|
||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
|
||||
|
||||
@@ -8,8 +11,7 @@
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_client {
|
||||
namespace esphome::esp32_ble_client {
|
||||
|
||||
namespace espbt = esphome::esp32_ble_tracker;
|
||||
|
||||
@@ -30,7 +32,7 @@ class BLEService {
|
||||
BLECharacteristic *get_characteristic(uint16_t uuid);
|
||||
};
|
||||
|
||||
} // namespace esp32_ble_client
|
||||
} // namespace esphome
|
||||
} // namespace esphome::esp32_ble_client
|
||||
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
#endif // USE_ESP32
|
||||
|
@@ -571,6 +571,7 @@ async def to_code(config):
|
||||
config[CONF_ON_DISCONNECT],
|
||||
)
|
||||
cg.add_define("USE_ESP32_BLE_SERVER")
|
||||
cg.add_define("USE_ESP32_BLE_ADVERTISING")
|
||||
if CORE.using_esp_idf:
|
||||
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
|
||||
|
||||
@@ -628,5 +629,4 @@ async def ble_server_descriptor_set_value(config, action_id, template_arg, args)
|
||||
)
|
||||
async def ble_server_characteristic_notify(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
return var
|
||||
return cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
@@ -36,6 +36,7 @@ from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["esp32_ble"]
|
||||
DEPENDENCIES = ["esp32"]
|
||||
CODEOWNERS = ["@bdraco"]
|
||||
|
||||
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
|
||||
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
|
||||
@@ -354,11 +355,6 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
|
||||
)
|
||||
# CONFIG_BT_GATTC_NOTIF_REG_MAX controls the number of
|
||||
# max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS
|
||||
# is enough in 4.x
|
||||
# https://github.com/esphome/issues/issues/6808
|
||||
add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9)
|
||||
|
||||
cg.add_define("USE_OTA_STATE_CALLBACK") # To be notified when an OTA update starts
|
||||
cg.add_define("USE_ESP32_BLE_CLIENT")
|
||||
|
@@ -41,6 +41,31 @@ static const char *const TAG = "esp32_ble_tracker";
|
||||
|
||||
ESP32BLETracker *global_esp32_ble_tracker = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
const char *client_state_to_string(ClientState state) {
|
||||
switch (state) {
|
||||
case ClientState::INIT:
|
||||
return "INIT";
|
||||
case ClientState::DISCONNECTING:
|
||||
return "DISCONNECTING";
|
||||
case ClientState::IDLE:
|
||||
return "IDLE";
|
||||
case ClientState::SEARCHING:
|
||||
return "SEARCHING";
|
||||
case ClientState::DISCOVERED:
|
||||
return "DISCOVERED";
|
||||
case ClientState::READY_TO_CONNECT:
|
||||
return "READY_TO_CONNECT";
|
||||
case ClientState::CONNECTING:
|
||||
return "CONNECTING";
|
||||
case ClientState::CONNECTED:
|
||||
return "CONNECTED";
|
||||
case ClientState::ESTABLISHED:
|
||||
return "ESTABLISHED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
|
||||
|
||||
void ESP32BLETracker::setup() {
|
||||
@@ -49,13 +74,6 @@ void ESP32BLETracker::setup() {
|
||||
ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
|
||||
return;
|
||||
}
|
||||
RAMAllocator<BLEScanResult> allocator;
|
||||
this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
|
||||
|
||||
if (this->scan_ring_buffer_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
|
||||
this->mark_failed();
|
||||
}
|
||||
|
||||
global_esp32_ble_tracker = this;
|
||||
|
||||
@@ -83,127 +101,49 @@ void ESP32BLETracker::loop() {
|
||||
this->start_scan();
|
||||
}
|
||||
}
|
||||
int connecting = 0;
|
||||
int discovered = 0;
|
||||
int searching = 0;
|
||||
int disconnecting = 0;
|
||||
for (auto *client : this->clients_) {
|
||||
switch (client->state()) {
|
||||
case ClientState::DISCONNECTING:
|
||||
disconnecting++;
|
||||
break;
|
||||
case ClientState::DISCOVERED:
|
||||
discovered++;
|
||||
break;
|
||||
case ClientState::SEARCHING:
|
||||
searching++;
|
||||
break;
|
||||
case ClientState::CONNECTING:
|
||||
case ClientState::READY_TO_CONNECT:
|
||||
connecting++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (connecting != connecting_ || discovered != discovered_ || searching != searching_ ||
|
||||
disconnecting != disconnecting_) {
|
||||
connecting_ = connecting;
|
||||
discovered_ = discovered;
|
||||
searching_ = searching;
|
||||
disconnecting_ = disconnecting;
|
||||
ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
|
||||
searching_, disconnecting_);
|
||||
}
|
||||
bool promote_to_connecting = discovered && !searching && !connecting;
|
||||
|
||||
// Process scan results from lock-free SPSC ring buffer
|
||||
// Consumer side: This runs in the main loop thread
|
||||
// Check for scan timeout - moved here from scheduler to avoid false reboots
|
||||
// when the loop is blocked
|
||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
// Load our own index with relaxed ordering (we're the only writer)
|
||||
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
|
||||
|
||||
// Load producer's index with acquire to see their latest writes
|
||||
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
|
||||
|
||||
while (read_idx != write_idx) {
|
||||
// Calculate how many contiguous results we can process in one batch
|
||||
// If write > read: process all results from read to write
|
||||
// If write <= read (wraparound): process from read to end of buffer first
|
||||
size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx);
|
||||
|
||||
// Process the batch for raw advertisements
|
||||
if (this->raw_advertisements_) {
|
||||
for (auto *listener : this->listeners_) {
|
||||
listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
|
||||
}
|
||||
for (auto *client : this->clients_) {
|
||||
client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
|
||||
switch (this->scan_timeout_state_) {
|
||||
case ScanTimeoutState::MONITORING: {
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
uint32_t timeout_ms = this->scan_duration_ * 2000;
|
||||
// Robust time comparison that handles rollover correctly
|
||||
// This works because unsigned arithmetic wraps around predictably
|
||||
if ((now - this->scan_start_time_) > timeout_ms) {
|
||||
// First time we've seen the timeout exceeded - wait one more loop iteration
|
||||
// This ensures all components have had a chance to process pending events
|
||||
// This is because esp32_ble may not have run yet and called
|
||||
// gap_scan_event_handler yet when the loop unblocks
|
||||
ESP_LOGW(TAG, "Scan timeout exceeded");
|
||||
this->scan_timeout_state_ = ScanTimeoutState::EXCEEDED_WAIT;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ScanTimeoutState::EXCEEDED_WAIT:
|
||||
// We've waited at least one full loop iteration, and scan is still running
|
||||
ESP_LOGE(TAG, "Scan never terminated, rebooting");
|
||||
App.reboot();
|
||||
break;
|
||||
|
||||
// Process individual results for parsed advertisements
|
||||
if (this->parse_advertisements_) {
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
for (size_t i = 0; i < batch_size; i++) {
|
||||
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i];
|
||||
ESPBTDevice device;
|
||||
device.parse_scan_rst(scan_result);
|
||||
|
||||
bool found = false;
|
||||
for (auto *listener : this->listeners_) {
|
||||
if (listener->parse_device(device))
|
||||
found = true;
|
||||
}
|
||||
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->parse_device(device)) {
|
||||
found = true;
|
||||
if (!connecting && client->state() == ClientState::DISCOVERED) {
|
||||
promote_to_connecting = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found && !this->scan_continuous_) {
|
||||
this->print_bt_device_info(device);
|
||||
}
|
||||
}
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
}
|
||||
|
||||
// Update read index for entire batch
|
||||
read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE;
|
||||
|
||||
// Store with release to ensure reads complete before index update
|
||||
this->ring_read_index_.store(read_idx, std::memory_order_release);
|
||||
}
|
||||
|
||||
// Log dropped results periodically
|
||||
size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed);
|
||||
if (dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped);
|
||||
case ScanTimeoutState::INACTIVE:
|
||||
// This case should be unreachable - scanner and timeout states are always synchronized
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (this->scanner_state_ == ScannerState::STOPPED) {
|
||||
this->end_of_scan_(); // Change state to IDLE
|
||||
|
||||
ClientStateCounts counts = this->count_client_states_();
|
||||
if (counts != this->client_state_counts_) {
|
||||
this->client_state_counts_ = counts;
|
||||
ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d",
|
||||
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
|
||||
this->client_state_counts_.searching, this->client_state_counts_.disconnecting);
|
||||
}
|
||||
|
||||
if (this->scanner_state_ == ScannerState::FAILED ||
|
||||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
|
||||
this->stop_scan_();
|
||||
if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
|
||||
ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)",
|
||||
std::numeric_limits<uint8_t>::max());
|
||||
App.reboot();
|
||||
}
|
||||
if (this->scan_start_failed_) {
|
||||
ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
|
||||
this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
|
||||
}
|
||||
if (this->scan_set_param_failed_) {
|
||||
ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
|
||||
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
|
||||
}
|
||||
this->handle_scanner_failure_();
|
||||
}
|
||||
/*
|
||||
|
||||
@@ -218,13 +158,12 @@ void ESP32BLETracker::loop() {
|
||||
https://github.com/espressif/esp-idf/issues/6688
|
||||
|
||||
*/
|
||||
if (this->scanner_state_ == ScannerState::IDLE && !connecting && !disconnecting && !promote_to_connecting) {
|
||||
bool promote_to_connecting = counts.discovered && !counts.searching && !counts.connecting;
|
||||
|
||||
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting &&
|
||||
!promote_to_connecting) {
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
if (this->coex_prefer_ble_) {
|
||||
this->coex_prefer_ble_ = false;
|
||||
ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
|
||||
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
|
||||
}
|
||||
this->update_coex_preference_(false);
|
||||
#endif
|
||||
if (this->scan_continuous_) {
|
||||
this->start_scan_(false); // first = false
|
||||
@@ -232,31 +171,13 @@ void ESP32BLETracker::loop() {
|
||||
}
|
||||
// If there is a discovered client and no connecting
|
||||
// clients and no clients using the scanner to search for
|
||||
// devices, then stop scanning and promote the discovered
|
||||
// client to ready to connect.
|
||||
// devices, then promote the discovered client to ready to connect.
|
||||
// We check both RUNNING and IDLE states because:
|
||||
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
|
||||
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
|
||||
if (promote_to_connecting &&
|
||||
(this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->state() == ClientState::DISCOVERED) {
|
||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
ESP_LOGD(TAG, "Stopping scan to make connection");
|
||||
this->stop_scan_();
|
||||
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
||||
ESP_LOGD(TAG, "Promoting client to connect");
|
||||
// We only want to promote one client at a time.
|
||||
// once the scanner is fully stopped.
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
|
||||
if (!this->coex_prefer_ble_) {
|
||||
this->coex_prefer_ble_ = true;
|
||||
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
|
||||
}
|
||||
#endif
|
||||
client->set_state(ClientState::READY_TO_CONNECT);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
this->try_promote_discovered_clients_();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,18 +193,11 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
|
||||
|
||||
void ESP32BLETracker::stop_scan_() {
|
||||
if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
|
||||
if (this->scanner_state_ == ScannerState::IDLE) {
|
||||
ESP_LOGE(TAG, "Scan is already stopped while trying to stop.");
|
||||
} else if (this->scanner_state_ == ScannerState::STARTING) {
|
||||
ESP_LOGE(TAG, "Scan is starting while trying to stop.");
|
||||
} else if (this->scanner_state_ == ScannerState::STOPPING) {
|
||||
ESP_LOGE(TAG, "Scan is already stopping while trying to stop.");
|
||||
} else if (this->scanner_state_ == ScannerState::STOPPED) {
|
||||
ESP_LOGE(TAG, "Scan is already stopped while trying to stop.");
|
||||
}
|
||||
ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
|
||||
return;
|
||||
}
|
||||
this->cancel_timeout("scan");
|
||||
// Reset timeout state machine when stopping scan
|
||||
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
|
||||
this->set_scanner_state_(ScannerState::STOPPING);
|
||||
esp_err_t err = esp_ble_gap_stop_scanning();
|
||||
if (err != ESP_OK) {
|
||||
@@ -298,17 +212,7 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
return;
|
||||
}
|
||||
if (this->scanner_state_ != ScannerState::IDLE) {
|
||||
if (this->scanner_state_ == ScannerState::STARTING) {
|
||||
ESP_LOGE(TAG, "Cannot start scan while already starting.");
|
||||
} else if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
ESP_LOGE(TAG, "Cannot start scan while already running.");
|
||||
} else if (this->scanner_state_ == ScannerState::STOPPING) {
|
||||
ESP_LOGE(TAG, "Cannot start scan while already stopping.");
|
||||
} else if (this->scanner_state_ == ScannerState::FAILED) {
|
||||
ESP_LOGE(TAG, "Cannot start scan while already failed.");
|
||||
} else if (this->scanner_state_ == ScannerState::STOPPED) {
|
||||
ESP_LOGE(TAG, "Cannot start scan while already stopped.");
|
||||
}
|
||||
this->log_unexpected_state_("start scan", ScannerState::IDLE);
|
||||
return;
|
||||
}
|
||||
this->set_scanner_state_(ScannerState::STARTING);
|
||||
@@ -317,18 +221,19 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
for (auto *listener : this->listeners_)
|
||||
listener->on_scan_end();
|
||||
}
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
this->already_discovered_.clear();
|
||||
#endif
|
||||
this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE;
|
||||
this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
|
||||
this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
|
||||
this->scan_params_.scan_interval = this->scan_interval_;
|
||||
this->scan_params_.scan_window = this->scan_window_;
|
||||
|
||||
// Start timeout before scan is started. Otherwise scan never starts if any error.
|
||||
this->set_timeout("scan", this->scan_duration_ * 2000, []() {
|
||||
ESP_LOGE(TAG, "Scan never terminated, rebooting to restore stack (IDF)");
|
||||
App.reboot();
|
||||
});
|
||||
// Start timeout monitoring in loop() instead of using scheduler
|
||||
// This prevents false reboots when the loop is blocked
|
||||
this->scan_start_time_ = App.get_loop_component_start_time();
|
||||
this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
|
||||
|
||||
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
|
||||
if (err != ESP_OK) {
|
||||
@@ -342,21 +247,6 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLETracker::end_of_scan_() {
|
||||
// The lock must be held when calling this function.
|
||||
if (this->scanner_state_ != ScannerState::STOPPED) {
|
||||
ESP_LOGE(TAG, "end_of_scan_ called while scanner is not stopped.");
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "End of scan, set scanner state to IDLE.");
|
||||
this->already_discovered_.clear();
|
||||
this->cancel_timeout("scan");
|
||||
|
||||
for (auto *listener : this->listeners_)
|
||||
listener->on_scan_end();
|
||||
this->set_scanner_state_(ScannerState::IDLE);
|
||||
}
|
||||
|
||||
void ESP32BLETracker::register_client(ESPBTClient *client) {
|
||||
client->app_id = ++this->app_id_;
|
||||
this->clients_.push_back(client);
|
||||
@@ -389,6 +279,8 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
|
||||
// Note: This handler is called from the main loop context, not directly from the BT task.
|
||||
// The esp32_ble component queues events via enqueue_ble_event() and processes them in loop().
|
||||
switch (event) {
|
||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
|
||||
this->gap_scan_set_param_complete_(param->scan_param_cmpl);
|
||||
@@ -409,51 +301,32 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
|
||||
// Note: This handler is called from the main loop context via esp32_ble's event queue.
|
||||
// We process advertisements immediately instead of buffering them.
|
||||
ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
|
||||
|
||||
if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
|
||||
// Lock-free SPSC ring buffer write (Producer side)
|
||||
// This runs in the ESP-IDF Bluetooth stack callback thread
|
||||
// IMPORTANT: Only this thread writes to ring_write_index_
|
||||
// Process the scan result immediately
|
||||
bool found_discovered_client = this->process_scan_result_(scan_result);
|
||||
|
||||
// Load our own index with relaxed ordering (we're the only writer)
|
||||
uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed);
|
||||
uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
|
||||
|
||||
// Load consumer's index with acquire to see their latest updates
|
||||
uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire);
|
||||
|
||||
// Check if buffer is full
|
||||
if (next_write_idx != read_idx) {
|
||||
// Write to ring buffer
|
||||
this->scan_ring_buffer_[write_idx] = scan_result;
|
||||
|
||||
// Store with release to ensure the write is visible before index update
|
||||
this->ring_write_index_.store(next_write_idx, std::memory_order_release);
|
||||
} else {
|
||||
// Buffer full, track dropped results
|
||||
this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed);
|
||||
// If we found a discovered client that needs promotion, stop scanning
|
||||
// This replaces the promote_to_connecting logic from loop()
|
||||
if (found_discovered_client && this->scanner_state_ == ScannerState::RUNNING) {
|
||||
ESP_LOGD(TAG, "Found discovered client, stopping scan for connection");
|
||||
this->stop_scan_();
|
||||
}
|
||||
} else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
|
||||
// Scan finished on its own
|
||||
if (this->scanner_state_ != ScannerState::RUNNING) {
|
||||
if (this->scanner_state_ == ScannerState::STOPPING) {
|
||||
ESP_LOGE(TAG, "Scan was not running when scan completed.");
|
||||
} else if (this->scanner_state_ == ScannerState::STARTING) {
|
||||
ESP_LOGE(TAG, "Scan was not started when scan completed.");
|
||||
} else if (this->scanner_state_ == ScannerState::FAILED) {
|
||||
ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
|
||||
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
||||
ESP_LOGE(TAG, "Scan was idle when scan completed.");
|
||||
} else if (this->scanner_state_ == ScannerState::STOPPED) {
|
||||
ESP_LOGE(TAG, "Scan was stopped when scan completed.");
|
||||
}
|
||||
this->log_unexpected_state_("scan complete", ScannerState::RUNNING);
|
||||
}
|
||||
this->set_scanner_state_(ScannerState::STOPPED);
|
||||
// Scan completed naturally, perform cleanup and transition to IDLE
|
||||
this->cleanup_scan_state_(false);
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) {
|
||||
// Called from main loop context via gap_event_handler after being queued from BT task
|
||||
ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
|
||||
if (param.status == ESP_BT_STATUS_DONE) {
|
||||
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
|
||||
@@ -463,20 +336,11 @@ void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t:
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param ¶m) {
|
||||
// Called from main loop context via gap_event_handler after being queued from BT task
|
||||
ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
|
||||
this->scan_start_failed_ = param.status;
|
||||
if (this->scanner_state_ != ScannerState::STARTING) {
|
||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
ESP_LOGE(TAG, "Scan was already running when start complete.");
|
||||
} else if (this->scanner_state_ == ScannerState::STOPPING) {
|
||||
ESP_LOGE(TAG, "Scan was stopping when start complete.");
|
||||
} else if (this->scanner_state_ == ScannerState::FAILED) {
|
||||
ESP_LOGE(TAG, "Scan was in failed state when start complete.");
|
||||
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
||||
ESP_LOGE(TAG, "Scan was idle when start complete.");
|
||||
} else if (this->scanner_state_ == ScannerState::STOPPED) {
|
||||
ESP_LOGE(TAG, "Scan was stopped when start complete.");
|
||||
}
|
||||
this->log_unexpected_state_("start complete", ScannerState::STARTING);
|
||||
}
|
||||
if (param.status == ESP_BT_STATUS_SUCCESS) {
|
||||
this->scan_start_fail_count_ = 0;
|
||||
@@ -490,21 +354,15 @@ void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m) {
|
||||
// Called from main loop context via gap_event_handler after being queued from BT task
|
||||
// This allows us to safely transition to IDLE state and perform cleanup without race conditions
|
||||
ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
|
||||
if (this->scanner_state_ != ScannerState::STOPPING) {
|
||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
ESP_LOGE(TAG, "Scan was not running when stop complete.");
|
||||
} else if (this->scanner_state_ == ScannerState::STARTING) {
|
||||
ESP_LOGE(TAG, "Scan was not started when stop complete.");
|
||||
} else if (this->scanner_state_ == ScannerState::FAILED) {
|
||||
ESP_LOGE(TAG, "Scan was in failed state when stop complete.");
|
||||
} else if (this->scanner_state_ == ScannerState::IDLE) {
|
||||
ESP_LOGE(TAG, "Scan was idle when stop complete.");
|
||||
} else if (this->scanner_state_ == ScannerState::STOPPED) {
|
||||
ESP_LOGE(TAG, "Scan was stopped when stop complete.");
|
||||
}
|
||||
this->log_unexpected_state_("stop complete", ScannerState::STOPPING);
|
||||
}
|
||||
this->set_scanner_state_(ScannerState::STOPPED);
|
||||
|
||||
// Perform cleanup and transition to IDLE
|
||||
this->cleanup_scan_state_(true);
|
||||
}
|
||||
|
||||
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
@@ -781,28 +639,10 @@ void ESP32BLETracker::dump_config() {
|
||||
" Continuous Scanning: %s",
|
||||
this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f,
|
||||
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
|
||||
switch (this->scanner_state_) {
|
||||
case ScannerState::IDLE:
|
||||
ESP_LOGCONFIG(TAG, " Scanner State: IDLE");
|
||||
break;
|
||||
case ScannerState::STARTING:
|
||||
ESP_LOGCONFIG(TAG, " Scanner State: STARTING");
|
||||
break;
|
||||
case ScannerState::RUNNING:
|
||||
ESP_LOGCONFIG(TAG, " Scanner State: RUNNING");
|
||||
break;
|
||||
case ScannerState::STOPPING:
|
||||
ESP_LOGCONFIG(TAG, " Scanner State: STOPPING");
|
||||
break;
|
||||
case ScannerState::STOPPED:
|
||||
ESP_LOGCONFIG(TAG, " Scanner State: STOPPED");
|
||||
break;
|
||||
case ScannerState::FAILED:
|
||||
ESP_LOGCONFIG(TAG, " Scanner State: FAILED");
|
||||
break;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
|
||||
searching_, disconnecting_);
|
||||
ESP_LOGCONFIG(TAG, " Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_));
|
||||
ESP_LOGCONFIG(TAG, " Connecting: %d, discovered: %d, searching: %d, disconnecting: %d",
|
||||
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
|
||||
this->client_state_counts_.searching, this->client_state_counts_.disconnecting);
|
||||
if (this->scan_start_fail_count_) {
|
||||
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
|
||||
}
|
||||
@@ -879,8 +719,158 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
|
||||
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
|
||||
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
|
||||
}
|
||||
|
||||
bool ESP32BLETracker::has_connecting_clients_() const {
|
||||
for (auto *client : this->clients_) {
|
||||
auto state = client->state();
|
||||
if (state == ClientState::CONNECTING || state == ClientState::READY_TO_CONNECT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
|
||||
bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
|
||||
bool found_discovered_client = false;
|
||||
|
||||
// Process raw advertisements
|
||||
if (this->raw_advertisements_) {
|
||||
for (auto *listener : this->listeners_) {
|
||||
listener->parse_devices(&scan_result, 1);
|
||||
}
|
||||
for (auto *client : this->clients_) {
|
||||
client->parse_devices(&scan_result, 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Process parsed advertisements
|
||||
if (this->parse_advertisements_) {
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
ESPBTDevice device;
|
||||
device.parse_scan_rst(scan_result);
|
||||
|
||||
bool found = false;
|
||||
for (auto *listener : this->listeners_) {
|
||||
if (listener->parse_device(device))
|
||||
found = true;
|
||||
}
|
||||
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->parse_device(device)) {
|
||||
found = true;
|
||||
// Check if this client is discovered and needs promotion
|
||||
if (client->state() == ClientState::DISCOVERED) {
|
||||
// Only check for connecting clients if we found a discovered client
|
||||
// This matches the original logic: !connecting && client->state() == DISCOVERED
|
||||
if (!this->has_connecting_clients_()) {
|
||||
found_discovered_client = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!found && !this->scan_continuous_) {
|
||||
this->print_bt_device_info(device);
|
||||
}
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
}
|
||||
|
||||
return found_discovered_client;
|
||||
}
|
||||
|
||||
void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
|
||||
ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : "");
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
this->already_discovered_.clear();
|
||||
#endif
|
||||
// Reset timeout state machine instead of cancelling scheduler timeout
|
||||
this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
|
||||
|
||||
for (auto *listener : this->listeners_)
|
||||
listener->on_scan_end();
|
||||
|
||||
this->set_scanner_state_(ScannerState::IDLE);
|
||||
}
|
||||
|
||||
void ESP32BLETracker::handle_scanner_failure_() {
|
||||
this->stop_scan_();
|
||||
if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
|
||||
ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)",
|
||||
std::numeric_limits<uint8_t>::max());
|
||||
App.reboot();
|
||||
}
|
||||
if (this->scan_start_failed_) {
|
||||
ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
|
||||
this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
|
||||
}
|
||||
if (this->scan_set_param_failed_) {
|
||||
ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
|
||||
this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLETracker::try_promote_discovered_clients_() {
|
||||
// Only promote the first discovered client to avoid multiple simultaneous connections
|
||||
for (auto *client : this->clients_) {
|
||||
if (client->state() != ClientState::DISCOVERED) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
ESP_LOGD(TAG, "Stopping scan to make connection");
|
||||
this->stop_scan_();
|
||||
// Don't wait for scan stop complete - promote immediately.
|
||||
// This is safe because ESP-IDF processes BLE commands sequentially through its internal mailbox queue.
|
||||
// This guarantees that the stop scan command will be fully processed before any subsequent connect command,
|
||||
// preventing race conditions or overlapping operations.
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Promoting client to connect");
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
this->update_coex_preference_(true);
|
||||
#endif
|
||||
client->set_state(ClientState::READY_TO_CONNECT);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const {
|
||||
switch (state) {
|
||||
case ScannerState::IDLE:
|
||||
return "IDLE";
|
||||
case ScannerState::STARTING:
|
||||
return "STARTING";
|
||||
case ScannerState::RUNNING:
|
||||
return "RUNNING";
|
||||
case ScannerState::STOPPING:
|
||||
return "STOPPING";
|
||||
case ScannerState::FAILED:
|
||||
return "FAILED";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState expected_state) const {
|
||||
ESP_LOGE(TAG, "Unexpected state: %s on %s, expected: %s", this->scanner_state_to_string_(this->scanner_state_),
|
||||
operation, this->scanner_state_to_string_(expected_state));
|
||||
}
|
||||
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
void ESP32BLETracker::update_coex_preference_(bool force_ble) {
|
||||
if (force_ble && !this->coex_prefer_ble_) {
|
||||
ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
|
||||
this->coex_prefer_ble_ = true;
|
||||
esp_coex_preference_set(ESP_COEX_PREFER_BT); // Prioritize Bluetooth
|
||||
} else if (!force_ble && this->coex_prefer_ble_) {
|
||||
ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
|
||||
this->coex_prefer_ble_ = false;
|
||||
esp_coex_preference_set(ESP_COEX_PREFER_BALANCE); // Reset to default
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::esp32_ble_tracker
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
@@ -6,7 +6,6 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -21,6 +20,7 @@
|
||||
|
||||
#include "esphome/components/esp32_ble/ble.h"
|
||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
||||
#include "esphome/components/esp32_ble/ble_scan_result.h"
|
||||
|
||||
namespace esphome::esp32_ble_tracker {
|
||||
|
||||
@@ -136,6 +136,20 @@ class ESPBTDeviceListener {
|
||||
ESP32BLETracker *parent_{nullptr};
|
||||
};
|
||||
|
||||
struct ClientStateCounts {
|
||||
uint8_t connecting = 0;
|
||||
uint8_t discovered = 0;
|
||||
uint8_t searching = 0;
|
||||
uint8_t disconnecting = 0;
|
||||
|
||||
bool operator==(const ClientStateCounts &other) const {
|
||||
return connecting == other.connecting && discovered == other.discovered && searching == other.searching &&
|
||||
disconnecting == other.disconnecting;
|
||||
}
|
||||
|
||||
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
|
||||
};
|
||||
|
||||
enum class ClientState : uint8_t {
|
||||
// Connection is allocated
|
||||
INIT,
|
||||
@@ -158,20 +172,21 @@ enum class ClientState : uint8_t {
|
||||
};
|
||||
|
||||
enum class ScannerState {
|
||||
// Scanner is idle, init state, set from the main loop when processing STOPPED
|
||||
// Scanner is idle, init state
|
||||
IDLE,
|
||||
// Scanner is starting, set from the main loop only
|
||||
// Scanner is starting
|
||||
STARTING,
|
||||
// Scanner is running, set from the ESP callback only
|
||||
// Scanner is running
|
||||
RUNNING,
|
||||
// Scanner failed to start, set from the ESP callback only
|
||||
// Scanner failed to start
|
||||
FAILED,
|
||||
// Scanner is stopping, set from the main loop only
|
||||
// Scanner is stopping
|
||||
STOPPING,
|
||||
// Scanner is stopped, set from the ESP callback only
|
||||
STOPPED,
|
||||
};
|
||||
|
||||
// Helper function to convert ClientState to string
|
||||
const char *client_state_to_string(ClientState state);
|
||||
|
||||
enum class ConnectionType : uint8_t {
|
||||
// The default connection type, we hold all the services in ram
|
||||
// for the duration of the connection.
|
||||
@@ -262,8 +277,6 @@ class ESP32BLETracker : public Component,
|
||||
void stop_scan_();
|
||||
/// Start a single scan by setting up the parameters and doing some esp-idf calls.
|
||||
void start_scan_(bool first);
|
||||
/// Called when a scan ends
|
||||
void end_of_scan_();
|
||||
/// Called when a `ESP_GAP_BLE_SCAN_RESULT_EVT` event is received.
|
||||
void gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m);
|
||||
/// Called when a `ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT` event is received.
|
||||
@@ -274,47 +287,94 @@ class ESP32BLETracker : public Component,
|
||||
void gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param ¶m);
|
||||
/// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed.
|
||||
void set_scanner_state_(ScannerState state);
|
||||
/// Common cleanup logic when transitioning scanner to IDLE state
|
||||
void cleanup_scan_state_(bool is_stop_complete);
|
||||
/// Process a single scan result immediately
|
||||
/// Returns true if a discovered client needs promotion to READY_TO_CONNECT
|
||||
bool process_scan_result_(const BLEScanResult &scan_result);
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
/// Check if any clients are in connecting or ready to connect state
|
||||
bool has_connecting_clients_() const;
|
||||
#endif
|
||||
/// Handle scanner failure states
|
||||
void handle_scanner_failure_();
|
||||
/// Try to promote discovered clients to ready to connect
|
||||
void try_promote_discovered_clients_();
|
||||
/// Convert scanner state enum to string for logging
|
||||
const char *scanner_state_to_string_(ScannerState state) const;
|
||||
/// Log an unexpected scanner state
|
||||
void log_unexpected_state_(const char *operation, ScannerState expected_state) const;
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
/// Update BLE coexistence preference
|
||||
void update_coex_preference_(bool force_ble);
|
||||
#endif
|
||||
/// Count clients in each state
|
||||
ClientStateCounts count_client_states_() const {
|
||||
ClientStateCounts counts;
|
||||
for (auto *client : this->clients_) {
|
||||
switch (client->state()) {
|
||||
case ClientState::DISCONNECTING:
|
||||
counts.disconnecting++;
|
||||
break;
|
||||
case ClientState::DISCOVERED:
|
||||
counts.discovered++;
|
||||
break;
|
||||
case ClientState::SEARCHING:
|
||||
counts.searching++;
|
||||
break;
|
||||
case ClientState::CONNECTING:
|
||||
case ClientState::READY_TO_CONNECT:
|
||||
counts.connecting++;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return counts;
|
||||
}
|
||||
|
||||
uint8_t app_id_{0};
|
||||
|
||||
// Group 1: Large objects (12+ bytes) - vectors and callback manager
|
||||
std::vector<ESPBTDeviceListener *> listeners_;
|
||||
std::vector<ESPBTClient *> clients_;
|
||||
CallbackManager<void(ScannerState)> scanner_state_callbacks_;
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
/// Vector of addresses that have already been printed in print_bt_device_info
|
||||
std::vector<uint64_t> already_discovered_;
|
||||
std::vector<ESPBTDeviceListener *> listeners_;
|
||||
/// Client parameters.
|
||||
std::vector<ESPBTClient *> clients_;
|
||||
#endif
|
||||
|
||||
// Group 2: Structs (aligned to 4 bytes)
|
||||
/// A structure holding the ESP BLE scan parameters.
|
||||
esp_ble_scan_params_t scan_params_;
|
||||
ClientStateCounts client_state_counts_;
|
||||
|
||||
// Group 3: 4-byte types
|
||||
/// The interval in seconds to perform scans.
|
||||
uint32_t scan_duration_;
|
||||
uint32_t scan_interval_;
|
||||
uint32_t scan_window_;
|
||||
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
|
||||
// Group 4: 1-byte types (enums, uint8_t, bool)
|
||||
uint8_t app_id_{0};
|
||||
uint8_t scan_start_fail_count_{0};
|
||||
ScannerState scanner_state_{ScannerState::IDLE};
|
||||
bool scan_continuous_;
|
||||
bool scan_active_;
|
||||
ScannerState scanner_state_{ScannerState::IDLE};
|
||||
CallbackManager<void(ScannerState)> scanner_state_callbacks_;
|
||||
bool ble_was_disabled_{true};
|
||||
bool raw_advertisements_{false};
|
||||
bool parse_advertisements_{false};
|
||||
|
||||
// Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results
|
||||
// Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler)
|
||||
// Consumer: ESPHome main loop (loop() method)
|
||||
// This design ensures zero blocking in the BT callback and prevents scan result loss
|
||||
BLEScanResult *scan_ring_buffer_;
|
||||
std::atomic<uint8_t> ring_write_index_{0}; // Written only by BT callback (producer)
|
||||
std::atomic<uint8_t> ring_read_index_{0}; // Written only by main loop (consumer)
|
||||
std::atomic<uint16_t> scan_results_dropped_{0}; // Tracks buffer overflow events
|
||||
|
||||
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
|
||||
int connecting_{0};
|
||||
int discovered_{0};
|
||||
int searching_{0};
|
||||
int disconnecting_{0};
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
bool coex_prefer_ble_{false};
|
||||
#endif
|
||||
// Scan timeout state machine
|
||||
enum class ScanTimeoutState : uint8_t {
|
||||
INACTIVE, // No timeout monitoring
|
||||
MONITORING, // Actively monitoring for timeout
|
||||
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
|
||||
};
|
||||
uint32_t scan_start_time_{0};
|
||||
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
|
||||
};
|
||||
|
||||
// NOLINTNEXTLINE
|
||||
|
@@ -345,7 +345,7 @@ async def to_code(config):
|
||||
cg.add_define("USE_CAMERA")
|
||||
|
||||
if CORE.using_esp_idf:
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.0.15")
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
|
||||
|
||||
for conf in config.get(CONF_ON_STREAM_START, []):
|
||||
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
|
||||
|
@@ -2,11 +2,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#include <esp32-hal-dac.h>
|
||||
#endif
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_dac {
|
||||
@@ -23,18 +19,12 @@ void ESP32DAC::setup() {
|
||||
this->pin_->setup();
|
||||
this->turn_off();
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1;
|
||||
const dac_oneshot_config_t oneshot_cfg{channel};
|
||||
dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_);
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESP32DAC::on_safe_shutdown() {
|
||||
#ifdef USE_ESP_IDF
|
||||
dac_oneshot_del_channel(this->dac_handle_);
|
||||
#endif
|
||||
}
|
||||
void ESP32DAC::on_safe_shutdown() { dac_oneshot_del_channel(this->dac_handle_); }
|
||||
|
||||
void ESP32DAC::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "ESP32 DAC:");
|
||||
@@ -48,15 +38,10 @@ void ESP32DAC::write_state(float state) {
|
||||
|
||||
state = state * 255;
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
dac_oneshot_output_voltage(this->dac_handle_, state);
|
||||
#endif
|
||||
#ifdef USE_ARDUINO
|
||||
dacWrite(this->pin_->get_pin(), state);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace esp32_dac
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2
|
||||
|
@@ -1,15 +1,13 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/output/float_output.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/components/output/float_output.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
|
||||
|
||||
#ifdef USE_ESP_IDF
|
||||
#include <driver/dac_oneshot.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_dac {
|
||||
@@ -29,12 +27,10 @@ class ESP32DAC : public output::FloatOutput, public Component {
|
||||
void write_state(float state) override;
|
||||
|
||||
InternalGPIOPin *pin_;
|
||||
#ifdef USE_ESP_IDF
|
||||
dac_oneshot_handle_t dac_handle_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace esp32_dac
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
||||
#endif // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2
|
||||
|
@@ -42,9 +42,6 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
|
||||
symbols[i] = params->bit0;
|
||||
}
|
||||
}
|
||||
if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) {
|
||||
*done = true;
|
||||
}
|
||||
return RMT_SYMBOLS_PER_BYTE;
|
||||
}
|
||||
|
||||
@@ -110,7 +107,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
|
||||
memset(&encoder, 0, sizeof(encoder));
|
||||
encoder.callback = encoder_callback;
|
||||
encoder.arg = &this->params_;
|
||||
encoder.min_chunk_size = 8;
|
||||
encoder.min_chunk_size = RMT_SYMBOLS_PER_BYTE;
|
||||
if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Encoder creation failed");
|
||||
this->mark_failed();
|
||||
|
@@ -171,8 +171,8 @@ class ESP32TouchComponent : public Component {
|
||||
// based on the filter configuration
|
||||
uint32_t read_touch_value(touch_pad_t pad) const;
|
||||
|
||||
// Helper to update touch state with a known state
|
||||
void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched);
|
||||
// Helper to update touch state with a known state and value
|
||||
void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value);
|
||||
|
||||
// Helper to read touch value and update state for a given child
|
||||
bool check_and_update_touch_state_(ESP32TouchBinarySensor *child);
|
||||
@@ -234,9 +234,13 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
|
||||
touch_pad_t get_touch_pad() const { return this->touch_pad_; }
|
||||
uint32_t get_threshold() const { return this->threshold_; }
|
||||
void set_threshold(uint32_t threshold) { this->threshold_ = threshold; }
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
|
||||
/// Get the raw touch measurement value.
|
||||
/// @note Although this method may appear unused within the component, it is a public API
|
||||
/// used by lambdas in user configurations for custom touch value processing.
|
||||
/// @return The current raw touch sensor reading
|
||||
uint32_t get_value() const { return this->value_; }
|
||||
#endif
|
||||
|
||||
uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; }
|
||||
|
||||
protected:
|
||||
@@ -245,9 +249,8 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
|
||||
touch_pad_t touch_pad_{TOUCH_PAD_MAX};
|
||||
uint32_t threshold_{0};
|
||||
uint32_t benchmark_{};
|
||||
#ifdef USE_ESP32_VARIANT_ESP32
|
||||
/// Stores the last raw touch measurement value.
|
||||
uint32_t value_{0};
|
||||
#endif
|
||||
bool last_state_{false};
|
||||
const uint32_t wakeup_threshold_{0};
|
||||
|
||||
|
@@ -100,6 +100,8 @@ void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) {
|
||||
#else
|
||||
// Read the value being used for touch detection
|
||||
uint32_t value = this->read_touch_value(child->get_touch_pad());
|
||||
// Store the value for get_value() access in lambdas
|
||||
child->value_ = value;
|
||||
ESP_LOGD(TAG, "Touch Pad '%s' (T%d): %d", child->get_name().c_str(), child->get_touch_pad(), value);
|
||||
#endif
|
||||
}
|
||||
|
@@ -201,15 +201,13 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
|
||||
touch_pad_t pad = child->get_touch_pad();
|
||||
|
||||
// Read current value using ISR-safe API
|
||||
uint32_t value;
|
||||
if (component->iir_filter_enabled_()) {
|
||||
uint16_t temp_value = 0;
|
||||
touch_pad_read_filtered(pad, &temp_value);
|
||||
value = temp_value;
|
||||
} else {
|
||||
// Use low-level HAL function when filter is not enabled
|
||||
value = touch_ll_read_raw_data(pad);
|
||||
}
|
||||
// IMPORTANT: ESP-IDF v5.4 regression - touch_pad_read_filtered() is no longer ISR-safe
|
||||
// In ESP-IDF v5.3 and earlier it was ISR-safe, but ESP-IDF v5.4 added mutex protection that causes:
|
||||
// "assert failed: xQueueSemaphoreTake queue.c:1718"
|
||||
// We must use raw values even when filter is enabled as a workaround.
|
||||
// Users should adjust thresholds to compensate for the lack of IIR filtering.
|
||||
// See: https://github.com/espressif/esp-idf/issues/17045
|
||||
uint32_t value = touch_ll_read_raw_data(pad);
|
||||
|
||||
// Skip pads that aren’t in the trigger mask
|
||||
if (((mask >> pad) & 1) == 0) {
|
||||
|
@@ -10,8 +10,11 @@ namespace esp32_touch {
|
||||
|
||||
static const char *const TAG = "esp32_touch";
|
||||
|
||||
// Helper to update touch state with a known state
|
||||
void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) {
|
||||
// Helper to update touch state with a known state and value
|
||||
void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value) {
|
||||
// Store the value for get_value() access in lambdas
|
||||
child->value_ = value;
|
||||
|
||||
// Always update timer when touched
|
||||
if (is_touched) {
|
||||
child->last_touch_time_ = App.get_loop_component_start_time();
|
||||
@@ -21,9 +24,8 @@ void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, boo
|
||||
child->last_state_ = is_touched;
|
||||
child->publish_state(is_touched);
|
||||
if (is_touched) {
|
||||
// ESP32-S2/S3 v2: touched when value > threshold
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(),
|
||||
this->read_touch_value(child->touch_pad_), child->threshold_ + child->benchmark_);
|
||||
value, child->threshold_ + child->benchmark_);
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str());
|
||||
}
|
||||
@@ -41,7 +43,7 @@ bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *
|
||||
child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_);
|
||||
bool is_touched = value > child->benchmark_ + child->threshold_;
|
||||
|
||||
this->update_touch_state_(child, is_touched);
|
||||
this->update_touch_state_(child, is_touched, value);
|
||||
return is_touched;
|
||||
}
|
||||
|
||||
@@ -296,7 +298,9 @@ void ESP32TouchComponent::loop() {
|
||||
this->check_and_update_touch_state_(child);
|
||||
} else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) {
|
||||
// We only get ACTIVE interrupts now, releases are detected by timeout
|
||||
this->update_touch_state_(child, true); // Always touched for ACTIVE interrupts
|
||||
// Read the current value
|
||||
uint32_t value = this->read_touch_value(child->touch_pad_);
|
||||
this->update_touch_state_(child, true, value); // Always touched for ACTIVE interrupts
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
309
esphome/components/espnow/__init__.py
Normal file
309
esphome/components/espnow/__init__.py
Normal file
@@ -0,0 +1,309 @@
|
||||
from esphome import automation, core
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import wifi
|
||||
from esphome.components.udp import CONF_ON_RECEIVE
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ADDRESS,
|
||||
CONF_CHANNEL,
|
||||
CONF_DATA,
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_ID,
|
||||
CONF_ON_ERROR,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WIFI,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@jesserockz"]
|
||||
|
||||
byte_vector = cg.std_vector.template(cg.uint8)
|
||||
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
|
||||
|
||||
espnow_ns = cg.esphome_ns.namespace("espnow")
|
||||
ESPNowComponent = espnow_ns.class_("ESPNowComponent", cg.Component)
|
||||
|
||||
# Handler interfaces that other components can use to register callbacks
|
||||
ESPNowReceivedPacketHandler = espnow_ns.class_("ESPNowReceivedPacketHandler")
|
||||
ESPNowUnknownPeerHandler = espnow_ns.class_("ESPNowUnknownPeerHandler")
|
||||
ESPNowBroadcastedHandler = espnow_ns.class_("ESPNowBroadcastedHandler")
|
||||
|
||||
ESPNowRecvInfo = espnow_ns.class_("ESPNowRecvInfo")
|
||||
ESPNowRecvInfoConstRef = ESPNowRecvInfo.operator("const").operator("ref")
|
||||
|
||||
SendAction = espnow_ns.class_("SendAction", automation.Action)
|
||||
SetChannelAction = espnow_ns.class_("SetChannelAction", automation.Action)
|
||||
AddPeerAction = espnow_ns.class_("AddPeerAction", automation.Action)
|
||||
DeletePeerAction = espnow_ns.class_("DeletePeerAction", automation.Action)
|
||||
|
||||
ESPNowHandlerTrigger = automation.Trigger.template(
|
||||
ESPNowRecvInfoConstRef,
|
||||
cg.uint8.operator("const").operator("ptr"),
|
||||
cg.uint8,
|
||||
)
|
||||
|
||||
OnUnknownPeerTrigger = espnow_ns.class_(
|
||||
"OnUnknownPeerTrigger", ESPNowHandlerTrigger, ESPNowUnknownPeerHandler
|
||||
)
|
||||
OnReceiveTrigger = espnow_ns.class_(
|
||||
"OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivedPacketHandler
|
||||
)
|
||||
OnBroadcastedTrigger = espnow_ns.class_(
|
||||
"OnBroadcastedTrigger", ESPNowHandlerTrigger, ESPNowBroadcastedHandler
|
||||
)
|
||||
|
||||
|
||||
CONF_AUTO_ADD_PEER = "auto_add_peer"
|
||||
CONF_PEERS = "peers"
|
||||
CONF_ON_SENT = "on_sent"
|
||||
CONF_ON_UNKNOWN_PEER = "on_unknown_peer"
|
||||
CONF_ON_BROADCAST = "on_broadcast"
|
||||
CONF_CONTINUE_ON_ERROR = "continue_on_error"
|
||||
CONF_WAIT_FOR_SENT = "wait_for_sent"
|
||||
|
||||
MAX_ESPNOW_PACKET_SIZE = 250 # Maximum size of the payload in bytes
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(ESPNowComponent),
|
||||
cv.OnlyWithout(CONF_CHANNEL, CONF_WIFI): wifi.validate_channel,
|
||||
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_AUTO_ADD_PEER, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PEERS): cv.ensure_list(cv.mac_address),
|
||||
cv.Optional(CONF_ON_UNKNOWN_PEER): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnUnknownPeerTrigger),
|
||||
},
|
||||
single=True,
|
||||
),
|
||||
cv.Optional(CONF_ON_RECEIVE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnReceiveTrigger),
|
||||
cv.Optional(CONF_ADDRESS): cv.mac_address,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_ON_BROADCAST): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastedTrigger),
|
||||
cv.Optional(CONF_ADDRESS): cv.mac_address,
|
||||
}
|
||||
),
|
||||
},
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.only_on_esp32,
|
||||
)
|
||||
|
||||
|
||||
async def _trigger_to_code(config):
|
||||
if address := config.get(CONF_ADDRESS):
|
||||
address = address.parts
|
||||
trigger = cg.new_Pvariable(config[CONF_TRIGGER_ID], address)
|
||||
await automation.build_automation(
|
||||
trigger,
|
||||
[
|
||||
(ESPNowRecvInfoConstRef, "info"),
|
||||
(cg.uint8.operator("const").operator("ptr"), "data"),
|
||||
(cg.uint8, "size"),
|
||||
],
|
||||
config,
|
||||
)
|
||||
return trigger
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if CORE.using_arduino:
|
||||
cg.add_library("WiFi", None)
|
||||
|
||||
cg.add_define("USE_ESPNOW")
|
||||
if wifi_channel := config.get(CONF_CHANNEL):
|
||||
cg.add(var.set_wifi_channel(wifi_channel))
|
||||
|
||||
cg.add(var.set_auto_add_peer(config[CONF_AUTO_ADD_PEER]))
|
||||
|
||||
for peer in config.get(CONF_PEERS, []):
|
||||
cg.add(var.add_peer(peer.parts))
|
||||
|
||||
if on_receive := config.get(CONF_ON_UNKNOWN_PEER):
|
||||
trigger = await _trigger_to_code(on_receive)
|
||||
cg.add(var.register_unknown_peer_handler(trigger))
|
||||
|
||||
for on_receive in config.get(CONF_ON_RECEIVE, []):
|
||||
trigger = await _trigger_to_code(on_receive)
|
||||
cg.add(var.register_received_handler(trigger))
|
||||
|
||||
for on_receive in config.get(CONF_ON_BROADCAST, []):
|
||||
trigger = await _trigger_to_code(on_receive)
|
||||
cg.add(var.register_broadcasted_handler(trigger))
|
||||
|
||||
|
||||
# ========================================== A C T I O N S ================================================
|
||||
|
||||
|
||||
def validate_peer(value):
|
||||
if isinstance(value, cv.Lambda):
|
||||
return cv.returning_lambda(value)
|
||||
return cv.mac_address(value)
|
||||
|
||||
|
||||
def _validate_raw_data(value):
|
||||
if isinstance(value, str):
|
||||
if len(value) >= MAX_ESPNOW_PACKET_SIZE:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}"
|
||||
)
|
||||
return value
|
||||
if isinstance(value, list):
|
||||
if len(value) > MAX_ESPNOW_PACKET_SIZE:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}"
|
||||
)
|
||||
return cv.Schema([cv.hex_uint8_t])(value)
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_DATA}' must either be a string wrapped in quotes or a list of bytes"
|
||||
)
|
||||
|
||||
|
||||
async def register_peer(var, config, args):
|
||||
peer = config[CONF_ADDRESS]
|
||||
if isinstance(peer, core.MACAddress):
|
||||
peer = [HexInt(p) for p in peer.parts]
|
||||
|
||||
template_ = await cg.templatable(peer, args, peer_address_t, peer_address_t)
|
||||
cg.add(var.set_address(template_))
|
||||
|
||||
|
||||
PEER_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(ESPNowComponent),
|
||||
cv.Required(CONF_ADDRESS): cv.templatable(cv.mac_address),
|
||||
}
|
||||
)
|
||||
|
||||
SEND_SCHEMA = PEER_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_DATA): cv.templatable(_validate_raw_data),
|
||||
cv.Optional(CONF_ON_SENT): automation.validate_action_list,
|
||||
cv.Optional(CONF_ON_ERROR): automation.validate_action_list,
|
||||
cv.Optional(CONF_WAIT_FOR_SENT, default=True): cv.boolean,
|
||||
cv.Optional(CONF_CONTINUE_ON_ERROR, default=True): cv.boolean,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _validate_send_action(config):
|
||||
if not config[CONF_WAIT_FOR_SENT] and not config[CONF_CONTINUE_ON_ERROR]:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_CONTINUE_ON_ERROR}' cannot be false if '{CONF_WAIT_FOR_SENT}' is false as the automation will not wait for the failed result.",
|
||||
path=[CONF_CONTINUE_ON_ERROR],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
SEND_SCHEMA.add_extra(_validate_send_action)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"espnow.send",
|
||||
SendAction,
|
||||
SEND_SCHEMA,
|
||||
)
|
||||
@automation.register_action(
|
||||
"espnow.broadcast",
|
||||
SendAction,
|
||||
cv.maybe_simple_value(
|
||||
SEND_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ADDRESS, default="FF:FF:FF:FF:FF:FF"): cv.mac_address,
|
||||
}
|
||||
),
|
||||
key=CONF_DATA,
|
||||
),
|
||||
)
|
||||
async def send_action(
|
||||
config: ConfigType,
|
||||
action_id: core.ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: list[tuple],
|
||||
):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
|
||||
await register_peer(var, config, args)
|
||||
|
||||
data = config.get(CONF_DATA, [])
|
||||
if isinstance(data, str):
|
||||
data = [cg.RawExpression(f"'{c}'") for c in data]
|
||||
templ = await cg.templatable(data, args, byte_vector, byte_vector)
|
||||
cg.add(var.set_data(templ))
|
||||
|
||||
cg.add(var.set_wait_for_sent(config[CONF_WAIT_FOR_SENT]))
|
||||
cg.add(var.set_continue_on_error(config[CONF_CONTINUE_ON_ERROR]))
|
||||
|
||||
if on_sent_config := config.get(CONF_ON_SENT):
|
||||
actions = await automation.build_action_list(on_sent_config, template_arg, args)
|
||||
cg.add(var.add_on_sent(actions))
|
||||
if on_error_config := config.get(CONF_ON_ERROR):
|
||||
actions = await automation.build_action_list(
|
||||
on_error_config, template_arg, args
|
||||
)
|
||||
cg.add(var.add_on_error(actions))
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"espnow.peer.add",
|
||||
AddPeerAction,
|
||||
cv.maybe_simple_value(
|
||||
PEER_SCHEMA,
|
||||
key=CONF_ADDRESS,
|
||||
),
|
||||
)
|
||||
@automation.register_action(
|
||||
"espnow.peer.delete",
|
||||
DeletePeerAction,
|
||||
cv.maybe_simple_value(
|
||||
PEER_SCHEMA,
|
||||
key=CONF_ADDRESS,
|
||||
),
|
||||
)
|
||||
async def peer_action(
|
||||
config: ConfigType,
|
||||
action_id: core.ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: list[tuple],
|
||||
):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
await register_peer(var, config, args)
|
||||
|
||||
return var
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"espnow.set_channel",
|
||||
SetChannelAction,
|
||||
cv.maybe_simple_value(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(ESPNowComponent),
|
||||
cv.Required(CONF_CHANNEL): cv.templatable(wifi.validate_channel),
|
||||
},
|
||||
key=CONF_CHANNEL,
|
||||
),
|
||||
)
|
||||
async def channel_action(
|
||||
config: ConfigType,
|
||||
action_id: core.ID,
|
||||
template_arg: cg.TemplateArguments,
|
||||
args: list[tuple],
|
||||
):
|
||||
var = cg.new_Pvariable(action_id, template_arg)
|
||||
await cg.register_parented(var, config[CONF_ID])
|
||||
template_ = await cg.templatable(config[CONF_CHANNEL], args, cg.uint8)
|
||||
cg.add(var.set_channel(template_))
|
||||
return var
|
175
esphome/components/espnow/automation.h
Normal file
175
esphome/components/espnow/automation.h
Normal file
@@ -0,0 +1,175 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "espnow_component.h"
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/base_automation.h"
|
||||
|
||||
namespace esphome::espnow {
|
||||
|
||||
template<typename... Ts> class SendAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
TEMPLATABLE_VALUE(peer_address_t, address);
|
||||
TEMPLATABLE_VALUE(std::vector<uint8_t>, data);
|
||||
|
||||
public:
|
||||
void add_on_sent(const std::vector<Action<Ts...> *> &actions) {
|
||||
this->sent_.add_actions(actions);
|
||||
if (this->flags_.wait_for_sent) {
|
||||
this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
|
||||
}
|
||||
}
|
||||
void add_on_error(const std::vector<Action<Ts...> *> &actions) {
|
||||
this->error_.add_actions(actions);
|
||||
if (this->flags_.wait_for_sent) {
|
||||
this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
|
||||
if (this->flags_.continue_on_error) {
|
||||
this->play_next_(x...);
|
||||
} else {
|
||||
this->stop_complex();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
void set_wait_for_sent(bool wait_for_sent) { this->flags_.wait_for_sent = wait_for_sent; }
|
||||
void set_continue_on_error(bool continue_on_error) { this->flags_.continue_on_error = continue_on_error; }
|
||||
|
||||
void play_complex(Ts... x) override {
|
||||
this->num_running_++;
|
||||
send_callback_t send_callback = [this, x...](esp_err_t status) {
|
||||
if (status == ESP_OK) {
|
||||
if (!this->sent_.empty()) {
|
||||
this->sent_.play(x...);
|
||||
} else if (this->flags_.wait_for_sent) {
|
||||
this->play_next_(x...);
|
||||
}
|
||||
} else {
|
||||
if (!this->error_.empty()) {
|
||||
this->error_.play(x...);
|
||||
} else if (this->flags_.wait_for_sent) {
|
||||
if (this->flags_.continue_on_error) {
|
||||
this->play_next_(x...);
|
||||
} else {
|
||||
this->stop_complex();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
peer_address_t address = this->address_.value(x...);
|
||||
std::vector<uint8_t> data = this->data_.value(x...);
|
||||
esp_err_t err = this->parent_->send(address.data(), data, send_callback);
|
||||
if (err != ESP_OK) {
|
||||
send_callback(err);
|
||||
} else if (!this->flags_.wait_for_sent) {
|
||||
this->play_next_(x...);
|
||||
}
|
||||
}
|
||||
|
||||
void play(Ts... x) override { /* ignore - see play_complex */
|
||||
}
|
||||
|
||||
void stop() override {
|
||||
this->sent_.stop();
|
||||
this->error_.stop();
|
||||
}
|
||||
|
||||
protected:
|
||||
ActionList<Ts...> sent_;
|
||||
ActionList<Ts...> error_;
|
||||
|
||||
struct {
|
||||
uint8_t wait_for_sent : 1; // Wait for the send operation to complete before continuing automation
|
||||
uint8_t continue_on_error : 1; // Continue automation even if the send operation fails
|
||||
uint8_t reserved : 6; // Reserved for future use
|
||||
} flags_{0};
|
||||
};
|
||||
|
||||
template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
TEMPLATABLE_VALUE(peer_address_t, address);
|
||||
|
||||
public:
|
||||
void play(Ts... x) override {
|
||||
peer_address_t address = this->address_.value(x...);
|
||||
this->parent_->add_peer(address.data());
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
TEMPLATABLE_VALUE(peer_address_t, address);
|
||||
|
||||
public:
|
||||
void play(Ts... x) override {
|
||||
peer_address_t address = this->address_.value(x...);
|
||||
this->parent_->del_peer(address.data());
|
||||
}
|
||||
};
|
||||
|
||||
template<typename... Ts> class SetChannelAction : public Action<Ts...>, public Parented<ESPNowComponent> {
|
||||
public:
|
||||
TEMPLATABLE_VALUE(uint8_t, channel)
|
||||
void play(Ts... x) override {
|
||||
if (this->parent_->is_wifi_enabled()) {
|
||||
return;
|
||||
}
|
||||
this->parent_->set_wifi_channel(this->channel_.value(x...));
|
||||
this->parent_->apply_wifi_channel();
|
||||
}
|
||||
};
|
||||
|
||||
class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowReceivedPacketHandler {
|
||||
public:
|
||||
explicit OnReceiveTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
|
||||
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
|
||||
}
|
||||
|
||||
explicit OnReceiveTrigger() : has_address_(false) {}
|
||||
|
||||
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
|
||||
if (!match)
|
||||
return false;
|
||||
|
||||
this->trigger(info, data, size);
|
||||
return false; // Return false to continue processing other internal handlers
|
||||
}
|
||||
|
||||
protected:
|
||||
bool has_address_{false};
|
||||
const uint8_t *address_[ESP_NOW_ETH_ALEN];
|
||||
};
|
||||
class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowUnknownPeerHandler {
|
||||
public:
|
||||
bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
this->trigger(info, data, size);
|
||||
return false; // Return false to continue processing other internal handlers
|
||||
}
|
||||
};
|
||||
class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
|
||||
public ESPNowBroadcastedHandler {
|
||||
public:
|
||||
explicit OnBroadcastedTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
|
||||
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
|
||||
}
|
||||
explicit OnBroadcastedTrigger() : has_address_(false) {}
|
||||
|
||||
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
|
||||
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
|
||||
if (!match)
|
||||
return false;
|
||||
|
||||
this->trigger(info, data, size);
|
||||
return false; // Return false to continue processing other internal handlers
|
||||
}
|
||||
|
||||
protected:
|
||||
bool has_address_{false};
|
||||
const uint8_t *address_[ESP_NOW_ETH_ALEN];
|
||||
};
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
#endif // USE_ESP32
|
468
esphome/components/espnow/espnow_component.cpp
Normal file
468
esphome/components/espnow/espnow_component.cpp
Normal file
@@ -0,0 +1,468 @@
|
||||
#include "espnow_component.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "espnow_err.h"
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <esp_event.h>
|
||||
#include <esp_mac.h>
|
||||
#include <esp_now.h>
|
||||
#include <esp_random.h>
|
||||
#include <esp_wifi.h>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
#ifdef USE_WIFI
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::espnow {
|
||||
|
||||
static constexpr const char *TAG = "espnow";
|
||||
|
||||
static const esp_err_t CONFIG_ESPNOW_WAKE_WINDOW = 50;
|
||||
static const esp_err_t CONFIG_ESPNOW_WAKE_INTERVAL = 100;
|
||||
|
||||
ESPNowComponent *global_esp_now = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static const LogString *espnow_error_to_str(esp_err_t error) {
|
||||
switch (error) {
|
||||
case ESP_ERR_ESPNOW_FAILED:
|
||||
return LOG_STR("ESPNow is in fail mode");
|
||||
case ESP_ERR_ESPNOW_OWN_ADDRESS:
|
||||
return LOG_STR("Message to your self");
|
||||
case ESP_ERR_ESPNOW_DATA_SIZE:
|
||||
return LOG_STR("Data size to large");
|
||||
case ESP_ERR_ESPNOW_PEER_NOT_SET:
|
||||
return LOG_STR("Peer address not set");
|
||||
case ESP_ERR_ESPNOW_PEER_NOT_PAIRED:
|
||||
return LOG_STR("Peer address not paired");
|
||||
case ESP_ERR_ESPNOW_NOT_INIT:
|
||||
return LOG_STR("Not init");
|
||||
case ESP_ERR_ESPNOW_ARG:
|
||||
return LOG_STR("Invalid argument");
|
||||
case ESP_ERR_ESPNOW_INTERNAL:
|
||||
return LOG_STR("Internal Error");
|
||||
case ESP_ERR_ESPNOW_NO_MEM:
|
||||
return LOG_STR("Our of memory");
|
||||
case ESP_ERR_ESPNOW_NOT_FOUND:
|
||||
return LOG_STR("Peer not found");
|
||||
case ESP_ERR_ESPNOW_IF:
|
||||
return LOG_STR("Interface does not match");
|
||||
case ESP_OK:
|
||||
return LOG_STR("OK");
|
||||
case ESP_NOW_SEND_FAIL:
|
||||
return LOG_STR("Failed");
|
||||
default:
|
||||
return LOG_STR("Unknown Error");
|
||||
}
|
||||
}
|
||||
|
||||
std::string peer_str(uint8_t *peer) {
|
||||
if (peer == nullptr || peer[0] == 0) {
|
||||
return "[Not Set]";
|
||||
} else if (memcmp(peer, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
|
||||
return "[Broadcast]";
|
||||
} else if (memcmp(peer, ESPNOW_MULTICAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
|
||||
return "[Multicast]";
|
||||
} else {
|
||||
return format_mac_address_pretty(peer);
|
||||
}
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status)
|
||||
#else
|
||||
void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
|
||||
#endif
|
||||
{
|
||||
// Allocate an event from the pool
|
||||
ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate();
|
||||
if (packet == nullptr) {
|
||||
// No events available - queue is full or we're out of memory
|
||||
global_esp_now->receive_packet_queue_.increment_dropped_count();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load new packet data (replaces previous packet)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
packet->load_sent_data(info->des_addr, status);
|
||||
#else
|
||||
packet->load_sent_data(mac_addr, status);
|
||||
#endif
|
||||
|
||||
// Push the packet to the queue
|
||||
global_esp_now->receive_packet_queue_.push(packet);
|
||||
// Push always because we're the only producer and the pool ensures we never exceed queue size
|
||||
}
|
||||
|
||||
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
|
||||
// Allocate an event from the pool
|
||||
ESPNowPacket *packet = global_esp_now->receive_packet_pool_.allocate();
|
||||
if (packet == nullptr) {
|
||||
// No events available - queue is full or we're out of memory
|
||||
global_esp_now->receive_packet_queue_.increment_dropped_count();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load new packet data (replaces previous packet)
|
||||
packet->load_received_data(info, data, size);
|
||||
|
||||
// Push the packet to the queue
|
||||
global_esp_now->receive_packet_queue_.push(packet);
|
||||
// Push always because we're the only producer and the pool ensures we never exceed queue size
|
||||
}
|
||||
|
||||
ESPNowComponent::ESPNowComponent() { global_esp_now = this; }
|
||||
|
||||
void ESPNowComponent::dump_config() {
|
||||
uint32_t version = 0;
|
||||
esp_now_get_version(&version);
|
||||
|
||||
ESP_LOGCONFIG(TAG, "espnow:");
|
||||
if (this->is_disabled()) {
|
||||
ESP_LOGCONFIG(TAG, " Disabled");
|
||||
return;
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Own address: %s\n"
|
||||
" Version: v%" PRIu32 "\n"
|
||||
" Wi-Fi channel: %d",
|
||||
format_mac_address_pretty(this->own_address_).c_str(), version, this->wifi_channel_);
|
||||
#ifdef USE_WIFI
|
||||
ESP_LOGCONFIG(TAG, " Wi-Fi enabled: %s", YESNO(this->is_wifi_enabled()));
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ESPNowComponent::is_wifi_enabled() {
|
||||
#ifdef USE_WIFI
|
||||
return wifi::global_wifi_component != nullptr && !wifi::global_wifi_component->is_disabled();
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
void ESPNowComponent::setup() {
|
||||
if (this->enable_on_boot_) {
|
||||
this->enable_();
|
||||
} else {
|
||||
this->state_ = ESPNOW_STATE_DISABLED;
|
||||
}
|
||||
}
|
||||
|
||||
void ESPNowComponent::enable() {
|
||||
if (this->state_ == ESPNOW_STATE_ENABLED)
|
||||
return;
|
||||
|
||||
ESP_LOGD(TAG, "Enabling");
|
||||
this->state_ = ESPNOW_STATE_OFF;
|
||||
|
||||
this->enable_();
|
||||
}
|
||||
|
||||
void ESPNowComponent::enable_() {
|
||||
if (!this->is_wifi_enabled()) {
|
||||
esp_event_loop_create_default();
|
||||
|
||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||
|
||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
|
||||
ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
|
||||
ESP_ERROR_CHECK(esp_wifi_start());
|
||||
ESP_ERROR_CHECK(esp_wifi_disconnect());
|
||||
|
||||
this->apply_wifi_channel();
|
||||
}
|
||||
this->get_wifi_channel();
|
||||
|
||||
esp_err_t err = esp_now_init();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_now_init failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_now_register_recv_cb(on_data_received);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
err = esp_now_register_send_cb(on_send_report);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_now_register_recv_cb failed: %s", esp_err_to_name(err));
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
esp_wifi_get_mac(WIFI_IF_STA, this->own_address_);
|
||||
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
esp_now_set_wake_window(CONFIG_ESPNOW_WAKE_WINDOW);
|
||||
esp_wifi_connectionless_module_set_wake_interval(CONFIG_ESPNOW_WAKE_INTERVAL);
|
||||
#endif
|
||||
|
||||
for (auto peer : this->peers_) {
|
||||
this->add_peer(peer.address);
|
||||
}
|
||||
|
||||
this->state_ = ESPNOW_STATE_ENABLED;
|
||||
}
|
||||
|
||||
void ESPNowComponent::disable() {
|
||||
if (this->state_ == ESPNOW_STATE_DISABLED)
|
||||
return;
|
||||
|
||||
ESP_LOGD(TAG, "Disabling");
|
||||
this->state_ = ESPNOW_STATE_DISABLED;
|
||||
|
||||
esp_now_unregister_recv_cb();
|
||||
esp_now_unregister_send_cb();
|
||||
|
||||
esp_err_t err = esp_now_deinit();
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "esp_now_deinit failed! 0x%x", err);
|
||||
}
|
||||
}
|
||||
|
||||
void ESPNowComponent::apply_wifi_channel() {
|
||||
if (this->state_ == ESPNOW_STATE_DISABLED) {
|
||||
ESP_LOGE(TAG, "Cannot set channel when ESPNOW disabled");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
if (this->is_wifi_enabled()) {
|
||||
ESP_LOGE(TAG, "Cannot set channel when Wi-Fi enabled");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "Channel set to %d.", this->wifi_channel_);
|
||||
esp_wifi_set_promiscuous(true);
|
||||
esp_wifi_set_channel(this->wifi_channel_, WIFI_SECOND_CHAN_NONE);
|
||||
esp_wifi_set_promiscuous(false);
|
||||
}
|
||||
|
||||
void ESPNowComponent::loop() {
|
||||
#ifdef USE_WIFI
|
||||
if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->is_connected()) {
|
||||
int32_t new_channel = wifi::global_wifi_component->get_wifi_channel();
|
||||
if (new_channel != this->wifi_channel_) {
|
||||
ESP_LOGI(TAG, "Wifi Channel is changed from %d to %d.", this->wifi_channel_, new_channel);
|
||||
this->wifi_channel_ = new_channel;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Process received packets
|
||||
ESPNowPacket *packet = this->receive_packet_queue_.pop();
|
||||
while (packet != nullptr) {
|
||||
switch (packet->type_) {
|
||||
case ESPNowPacket::RECEIVED: {
|
||||
const ESPNowRecvInfo info = packet->get_receive_info();
|
||||
if (!esp_now_is_peer_exist(info.src_addr)) {
|
||||
bool handled = false;
|
||||
for (auto *handler : this->unknown_peer_handlers_) {
|
||||
if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size)) {
|
||||
handled = true;
|
||||
break; // If a handler returns true, stop processing further handlers
|
||||
}
|
||||
}
|
||||
if (!handled && this->auto_add_peer_) {
|
||||
this->add_peer(info.src_addr);
|
||||
}
|
||||
}
|
||||
// Intentionally left as if instead of else in case the peer is added above
|
||||
if (esp_now_is_peer_exist(info.src_addr)) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG, "<<< [%s -> %s] %s", format_mac_address_pretty(info.src_addr).c_str(),
|
||||
format_mac_address_pretty(info.des_addr).c_str(),
|
||||
format_hex_pretty(packet->packet_.receive.data, packet->packet_.receive.size).c_str());
|
||||
#endif
|
||||
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
|
||||
for (auto *handler : this->broadcasted_handlers_) {
|
||||
if (handler->on_broadcasted(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
break; // If a handler returns true, stop processing further handlers
|
||||
}
|
||||
} else {
|
||||
for (auto *handler : this->received_handlers_) {
|
||||
if (handler->on_received(info, packet->packet_.receive.data, packet->packet_.receive.size))
|
||||
break; // If a handler returns true, stop processing further handlers
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESPNowPacket::SENT: {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
ESP_LOGV(TAG, ">>> [%s] %s", format_mac_address_pretty(packet->packet_.sent.address).c_str(),
|
||||
LOG_STR_ARG(espnow_error_to_str(packet->packet_.sent.status)));
|
||||
#endif
|
||||
if (this->current_send_packet_ != nullptr) {
|
||||
this->current_send_packet_->callback_(packet->packet_.sent.status);
|
||||
this->send_packet_pool_.release(this->current_send_packet_);
|
||||
this->current_send_packet_ = nullptr; // Reset current packet after sending
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
// Return the packet to the pool
|
||||
this->receive_packet_pool_.release(packet);
|
||||
packet = this->receive_packet_queue_.pop();
|
||||
}
|
||||
|
||||
// Process sending packet queue
|
||||
if (this->current_send_packet_ == nullptr) {
|
||||
this->send_();
|
||||
}
|
||||
|
||||
// Log dropped received packets periodically
|
||||
uint16_t received_dropped = this->receive_packet_queue_.get_and_reset_dropped_count();
|
||||
if (received_dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u received packets due to buffer overflow", received_dropped);
|
||||
}
|
||||
|
||||
// Log dropped send packets periodically
|
||||
uint16_t send_dropped = this->send_packet_queue_.get_and_reset_dropped_count();
|
||||
if (send_dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u send packets due to buffer overflow", send_dropped);
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t ESPNowComponent::get_wifi_channel() {
|
||||
wifi_second_chan_t dummy;
|
||||
esp_wifi_get_channel(&this->wifi_channel_, &dummy);
|
||||
return this->wifi_channel_;
|
||||
}
|
||||
|
||||
esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
|
||||
const send_callback_t &callback) {
|
||||
if (this->state_ != ESPNOW_STATE_ENABLED) {
|
||||
return ESP_ERR_ESPNOW_NOT_INIT;
|
||||
} else if (this->is_failed()) {
|
||||
return ESP_ERR_ESPNOW_FAILED;
|
||||
} else if (peer_address == 0ULL) {
|
||||
return ESP_ERR_ESPNOW_PEER_NOT_SET;
|
||||
} else if (memcmp(peer_address, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
|
||||
return ESP_ERR_ESPNOW_OWN_ADDRESS;
|
||||
} else if (size > ESP_NOW_MAX_DATA_LEN) {
|
||||
return ESP_ERR_ESPNOW_DATA_SIZE;
|
||||
} else if (!esp_now_is_peer_exist(peer_address)) {
|
||||
if (memcmp(peer_address, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0 || this->auto_add_peer_) {
|
||||
esp_err_t err = this->add_peer(peer_address);
|
||||
if (err != ESP_OK) {
|
||||
return err;
|
||||
}
|
||||
} else {
|
||||
return ESP_ERR_ESPNOW_PEER_NOT_PAIRED;
|
||||
}
|
||||
}
|
||||
// Allocate a packet from the pool
|
||||
ESPNowSendPacket *packet = this->send_packet_pool_.allocate();
|
||||
if (packet == nullptr) {
|
||||
this->send_packet_queue_.increment_dropped_count();
|
||||
ESP_LOGE(TAG, "Failed to allocate send packet from pool");
|
||||
this->status_momentary_warning("send-packet-pool-full");
|
||||
return ESP_ERR_ESPNOW_NO_MEM;
|
||||
}
|
||||
// Load the packet data
|
||||
packet->load_data(peer_address, payload, size, callback);
|
||||
// Push the packet to the send queue
|
||||
this->send_packet_queue_.push(packet);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
void ESPNowComponent::send_() {
|
||||
ESPNowSendPacket *packet = this->send_packet_queue_.pop();
|
||||
if (packet == nullptr) {
|
||||
return; // No packets to send
|
||||
}
|
||||
|
||||
this->current_send_packet_ = packet;
|
||||
esp_err_t err = esp_now_send(packet->address_, packet->data_, packet->size_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to send packet to %s - %s", format_mac_address_pretty(packet->address_).c_str(),
|
||||
LOG_STR_ARG(espnow_error_to_str(err)));
|
||||
if (packet->callback_ != nullptr) {
|
||||
packet->callback_(err);
|
||||
}
|
||||
this->status_momentary_warning("send-failed");
|
||||
this->send_packet_pool_.release(packet);
|
||||
this->current_send_packet_ = nullptr; // Reset current packet
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t ESPNowComponent::add_peer(const uint8_t *peer) {
|
||||
if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) {
|
||||
return ESP_ERR_ESPNOW_NOT_INIT;
|
||||
}
|
||||
|
||||
if (memcmp(peer, this->own_address_, ESP_NOW_ETH_ALEN) == 0) {
|
||||
this->mark_failed();
|
||||
return ESP_ERR_INVALID_MAC;
|
||||
}
|
||||
|
||||
if (!esp_now_is_peer_exist(peer)) {
|
||||
esp_now_peer_info_t peer_info = {};
|
||||
memset(&peer_info, 0, sizeof(esp_now_peer_info_t));
|
||||
peer_info.ifidx = WIFI_IF_STA;
|
||||
memcpy(peer_info.peer_addr, peer, ESP_NOW_ETH_ALEN);
|
||||
esp_err_t err = esp_now_add_peer(&peer_info);
|
||||
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to add peer %s - %s", format_mac_address_pretty(peer).c_str(),
|
||||
LOG_STR_ARG(espnow_error_to_str(err)));
|
||||
this->status_momentary_warning("peer-add-failed");
|
||||
return err;
|
||||
}
|
||||
}
|
||||
bool found = false;
|
||||
for (auto &it : this->peers_) {
|
||||
if (it == peer) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
ESPNowPeer new_peer;
|
||||
memcpy(new_peer.address, peer, ESP_NOW_ETH_ALEN);
|
||||
this->peers_.push_back(new_peer);
|
||||
}
|
||||
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t ESPNowComponent::del_peer(const uint8_t *peer) {
|
||||
if (this->state_ != ESPNOW_STATE_ENABLED || this->is_failed()) {
|
||||
return ESP_ERR_ESPNOW_NOT_INIT;
|
||||
}
|
||||
if (esp_now_is_peer_exist(peer)) {
|
||||
esp_err_t err = esp_now_del_peer(peer);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Failed to delete peer %s - %s", format_mac_address_pretty(peer).c_str(),
|
||||
LOG_STR_ARG(espnow_error_to_str(err)));
|
||||
this->status_momentary_warning("peer-del-failed");
|
||||
return err;
|
||||
}
|
||||
}
|
||||
for (auto it = this->peers_.begin(); it != this->peers_.end(); ++it) {
|
||||
if (*it == peer) {
|
||||
this->peers_.erase(it);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
#endif // USE_ESP32
|
183
esphome/components/espnow/espnow_component.h
Normal file
183
esphome/components/espnow/espnow_component.h
Normal file
@@ -0,0 +1,183 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/event_pool.h"
|
||||
#include "esphome/core/lock_free_queue.h"
|
||||
#include "espnow_packet.h"
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
#include <esp_mac.h>
|
||||
#include <esp_now.h>
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::espnow {
|
||||
|
||||
// Maximum size of the ESPNow event queue - must be power of 2 for lock-free queue
|
||||
static constexpr size_t MAX_ESP_NOW_SEND_QUEUE_SIZE = 16;
|
||||
static constexpr size_t MAX_ESP_NOW_RECEIVE_QUEUE_SIZE = 16;
|
||||
|
||||
using peer_address_t = std::array<uint8_t, ESP_NOW_ETH_ALEN>;
|
||||
|
||||
enum class ESPNowTriggers : uint8_t {
|
||||
TRIGGER_NONE = 0,
|
||||
ON_NEW_PEER = 1,
|
||||
ON_RECEIVED = 2,
|
||||
ON_BROADCASTED = 3,
|
||||
ON_SUCCEED = 10,
|
||||
ON_FAILED = 11,
|
||||
};
|
||||
|
||||
enum ESPNowState : uint8_t {
|
||||
/** Nothing has been initialized yet. */
|
||||
ESPNOW_STATE_OFF = 0,
|
||||
/** ESPNOW is disabled. */
|
||||
ESPNOW_STATE_DISABLED,
|
||||
/** ESPNOW is enabled. */
|
||||
ESPNOW_STATE_ENABLED,
|
||||
};
|
||||
|
||||
struct ESPNowPeer {
|
||||
uint8_t address[ESP_NOW_ETH_ALEN]; // MAC address of the peer
|
||||
|
||||
bool operator==(const ESPNowPeer &other) const { return memcmp(this->address, other.address, ESP_NOW_ETH_ALEN) == 0; }
|
||||
bool operator==(const uint8_t *other) const { return memcmp(this->address, other, ESP_NOW_ETH_ALEN) == 0; }
|
||||
};
|
||||
|
||||
/// Handler interface for receiving ESPNow packets from unknown peers
|
||||
/// Components should inherit from this class to handle incoming ESPNow data
|
||||
class ESPNowUnknownPeerHandler {
|
||||
public:
|
||||
/// Called when an ESPNow packet is received from an unknown peer
|
||||
/// @param info Information about the received packet (sender MAC, etc.)
|
||||
/// @param data Pointer to the received data payload
|
||||
/// @param size Size of the received data in bytes
|
||||
/// @return true if the packet was handled, false otherwise
|
||||
virtual bool on_unknown_peer(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
};
|
||||
|
||||
/// Handler interface for receiving ESPNow packets
|
||||
/// Components should inherit from this class to handle incoming ESPNow data
|
||||
class ESPNowReceivedPacketHandler {
|
||||
public:
|
||||
/// Called when an ESPNow packet is received
|
||||
/// @param info Information about the received packet (sender MAC, etc.)
|
||||
/// @param data Pointer to the received data payload
|
||||
/// @param size Size of the received data in bytes
|
||||
/// @return true if the packet was handled, false otherwise
|
||||
virtual bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
};
|
||||
/// Handler interface for receiving broadcasted ESPNow packets
|
||||
/// Components should inherit from this class to handle incoming ESPNow data
|
||||
class ESPNowBroadcastedHandler {
|
||||
public:
|
||||
/// Called when a broadcasted ESPNow packet is received
|
||||
/// @param info Information about the received packet (sender MAC, etc.)
|
||||
/// @param data Pointer to the received data payload
|
||||
/// @param size Size of the received data in bytes
|
||||
/// @return true if the packet was handled, false otherwise
|
||||
virtual bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
|
||||
};
|
||||
|
||||
class ESPNowComponent : public Component {
|
||||
public:
|
||||
ESPNowComponent();
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::LATE; }
|
||||
|
||||
// Add a peer to the internal list of peers
|
||||
void add_peer(peer_address_t address) {
|
||||
ESPNowPeer peer;
|
||||
memcpy(peer.address, address.data(), ESP_NOW_ETH_ALEN);
|
||||
this->peers_.push_back(peer);
|
||||
}
|
||||
// Add a peer with the esp_now api and add to the internal list if doesnt exist already
|
||||
esp_err_t add_peer(const uint8_t *peer);
|
||||
// Remove a peer with the esp_now api and remove from the internal list if exists
|
||||
esp_err_t del_peer(const uint8_t *peer);
|
||||
|
||||
void set_wifi_channel(uint8_t channel) { this->wifi_channel_ = channel; }
|
||||
void apply_wifi_channel();
|
||||
uint8_t get_wifi_channel();
|
||||
|
||||
void set_auto_add_peer(bool value) { this->auto_add_peer_ = value; }
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
bool is_disabled() const { return this->state_ == ESPNOW_STATE_DISABLED; };
|
||||
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
|
||||
bool is_wifi_enabled();
|
||||
|
||||
/// @brief Queue a packet to be sent to a specific peer address.
|
||||
/// This method will add the packet to the internal queue and
|
||||
/// call the callback when the packet is sent.
|
||||
/// Only one packet will be sent at any given time and the next one will not be sent until
|
||||
/// the previous one has been acknowledged or failed.
|
||||
/// @param peer_address MAC address of the peer to send the packet to
|
||||
/// @param payload Data payload to send
|
||||
/// @param callback Callback to call when the send operation is complete
|
||||
/// @return ESP_OK on success, or an error code on failure
|
||||
esp_err_t send(const uint8_t *peer_address, const std::vector<uint8_t> &payload,
|
||||
const send_callback_t &callback = nullptr) {
|
||||
return this->send(peer_address, payload.data(), payload.size(), callback);
|
||||
}
|
||||
esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
|
||||
const send_callback_t &callback = nullptr);
|
||||
|
||||
void register_received_handler(ESPNowReceivedPacketHandler *handler) { this->received_handlers_.push_back(handler); }
|
||||
void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) {
|
||||
this->unknown_peer_handlers_.push_back(handler);
|
||||
}
|
||||
void register_broadcasted_handler(ESPNowBroadcastedHandler *handler) {
|
||||
this->broadcasted_handlers_.push_back(handler);
|
||||
}
|
||||
|
||||
protected:
|
||||
friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size);
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
friend void on_send_report(const esp_now_send_info_t *info, esp_now_send_status_t status);
|
||||
#else
|
||||
friend void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status);
|
||||
#endif
|
||||
|
||||
void enable_();
|
||||
void send_();
|
||||
|
||||
std::vector<ESPNowUnknownPeerHandler *> unknown_peer_handlers_;
|
||||
std::vector<ESPNowReceivedPacketHandler *> received_handlers_;
|
||||
std::vector<ESPNowBroadcastedHandler *> broadcasted_handlers_;
|
||||
|
||||
std::vector<ESPNowPeer> peers_{};
|
||||
|
||||
uint8_t own_address_[ESP_NOW_ETH_ALEN]{0};
|
||||
LockFreeQueue<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_queue_{};
|
||||
EventPool<ESPNowPacket, MAX_ESP_NOW_RECEIVE_QUEUE_SIZE> receive_packet_pool_{};
|
||||
|
||||
LockFreeQueue<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_queue_{};
|
||||
EventPool<ESPNowSendPacket, MAX_ESP_NOW_SEND_QUEUE_SIZE> send_packet_pool_{};
|
||||
ESPNowSendPacket *current_send_packet_{nullptr}; // Currently sending packet, nullptr if none
|
||||
|
||||
uint8_t wifi_channel_{0};
|
||||
ESPNowState state_{ESPNOW_STATE_OFF};
|
||||
|
||||
bool auto_add_peer_{false};
|
||||
bool enable_on_boot_{true};
|
||||
};
|
||||
|
||||
extern ESPNowComponent *global_esp_now; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
#endif // USE_ESP32
|
19
esphome/components/espnow/espnow_err.h
Normal file
19
esphome/components/espnow/espnow_err.h
Normal file
@@ -0,0 +1,19 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_now.h>
|
||||
|
||||
namespace esphome::espnow {
|
||||
|
||||
static const esp_err_t ESP_ERR_ESPNOW_CMP_BASE = (ESP_ERR_ESPNOW_BASE + 20);
|
||||
static const esp_err_t ESP_ERR_ESPNOW_FAILED = (ESP_ERR_ESPNOW_CMP_BASE + 1);
|
||||
static const esp_err_t ESP_ERR_ESPNOW_OWN_ADDRESS = (ESP_ERR_ESPNOW_CMP_BASE + 2);
|
||||
static const esp_err_t ESP_ERR_ESPNOW_DATA_SIZE = (ESP_ERR_ESPNOW_CMP_BASE + 3);
|
||||
static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_SET = (ESP_ERR_ESPNOW_CMP_BASE + 4);
|
||||
static const esp_err_t ESP_ERR_ESPNOW_PEER_NOT_PAIRED = (ESP_ERR_ESPNOW_CMP_BASE + 5);
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
#endif // USE_ESP32
|
166
esphome/components/espnow/espnow_packet.h
Normal file
166
esphome/components/espnow/espnow_packet.h
Normal file
@@ -0,0 +1,166 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "espnow_err.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
#include <esp_err.h>
|
||||
#include <esp_idf_version.h>
|
||||
#include <esp_now.h>
|
||||
|
||||
namespace esphome::espnow {
|
||||
|
||||
static const uint8_t ESPNOW_BROADCAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF};
|
||||
static const uint8_t ESPNOW_MULTICAST_ADDR[ESP_NOW_ETH_ALEN] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE};
|
||||
|
||||
struct WifiPacketRxControl {
|
||||
int8_t rssi; // Received Signal Strength Indicator (RSSI) of packet, unit: dBm
|
||||
uint32_t timestamp; // Timestamp in microseconds when the packet was received, precise only if modem sleep or
|
||||
// light sleep is not enabled
|
||||
};
|
||||
|
||||
struct ESPNowRecvInfo {
|
||||
uint8_t src_addr[ESP_NOW_ETH_ALEN]; /**< Source address of ESPNOW packet */
|
||||
uint8_t des_addr[ESP_NOW_ETH_ALEN]; /**< Destination address of ESPNOW packet */
|
||||
wifi_pkt_rx_ctrl_t *rx_ctrl; /**< Rx control info of ESPNOW packet */
|
||||
};
|
||||
|
||||
using send_callback_t = std::function<void(esp_err_t)>;
|
||||
|
||||
class ESPNowPacket {
|
||||
public:
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
enum esp_now_packet_type_t : uint8_t {
|
||||
RECEIVED,
|
||||
SENT,
|
||||
};
|
||||
|
||||
// Constructor for received data
|
||||
ESPNowPacket(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
|
||||
this->init_received_data_(info, data, size);
|
||||
};
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
|
||||
// Constructor for sent data
|
||||
ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) {
|
||||
this->init_sent_data_(info->src_addr, status);
|
||||
}
|
||||
#else
|
||||
// Constructor for sent data
|
||||
ESPNowPacket(const uint8_t *mac_addr, esp_now_send_status_t status) { this->init_sent_data_(mac_addr, status); }
|
||||
#endif
|
||||
|
||||
// Default constructor for pre-allocation in pool
|
||||
ESPNowPacket() {}
|
||||
|
||||
void release() {}
|
||||
|
||||
void load_received_data(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
|
||||
this->type_ = RECEIVED;
|
||||
this->init_received_data_(info, data, size);
|
||||
}
|
||||
|
||||
void load_sent_data(const uint8_t *mac_addr, esp_now_send_status_t status) {
|
||||
this->type_ = SENT;
|
||||
this->init_sent_data_(mac_addr, status);
|
||||
}
|
||||
|
||||
// Disable copy to prevent double-delete
|
||||
ESPNowPacket(const ESPNowPacket &) = delete;
|
||||
ESPNowPacket &operator=(const ESPNowPacket &) = delete;
|
||||
|
||||
union {
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
struct received_data {
|
||||
ESPNowRecvInfo info; // Information about the received packet
|
||||
uint8_t data[ESP_NOW_MAX_DATA_LEN]; // Data received in the packet
|
||||
uint8_t size; // Size of the received data
|
||||
WifiPacketRxControl rx_ctrl; // Status of the received packet
|
||||
} receive;
|
||||
|
||||
// NOLINTNEXTLINE(readability-identifier-naming)
|
||||
struct sent_data {
|
||||
uint8_t address[ESP_NOW_ETH_ALEN];
|
||||
esp_now_send_status_t status;
|
||||
} sent;
|
||||
} packet_;
|
||||
|
||||
esp_now_packet_type_t type_;
|
||||
|
||||
esp_now_packet_type_t type() const { return this->type_; }
|
||||
const ESPNowRecvInfo &get_receive_info() const { return this->packet_.receive.info; }
|
||||
|
||||
private:
|
||||
void init_received_data_(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
|
||||
memcpy(this->packet_.receive.info.src_addr, info->src_addr, ESP_NOW_ETH_ALEN);
|
||||
memcpy(this->packet_.receive.info.des_addr, info->des_addr, ESP_NOW_ETH_ALEN);
|
||||
memcpy(this->packet_.receive.data, data, size);
|
||||
this->packet_.receive.size = size;
|
||||
|
||||
this->packet_.receive.rx_ctrl.rssi = info->rx_ctrl->rssi;
|
||||
this->packet_.receive.rx_ctrl.timestamp = info->rx_ctrl->timestamp;
|
||||
|
||||
this->packet_.receive.info.rx_ctrl = reinterpret_cast<wifi_pkt_rx_ctrl_t *>(&this->packet_.receive.rx_ctrl);
|
||||
}
|
||||
|
||||
void init_sent_data_(const uint8_t *mac_addr, esp_now_send_status_t status) {
|
||||
memcpy(this->packet_.sent.address, mac_addr, ESP_NOW_ETH_ALEN);
|
||||
this->packet_.sent.status = status;
|
||||
}
|
||||
};
|
||||
|
||||
class ESPNowSendPacket {
|
||||
public:
|
||||
ESPNowSendPacket(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &&callback)
|
||||
: callback_(callback) {
|
||||
this->init_data_(peer_address, payload, size);
|
||||
}
|
||||
ESPNowSendPacket(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
|
||||
this->init_data_(peer_address, payload, size);
|
||||
}
|
||||
|
||||
// Default constructor for pre-allocation in pool
|
||||
ESPNowSendPacket() {}
|
||||
|
||||
void release() {}
|
||||
|
||||
// Disable copy to prevent double-delete
|
||||
ESPNowSendPacket(const ESPNowSendPacket &) = delete;
|
||||
ESPNowSendPacket &operator=(const ESPNowSendPacket &) = delete;
|
||||
|
||||
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size, const send_callback_t &callback) {
|
||||
this->init_data_(peer_address, payload, size);
|
||||
this->callback_ = callback;
|
||||
}
|
||||
|
||||
void load_data(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
|
||||
this->init_data_(peer_address, payload, size);
|
||||
this->callback_ = nullptr; // Reset callback
|
||||
}
|
||||
|
||||
uint8_t address_[ESP_NOW_ETH_ALEN]{0}; // MAC address of the peer to send the packet to
|
||||
uint8_t data_[ESP_NOW_MAX_DATA_LEN]{0}; // Data to send
|
||||
uint8_t size_{0}; // Size of the data to send, must be <= ESP_NOW_MAX_DATA_LEN
|
||||
send_callback_t callback_{nullptr}; // Callback to call when the send operation is complete
|
||||
|
||||
private:
|
||||
void init_data_(const uint8_t *peer_address, const uint8_t *payload, size_t size) {
|
||||
memcpy(this->address_, peer_address, ESP_NOW_ETH_ALEN);
|
||||
if (size > ESP_NOW_MAX_DATA_LEN) {
|
||||
this->size_ = 0;
|
||||
return;
|
||||
}
|
||||
this->size_ = size;
|
||||
memcpy(this->data_, payload, this->size_);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace esphome::espnow
|
||||
|
||||
#endif // USE_ESP32
|
@@ -145,5 +145,4 @@ async def event_fire_to_code(config, action_id, template_arg, args):
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_EVENT")
|
||||
cg.add_global(event_ns.using)
|
||||
|
@@ -400,5 +400,4 @@ async def fan_is_on_off_to_code(config, condition_id, template_arg, args):
|
||||
|
||||
@coroutine_with_priority(100.0)
|
||||
async def to_code(config):
|
||||
cg.add_define("USE_FAN")
|
||||
cg.add_global(fan_ns.using)
|
||||
|
@@ -4,6 +4,13 @@
|
||||
#pragma once
|
||||
|
||||
namespace esphome {
|
||||
|
||||
#ifdef USE_API
|
||||
namespace api {
|
||||
class APIConnection;
|
||||
} // namespace api
|
||||
#endif
|
||||
|
||||
namespace fan {
|
||||
|
||||
class FanTraits {
|
||||
@@ -36,6 +43,15 @@ class FanTraits {
|
||||
bool supports_preset_modes() const { return !this->preset_modes_.empty(); }
|
||||
|
||||
protected:
|
||||
#ifdef USE_API
|
||||
// The API connection is a friend class to access internal methods
|
||||
friend class api::APIConnection;
|
||||
// This method returns a reference to the internal preset modes set.
|
||||
// It is used by the API to avoid copying data when encoding messages.
|
||||
// Warning: Do not use this method outside of the API connection code.
|
||||
// It returns a reference to internal data that can be invalidated.
|
||||
const std::set<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; }
|
||||
#endif
|
||||
bool oscillation_{false};
|
||||
bool speed_{false};
|
||||
bool direction_{false};
|
||||
|
@@ -15,6 +15,7 @@ from freetype import (
|
||||
FT_LOAD_RENDER,
|
||||
FT_LOAD_TARGET_MONO,
|
||||
Face,
|
||||
FT_Exception,
|
||||
ft_pixel_mode_mono,
|
||||
)
|
||||
import requests
|
||||
@@ -94,7 +95,14 @@ class FontCache(MutableMapping):
|
||||
return self.store[self._keytransform(item)]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.store[self._keytransform(key)] = Face(str(value))
|
||||
transformed = self._keytransform(key)
|
||||
try:
|
||||
self.store[transformed] = Face(str(value))
|
||||
except FT_Exception as exc:
|
||||
file = transformed.split(":", 1)
|
||||
raise cv.Invalid(
|
||||
f"{file[0].capitalize()} {file[1]} is not a valid font file"
|
||||
) from exc
|
||||
|
||||
|
||||
FONT_CACHE = FontCache()
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user