Compare commits

..

2 Commits

Author SHA1 Message Date
J. Nick Koston
a30786b055 clamp 2025-11-28 21:19:47 -06:00
J. Nick Koston
192abf95ce [fan] Use uint8_t for speed_count and fix tuya max=256 validation bug 2025-11-28 21:07:02 -06:00
1262 changed files with 15355 additions and 39305 deletions

View File

@@ -276,12 +276,12 @@ This document provides essential context for AI models interacting with this pro
## 7. Specific Instructions for AI Collaboration
* **Contribution Workflow (Pull Request Process):**
1. **Fork & Branch:** Create a new branch based on the `dev` branch (always use `git checkout -b <branch-name> dev` to ensure you're branching from `dev`, not the currently checked out branch).
1. **Fork & Branch:** Create a new branch in your fork.
2. **Make Changes:** Adhere to all coding conventions and patterns.
3. **Test:** Create component tests for all supported platforms and run the full test suite locally.
4. **Lint:** Run `pre-commit` to ensure code is compliant.
5. **Commit:** Commit your changes. There is no strict format for commit messages.
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made using the `.github/PULL_REQUEST_TEMPLATE.md` template - fill out all sections completely without removing any parts of the template.
6. **Pull Request:** Submit a PR against the `dev` branch. The Pull Request title should have a prefix of the component being worked on (e.g., `[display] Fix bug`, `[abc123] Add new component`). Update documentation, examples, and add `CODEOWNERS` entries as needed. Pull requests should always be made with the PULL_REQUEST_TEMPLATE.md template filled out correctly.
* **Documentation Contributions:**
* Documentation is hosted in the separate `esphome/esphome-docs` repository.
@@ -402,45 +402,35 @@ This document provides essential context for AI models interacting with this pro
_use_feature = True
```
**Bad Pattern (Flat Keys):**
**Good Pattern (CORE.data with Helpers):**
```python
# Don't do this - keys should be namespaced under component domain
MY_FEATURE_KEY = "my_component_feature"
CORE.data[MY_FEATURE_KEY] = True
```
**Good Pattern (dataclass):**
```python
from dataclasses import dataclass, field
from esphome.core import CORE
DOMAIN = "my_component"
# Keys for CORE.data storage
COMPONENT_STATE_KEY = "my_component_state"
USE_FEATURE_KEY = "my_component_use_feature"
@dataclass
class MyComponentData:
feature_enabled: bool = False
item_count: int = 0
items: list[str] = field(default_factory=list)
def _get_component_state() -> list:
"""Get component state from CORE.data."""
return CORE.data.setdefault(COMPONENT_STATE_KEY, [])
def _get_data() -> MyComponentData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = MyComponentData()
return CORE.data[DOMAIN]
def _get_use_feature() -> bool | None:
"""Get feature flag from CORE.data."""
return CORE.data.get(USE_FEATURE_KEY)
def request_feature() -> None:
_get_data().feature_enabled = True
def _set_use_feature(value: bool) -> None:
"""Set feature flag in CORE.data."""
CORE.data[USE_FEATURE_KEY] = value
def add_item(item: str) -> None:
_get_data().items.append(item)
def enable_feature():
_set_use_feature(True)
```
If you need a real-world example, search for components that use `@dataclass` with `CORE.data` in the codebase. Note: Some components may use `TypedDict` for dictionary-based storage; both patterns are acceptable depending on your needs.
**Why this matters:**
- Module-level globals persist between compilation runs if the dashboard doesn't fork/exec
- `CORE.data` automatically clears between runs
- Namespacing under `DOMAIN` prevents key collisions between components
- `@dataclass` provides type safety and cleaner attribute access
- Typed helper functions provide better IDE support and maintainability
- Encapsulation makes state management explicit and testable
* **Security:** Be mindful of security when making changes to the API, web server, or any other network-related code. Do not hardcode secrets or keys.

View File

@@ -1 +1 @@
191a0e6ab5842d153dd77a2023bc5742f9d4333c334de8d81b57f2b8d4d4b65e
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c

View File

@@ -22,7 +22,7 @@ runs:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: venv
# yamllint disable-line rule:line-length

View File

@@ -22,11 +22,11 @@ jobs:
if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
uses: actions/create-github-app-token@7e473efe3cb98aa54f8d4bac15400b15fad77d94 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
@@ -62,7 +62,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: generated-proto-files
path: |

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0

View File

@@ -43,13 +43,13 @@ jobs:
- "docker"
# - "lint"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Set TAG
run: |

View File

@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo

View File

@@ -36,7 +36,7 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
@@ -47,7 +47,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: venv
# yamllint disable-line rule:line-length
@@ -70,7 +70,7 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -91,7 +91,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -132,7 +132,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
@@ -152,12 +152,12 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
with:
token: ${{ secrets.CODECOV_TOKEN }}
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -183,7 +183,7 @@ jobs:
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
# Fetch enough history to find the merge base
fetch-depth: 2
@@ -193,7 +193,7 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -223,7 +223,7 @@ jobs:
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -237,7 +237,7 @@ jobs:
if: needs.determine-jobs.outputs.integration-tests == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python 3.13
id: python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
@@ -245,7 +245,7 @@ jobs:
python-version: "3.13"
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -273,7 +273,7 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
@@ -321,7 +321,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -334,14 +334,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
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@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
@@ -400,7 +400,7 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -413,14 +413,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -489,7 +489,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -502,14 +502,14 @@ jobs:
- name: Cache platformio
if: github.ref == 'refs/heads/dev'
uses: actions/cache@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
@@ -577,7 +577,7 @@ jobs:
version: 1.0
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -662,13 +662,13 @@ jobs:
if: github.event_name == 'pull_request' && !startsWith(github.base_ref, 'beta') && !startsWith(github.base_ref, 'release')
steps:
- name: Check out code from GitHub
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
- uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
env:
SKIP: pylint,clang-tidy-hash
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
@@ -688,7 +688,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
ref: ${{ github.base_ref }}
@@ -735,7 +735,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -759,7 +759,7 @@ jobs:
- name: Cache platformio
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -800,7 +800,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -821,7 +821,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -840,14 +840,14 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache platformio
uses: actions/cache/restore@9255dc7a253b0ccc959486e2bca901246202afeb # v5.0.1
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.platformio
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
@@ -885,7 +885,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -908,20 +908,20 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
name: memory-analysis-pr
path: ./memory-analysis
@@ -959,13 +959,13 @@ jobs:
- memory-impact-comment
if: always()
steps:
- name: Check job results
- name: Success
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: exit 0
- name: Failure
if: ${{ contains(needs.*.result, 'failure') }}
env:
NEEDS_JSON: ${{ toJSON(needs) }}
JSON_DOC: ${{ toJSON(needs) }}
run: |
# memory-impact-target-branch is allowed to fail without blocking CI.
# This job builds the target branch (dev/beta/release) which may fail because:
# 1. The target branch has a build issue independent of this PR
# 2. This PR fixes a build issue on the target branch
# In either case, we only care that the PR branch builds successfully.
echo "$NEEDS_JSON" | jq -e 'del(.["memory-impact-target-branch"]) | all(.result != "failure")'
echo $JSON_DOC | jq
exit 1

View File

@@ -54,11 +54,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/init@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/analyze@fdbfb4d2750291e159f0156def62b853c2798ca2 # v4.31.5
with:
category: "/language:${{matrix.language}}"

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Get tag
id: tag
# yamllint disable rule:line-length
@@ -60,7 +60,7 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
@@ -92,14 +92,14 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up Python
uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0
with:
python-version: "3.11"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to docker hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
@@ -138,7 +138,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -168,17 +168,17 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Download digests
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
@@ -219,19 +219,10 @@ jobs:
- init
- deploy-manifest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: home-assistant-addon
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
github-token: ${{ secrets.DEPLOY_HA_ADDON_REPO_TOKEN }}
script: |
let description = "ESPHome";
if (context.eventName == "release") {
@@ -254,19 +245,10 @@ jobs:
needs: [init]
environment: ${{ needs.init.outputs.deploy_env }}
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: esphome-schema
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
github-token: ${{ secrets.DEPLOY_ESPHOME_SCHEMA_REPO_TOKEN }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
@@ -277,34 +259,3 @@ jobs:
version: "${{ needs.init.outputs.tag }}",
}
})
version-notifier:
if: github.repository == 'esphome/esphome' && needs.init.outputs.branch_build == 'false'
runs-on: ubuntu-latest
needs:
- init
- deploy-manifest
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: version-notifier
- name: Trigger Workflow
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
github.rest.actions.createWorkflowDispatch({
owner: "esphome",
repo: "version-notifier",
workflow_id: "notify.yml",
ref: "main",
inputs: {
version: "${{ needs.init.outputs.tag }}",
}
})

View File

@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Stale
uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
with:
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
remove-stale-when-updated: true

View File

@@ -13,10 +13,10 @@ jobs:
if: github.repository == 'esphome/esphome'
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Checkout Home Assistant
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
with:
repository: home-assistant/core
path: lib/home-assistant
@@ -41,7 +41,7 @@ jobs:
python script/run-in-env.py pre-commit run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@98357b18bf14b5342f975ff684046ec3b2a07725 # v8.0.0
uses: peter-evans/create-pull-request@84ae59a2cdc2258d6fa0732dd66352dddae2a412 # v7.0.9
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphomebot <esphome@openhomefoundation.org>

4
.gitignore vendored
View File

@@ -91,10 +91,6 @@ venv-*/
# mypy
.mypy_cache/
# nix
/default.nix
/shell.nix
.pioenvs
.piolibdeps
.pio

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.10
rev: v0.14.5
hooks:
# Run the linter.
- id: ruff

View File

@@ -21,7 +21,6 @@ esphome/components/adc128s102/* @DeerMaximum
esphome/components/addressable_light/* @justfalter
esphome/components/ade7880/* @kpfleming
esphome/components/ade7953/* @angelnu
esphome/components/ade7953_base/* @angelnu
esphome/components/ade7953_i2c/* @angelnu
esphome/components/ade7953_spi/* @angelnu
esphome/components/ads1118/* @solomondg1
@@ -42,7 +41,6 @@ esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/apds9306/* @aodrenah
esphome/components/api/* @esphome/core
esphome/components/aqi/* @freekode @jasstrong @ximex
esphome/components/as5600/* @ammmze
esphome/components/as5600/sensor/* @ammmze
esphome/components/as7341/* @mrgnr
@@ -91,7 +89,6 @@ esphome/components/bmp3xx_spi/* @latonita
esphome/components/bmp581/* @kahrendt
esphome/components/bp1658cj/* @Cossid
esphome/components/bp5758d/* @Cossid
esphome/components/bthome_mithermometer/* @nagyrobi
esphome/components/button/* @esphome/core
esphome/components/bytebuffer/* @clydebarrow
esphome/components/camera/* @bdraco @DT-art1
@@ -99,7 +96,6 @@ esphome/components/camera_encoder/* @DT-art1
esphome/components/canbus/* @danielschramm @mvturnho
esphome/components/cap1188/* @mreditor97
esphome/components/captive_portal/* @esphome/core
esphome/components/cc1101/* @gabest11 @lygris
esphome/components/ccs811/* @habbie
esphome/components/cd74hc4067/* @asoehlke
esphome/components/ch422g/* @clydebarrow @jesterret
@@ -135,7 +131,7 @@ esphome/components/display_menu_base/* @numo68
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its
esphome/components/dsmr/* @glmnet @PolarGoose @zuidwijk
esphome/components/dsmr/* @glmnet @zuidwijk
esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/touchscreen/* @jesserockz
@@ -193,7 +189,6 @@ esphome/components/gps/* @coogle @ximex
esphome/components/graph/* @synco
esphome/components/graphical_display_menu/* @MrMDavidson
esphome/components/gree/* @orestismers
esphome/components/gree/switch/* @nagyrobi
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
esphome/components/grove_tb6612fng/* @max246
esphome/components/growatt_solar/* @leeuwte
@@ -214,10 +209,8 @@ esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
esphome/components/hlk_fm22x/* @OnFreund
esphome/components/hlw8032/* @rici4kubicek
esphome/components/hm3301/* @freekode
esphome/components/hmac_md5/* @dwmw2
esphome/components/hmac_sha256/* @dwmw2
esphome/components/homeassistant/* @esphome/core @OttoWinter
esphome/components/homeassistant/number/* @landonr
esphome/components/homeassistant/switch/* @Links2004
@@ -231,7 +224,6 @@ esphome/components/hte501/* @Stock-M
esphome/components/http_request/ota/* @oarcher
esphome/components/http_request/update/* @jesserockz
esphome/components/htu31d/* @betterengineering
esphome/components/hub75/* @stuartparmenter
esphome/components/hydreon_rgxx/* @functionpointer
esphome/components/hyt271/* @Philippe12
esphome/components/i2c/* @esphome/core
@@ -311,7 +303,7 @@ esphome/components/md5/* @esphome/core
esphome/components/mdns/* @esphome/core
esphome/components/media_player/* @jesserockz
esphome/components/micro_wake_word/* @jesserockz @kahrendt
esphome/components/micronova/* @edenhaus @jorre05
esphome/components/micronova/* @jorre05
esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov
@@ -395,7 +387,6 @@ esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet
esphome/components/rc522_i2c/* @glmnet
esphome/components/rc522_spi/* @glmnet
esphome/components/rd03d/* @jasstrong
esphome/components/resampler/speaker/* @kahrendt
esphome/components/restart/* @esphome/core
esphome/components/rf_bridge/* @jesserockz
@@ -521,7 +512,6 @@ esphome/components/tuya/switch/* @jesserockz
esphome/components/tuya/text_sensor/* @dentra
esphome/components/uart/* @esphome/core
esphome/components/uart/button/* @ssieb
esphome/components/uart/event/* @eoasmxd
esphome/components/uart/packet_transport/* @clydebarrow
esphome/components/udp/* @clydebarrow
esphome/components/ufire_ec/* @pvizeli
@@ -529,7 +519,6 @@ esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter
esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_cdc_acm/* @kbx81
esphome/components/usb_host/* @clydebarrow
esphome/components/usb_uart/* @clydebarrow
esphome/components/valve/* @esphome/core
@@ -540,7 +529,6 @@ esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/water_heater/* @dhoeben
esphome/components/waveshare_epaper/* @clydebarrow
esphome/components/web_server/ota/* @esphome/core
esphome/components/web_server_base/* @esphome/core
@@ -576,6 +564,5 @@ esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68
esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zigbee/* @tomaszduda23
esphome/components/zio_ultrasonic/* @kahrendt
esphome/components/zwave_proxy/* @kbx81

View File

@@ -2,7 +2,7 @@
We welcome contributions to the ESPHome suite of code and documentation!
Please read our [contributing guide](https://developers.esphome.io/contributing/code/) if you wish to contribute to the
Please read our [contributing guide](https://esphome.io/guides/contributing.html) if you wish to contribute to the
project and be sure to join us on [Discord](https://discord.gg/KhAMKrd).
**See also:**

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.1.0-dev
PROJECT_NUMBER = 2025.12.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -1,7 +1,6 @@
include LICENSE
include README.md
include requirements.txt
recursive-include esphome *.yaml
recursive-include esphome *.cpp *.h *.tcc *.c
recursive-include esphome *.py.script
recursive-include esphome LICENSE.txt

View File

@@ -2,8 +2,8 @@
<a href="https://esphome.io/">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://media.esphome.io/logo/logo-text-on-dark.svg">
<img src="https://media.esphome.io/logo/logo-text-on-light.svg" alt="ESPHome Logo">
<source media="(prefers-color-scheme: dark)" srcset="https://esphome.io/_static/logo-text-on-dark.svg", alt="ESPHome Logo">
<img src="https://esphome.io/_static/logo-text-on-light.svg" alt="ESPHome Logo">
</picture>
</a>

View File

@@ -11,16 +11,6 @@ FROM base-source-${BUILD_TYPE} AS base
RUN git config --system --add safe.directory "*"
# Install build tools for Python packages that require compilation
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
RUN if command -v apk > /dev/null; then \
apk add --no-cache build-base; \
else \
apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*; \
fi
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
RUN pip install --no-cache-dir -U pip uv==0.6.14

View File

@@ -62,9 +62,6 @@ from esphome.util import (
_LOGGER = logging.getLogger(__name__)
# Maximum buffer size for serial log reading to prevent unbounded memory growth
SERIAL_BUFFER_MAX_SIZE = 65536
# Special non-component keys that appear in configs
_NON_COMPONENT_KEYS = frozenset(
{
@@ -434,37 +431,25 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
while tries < 5:
try:
with ser:
buffer = b""
ser.timeout = 0.1 # 100ms timeout for non-blocking reads
while True:
try:
# Read all available data and timestamp it
chunk = ser.read(ser.in_waiting or 1)
if not chunk:
continue
time_ = datetime.now()
milliseconds = time_.microsecond // 1000
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{milliseconds:03}]"
# Add to buffer and process complete lines
# Limit buffer size to prevent unbounded memory growth
# if device sends data without newlines
buffer += chunk
if len(buffer) > SERIAL_BUFFER_MAX_SIZE:
buffer = buffer[-SERIAL_BUFFER_MAX_SIZE:]
while b"\n" in buffer:
raw_line, buffer = buffer.split(b"\n", 1)
line = raw_line.replace(b"\r", b"").decode(
"utf8", "backslashreplace"
)
safe_print(parser.parse_line(line, time_str))
backtrace_state = platformio_api.process_stacktrace(
config, line, backtrace_state=backtrace_state
)
raw = ser.readline()
except serial.SerialException:
_LOGGER.error("Serial port closed!")
return 0
line = (
raw.replace(b"\r", b"")
.replace(b"\n", b"")
.decode("utf8", "backslashreplace")
)
time_ = datetime.now()
nanoseconds = time_.microsecond // 1000
time_str = f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{nanoseconds:03}]"
safe_print(parser.parse_line(line, time_str))
backtrace_state = platformio_api.process_stacktrace(
config, line, backtrace_state=backtrace_state
)
except serial.SerialException:
tries += 1
time.sleep(1)
@@ -533,49 +518,10 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
rc = platformio_api.run_compile(config, CORE.verbose)
if rc != 0:
return rc
# Check if firmware was rebuilt and emit build_info + create manifest
_check_and_emit_build_info()
idedata = platformio_api.get_idedata(config)
return 0 if idedata is not None else 1
def _check_and_emit_build_info() -> None:
"""Check if firmware was rebuilt and emit build_info."""
import json
firmware_path = CORE.firmware_bin
build_info_json_path = CORE.relative_build_path("build_info.json")
# Check if both files exist
if not firmware_path.exists() or not build_info_json_path.exists():
return
# Check if firmware is newer than build_info (indicating a relink occurred)
if firmware_path.stat().st_mtime <= build_info_json_path.stat().st_mtime:
return
# Read build_info from JSON
try:
with open(build_info_json_path, encoding="utf-8") as f:
build_info = json.load(f)
except (OSError, json.JSONDecodeError) as e:
_LOGGER.debug("Failed to read build_info: %s", e)
return
config_hash = build_info.get("config_hash")
build_time_str = build_info.get("build_time_str")
if config_hash is None or build_time_str is None:
return
# Emit build_info with human-readable time
_LOGGER.info(
"Build Info: config_hash=0x%08x build_time_str=%s", config_hash, build_time_str
)
def upload_using_esptool(
config: ConfigType, port: str, file: str, speed: int
) -> str | int:
@@ -804,13 +750,7 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
exit_code = compile_program(args, config)
if exit_code != 0:
return exit_code
if CORE.is_host:
from esphome.platformio_api import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Successfully compiled program to path '%s'", program_path)
else:
_LOGGER.info("Successfully compiled program.")
_LOGGER.info("Successfully compiled program.")
return 0
@@ -860,8 +800,10 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
if CORE.is_host:
from esphome.platformio_api import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Running program from path '%s'", program_path)
idedata = get_idedata(config)
if idedata is None:
return 1
program_path = idedata.raw["prog_path"]
return run_external_process(program_path)
# Get devices, resolving special identifiers like OTA
@@ -1002,7 +944,6 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
"""
from esphome import platformio_api
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from esphome.analyze_memory.ram_strings import RamStringsAnalyzer
# Always compile to ensure fresh data (fast if no changes - just relinks)
exit_code = write_cpp(config)
@@ -1025,39 +966,21 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
external_components = detect_external_components(config)
_LOGGER.debug("Detected external components: %s", external_components)
# Perform component memory analysis
# Perform memory analysis
_LOGGER.info("Analyzing memory usage...")
analyzer = MemoryAnalyzerCLI(
str(firmware_elf),
idedata.objdump_path,
idedata.readelf_path,
external_components,
idedata=idedata,
)
analyzer.analyze()
# Generate and display component report
# Generate and display report
report = analyzer.generate_report()
print()
print(report)
# Perform RAM strings analysis
_LOGGER.info("Analyzing RAM strings...")
try:
ram_analyzer = RamStringsAnalyzer(
str(firmware_elf),
objdump_path=idedata.objdump_path,
platform=CORE.target_platform,
)
ram_analyzer.analyze()
# Generate and display RAM strings report
ram_report = ram_analyzer.generate_report()
print()
print(ram_report)
except Exception as e: # pylint: disable=broad-except
_LOGGER.warning("RAM strings analysis failed: %s", e)
return 0

View File

@@ -15,20 +15,27 @@ from .const import (
SECTION_TO_ATTR,
SYMBOL_PATTERNS,
)
from .demangle import batch_demangle
from .helpers import (
get_component_class_patterns,
get_esphome_components,
map_section_name,
parse_symbol_line,
)
from .toolchain import find_tool, run_tool
if TYPE_CHECKING:
from esphome.platformio_api import IDEData
_LOGGER = logging.getLogger(__name__)
# GCC global constructor/destructor prefix annotations
_GCC_PREFIX_ANNOTATIONS = {
"_GLOBAL__sub_I_": "global constructor for",
"_GLOBAL__sub_D_": "global destructor for",
}
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
# C++ runtime patterns for categorization
_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"])
@@ -54,9 +61,6 @@ _NAMESPACE_STD = "std::"
# Type alias for symbol information: (symbol_name, size, component)
SymbolInfoType = tuple[str, int, str]
# RAM sections - symbols in these sections consume RAM
RAM_SECTIONS = frozenset([".data", ".bss"])
@dataclass
class MemorySection:
@@ -64,20 +68,7 @@ class MemorySection:
name: str
symbols: list[SymbolInfoType] = field(default_factory=list)
total_size: int = 0 # Actual section size from ELF headers
symbol_size: int = 0 # Sum of symbol sizes (may be less than total_size)
@dataclass
class SDKSymbol:
"""Represents a symbol from an SDK library that's not in the ELF symbol table."""
name: str
size: int
library: str # Name of the .a file (e.g., "libpp.a")
section: str # ".bss" or ".data"
is_local: bool # True if static/local symbol (lowercase in nm output)
demangled: str = "" # Demangled name (populated after analysis)
total_size: int = 0
@dataclass
@@ -135,10 +126,6 @@ class MemoryAnalyzer:
self.objdump_path = objdump_path or "objdump"
self.readelf_path = readelf_path or "readelf"
self.external_components = external_components or set()
self._idedata = idedata
# Derive nm path from objdump path using shared toolchain utility
self.nm_path = find_tool("nm", self.objdump_path)
self.sections: dict[str, MemorySection] = {}
self.components: dict[str, ComponentMemory] = defaultdict(
@@ -149,25 +136,15 @@ class MemoryAnalyzer:
self._esphome_core_symbols: list[
tuple[str, str, int]
] = [] # Track core symbols
# Track symbols for all components: (symbol_name, demangled, size, section)
self._component_symbols: dict[str, list[tuple[str, str, int, str]]] = (
defaultdict(list)
)
# Track RAM symbols separately for detailed analysis: (symbol_name, demangled, size, section)
self._ram_symbols: dict[str, list[tuple[str, str, int, str]]] = defaultdict(
self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict(
list
)
# Track ELF symbol names for SDK cross-reference
self._elf_symbol_names: set[str] = set()
# SDK symbols not in ELF (static/local symbols from closed-source libs)
self._sdk_symbols: list[SDKSymbol] = []
) # Track symbols for all components
def analyze(self) -> dict[str, ComponentMemory]:
"""Analyze the ELF file and return component memory usage."""
self._parse_sections()
self._parse_symbols()
self._categorize_symbols()
self._analyze_sdk_libraries()
return dict(self.components)
def _parse_sections(self) -> None:
@@ -221,8 +198,6 @@ class MemoryAnalyzer:
continue
self.sections[section].symbols.append((name, size, ""))
self.sections[section].symbol_size += size
self._elf_symbol_names.add(name)
seen_addresses.add(address)
def _categorize_symbols(self) -> None:
@@ -266,13 +241,8 @@ class MemoryAnalyzer:
if size > 0:
demangled = self._demangle_symbol(symbol_name)
self._component_symbols[component].append(
(symbol_name, demangled, size, section_name)
(symbol_name, demangled, size)
)
# Track RAM symbols separately for detailed RAM analysis
if section_name in RAM_SECTIONS:
self._ram_symbols[component].append(
(symbol_name, demangled, size, section_name)
)
def _identify_component(self, symbol_name: str) -> str:
"""Identify which component a symbol belongs to."""
@@ -342,9 +312,168 @@ class MemoryAnalyzer:
if not symbols:
return
# Try to find the appropriate c++filt for the platform
cppfilt_cmd = "c++filt"
_LOGGER.info("Demangling %d symbols", len(symbols))
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path)
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
_LOGGER.debug("objdump_path = %s", self.objdump_path)
# Check if we have a toolchain-specific c++filt
if self.objdump_path and self.objdump_path != "objdump":
# Replace objdump with c++filt in the path
potential_cppfilt = self.objdump_path.replace("objdump", "c++filt")
_LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt)
if Path(potential_cppfilt).exists():
cppfilt_cmd = potential_cppfilt
_LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd)
else:
_LOGGER.info(
"✗ Toolchain c++filt not found at %s, using system c++filt",
potential_cppfilt,
)
else:
_LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path)
# Strip GCC optimization suffixes and prefixes before demangling
# Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt
# Prefixes like _GLOBAL__sub_I_ need to be removed and tracked
symbols_stripped: list[str] = []
symbols_prefixes: list[str] = [] # Track removed prefixes
for symbol in symbols:
# Remove GCC optimization markers
stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
# Handle GCC global constructor/initializer prefixes
# _GLOBAL__sub_I_<mangled> -> extract <mangled> for demangling
prefix = ""
for gcc_prefix in _GCC_PREFIX_ANNOTATIONS:
if stripped.startswith(gcc_prefix):
prefix = gcc_prefix
stripped = stripped[len(prefix) :]
break
symbols_stripped.append(stripped)
symbols_prefixes.append(prefix)
try:
# Send all symbols to c++filt at once
result = subprocess.run(
[cppfilt_cmd],
input="\n".join(symbols_stripped),
capture_output=True,
text=True,
check=False,
)
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
# On error, cache originals
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
for symbol in symbols:
self._demangle_cache[symbol] = symbol
return
if result.returncode != 0:
_LOGGER.warning(
"c++filt exited with code %d: %s",
result.returncode,
result.stderr[:200] if result.stderr else "(no error output)",
)
# Cache originals on failure
for symbol in symbols:
self._demangle_cache[symbol] = symbol
return
# Process demangled output
self._process_demangled_output(
symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd
)
def _process_demangled_output(
self,
symbols: list[str],
symbols_stripped: list[str],
symbols_prefixes: list[str],
demangled_output: str,
cppfilt_cmd: str,
) -> None:
"""Process demangled symbol output and populate cache.
Args:
symbols: Original symbol names
symbols_stripped: Stripped symbol names sent to c++filt
symbols_prefixes: Removed prefixes to restore
demangled_output: Output from c++filt
cppfilt_cmd: Path to c++filt command (for logging)
"""
demangled_lines = demangled_output.strip().split("\n")
failed_count = 0
for original, stripped, prefix, demangled in zip(
symbols, symbols_stripped, symbols_prefixes, demangled_lines
):
# Add back any prefix that was removed
demangled = self._restore_symbol_prefix(prefix, stripped, demangled)
# If we stripped a suffix, add it back to the demangled name for clarity
if original != stripped and not prefix:
demangled = self._restore_symbol_suffix(original, demangled)
self._demangle_cache[original] = demangled
# Log symbols that failed to demangle (stayed the same as stripped version)
if stripped == demangled and stripped.startswith("_Z"):
failed_count += 1
if failed_count <= 5: # Only log first 5 failures
_LOGGER.warning("Failed to demangle: %s", original)
if failed_count == 0:
_LOGGER.info("Successfully demangled all %d symbols", len(symbols))
return
_LOGGER.warning(
"Failed to demangle %d/%d symbols using %s",
failed_count,
len(symbols),
cppfilt_cmd,
)
@staticmethod
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
"""Restore prefix that was removed before demangling.
Args:
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
stripped: Stripped symbol name
demangled: Demangled symbol name
Returns:
Demangled name with prefix restored/annotated
"""
if not prefix:
return demangled
# Successfully demangled - add descriptive prefix
if demangled != stripped and (
annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix)
):
return f"[{annotation}: {demangled}]"
# Failed to demangle - restore original prefix
return prefix + demangled
@staticmethod
def _restore_symbol_suffix(original: str, demangled: str) -> str:
"""Restore GCC optimization suffix that was removed before demangling.
Args:
original: Original symbol name with suffix
demangled: Demangled symbol name without suffix
Returns:
Demangled name with suffix annotation
"""
if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
return f"{demangled} [{suffix_match.group(1)}]"
return demangled
def _demangle_symbol(self, symbol: str) -> str:
"""Get demangled C++ symbol name from cache."""
@@ -366,247 +495,6 @@ class MemoryAnalyzer:
return "Other Core"
def get_unattributed_ram(self) -> tuple[int, int, int]:
"""Get unattributed RAM sizes (SDK/framework overhead).
Returns:
Tuple of (unattributed_bss, unattributed_data, total_unattributed)
These are bytes in RAM sections that have no corresponding symbols.
"""
bss_section = self.sections.get(".bss")
data_section = self.sections.get(".data")
unattributed_bss = 0
unattributed_data = 0
if bss_section:
unattributed_bss = max(0, bss_section.total_size - bss_section.symbol_size)
if data_section:
unattributed_data = max(
0, data_section.total_size - data_section.symbol_size
)
return unattributed_bss, unattributed_data, unattributed_bss + unattributed_data
def _find_sdk_library_dirs(self) -> list[Path]:
"""Find SDK library directories based on platform.
Returns:
List of paths to SDK library directories containing .a files.
"""
sdk_dirs: list[Path] = []
if self._idedata is None:
return sdk_dirs
# Get the CC path to determine the framework location
cc_path = getattr(self._idedata, "cc_path", None)
if not cc_path:
return sdk_dirs
cc_path = Path(cc_path)
# For ESP8266 Arduino framework
# CC is like: ~/.platformio/packages/toolchain-xtensa/bin/xtensa-lx106-elf-gcc
# SDK libs are in: ~/.platformio/packages/framework-arduinoespressif8266/tools/sdk/lib/
if "xtensa-lx106" in str(cc_path):
platformio_dir = cc_path.parent.parent.parent
esp8266_sdk = (
platformio_dir
/ "framework-arduinoespressif8266"
/ "tools"
/ "sdk"
/ "lib"
)
if esp8266_sdk.exists():
sdk_dirs.append(esp8266_sdk)
# Also check for NONOSDK subdirectories (closed-source libs)
sdk_dirs.extend(
subdir
for subdir in esp8266_sdk.iterdir()
if subdir.is_dir() and subdir.name.startswith("NONOSDK")
)
# For ESP32 IDF framework
# CC is like: ~/.platformio/packages/toolchain-xtensa-esp-elf/bin/xtensa-esp32-elf-gcc
# or: ~/.platformio/packages/toolchain-riscv32-esp/bin/riscv32-esp-elf-gcc
elif "xtensa-esp" in str(cc_path) or "riscv32-esp" in str(cc_path):
# Detect ESP32 variant from CC path or defines
variant = self._detect_esp32_variant()
if variant:
platformio_dir = cc_path.parent.parent.parent
espidf_dir = platformio_dir / "framework-espidf" / "components"
if espidf_dir.exists():
# Find all directories named after the variant that contain .a files
# This handles various ESP-IDF library layouts:
# - components/*/lib/<variant>/
# - components/*/<variant>/
# - components/*/lib/lib/<variant>/
# - components/*/*/lib_*/<variant>/
sdk_dirs.extend(
variant_dir
for variant_dir in espidf_dir.rglob(variant)
if variant_dir.is_dir() and any(variant_dir.glob("*.a"))
)
return sdk_dirs
def _detect_esp32_variant(self) -> str | None:
"""Detect ESP32 variant from idedata defines.
Returns:
Variant string like 'esp32', 'esp32s2', 'esp32c3', etc. or None.
"""
if self._idedata is None:
return None
defines = getattr(self._idedata, "defines", [])
if not defines:
return None
# ESPHome always adds USE_ESP32_VARIANT_xxx defines
variant_prefix = "USE_ESP32_VARIANT_"
for define in defines:
if define.startswith(variant_prefix):
# Extract variant name and convert to lowercase
# USE_ESP32_VARIANT_ESP32 -> esp32
# USE_ESP32_VARIANT_ESP32S3 -> esp32s3
return define[len(variant_prefix) :].lower()
return None
def _parse_sdk_library(
self, lib_path: Path
) -> tuple[list[tuple[str, int, str, bool]], set[str]]:
"""Parse a single SDK library for symbols.
Args:
lib_path: Path to the .a library file
Returns:
Tuple of:
- List of BSS/DATA symbols: (symbol_name, size, section, is_local)
- Set of global BSS/DATA symbol names (for checking if RAM is linked)
"""
ram_symbols: list[tuple[str, int, str, bool]] = []
global_ram_symbols: set[str] = set()
result = run_tool([self.nm_path, "--size-sort", str(lib_path)], timeout=10)
if result is None:
return ram_symbols, global_ram_symbols
for line in result.stdout.splitlines():
parts = line.split()
if len(parts) < 3:
continue
try:
size = int(parts[0], 16)
sym_type = parts[1]
name = parts[2]
# Only collect BSS (b/B) and DATA (d/D) for RAM analysis
if sym_type in ("b", "B"):
section = ".bss"
is_local = sym_type == "b"
ram_symbols.append((name, size, section, is_local))
# Track global RAM symbols (B/D) for linking check
if sym_type == "B":
global_ram_symbols.add(name)
elif sym_type in ("d", "D"):
section = ".data"
is_local = sym_type == "d"
ram_symbols.append((name, size, section, is_local))
if sym_type == "D":
global_ram_symbols.add(name)
except (ValueError, IndexError):
continue
return ram_symbols, global_ram_symbols
def _analyze_sdk_libraries(self) -> None:
"""Analyze SDK libraries to find symbols not in the ELF.
This finds static/local symbols from closed-source SDK libraries
that consume RAM but don't appear in the final ELF symbol table.
Only includes symbols from libraries that have RAM actually linked
(at least one global BSS/DATA symbol in the ELF).
"""
sdk_dirs = self._find_sdk_library_dirs()
if not sdk_dirs:
_LOGGER.debug("No SDK library directories found")
return
_LOGGER.debug("Analyzing SDK libraries in %d directories", len(sdk_dirs))
# Track seen symbols to avoid duplicates from multiple SDK versions
seen_symbols: set[str] = set()
for sdk_dir in sdk_dirs:
for lib_path in sorted(sdk_dir.glob("*.a")):
lib_name = lib_path.name
ram_symbols, global_ram_symbols = self._parse_sdk_library(lib_path)
# Check if this library's RAM is actually linked by seeing if any
# of its global BSS/DATA symbols appear in the ELF
if not global_ram_symbols & self._elf_symbol_names:
# No RAM from this library is in the ELF - skip it
continue
for name, size, section, is_local in ram_symbols:
# Skip if already in ELF or already seen from another lib
if name in self._elf_symbol_names or name in seen_symbols:
continue
# Only track symbols with non-zero size
if size > 0:
self._sdk_symbols.append(
SDKSymbol(
name=name,
size=size,
library=lib_name,
section=section,
is_local=is_local,
)
)
seen_symbols.add(name)
# Demangle SDK symbols for better readability
if self._sdk_symbols:
sdk_names = [sym.name for sym in self._sdk_symbols]
demangled_map = batch_demangle(sdk_names, objdump_path=self.objdump_path)
for sym in self._sdk_symbols:
sym.demangled = demangled_map.get(sym.name, sym.name)
# Sort by size descending for reporting
self._sdk_symbols.sort(key=lambda s: s.size, reverse=True)
total_sdk_ram = sum(s.size for s in self._sdk_symbols)
_LOGGER.debug(
"Found %d SDK symbols not in ELF, totaling %d bytes",
len(self._sdk_symbols),
total_sdk_ram,
)
def get_sdk_ram_symbols(self) -> list[SDKSymbol]:
"""Get SDK symbols that consume RAM but aren't in the ELF symbol table.
Returns:
List of SDKSymbol objects sorted by size descending.
"""
return self._sdk_symbols
def get_sdk_ram_by_library(self) -> dict[str, list[SDKSymbol]]:
"""Get SDK RAM symbols grouped by library.
Returns:
Dictionary mapping library name to list of symbols.
"""
by_lib: dict[str, list[SDKSymbol]] = defaultdict(list)
for sym in self._sdk_symbols:
by_lib[sym.library].append(sym)
return dict(by_lib)
if __name__ == "__main__":
from .cli import main

View File

@@ -1,24 +1,16 @@
"""CLI interface for memory analysis with report generation."""
from __future__ import annotations
from collections import defaultdict
from collections.abc import Callable
import sys
from typing import TYPE_CHECKING
from . import (
_COMPONENT_API,
_COMPONENT_CORE,
_COMPONENT_PREFIX_ESPHOME,
_COMPONENT_PREFIX_EXTERNAL,
RAM_SECTIONS,
MemoryAnalyzer,
)
if TYPE_CHECKING:
from . import ComponentMemory
class MemoryAnalyzerCLI(MemoryAnalyzer):
"""Memory analyzer with CLI-specific report generation."""
@@ -27,8 +19,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
SYMBOL_SIZE_THRESHOLD: int = (
100 # Show symbols larger than this in detailed analysis
)
# Lower threshold for RAM symbols (RAM is more constrained)
RAM_SYMBOL_SIZE_THRESHOLD: int = 24
# Column width constants
COL_COMPONENT: int = 29
@@ -93,60 +83,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
COL_CORE_PERCENT,
)
def _add_section_header(self, lines: list[str], title: str) -> None:
"""Add a section header with title centered between separator lines."""
lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(title.center(self.TABLE_WIDTH))
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
def _add_top_consumers(
self,
lines: list[str],
title: str,
components: list[tuple[str, ComponentMemory]],
get_size: Callable[[ComponentMemory], int],
total: int,
memory_type: str,
limit: int = 25,
) -> None:
"""Add a formatted list of top memory consumers to the report.
Args:
lines: List of report lines to append the output to.
title: Section title to print before the list.
components: Sequence of (name, ComponentMemory) tuples to analyze.
get_size: Callable that takes a ComponentMemory and returns the
size in bytes to use for ranking and display.
total: Total size in bytes for computing percentage usage.
memory_type: Label for the memory region (e.g., "flash" or "RAM").
limit: Maximum number of components to include in the list.
"""
lines.append("")
lines.append(f"{title}:")
for i, (name, mem) in enumerate(components[:limit]):
size = get_size(mem)
if size > 0:
percentage = (size / total * 100) if total > 0 else 0
lines.append(
f"{i + 1}. {name} ({size:,} B) - {percentage:.1f}% of analyzed {memory_type}"
)
def _format_symbol_with_section(
self, demangled: str, size: int, section: str | None = None
) -> str:
"""Format a symbol entry, optionally adding a RAM section label.
If section is one of the RAM sections (.data or .bss), a label like
" [data]" or " [bss]" is appended. For non-RAM sections or when
section is None, no section label is added.
"""
section_label = ""
if section in RAM_SECTIONS:
section_label = f" [{section[1:]}]" # .data -> [data], .bss -> [bss]
return f"{demangled} ({size:,} B){section_label}"
def generate_report(self, detailed: bool = False) -> str:
"""Generate a formatted memory report."""
components = sorted(
@@ -187,70 +123,43 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B"
)
# Show unattributed RAM (SDK/framework overhead)
unattributed_bss, unattributed_data, unattributed_total = (
self.get_unattributed_ram()
)
if unattributed_total > 0:
lines.append("")
lines.append(
f"Unattributed RAM: {unattributed_total:,} B (SDK/framework overhead)"
)
if unattributed_bss > 0 and unattributed_data > 0:
# Top consumers
lines.append("")
lines.append("Top Flash Consumers:")
for i, (name, mem) in enumerate(components[:25]):
if mem.flash_total > 0:
percentage = (
(mem.flash_total / total_flash * 100) if total_flash > 0 else 0
)
lines.append(
f" .bss: {unattributed_bss:,} B | .data: {unattributed_data:,} B"
f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash"
)
# Show SDK symbol breakdown if available
sdk_by_lib = self.get_sdk_ram_by_library()
if sdk_by_lib:
lines.append("")
lines.append("SDK library breakdown (static symbols not in ELF):")
# Sort libraries by total size
lib_totals = [
(lib, sum(s.size for s in syms), syms)
for lib, syms in sdk_by_lib.items()
]
lib_totals.sort(key=lambda x: x[1], reverse=True)
for lib_name, lib_total, syms in lib_totals:
if lib_total == 0:
continue
lines.append(f" {lib_name}: {lib_total:,} B")
# Show top symbols from this library
for sym in sorted(syms, key=lambda s: s.size, reverse=True)[:3]:
section_label = sym.section.lstrip(".")
# Use demangled name (falls back to original if not demangled)
display_name = sym.demangled or sym.name
if len(display_name) > 50:
display_name = f"{display_name[:47]}..."
lines.append(
f" {sym.size:>6,} B [{section_label}] {display_name}"
)
# Top consumers
self._add_top_consumers(
lines,
"Top Flash Consumers",
components,
lambda m: m.flash_total,
total_flash,
"flash",
)
lines.append("")
lines.append("Top RAM Consumers:")
ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True)
self._add_top_consumers(
lines,
"Top RAM Consumers",
ram_components,
lambda m: m.ram_total,
total_ram,
"RAM",
for i, (name, mem) in enumerate(ram_components[:25]):
if mem.ram_total > 0:
percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0
lines.append(
f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM"
)
lines.append("")
lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
)
lines.append("=" * self.TABLE_WIDTH)
# Add ESPHome core detailed analysis if there are core symbols
if self._esphome_core_symbols:
self._add_section_header(lines, f"{_COMPONENT_CORE} Detailed Analysis")
lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(
f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH)
)
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Group core symbols by subcategory
core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict(
@@ -302,11 +211,7 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
)
for i, (symbol, demangled, size) in enumerate(large_core_symbols):
# Core symbols only track (symbol, demangled, size) without section info,
# so we don't show section labels here
lines.append(
f"{i + 1}. {self._format_symbol_with_section(demangled, size)}"
)
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
lines.append("=" * self.TABLE_WIDTH)
@@ -362,7 +267,11 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
for comp_name, comp_mem in components_to_analyze:
if not (comp_symbols := self._component_symbols.get(comp_name, [])):
continue
self._add_section_header(lines, f"{comp_name} Detailed Analysis")
lines.append("")
lines.append("=" * self.TABLE_WIDTH)
lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH))
lines.append("=" * self.TABLE_WIDTH)
lines.append("")
# Sort symbols by size
sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True)
@@ -373,69 +282,19 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
# Show all symbols above threshold for better visibility
large_symbols = [
(sym, dem, size, sec)
for sym, dem, size, sec in sorted_symbols
(sym, dem, size)
for sym, dem, size in sorted_symbols
if size > self.SYMBOL_SIZE_THRESHOLD
]
lines.append(
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):"
)
for i, (symbol, demangled, size, section) in enumerate(large_symbols):
lines.append(
f"{i + 1}. {self._format_symbol_with_section(demangled, size, section)}"
)
for i, (symbol, demangled, size) in enumerate(large_symbols):
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
lines.append("=" * self.TABLE_WIDTH)
# Detailed RAM analysis by component (at end, before RAM strings analysis)
self._add_section_header(lines, "RAM Symbol Analysis by Component")
# Show top 15 RAM consumers with their large symbols
for name, mem in ram_components[:15]:
if mem.ram_total == 0:
continue
ram_syms = self._ram_symbols.get(name, [])
if not ram_syms:
continue
# Sort by size descending
sorted_ram_syms = sorted(ram_syms, key=lambda x: x[2], reverse=True)
large_ram_syms = [
s for s in sorted_ram_syms if s[2] > self.RAM_SYMBOL_SIZE_THRESHOLD
]
lines.append(f"{name} ({mem.ram_total:,} B total RAM):")
# Show breakdown by section type
data_size = sum(s[2] for s in ram_syms if s[3] == ".data")
bss_size = sum(s[2] for s in ram_syms if s[3] == ".bss")
lines.append(f" .data (initialized): {data_size:,} B")
lines.append(f" .bss (uninitialized): {bss_size:,} B")
if large_ram_syms:
lines.append(
f" Symbols > {self.RAM_SYMBOL_SIZE_THRESHOLD} B ({len(large_ram_syms)}):"
)
for symbol, demangled, size, section in large_ram_syms[:10]:
# Format section label consistently by stripping leading dot
section_label = section.lstrip(".") if section else ""
# Add ellipsis if name is truncated
demangled_display = (
f"{demangled[:70]}..." if len(demangled) > 70 else demangled
)
lines.append(
f" {size:>6,} B [{section_label}] {demangled_display}"
)
if len(large_ram_syms) > 10:
lines.append(f" ... and {len(large_ram_syms) - 10} more")
lines.append("")
lines.append(
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
)
lines.append("=" * self.TABLE_WIDTH)
return "\n".join(lines)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:

View File

@@ -7,13 +7,11 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
# Section mapping for ELF file sections
# Maps standard section names to their various platform-specific variants
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
SECTION_MAPPING = {
".text": frozenset([".text", ".iram"]),
".rodata": frozenset([".rodata"]),
".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss"
".data": frozenset([".data", ".dram"]),
".bss": frozenset([".bss"]),
}
# Section to ComponentMemory attribute mapping
@@ -90,77 +88,6 @@ SYMBOL_PATTERNS = {
"sys_mbox_new",
"sys_arch_mbox_tryfetch",
],
# LibreTiny/Beken BK7231 radio calibration
"bk_radio_cal": [
"bk7011_",
"calibration_main",
"gcali_",
"rwnx_cal",
],
# LibreTiny/Beken WiFi MAC layer
"bk_wifi_mac": [
"rxu_", # RX upper layer
"txu_", # TX upper layer
"txl_", # TX lower layer
"rxl_", # RX lower layer
"scanu_", # Scan unit
"mm_hw_", # MAC management hardware
"mm_bcn", # MAC management beacon
"mm_tim", # MAC management TIM
"mm_check", # MAC management checks
"sm_connect", # Station management
"me_beacon", # Management entity beacon
"me_build", # Management entity build
"hapd_", # Host AP daemon
"chan_pre_", # Channel management
"handle_probe_", # Probe handling
],
# LibreTiny/Beken system control
"bk_system": [
"sctrl_", # System control
"icu_ctrl", # Interrupt control unit
"gdma_ctrl", # DMA control
"mpb_ctrl", # MPB control
"uf2_", # UF2 OTA
"bkreg_", # Beken registers
],
# LibreTiny/Beken BLE stack
"bk_ble": [
"gapc_", # GAP client
"gattc_", # GATT client
"attc_", # ATT client
"attmdb_", # ATT database
"atts_", # ATT server
"l2cc_", # L2CAP
"prf_env", # Profile environment
],
# LibreTiny/Beken scheduler
"bk_scheduler": [
"sch_plan_", # Scheduler plan
"sch_prog_", # Scheduler program
"sch_arb_", # Scheduler arbiter
],
# LibreTiny/Beken DMA descriptors
"bk_dma": [
"rx_payload_desc",
"rx_dma_hdrdesc",
"tx_hw_desc",
"host_event_data",
"host_cmd_data",
],
# ARM EABI compiler runtime (LibreTiny uses ARM Cortex-M)
"arm_runtime": [
"__aeabi_",
"__adddf3",
"__subdf3",
"__muldf3",
"__divdf3",
"__addsf3",
"__subsf3",
"__mulsf3",
"__divsf3",
"__gnu_unwind",
],
"xtensa": ["xt_", "_xt_", "xPortEnterCriticalTimeout"],
"heap": ["heap_", "multi_heap"],
"spi_flash": ["spi_flash"],
@@ -855,22 +782,7 @@ SYMBOL_PATTERNS = {
"math_internal": ["__mdiff", "__lshift", "__mprec_tens", "quorem"],
"character_class": ["__chclass"],
"camellia": ["camellia_", "camellia_feistel"],
"crypto_tables": [
"FSb",
"FSb2",
"FSb3",
"FSb4",
"Te0", # AES encryption table
"Td0", # AES decryption table
"crc32_table", # CRC32 lookup table
"crc_tab", # CRC lookup table
],
"crypto_hash": [
"SHA1Transform", # SHA1 hash function
"MD5Transform", # MD5 hash function
"SHA256",
"SHA512",
],
"crypto_tables": ["FSb", "FSb2", "FSb3", "FSb4"],
"event_buffer": ["g_eb_list_desc", "eb_space"],
"base_node": ["base_node_", "base_node_add_handler"],
"file_descriptor": ["s_fd_table"],

View File

@@ -1,182 +0,0 @@
"""Symbol demangling utilities for memory analysis.
This module provides functions for demangling C++ symbol names using c++filt.
"""
from __future__ import annotations
import logging
import re
import subprocess
from .toolchain import find_tool
_LOGGER = logging.getLogger(__name__)
# GCC global constructor/destructor prefix annotations
GCC_PREFIX_ANNOTATIONS = {
"_GLOBAL__sub_I_": "global constructor for",
"_GLOBAL__sub_D_": "global destructor for",
}
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
def _strip_gcc_annotations(symbol: str) -> tuple[str, str]:
"""Strip GCC optimization suffixes and prefixes from a symbol.
Args:
symbol: The mangled symbol name
Returns:
Tuple of (stripped_symbol, removed_prefix)
"""
# Remove GCC optimization markers
stripped = GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
# Handle GCC global constructor/initializer prefixes
prefix = ""
for gcc_prefix in GCC_PREFIX_ANNOTATIONS:
if stripped.startswith(gcc_prefix):
prefix = gcc_prefix
stripped = stripped[len(prefix) :]
break
return stripped, prefix
def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
"""Restore prefix that was removed before demangling.
Args:
prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
stripped: Stripped symbol name
demangled: Demangled symbol name
Returns:
Demangled name with prefix restored/annotated
"""
if not prefix:
return demangled
# Successfully demangled - add descriptive prefix
if demangled != stripped and (annotation := GCC_PREFIX_ANNOTATIONS.get(prefix)):
return f"[{annotation}: {demangled}]"
# Failed to demangle - restore original prefix
return prefix + demangled
def _restore_symbol_suffix(original: str, demangled: str) -> str:
"""Restore GCC optimization suffix that was removed before demangling.
Args:
original: Original symbol name with suffix
demangled: Demangled symbol name without suffix
Returns:
Demangled name with suffix annotation
"""
if suffix_match := GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
return f"{demangled} [{suffix_match.group(1)}]"
return demangled
def batch_demangle(
symbols: list[str],
cppfilt_path: str | None = None,
objdump_path: str | None = None,
) -> dict[str, str]:
"""Batch demangle C++ symbol names.
Args:
symbols: List of symbol names to demangle
cppfilt_path: Path to c++filt binary (auto-detected if not provided)
objdump_path: Path to objdump binary to derive c++filt path from
Returns:
Dictionary mapping original symbol names to demangled names
"""
cache: dict[str, str] = {}
if not symbols:
return cache
# Find c++filt tool
cppfilt_cmd = cppfilt_path or find_tool("c++filt", objdump_path)
if not cppfilt_cmd:
_LOGGER.warning("Could not find c++filt, symbols will not be demangled")
return {s: s for s in symbols}
_LOGGER.debug("Demangling %d symbols using %s", len(symbols), cppfilt_cmd)
# Strip GCC optimization suffixes and prefixes before demangling
symbols_stripped: list[str] = []
symbols_prefixes: list[str] = []
for symbol in symbols:
stripped, prefix = _strip_gcc_annotations(symbol)
symbols_stripped.append(stripped)
symbols_prefixes.append(prefix)
try:
result = subprocess.run(
[cppfilt_cmd],
input="\n".join(symbols_stripped),
capture_output=True,
text=True,
check=False,
)
except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
_LOGGER.warning("Failed to batch demangle symbols: %s", e)
return {s: s for s in symbols}
if result.returncode != 0:
_LOGGER.warning(
"c++filt exited with code %d: %s",
result.returncode,
result.stderr[:200] if result.stderr else "(no error output)",
)
return {s: s for s in symbols}
# Process demangled output
demangled_lines = result.stdout.strip().split("\n")
# Check for output length mismatch
if len(demangled_lines) != len(symbols):
_LOGGER.warning(
"c++filt output mismatch: expected %d lines, got %d",
len(symbols),
len(demangled_lines),
)
return {s: s for s in symbols}
failed_count = 0
for original, stripped, prefix, demangled in zip(
symbols, symbols_stripped, symbols_prefixes, demangled_lines
):
# Add back any prefix that was removed
demangled = _restore_symbol_prefix(prefix, stripped, demangled)
# If we stripped a suffix, add it back to the demangled name for clarity
if original != stripped and not prefix:
demangled = _restore_symbol_suffix(original, demangled)
cache[original] = demangled
# Count symbols that failed to demangle
if stripped == demangled and stripped.startswith("_Z"):
failed_count += 1
if failed_count <= 5:
_LOGGER.debug("Failed to demangle: %s", original)
if failed_count > 0:
_LOGGER.debug(
"Failed to demangle %d/%d symbols using %s",
failed_count,
len(symbols),
cppfilt_cmd,
)
return cache

View File

@@ -1,493 +0,0 @@
"""Analyzer for RAM-stored strings in ESP8266/ESP32 firmware ELF files.
This module identifies strings that are stored in RAM sections (.data, .bss, .rodata)
rather than in flash sections (.irom0.text, .irom.text), which is important for
memory-constrained platforms like ESP8266.
"""
from __future__ import annotations
from collections import defaultdict
from dataclasses import dataclass
import logging
from pathlib import Path
import re
import subprocess
from .demangle import batch_demangle
from .toolchain import find_tool
_LOGGER = logging.getLogger(__name__)
# ESP8266: .rodata is in RAM (DRAM), not flash
# ESP32: .rodata is in flash, mapped to data bus
ESP8266_RAM_SECTIONS = frozenset([".data", ".rodata", ".bss"])
ESP8266_FLASH_SECTIONS = frozenset([".irom0.text", ".irom.text", ".text"])
# ESP32: .rodata is memory-mapped from flash
ESP32_RAM_SECTIONS = frozenset([".data", ".bss", ".dram0.data", ".dram0.bss"])
ESP32_FLASH_SECTIONS = frozenset([".text", ".rodata", ".flash.text", ".flash.rodata"])
# nm symbol types for data symbols (D=global data, d=local data, R=rodata, B=bss)
DATA_SYMBOL_TYPES = frozenset(["D", "d", "R", "r", "B", "b"])
@dataclass
class SectionInfo:
"""Information about an ELF section."""
name: str
address: int
size: int
@dataclass
class RamString:
"""A string found in RAM."""
section: str
address: int
content: str
@property
def size(self) -> int:
"""Size in bytes including null terminator."""
return len(self.content) + 1
@dataclass
class RamSymbol:
"""A symbol found in RAM."""
name: str
sym_type: str
address: int
size: int
section: str
demangled: str = "" # Demangled name, set after batch demangling
class RamStringsAnalyzer:
"""Analyzes ELF files to find strings stored in RAM."""
def __init__(
self,
elf_path: str,
objdump_path: str | None = None,
min_length: int = 8,
platform: str = "esp32",
) -> None:
"""Initialize the RAM strings analyzer.
Args:
elf_path: Path to the ELF file to analyze
objdump_path: Path to objdump binary (used to find other tools)
min_length: Minimum string length to report (default: 8)
platform: Platform name ("esp8266", "esp32", etc.) for section mapping
"""
self.elf_path = Path(elf_path)
if not self.elf_path.exists():
raise FileNotFoundError(f"ELF file not found: {elf_path}")
self.objdump_path = objdump_path
self.min_length = min_length
self.platform = platform
# Set RAM/flash sections based on platform
if self.platform == "esp8266":
self.ram_sections = ESP8266_RAM_SECTIONS
self.flash_sections = ESP8266_FLASH_SECTIONS
else:
# ESP32 and other platforms
self.ram_sections = ESP32_RAM_SECTIONS
self.flash_sections = ESP32_FLASH_SECTIONS
self.sections: dict[str, SectionInfo] = {}
self.ram_strings: list[RamString] = []
self.ram_symbols: list[RamSymbol] = []
def _run_command(self, cmd: list[str]) -> str:
"""Run a command and return its output."""
try:
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return result.stdout
except subprocess.CalledProcessError as e:
_LOGGER.debug("Command failed: %s - %s", " ".join(cmd), e.stderr)
raise
except FileNotFoundError:
_LOGGER.warning("Command not found: %s", cmd[0])
raise
def analyze(self) -> None:
"""Perform the full RAM analysis."""
self._parse_sections()
self._extract_strings()
self._analyze_symbols()
self._demangle_symbols()
def _parse_sections(self) -> None:
"""Parse section headers from ELF file."""
objdump = find_tool("objdump", self.objdump_path)
if not objdump:
_LOGGER.error("Could not find objdump command")
return
try:
output = self._run_command([objdump, "-h", str(self.elf_path)])
except (subprocess.CalledProcessError, FileNotFoundError):
return
# Parse section headers
# Format: Idx Name Size VMA LMA File off Algn
section_pattern = re.compile(
r"^\s*\d+\s+(\S+)\s+([0-9a-fA-F]+)\s+([0-9a-fA-F]+)"
)
for line in output.split("\n"):
if match := section_pattern.match(line):
name = match.group(1)
size = int(match.group(2), 16)
vma = int(match.group(3), 16)
self.sections[name] = SectionInfo(name, vma, size)
def _extract_strings(self) -> None:
"""Extract strings from RAM sections."""
objdump = find_tool("objdump", self.objdump_path)
if not objdump:
return
for section_name in self.ram_sections:
if section_name not in self.sections:
continue
try:
output = self._run_command(
[objdump, "-s", "-j", section_name, str(self.elf_path)]
)
except subprocess.CalledProcessError:
# Section may exist but have no content (e.g., .bss)
continue
except FileNotFoundError:
continue
strings = self._parse_hex_dump(output, section_name)
self.ram_strings.extend(strings)
def _parse_hex_dump(self, output: str, section_name: str) -> list[RamString]:
"""Parse hex dump output to extract strings.
Args:
output: Output from objdump -s
section_name: Name of the section being parsed
Returns:
List of RamString objects
"""
strings: list[RamString] = []
current_string = bytearray()
string_start_addr = 0
for line in output.split("\n"):
# Lines look like: " 3ffef8a0 00000000 00000000 00000000 00000000 ................"
match = re.match(r"^\s+([0-9a-fA-F]+)\s+((?:[0-9a-fA-F]{2,8}\s*)+)", line)
if not match:
continue
addr = int(match.group(1), 16)
hex_data = match.group(2).strip()
# Convert hex to bytes
hex_bytes = hex_data.split()
byte_offset = 0
for hex_chunk in hex_bytes:
# Handle both byte-by-byte and word formats
for i in range(0, len(hex_chunk), 2):
byte_val = int(hex_chunk[i : i + 2], 16)
if 0x20 <= byte_val <= 0x7E: # Printable ASCII
if not current_string:
string_start_addr = addr + byte_offset
current_string.append(byte_val)
else:
if byte_val == 0 and len(current_string) >= self.min_length:
# Found null terminator
strings.append(
RamString(
section=section_name,
address=string_start_addr,
content=current_string.decode(
"ascii", errors="ignore"
),
)
)
current_string = bytearray()
byte_offset += 1
return strings
def _analyze_symbols(self) -> None:
"""Analyze symbols in RAM sections."""
nm = find_tool("nm", self.objdump_path)
if not nm:
return
try:
output = self._run_command([nm, "-S", "--size-sort", str(self.elf_path)])
except (subprocess.CalledProcessError, FileNotFoundError):
return
for line in output.split("\n"):
parts = line.split()
if len(parts) < 4:
continue
try:
addr = int(parts[0], 16)
size = int(parts[1], 16) if parts[1] != "?" else 0
except ValueError:
continue
sym_type = parts[2]
name = " ".join(parts[3:])
# Filter for data symbols
if sym_type not in DATA_SYMBOL_TYPES:
continue
# Check if symbol is in a RAM section
for section_name in self.ram_sections:
if section_name not in self.sections:
continue
section = self.sections[section_name]
if section.address <= addr < section.address + section.size:
self.ram_symbols.append(
RamSymbol(
name=name,
sym_type=sym_type,
address=addr,
size=size,
section=section_name,
)
)
break
def _demangle_symbols(self) -> None:
"""Batch demangle all RAM symbol names."""
if not self.ram_symbols:
return
# Collect all symbol names and demangle them
symbol_names = [s.name for s in self.ram_symbols]
demangle_cache = batch_demangle(symbol_names, objdump_path=self.objdump_path)
# Assign demangled names to symbols
for symbol in self.ram_symbols:
symbol.demangled = demangle_cache.get(symbol.name, symbol.name)
def _get_sections_size(self, section_names: frozenset[str]) -> int:
"""Get total size of specified sections."""
return sum(
section.size
for name, section in self.sections.items()
if name in section_names
)
def get_total_ram_usage(self) -> int:
"""Get total RAM usage from RAM sections."""
return self._get_sections_size(self.ram_sections)
def get_total_flash_usage(self) -> int:
"""Get total flash usage from flash sections."""
return self._get_sections_size(self.flash_sections)
def get_total_string_bytes(self) -> int:
"""Get total bytes used by strings in RAM."""
return sum(s.size for s in self.ram_strings)
def get_repeated_strings(self) -> list[tuple[str, int]]:
"""Find strings that appear multiple times.
Returns:
List of (string, count) tuples sorted by potential savings
"""
string_counts: dict[str, int] = defaultdict(int)
for ram_string in self.ram_strings:
string_counts[ram_string.content] += 1
return sorted(
[(s, c) for s, c in string_counts.items() if c > 1],
key=lambda x: x[1] * (len(x[0]) + 1),
reverse=True,
)
def get_long_strings(self, min_len: int = 20) -> list[RamString]:
"""Get strings longer than the specified length.
Args:
min_len: Minimum string length
Returns:
List of RamString objects sorted by length
"""
return sorted(
[s for s in self.ram_strings if len(s.content) >= min_len],
key=lambda x: len(x.content),
reverse=True,
)
def get_largest_symbols(self, min_size: int = 100) -> list[RamSymbol]:
"""Get RAM symbols larger than the specified size.
Args:
min_size: Minimum symbol size in bytes
Returns:
List of RamSymbol objects sorted by size
"""
return sorted(
[s for s in self.ram_symbols if s.size >= min_size],
key=lambda x: x.size,
reverse=True,
)
def generate_report(self, show_all_sections: bool = False) -> str:
"""Generate a formatted RAM strings analysis report.
Args:
show_all_sections: If True, show all sections, not just RAM
Returns:
Formatted report string
"""
lines: list[str] = []
table_width = 80
lines.append("=" * table_width)
lines.append(
f"RAM Strings Analysis ({self.platform.upper()})".center(table_width)
)
lines.append("=" * table_width)
lines.append("")
# Section Analysis
lines.append("SECTION ANALYSIS")
lines.append("-" * table_width)
lines.append(f"{'Section':<20} {'Address':<12} {'Size':<12} {'Location'}")
lines.append("-" * table_width)
total_ram_usage = 0
total_flash_usage = 0
for name, section in sorted(self.sections.items(), key=lambda x: x[1].address):
if name in self.ram_sections:
location = "RAM"
total_ram_usage += section.size
elif name in self.flash_sections:
location = "FLASH"
total_flash_usage += section.size
else:
location = "OTHER"
if show_all_sections or name in self.ram_sections:
lines.append(
f"{name:<20} 0x{section.address:08x} {section.size:>8} B {location}"
)
lines.append("-" * table_width)
lines.append(f"Total RAM sections size: {total_ram_usage:,} bytes")
lines.append(f"Total Flash sections size: {total_flash_usage:,} bytes")
# Strings in RAM
lines.append("")
lines.append("=" * table_width)
lines.append("STRINGS IN RAM SECTIONS")
lines.append("=" * table_width)
lines.append(
"Note: .bss sections contain uninitialized data (no strings to extract)"
)
# Group strings by section
strings_by_section: dict[str, list[RamString]] = defaultdict(list)
for ram_string in self.ram_strings:
strings_by_section[ram_string.section].append(ram_string)
for section_name in sorted(strings_by_section.keys()):
section_strings = strings_by_section[section_name]
lines.append(f"\nSection: {section_name}")
lines.append("-" * 40)
for ram_string in sorted(section_strings, key=lambda x: x.address):
clean_string = ram_string.content[:100] + (
"..." if len(ram_string.content) > 100 else ""
)
lines.append(
f' 0x{ram_string.address:08x}: "{clean_string}" (len={len(ram_string.content)})'
)
# Large RAM symbols
lines.append("")
lines.append("=" * table_width)
lines.append("LARGE DATA SYMBOLS IN RAM (>= 50 bytes)")
lines.append("=" * table_width)
largest_symbols = self.get_largest_symbols(50)
lines.append(f"\n{'Symbol':<50} {'Type':<6} {'Size':<10} {'Section'}")
lines.append("-" * table_width)
for symbol in largest_symbols:
# Use demangled name if available, otherwise raw name
display_name = symbol.demangled or symbol.name
name_display = display_name[:49] if len(display_name) > 49 else display_name
lines.append(
f"{name_display:<50} {symbol.sym_type:<6} {symbol.size:>8} B {symbol.section}"
)
# Summary
lines.append("")
lines.append("=" * table_width)
lines.append("SUMMARY")
lines.append("=" * table_width)
lines.append(f"Total strings found in RAM: {len(self.ram_strings)}")
total_string_bytes = self.get_total_string_bytes()
lines.append(f"Total bytes used by strings: {total_string_bytes:,}")
# Optimization targets
lines.append("")
lines.append("=" * table_width)
lines.append("POTENTIAL OPTIMIZATION TARGETS")
lines.append("=" * table_width)
# Repeated strings
repeated = self.get_repeated_strings()[:10]
if repeated:
lines.append("\nRepeated strings (could be deduplicated):")
for string, count in repeated:
savings = (count - 1) * (len(string) + 1)
clean_string = string[:50] + ("..." if len(string) > 50 else "")
lines.append(
f' "{clean_string}" - appears {count} times (potential savings: {savings} bytes)'
)
# Long strings - platform-specific advice
long_strings = self.get_long_strings(20)[:10]
if long_strings:
if self.platform == "esp8266":
lines.append(
"\nLong strings that could be moved to PROGMEM (>= 20 chars):"
)
else:
# ESP32: strings in DRAM are typically there for a reason
# (interrupt handlers, pre-flash-init code, etc.)
lines.append("\nLong strings in DRAM (>= 20 chars):")
lines.append(
"Note: ESP32 DRAM strings may be required for interrupt/early-boot contexts"
)
for ram_string in long_strings:
clean_string = ram_string.content[:60] + (
"..." if len(ram_string.content) > 60 else ""
)
lines.append(
f' {ram_string.section} @ 0x{ram_string.address:08x}: "{clean_string}" ({len(ram_string.content)} bytes)'
)
lines.append("")
return "\n".join(lines)

View File

@@ -1,93 +0,0 @@
"""Toolchain utilities for memory analysis."""
from __future__ import annotations
import logging
from pathlib import Path
import subprocess
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from collections.abc import Sequence
_LOGGER = logging.getLogger(__name__)
# Platform-specific toolchain prefixes
TOOLCHAIN_PREFIXES = [
"xtensa-lx106-elf-", # ESP8266
"xtensa-esp32-elf-", # ESP32
"xtensa-esp-elf-", # ESP32 (newer IDF)
"", # System default (no prefix)
]
def find_tool(
tool_name: str,
objdump_path: str | None = None,
) -> str | None:
"""Find a toolchain tool by name.
First tries to derive the tool path from objdump_path (if provided),
then falls back to searching for platform-specific tools.
Args:
tool_name: Name of the tool (e.g., "objdump", "nm", "c++filt")
objdump_path: Path to objdump binary to derive other tool paths from
Returns:
Path to the tool or None if not found
"""
# Try to derive from objdump path first (most reliable)
if objdump_path and objdump_path != "objdump":
objdump_file = Path(objdump_path)
# Replace just the filename portion, preserving any prefix (e.g., xtensa-esp32-elf-)
new_name = objdump_file.name.replace("objdump", tool_name)
potential_path = str(objdump_file.with_name(new_name))
if Path(potential_path).exists():
_LOGGER.debug("Found %s at: %s", tool_name, potential_path)
return potential_path
# Try platform-specific tools
for prefix in TOOLCHAIN_PREFIXES:
cmd = f"{prefix}{tool_name}"
try:
subprocess.run([cmd, "--version"], capture_output=True, check=True)
_LOGGER.debug("Found %s: %s", tool_name, cmd)
return cmd
except (subprocess.CalledProcessError, FileNotFoundError):
continue
_LOGGER.warning("Could not find %s tool", tool_name)
return None
def run_tool(
cmd: Sequence[str],
timeout: int = 30,
) -> subprocess.CompletedProcess[str] | None:
"""Run a toolchain command and return the result.
Args:
cmd: Command and arguments to run
timeout: Timeout in seconds
Returns:
CompletedProcess on success, None on failure
"""
try:
return subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
check=False,
)
except subprocess.TimeoutExpired:
_LOGGER.warning("Command timed out: %s", " ".join(cmd))
return None
except FileNotFoundError:
_LOGGER.warning("Command not found: %s", cmd[0])
return None
except OSError as e:
_LOGGER.warning("Failed to run command %s: %s", cmd[0], e)
return None

View File

@@ -30,9 +30,7 @@ void A01nyubComponent::check_buffer_() {
ESP_LOGV(TAG, "Distance from sensor: %f mm, %f m", distance, meters);
this->publish_state(meters);
} else {
char hex_buf[format_hex_pretty_size(4)];
ESP_LOGW(TAG, "Invalid data read from sensor: %s",
format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size()));
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
}
} else {
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);

View File

@@ -29,9 +29,7 @@ void A02yyuwComponent::check_buffer_() {
ESP_LOGV(TAG, "Distance from sensor: %f mm", distance);
this->publish_state(distance);
} else {
char hex_buf[format_hex_pretty_size(4)];
ESP_LOGW(TAG, "Invalid data read from sensor: %s",
format_hex_pretty_to(hex_buf, this->buffer_.data(), this->buffer_.size()));
ESP_LOGW(TAG, "Invalid data read from sensor: %s", format_hex_pretty(this->buffer_).c_str());
}
} else {
ESP_LOGW(TAG, "checksum failed: %02x != %02x", checksum, this->buffer_[3]);

View File

@@ -90,16 +90,13 @@ void AbsoluteHumidityComponent::loop() {
this->status_set_error(LOG_STR("Invalid saturation vapor pressure equation selection!"));
return;
}
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa", es);
// Calculate absolute humidity
const float absolute_humidity = vapor_density(es, hr, temperature_k);
ESP_LOGD(TAG,
"Saturation vapor pressure %f kPa\n"
"Publishing absolute humidity %f g/m³",
es, absolute_humidity);
// Publish absolute humidity
ESP_LOGD(TAG, "Publishing absolute humidity %f g/m³", absolute_humidity);
this->status_clear_warning();
this->publish_state(absolute_humidity);
}
@@ -166,7 +163,7 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
}
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
// H/T to https://esphome.io/cookbook/bme280_environment/
// H/T to https://esphome.io/cookbook/bme280_environment.html
// H/T to https://carnotcycle.wordpress.com/2012/08/04/how-to-convert-relative-humidity-to-absolute-humidity/
float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
// es = saturated vapor pressure (kPa)

View File

@@ -211,13 +211,13 @@ void AcDimmer::write_state(float state) {
this->store_.value = new_value;
}
void AcDimmer::dump_config() {
ESP_LOGCONFIG(TAG, "AcDimmer:");
LOG_PIN(" Output Pin: ", this->gate_pin_);
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
ESP_LOGCONFIG(TAG,
"AcDimmer:\n"
" Min Power: %.1f%%\n"
" Init with half cycle: %s",
this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_));
LOG_PIN(" Output Pin: ", this->gate_pin_);
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
if (method_ == DIM_METHOD_LEADING_PULSE) {
ESP_LOGCONFIG(TAG, " Method: leading pulse");
} else if (method_ == DIM_METHOD_LEADING) {

View File

@@ -3,7 +3,6 @@ import esphome.codegen as cg
from esphome.components import output
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_METHOD, CONF_MIN_POWER
from esphome.core import CORE
CODEOWNERS = ["@glmnet"]
@@ -37,12 +36,6 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
if CORE.is_esp8266:
# ac_dimmer uses setTimer1Callback which requires the waveform generator
from esphome.components.esp8266.const import require_waveform
require_waveform()
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)

View File

@@ -1,17 +1,15 @@
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import (
from esphome.components.esp32 import VARIANT_ESP32P4, get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
)
import esphome.config_validation as cv
from esphome.const import CONF_ANALOG, CONF_INPUT, CONF_NUMBER, PLATFORM_ESP8266
@@ -101,13 +99,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
5: adc_channel_t.ADC_CHANNEL_5,
6: adc_channel_t.ADC_CHANNEL_6,
},
# https://docs.espressif.com/projects/esp-idf/en/latest/esp32c61/api-reference/peripherals/gpio.html
VARIANT_ESP32C61: {
1: adc_channel_t.ADC_CHANNEL_0,
3: adc_channel_t.ADC_CHANNEL_1,
4: adc_channel_t.ADC_CHANNEL_2,
5: adc_channel_t.ADC_CHANNEL_3,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {
1: adc_channel_t.ADC_CHANNEL_0,
@@ -116,17 +107,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
4: adc_channel_t.ADC_CHANNEL_3,
5: adc_channel_t.ADC_CHANNEL_4,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
VARIANT_ESP32P4: {
16: adc_channel_t.ADC_CHANNEL_0,
17: adc_channel_t.ADC_CHANNEL_1,
18: adc_channel_t.ADC_CHANNEL_2,
19: adc_channel_t.ADC_CHANNEL_3,
20: adc_channel_t.ADC_CHANNEL_4,
21: adc_channel_t.ADC_CHANNEL_5,
22: adc_channel_t.ADC_CHANNEL_6,
23: adc_channel_t.ADC_CHANNEL_7,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: {
1: adc_channel_t.ADC_CHANNEL_0,
@@ -153,6 +133,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
@@ -183,19 +173,8 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
VARIANT_ESP32C5: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32c6/include/soc/adc_channel.h
VARIANT_ESP32C6: {}, # no ADC2
# ESP32-C61 has no ADC2
VARIANT_ESP32C61: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32h2/include/soc/adc_channel.h
VARIANT_ESP32H2: {}, # no ADC2
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32p4/include/soc/adc_channel.h
VARIANT_ESP32P4: {
49: adc_channel_t.ADC_CHANNEL_0,
50: adc_channel_t.ADC_CHANNEL_1,
51: adc_channel_t.ADC_CHANNEL_2,
52: adc_channel_t.ADC_CHANNEL_3,
53: adc_channel_t.ADC_CHANNEL_4,
54: adc_channel_t.ADC_CHANNEL_5,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s2/include/soc/adc_channel.h
VARIANT_ESP32S2: {
11: adc_channel_t.ADC_CHANNEL_0,
@@ -222,6 +201,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,
},
}

View File

@@ -42,11 +42,10 @@ void ADCSensor::setup() {
adc_oneshot_unit_init_cfg_t init_config = {}; // Zero initialize
init_config.unit_id = this->adc_unit_;
init_config.ulp_mode = ADC_ULP_MODE_DISABLE;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || USE_ESP32_VARIANT_ESP32H2
init_config.clk_src = ADC_DIGI_CLK_SRC_DEFAULT;
#endif // USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 ||
// USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2
// USE_ESP32_VARIANT_ESP32H2
esp_err_t err = adc_oneshot_new_unit(&init_config, &ADCSensor::shared_adc_handles[this->adc_unit_]);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error initializing %s: %d", LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)), err);
@@ -75,7 +74,7 @@ void ADCSensor::setup() {
adc_cali_handle_t handle = nullptr;
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
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)
@@ -112,7 +111,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
}
this->setup_flags_.init_complete = true;
@@ -121,21 +120,23 @@ void ADCSensor::setup() {
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
ESP_LOGCONFIG(TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)));
ESP_LOGCONFIG(
TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s\n"
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
@@ -185,11 +186,11 @@ float ADCSensor::sample_fixed_attenuation_() {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
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_);
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
#endif // USE_ESP32_VARIANT_ESP32C3 || ESP32C5 || ESP32C6 || ESP32S3 || ESP32H2
this->calibration_handle_ = nullptr;
}
}
@@ -218,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_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
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_);
@@ -230,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_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
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_;
@@ -265,7 +266,7 @@ float ADCSensor::sample_autorange_() {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) {
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
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);
@@ -287,7 +288,7 @@ float ADCSensor::sample_autorange_() {
}
// Clean up calibration handle
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
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);

View File

@@ -25,13 +25,11 @@ class AddressableLightDisplay : public display::DisplayBuffer {
if (enabled_ && !enabled) { // enabled -> disabled
// - Tell the parent light to refresh, effectively wiping the display. Also
// restores the previous effect (if any).
if (this->last_effect_index_.has_value()) {
light_state_->make_call().set_effect(*this->last_effect_index_).perform();
}
light_state_->make_call().set_effect(this->last_effect_).perform();
} else if (!enabled_ && enabled) { // disabled -> enabled
// - Save the current effect index.
this->last_effect_index_ = light_state_->get_current_effect_index();
// - Save the current effect.
this->last_effect_ = light_state_->get_effect_name();
// - Disable any current effect.
light_state_->make_call().set_effect(0).perform();
}
@@ -58,7 +56,7 @@ class AddressableLightDisplay : public display::DisplayBuffer {
int32_t width_;
int32_t height_;
std::vector<Color> addressable_light_buffer_;
optional<uint32_t> last_effect_index_;
optional<std::string> last_effect_;
optional<std::function<int(int, int)>> pixel_mapper_f_;
};
} // namespace addressable_light

View File

@@ -162,13 +162,11 @@ void ADE7880::update() {
}
void ADE7880::dump_config() {
ESP_LOGCONFIG(TAG,
"ADE7880:\n"
" Frequency: %.0f Hz",
this->frequency_);
ESP_LOGCONFIG(TAG, "ADE7880:");
LOG_PIN(" IRQ0 Pin: ", this->irq0_pin_);
LOG_PIN(" IRQ1 Pin: ", this->irq1_pin_);
LOG_PIN(" RESET Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " Frequency: %.0f Hz", this->frequency_);
if (this->channel_a_ != nullptr) {
ESP_LOGCONFIG(TAG, " Phase A:");

View File

@@ -227,7 +227,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(ADE7880),
cv.Optional(CONF_FREQUENCY, default="50Hz"): cv.All(
cv.frequency, cv.float_range(min=45.0, max=66.0)
cv.frequency, cv.Range(min=45.0, max=66.0)
),
cv.Optional(CONF_IRQ0_PIN): pins.internal_gpio_input_pin_schema,
cv.Required(CONF_IRQ1_PIN): pins.internal_gpio_input_pin_schema,

View File

@@ -24,8 +24,6 @@ from esphome.const import (
UNIT_WATT,
)
CODEOWNERS = ["@angelnu"]
CONF_CURRENT_A = "current_a"
CONF_CURRENT_B = "current_b"
CONF_ACTIVE_POWER_A = "active_power_a"

View File

@@ -25,8 +25,7 @@ void ADE7953::setup() {
this->ade_write_8(PGA_V_8, pga_v_);
this->ade_write_8(PGA_IA_8, pga_ia_);
this->ade_write_8(PGA_IB_8, pga_ib_);
this->ade_write_32(AVGAIN_32, avgain_);
this->ade_write_32(BVGAIN_32, bvgain_);
this->ade_write_32(AVGAIN_32, vgain_);
this->ade_write_32(AIGAIN_32, aigain_);
this->ade_write_32(BIGAIN_32, bigain_);
this->ade_write_32(AWGAIN_32, awgain_);
@@ -35,8 +34,7 @@ void ADE7953::setup() {
this->ade_read_8(PGA_V_8, &pga_v_);
this->ade_read_8(PGA_IA_8, &pga_ia_);
this->ade_read_8(PGA_IB_8, &pga_ib_);
this->ade_read_32(AVGAIN_32, &avgain_);
this->ade_read_32(BVGAIN_32, &bvgain_);
this->ade_read_32(AVGAIN_32, &vgain_);
this->ade_read_32(AIGAIN_32, &aigain_);
this->ade_read_32(BIGAIN_32, &bigain_);
this->ade_read_32(AWGAIN_32, &awgain_);
@@ -65,14 +63,13 @@ void ADE7953::dump_config() {
" PGA_V_8: 0x%X\n"
" PGA_IA_8: 0x%X\n"
" PGA_IB_8: 0x%X\n"
" AVGAIN_32: 0x%08jX\n"
" BVGAIN_32: 0x%08jX\n"
" VGAIN_32: 0x%08jX\n"
" AIGAIN_32: 0x%08jX\n"
" BIGAIN_32: 0x%08jX\n"
" AWGAIN_32: 0x%08jX\n"
" BWGAIN_32: 0x%08jX",
this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) avgain_, (uintmax_t) bvgain_,
(uintmax_t) aigain_, (uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_);
this->use_acc_energy_regs_, pga_v_, pga_ia_, pga_ib_, (uintmax_t) vgain_, (uintmax_t) aigain_,
(uintmax_t) bigain_, (uintmax_t) awgain_, (uintmax_t) bwgain_);
}
#define ADE_PUBLISH_(name, val, factor) \

View File

@@ -46,12 +46,7 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
void set_pga_ib(uint8_t pga_ib) { pga_ib_ = pga_ib; }
// Set input gains
void set_vgain(uint32_t vgain) {
// Datasheet says: "to avoid discrepancies in other registers,
// if AVGAIN is set then BVGAIN should be set to the same value."
avgain_ = vgain;
bvgain_ = vgain;
}
void set_vgain(uint32_t vgain) { vgain_ = vgain; }
void set_aigain(uint32_t aigain) { aigain_ = aigain; }
void set_bigain(uint32_t bigain) { bigain_ = bigain; }
void set_awgain(uint32_t awgain) { awgain_ = awgain; }
@@ -105,8 +100,7 @@ class ADE7953 : public PollingComponent, public sensor::Sensor {
uint8_t pga_v_;
uint8_t pga_ia_;
uint8_t pga_ib_;
uint32_t avgain_;
uint32_t bvgain_;
uint32_t vgain_;
uint32_t aigain_;
uint32_t bigain_;
uint32_t awgain_;

View File

@@ -21,12 +21,10 @@ void ADS1115Sensor::update() {
void ADS1115Sensor::dump_config() {
LOG_SENSOR(" ", "ADS1115 Sensor", this);
ESP_LOGCONFIG(TAG,
" Multiplexer: %u\n"
" Gain: %u\n"
" Resolution: %u\n"
" Sample rate: %u",
this->multiplexer_, this->gain_, this->resolution_, this->samplerate_);
ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_);
ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_);
ESP_LOGCONFIG(TAG, " Resolution: %u", this->resolution_);
ESP_LOGCONFIG(TAG, " Sample rate: %u", this->samplerate_);
}
} // namespace ads1115

View File

@@ -9,10 +9,8 @@ static const char *const TAG = "ads1118.sensor";
void ADS1118Sensor::dump_config() {
LOG_SENSOR(" ", "ADS1118 Sensor", this);
ESP_LOGCONFIG(TAG,
" Multiplexer: %u\n"
" Gain: %u",
this->multiplexer_, this->gain_);
ESP_LOGCONFIG(TAG, " Multiplexer: %u", this->multiplexer_);
ESP_LOGCONFIG(TAG, " Gain: %u", this->gain_);
}
float ADS1118Sensor::sample() {

View File

@@ -20,8 +20,7 @@ bool AirthingsListener::parse_device(const esp32_ble_tracker::ESPBTDevice &devic
sn |= ((uint32_t) it.data[2] << 16);
sn |= ((uint32_t) it.data[3] << 24);
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGD(TAG, "Found AirThings device Serial:%" PRIu32 " (MAC: %s)", sn, device.address_str_to(addr_buf));
ESP_LOGD(TAG, "Found AirThings device Serial:%" PRIu32 " (MAC: %s)", sn, device.address_str().c_str());
return true;
}
}

View File

@@ -1,5 +1,4 @@
#include "airthings_wave_base.h"
#include "esphome/components/esp32_ble/ble_uuid.h"
// All information related to reading battery information came from the sensors.airthings_wave
// project by Sverre Hamre (https://github.com/sverrham/sensor.airthings_wave)
@@ -94,10 +93,8 @@ void AirthingsWaveBase::update() {
bool AirthingsWaveBase::request_read_values_() {
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_);
if (chr == nullptr) {
char service_buf[esp32_ble::UUID_STR_LEN];
char char_buf[esp32_ble::UUID_STR_LEN];
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_str(service_buf),
this->sensors_data_characteristic_uuid_.to_str(char_buf));
ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(),
this->sensors_data_characteristic_uuid_.to_string().c_str());
return false;
}
@@ -120,20 +117,17 @@ bool AirthingsWaveBase::request_battery_() {
auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->access_control_point_characteristic_uuid_);
if (chr == nullptr) {
char service_buf[esp32_ble::UUID_STR_LEN];
char char_buf[esp32_ble::UUID_STR_LEN];
ESP_LOGW(TAG, "No access control point characteristic found at service %s char %s",
this->service_uuid_.to_str(service_buf), this->access_control_point_characteristic_uuid_.to_str(char_buf));
this->service_uuid_.to_string().c_str(),
this->access_control_point_characteristic_uuid_.to_string().c_str());
return false;
}
auto *descr = this->parent()->get_descriptor(this->service_uuid_, this->access_control_point_characteristic_uuid_,
CLIENT_CHARACTERISTIC_CONFIGURATION_DESCRIPTOR_UUID);
if (descr == nullptr) {
char service_buf[esp32_ble::UUID_STR_LEN];
char char_buf[esp32_ble::UUID_STR_LEN];
ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_str(service_buf),
this->access_control_point_characteristic_uuid_.to_str(char_buf));
ESP_LOGW(TAG, "No CCC descriptor found at service %s char %s", this->service_uuid_.to_string().c_str(),
this->access_control_point_characteristic_uuid_.to_string().c_str());
return false;
}

View File

@@ -8,7 +8,8 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::alarm_control_panel {
namespace esphome {
namespace alarm_control_panel {
static const char *const TAG = "alarm_control_panel";
@@ -34,12 +35,26 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states
this->state_callback_.call();
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_alarm_control_panel_update(this);
#endif
// Cleared fires when leaving TRIGGERED state
if (state == ACP_STATE_TRIGGERED) {
this->triggered_callback_.call();
} else if (state == ACP_STATE_ARMING) {
this->arming_callback_.call();
} else if (state == ACP_STATE_PENDING) {
this->pending_callback_.call();
} else if (state == ACP_STATE_ARMED_HOME) {
this->armed_home_callback_.call();
} else if (state == ACP_STATE_ARMED_NIGHT) {
this->armed_night_callback_.call();
} else if (state == ACP_STATE_ARMED_AWAY) {
this->armed_away_callback_.call();
} else if (state == ACP_STATE_DISARMED) {
this->disarmed_callback_.call();
}
if (prev_state == ACP_STATE_TRIGGERED) {
this->cleared_callback_.call();
}
@@ -54,6 +69,34 @@ void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback)
this->state_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_triggered_callback(std::function<void()> &&callback) {
this->triggered_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_arming_callback(std::function<void()> &&callback) {
this->arming_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_home_callback(std::function<void()> &&callback) {
this->armed_home_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_night_callback(std::function<void()> &&callback) {
this->armed_night_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_armed_away_callback(std::function<void()> &&callback) {
this->armed_away_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_pending_callback(std::function<void()> &&callback) {
this->pending_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_disarmed_callback(std::function<void()> &&callback) {
this->disarmed_callback_.add(std::move(callback));
}
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
this->cleared_callback_.add(std::move(callback));
}
@@ -114,4 +157,5 @@ void AlarmControlPanel::disarm(optional<std::string> code) {
call.perform();
}
} // namespace esphome::alarm_control_panel
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -1,5 +1,7 @@
#pragma once
#include <map>
#include "alarm_control_panel_call.h"
#include "alarm_control_panel_state.h"
@@ -7,7 +9,8 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/log.h"
namespace esphome::alarm_control_panel {
namespace esphome {
namespace alarm_control_panel {
enum AlarmControlPanelFeature : uint8_t {
// Matches Home Assistant values
@@ -32,13 +35,54 @@ class AlarmControlPanel : public EntityBase {
*/
void publish_state(AlarmControlPanelState state);
/** Add a callback for when the state of the alarm_control_panel changes.
* Triggers can check get_state() to determine the new state.
/** Add a callback for when the state of the alarm_control_panel changes
*
* @param callback The callback function
*/
void add_on_state_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to triggered
*
* @param callback The callback function
*/
void add_on_triggered_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel chanes to arming
*
* @param callback The callback function
*/
void add_on_arming_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to pending
*
* @param callback The callback function
*/
void add_on_pending_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_home
*
* @param callback The callback function
*/
void add_on_armed_home_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_night
*
* @param callback The callback function
*/
void add_on_armed_night_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to armed_away
*
* @param callback The callback function
*/
void add_on_armed_away_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel changes to disarmed
*
* @param callback The callback function
*/
void add_on_disarmed_callback(std::function<void()> &&callback);
/** Add a callback for when the state of the alarm_control_panel clears from triggered
*
* @param callback The callback function
@@ -128,14 +172,29 @@ class AlarmControlPanel : public EntityBase {
uint32_t last_update_;
// the call control function
virtual void control(const AlarmControlPanelCall &call) = 0;
// state callback - triggers check get_state() for specific state
LazyCallbackManager<void()> state_callback_{};
// clear callback - fires when leaving TRIGGERED state
LazyCallbackManager<void()> cleared_callback_{};
// state callback
CallbackManager<void()> state_callback_{};
// trigger callback
CallbackManager<void()> triggered_callback_{};
// arming callback
CallbackManager<void()> arming_callback_{};
// pending callback
CallbackManager<void()> pending_callback_{};
// armed_home callback
CallbackManager<void()> armed_home_callback_{};
// armed_night callback
CallbackManager<void()> armed_night_callback_{};
// armed_away callback
CallbackManager<void()> armed_away_callback_{};
// disarmed callback
CallbackManager<void()> disarmed_callback_{};
// clear callback
CallbackManager<void()> cleared_callback_{};
// chime callback
LazyCallbackManager<void()> chime_callback_{};
CallbackManager<void()> chime_callback_{};
// ready callback
LazyCallbackManager<void()> ready_callback_{};
CallbackManager<void()> ready_callback_{};
};
} // namespace esphome::alarm_control_panel
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -4,7 +4,8 @@
#include "esphome/core/log.h"
namespace esphome::alarm_control_panel {
namespace esphome {
namespace alarm_control_panel {
static const char *const TAG = "alarm_control_panel";
@@ -98,4 +99,5 @@ void AlarmControlPanelCall::perform() {
}
}
} // namespace esphome::alarm_control_panel
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -6,7 +6,8 @@
#include "esphome/core/helpers.h"
namespace esphome::alarm_control_panel {
namespace esphome {
namespace alarm_control_panel {
class AlarmControlPanel;
@@ -35,4 +36,5 @@ class AlarmControlPanelCall {
void validate_();
};
} // namespace esphome::alarm_control_panel
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -1,6 +1,7 @@
#include "alarm_control_panel_state.h"
namespace esphome::alarm_control_panel {
namespace esphome {
namespace alarm_control_panel {
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) {
switch (state) {
@@ -29,4 +30,5 @@ const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState stat
}
}
} // namespace esphome::alarm_control_panel
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -3,7 +3,8 @@
#include <cstdint>
#include "esphome/core/log.h"
namespace esphome::alarm_control_panel {
namespace esphome {
namespace alarm_control_panel {
enum AlarmControlPanelState : uint8_t {
ACP_STATE_DISARMED = 0,
@@ -24,4 +25,5 @@ enum AlarmControlPanelState : uint8_t {
*/
const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state);
} // namespace esphome::alarm_control_panel
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -3,9 +3,9 @@
#include "esphome/core/automation.h"
#include "alarm_control_panel.h"
namespace esphome::alarm_control_panel {
namespace esphome {
namespace alarm_control_panel {
/// Trigger on any state change
class StateTrigger : public Trigger<> {
public:
explicit StateTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -13,30 +13,55 @@ class StateTrigger : public Trigger<> {
}
};
/// Template trigger that fires when entering a specific state
template<AlarmControlPanelState State> class StateEnterTrigger : public Trigger<> {
class TriggeredTrigger : public Trigger<> {
public:
explicit StateEnterTrigger(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {
alarm_control_panel->add_on_state_callback([this]() {
if (this->alarm_control_panel_->get_state() == State)
this->trigger();
});
explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); });
}
protected:
AlarmControlPanel *alarm_control_panel_;
};
// Type aliases for state-specific triggers
using TriggeredTrigger = StateEnterTrigger<ACP_STATE_TRIGGERED>;
using ArmingTrigger = StateEnterTrigger<ACP_STATE_ARMING>;
using PendingTrigger = StateEnterTrigger<ACP_STATE_PENDING>;
using ArmedHomeTrigger = StateEnterTrigger<ACP_STATE_ARMED_HOME>;
using ArmedNightTrigger = StateEnterTrigger<ACP_STATE_ARMED_NIGHT>;
using ArmedAwayTrigger = StateEnterTrigger<ACP_STATE_ARMED_AWAY>;
using DisarmedTrigger = StateEnterTrigger<ACP_STATE_DISARMED>;
class ArmingTrigger : public Trigger<> {
public:
explicit ArmingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_arming_callback([this]() { this->trigger(); });
}
};
class PendingTrigger : public Trigger<> {
public:
explicit PendingTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_pending_callback([this]() { this->trigger(); });
}
};
class ArmedHomeTrigger : public Trigger<> {
public:
explicit ArmedHomeTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_home_callback([this]() { this->trigger(); });
}
};
class ArmedNightTrigger : public Trigger<> {
public:
explicit ArmedNightTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_night_callback([this]() { this->trigger(); });
}
};
class ArmedAwayTrigger : public Trigger<> {
public:
explicit ArmedAwayTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_armed_away_callback([this]() { this->trigger(); });
}
};
class DisarmedTrigger : public Trigger<> {
public:
explicit DisarmedTrigger(AlarmControlPanel *alarm_control_panel) {
alarm_control_panel->add_on_disarmed_callback([this]() { this->trigger(); });
}
};
/// Trigger when leaving TRIGGERED state (alarm cleared)
class ClearedTrigger : public Trigger<> {
public:
explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -44,7 +69,6 @@ class ClearedTrigger : public Trigger<> {
}
};
/// Trigger on chime event (zone opened while disarmed)
class ChimeTrigger : public Trigger<> {
public:
explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -52,7 +76,6 @@ class ChimeTrigger : public Trigger<> {
}
};
/// Trigger on ready state change
class ReadyTrigger : public Trigger<> {
public:
explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
@@ -164,4 +187,5 @@ template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts.
AlarmControlPanel *parent_;
};
} // namespace esphome::alarm_control_panel
} // namespace alarm_control_panel
} // namespace esphome

View File

@@ -15,8 +15,10 @@ namespace alpha3 {
namespace espbt = esphome::esp32_ble_tracker;
static const espbt::ESPBTUUID ALPHA3_GENI_SERVICE_UUID = espbt::ESPBTUUID::from_uint16(0xfe5d);
static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID = espbt::ESPBTUUID::from_raw(
{0xa9, 0x7b, 0xb8, 0x85, 0x00, 0x1a, 0x28, 0xaa, 0x2a, 0x43, 0x6e, 0x03, 0xd1, 0xff, 0x9c, 0x85});
static const espbt::ESPBTUUID ALPHA3_GENI_CHARACTERISTIC_UUID =
espbt::ESPBTUUID::from_raw({static_cast<char>(0xa9), 0x7b, static_cast<char>(0xb8), static_cast<char>(0x85), 0x0,
0x1a, 0x28, static_cast<char>(0xaa), 0x2a, 0x43, 0x6e, 0x3, static_cast<char>(0xd1),
static_cast<char>(0xff), static_cast<char>(0x9c), static_cast<char>(0x85)});
static const int16_t GENI_RESPONSE_HEADER_LENGTH = 13;
static const size_t GENI_RESPONSE_TYPE_LENGTH = 8;

View File

@@ -12,11 +12,10 @@ void AnalogThresholdBinarySensor::setup() {
// TRUE state is defined to be when sensor is >= threshold
// so when undefined sensor value initialize to FALSE
if (std::isnan(sensor_value)) {
this->raw_state_ = false;
this->publish_initial_state(false);
} else {
this->raw_state_ = sensor_value >= (this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f;
this->publish_initial_state(this->raw_state_);
this->publish_initial_state(sensor_value >=
(this->lower_threshold_.value() + this->upper_threshold_.value()) / 2.0f);
}
}
@@ -26,10 +25,8 @@ void AnalogThresholdBinarySensor::set_sensor(sensor::Sensor *analog_sensor) {
this->sensor_->add_on_state_callback([this](float sensor_value) {
// if there is an invalid sensor reading, ignore the change and keep the current state
if (!std::isnan(sensor_value)) {
// Use raw_state_ for hysteresis logic, not this->state which is post-filter
this->raw_state_ =
sensor_value >= (this->raw_state_ ? this->lower_threshold_.value() : this->upper_threshold_.value());
this->publish_state(this->raw_state_);
this->publish_state(sensor_value >=
(this->state ? this->lower_threshold_.value() : this->upper_threshold_.value()));
}
});
}

View File

@@ -20,7 +20,6 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
sensor::Sensor *sensor_{nullptr};
TemplatableValue<float> upper_threshold_{};
TemplatableValue<float> lower_threshold_{};
bool raw_state_{false}; // Pre-filter state for hysteresis logic
};
} // namespace analog_threshold

View File

@@ -67,10 +67,8 @@ void Anova::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_
case ESP_GATTC_SEARCH_CMPL_EVT: {
auto *chr = this->parent_->get_characteristic(ANOVA_SERVICE_UUID, ANOVA_CHARACTERISTIC_UUID);
if (chr == nullptr) {
ESP_LOGW(TAG,
"[%s] No control service found at device, not an Anova..?\n"
"[%s] Note, this component does not currently support Anova Nano.",
this->get_name().c_str(), this->get_name().c_str());
ESP_LOGW(TAG, "[%s] No control service found at device, not an Anova..?", this->get_name().c_str());
ESP_LOGW(TAG, "[%s] Note, this component does not currently support Anova Nano.", this->get_name().c_str());
break;
}
this->char_handle_ = chr->handle;

View File

@@ -27,13 +27,12 @@ from esphome.const import (
CONF_SERVICE,
CONF_SERVICES,
CONF_TAG,
CONF_THEN,
CONF_TRIGGER_ID,
CONF_VARIABLES,
)
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.types import ConfigFragmentType, ConfigType
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import TemplateArgsType
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -64,21 +63,17 @@ HomeAssistantActionResponseTrigger = api_ns.class_(
"HomeAssistantActionResponseTrigger", automation.Trigger
)
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
APIRespondAction = api_ns.class_("APIRespondAction", automation.Action)
APIUnregisterServiceCallAction = api_ns.class_(
"APIUnregisterServiceCallAction", automation.Action
)
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
ListEntitiesServicesArgument = api_ns.class_("ListEntitiesServicesArgument")
SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
"bool": cg.bool_,
SERVICE_ARG_NATIVE_TYPES = {
"bool": bool,
"int": cg.int32,
"float": cg.float_,
"float": float,
"string": cg.std_string,
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
"bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(float).operator("const").operator("ref"),
"string[]": cg.FixedVector.template(cg.std_string)
.operator("const")
.operator("ref"),
@@ -107,85 +102,6 @@ def validate_encryption_key(value):
return value
CONF_SUPPORTS_RESPONSE = "supports_response"
# Enum values in api::enums namespace
enums_ns = api_ns.namespace("enums")
SUPPORTS_RESPONSE_OPTIONS = {
"none": enums_ns.SUPPORTS_RESPONSE_NONE,
"optional": enums_ns.SUPPORTS_RESPONSE_OPTIONAL,
"only": enums_ns.SUPPORTS_RESPONSE_ONLY,
"status": enums_ns.SUPPORTS_RESPONSE_STATUS,
}
def _auto_detect_supports_response(config: ConfigType) -> ConfigType:
"""Auto-detect supports_response based on api.respond usage in the action's then block.
- If api.respond with data found: set to "optional" (unless user explicitly set)
- If api.respond without data found: set to "status" (unless user explicitly set)
- If no api.respond found: set to "none" (unless user explicitly set)
"""
def scan_actions(items: ConfigFragmentType) -> tuple[bool, bool]:
"""Recursively scan actions for api.respond.
Returns: (found, has_data) tuple - has_data is True if ANY api.respond has data
"""
found_any = False
has_data_any = False
if isinstance(items, list):
for item in items:
found, has_data = scan_actions(item)
if found:
found_any = True
has_data_any = has_data_any or has_data
elif isinstance(items, dict):
# Check if this is an api.respond action
if "api.respond" in items:
respond_config = items["api.respond"]
has_data = isinstance(respond_config, dict) and "data" in respond_config
return True, has_data
# Recursively check all values
for value in items.values():
found, has_data = scan_actions(value)
if found:
found_any = True
has_data_any = has_data_any or has_data
return found_any, has_data_any
then = config.get(CONF_THEN, [])
action_name = config.get(CONF_ACTION)
found, has_data = scan_actions(then)
# If user explicitly set supports_response, validate and use that
if CONF_SUPPORTS_RESPONSE in config:
user_value = config[CONF_SUPPORTS_RESPONSE]
# Validate: "only" requires api.respond with data
if user_value == "only" and not has_data:
raise cv.Invalid(
f"Action '{action_name}' has supports_response=only but no api.respond "
"action with 'data:' was found. Use 'status' for responses without data, "
"or add 'data:' to your api.respond action."
)
return config
# Auto-detect based on api.respond usage
if found:
config[CONF_SUPPORTS_RESPONSE] = "optional" if has_data else "status"
else:
config[CONF_SUPPORTS_RESPONSE] = "none"
return config
def _validate_supports_response(value):
"""Validate supports_response after auto-detection has set the value."""
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
ACTIONS_SCHEMA = automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
@@ -196,20 +112,10 @@ ACTIONS_SCHEMA = automation.validate_automation(
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
}
),
# No default - auto-detected by _auto_detect_supports_response
cv.Optional(CONF_SUPPORTS_RESPONSE): cv.enum(
SUPPORTS_RESPONSE_OPTIONS, lower=True
),
},
cv.All(
cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
cv.rename_key(CONF_SERVICE, CONF_ACTION),
_auto_detect_supports_response,
# Re-validate supports_response after auto-detection sets it
cv.Schema(
{cv.Required(CONF_SUPPORTS_RESPONSE): _validate_supports_response},
extra=cv.ALLOW_EXTRA,
),
),
)
@@ -226,6 +132,32 @@ def _encryption_schema(config):
return ENCRYPTION_SCHEMA(config)
def _validate_api_config(config: ConfigType) -> ConfigType:
"""Validate API configuration with mutual exclusivity check and deprecation warning."""
# Check if both password and encryption are configured
has_password = CONF_PASSWORD in config and config[CONF_PASSWORD]
has_encryption = CONF_ENCRYPTION in config
if has_password and has_encryption:
raise cv.Invalid(
"The 'password' and 'encryption' options are mutually exclusive. "
"The API client only supports one authentication method at a time. "
"Please remove one of them. "
"Note: 'password' authentication is deprecated and will be removed in version 2026.1.0. "
"We strongly recommend using 'encryption' instead for better security."
)
# Warn about password deprecation
if has_password:
_LOGGER.warning(
"API 'password' authentication has been deprecated since May 2022 and will be removed in version 2026.1.0. "
"Please migrate to the 'encryption' configuration. "
"See https://esphome.io/components/api.html#configuration-variables"
)
return config
def _consume_api_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for API component."""
from esphome.components import socket
@@ -242,17 +174,7 @@ CONFIG_SCHEMA = cv.All(
{
cv.GenerateID(): cv.declare_id(APIServer),
cv.Optional(CONF_PORT, default=6053): cv.port,
# Removed in 2026.1.0 - kept to provide helpful error message
cv.Optional(CONF_PASSWORD): cv.invalid(
"The 'password' option has been removed in ESPHome 2026.1.0.\n"
"Password authentication was deprecated in May 2022.\n"
"Please migrate to encryption for secure API communication:\n\n"
"api:\n"
" encryption:\n"
" key: !secret api_encryption_key\n\n"
"Generate a key with: openssl rand -base64 32\n"
"Or visit https://esphome.io/components/api/#configuration-variables"
),
cv.Optional(CONF_PASSWORD, default=""): cv.string_strict,
cv.Optional(
CONF_REBOOT_TIMEOUT, default="15min"
): cv.positive_time_period_milliseconds,
@@ -314,12 +236,13 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
_validate_api_config,
_consume_api_sockets,
)
@coroutine_with_priority(CoroPriority.WEB)
async def to_code(config: ConfigType) -> None:
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -327,6 +250,9 @@ async def to_code(config: ConfigType) -> None:
CORE.register_controller()
cg.add(var.set_port(config[CONF_PORT]))
if config[CONF_PASSWORD]:
cg.add_define("USE_API_PASSWORD")
cg.add(var.set_password(config[CONF_PASSWORD]))
cg.add(var.set_reboot_timeout(config[CONF_REBOOT_TIMEOUT]))
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
if CONF_LISTEN_BACKLOG in config:
@@ -353,61 +279,20 @@ async def to_code(config: ConfigType) -> None:
# Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.Pvariable] = []
for conf in actions:
func_args: list[tuple[MockObj, str]] = []
service_template_args: list[MockObj] = [] # User service argument types
# Determine supports_response mode
# cv.enum returns the key with enum_value attribute containing the MockObj
supports_response_key = conf[CONF_SUPPORTS_RESPONSE]
supports_response = supports_response_key.enum_value
is_none = supports_response_key == "none"
is_optional = supports_response_key == "optional"
# Add call_id and return_response based on supports_response mode
# These must match the C++ Trigger template arguments
# - none: no extra args
# - status: call_id only (for reporting success/error without data)
# - only: call_id only (response always expected with data)
# - optional: call_id + return_response (client decides)
if not is_none:
# call_id is present for "optional", "only", and "status"
func_args.append((cg.uint32, "call_id"))
# return_response only present for "optional"
if is_optional:
func_args.append((cg.bool_, "return_response"))
service_arg_names: list[str] = []
template_args = []
func_args = []
service_arg_names = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
service_template_args.append(native)
template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)
# Template args: supports_response mode, then user service arg types
templ = cg.TemplateArguments(supports_response, *service_template_args)
templ = cg.TemplateArguments(*template_args)
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID],
templ,
conf[CONF_ACTION],
service_arg_names,
conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
)
triggers.append(trigger)
auto = await automation.build_automation(trigger, func_args, conf)
# For non-none response modes, automatically append unregister action
# This ensures the call is unregistered after all actions complete (including async ones)
if not is_none:
arg_types = [arg[0] for arg in func_args]
action_templ = cg.TemplateArguments(*arg_types)
unregister_id = ID(
f"{conf[CONF_TRIGGER_ID]}__unregister",
is_declaration=True,
type=APIUnregisterServiceCallAction.template(action_templ),
)
unregister_action = cg.new_Pvariable(
unregister_id,
var,
)
cg.add(auto.add_actions([unregister_action]))
await automation.build_automation(trigger, func_args, conf)
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
@@ -653,80 +538,6 @@ async def homeassistant_tag_scanned_to_code(config, action_id, template_arg, arg
return var
CONF_SUCCESS = "success"
CONF_ERROR_MESSAGE = "error_message"
def _validate_api_respond_data(config):
"""Set flag during validation so AUTO_LOAD can include json component."""
if CONF_DATA in config:
CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
return config
API_RESPOND_ACTION_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),
cv.Optional(CONF_SUCCESS, default=True): cv.templatable(cv.boolean),
cv.Optional(CONF_ERROR_MESSAGE, default=""): cv.templatable(cv.string),
cv.Optional(CONF_DATA): cv.lambda_,
}
),
_validate_api_respond_data,
)
@automation.register_action(
"api.respond",
APIRespondAction,
API_RESPOND_ACTION_SCHEMA,
)
async def api_respond_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
# Validate that api.respond is used inside an API action context.
# We can't easily validate this at config time since the schema validation
# doesn't have access to the parent action context. Validating here in to_code
# is still much better than a cryptic C++ compile error.
has_call_id = any(name == "call_id" for _, name in args)
if not has_call_id:
raise EsphomeError(
"api.respond can only be used inside an API action's 'then:' block. "
"The 'call_id' variable is required to send a response."
)
cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES")
serv = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, serv)
# Check if we're in optional mode (has return_response arg)
is_optional = any(name == "return_response" for _, name in args)
if is_optional:
cg.add(var.set_is_optional_mode(True))
templ = await cg.templatable(config[CONF_SUCCESS], args, cg.bool_)
cg.add(var.set_success(templ))
templ = await cg.templatable(config[CONF_ERROR_MESSAGE], args, cg.std_string)
cg.add(var.set_error_message(templ))
if CONF_DATA in config:
cg.add_define("USE_API_USER_DEFINED_ACTION_RESPONSES_JSON")
# Lambda populates the JsonObject root - no return value needed
lambda_ = await cg.process_lambda(
config[CONF_DATA],
args + [(cg.JsonObject, "root")],
return_type=cg.void,
)
cg.add(var.set_data(lambda_))
return var
API_CONNECTED_CONDITION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(APIServer),

View File

@@ -7,7 +7,10 @@ service APIConnection {
option (needs_setup_connection) = false;
option (needs_authentication) = false;
}
// REMOVED in ESPHome 2026.1.0: rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse)
rpc authenticate (AuthenticationRequest) returns (AuthenticationResponse) {
option (needs_setup_connection) = false;
option (needs_authentication) = false;
}
rpc disconnect (DisconnectRequest) returns (DisconnectResponse) {
option (needs_setup_connection) = false;
option (needs_authentication) = false;
@@ -79,13 +82,14 @@ service APIConnection {
// * VarInt denoting the type of message.
// * The message object encoded as a ProtoBuf message
// The connection is established in 2 steps:
// The connection is established in 4 steps:
// * First, the client connects to the server and sends a "Hello Request" identifying itself
// * The server responds with a "Hello Response" and the connection is authenticated
// * The server responds with a "Hello Response" and selects the protocol version
// * After receiving this message, the client attempts to authenticate itself using
// the password and a "Connect Request"
// * The server responds with a "Connect Response" and notifies of invalid password.
// If anything in this initial process fails, the connection must immediately closed
// by both sides and _no_ disconnection message is to be sent.
// Note: Password authentication via AuthenticationRequest/AuthenticationResponse (message IDs 3, 4)
// was removed in ESPHome 2026.1.0. Those message IDs are reserved and should not be reused.
// Message sent at the beginning of each connection
// Can only be sent by the client and only at the beginning of the connection
@@ -98,7 +102,7 @@ message HelloRequest {
// For example "Home Assistant"
// Not strictly necessary to send but nice for debugging
// purposes.
string client_info = 1;
string client_info = 1 [(pointer_to_buffer) = true];
uint32 api_version_major = 2;
uint32 api_version_minor = 3;
}
@@ -126,23 +130,25 @@ message HelloResponse {
string name = 4;
}
// DEPRECATED in ESPHome 2026.1.0 - Password authentication is no longer supported.
// These messages are kept for protocol documentation but are not processed by the server.
// Use noise encryption instead: https://esphome.io/components/api/#configuration-variables
// Message sent at the beginning of each connection to authenticate the client
// Can only be sent by the client and only at the beginning of the connection
message AuthenticationRequest {
option (id) = 3;
option (source) = SOURCE_CLIENT;
option (no_delay) = true;
option deprecated = true;
option (ifdef) = "USE_API_PASSWORD";
string password = 1;
// The password to log in with
string password = 1 [(pointer_to_buffer) = true];
}
// Confirmation of successful connection. After this the connection is available for all traffic.
// Can only be sent by the server and only at the beginning of the connection
message AuthenticationResponse {
option (id) = 4;
option (source) = SOURCE_SERVER;
option (no_delay) = true;
option deprecated = true;
option (ifdef) = "USE_API_PASSWORD";
bool invalid_password = 1;
}
@@ -199,9 +205,7 @@ message DeviceInfoResponse {
option (id) = 10;
option (source) = SOURCE_SERVER;
// Deprecated in ESPHome 2026.1.0, but kept for backward compatibility
// with older ESPHome versions that still send this field.
bool uses_password = 1 [deprecated = true];
bool uses_password = 1 [(field_ifdef) = "USE_API_PASSWORD"];
// The name of the node, given by "App.set_name()"
string name = 2;
@@ -585,7 +589,6 @@ enum SensorStateClass {
STATE_CLASS_MEASUREMENT = 1;
STATE_CLASS_TOTAL_INCREASING = 2;
STATE_CLASS_TOTAL = 3;
STATE_CLASS_MEASUREMENT_ANGLE = 4;
}
// Deprecated in API version 1.5
@@ -792,7 +795,7 @@ message HomeassistantActionResponse {
uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
bool success = 2; // Whether the service call succeeded
string error_message = 3; // Error message if success = false
bytes response_data = 4 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
}
// ==================== IMPORT HOME ASSISTANT STATES ====================
@@ -837,7 +840,7 @@ message GetTimeResponse {
option (no_delay) = true;
fixed32 epoch_seconds = 1;
string timezone = 2;
string timezone = 2 [(pointer_to_buffer) = true];
}
// ==================== USER-DEFINES SERVICES ====================
@@ -851,14 +854,6 @@ enum ServiceArgType {
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6;
SERVICE_ARG_TYPE_STRING_ARRAY = 7;
}
enum SupportsResponseType {
SUPPORTS_RESPONSE_NONE = 0;
SUPPORTS_RESPONSE_OPTIONAL = 1;
SUPPORTS_RESPONSE_ONLY = 2;
// Status-only response - reports success/error without data payload
// Value is higher to avoid conflicts with future Home Assistant values
SUPPORTS_RESPONSE_STATUS = 100;
}
message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
@@ -872,7 +867,6 @@ message ListEntitiesServicesResponse {
string name = 1;
fixed32 key = 2;
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
@@ -895,21 +889,6 @@ message ExecuteServiceRequest {
fixed32 key = 1;
repeated ExecuteServiceArgument args = 2 [(fixed_vector) = true];
uint32 call_id = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
bool return_response = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES"];
}
// Message sent by ESPHome to Home Assistant with service execution response data
message ExecuteServiceResponse {
option (id) = 131;
option (source) = SOURCE_SERVER;
option (no_delay) = true;
option (ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES";
uint32 call_id = 1; // Matches the call_id from ExecuteServiceRequest
bool success = 2; // Whether the service execution succeeded
string error_message = 3; // Error message if success = false
bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_USER_DEFINED_ACTION_RESPONSES_JSON"];
}
// ==================== CAMERA ====================
@@ -1097,85 +1076,6 @@ message ClimateCommandRequest {
uint32 device_id = 24 [(field_ifdef) = "USE_DEVICES"];
}
// ==================== WATER_HEATER ====================
enum WaterHeaterMode {
WATER_HEATER_MODE_OFF = 0;
WATER_HEATER_MODE_ECO = 1;
WATER_HEATER_MODE_ELECTRIC = 2;
WATER_HEATER_MODE_PERFORMANCE = 3;
WATER_HEATER_MODE_HIGH_DEMAND = 4;
WATER_HEATER_MODE_HEAT_PUMP = 5;
WATER_HEATER_MODE_GAS = 6;
}
message ListEntitiesWaterHeaterResponse {
option (id) = 132;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
string object_id = 1;
fixed32 key = 2;
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
float min_temperature = 8;
float max_temperature = 9;
float target_temperature_step = 10;
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
// Bitmask of WaterHeaterFeature flags
uint32 supported_features = 12;
}
message WaterHeaterStateResponse {
option (id) = 133;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
fixed32 key = 1;
float current_temperature = 2;
float target_temperature = 3;
WaterHeaterMode mode = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// Bitmask of current state flags (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// Bitmask for WaterHeaterCommandRequest.has_fields
enum WaterHeaterCommandHasField {
WATER_HEATER_COMMAND_HAS_NONE = 0;
WATER_HEATER_COMMAND_HAS_MODE = 1;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2;
WATER_HEATER_COMMAND_HAS_STATE = 4;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8;
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16;
}
message WaterHeaterCommandRequest {
option (id) = 134;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_WATER_HEATER";
option (no_delay) = true;
option (base_class) = "CommandProtoMessage";
fixed32 key = 1;
// Bitmask of which fields are set (see WaterHeaterCommandHasField)
uint32 has_fields = 2;
WaterHeaterMode mode = 3;
float target_temperature = 4;
uint32 device_id = 5 [(field_ifdef) = "USE_DEVICES"];
// State flags bitmask (bit 0 = away, bit 1 = on)
uint32 state = 6;
float target_temperature_low = 7;
float target_temperature_high = 8;
}
// ==================== NUMBER ====================
enum NumberMode {
NUMBER_MODE_AUTO = 0;
@@ -1288,7 +1188,7 @@ message ListEntitiesSirenResponse {
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
bool disabled_by_default = 6;
repeated string tones = 7 [(container_pointer_no_template) = "FixedVector<const char *>"];
repeated string tones = 7;
bool supports_duration = 8;
bool supports_volume = 9;
EntityCategory entity_category = 10;
@@ -1688,7 +1588,7 @@ message BluetoothGATTWriteRequest {
uint32 handle = 2;
bool response = 3;
bytes data = 4;
bytes data = 4 [(pointer_to_buffer) = true];
}
message BluetoothGATTReadDescriptorRequest {
@@ -1708,7 +1608,7 @@ message BluetoothGATTWriteDescriptorRequest {
uint64 address = 1;
uint32 handle = 2;
bytes data = 3;
bytes data = 3 [(pointer_to_buffer) = true];
}
message BluetoothGATTNotifyRequest {
@@ -1933,7 +1833,7 @@ message VoiceAssistantAudio {
option (source) = SOURCE_BOTH;
option (ifdef) = "USE_VOICE_ASSISTANT";
bytes data = 1 [(pointer_to_buffer) = true];
bytes data = 1;
bool end = 2;
}
@@ -2421,7 +2321,7 @@ message ZWaveProxyFrame {
option (ifdef) = "USE_ZWAVE_PROXY";
option (no_delay) = true;
bytes data = 1;
bytes data = 1 [(pointer_to_buffer) = true];
}
enum ZWaveProxyRequestType {
@@ -2435,5 +2335,5 @@ message ZWaveProxyRequest {
option (ifdef) = "USE_ZWAVE_PROXY";
ZWaveProxyRequestType type = 1;
bytes data = 2;
bytes data = 2 [(pointer_to_buffer) = true];
}

File diff suppressed because it is too large Load Diff

View File

@@ -9,23 +9,32 @@
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
#include <functional>
#include <vector>
namespace esphome::api {
// Client information structure
struct ClientInfo {
std::string name; // Client name from Hello message
std::string peername; // IP:port from socket
};
// Keepalive timeout in milliseconds
static constexpr uint32_t KEEPALIVE_TIMEOUT_MS = 60000;
// Maximum number of entities to process in a single batch during initial state/info sending
// API 1.14+ clients compute object_id client-side, so messages are smaller and we can fit more per batch
// TODO: Remove MAX_INITIAL_PER_BATCH_LEGACY before 2026.7.0 - all clients should support API 1.14 by then
static constexpr size_t MAX_INITIAL_PER_BATCH_LEGACY = 24; // For clients < API 1.14 (includes object_id)
static constexpr size_t MAX_INITIAL_PER_BATCH = 34; // For clients >= API 1.14 (no object_id)
// Verify MAX_MESSAGES_PER_BATCH (defined in api_frame_helper.h) can hold the initial batch
static_assert(MAX_MESSAGES_PER_BATCH >= MAX_INITIAL_PER_BATCH,
"MAX_MESSAGES_PER_BATCH must be >= MAX_INITIAL_PER_BATCH");
// This was increased from 20 to 24 after removing the unique_id field from entity info messages,
// which reduced message sizes allowing more entities per batch without exceeding packet limits
static constexpr size_t MAX_INITIAL_PER_BATCH = 24;
// Maximum number of packets to process in a single batch (platform-dependent)
// This limit exists to prevent stack overflow from the PacketInfo array in process_batch_
// Each PacketInfo is 8 bytes, so 64 * 8 = 512 bytes, 32 * 8 = 256 bytes
#if defined(USE_ESP32) || defined(USE_HOST)
static constexpr size_t MAX_PACKETS_PER_BATCH = 64; // ESP32 has 8KB+ stack, HOST has plenty
#else
static constexpr size_t MAX_PACKETS_PER_BATCH = 32; // ESP8266/RP2040/etc have smaller stacks
#endif
class APIConnection final : public APIServerConnection {
public:
@@ -167,11 +176,6 @@ class APIConnection final : public APIServerConnection {
void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override;
#endif
#ifdef USE_WATER_HEATER
bool send_water_heater_state(water_heater::WaterHeater *water_heater);
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override;
#endif
#ifdef USE_EVENT
void send_event(event::Event *event, const char *event_type);
#endif
@@ -193,17 +197,16 @@ class APIConnection final : public APIServerConnection {
void on_get_time_response(const GetTimeResponse &value) override;
#endif
bool send_hello_response(const HelloRequest &msg) override;
#ifdef USE_API_PASSWORD
bool send_authenticate_response(const AuthenticationRequest &msg) override;
#endif
bool send_disconnect_response(const DisconnectRequest &msg) override;
bool send_ping_response(const PingRequest &msg) override;
bool send_device_info_response(const DeviceInfoRequest &msg) override;
void list_entities(const ListEntitiesRequest &msg) override { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
void list_entities(const ListEntitiesRequest &msg) override { this->list_entities_iterator_.begin(); }
void subscribe_states(const SubscribeStatesRequest &msg) override {
this->flags_.state_subscription = true;
// Start initial state iterator only if no iterator is active
// If list_entities is running, we'll start initial_state when it completes
if (this->active_iterator_ == ActiveIterator::NONE) {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
this->initial_state_iterator_.begin();
}
void subscribe_logs(const SubscribeLogsRequest &msg) override {
this->flags_.log_subscription = msg.level;
@@ -220,13 +223,6 @@ class APIConnection final : public APIServerConnection {
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
void execute_service(const ExecuteServiceRequest &msg) override;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void send_execute_service_response(uint32_t call_id, bool success, StringRef error_message,
const uint8_t *response_data, size_t response_data_len);
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#ifdef USE_API_NOISE
bool send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) override;
@@ -248,6 +244,9 @@ class APIConnection final : public APIServerConnection {
}
void on_fatal_error() override;
#ifdef USE_API_PASSWORD
void on_unauthenticated_access() override;
#endif
void on_no_setup_connection() override;
ProtoWriteBuffer create_buffer(uint32_t reserve_size) override {
// FIXME: ensure no recursive writes can happen
@@ -274,18 +273,13 @@ class APIConnection final : public APIServerConnection {
bool try_to_clear_buffer(bool log_out_of_space);
bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
const char *get_name() const { return this->helper_->get_client_name(); }
/// Get peer name (IP address) - cached at connection init time
const char *get_peername() const { return this->helper_->get_client_peername(); }
const std::string &get_name() const { return this->client_info_.name; }
const std::string &get_peername() const { return this->client_info_.peername; }
protected:
// Helper function to handle authentication completion
void complete_authentication_();
#ifdef USE_CAMERA
void try_send_camera_image_();
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_();
#endif
@@ -309,24 +303,25 @@ class APIConnection final : public APIServerConnection {
APIConnection *conn, uint32_t remaining_size, bool is_single) {
// Set common fields that are shared by all entity types
msg.key = entity->get_object_id_hash();
// API 1.14+ clients compute object_id client-side from the entity name
// For older clients, we must send object_id for backward compatibility
// See: https://github.com/esphome/backlog/issues/76
// TODO: Remove this backward compat code before 2026.7.0 - all clients should support API 1.14 by then
// Buffer must remain in scope until encode_message_to_buffer is called
char object_id_buf[OBJECT_ID_MAX_LEN];
if (!conn->client_supports_api_version(1, 14)) {
msg.object_id = entity->get_object_id_to(object_id_buf);
// Try to use static reference first to avoid allocation
StringRef static_ref = entity->get_object_id_ref_for_api_();
// Store dynamic string outside the if-else to maintain lifetime
std::string object_id;
if (!static_ref.empty()) {
msg.set_object_id(static_ref);
} else {
// Dynamic case - need to allocate
object_id = entity->get_object_id();
msg.set_object_id(StringRef(object_id));
}
if (entity->has_own_name()) {
msg.name = entity->get_name();
msg.set_name(entity->get_name());
}
// Set common EntityBase properties
#ifdef USE_ENTITY_ICON
msg.icon = entity->get_icon_ref();
msg.set_icon(entity->get_icon_ref());
#endif
msg.disabled_by_default = entity->is_disabled_by_default();
msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
@@ -341,24 +336,16 @@ class APIConnection final : public APIServerConnection {
inline bool check_voice_assistant_api_connection_() const;
#endif
// Get the max batch size based on client API version
// API 1.14+ clients don't receive object_id, so messages are smaller and more fit per batch
// TODO: Remove this method before 2026.7.0 and use MAX_INITIAL_PER_BATCH directly
size_t get_max_batch_size_() const {
return this->client_supports_api_version(1, 14) ? MAX_INITIAL_PER_BATCH : MAX_INITIAL_PER_BATCH_LEGACY;
}
// Helper method to process multiple entities from an iterator in a batch
template<typename Iterator> void process_iterator_batch_(Iterator &iterator) {
size_t initial_size = this->deferred_batch_.size();
size_t max_batch = this->get_max_batch_size_();
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < MAX_INITIAL_PER_BATCH) {
iterator.advance();
}
// If the batch is full, process it immediately
// Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
if (this->deferred_batch_.size() >= max_batch) {
if (this->deferred_batch_.size() >= MAX_INITIAL_PER_BATCH) {
this->process_batch_();
}
}
@@ -462,12 +449,6 @@ class APIConnection final : public APIServerConnection {
static uint16_t try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_WATER_HEATER
static uint16_t try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single);
#endif
#ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);
@@ -502,27 +483,18 @@ class APIConnection final : public APIServerConnection {
std::unique_ptr<APIFrameHelper> helper_;
APIServer *parent_;
// Group 2: Iterator union (saves ~16 bytes vs separate iterators)
// These iterators are never active simultaneously - list_entities runs to completion
// before initial_state begins, so we use a union with explicit construction/destruction.
enum class ActiveIterator : uint8_t { NONE, LIST_ENTITIES, INITIAL_STATE };
union IteratorUnion {
ListEntitiesIterator list_entities;
InitialStateIterator initial_state;
// Constructor/destructor do nothing - use placement new/explicit destructor
IteratorUnion() {}
~IteratorUnion() {}
} iterator_storage_;
// Helper methods for iterator lifecycle management
void destroy_active_iterator_();
void begin_iterator_(ActiveIterator type);
// Group 2: Larger objects (must be 4-byte aligned)
// These contain vectors/pointers internally, so putting them early ensures good alignment
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
#ifdef USE_CAMERA
std::unique_ptr<camera::CameraImageReader> image_reader_;
#endif
// Group 3: 4-byte types
// Group 3: Client info struct (24 bytes on 32-bit: 2 strings × 12 bytes each)
ClientInfo client_info_;
// Group 4: 4-byte types
uint32_t last_traffic_;
#ifdef USE_API_HOMEASSISTANT_STATES
int state_subs_at_ = -1;
@@ -533,9 +505,28 @@ class APIConnection final : public APIServerConnection {
class MessageCreator {
public:
// Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
// Constructor for const char * (Event types - no allocation needed)
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
// Delete copy operations - MessageCreator should only be moved
MessageCreator(const MessageCreator &other) = delete;
MessageCreator &operator=(const MessageCreator &other) = delete;
// Move constructor
MessageCreator(MessageCreator &&other) noexcept : data_(other.data_) { other.data_.function_ptr = nullptr; }
// Move assignment
MessageCreator &operator=(MessageCreator &&other) noexcept {
if (this != &other) {
data_ = other.data_;
other.data_.function_ptr = nullptr;
}
return *this;
}
// Call operator - uses message_type to determine union type
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint8_t message_type) const;
@@ -544,7 +535,7 @@ class APIConnection final : public APIServerConnection {
union Data {
MessageCreatorPtr function_ptr;
const char *const_char_ptr;
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
};
// Generic batching mechanism for both state updates and entity info
@@ -557,7 +548,7 @@ class APIConnection final : public APIServerConnection {
// Constructor for creating BatchItem
BatchItem(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size)
: entity(entity), creator(creator), message_type(message_type), estimated_size(estimated_size) {}
: entity(entity), creator(std::move(creator)), message_type(message_type), estimated_size(estimated_size) {}
};
std::vector<BatchItem> items;
@@ -629,9 +620,7 @@ class APIConnection final : public APIServerConnection {
// 2-byte types immediately after flags_ (no padding between them)
uint16_t client_api_version_major_{0};
uint16_t client_api_version_minor_{0};
// 1-byte type to fill padding
ActiveIterator active_iterator_{ActiveIterator::NONE};
// Total: 2 (flags) + 2 + 2 + 1 = 7 bytes, then 1 byte padding to next 4-byte boundary
// Total: 2 (flags) + 2 + 2 = 6 bytes, then 2 bytes padding to next 4-byte boundary
uint32_t get_batch_delay_ms_() const;
// Message will use 8 more bytes than the minimum size, and typical
@@ -727,12 +716,12 @@ class APIConnection final : public APIServerConnection {
}
// Fall back to scheduled batching
return this->schedule_message_(entity, creator, message_type, estimated_size);
return this->schedule_message_(entity, std::move(creator), message_type, estimated_size);
}
// Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item(entity, creator, message_type, estimated_size);
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);
return this->schedule_batch_();
}
@@ -749,8 +738,6 @@ class APIConnection final : public APIServerConnection {
return this->schedule_batch_();
}
// Helper function to log client messages with name and peername
void log_client_(int level, const LogString *message);
// Helper function to log API errors with errno
void log_warning_(const LogString *message, APIError err);
// Helper to handle fatal errors with logging

View File

@@ -1,5 +1,6 @@
#include "api_frame_helper.h"
#ifdef USE_API
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
@@ -12,29 +13,12 @@ namespace esphome::api {
static const char *const TAG = "api.frame_helper";
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
#define HELPER_LOG(msg, ...) \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Received frame: %s", \
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
@@ -245,8 +229,6 @@ APIError APIFrameHelper::init_common_() {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
// Cache peername now while socket is valid - needed for error logging after socket failure
this->socket_->getpeername_to(this->client_peername_);
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;

View File

@@ -29,28 +29,24 @@ static constexpr uint16_t MAX_MESSAGE_SIZE = 8192; // 8 KiB for ESP8266
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768; // 32 KiB for ESP32 and other platforms
#endif
// Maximum number of messages to batch in a single write operation
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
// Forward declaration
struct ClientInfo;
class ProtoWriteBuffer;
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32;
struct ReadPacketBuffer {
const uint8_t *data; // Points directly into frame helper's rx_buf_ (valid until next read_packet call)
uint16_t data_len;
uint16_t type;
};
// Packed message info structure to minimize memory usage
struct MessageInfo {
// Packed packet info structure to minimize memory usage
struct PacketInfo {
uint16_t offset; // Offset in buffer where message starts
uint16_t payload_size; // Size of the message payload
uint8_t message_type; // Message type (0-255)
MessageInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
PacketInfo(uint8_t type, uint16_t off, uint16_t size) : offset(off), payload_size(size), message_type(type) {}
};
enum class APIError : uint16_t {
@@ -86,23 +82,14 @@ const LogString *api_error_to_logstr(APIError err);
class APIFrameHelper {
public:
APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {}
// Get client name (null-terminated)
const char *get_client_name() const { return this->client_name_; }
// Get client peername/IP (null-terminated, cached at init time for availability after socket failure)
const char *get_client_peername() const { return this->client_peername_; }
// Set client name from buffer with length (truncates if needed)
void set_client_name(const char *name, size_t len) {
size_t copy_len = std::min(len, sizeof(this->client_name_) - 1);
memcpy(this->client_name_, name, copy_len);
this->client_name_[copy_len] = '\0';
}
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: socket_(std::move(socket)), client_info_(client_info) {}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop();
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
std::string getpeername() { return socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
state_ = State::CLOSED;
@@ -120,32 +107,11 @@ class APIFrameHelper {
}
return APIError::OK;
}
/// Toggle TCP_NODELAY socket option to control Nagle's algorithm.
///
/// This is used to allow log messages to coalesce (Nagle enabled) while keeping
/// state updates low-latency (NODELAY enabled). Without this, many small log
/// packets fill the TCP send buffer, crowding out important state updates.
///
/// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040)
/// this is just a boolean assignment; on other platforms it's a lightweight syscall.
///
/// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle
/// @return true if successful or already in desired state
bool set_nodelay(bool enable) {
if (this->nodelay_enabled_ == enable)
return true;
int val = enable ? 1 : 0;
int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int));
if (err == 0) {
this->nodelay_enabled_ = enable;
}
return err == 0;
}
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
// Write multiple protobuf messages in a single operation
// messages contains (message_type, offset, length) for each message in the buffer
// Write multiple protobuf packets in a single operation
// packets contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each
virtual APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) = 0;
virtual APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) = 0;
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() const { return frame_header_padding_; }
// Get the frame footer size required by this protocol
@@ -161,6 +127,12 @@ class APIFrameHelper {
// Use swap trick since shrink_to_fit() is non-binding and may be ignored
std::vector<uint8_t>().swap(this->rx_buf_);
}
// reusable_iovs_: Safe to release unconditionally.
// Only used within write_protobuf_packets() calls - cleared at start,
// populated with pointers, used for writev(), then function returns.
// The iovecs contain stale pointers after the call (data was either sent
// or copied to tx_buf_), and are cleared on next write_protobuf_packets().
std::vector<struct iovec>().swap(this->reusable_iovs_);
}
protected:
@@ -214,12 +186,12 @@ class APIFrameHelper {
// Containers (size varies, but typically 12+ bytes on 32-bit)
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
std::vector<struct iovec> reusable_iovs_;
std::vector<uint8_t> rx_buf_;
// Client name buffer - stores name from Hello message or initial peername
char client_name_[CLIENT_INFO_NAME_MAX_LEN]{};
// Cached peername/IP address - captured at init time for availability after socket failure
char client_peername_[socket::SOCKADDR_STR_LEN]{};
// Pointer to client info (4 bytes on 32-bit)
// Note: The pointed-to ClientInfo object must outlive this APIFrameHelper instance.
const ClientInfo *client_info_{nullptr};
// Group smaller types together
uint16_t rx_buf_len_ = 0;
@@ -229,10 +201,7 @@ class APIFrameHelper {
uint8_t tx_buf_head_{0};
uint8_t tx_buf_tail_{0};
uint8_t tx_buf_count_{0};
// Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true
// since init_common_() enables NODELAY. Used by set_nodelay() to allow log
// messages to coalesce while keeping state updates low-latency.
bool nodelay_enabled_{true};
// 8 bytes total, 0 bytes padding
// Common initialization for both plaintext and noise protocols
APIError init_common_();

View File

@@ -24,29 +24,12 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
#endif
static constexpr size_t PROLOGUE_INIT_LEN = 12; // strlen("NoiseAPIInit")
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
#define HELPER_LOG(msg, ...) \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Received frame: %s", \
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
@@ -432,12 +415,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize to include MAC space (required for Noise encryption)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
PacketInfo packet{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
APIError aerr = state_action_();
if (aerr != APIError::OK) {
return aerr;
@@ -447,20 +430,20 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
return APIError::WOULD_BLOCK;
}
if (messages.empty()) {
if (packets.empty()) {
return APIError::OK;
}
uint8_t *buffer_data = buffer.get_buffer()->data();
// Stack-allocated iovec array - no heap allocation
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0;
// We need to encrypt each message in place
for (const auto &msg : messages) {
// We need to encrypt each packet in place
for (const auto &packet : packets) {
// The buffer already has padding at offset
uint8_t *buf_start = buffer_data + msg.offset;
uint8_t *buf_start = buffer_data + packet.offset;
// Write noise header
buf_start[0] = 0x01; // indicator
@@ -468,10 +451,10 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
// Write message header (to be encrypted)
const uint8_t msg_offset = 3;
buf_start[msg_offset] = static_cast<uint8_t>(msg.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(msg.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(msg.payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(msg.payload_size); // data_len low byte
buf_start[msg_offset] = static_cast<uint8_t>(packet.message_type >> 8); // type high byte
buf_start[msg_offset + 1] = static_cast<uint8_t>(packet.message_type); // type low byte
buf_start[msg_offset + 2] = static_cast<uint8_t>(packet.payload_size >> 8); // data_len high byte
buf_start[msg_offset + 3] = static_cast<uint8_t>(packet.payload_size); // data_len low byte
// payload data is already in the buffer starting at offset + 7
// Make sure we have space for MAC
@@ -480,8 +463,8 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
// Encrypt the message in place
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + msg.payload_size,
4 + msg.payload_size + frame_footer_size_);
noise_buffer_set_inout(mbuf, buf_start + msg_offset, 4 + packet.payload_size,
4 + packet.payload_size + frame_footer_size_);
int err = noise_cipherstate_encrypt(send_cipher_, &mbuf);
APIError aerr =
@@ -493,14 +476,14 @@ APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, s
buf_start[1] = static_cast<uint8_t>(mbuf.size >> 8);
buf_start[2] = static_cast<uint8_t>(mbuf.size);
// Add iovec for this encrypted message
size_t msg_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
iovs.push_back({buf_start, msg_len});
total_write_len += msg_len;
// Add iovec for this encrypted packet
size_t packet_len = static_cast<size_t>(3 + mbuf.size); // indicator + size + encrypted data
this->reusable_iovs_.push_back({buf_start, packet_len});
total_write_len += packet_len;
}
// Send all encrypted messages in one writev call
return this->write_raw_(iovs.data(), iovs.size(), total_write_len);
// Send all encrypted packets in one writev call
return this->write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
@@ -556,8 +539,7 @@ APIError APINoiseFrameHelper::init_handshake_() {
if (aerr != APIError::OK)
return aerr;
// set_prologue copies it into handshakestate, so we can get rid of it now
// Use swap idiom to actually release memory (= {} only clears size, not capacity)
std::vector<uint8_t>().swap(prologue_);
prologue_ = {};
err = noise_handshakestate_start(handshake_);
aerr = handle_noise_error_(err, LOG_STR("noise_handshakestate_start"), APIError::HANDSHAKESTATE_SETUP_FAILED);

View File

@@ -9,8 +9,8 @@ namespace esphome::api {
class APINoiseFrameHelper final : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx)
: APIFrameHelper(std::move(socket)), ctx_(ctx) {
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, APINoiseContext &ctx, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info), ctx_(ctx) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
@@ -23,7 +23,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
protected:
APIError state_action_();

View File

@@ -1,6 +1,7 @@
#include "api_frame_helper_plaintext.h"
#ifdef USE_API
#ifdef USE_API_PLAINTEXT
#include "api_connection.h" // For ClientInfo struct
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
@@ -17,29 +18,12 @@ namespace esphome::api {
static const char *const TAG = "api.plaintext";
// Maximum bytes to log in hex format (168 * 3 = 504, under TX buffer size of 512)
static constexpr size_t API_MAX_LOG_BYTES = 168;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s (%s): " msg, this->client_name_, this->client_peername_, ##__VA_ARGS__)
#else
#define HELPER_LOG(msg, ...) ((void) 0)
#endif
#define HELPER_LOG(msg, ...) \
ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
#ifdef HELPER_LOG_PACKETS
#define LOG_PACKET_RECEIVED(buffer) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Received frame: %s", \
format_hex_pretty_to(hex_buf_, (buffer).data(), \
(buffer).size() < API_MAX_LOG_BYTES ? (buffer).size() : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_SENDING(data, len) \
do { \
char hex_buf_[format_hex_pretty_size(API_MAX_LOG_BYTES)]; \
ESP_LOGVV(TAG, "Sending raw: %s", \
format_hex_pretty_to(hex_buf_, data, (len) < API_MAX_LOG_BYTES ? (len) : API_MAX_LOG_BYTES)); \
} while (0)
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
#define LOG_PACKET_SENDING(data, len) ESP_LOGVV(TAG, "Sending raw: %s", format_hex_pretty(data, len).c_str())
#else
#define LOG_PACKET_RECEIVED(buffer) ((void) 0)
#define LOG_PACKET_SENDING(data, len) ((void) 0)
@@ -232,30 +216,29 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::OK;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
PacketInfo packet{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
return write_protobuf_packets(buffer, std::span<const PacketInfo>(&packet, 1));
}
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
std::span<const MessageInfo> messages) {
APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
if (messages.empty()) {
if (packets.empty()) {
return APIError::OK;
}
uint8_t *buffer_data = buffer.get_buffer()->data();
// Stack-allocated iovec array - no heap allocation
StaticVector<struct iovec, MAX_MESSAGES_PER_BATCH> iovs;
this->reusable_iovs_.clear();
this->reusable_iovs_.reserve(packets.size());
uint16_t total_write_len = 0;
for (const auto &msg : messages) {
for (const auto &packet : packets) {
// Calculate varint sizes for header layout
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.payload_size));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.message_type));
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.payload_size));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(packet.message_type));
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header
@@ -283,25 +266,25 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
//
// The message starts at offset + frame_header_padding_
// So we write the header starting at offset + frame_header_padding_ - total_header_len
uint8_t *buf_start = buffer_data + msg.offset;
uint8_t *buf_start = buffer_data + packet.offset;
uint32_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
// Encode varints directly into buffer
ProtoVarInt(msg.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
ProtoVarInt(msg.message_type)
ProtoVarInt(packet.payload_size).encode_to_buffer_unchecked(buf_start + header_offset + 1, size_varint_len);
ProtoVarInt(packet.message_type)
.encode_to_buffer_unchecked(buf_start + header_offset + 1 + size_varint_len, type_varint_len);
// Add iovec for this message (header + payload)
size_t msg_len = static_cast<size_t>(total_header_len + msg.payload_size);
iovs.push_back({buf_start + header_offset, msg_len});
total_write_len += msg_len;
// Add iovec for this packet (header + payload)
size_t packet_len = static_cast<size_t>(total_header_len + packet.payload_size);
this->reusable_iovs_.push_back({buf_start + header_offset, packet_len});
total_write_len += packet_len;
}
// Send all messages in one writev call
return write_raw_(iovs.data(), iovs.size(), total_write_len);
// Send all packets in one writev call
return write_raw_(this->reusable_iovs_.data(), this->reusable_iovs_.size(), total_write_len);
}
} // namespace esphome::api

View File

@@ -7,7 +7,8 @@ namespace esphome::api {
class APIPlaintextFrameHelper final : public APIFrameHelper {
public:
explicit APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket, const ClientInfo *client_info)
: APIFrameHelper(std::move(socket), client_info) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
@@ -20,7 +21,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
protected:
APIError try_read_frame_();

File diff suppressed because it is too large Load Diff

View File

@@ -51,7 +51,6 @@ enum SensorStateClass : uint32_t {
STATE_CLASS_MEASUREMENT = 1,
STATE_CLASS_TOTAL_INCREASING = 2,
STATE_CLASS_TOTAL = 3,
STATE_CLASS_MEASUREMENT_ANGLE = 4,
};
#endif
enum LogLevel : uint32_t {
@@ -75,12 +74,6 @@ enum ServiceArgType : uint32_t {
SERVICE_ARG_TYPE_FLOAT_ARRAY = 6,
SERVICE_ARG_TYPE_STRING_ARRAY = 7,
};
enum SupportsResponseType : uint32_t {
SUPPORTS_RESPONSE_NONE = 0,
SUPPORTS_RESPONSE_OPTIONAL = 1,
SUPPORTS_RESPONSE_ONLY = 2,
SUPPORTS_RESPONSE_STATUS = 100,
};
#endif
#ifdef USE_CLIMATE
enum ClimateMode : uint32_t {
@@ -129,25 +122,6 @@ enum ClimatePreset : uint32_t {
CLIMATE_PRESET_ACTIVITY = 7,
};
#endif
#ifdef USE_WATER_HEATER
enum WaterHeaterMode : uint32_t {
WATER_HEATER_MODE_OFF = 0,
WATER_HEATER_MODE_ECO = 1,
WATER_HEATER_MODE_ELECTRIC = 2,
WATER_HEATER_MODE_PERFORMANCE = 3,
WATER_HEATER_MODE_HIGH_DEMAND = 4,
WATER_HEATER_MODE_HEAT_PUMP = 5,
WATER_HEATER_MODE_GAS = 6,
};
#endif
enum WaterHeaterCommandHasField : uint32_t {
WATER_HEATER_COMMAND_HAS_NONE = 0,
WATER_HEATER_COMMAND_HAS_MODE = 1,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2,
WATER_HEATER_COMMAND_HAS_STATE = 4,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8,
WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16,
};
#ifdef USE_NUMBER
enum NumberMode : uint32_t {
NUMBER_MODE_AUTO = 0,
@@ -315,12 +289,15 @@ enum ZWaveProxyRequestType : uint32_t {
class InfoResponseProtoMessage : public ProtoMessage {
public:
~InfoResponseProtoMessage() override = default;
StringRef object_id{};
StringRef object_id_ref_{};
void set_object_id(const StringRef &ref) { this->object_id_ref_ = ref; }
uint32_t key{0};
StringRef name{};
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
bool disabled_by_default{false};
#ifdef USE_ENTITY_ICON
StringRef icon{};
StringRef icon_ref_{};
void set_icon(const StringRef &ref) { this->icon_ref_ = ref; }
#endif
enums::EntityCategory entity_category{};
#ifdef USE_DEVICES
@@ -354,11 +331,12 @@ class CommandProtoMessage : public ProtoDecodableMessage {
class HelloRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 1;
static constexpr uint8_t ESTIMATED_SIZE = 17;
static constexpr uint8_t ESTIMATED_SIZE = 27;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "hello_request"; }
#endif
StringRef client_info{};
const uint8_t *client_info{nullptr};
uint16_t client_info_len{0};
uint32_t api_version_major{0};
uint32_t api_version_minor{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -378,8 +356,10 @@ class HelloResponse final : public ProtoMessage {
#endif
uint32_t api_version_major{0};
uint32_t api_version_minor{0};
StringRef server_info{};
StringRef name{};
StringRef server_info_ref_{};
void set_server_info(const StringRef &ref) { this->server_info_ref_ = ref; }
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -388,6 +368,40 @@ class HelloResponse final : public ProtoMessage {
protected:
};
#ifdef USE_API_PASSWORD
class AuthenticationRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 3;
static constexpr uint8_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "authentication_request"; }
#endif
const uint8_t *password{nullptr};
uint16_t password_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class AuthenticationResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 4;
static constexpr uint8_t ESTIMATED_SIZE = 2;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "authentication_response"; }
#endif
bool invalid_password{false};
void encode(ProtoWriteBuffer buffer) 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
class DisconnectRequest final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 5;
@@ -457,7 +471,8 @@ class DeviceInfoRequest final : public ProtoMessage {
class AreaInfo final : public ProtoMessage {
public:
uint32_t area_id{0};
StringRef name{};
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -471,7 +486,8 @@ class AreaInfo final : public ProtoMessage {
class DeviceInfo final : public ProtoMessage {
public:
uint32_t device_id{0};
StringRef name{};
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
uint32_t area_id{0};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -485,23 +501,33 @@ class DeviceInfo final : public ProtoMessage {
class DeviceInfoResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 10;
static constexpr uint8_t ESTIMATED_SIZE = 255;
static constexpr uint16_t ESTIMATED_SIZE = 257;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "device_info_response"; }
#endif
StringRef name{};
StringRef mac_address{};
StringRef esphome_version{};
StringRef compilation_time{};
StringRef model{};
#ifdef USE_API_PASSWORD
bool uses_password{false};
#endif
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
StringRef mac_address_ref_{};
void set_mac_address(const StringRef &ref) { this->mac_address_ref_ = ref; }
StringRef esphome_version_ref_{};
void set_esphome_version(const StringRef &ref) { this->esphome_version_ref_ = ref; }
StringRef compilation_time_ref_{};
void set_compilation_time(const StringRef &ref) { this->compilation_time_ref_ = ref; }
StringRef model_ref_{};
void set_model(const StringRef &ref) { this->model_ref_ = ref; }
#ifdef USE_DEEP_SLEEP
bool has_deep_sleep{false};
#endif
#ifdef ESPHOME_PROJECT_NAME
StringRef project_name{};
StringRef project_name_ref_{};
void set_project_name(const StringRef &ref) { this->project_name_ref_ = ref; }
#endif
#ifdef ESPHOME_PROJECT_NAME
StringRef project_version{};
StringRef project_version_ref_{};
void set_project_version(const StringRef &ref) { this->project_version_ref_ = ref; }
#endif
#ifdef USE_WEBSERVER
uint32_t webserver_port{0};
@@ -509,16 +535,20 @@ class DeviceInfoResponse final : public ProtoMessage {
#ifdef USE_BLUETOOTH_PROXY
uint32_t bluetooth_proxy_feature_flags{0};
#endif
StringRef manufacturer{};
StringRef friendly_name{};
StringRef manufacturer_ref_{};
void set_manufacturer(const StringRef &ref) { this->manufacturer_ref_ = ref; }
StringRef friendly_name_ref_{};
void set_friendly_name(const StringRef &ref) { this->friendly_name_ref_ = ref; }
#ifdef USE_VOICE_ASSISTANT
uint32_t voice_assistant_feature_flags{0};
#endif
#ifdef USE_AREAS
StringRef suggested_area{};
StringRef suggested_area_ref_{};
void set_suggested_area(const StringRef &ref) { this->suggested_area_ref_ = ref; }
#endif
#ifdef USE_BLUETOOTH_PROXY
StringRef bluetooth_mac_address{};
StringRef bluetooth_mac_address_ref_{};
void set_bluetooth_mac_address(const StringRef &ref) { this->bluetooth_mac_address_ref_ = ref; }
#endif
#ifdef USE_API_NOISE
bool api_encryption_supported{false};
@@ -593,7 +623,8 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_binary_sensor_response"; }
#endif
StringRef device_class{};
StringRef device_class_ref_{};
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(ProtoSize &size) const override;
@@ -632,7 +663,8 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
bool assumed_state{false};
bool supports_position{false};
bool supports_tilt{false};
StringRef device_class{};
StringRef device_class_ref_{};
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(ProtoSize &size) const override;
@@ -713,7 +745,8 @@ class FanStateResponse final : public StateResponseProtoMessage {
bool oscillating{false};
enums::FanDirection direction{};
int32_t speed_level{0};
StringRef preset_mode{};
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(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -738,7 +771,7 @@ class FanCommandRequest final : public CommandProtoMessage {
bool has_speed_level{false};
int32_t speed_level{0};
bool has_preset_mode{false};
StringRef preset_mode{};
std::string preset_mode{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -787,7 +820,8 @@ class LightStateResponse final : public StateResponseProtoMessage {
float color_temperature{0.0f};
float cold_white{0.0f};
float warm_white{0.0f};
StringRef effect{};
StringRef effect_ref_{};
void set_effect(const StringRef &ref) { this->effect_ref_ = ref; }
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -828,7 +862,7 @@ class LightCommandRequest final : public CommandProtoMessage {
bool has_flash_length{false};
uint32_t flash_length{0};
bool has_effect{false};
StringRef effect{};
std::string effect{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -847,10 +881,12 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_sensor_response"; }
#endif
StringRef unit_of_measurement{};
StringRef unit_of_measurement_ref_{};
void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; }
int32_t accuracy_decimals{0};
bool force_update{false};
StringRef device_class{};
StringRef device_class_ref_{};
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(ProtoSize &size) const override;
@@ -887,7 +923,8 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage {
const char *message_name() const override { return "list_entities_switch_response"; }
#endif
bool assumed_state{false};
StringRef device_class{};
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(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -937,7 +974,8 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_text_sensor_response"; }
#endif
StringRef device_class{};
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(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -953,7 +991,8 @@ class TextSensorStateResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "text_sensor_state_response"; }
#endif
StringRef state{};
StringRef state_ref_{};
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -983,7 +1022,7 @@ class SubscribeLogsRequest final : public ProtoDecodableMessage {
class SubscribeLogsResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 29;
static constexpr uint8_t ESTIMATED_SIZE = 21;
static constexpr uint8_t ESTIMATED_SIZE = 11;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_logs_response"; }
#endif
@@ -1006,12 +1045,11 @@ class SubscribeLogsResponse final : public ProtoMessage {
class NoiseEncryptionSetKeyRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 124;
static constexpr uint8_t ESTIMATED_SIZE = 19;
static constexpr uint8_t ESTIMATED_SIZE = 9;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "noise_encryption_set_key_request"; }
#endif
const uint8_t *key{nullptr};
uint16_t key_len{0};
std::string key{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1052,7 +1090,8 @@ class SubscribeHomeassistantServicesRequest final : public ProtoMessage {
};
class HomeassistantServiceMap final : public ProtoMessage {
public:
StringRef key{};
StringRef key_ref_{};
void set_key(const StringRef &ref) { this->key_ref_ = ref; }
std::string value{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -1069,7 +1108,8 @@ class HomeassistantActionRequest final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "homeassistant_action_request"; }
#endif
StringRef service{};
StringRef service_ref_{};
void set_service(const StringRef &ref) { this->service_ref_ = ref; }
FixedVector<HomeassistantServiceMap> data{};
FixedVector<HomeassistantServiceMap> data_template{};
FixedVector<HomeassistantServiceMap> variables{};
@@ -1102,7 +1142,7 @@ class HomeassistantActionResponse final : public ProtoDecodableMessage {
#endif
uint32_t call_id{0};
bool success{false};
StringRef error_message{};
std::string error_message{};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
const uint8_t *response_data{nullptr};
uint16_t response_data_len{0};
@@ -1137,8 +1177,10 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "subscribe_home_assistant_state_response"; }
#endif
StringRef entity_id{};
StringRef attribute{};
StringRef entity_id_ref_{};
void set_entity_id(const StringRef &ref) { this->entity_id_ref_ = ref; }
StringRef attribute_ref_{};
void set_attribute(const StringRef &ref) { this->attribute_ref_ = ref; }
bool once{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -1155,9 +1197,9 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "home_assistant_state_response"; }
#endif
StringRef entity_id{};
StringRef state{};
StringRef attribute{};
std::string entity_id{};
std::string state{};
std::string attribute{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1182,12 +1224,13 @@ class GetTimeRequest final : public ProtoMessage {
class GetTimeResponse final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 37;
static constexpr uint8_t ESTIMATED_SIZE = 14;
static constexpr uint8_t ESTIMATED_SIZE = 24;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "get_time_response"; }
#endif
uint32_t epoch_seconds{0};
StringRef timezone{};
const uint8_t *timezone{nullptr};
uint16_t timezone_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1199,7 +1242,8 @@ class GetTimeResponse final : public ProtoDecodableMessage {
#ifdef USE_API_USER_DEFINED_ACTIONS
class ListEntitiesServicesArgument final : public ProtoMessage {
public:
StringRef name{};
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
enums::ServiceArgType type{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -1212,14 +1256,14 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
class ListEntitiesServicesResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 41;
static constexpr uint8_t ESTIMATED_SIZE = 50;
static constexpr uint8_t ESTIMATED_SIZE = 48;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_services_response"; }
#endif
StringRef name{};
StringRef name_ref_{};
void set_name(const StringRef &ref) { this->name_ref_ = ref; }
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1233,7 +1277,7 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
bool bool_{false};
int32_t legacy_int{0};
float float_{0.0f};
StringRef string_{};
std::string string_{};
int32_t int_{0};
FixedVector<bool> bool_array{};
FixedVector<int32_t> int_array{};
@@ -1252,18 +1296,12 @@ class ExecuteServiceArgument final : public ProtoDecodableMessage {
class ExecuteServiceRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 42;
static constexpr uint8_t ESTIMATED_SIZE = 45;
static constexpr uint8_t ESTIMATED_SIZE = 39;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "execute_service_request"; }
#endif
uint32_t key{0};
FixedVector<ExecuteServiceArgument> args{};
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
uint32_t call_id{0};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
bool return_response{false};
#endif
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@@ -1272,31 +1310,6 @@ class ExecuteServiceRequest final : public ProtoDecodableMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
class ExecuteServiceResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 131;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "execute_service_response"; }
#endif
uint32_t call_id{0};
bool success{false};
StringRef error_message{};
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
const uint8_t *response_data{nullptr};
uint16_t response_data_len{0};
#endif
void encode(ProtoWriteBuffer buffer) 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_CAMERA
@@ -1318,7 +1331,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage {
class CameraImageResponse final : public StateResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 44;
static constexpr uint8_t ESTIMATED_SIZE = 30;
static constexpr uint8_t ESTIMATED_SIZE = 20;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "camera_image_response"; }
#endif
@@ -1403,9 +1416,11 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
enums::ClimateAction action{};
enums::ClimateFanMode fan_mode{};
enums::ClimateSwingMode swing_mode{};
StringRef custom_fan_mode{};
StringRef custom_fan_mode_ref_{};
void set_custom_fan_mode(const StringRef &ref) { this->custom_fan_mode_ref_ = ref; }
enums::ClimatePreset preset{};
StringRef custom_preset{};
StringRef custom_preset_ref_{};
void set_custom_preset(const StringRef &ref) { this->custom_preset_ref_ = ref; }
float current_humidity{0.0f};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer buffer) const override;
@@ -1436,11 +1451,11 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool has_swing_mode{false};
enums::ClimateSwingMode swing_mode{};
bool has_custom_fan_mode{false};
StringRef custom_fan_mode{};
std::string custom_fan_mode{};
bool has_preset{false};
enums::ClimatePreset preset{};
bool has_custom_preset{false};
StringRef custom_preset{};
std::string custom_preset{};
bool has_target_humidity{false};
float target_humidity{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1453,70 +1468,6 @@ class ClimateCommandRequest final : public CommandProtoMessage {
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_WATER_HEATER
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 132;
static constexpr uint8_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_water_heater_response"; }
#endif
float min_temperature{0.0f};
float max_temperature{0.0f};
float target_temperature_step{0.0f};
const water_heater::WaterHeaterModeMask *supported_modes{};
uint32_t supported_features{0};
void encode(ProtoWriteBuffer buffer) 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 WaterHeaterStateResponse final : public StateResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 133;
static constexpr uint8_t ESTIMATED_SIZE = 35;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_state_response"; }
#endif
float current_temperature{0.0f};
float target_temperature{0.0f};
enums::WaterHeaterMode mode{};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
void encode(ProtoWriteBuffer buffer) 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 WaterHeaterCommandRequest final : public CommandProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 134;
static constexpr uint8_t ESTIMATED_SIZE = 34;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "water_heater_command_request"; }
#endif
uint32_t has_fields{0};
enums::WaterHeaterMode mode{};
float target_temperature{0.0f};
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
#endif
#ifdef USE_NUMBER
class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
public:
@@ -1528,9 +1479,11 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
float min_value{0.0f};
float max_value{0.0f};
float step{0.0f};
StringRef unit_of_measurement{};
StringRef unit_of_measurement_ref_{};
void set_unit_of_measurement(const StringRef &ref) { this->unit_of_measurement_ref_ = ref; }
enums::NumberMode mode{};
StringRef device_class{};
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(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1597,7 +1550,8 @@ class SelectStateResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "select_state_response"; }
#endif
StringRef state{};
StringRef state_ref_{};
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -1614,7 +1568,7 @@ class SelectCommandRequest final : public CommandProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "select_command_request"; }
#endif
StringRef state{};
std::string state{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1633,7 +1587,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_siren_response"; }
#endif
const FixedVector<const char *> *tones{};
std::vector<std::string> tones{};
bool supports_duration{false};
bool supports_volume{false};
void encode(ProtoWriteBuffer buffer) const override;
@@ -1670,7 +1624,7 @@ class SirenCommandRequest final : public CommandProtoMessage {
bool has_state{false};
bool state{false};
bool has_tone{false};
StringRef tone{};
std::string tone{};
bool has_duration{false};
uint32_t duration{0};
bool has_volume{false};
@@ -1696,7 +1650,8 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage {
bool assumed_state{false};
bool supports_open{false};
bool requires_code{false};
StringRef code_format{};
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(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1730,7 +1685,7 @@ class LockCommandRequest final : public CommandProtoMessage {
#endif
enums::LockCommand command{};
bool has_code{false};
StringRef code{};
std::string code{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -1749,7 +1704,8 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_button_response"; }
#endif
StringRef device_class{};
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(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1777,7 +1733,8 @@ class ButtonCommandRequest final : public CommandProtoMessage {
#ifdef USE_MEDIA_PLAYER
class MediaPlayerSupportedFormat final : public ProtoMessage {
public:
StringRef format{};
StringRef format_ref_{};
void set_format(const StringRef &ref) { this->format_ref_ = ref; }
uint32_t sample_rate{0};
uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{};
@@ -1838,7 +1795,7 @@ class MediaPlayerCommandRequest final : public CommandProtoMessage {
bool has_volume{false};
float volume{0.0f};
bool has_media_url{false};
StringRef media_url{};
std::string media_url{};
bool has_announcement{false};
bool announcement{false};
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -2045,7 +2002,7 @@ class BluetoothGATTReadRequest final : public ProtoDecodableMessage {
class BluetoothGATTReadResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 74;
static constexpr uint8_t ESTIMATED_SIZE = 27;
static constexpr uint8_t ESTIMATED_SIZE = 17;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_gatt_read_response"; }
#endif
@@ -2140,7 +2097,7 @@ class BluetoothGATTNotifyRequest final : public ProtoDecodableMessage {
class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 79;
static constexpr uint8_t ESTIMATED_SIZE = 27;
static constexpr uint8_t ESTIMATED_SIZE = 17;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "bluetooth_gatt_notify_data_response"; }
#endif
@@ -2382,10 +2339,12 @@ class VoiceAssistantRequest final : public ProtoMessage {
const char *message_name() const override { return "voice_assistant_request"; }
#endif
bool start{false};
StringRef conversation_id{};
StringRef conversation_id_ref_{};
void set_conversation_id(const StringRef &ref) { this->conversation_id_ref_ = ref; }
uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{};
StringRef wake_word_phrase{};
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(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -2412,8 +2371,8 @@ class VoiceAssistantResponse final : public ProtoDecodableMessage {
};
class VoiceAssistantEventData final : public ProtoDecodableMessage {
public:
StringRef name{};
StringRef value{};
std::string name{};
std::string value{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2441,12 +2400,17 @@ class VoiceAssistantEventResponse final : public ProtoDecodableMessage {
class VoiceAssistantAudio final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 106;
static constexpr uint8_t ESTIMATED_SIZE = 21;
static constexpr uint8_t ESTIMATED_SIZE = 11;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "voice_assistant_audio"; }
#endif
const uint8_t *data{nullptr};
uint16_t data_len{0};
std::string data{};
const uint8_t *data_ptr_{nullptr};
size_t data_len_{0};
void set_data(const uint8_t *data, size_t len) {
this->data_ptr_ = data;
this->data_len_ = len;
}
bool end{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -2466,8 +2430,8 @@ class VoiceAssistantTimerEventResponse final : public ProtoDecodableMessage {
const char *message_name() const override { return "voice_assistant_timer_event_response"; }
#endif
enums::VoiceAssistantTimerEvent event_type{};
StringRef timer_id{};
StringRef name{};
std::string timer_id{};
std::string name{};
uint32_t total_seconds{0};
uint32_t seconds_left{0};
bool is_active{false};
@@ -2486,9 +2450,9 @@ class VoiceAssistantAnnounceRequest final : public ProtoDecodableMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "voice_assistant_announce_request"; }
#endif
StringRef media_id{};
StringRef text{};
StringRef preannounce_media_id{};
std::string media_id{};
std::string text{};
std::string preannounce_media_id{};
bool start_conversation{false};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@@ -2516,8 +2480,10 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage {
};
class VoiceAssistantWakeWord final : public ProtoMessage {
public:
StringRef id{};
StringRef wake_word{};
StringRef id_ref_{};
void set_id(const StringRef &ref) { this->id_ref_ = ref; }
StringRef wake_word_ref_{};
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(ProtoSize &size) const override;
@@ -2529,13 +2495,13 @@ class VoiceAssistantWakeWord final : public ProtoMessage {
};
class VoiceAssistantExternalWakeWord final : public ProtoDecodableMessage {
public:
StringRef id{};
StringRef wake_word{};
std::string id{};
std::string wake_word{};
std::vector<std::string> trained_languages{};
StringRef model_type{};
std::string model_type{};
uint32_t model_size{0};
StringRef model_hash{};
StringRef url{};
std::string model_hash{};
std::string url{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2636,7 +2602,7 @@ class AlarmControlPanelCommandRequest final : public CommandProtoMessage {
const char *message_name() const override { return "alarm_control_panel_command_request"; }
#endif
enums::AlarmControlPanelStateCommand command{};
StringRef code{};
std::string code{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2657,7 +2623,8 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage {
#endif
uint32_t min_length{0};
uint32_t max_length{0};
StringRef pattern{};
StringRef pattern_ref_{};
void set_pattern(const StringRef &ref) { this->pattern_ref_ = ref; }
enums::TextMode mode{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -2674,7 +2641,8 @@ class TextStateResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "text_state_response"; }
#endif
StringRef state{};
StringRef state_ref_{};
void set_state(const StringRef &ref) { this->state_ref_ = ref; }
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -2691,7 +2659,7 @@ class TextCommandRequest final : public CommandProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "text_command_request"; }
#endif
StringRef state{};
std::string state{};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif
@@ -2818,7 +2786,8 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_event_response"; }
#endif
StringRef device_class{};
StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
const FixedVector<const char *> *event_types{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -2835,7 +2804,8 @@ class EventResponse final : public StateResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "event_response"; }
#endif
StringRef event_type{};
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(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -2853,7 +2823,8 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_valve_response"; }
#endif
StringRef device_class{};
StringRef device_class_ref_{};
void set_device_class(const StringRef &ref) { this->device_class_ref_ = ref; }
bool assumed_state{false};
bool supports_position{false};
bool supports_stop{false};
@@ -2959,7 +2930,8 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "list_entities_update_response"; }
#endif
StringRef device_class{};
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(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -2979,11 +2951,16 @@ class UpdateStateResponse final : public StateResponseProtoMessage {
bool in_progress{false};
bool has_progress{false};
float progress{0.0f};
StringRef current_version{};
StringRef latest_version{};
StringRef title{};
StringRef release_summary{};
StringRef release_url{};
StringRef current_version_ref_{};
void set_current_version(const StringRef &ref) { this->current_version_ref_ = ref; }
StringRef latest_version_ref_{};
void set_latest_version(const StringRef &ref) { this->latest_version_ref_ = ref; }
StringRef title_ref_{};
void set_title(const StringRef &ref) { this->title_ref_ = ref; }
StringRef release_summary_ref_{};
void set_release_summary(const StringRef &ref) { this->release_summary_ref_ = ref; }
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(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP

View File

@@ -179,8 +179,6 @@ template<> const char *proto_enum_to_string<enums::SensorStateClass>(enums::Sens
return "STATE_CLASS_TOTAL_INCREASING";
case enums::STATE_CLASS_TOTAL:
return "STATE_CLASS_TOTAL";
case enums::STATE_CLASS_MEASUREMENT_ANGLE:
return "STATE_CLASS_MEASUREMENT_ANGLE";
default:
return "UNKNOWN";
}
@@ -231,20 +229,6 @@ template<> const char *proto_enum_to_string<enums::ServiceArgType>(enums::Servic
return "UNKNOWN";
}
}
template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::SupportsResponseType value) {
switch (value) {
case enums::SUPPORTS_RESPONSE_NONE:
return "SUPPORTS_RESPONSE_NONE";
case enums::SUPPORTS_RESPONSE_OPTIONAL:
return "SUPPORTS_RESPONSE_OPTIONAL";
case enums::SUPPORTS_RESPONSE_ONLY:
return "SUPPORTS_RESPONSE_ONLY";
case enums::SUPPORTS_RESPONSE_STATUS:
return "SUPPORTS_RESPONSE_STATUS";
default:
return "UNKNOWN";
}
}
#endif
#ifdef USE_CLIMATE
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
@@ -348,47 +332,6 @@ template<> const char *proto_enum_to_string<enums::ClimatePreset>(enums::Climate
}
}
#endif
#ifdef USE_WATER_HEATER
template<> const char *proto_enum_to_string<enums::WaterHeaterMode>(enums::WaterHeaterMode value) {
switch (value) {
case enums::WATER_HEATER_MODE_OFF:
return "WATER_HEATER_MODE_OFF";
case enums::WATER_HEATER_MODE_ECO:
return "WATER_HEATER_MODE_ECO";
case enums::WATER_HEATER_MODE_ELECTRIC:
return "WATER_HEATER_MODE_ELECTRIC";
case enums::WATER_HEATER_MODE_PERFORMANCE:
return "WATER_HEATER_MODE_PERFORMANCE";
case enums::WATER_HEATER_MODE_HIGH_DEMAND:
return "WATER_HEATER_MODE_HIGH_DEMAND";
case enums::WATER_HEATER_MODE_HEAT_PUMP:
return "WATER_HEATER_MODE_HEAT_PUMP";
case enums::WATER_HEATER_MODE_GAS:
return "WATER_HEATER_MODE_GAS";
default:
return "UNKNOWN";
}
}
#endif
template<>
const char *proto_enum_to_string<enums::WaterHeaterCommandHasField>(enums::WaterHeaterCommandHasField value) {
switch (value) {
case enums::WATER_HEATER_COMMAND_HAS_NONE:
return "WATER_HEATER_COMMAND_HAS_NONE";
case enums::WATER_HEATER_COMMAND_HAS_MODE:
return "WATER_HEATER_COMMAND_HAS_MODE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE";
case enums::WATER_HEATER_COMMAND_HAS_STATE:
return "WATER_HEATER_COMMAND_HAS_STATE";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW";
case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH:
return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH";
default:
return "UNKNOWN";
}
}
#ifdef USE_NUMBER
template<> const char *proto_enum_to_string<enums::NumberMode>(enums::NumberMode value) {
switch (value) {
@@ -735,7 +678,9 @@ template<> const char *proto_enum_to_string<enums::ZWaveProxyRequestType>(enums:
void HelloRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HelloRequest");
dump_field(out, "client_info", this->client_info);
out.append(" client_info: ");
out.append(format_hex_pretty(this->client_info, this->client_info_len));
out.append("\n");
dump_field(out, "api_version_major", this->api_version_major);
dump_field(out, "api_version_minor", this->api_version_minor);
}
@@ -743,9 +688,21 @@ void HelloResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HelloResponse");
dump_field(out, "api_version_major", this->api_version_major);
dump_field(out, "api_version_minor", this->api_version_minor);
dump_field(out, "server_info", this->server_info);
dump_field(out, "name", this->name);
dump_field(out, "server_info", this->server_info_ref_);
dump_field(out, "name", this->name_ref_);
}
#ifdef USE_API_PASSWORD
void AuthenticationRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "AuthenticationRequest");
out.append(" password: ");
out.append(format_hex_pretty(this->password, this->password_len));
out.append("\n");
}
void AuthenticationResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "AuthenticationResponse");
dump_field(out, "invalid_password", this->invalid_password);
}
#endif
void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); }
void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); }
void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); }
@@ -755,32 +712,35 @@ void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfo
void AreaInfo::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "AreaInfo");
dump_field(out, "area_id", this->area_id);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
}
#endif
#ifdef USE_DEVICES
void DeviceInfo::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "DeviceInfo");
dump_field(out, "device_id", this->device_id);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
dump_field(out, "area_id", this->area_id);
}
#endif
void DeviceInfoResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "DeviceInfoResponse");
dump_field(out, "name", this->name);
dump_field(out, "mac_address", this->mac_address);
dump_field(out, "esphome_version", this->esphome_version);
dump_field(out, "compilation_time", this->compilation_time);
dump_field(out, "model", this->model);
#ifdef USE_API_PASSWORD
dump_field(out, "uses_password", this->uses_password);
#endif
dump_field(out, "name", this->name_ref_);
dump_field(out, "mac_address", this->mac_address_ref_);
dump_field(out, "esphome_version", this->esphome_version_ref_);
dump_field(out, "compilation_time", this->compilation_time_ref_);
dump_field(out, "model", this->model_ref_);
#ifdef USE_DEEP_SLEEP
dump_field(out, "has_deep_sleep", this->has_deep_sleep);
#endif
#ifdef ESPHOME_PROJECT_NAME
dump_field(out, "project_name", this->project_name);
dump_field(out, "project_name", this->project_name_ref_);
#endif
#ifdef ESPHOME_PROJECT_NAME
dump_field(out, "project_version", this->project_version);
dump_field(out, "project_version", this->project_version_ref_);
#endif
#ifdef USE_WEBSERVER
dump_field(out, "webserver_port", this->webserver_port);
@@ -788,16 +748,16 @@ void DeviceInfoResponse::dump_to(std::string &out) const {
#ifdef USE_BLUETOOTH_PROXY
dump_field(out, "bluetooth_proxy_feature_flags", this->bluetooth_proxy_feature_flags);
#endif
dump_field(out, "manufacturer", this->manufacturer);
dump_field(out, "friendly_name", this->friendly_name);
dump_field(out, "manufacturer", this->manufacturer_ref_);
dump_field(out, "friendly_name", this->friendly_name_ref_);
#ifdef USE_VOICE_ASSISTANT
dump_field(out, "voice_assistant_feature_flags", this->voice_assistant_feature_flags);
#endif
#ifdef USE_AREAS
dump_field(out, "suggested_area", this->suggested_area);
dump_field(out, "suggested_area", this->suggested_area_ref_);
#endif
#ifdef USE_BLUETOOTH_PROXY
dump_field(out, "bluetooth_mac_address", this->bluetooth_mac_address);
dump_field(out, "bluetooth_mac_address", this->bluetooth_mac_address_ref_);
#endif
#ifdef USE_API_NOISE
dump_field(out, "api_encryption_supported", this->api_encryption_supported);
@@ -834,14 +794,14 @@ void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("Subsc
#ifdef USE_BINARY_SENSOR
void ListEntitiesBinarySensorResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesBinarySensorResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "device_class", this->device_class);
dump_field(out, "name", this->name_ref_);
dump_field(out, "device_class", this->device_class_ref_);
dump_field(out, "is_status_binary_sensor", this->is_status_binary_sensor);
dump_field(out, "disabled_by_default", this->disabled_by_default);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
@@ -861,16 +821,16 @@ void BinarySensorStateResponse::dump_to(std::string &out) const {
#ifdef USE_COVER
void ListEntitiesCoverResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesCoverResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
dump_field(out, "assumed_state", this->assumed_state);
dump_field(out, "supports_position", this->supports_position);
dump_field(out, "supports_tilt", this->supports_tilt);
dump_field(out, "device_class", this->device_class);
dump_field(out, "device_class", this->device_class_ref_);
dump_field(out, "disabled_by_default", this->disabled_by_default);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "supports_stop", this->supports_stop);
@@ -904,16 +864,16 @@ void CoverCommandRequest::dump_to(std::string &out) const {
#ifdef USE_FAN
void ListEntitiesFanResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesFanResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
dump_field(out, "supports_oscillation", this->supports_oscillation);
dump_field(out, "supports_speed", this->supports_speed);
dump_field(out, "supports_direction", this->supports_direction);
dump_field(out, "supported_speed_count", this->supported_speed_count);
dump_field(out, "disabled_by_default", this->disabled_by_default);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
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) {
@@ -930,7 +890,7 @@ void FanStateResponse::dump_to(std::string &out) const {
dump_field(out, "oscillating", this->oscillating);
dump_field(out, "direction", static_cast<enums::FanDirection>(this->direction));
dump_field(out, "speed_level", this->speed_level);
dump_field(out, "preset_mode", this->preset_mode);
dump_field(out, "preset_mode", this->preset_mode_ref_);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -956,9 +916,9 @@ void FanCommandRequest::dump_to(std::string &out) const {
#ifdef USE_LIGHT
void ListEntitiesLightResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesLightResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
for (const auto &it : *this->supported_color_modes) {
dump_field(out, "supported_color_modes", static_cast<enums::ColorMode>(it), 4);
}
@@ -969,7 +929,7 @@ void ListEntitiesLightResponse::dump_to(std::string &out) const {
}
dump_field(out, "disabled_by_default", this->disabled_by_default);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
@@ -990,7 +950,7 @@ void LightStateResponse::dump_to(std::string &out) const {
dump_field(out, "color_temperature", this->color_temperature);
dump_field(out, "cold_white", this->cold_white);
dump_field(out, "warm_white", this->warm_white);
dump_field(out, "effect", this->effect);
dump_field(out, "effect", this->effect_ref_);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1032,16 +992,16 @@ void LightCommandRequest::dump_to(std::string &out) const {
#ifdef USE_SENSOR
void ListEntitiesSensorResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesSensorResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "unit_of_measurement", this->unit_of_measurement);
dump_field(out, "unit_of_measurement", this->unit_of_measurement_ref_);
dump_field(out, "accuracy_decimals", this->accuracy_decimals);
dump_field(out, "force_update", this->force_update);
dump_field(out, "device_class", this->device_class);
dump_field(out, "device_class", this->device_class_ref_);
dump_field(out, "state_class", static_cast<enums::SensorStateClass>(this->state_class));
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
@@ -1062,16 +1022,16 @@ void SensorStateResponse::dump_to(std::string &out) const {
#ifdef USE_SWITCH
void ListEntitiesSwitchResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesSwitchResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "assumed_state", this->assumed_state);
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "device_class", this->device_class);
dump_field(out, "device_class", this->device_class_ref_);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1096,15 +1056,15 @@ void SwitchCommandRequest::dump_to(std::string &out) const {
#ifdef USE_TEXT_SENSOR
void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesTextSensorResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "device_class", this->device_class);
dump_field(out, "device_class", this->device_class_ref_);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1112,7 +1072,7 @@ void ListEntitiesTextSensorResponse::dump_to(std::string &out) const {
void TextSensorStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "TextSensorStateResponse");
dump_field(out, "key", this->key);
dump_field(out, "state", this->state);
dump_field(out, "state", this->state_ref_);
dump_field(out, "missing_state", this->missing_state);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
@@ -1135,13 +1095,10 @@ void SubscribeLogsResponse::dump_to(std::string &out) const {
void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest");
out.append(" key: ");
out.append(format_hex_pretty(this->key, this->key_len));
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->key.data()), this->key.size()));
out.append("\n");
}
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "NoiseEncryptionSetKeyResponse");
dump_field(out, "success", this->success);
}
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 {
@@ -1149,12 +1106,12 @@ void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const {
}
void HomeassistantServiceMap::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeassistantServiceMap");
dump_field(out, "key", this->key);
dump_field(out, "key", this->key_ref_);
dump_field(out, "value", this->value);
}
void HomeassistantActionRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeassistantActionRequest");
dump_field(out, "service", this->service);
dump_field(out, "service", this->service_ref_);
for (const auto &it : this->data) {
out.append(" data: ");
it.dump_to(out);
@@ -1201,8 +1158,8 @@ void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const {
}
void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "SubscribeHomeAssistantStateResponse");
dump_field(out, "entity_id", this->entity_id);
dump_field(out, "attribute", this->attribute);
dump_field(out, "entity_id", this->entity_id_ref_);
dump_field(out, "attribute", this->attribute_ref_);
dump_field(out, "once", this->once);
}
void HomeAssistantStateResponse::dump_to(std::string &out) const {
@@ -1216,24 +1173,25 @@ void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeReques
void GetTimeResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "GetTimeResponse");
dump_field(out, "epoch_seconds", this->epoch_seconds);
dump_field(out, "timezone", this->timezone);
out.append(" timezone: ");
out.append(format_hex_pretty(this->timezone, this->timezone_len));
out.append("\n");
}
#ifdef USE_API_USER_DEFINED_ACTIONS
void ListEntitiesServicesArgument::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesServicesArgument");
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
dump_field(out, "type", static_cast<enums::ServiceArgType>(this->type));
}
void ListEntitiesServicesResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesServicesResponse");
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
dump_field(out, "key", this->key);
for (const auto &it : this->args) {
out.append(" args: ");
it.dump_to(out);
out.append("\n");
}
dump_field(out, "supports_response", static_cast<enums::SupportsResponseType>(this->supports_response));
}
void ExecuteServiceArgument::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ExecuteServiceArgument");
@@ -1263,36 +1221,17 @@ void ExecuteServiceRequest::dump_to(std::string &out) const {
it.dump_to(out);
out.append("\n");
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
dump_field(out, "call_id", this->call_id);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
dump_field(out, "return_response", this->return_response);
#endif
}
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
void ExecuteServiceResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ExecuteServiceResponse");
dump_field(out, "call_id", this->call_id);
dump_field(out, "success", this->success);
dump_field(out, "error_message", this->error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
out.append(" response_data: ");
out.append(format_hex_pretty(this->response_data, this->response_data_len));
out.append("\n");
#endif
}
#endif
#ifdef USE_CAMERA
void ListEntitiesCameraResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesCameraResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
dump_field(out, "disabled_by_default", this->disabled_by_default);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
@@ -1319,9 +1258,9 @@ void CameraImageRequest::dump_to(std::string &out) const {
#ifdef USE_CLIMATE
void ListEntitiesClimateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesClimateResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
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) {
@@ -1348,7 +1287,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
}
dump_field(out, "disabled_by_default", this->disabled_by_default);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "visual_current_temperature_step", this->visual_current_temperature_step);
@@ -1372,9 +1311,9 @@ void ClimateStateResponse::dump_to(std::string &out) const {
dump_field(out, "action", static_cast<enums::ClimateAction>(this->action));
dump_field(out, "fan_mode", static_cast<enums::ClimateFanMode>(this->fan_mode));
dump_field(out, "swing_mode", static_cast<enums::ClimateSwingMode>(this->swing_mode));
dump_field(out, "custom_fan_mode", this->custom_fan_mode);
dump_field(out, "custom_fan_mode", this->custom_fan_mode_ref_);
dump_field(out, "preset", static_cast<enums::ClimatePreset>(this->preset));
dump_field(out, "custom_preset", this->custom_preset);
dump_field(out, "custom_preset", this->custom_preset_ref_);
dump_field(out, "current_humidity", this->current_humidity);
dump_field(out, "target_humidity", this->target_humidity);
#ifdef USE_DEVICES
@@ -1409,72 +1348,23 @@ void ClimateCommandRequest::dump_to(std::string &out) const {
#endif
}
#endif
#ifdef USE_WATER_HEATER
void ListEntitiesWaterHeaterResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesWaterHeaterResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "min_temperature", this->min_temperature);
dump_field(out, "max_temperature", this->max_temperature);
dump_field(out, "target_temperature_step", this->target_temperature_step);
for (const auto &it : *this->supported_modes) {
dump_field(out, "supported_modes", static_cast<enums::WaterHeaterMode>(it), 4);
}
dump_field(out, "supported_features", this->supported_features);
}
void WaterHeaterStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterStateResponse");
dump_field(out, "key", this->key);
dump_field(out, "current_temperature", this->current_temperature);
dump_field(out, "target_temperature", this->target_temperature);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
void WaterHeaterCommandRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "WaterHeaterCommandRequest");
dump_field(out, "key", this->key);
dump_field(out, "has_fields", this->has_fields);
dump_field(out, "mode", static_cast<enums::WaterHeaterMode>(this->mode));
dump_field(out, "target_temperature", this->target_temperature);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
dump_field(out, "state", this->state);
dump_field(out, "target_temperature_low", this->target_temperature_low);
dump_field(out, "target_temperature_high", this->target_temperature_high);
}
#endif
#ifdef USE_NUMBER
void ListEntitiesNumberResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesNumberResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "min_value", this->min_value);
dump_field(out, "max_value", this->max_value);
dump_field(out, "step", this->step);
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "unit_of_measurement", this->unit_of_measurement);
dump_field(out, "unit_of_measurement", this->unit_of_measurement_ref_);
dump_field(out, "mode", static_cast<enums::NumberMode>(this->mode));
dump_field(out, "device_class", this->device_class);
dump_field(out, "device_class", this->device_class_ref_);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1500,11 +1390,11 @@ void NumberCommandRequest::dump_to(std::string &out) const {
#ifdef USE_SELECT
void ListEntitiesSelectResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesSelectResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
for (const auto &it : *this->options) {
dump_field(out, "options", it, 4);
@@ -1518,7 +1408,7 @@ void ListEntitiesSelectResponse::dump_to(std::string &out) const {
void SelectStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "SelectStateResponse");
dump_field(out, "key", this->key);
dump_field(out, "state", this->state);
dump_field(out, "state", this->state_ref_);
dump_field(out, "missing_state", this->missing_state);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
@@ -1536,14 +1426,14 @@ void SelectCommandRequest::dump_to(std::string &out) const {
#ifdef USE_SIREN
void ListEntitiesSirenResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesSirenResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
for (const auto &it : *this->tones) {
for (const auto &it : this->tones) {
dump_field(out, "tones", it, 4);
}
dump_field(out, "supports_duration", this->supports_duration);
@@ -1580,18 +1470,18 @@ void SirenCommandRequest::dump_to(std::string &out) const {
#ifdef USE_LOCK
void ListEntitiesLockResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesLockResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "assumed_state", this->assumed_state);
dump_field(out, "supports_open", this->supports_open);
dump_field(out, "requires_code", this->requires_code);
dump_field(out, "code_format", this->code_format);
dump_field(out, "code_format", this->code_format_ref_);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1618,15 +1508,15 @@ void LockCommandRequest::dump_to(std::string &out) const {
#ifdef USE_BUTTON
void ListEntitiesButtonResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesButtonResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "device_class", this->device_class);
dump_field(out, "device_class", this->device_class_ref_);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -1642,7 +1532,7 @@ void ButtonCommandRequest::dump_to(std::string &out) const {
#ifdef USE_MEDIA_PLAYER
void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "MediaPlayerSupportedFormat");
dump_field(out, "format", this->format);
dump_field(out, "format", this->format_ref_);
dump_field(out, "sample_rate", this->sample_rate);
dump_field(out, "num_channels", this->num_channels);
dump_field(out, "purpose", static_cast<enums::MediaPlayerFormatPurpose>(this->purpose));
@@ -1650,11 +1540,11 @@ void MediaPlayerSupportedFormat::dump_to(std::string &out) const {
}
void ListEntitiesMediaPlayerResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesMediaPlayerResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
@@ -1731,10 +1621,7 @@ void BluetoothDeviceConnectionResponse::dump_to(std::string &out) const {
dump_field(out, "mtu", this->mtu);
dump_field(out, "error", this->error);
}
void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTGetServicesRequest");
dump_field(out, "address", this->address);
}
void BluetoothGATTGetServicesRequest::dump_to(std::string &out) const { dump_field(out, "address", this->address); }
void BluetoothGATTDescriptor::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTDescriptor");
for (const auto &it : this->uuid) {
@@ -1906,12 +1793,12 @@ void VoiceAssistantAudioSettings::dump_to(std::string &out) const {
void VoiceAssistantRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "VoiceAssistantRequest");
dump_field(out, "start", this->start);
dump_field(out, "conversation_id", this->conversation_id);
dump_field(out, "conversation_id", this->conversation_id_ref_);
dump_field(out, "flags", this->flags);
out.append(" audio_settings: ");
this->audio_settings.dump_to(out);
out.append("\n");
dump_field(out, "wake_word_phrase", this->wake_word_phrase);
dump_field(out, "wake_word_phrase", this->wake_word_phrase_ref_);
}
void VoiceAssistantResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "VoiceAssistantResponse");
@@ -1935,7 +1822,11 @@ void VoiceAssistantEventResponse::dump_to(std::string &out) const {
void VoiceAssistantAudio::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "VoiceAssistantAudio");
out.append(" data: ");
out.append(format_hex_pretty(this->data, this->data_len));
if (this->data_ptr_ != nullptr) {
out.append(format_hex_pretty(this->data_ptr_, this->data_len_));
} else {
out.append(format_hex_pretty(reinterpret_cast<const uint8_t *>(this->data.data()), this->data.size()));
}
out.append("\n");
dump_field(out, "end", this->end);
}
@@ -1955,14 +1846,11 @@ void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const {
dump_field(out, "preannounce_media_id", this->preannounce_media_id);
dump_field(out, "start_conversation", this->start_conversation);
}
void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "VoiceAssistantAnnounceFinished");
dump_field(out, "success", this->success);
}
void VoiceAssistantAnnounceFinished::dump_to(std::string &out) const { dump_field(out, "success", this->success); }
void VoiceAssistantWakeWord::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "VoiceAssistantWakeWord");
dump_field(out, "id", this->id);
dump_field(out, "wake_word", this->wake_word);
dump_field(out, "id", this->id_ref_);
dump_field(out, "wake_word", this->wake_word_ref_);
for (const auto &it : this->trained_languages) {
dump_field(out, "trained_languages", it, 4);
}
@@ -2009,11 +1897,11 @@ void VoiceAssistantSetConfiguration::dump_to(std::string &out) const {
#ifdef USE_ALARM_CONTROL_PANEL
void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesAlarmControlPanelResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
@@ -2045,17 +1933,17 @@ void AlarmControlPanelCommandRequest::dump_to(std::string &out) const {
#ifdef USE_TEXT
void ListEntitiesTextResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesTextResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "min_length", this->min_length);
dump_field(out, "max_length", this->max_length);
dump_field(out, "pattern", this->pattern);
dump_field(out, "pattern", this->pattern_ref_);
dump_field(out, "mode", static_cast<enums::TextMode>(this->mode));
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
@@ -2064,7 +1952,7 @@ void ListEntitiesTextResponse::dump_to(std::string &out) const {
void TextStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "TextStateResponse");
dump_field(out, "key", this->key);
dump_field(out, "state", this->state);
dump_field(out, "state", this->state_ref_);
dump_field(out, "missing_state", this->missing_state);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
@@ -2082,11 +1970,11 @@ void TextCommandRequest::dump_to(std::string &out) const {
#ifdef USE_DATETIME_DATE
void ListEntitiesDateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesDateResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
@@ -2119,11 +2007,11 @@ void DateCommandRequest::dump_to(std::string &out) const {
#ifdef USE_DATETIME_TIME
void ListEntitiesTimeResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesTimeResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
@@ -2156,15 +2044,15 @@ void TimeCommandRequest::dump_to(std::string &out) const {
#ifdef USE_EVENT
void ListEntitiesEventResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesEventResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "device_class", this->device_class);
dump_field(out, "device_class", this->device_class_ref_);
for (const auto &it : *this->event_types) {
dump_field(out, "event_types", it, 4);
}
@@ -2175,7 +2063,7 @@ void ListEntitiesEventResponse::dump_to(std::string &out) const {
void EventResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "EventResponse");
dump_field(out, "key", this->key);
dump_field(out, "event_type", this->event_type);
dump_field(out, "event_type", this->event_type_ref_);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -2184,15 +2072,15 @@ void EventResponse::dump_to(std::string &out) const {
#ifdef USE_VALVE
void ListEntitiesValveResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesValveResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "device_class", this->device_class);
dump_field(out, "device_class", this->device_class_ref_);
dump_field(out, "assumed_state", this->assumed_state);
dump_field(out, "supports_position", this->supports_position);
dump_field(out, "supports_stop", this->supports_stop);
@@ -2223,11 +2111,11 @@ void ValveCommandRequest::dump_to(std::string &out) const {
#ifdef USE_DATETIME_DATETIME
void ListEntitiesDateTimeResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesDateTimeResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
@@ -2256,15 +2144,15 @@ void DateTimeCommandRequest::dump_to(std::string &out) const {
#ifdef USE_UPDATE
void ListEntitiesUpdateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ListEntitiesUpdateResponse");
dump_field(out, "object_id", this->object_id);
dump_field(out, "object_id", this->object_id_ref_);
dump_field(out, "key", this->key);
dump_field(out, "name", this->name);
dump_field(out, "name", this->name_ref_);
#ifdef USE_ENTITY_ICON
dump_field(out, "icon", this->icon);
dump_field(out, "icon", this->icon_ref_);
#endif
dump_field(out, "disabled_by_default", this->disabled_by_default);
dump_field(out, "entity_category", static_cast<enums::EntityCategory>(this->entity_category));
dump_field(out, "device_class", this->device_class);
dump_field(out, "device_class", this->device_class_ref_);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif
@@ -2276,11 +2164,11 @@ void UpdateStateResponse::dump_to(std::string &out) const {
dump_field(out, "in_progress", this->in_progress);
dump_field(out, "has_progress", this->has_progress);
dump_field(out, "progress", this->progress);
dump_field(out, "current_version", this->current_version);
dump_field(out, "latest_version", this->latest_version);
dump_field(out, "title", this->title);
dump_field(out, "release_summary", this->release_summary);
dump_field(out, "release_url", this->release_url);
dump_field(out, "current_version", this->current_version_ref_);
dump_field(out, "latest_version", this->latest_version_ref_);
dump_field(out, "title", this->title_ref_);
dump_field(out, "release_summary", this->release_summary_ref_);
dump_field(out, "release_url", this->release_url_ref_);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
#endif

View File

@@ -10,10 +10,6 @@
#include "esphome/components/climate/climate_traits.h"
#endif
#ifdef USE_WATER_HEATER
#include "esphome/components/water_heater/water_heater.h"
#endif
#ifdef USE_LIGHT
#include "esphome/components/light/light_traits.h"
#endif

View File

@@ -24,6 +24,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_hello_request(msg);
break;
}
#ifdef USE_API_PASSWORD
case AuthenticationRequest::MESSAGE_TYPE: {
AuthenticationRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_authentication_request: %s", msg.dump().c_str());
#endif
this->on_authentication_request(msg);
break;
}
#endif
case DisconnectRequest::MESSAGE_TYPE: {
DisconnectRequest msg;
// Empty message: no decode needed
@@ -610,17 +621,6 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
this->on_homeassistant_action_response(msg);
break;
}
#endif
#ifdef USE_WATER_HEATER
case WaterHeaterCommandRequest::MESSAGE_TYPE: {
WaterHeaterCommandRequest msg;
msg.decode(msg_data, msg_size);
#ifdef HAS_PROTO_MESSAGE_DUMP
ESP_LOGVV(TAG, "on_water_heater_command_request: %s", msg.dump().c_str());
#endif
this->on_water_heater_command_request(msg);
break;
}
#endif
default:
break;
@@ -632,6 +632,13 @@ void APIServerConnection::on_hello_request(const HelloRequest &msg) {
this->on_fatal_error();
}
}
#ifdef USE_API_PASSWORD
void APIServerConnection::on_authentication_request(const AuthenticationRequest &msg) {
if (!this->send_authenticate_response(msg)) {
this->on_fatal_error();
}
}
#endif
void APIServerConnection::on_disconnect_request(const DisconnectRequest &msg) {
if (!this->send_disconnect_response(msg)) {
this->on_fatal_error();
@@ -823,7 +830,10 @@ void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg)
void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements for messages
switch (msg_type) {
case HelloRequest::MESSAGE_TYPE: // No setup required
case HelloRequest::MESSAGE_TYPE: // No setup required
#ifdef USE_API_PASSWORD
case AuthenticationRequest::MESSAGE_TYPE: // No setup required
#endif
case DisconnectRequest::MESSAGE_TYPE: // No setup required
case PingRequest::MESSAGE_TYPE: // No setup required
break; // Skip all checks for these messages

View File

@@ -26,6 +26,10 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_hello_request(const HelloRequest &value){};
#ifdef USE_API_PASSWORD
virtual void on_authentication_request(const AuthenticationRequest &value){};
#endif
virtual void on_disconnect_request(const DisconnectRequest &value){};
virtual void on_disconnect_response(const DisconnectResponse &value){};
virtual void on_ping_request(const PingRequest &value){};
@@ -87,10 +91,6 @@ class APIServerConnectionBase : public ProtoService {
virtual void on_climate_command_request(const ClimateCommandRequest &value){};
#endif
#ifdef USE_WATER_HEATER
virtual void on_water_heater_command_request(const WaterHeaterCommandRequest &value){};
#endif
#ifdef USE_NUMBER
virtual void on_number_command_request(const NumberCommandRequest &value){};
#endif
@@ -224,6 +224,9 @@ class APIServerConnectionBase : public ProtoService {
class APIServerConnection : public APIServerConnectionBase {
public:
virtual bool send_hello_response(const HelloRequest &msg) = 0;
#ifdef USE_API_PASSWORD
virtual bool send_authenticate_response(const AuthenticationRequest &msg) = 0;
#endif
virtual bool send_disconnect_response(const DisconnectRequest &msg) = 0;
virtual bool send_ping_response(const PingRequest &msg) = 0;
virtual bool send_device_info_response(const DeviceInfoRequest &msg) = 0;
@@ -350,6 +353,9 @@ class APIServerConnection : public APIServerConnectionBase {
#endif
protected:
void on_hello_request(const HelloRequest &msg) override;
#ifdef USE_API_PASSWORD
void on_authentication_request(const AuthenticationRequest &msg) override;
#endif
void on_disconnect_request(const DisconnectRequest &msg) override;
void on_ping_request(const PingRequest &msg) override;
void on_device_info_request(const DeviceInfoRequest &msg) override;

View File

@@ -4,8 +4,8 @@
#include "api_connection.h"
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
@@ -52,6 +52,11 @@ void APIServer::setup() {
#endif
#endif
// Schedule reboot if no clients connect within timeout
if (this->reboot_timeout_ != 0) {
this->schedule_reboot_timeout_();
}
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0); // monitored for incoming connections
if (this->socket_ == nullptr) {
ESP_LOGW(TAG, "Could not create socket");
@@ -102,16 +107,24 @@ void APIServer::setup() {
#ifdef USE_CAMERA
if (camera::Camera::instance() != nullptr && !camera::Camera::instance()->is_internal()) {
camera::Camera::instance()->add_listener(this);
camera::Camera::instance()->add_image_callback([this](const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->clients_) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
});
}
#endif
}
// Initialize last_connected_ for reboot timeout tracking
this->last_connected_ = App.get_loop_component_start_time();
// Set warning status if reboot timeout is enabled
if (this->reboot_timeout_ != 0) {
this->status_set_warning();
}
void APIServer::schedule_reboot_timeout_() {
this->status_set_warning();
this->set_timeout("api_reboot", this->reboot_timeout_, []() {
if (!global_api_server->is_connected()) {
ESP_LOGE(TAG, "No clients; rebooting");
App.reboot();
}
});
}
void APIServer::loop() {
@@ -125,41 +138,29 @@ void APIServer::loop() {
if (!sock)
break;
char peername[socket::SOCKADDR_STR_LEN];
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, sock->getpeername().c_str());
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
}
ESP_LOGD(TAG, "Accept %s", peername);
ESP_LOGD(TAG, "Accept %s", sock->getpeername().c_str());
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
conn->start();
// First client connected - clear warning and update timestamp
// Clear warning status and cancel reboot when first client connects
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
this->cancel_timeout("api_reboot");
}
}
}
if (this->clients_.empty()) {
// Check reboot timeout - done in loop to avoid scheduler heap churn
// (cancelled scheduler items sit in heap memory until their scheduled time)
if (this->reboot_timeout_ != 0) {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_connected_ > this->reboot_timeout_) {
ESP_LOGE(TAG, "No clients; rebooting");
App.reboot();
}
}
return;
}
@@ -169,7 +170,8 @@ void APIServer::loop() {
// Network is down - disconnect all clients
for (auto &client : this->clients_) {
client->on_fatal_error();
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(),
client->client_info_.peername.c_str());
}
// Continue to process and clean up the clients below
}
@@ -187,12 +189,9 @@ void APIServer::loop() {
// Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->client_disconnected_trigger_->trigger(std::string(client->get_name()), std::string(client->get_peername()));
this->client_disconnected_trigger_->trigger(client->client_info_.name, client->client_info_.peername);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
ESP_LOGV(TAG, "Remove connection %s", client->client_info_.name.c_str());
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
@@ -200,10 +199,9 @@ void APIServer::loop() {
}
this->clients_.pop_back();
// Last client disconnected - set warning and start tracking for reboot timeout
// Schedule reboot when last client disconnects
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
this->schedule_reboot_timeout_();
}
// Don't increment client_index since we need to process the swapped element
}
@@ -226,6 +224,38 @@ void APIServer::dump_config() {
#endif
}
#ifdef USE_API_PASSWORD
bool APIServer::check_password(const uint8_t *password_data, size_t password_len) const {
// depend only on input password length
const char *a = this->password_.c_str();
uint32_t len_a = this->password_.length();
const char *b = reinterpret_cast<const char *>(password_data);
uint32_t len_b = password_len;
// disable optimization with volatile
volatile uint32_t length = len_b;
volatile const char *left = nullptr;
volatile const char *right = b;
uint8_t result = 0;
if (len_a == length) {
left = *((volatile const char **) &a);
result = 0;
}
if (len_a != length) {
left = b;
result = 1;
}
for (size_t i = 0; i < length; i++) {
result |= *left++ ^ *right++; // NOLINT
}
return result == 0;
}
#endif
void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for controller update dispatch
@@ -305,10 +335,6 @@ API_DISPATCH_UPDATE(valve::Valve, valve)
API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif
#ifdef USE_WATER_HEATER
API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
#endif
#ifdef USE_EVENT
// Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type()
@@ -347,6 +373,10 @@ float APIServer::get_setup_priority() const { return setup_priority::AFTER_WIFI;
void APIServer::set_port(uint16_t port) { this->port_ = port; }
#ifdef USE_API_PASSWORD
void APIServer::set_password(const std::string &password) { this->password_ = password; }
#endif
void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = batch_delay; }
#ifdef USE_API_HOMEASSISTANT_SERVICES
@@ -360,7 +390,7 @@ void APIServer::register_action_response_callback(uint32_t call_id, ActionRespon
this->action_response_callbacks_.push_back({call_id, std::move(callback)});
}
void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message) {
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
if (it->call_id == call_id) {
auto callback = std::move(it->callback);
@@ -372,7 +402,7 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
}
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef error_message,
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len) {
for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
if (it->call_id == call_id) {
@@ -389,76 +419,25 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, StringRef
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication)
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once) {
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = false,
});
}
// Helper to add subscription with heap-allocated strings (reduces duplication)
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f, bool once) {
HomeAssistantStateSubscription sub;
// Allocate heap storage for the strings
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
sub.entity_id = sub.entity_id_dynamic_storage->c_str();
if (attribute.has_value()) {
sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
sub.attribute = sub.attribute_dynamic_storage->c_str();
} else {
sub.attribute = nullptr;
}
sub.callback = std::move(f);
sub.once = once;
this->state_subs_.push_back(std::move(sub));
}
// New const char* overload (for internal components - zero allocation)
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
}
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(StringRef)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
}
// std::string overload with StringRef callback (zero-allocation callback)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
// Legacy helper: wraps std::string callback and delegates to StringRef version
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once) {
// Wrap callback to convert StringRef -> std::string, then delegate
this->add_state_subscription_(std::move(entity_id), std::move(attribute),
std::function<void(StringRef)>([f = std::move(f)](StringRef state) { f(state.str()); }),
once);
}
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f) {
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = true,
});
};
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
return this->state_subs_;
@@ -565,15 +544,6 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
}
#endif
#ifdef USE_CAMERA
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->clients_) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
}
#endif
void APIServer::on_shutdown() {
this->shutting_down_ = true;
@@ -609,84 +579,5 @@ bool APIServer::teardown() {
return this->clients_.empty();
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Timeout for action calls - matches aioesphomeapi client timeout (default 30s)
// Can be overridden via USE_API_ACTION_CALL_TIMEOUT_MS define for testing
#ifndef USE_API_ACTION_CALL_TIMEOUT_MS
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
#endif
uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
uint32_t action_call_id = this->next_action_call_id_++;
// Handle wraparound (skip 0 as it means "no call")
if (this->next_action_call_id_ == 0) {
this->next_action_call_id_ = 1;
}
this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
// Schedule automatic cleanup after timeout (client will have given up by then)
this->set_timeout(str_sprintf("action_call_%u", action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS,
[this, action_call_id]() {
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
this->unregister_active_action_call(action_call_id);
});
return action_call_id;
}
void APIServer::unregister_active_action_call(uint32_t action_call_id) {
// Cancel the timeout for this action call
this->cancel_timeout(str_sprintf("action_call_%u", action_call_id));
// Swap-and-pop is more efficient than remove_if for unordered vectors
for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
if (this->active_action_calls_[i].action_call_id == action_call_id) {
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
this->active_action_calls_.pop_back();
return;
}
}
}
void APIServer::unregister_active_action_calls_for_connection(APIConnection *conn) {
// Remove all active action calls for disconnected connection using swap-and-pop
for (size_t i = 0; i < this->active_action_calls_.size();) {
if (this->active_action_calls_[i].connection == conn) {
// Cancel the timeout for this action call
this->cancel_timeout(str_sprintf("action_call_%u", this->active_action_calls_[i].action_call_id));
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
this->active_action_calls_.pop_back();
// Don't increment i - need to check the swapped element
} else {
i++;
}
}
}
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message) {
for (auto &call : this->active_action_calls_) {
if (call.action_call_id == action_call_id) {
call.connection->send_execute_service_response(call.client_call_id, success, error_message);
return;
}
}
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void APIServer::send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
const uint8_t *response_data, size_t response_data_len) {
for (auto &call : this->active_action_calls_) {
if (call.action_call_id == action_call_id) {
call.connection->send_execute_service_response(call.client_call_id, success, error_message, response_data,
response_data_len);
return;
}
}
ESP_LOGW(TAG, "Cannot send response: no active call found for action_call_id %u", action_call_id);
}
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
} // namespace esphome::api
#endif

View File

@@ -10,25 +10,20 @@
#include "esphome/core/component.h"
#include "esphome/core/controller.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
#include "list_entities.h"
#include "subscribe_state.h"
#ifdef USE_API_USER_DEFINED_ACTIONS
#include "user_services.h"
#endif
#ifdef USE_LOGGER
#include "esphome/components/logger/logger.h"
#endif
#ifdef USE_CAMERA
#include "esphome/components/camera/camera.h"
#endif
#include <map>
#include <vector>
namespace esphome::api {
#ifdef USE_API_USER_DEFINED_ACTIONS
// Forward declaration - full definition in user_services.h
class UserServiceDescriptor;
#endif
#ifdef USE_API_NOISE
struct SavedNoisePsk {
psk_t psk;
@@ -41,10 +36,6 @@ class APIServer : public Component,
,
public logger::LogListener
#endif
#ifdef USE_CAMERA
,
public camera::CameraListener
#endif
{
public:
APIServer();
@@ -58,8 +49,9 @@ class APIServer : public Component,
#ifdef USE_LOGGER
void on_log(uint8_t level, const char *tag, const char *message, size_t message_len) override;
#endif
#ifdef USE_CAMERA
void on_camera_image(const std::shared_ptr<camera::CameraImage> &image) override;
#ifdef USE_API_PASSWORD
bool check_password(const uint8_t *password_data, size_t password_len) const;
void set_password(const std::string &password);
#endif
void set_port(uint16_t port);
void set_reboot_timeout(uint32_t reboot_timeout);
@@ -130,9 +122,6 @@ class APIServer : public Component,
#ifdef USE_MEDIA_PLAYER
void on_media_player_update(media_player::MediaPlayer *obj) override;
#endif
#ifdef USE_WATER_HEATER
void on_water_heater_update(water_heater::WaterHeater *obj) override;
#endif
#ifdef USE_API_HOMEASSISTANT_SERVICES
void send_homeassistant_action(const HomeassistantActionRequest &call);
@@ -140,10 +129,10 @@ class APIServer : public Component,
// Action response handling
using ActionResponseCallback = std::function<void(const class ActionResponse &)>;
void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
void handle_action_response(uint32_t call_id, bool success, StringRef error_message);
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
void handle_action_response(uint32_t call_id, bool success, StringRef error_message, const uint8_t *response_data,
size_t response_data_len);
void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
const uint8_t *response_data, size_t response_data_len);
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
#endif // USE_API_HOMEASSISTANT_ACTION_RESPONSES
#endif // USE_API_HOMEASSISTANT_SERVICES
@@ -155,19 +144,6 @@ class APIServer : public Component,
// Only compile push_back method when custom_services: true (external components)
void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Action call context management - supports concurrent calls from multiple clients
// Returns server-generated action_call_id to avoid collisions when clients use same call_id
uint32_t register_active_action_call(uint32_t client_call_id, APIConnection *conn);
void unregister_active_action_call(uint32_t action_call_id);
void unregister_active_action_calls_for_connection(APIConnection *conn);
// Send response for a specific action call (uses action_call_id, sends client_call_id in response)
void send_action_response(uint32_t action_call_id, bool success, StringRef error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void send_action_response(uint32_t action_call_id, bool success, StringRef error_message,
const uint8_t *response_data, size_t response_data_len);
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#ifdef USE_HOMEASSISTANT_TIME
void request_time();
@@ -190,33 +166,16 @@ class APIServer : public Component,
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
const char *entity_id; // Pointer to flash (internal) or heap (external)
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute)
std::function<void(StringRef)> callback;
std::string entity_id;
optional<std::string> attribute;
std::function<void(std::string)> callback;
bool once;
// Dynamic storage for external components using std::string API (custom_api_device.h)
// These are only allocated when using the std::string overload (nullptr for const char* overload)
std::unique_ptr<std::string> entity_id_dynamic_storage;
std::unique_ptr<std::string> attribute_dynamic_storage;
};
// New const char* overload (for internal components - zero allocation)
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(StringRef)> f);
// std::string overload with StringRef callback (for custom_api_device.h with zero-allocation callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f);
std::function<void(std::string)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(StringRef)> f);
// Legacy std::string overload (for custom_api_device.h - converts StringRef to std::string for callback)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f);
std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
@@ -233,20 +192,11 @@ class APIServer : public Component,
#endif
protected:
void schedule_reboot_timeout_();
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active);
#endif // USE_API_NOISE
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper methods to reduce code duplication
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(StringRef)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute, std::function<void(StringRef)> f,
bool once);
// Legacy helper: wraps std::string callback and delegates to StringRef version
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(const std::string &)> f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES
// Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr;
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
@@ -258,27 +208,18 @@ class APIServer : public Component,
// 4-byte aligned types
uint32_t reboot_timeout_{300000};
uint32_t last_connected_{0};
// Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_;
#ifdef USE_API_PASSWORD
std::string password_;
#endif
std::vector<uint8_t> shared_write_buffer_; // Shared proto write buffer for all connections
#ifdef USE_API_HOMEASSISTANT_STATES
std::vector<HomeAssistantStateSubscription> state_subs_;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
std::vector<UserServiceDescriptor *> user_services_;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Active action calls - supports concurrent calls from multiple clients
// Uses server-generated action_call_id to avoid collisions when multiple clients use same call_id
struct ActiveActionCall {
uint32_t action_call_id; // Server-generated unique ID (passed to actions)
uint32_t client_call_id; // Client's original call_id (used in response)
APIConnection *connection;
};
std::vector<ActiveActionCall> active_action_calls_;
uint32_t next_action_call_id_{1}; // Counter for generating unique action_call_ids
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
struct PendingActionResponse {

View File

@@ -16,7 +16,7 @@ with warnings.catch_warnings():
import contextlib
from esphome.const import CONF_KEY, CONF_PORT, __version__
from esphome.const import CONF_KEY, CONF_PASSWORD, CONF_PORT, __version__
from esphome.core import CORE
from . import CONF_ENCRYPTION
@@ -35,6 +35,7 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
conf = config["api"]
name = config["esphome"]["name"]
port: int = int(conf[CONF_PORT])
password: str = conf[CONF_PASSWORD]
noise_psk: str | None = None
if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
noise_psk = key
@@ -49,7 +50,7 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
cli = APIClient(
addresses[0], # Primary address for compatibility
port,
"", # Password auth removed in 2026.1.0
password,
client_info=f"ESPHome Logs {__version__}",
noise_psk=noise_psk,
addresses=addresses, # Pass all addresses for automatic retry

View File

@@ -16,10 +16,7 @@ template<typename T, typename... Ts> class CustomAPIDeviceService : public UserS
: UserServiceDynamic<Ts...>(name, arg_names), obj_(obj), callback_(callback) {}
protected:
// CustomAPIDevice services don't support action responses - ignore call_id and return_response
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override {
(this->obj_->*this->callback_)(x...); // NOLINT
}
void execute(Ts... x) override { (this->obj_->*this->callback_)(x...); } // NOLINT
T *obj_;
void (T::*callback_)(Ts...);
@@ -122,36 +119,21 @@ class CustomAPIDevice {
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "climate.kitchen", "current_temperature");
* }
*
* void on_state_changed(StringRef state) {
* // State of climate.kitchen current_temperature is `state`
* // Use state.c_str() for C string, state.str() for std::string
* void on_state_changed(std::string state) {
* // State of sensor.weather_forecast is `state`
* }
* ```
*
* @tparam T The class type creating the service, automatically deduced from the function pointer.
* @param callback The member function to call when the entity state changes (zero-allocation).
* @param callback The member function to call when the entity state changes.
* @param entity_id The entity_id to track.
* @param attribute The entity state attribute to track.
*/
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
*
* @deprecated Use the StringRef overload for zero-allocation callbacks. Will be removed in 2027.1.0.
*/
template<typename T>
ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, std::placeholders::_1);
// Explicit type to disambiguate overload resolution
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
@@ -163,45 +145,23 @@ class CustomAPIDevice {
* subscribe_homeassistant_state(&CustomNativeAPI::on_state_changed, "sensor.weather_forecast");
* }
*
* void on_state_changed(const std::string &entity_id, StringRef state) {
* void on_state_changed(std::string entity_id, std::string state) {
* // State of `entity_id` is `state`
* }
* ```
*
* @tparam T The class type creating the service, automatically deduced from the function pointer.
* @param callback The member function to call when the entity state changes (zero-allocation for state).
* @param callback The member function to call when the entity state changes.
* @param entity_id The entity_id to track.
* @param attribute The entity state attribute to track.
*/
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), std::move(f));
}
/** Subscribe to the state (or attribute state) of an entity from Home Assistant (legacy std::string version).
*
* @deprecated Use the StringRef overload for zero-allocation callbacks. Will be removed in 2027.1.0.
*/
template<typename T>
ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
// Explicit type to disambiguate overload resolution
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
std::function<void(const std::string &)>(f));
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
}
#else
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
const std::string &attribute = "") {
@@ -210,14 +170,6 @@ class CustomAPIDevice {
"of your YAML configuration");
}
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
const std::string &attribute = "") {
static_assert(sizeof(T) == 0,
"subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
"of your YAML configuration");
}
template<typename T>
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
const std::string &attribute = "") {
@@ -240,7 +192,7 @@ class CustomAPIDevice {
*/
void call_homeassistant_service(const std::string &service_name) {
HomeassistantActionRequest resp;
resp.service = StringRef(service_name);
resp.set_service(StringRef(service_name));
global_api_server->send_homeassistant_action(resp);
}
@@ -260,12 +212,12 @@ class CustomAPIDevice {
*/
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp;
resp.service = StringRef(service_name);
resp.set_service(StringRef(service_name));
resp.data.init(data.size());
for (auto &it : data) {
auto &kv = resp.data.emplace_back();
kv.key = StringRef(it.first);
kv.value = it.second; // value is std::string (no_zero_copy), assign directly
kv.set_key(StringRef(it.first));
kv.value = it.second;
}
global_api_server->send_homeassistant_action(resp);
}
@@ -282,7 +234,7 @@ class CustomAPIDevice {
*/
void fire_homeassistant_event(const std::string &event_name) {
HomeassistantActionRequest resp;
resp.service = StringRef(event_name);
resp.set_service(StringRef(event_name));
resp.is_event = true;
global_api_server->send_homeassistant_action(resp);
}
@@ -302,13 +254,13 @@ class CustomAPIDevice {
*/
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp;
resp.service = StringRef(service_name);
resp.set_service(StringRef(service_name));
resp.is_event = true;
resp.data.init(data.size());
for (auto &it : data) {
auto &kv = resp.data.emplace_back();
kv.key = StringRef(it.first);
kv.value = it.second; // value is std::string (no_zero_copy), assign directly
kv.set_key(StringRef(it.first));
kv.value = it.second;
}
global_api_server->send_homeassistant_action(resp);
}

View File

@@ -12,17 +12,10 @@
#endif
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
template<typename... X> class TemplatableStringValue : public TemplatableValue<std::string, X...> {
// Verify that const char* uses the base class STATIC_STRING optimization (no heap allocation)
// rather than being wrapped in a lambda. The base class constructor for const char* is more
// specialized than the templated constructor here, so it should be selected.
static_assert(std::is_constructible_v<TemplatableValue<std::string, X...>, const char *>,
"Base class must have const char* constructor for STATIC_STRING optimization");
private:
// Helper to convert value to string - handles the case where value is already a string
template<typename T> static std::string value_to_string(T &&val) { return to_string(std::forward<T>(val)); }
@@ -53,25 +46,23 @@ template<typename... Ts> class TemplatableKeyValuePair {
// Keys are always string literals from YAML dictionary keys (e.g., "code", "event")
// and never templatable values or lambdas. Only the value parameter can be a lambda/template.
// Using const char* avoids std::string heap allocation - keys remain in flash.
template<typename T> TemplatableKeyValuePair(const char *key, T value) : key(key), value(value) {}
// Using pass-by-value with std::move allows optimal performance for both lvalues and rvalues.
template<typename T> TemplatableKeyValuePair(std::string key, T value) : key(std::move(key)), value(value) {}
const char *key{nullptr};
std::string key;
TemplatableStringValue<Ts...> value;
};
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
// Represents the response data from a Home Assistant action
// Note: This class holds a StringRef to the error_message from the protobuf message.
// The protobuf message must outlive the ActionResponse (which is guaranteed since
// the callback is invoked synchronously while the message is on the stack).
class ActionResponse {
public:
ActionResponse(bool success, StringRef error_message) : success_(success), error_message_(error_message) {}
ActionResponse(bool success, std::string error_message = "")
: success_(success), error_message_(std::move(error_message)) {}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
ActionResponse(bool success, StringRef error_message, const uint8_t *data, size_t data_len)
: success_(success), error_message_(error_message) {
ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
: success_(success), error_message_(std::move(error_message)) {
if (data == nullptr || data_len == 0)
return;
this->json_document_ = json::parse_json(data, data_len);
@@ -79,8 +70,7 @@ class ActionResponse {
#endif
bool is_success() const { return this->success_; }
// Returns reference to error message - can be implicitly converted to std::string if needed
const StringRef &get_error_message() const { return this->error_message_; }
const std::string &get_error_message() const { return this->error_message_; }
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
// Get data as parsed JSON object (const version returns read-only view)
@@ -89,7 +79,7 @@ class ActionResponse {
protected:
bool success_;
StringRef error_message_;
std::string error_message_;
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
JsonDocument json_document_;
#endif
@@ -115,15 +105,14 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
// Keys are always string literals from the Python code generation (e.g., cg.add(var.add_data("tag_id", templ))).
// The value parameter can be a lambda/template, but keys are never templatable.
// Using const char* for keys avoids std::string heap allocation - keys remain in flash.
template<typename V> void add_data(const char *key, V &&value) {
this->add_kv_(this->data_, key, std::forward<V>(value));
template<typename K, typename V> void add_data(K &&key, V &&value) {
this->add_kv_(this->data_, std::forward<K>(key), std::forward<V>(value));
}
template<typename V> void add_data_template(const char *key, V &&value) {
this->add_kv_(this->data_template_, key, std::forward<V>(value));
template<typename K, typename V> void add_data_template(K &&key, V &&value) {
this->add_kv_(this->data_template_, std::forward<K>(key), std::forward<V>(value));
}
template<typename V> void add_variable(const char *key, V &&value) {
this->add_kv_(this->variables_, key, std::forward<V>(value));
template<typename K, typename V> void add_variable(K &&key, V &&value) {
this->add_kv_(this->variables_, std::forward<K>(key), std::forward<V>(value));
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
@@ -147,7 +136,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
void play(const Ts &...x) override {
HomeassistantActionRequest resp;
std::string service_value = this->service_.value(x...);
resp.service = StringRef(service_value);
resp.set_service(StringRef(service_value));
resp.is_event = this->flags_.is_event;
this->populate_service_map(resp.data, this->data_, x...);
this->populate_service_map(resp.data_template, this->data_template_, x...);
@@ -196,11 +185,10 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
}
protected:
// Helper to add key-value pairs to FixedVectors
// Keys are always string literals (const char*), values can be lambdas/templates
template<typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, const char *key, V &&value) {
// Helper to add key-value pairs to FixedVectors with perfect forwarding to avoid copies
template<typename K, typename V> void add_kv_(FixedVector<TemplatableKeyValuePair<Ts...>> &vec, K &&key, V &&value) {
auto &kv = vec.emplace_back();
kv.key = key;
kv.key = std::forward<K>(key);
kv.value = std::forward<V>(value);
}
@@ -209,7 +197,7 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
dest.init(source.size());
for (auto &it : source) {
auto &kv = dest.emplace_back();
kv.key = StringRef(it.key);
kv.set_key(StringRef(it.key));
kv.value = it.value.value(x...);
}
}

View File

@@ -5,9 +5,6 @@
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#ifdef USE_API_USER_DEFINED_ACTIONS
#include "user_services.h"
#endif
namespace esphome::api {
@@ -73,9 +70,6 @@ LIST_ENTITIES_HANDLER(media_player, media_player::MediaPlayer, ListEntitiesMedia
LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel,
ListEntitiesAlarmControlPanelResponse)
#endif
#ifdef USE_WATER_HEATER
LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse)
#endif
#ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif

View File

@@ -82,9 +82,6 @@ class ListEntitiesIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *entity) override;
#endif

View File

@@ -54,16 +54,16 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) {
* 3. Global/static strings: StringRef(GLOBAL_CONSTANT) - Always safe
* 4. Local variables: Safe ONLY if encoding happens before function returns:
* std::string temp = compute_value();
* msg.field = StringRef(temp);
* msg.set_field(StringRef(temp));
* return this->send_message(msg); // temp is valid during encoding
*
* Unsafe Patterns (WILL cause crashes/corruption):
* 1. Temporaries: msg.field = StringRef(obj.get_string()) // get_string() returns by value
* 2. Concatenation: msg.field = StringRef(str1 + str2) // Result is temporary
* 1. Temporaries: msg.set_field(StringRef(obj.get_string())) // get_string() returns by value
* 2. Concatenation: msg.set_field(StringRef(str1 + str2)) // Result is temporary
*
* For unsafe patterns, store in a local variable first:
* std::string temp = get_string(); // or str1 + str2
* msg.field = StringRef(temp);
* msg.set_field(StringRef(temp));
*
* The send_*_response pattern ensures proper lifetime management by encoding
* within the same function scope where temporaries are created.
@@ -334,7 +334,7 @@ class ProtoWriteBuffer {
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, encode_zigzag64(value), force);
}
void encode_message(uint32_t field_id, const ProtoMessage &value);
void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
std::vector<uint8_t> *get_buffer() const { return buffer_; }
protected:
@@ -795,7 +795,7 @@ class ProtoSize {
};
// Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value, bool force) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message
// Calculate the message size first
@@ -833,6 +833,9 @@ class ProtoService {
virtual bool is_authenticated() = 0;
virtual bool is_connection_setup() = 0;
virtual void on_fatal_error() = 0;
#ifdef USE_API_PASSWORD
virtual void on_unauthenticated_access() = 0;
#endif
virtual void on_no_setup_connection() = 0;
/**
* Create a buffer with a reserved size.
@@ -870,7 +873,20 @@ class ProtoService {
return true;
}
inline bool check_authenticated_() { return this->check_connection_setup_(); }
inline bool check_authenticated_() {
#ifdef USE_API_PASSWORD
if (!this->check_connection_setup_()) {
return false;
}
if (!this->is_authenticated()) {
this->on_unauthenticated_access();
return false;
}
return true;
#else
return this->check_connection_setup_();
#endif
}
};
} // namespace esphome::api

View File

@@ -60,9 +60,6 @@ INITIAL_STATE_HANDLER(media_player, media_player::MediaPlayer)
#ifdef USE_ALARM_CONTROL_PANEL
INITIAL_STATE_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPanel)
#endif
#ifdef USE_WATER_HEATER
INITIAL_STATE_HANDLER(water_heater, water_heater::WaterHeater)
#endif
#ifdef USE_UPDATE
INITIAL_STATE_HANDLER(update, update::UpdateEntity)
#endif

View File

@@ -76,9 +76,6 @@ class InitialStateIterator : public ComponentIterator {
#ifdef USE_ALARM_CONTROL_PANEL
bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *entity) override;
#endif
#ifdef USE_WATER_HEATER
bool on_water_heater(water_heater::WaterHeater *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; };
#endif

View File

@@ -1,31 +1,20 @@
#pragma once
#include <tuple>
#include <utility>
#include <vector>
#include "api_pb2.h"
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
#include "esphome/components/json/json_util.h"
#endif
#include "esphome/core/automation.h"
#include "api_pb2.h"
#ifdef USE_API_USER_DEFINED_ACTIONS
namespace esphome::api {
// Forward declaration - full definition in api_server.h
class APIServer;
class UserServiceDescriptor {
public:
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Overload that accepts server-generated action_call_id (avoids client call_id collisions)
virtual bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) = 0;
#endif
bool is_internal() { return false; }
};
@@ -38,23 +27,21 @@ template<typename T> enums::ServiceArgType to_service_arg_type();
// Stores only pointers to string literals in flash - no heap allocation
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
public:
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names,
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
: name_(name), arg_names_(arg_names), supports_response_(supports_response) {
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: name_(name), arg_names_(arg_names) {
this->key_ = fnv1_hash(name);
}
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.set_name(StringRef(this->name_));
msg.key = this->key_;
msg.supports_response = this->supports_response_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = StringRef(this->arg_names_[i]);
arg.set_name(StringRef(this->arg_names_[i]));
}
return msg;
}
@@ -64,37 +51,21 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
return false;
if (req.args.size() != sizeof...(Ts))
return false;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
#else
this->execute_(req.args, 0, false, std::make_index_sequence<sizeof...(Ts)>{});
#endif
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
return true;
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override {
if (req.key != this->key_)
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
return true;
}
#endif
protected:
virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0;
virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, size_t... S>
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}
// Pointers to string literals in flash - no heap allocation
const char *name_;
std::array<const char *, sizeof...(Ts)> arg_names_;
uint32_t key_{0};
enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE};
};
// Separate class for custom_api_device services (rare case)
@@ -108,15 +79,14 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.set_name(StringRef(this->name_));
msg.key = this->key_;
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = StringRef(this->arg_names_[i]);
arg.set_name(StringRef(this->arg_names_[i]));
}
return msg;
}
@@ -126,31 +96,15 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
return false;
if (req.args.size() != sizeof...(Ts))
return false;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->execute_(req.args, req.call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
#else
this->execute_(req.args, 0, false, std::make_index_sequence<sizeof...(Ts)>{});
#endif
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
return true;
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Dynamic services don't support responses yet, but need to implement the interface
bool execute_service(const ExecuteServiceRequest &req, uint32_t action_call_id) override {
if (req.key != this->key_)
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, action_call_id, req.return_response, std::make_index_sequence<sizeof...(Ts)>{});
return true;
}
#endif
protected:
virtual void execute(uint32_t call_id, bool return_response, Ts... x) = 0;
virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, size_t... S>
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}
// Heap-allocated strings for runtime-generated names
@@ -159,149 +113,15 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
uint32_t key_{0};
};
// Primary template declaration
template<enums::SupportsResponseType Mode, typename... Ts> class UserServiceTrigger;
// Specialization for NONE - no extra trigger arguments
template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> : public UserServiceBase<Ts...>, public Trigger<Ts...> {
template<typename... Ts> class UserServiceTrigger : public UserServiceBase<Ts...>, public Trigger<Ts...> {
public:
// Constructor for static names (YAML-defined services - used by code generator)
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {}
: UserServiceBase<Ts...>(name, arg_names) {}
protected:
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
};
// Specialization for OPTIONAL - call_id and return_response trigger arguments
template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> : public UserServiceBase<Ts...>,
public Trigger<uint32_t, bool, Ts...> {
public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
protected:
void execute(uint32_t call_id, bool return_response, Ts... x) override {
this->trigger(call_id, return_response, x...);
}
};
// Specialization for ONLY - just call_id trigger argument
template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
};
// Specialization for STATUS - just call_id trigger argument (reports success/error without data)
template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
void execute(Ts... x) override { this->trigger(x...); } // NOLINT
};
} // namespace esphome::api
#endif // USE_API_USER_DEFINED_ACTIONS
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
// Include full definition of APIServer for template implementation
// Must be outside namespace to avoid including STL headers inside namespace
#include "api_server.h"
namespace esphome::api {
template<typename... Ts> class APIRespondAction : public Action<Ts...> {
public:
explicit APIRespondAction(APIServer *parent) : parent_(parent) {}
template<typename V> void set_success(V success) { this->success_ = success; }
template<typename V> void set_error_message(V error) { this->error_message_ = error; }
void set_is_optional_mode(bool is_optional) { this->is_optional_mode_ = is_optional; }
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
void set_data(std::function<void(Ts..., JsonObject)> func) {
this->json_builder_ = std::move(func);
this->has_data_ = true;
}
#endif
void play(const Ts &...x) override {
// Extract call_id from first argument - it's always first for optional/only/status modes
auto args = std::make_tuple(x...);
uint32_t call_id = std::get<0>(args);
bool success = this->success_.value(x...);
std::string error_message = this->error_message_.value(x...);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
if (this->has_data_) {
// For optional mode, check return_response (second arg) to decide if client wants data
// Use nested if constexpr to avoid compile error when tuple doesn't have enough elements
// (std::tuple_element_t is evaluated before the && short-circuit, so we must nest)
if constexpr (sizeof...(Ts) >= 2) {
if constexpr (std::is_same_v<std::tuple_element_t<1, std::tuple<Ts...>>, bool>) {
if (this->is_optional_mode_) {
bool return_response = std::get<1>(args);
if (!return_response) {
// Client doesn't want response data, just send success/error
this->parent_->send_action_response(call_id, success, StringRef(error_message));
return;
}
}
}
}
// Build and send JSON response
json::JsonBuilder builder;
this->json_builder_(x..., builder.root());
std::string json_str = builder.serialize();
this->parent_->send_action_response(call_id, success, StringRef(error_message),
reinterpret_cast<const uint8_t *>(json_str.data()), json_str.size());
return;
}
#endif
this->parent_->send_action_response(call_id, success, StringRef(error_message));
}
protected:
APIServer *parent_;
TemplatableValue<bool, Ts...> success_{true};
TemplatableValue<std::string, Ts...> error_message_{""};
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
std::function<void(Ts..., JsonObject)> json_builder_;
bool has_data_{false};
#endif
bool is_optional_mode_{false};
};
// Action to unregister a service call after execution completes
// Automatically appended to the end of action lists for non-none response modes
template<typename... Ts> class APIUnregisterServiceCallAction : public Action<Ts...> {
public:
explicit APIUnregisterServiceCallAction(APIServer *parent) : parent_(parent) {}
void play(const Ts &...x) override {
// Extract call_id from first argument - same convention as APIRespondAction
auto args = std::make_tuple(x...);
uint32_t call_id = std::get<0>(args);
if (call_id != 0) {
this->parent_->unregister_active_action_call(call_id);
}
}
protected:
APIServer *parent_;
};
} // namespace esphome::api
#endif // USE_API_USER_DEFINED_ACTION_RESPONSES

View File

@@ -1,14 +0,0 @@
import esphome.codegen as cg
CODEOWNERS = ["@jasstrong", "@ximex", "@freekode"]
aqi_ns = cg.esphome_ns.namespace("aqi")
AQICalculatorType = aqi_ns.enum("AQICalculatorType")
CONF_AQI = "aqi"
CONF_CALCULATION_TYPE = "calculation_type"
AQI_CALCULATION_TYPE = {
"CAQI": AQICalculatorType.CAQI_TYPE,
"AQI": AQICalculatorType.AQI_TYPE,
}

View File

@@ -1,52 +0,0 @@
#pragma once
#include <climits>
#include "abstract_aqi_calculator.h"
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
namespace esphome::aqi {
class AQICalculator : public AbstractAQICalculator {
public:
uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
int pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
int pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index;
}
protected:
static constexpr int NUM_LEVELS = 6;
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
static constexpr int PM2_5_GRID[NUM_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55}, {56, 125}, {126, 225}, {226, INT_MAX}};
static constexpr int PM10_0_GRID[NUM_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254},
{255, 354}, {355, 424}, {425, INT_MAX}};
static int calculate_index(uint16_t value, const int array[NUM_LEVELS][2]) {
int grid_index = get_grid_index(value, array);
if (grid_index == -1) {
return -1;
}
int aqi_lo = INDEX_GRID[grid_index][0];
int aqi_hi = INDEX_GRID[grid_index][1];
int conc_lo = array[grid_index][0];
int conc_hi = array[grid_index][1];
return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
}
static int get_grid_index(uint16_t value, const int array[NUM_LEVELS][2]) {
for (int i = 0; i < NUM_LEVELS; i++) {
if (value >= array[i][0] && value <= array[i][1]) {
return i;
}
}
return -1;
}
};
} // namespace esphome::aqi

View File

@@ -1,52 +0,0 @@
#include "aqi_sensor.h"
#include "esphome/core/log.h"
namespace esphome::aqi {
static const char *const TAG = "aqi";
void AQISensor::setup() {
if (this->pm_2_5_sensor_ != nullptr) {
this->pm_2_5_sensor_->add_on_state_callback([this](float value) {
this->pm_2_5_value_ = value;
// Defer calculation to avoid double-publishing if both sensors update in the same loop
this->defer("update", [this]() { this->calculate_aqi_(); });
});
}
if (this->pm_10_0_sensor_ != nullptr) {
this->pm_10_0_sensor_->add_on_state_callback([this](float value) {
this->pm_10_0_value_ = value;
this->defer("update", [this]() { this->calculate_aqi_(); });
});
}
}
void AQISensor::dump_config() {
ESP_LOGCONFIG(TAG, "AQI Sensor:");
ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI");
if (this->pm_2_5_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str());
}
if (this->pm_10_0_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " PM10 Sensor: '%s'", this->pm_10_0_sensor_->get_name().c_str());
}
LOG_SENSOR(" ", "AQI", this);
}
void AQISensor::calculate_aqi_() {
if (std::isnan(this->pm_2_5_value_) || std::isnan(this->pm_10_0_value_)) {
return;
}
AbstractAQICalculator *calculator = this->aqi_calculator_factory_.get_calculator(this->aqi_calc_type_);
if (calculator == nullptr) {
ESP_LOGW(TAG, "Unknown AQI calculator type");
return;
}
uint16_t aqi =
calculator->get_aqi(static_cast<uint16_t>(this->pm_2_5_value_), static_cast<uint16_t>(this->pm_10_0_value_));
this->publish_state(aqi);
}
} // namespace esphome::aqi

View File

@@ -1,31 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "aqi_calculator_factory.h"
namespace esphome::aqi {
class AQISensor : public sensor::Sensor, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::DATA; }
void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; }
void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; }
void set_aqi_calculation_type(AQICalculatorType type) { this->aqi_calc_type_ = type; }
protected:
void calculate_aqi_();
sensor::Sensor *pm_2_5_sensor_{nullptr};
sensor::Sensor *pm_10_0_sensor_{nullptr};
AQICalculatorType aqi_calc_type_{AQI_TYPE};
AQICalculatorFactory aqi_calculator_factory_;
float pm_2_5_value_{NAN};
float pm_10_0_value_{NAN};
};
} // namespace esphome::aqi

View File

@@ -1,49 +0,0 @@
#pragma once
#include "abstract_aqi_calculator.h"
namespace esphome::aqi {
class CAQICalculator : public AbstractAQICalculator {
public:
uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
int pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
int pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index;
}
protected:
static constexpr int NUM_LEVELS = 5;
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
static constexpr int PM2_5_GRID[NUM_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}};
static constexpr int PM10_0_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}};
static int calculate_index(uint16_t value, const int array[NUM_LEVELS][2]) {
int grid_index = get_grid_index(value, array);
if (grid_index == -1) {
return -1;
}
int aqi_lo = INDEX_GRID[grid_index][0];
int aqi_hi = INDEX_GRID[grid_index][1];
int conc_lo = array[grid_index][0];
int conc_hi = array[grid_index][1];
return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
}
static int get_grid_index(uint16_t value, const int array[NUM_LEVELS][2]) {
for (int i = 0; i < NUM_LEVELS; i++) {
if (value >= array[i][0] && value <= array[i][1]) {
return i;
}
}
return -1;
}
};
} // namespace esphome::aqi

View File

@@ -1,51 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_PM_2_5,
CONF_PM_10_0,
DEVICE_CLASS_AQI,
STATE_CLASS_MEASUREMENT,
)
from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
CODEOWNERS = ["@jasstrong"]
DEPENDENCIES = ["sensor"]
UNIT_INDEX = "index"
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
CONFIG_SCHEMA = (
sensor.sensor_schema(
AQISensor,
unit_of_measurement=UNIT_INDEX,
accuracy_decimals=0,
device_class=DEVICE_CLASS_AQI,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(
{
cv.Required(CONF_PM_2_5): cv.use_id(sensor.Sensor),
cv.Required(CONF_PM_10_0): cv.use_id(sensor.Sensor),
cv.Required(CONF_CALCULATION_TYPE): cv.enum(
AQI_CALCULATION_TYPE, upper=True
),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
pm_2_5_sensor = await cg.get_variable(config[CONF_PM_2_5])
cg.add(var.set_pm_2_5_sensor(pm_2_5_sensor))
pm_10_0_sensor = await cg.get_variable(config[CONF_PM_10_0])
cg.add(var.set_pm_10_0_sensor(pm_10_0_sensor))
cg.add(var.set_aqi_calculation_type(config[CONF_CALCULATION_TYPE]))

View File

@@ -305,14 +305,12 @@ bool AS3935Component::calibrate_oscillator() {
}
void AS3935Component::tune_antenna() {
ESP_LOGI(TAG, "Starting antenna tuning");
uint8_t div_ratio = this->read_div_ratio();
uint8_t tune_val = this->read_capacitance();
ESP_LOGI(TAG,
"Starting antenna tuning\n"
"Division Ratio is set to: %d\n"
"Internal Capacitor is set to: %d\n"
"Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio",
div_ratio, tune_val);
ESP_LOGI(TAG, "Division Ratio is set to: %d", div_ratio);
ESP_LOGI(TAG, "Internal Capacitor is set to: %d", tune_val);
ESP_LOGI(TAG, "Displaying oscillator on INT pin. Measure its frequency - multiply value by Division Ratio");
this->display_oscillator(true, ANTFREQ);
}

View File

@@ -1,50 +1,37 @@
# Async TCP client support for all platforms
# Dummy integration to allow relying on AsyncTCP
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"]
def AUTO_LOAD() -> list[str]:
# Socket component needed for platforms using socket-based implementation
# ESP32, ESP8266, RP2040, and LibreTiny use AsyncTCP libraries, others use sockets
if (
not CORE.is_esp32
and not CORE.is_esp8266
and not CORE.is_rp2040
and not CORE.is_libretiny
):
return ["socket"]
return []
# Support all platforms - Arduino/ESP-IDF get libraries, other platforms use socket implementation
CONFIG_SCHEMA = cv.Schema({})
CONFIG_SCHEMA = cv.All(
cv.Schema({}),
cv.only_with_arduino,
cv.only_on(
[
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
]
),
)
@coroutine_with_priority(CoroPriority.NETWORK_TRANSPORT)
async def to_code(config):
if CORE.is_esp32:
# https://github.com/ESP32Async/AsyncTCP
from esphome.components.esp32 import add_idf_component
add_idf_component(name="esp32async/asynctcp", ref="3.4.91")
elif CORE.is_libretiny:
if CORE.is_esp32 or CORE.is_libretiny:
# https://github.com/ESP32Async/AsyncTCP
cg.add_library("ESP32Async/AsyncTCP", "3.4.5")
elif CORE.is_esp8266:
# https://github.com/ESP32Async/ESPAsyncTCP
cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0")
elif CORE.is_rp2040:
# https://github.com/khoih-prog/AsyncTCP_RP2040W
cg.add_library("khoih-prog/AsyncTCP_RP2040W", "1.2.0")
# Other platforms (host, etc) use socket-based implementation
def FILTER_SOURCE_FILES() -> list[str]:
# Exclude socket implementation for platforms that use AsyncTCP libraries
if CORE.is_esp32 or CORE.is_esp8266 or CORE.is_rp2040 or CORE.is_libretiny:
return ["async_tcp_socket.cpp"]
return []

View File

@@ -1,16 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Use AsyncTCP library for ESP32 (Arduino or ESP-IDF) and LibreTiny
#include <AsyncTCP.h>
#elif defined(USE_ESP8266)
// Use ESPAsyncTCP library for ESP8266 (always Arduino)
#include <ESPAsyncTCP.h>
#elif defined(USE_RP2040)
// Use AsyncTCP_RP2040W library for RP2040
#include <AsyncTCP_RP2040W.h>
#else
// Use socket-based implementation for other platforms
#include "async_tcp_socket.h"
#endif

View File

@@ -1,162 +0,0 @@
#include "async_tcp_socket.h"
#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \
(defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS))
#include "esphome/components/network/util.h"
#include "esphome/core/log.h"
#include <cerrno>
#include <sys/select.h>
namespace esphome::async_tcp {
static const char *const TAG = "async_tcp";
// Read buffer size matches TCP MSS (1500 MTU - 40 bytes IP/TCP headers).
// This implementation only runs on ESP-IDF and host which have ample stack.
static constexpr size_t READ_BUFFER_SIZE = 1460;
bool AsyncClient::connect(const char *host, uint16_t port) {
if (connected_ || connecting_) {
ESP_LOGW(TAG, "Already connected/connecting");
return false;
}
// Resolve address
struct sockaddr_storage addr;
socklen_t addrlen = esphome::socket::set_sockaddr((struct sockaddr *) &addr, sizeof(addr), host, port);
if (addrlen == 0) {
ESP_LOGE(TAG, "Invalid address: %s", host);
if (error_cb_)
error_cb_(error_arg_, this, -1);
return false;
}
// Create socket with loop monitoring
int family = ((struct sockaddr *) &addr)->sa_family;
socket_ = esphome::socket::socket_loop_monitored(family, SOCK_STREAM, IPPROTO_TCP);
if (!socket_) {
ESP_LOGE(TAG, "Failed to create socket");
if (error_cb_)
error_cb_(error_arg_, this, -1);
return false;
}
socket_->setblocking(false);
int err = socket_->connect((struct sockaddr *) &addr, addrlen);
if (err == 0) {
// Connection succeeded immediately (rare, but possible for localhost)
connected_ = true;
if (connect_cb_)
connect_cb_(connect_arg_, this);
return true;
}
if (errno != EINPROGRESS) {
ESP_LOGE(TAG, "Connect failed: %d", errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
return false;
}
connecting_ = true;
return true;
}
void AsyncClient::close() {
socket_.reset();
bool was_connected = connected_;
connected_ = false;
connecting_ = false;
if (was_connected && disconnect_cb_)
disconnect_cb_(disconnect_arg_, this);
}
size_t AsyncClient::write(const char *data, size_t len) {
if (!socket_ || !connected_)
return 0;
ssize_t sent = socket_->write(data, len);
if (sent < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
ESP_LOGE(TAG, "Write error: %d", errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
}
return 0;
}
return sent;
}
void AsyncClient::loop() {
if (!socket_)
return;
if (connecting_) {
// For connecting, we need to check writability, not readability
// The Application's select() only monitors read FDs, so we do our own check here
// For ESP platforms lwip_select() might be faster, but this code isn't used
// on those platforms anyway. If it was, we'd fix the Application select()
// to report writability instead of doing it this way.
int fd = socket_->get_fd();
if (fd < 0) {
ESP_LOGW(TAG, "Invalid socket fd");
close();
return;
}
fd_set writefds;
FD_ZERO(&writefds);
FD_SET(fd, &writefds);
struct timeval tv = {0, 0};
int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv);
if (ret > 0 && FD_ISSET(fd, &writefds)) {
int error = 0;
socklen_t len = sizeof(error);
if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) {
connecting_ = false;
connected_ = true;
if (connect_cb_)
connect_cb_(connect_arg_, this);
} else {
ESP_LOGW(TAG, "Connection failed: %d", error);
close();
if (error_cb_)
error_cb_(error_arg_, this, error);
}
} else if (ret < 0) {
ESP_LOGE(TAG, "Select error: %d", errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
}
} else if (connected_) {
// For connected sockets, use the Application's select() results
if (!socket_->ready())
return;
uint8_t buf[READ_BUFFER_SIZE];
ssize_t len = socket_->read(buf, READ_BUFFER_SIZE);
if (len == 0) {
ESP_LOGI(TAG, "Connection closed by peer");
close();
} else if (len > 0) {
if (data_cb_)
data_cb_(data_arg_, this, buf, len);
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
ESP_LOGW(TAG, "Read error: %d", errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
}
}
}
} // namespace esphome::async_tcp
#endif

View File

@@ -1,73 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#if !defined(USE_ESP32) && !defined(USE_ESP8266) && !defined(USE_RP2040) && !defined(USE_LIBRETINY) && \
(defined(USE_SOCKET_IMPL_LWIP_SOCKETS) || defined(USE_SOCKET_IMPL_BSD_SOCKETS))
#include "esphome/components/socket/socket.h"
#include <functional>
#include <memory>
#include <string>
#include <utility>
namespace esphome::async_tcp {
/// AsyncClient API for platforms using sockets (ESP-IDF, host, etc.)
/// NOTE: This class is NOT thread-safe. All methods must be called from the main loop.
class AsyncClient {
public:
using AcConnectHandler = std::function<void(void *, AsyncClient *)>;
using AcDataHandler = std::function<void(void *, AsyncClient *, void *data, size_t len)>;
using AcErrorHandler = std::function<void(void *, AsyncClient *, int8_t error)>;
AsyncClient() = default;
~AsyncClient() = default;
[[nodiscard]] bool connect(const char *host, uint16_t port);
void close();
[[nodiscard]] bool connected() const { return connected_; }
size_t write(const char *data, size_t len);
void onConnect(AcConnectHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
connect_cb_ = std::move(cb);
connect_arg_ = arg;
}
void onDisconnect(AcConnectHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
disconnect_cb_ = std::move(cb);
disconnect_arg_ = arg;
}
/// Set data callback. NOTE: data pointer is only valid during callback execution.
void onData(AcDataHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
data_cb_ = std::move(cb);
data_arg_ = arg;
}
void onError(AcErrorHandler cb, void *arg = nullptr) { // NOLINT(readability-identifier-naming)
error_cb_ = std::move(cb);
error_arg_ = arg;
}
// Must be called from loop()
void loop();
private:
std::unique_ptr<esphome::socket::Socket> socket_;
AcConnectHandler connect_cb_{nullptr};
void *connect_arg_{nullptr};
AcConnectHandler disconnect_cb_{nullptr};
void *disconnect_arg_{nullptr};
AcDataHandler data_cb_{nullptr};
void *data_arg_{nullptr};
AcErrorHandler error_cb_{nullptr};
void *error_arg_{nullptr};
bool connected_{false};
bool connecting_{false};
};
} // namespace esphome::async_tcp
// Expose AsyncClient in global namespace to match library behavior
using esphome::async_tcp::AsyncClient; // NOLINT(google-global-names-in-headers)
#endif

View File

@@ -21,9 +21,7 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
return false;
}
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
const char *addr_str = device.address_str_to(addr_buf);
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", addr_str);
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
bool success = false;
for (auto &service_data : device.get_service_datas()) {
@@ -34,7 +32,7 @@ bool ATCMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &device
if (!(parse_message_(service_data.data, *res))) {
continue;
}
if (!(report_results_(res, addr_str))) {
if (!(report_results_(res, device.address_str()))) {
continue;
}
if (res->temperature.has_value() && this->temperature_ != nullptr)
@@ -105,13 +103,13 @@ bool ATCMiThermometer::parse_message_(const std::vector<uint8_t> &message, Parse
return true;
}
bool ATCMiThermometer::report_results_(const optional<ParseResult> &result, const char *address) {
bool ATCMiThermometer::report_results_(const optional<ParseResult> &result, const std::string &address) {
if (!result.has_value()) {
ESP_LOGVV(TAG, "report_results(): no results available.");
return false;
}
ESP_LOGD(TAG, "Got ATC MiThermometer (%s):", address);
ESP_LOGD(TAG, "Got ATC MiThermometer (%s):", address.c_str());
if (result->temperature.has_value()) {
ESP_LOGD(TAG, " Temperature: %.1f °C", *result->temperature);

View File

@@ -41,7 +41,7 @@ class ATCMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevice
optional<ParseResult> parse_header_(const esp32_ble_tracker::ServiceData &service_data);
bool parse_message_(const std::vector<uint8_t> &message, ParseResult &result);
bool report_results_(const optional<ParseResult> &result, const char *address);
bool report_results_(const optional<ParseResult> &result, const std::string &address);
};
} // namespace atc_mithermometer

View File

@@ -1,6 +1,6 @@
#include "audio_reader.h"
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"

View File

@@ -1,6 +1,6 @@
#pragma once
#ifdef USE_ESP32
#ifdef USE_ESP_IDF
#include "audio.h"
#include "audio_transfer_buffer.h"

View File

@@ -22,8 +22,7 @@ bool BParasite::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
ESP_LOGVV(TAG, "parse_device(): unknown MAC address.");
return false;
}
char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str_to(addr_buf));
ESP_LOGVV(TAG, "parse_device(): MAC address %s found.", device.address_str().c_str());
const auto &service_datas = device.get_service_datas();
if (service_datas.size() != 1) {
ESP_LOGE(TAG, "Unexpected service_datas size (%d)", service_datas.size());

View File

@@ -193,9 +193,8 @@ bool BedJetHub::discover_characteristics_() {
result = false;
} else if (descr->uuid.get_uuid().len != ESP_UUID_LEN_16 ||
descr->uuid.get_uuid().uuid.uuid16 != ESP_GATT_UUID_CHAR_CLIENT_CONFIG) {
char uuid_buf[espbt::UUID_STR_LEN];
ESP_LOGW(TAG, "Config descriptor 0x%x (uuid %s) is not a client config char uuid", this->char_handle_status_,
descr->uuid.to_str(uuid_buf));
descr->uuid.to_string().c_str());
result = false;
} else {
this->config_descr_status_ = descr->handle;
@@ -217,14 +216,11 @@ bool BedJetHub::discover_characteristics_() {
}
}
ESP_LOGI(TAG,
"[%s] Discovered service characteristics:\n"
" - Command char: 0x%x\n"
" - Status char: 0x%x\n"
" - config descriptor: 0x%x\n"
" - Name char: 0x%x",
this->get_name().c_str(), this->char_handle_cmd_, this->char_handle_status_, this->config_descr_status_,
this->char_handle_name_);
ESP_LOGI(TAG, "[%s] Discovered service characteristics: ", this->get_name().c_str());
ESP_LOGI(TAG, " - Command char: 0x%x", this->char_handle_cmd_);
ESP_LOGI(TAG, " - Status char: 0x%x", this->char_handle_status_);
ESP_LOGI(TAG, " - config descriptor: 0x%x", this->config_descr_status_);
ESP_LOGI(TAG, " - Name char: 0x%x", this->char_handle_name_);
return result;
}

Some files were not shown because too many files have changed in this diff Show More