mirror of
https://github.com/esphome/esphome.git
synced 2026-07-04 06:03:35 +00:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f8bec0813d | |||
| 84762e6ae0 | |||
| 2edf313ee3 | |||
| ae9c999052 | |||
| 7d2f6fbf55 | |||
| 608bef86cc | |||
| 6514dc2fe1 | |||
| 240afd23b3 | |||
| 156c2a8cb0 | |||
| 908c47bb5e | |||
| 6df3a30740 | |||
| 0aaf59dbed | |||
| 249c5bb724 | |||
| 54ea8dd207 | |||
| 4cfb794b62 | |||
| 917af8ff31 |
+1
-1
@@ -1 +1 @@
|
||||
9f5d763f95ff720024f3fdddba2fad3801e2bfe00b7cc2124e6d68c17d3504c6
|
||||
8e48e836c6fc196d3da000d46eb09db243b87fe33518a74e49c8e009d756074a
|
||||
|
||||
@@ -22,7 +22,7 @@ runs:
|
||||
python-version: ${{ inputs.python-version }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
|
||||
with:
|
||||
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
|
||||
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
|
||||
|
||||
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
- if: failure()
|
||||
name: Request changes
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
|
||||
})
|
||||
|
||||
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
|
||||
- if: success()
|
||||
name: Dismiss review
|
||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
||||
with:
|
||||
|
||||
+17
-69
@@ -47,7 +47,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -106,7 +106,6 @@ jobs:
|
||||
script/build_codeowners.py --check
|
||||
script/build_language_schema.py --check
|
||||
script/generate-esp32-boards.py --check
|
||||
script/generate-rp2040-boards.py --check
|
||||
|
||||
pytest:
|
||||
name: Run pytest
|
||||
@@ -154,12 +153,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@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3
|
||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
- name: Save Python virtual environment cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -171,8 +170,6 @@ jobs:
|
||||
- common
|
||||
outputs:
|
||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||
integration-tests-run-all: ${{ steps.determine.outputs.integration-tests-run-all }}
|
||||
integration-test-files: ${{ steps.determine.outputs.integration-test-files }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
python-linters: ${{ steps.determine.outputs.python-linters }}
|
||||
@@ -185,7 +182,6 @@ jobs:
|
||||
cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
|
||||
cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
|
||||
component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
|
||||
benchmarks: ${{ steps.determine.outputs.benchmarks }}
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
@@ -198,7 +194,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Restore components graph cache
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -214,8 +210,6 @@ jobs:
|
||||
|
||||
# Extract individual fields
|
||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||
echo "integration-tests-run-all=$(echo "$output" | jq -r '.integration_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "integration-test-files=$(echo "$output" | jq -c '.integration_test_files')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
|
||||
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
|
||||
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
|
||||
@@ -228,10 +222,9 @@ jobs:
|
||||
echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
|
||||
echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
|
||||
echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
|
||||
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
|
||||
- name: Save components graph cache
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: .temp/components_graph.json
|
||||
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
|
||||
@@ -253,7 +246,7 @@ jobs:
|
||||
python-version: "3.13"
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: venv
|
||||
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
|
||||
@@ -268,20 +261,9 @@ jobs:
|
||||
- name: Register matcher
|
||||
run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
|
||||
- name: Run integration tests
|
||||
env:
|
||||
INTEGRATION_TEST_FILES: ${{ needs.determine-jobs.outputs.integration-test-files }}
|
||||
INTEGRATION_TESTS_RUN_ALL: ${{ needs.determine-jobs.outputs.integration-tests-run-all }}
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
if [[ "$INTEGRATION_TESTS_RUN_ALL" == "true" ]]; then
|
||||
echo "Running all integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
else
|
||||
# Parse JSON array into bash array to avoid shell expansion issues
|
||||
mapfile -t test_files < <(echo "$INTEGRATION_TEST_FILES" | jq -r '.[]')
|
||||
echo "Running ${#test_files[@]} specific integration tests"
|
||||
pytest -vv --no-cov --tb=native -n auto "${test_files[@]}"
|
||||
fi
|
||||
pytest -vv --no-cov --tb=native -n auto tests/integration/
|
||||
|
||||
cpp-unit-tests:
|
||||
name: Run C++ unit tests
|
||||
@@ -310,40 +292,6 @@ jobs:
|
||||
script/cpp_unit_test.py $ARGS
|
||||
fi
|
||||
|
||||
benchmarks:
|
||||
name: Run CodSpeed benchmarks
|
||||
runs-on: ubuntu-24.04
|
||||
needs:
|
||||
- common
|
||||
- determine-jobs
|
||||
if: >-
|
||||
(github.event_name == 'push' && github.ref_name == 'dev') ||
|
||||
(github.event_name == 'pull_request' && needs.determine-jobs.outputs.benchmarks == 'true')
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
|
||||
- name: Build benchmarks
|
||||
id: build
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
export BENCHMARK_LIB_CONFIG=$(python script/setup_codspeed_lib.py)
|
||||
# --build-only prints BUILD_BINARY=<path> to stdout
|
||||
BINARY=$(script/cpp_benchmark.py --all --build-only | grep '^BUILD_BINARY=' | tail -1 | cut -d= -f2-)
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@1c8ae4843586d3ba879736b7f6b7b0c990757fab # v4
|
||||
with:
|
||||
run: ${{ steps.build.outputs.binary }}
|
||||
mode: simulation
|
||||
|
||||
clang-tidy-single:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -387,14 +335,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
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@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -466,14 +414,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -555,14 +503,14 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref == 'refs/heads/dev'
|
||||
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
- name: Cache platformio
|
||||
if: github.ref != 'refs/heads/dev'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
|
||||
@@ -817,7 +765,7 @@ jobs:
|
||||
- name: Restore cached memory analysis
|
||||
id: cache-memory-analysis
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -841,7 +789,7 @@ jobs:
|
||||
|
||||
- name: Cache platformio
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
@@ -882,7 +830,7 @@ jobs:
|
||||
|
||||
- name: Save memory analysis to cache
|
||||
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
|
||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: memory-analysis-target.json
|
||||
key: ${{ steps.cache-key.outputs.cache-key }}
|
||||
@@ -929,7 +877,7 @@ jobs:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache platformio
|
||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
||||
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
|
||||
with:
|
||||
path: ~/.platformio
|
||||
key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
|
||||
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -86,6 +86,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
||||
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -221,7 +221,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
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 }}
|
||||
@@ -256,7 +256,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
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 }}
|
||||
@@ -287,7 +287,7 @@ jobs:
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
||||
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 }}
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||
with:
|
||||
python-version: "3.14"
|
||||
python-version: 3.13
|
||||
|
||||
- name: Install Home Assistant
|
||||
run: |
|
||||
|
||||
+1
-2
@@ -244,6 +244,7 @@ esphome/components/hyt271/* @Philippe12
|
||||
esphome/components/i2c/* @esphome/core
|
||||
esphome/components/i2c_device/* @gabest11
|
||||
esphome/components/i2s_audio/* @jesserockz
|
||||
esphome/components/i2s_audio/media_player/* @jesserockz
|
||||
esphome/components/i2s_audio/microphone/* @jesserockz
|
||||
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/iaqcore/* @yozik04
|
||||
@@ -457,8 +458,6 @@ esphome/components/sn74hc165/* @jesserockz
|
||||
esphome/components/socket/* @esphome/core
|
||||
esphome/components/sonoff_d1/* @anatoly-savchenkov
|
||||
esphome/components/sound_level/* @kahrendt
|
||||
esphome/components/spa06_base/* @danielkent-net
|
||||
esphome/components/spa06_i2c/* @danielkent-net
|
||||
esphome/components/speaker/* @jesserockz @kahrendt
|
||||
esphome/components/speaker/media_player/* @kahrendt @synesthesiam
|
||||
esphome/components/speaker_source/* @kahrendt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/) [](https://codspeed.io/esphome/esphome)
|
||||
# ESPHome [](https://discord.gg/KhAMKrd) [](https://GitHub.com/esphome/esphome/releases/)
|
||||
|
||||
<a href="https://esphome.io/">
|
||||
<picture>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Memory usage analyzer for ESPHome compiled binaries."""
|
||||
|
||||
from collections import Counter, defaultdict
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from pathlib import Path
|
||||
@@ -40,15 +40,6 @@ _READELF_SECTION_PATTERN = re.compile(
|
||||
r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)"
|
||||
)
|
||||
|
||||
# Regex for extracting call targets from objdump disassembly
|
||||
# Matches direct call instructions across architectures:
|
||||
# Xtensa: call0/call4/call8/call12/callx0/callx4/callx8/callx12 <addr> <symbol>
|
||||
# ARM: bl/blx <addr> <symbol>
|
||||
# Captures the mangled symbol name inside angle brackets.
|
||||
_CALL_TARGET_PATTERN = re.compile(
|
||||
r"\t(?:call(?:0|4|8|12)|callx(?:0|4|8|12)|blx?)\s+[\da-fA-F]+ <([^>]+)>"
|
||||
)
|
||||
|
||||
# Component category prefixes
|
||||
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
|
||||
_COMPONENT_PREFIX_EXTERNAL = "[external]"
|
||||
@@ -201,27 +192,20 @@ class MemoryAnalyzer:
|
||||
self._cswtch_symbols: list[tuple[str, int, str, str]] = []
|
||||
# Library symbol mapping: symbol_name -> library_name
|
||||
self._lib_symbol_map: dict[str, str] = {}
|
||||
# Source file symbol mapping: symbol_name -> component_name
|
||||
# Used for extern "C" and other symbols without C++ namespace
|
||||
self._source_symbol_map: dict[str, str] = {}
|
||||
# Library dir to name mapping: "lib641" -> "espsoftwareserial",
|
||||
# "espressif__mdns" -> "mdns"
|
||||
self._lib_hash_to_name: dict[str, str] = {}
|
||||
# Heuristic category to library redirect: "mdns_lib" -> "[lib]mdns"
|
||||
self._heuristic_to_lib: dict[str, str] = {}
|
||||
# Function call counts: mangled_name -> call_count
|
||||
self._function_call_counts: Counter[str] = Counter()
|
||||
|
||||
def analyze(self) -> dict[str, ComponentMemory]:
|
||||
"""Analyze the ELF file and return component memory usage."""
|
||||
self._parse_sections()
|
||||
self._parse_symbols()
|
||||
self._scan_libraries()
|
||||
self._scan_source_symbols()
|
||||
self._categorize_symbols()
|
||||
self._analyze_cswtch_symbols()
|
||||
self._analyze_sdk_libraries()
|
||||
self._analyze_function_calls()
|
||||
return dict(self.components)
|
||||
|
||||
def _parse_sections(self) -> None:
|
||||
@@ -367,11 +351,6 @@ class MemoryAnalyzer:
|
||||
if lib_name := self._lib_symbol_map.get(symbol_name):
|
||||
return f"{_COMPONENT_PREFIX_LIB}{lib_name}"
|
||||
|
||||
# Check source file mapping (catches extern "C" functions in ESPHome sources)
|
||||
# Must be before heuristic patterns since source attribution is authoritative
|
||||
if component := self._source_symbol_map.get(symbol_name):
|
||||
return component
|
||||
|
||||
# Check against symbol patterns
|
||||
for component, patterns in SYMBOL_PATTERNS.items():
|
||||
if any(pattern in symbol_name for pattern in patterns):
|
||||
@@ -405,9 +384,8 @@ class MemoryAnalyzer:
|
||||
return
|
||||
|
||||
_LOGGER.info("Demangling %d symbols", len(symbols))
|
||||
demangled = batch_demangle(symbols, objdump_path=self.objdump_path)
|
||||
self._demangle_cache.update(demangled)
|
||||
_LOGGER.info("Successfully demangled %d symbols", len(demangled))
|
||||
self._demangle_cache = batch_demangle(symbols, objdump_path=self.objdump_path)
|
||||
_LOGGER.info("Successfully demangled %d symbols", len(self._demangle_cache))
|
||||
|
||||
def _demangle_symbol(self, symbol: str) -> str:
|
||||
"""Get demangled C++ symbol name from cache."""
|
||||
@@ -662,7 +640,6 @@ class MemoryAnalyzer:
|
||||
return None
|
||||
|
||||
symbol_map: dict[str, str] = {}
|
||||
source_symbol_map: dict[str, str] = {}
|
||||
current_symbol: str | None = None
|
||||
section_prefixes = (".text.", ".rodata.", ".data.", ".bss.", ".literal.")
|
||||
|
||||
@@ -698,18 +675,9 @@ class MemoryAnalyzer:
|
||||
if dir_key in source_path:
|
||||
symbol_map[current_symbol] = lib_name
|
||||
break
|
||||
else:
|
||||
# Map ESPHome source files to components for extern "C"
|
||||
# and other symbols without C++ namespace
|
||||
component = self._source_file_to_component(source_path)
|
||||
if component.startswith(
|
||||
(_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL)
|
||||
):
|
||||
source_symbol_map[current_symbol] = component
|
||||
|
||||
current_symbol = None
|
||||
|
||||
self._source_symbol_map = source_symbol_map
|
||||
return symbol_map or None
|
||||
|
||||
def _scan_libraries(self) -> None:
|
||||
@@ -760,112 +728,6 @@ class MemoryAnalyzer:
|
||||
len(libraries),
|
||||
)
|
||||
|
||||
def _scan_source_symbols(self) -> None:
|
||||
"""Scan ESPHome source object files to map extern "C" symbols to components.
|
||||
|
||||
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
|
||||
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
|
||||
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
|
||||
|
||||
Skips scanning if ``_source_symbol_map`` was already populated by
|
||||
``_parse_map_file()``.
|
||||
"""
|
||||
if self._source_symbol_map or not self.nm_path:
|
||||
return
|
||||
|
||||
obj_dir = self._find_object_files_dir()
|
||||
if obj_dir is None:
|
||||
return
|
||||
|
||||
# Find ESPHome source object files
|
||||
esphome_src_dir = obj_dir / "src" / "esphome"
|
||||
if not esphome_src_dir.is_dir():
|
||||
return
|
||||
|
||||
obj_files = sorted(esphome_src_dir.rglob("*.o"))
|
||||
if not obj_files:
|
||||
return
|
||||
|
||||
# Run nm with --print-file-name to get file:symbol mapping
|
||||
result = run_tool(
|
||||
[self.nm_path, "--print-file-name", "-g", "--defined-only"]
|
||||
+ [str(f) for f in obj_files],
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
_LOGGER.debug("nm scan of source objects failed")
|
||||
return
|
||||
|
||||
self._source_symbol_map = self._parse_nm_source_output(result.stdout, obj_dir)
|
||||
if self._source_symbol_map:
|
||||
_LOGGER.info(
|
||||
"Built source symbol map from nm: %d symbols",
|
||||
len(self._source_symbol_map),
|
||||
)
|
||||
|
||||
def _parse_nm_source_output(self, output: str, base_dir: Path) -> dict[str, str]:
|
||||
"""Parse nm output to map non-namespaced symbols to ESPHome components.
|
||||
|
||||
Extracts global defined symbols from ESPHome source object files that
|
||||
don't use C++ namespacing (e.g. ``extern "C"`` functions).
|
||||
|
||||
Args:
|
||||
output: Raw stdout from ``nm --print-file-name -g --defined-only``
|
||||
or ``nm --print-file-name -S``.
|
||||
base_dir: Build directory for computing relative paths.
|
||||
|
||||
Returns:
|
||||
Dict mapping symbol names to component names.
|
||||
"""
|
||||
source_map: dict[str, str] = {}
|
||||
for line in output.splitlines():
|
||||
# Format: /path/to/file.o: addr type name
|
||||
# or: /path/to/file.o: addr size type name (with -S)
|
||||
colon_idx = line.rfind(".o:")
|
||||
if colon_idx == -1:
|
||||
continue
|
||||
|
||||
file_path = line[: colon_idx + 2]
|
||||
fields = line[colon_idx + 3 :].split()
|
||||
if len(fields) < 3:
|
||||
continue
|
||||
|
||||
# With -S flag, format is: addr size type name
|
||||
# Without -S flag: addr type name
|
||||
# type is a single char; size is hex digits
|
||||
# Detect by checking if fields[1] is a single uppercase letter (type)
|
||||
if len(fields[1]) == 1 and fields[1].isalpha():
|
||||
# addr type name
|
||||
sym_type = fields[1]
|
||||
symbol_name = fields[2]
|
||||
elif len(fields) >= 4:
|
||||
# addr size type name
|
||||
sym_type = fields[2]
|
||||
symbol_name = fields[3]
|
||||
else:
|
||||
continue
|
||||
|
||||
# Only global defined symbols (uppercase type)
|
||||
if not sym_type.isupper() or sym_type == "U":
|
||||
continue
|
||||
|
||||
# Skip symbols already in esphome:: namespace
|
||||
if symbol_name.startswith("_ZN7esphome"):
|
||||
continue
|
||||
|
||||
# Make path relative to base_dir for _source_file_to_component
|
||||
try:
|
||||
rel_path = str(Path(file_path).relative_to(base_dir))
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
component = self._source_file_to_component(rel_path)
|
||||
if component.startswith(
|
||||
(_COMPONENT_PREFIX_ESPHOME, _COMPONENT_PREFIX_EXTERNAL)
|
||||
):
|
||||
source_map[symbol_name] = component
|
||||
|
||||
return source_map
|
||||
|
||||
def _find_object_files_dir(self) -> Path | None:
|
||||
"""Find the directory containing object files for this build.
|
||||
|
||||
@@ -1149,43 +1011,6 @@ class MemoryAnalyzer:
|
||||
total_size,
|
||||
)
|
||||
|
||||
def _analyze_function_calls(self) -> None:
|
||||
"""Count function call sites by parsing disassembly output.
|
||||
|
||||
Parses direct call instructions (call0/call8/bl/blx) from objdump -d
|
||||
to count how many times each function is called. This helps identify
|
||||
inlining candidates — frequently called small functions benefit most
|
||||
from inlining.
|
||||
"""
|
||||
result = run_tool(
|
||||
[self.objdump_path, "-d", str(self.elf_path)],
|
||||
timeout=60,
|
||||
)
|
||||
if result is None or result.returncode != 0:
|
||||
_LOGGER.debug("Failed to disassemble ELF for function call analysis")
|
||||
return
|
||||
|
||||
self._function_call_counts = Counter(
|
||||
match.group(1)
|
||||
for line in result.stdout.splitlines()
|
||||
if (match := _CALL_TARGET_PATTERN.search(line))
|
||||
)
|
||||
|
||||
# Demangle any call targets not already in the cache
|
||||
missing = [
|
||||
name
|
||||
for name in self._function_call_counts
|
||||
if name not in self._demangle_cache
|
||||
]
|
||||
if missing:
|
||||
self._batch_demangle_symbols(missing)
|
||||
|
||||
_LOGGER.debug(
|
||||
"Function call analysis: %d unique targets, %d total calls",
|
||||
len(self._function_call_counts),
|
||||
sum(self._function_call_counts.values()),
|
||||
)
|
||||
|
||||
def get_unattributed_ram(self) -> tuple[int, int, int]:
|
||||
"""Get unattributed RAM sizes (SDK/framework overhead).
|
||||
|
||||
|
||||
@@ -231,110 +231,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
lines.append(f" {size:>6,} B {sym_name}")
|
||||
lines.append("")
|
||||
|
||||
# Number of top called functions to show
|
||||
TOP_CALLS_LIMIT: int = 50
|
||||
# Number of inlining candidates to show
|
||||
INLINE_CANDIDATES_LIMIT: int = 25
|
||||
# Maximum function size in bytes to consider for inlining
|
||||
INLINE_SIZE_THRESHOLD: int = 16
|
||||
|
||||
def _build_symbol_sizes(self) -> dict[str, int]:
|
||||
"""Build a size lookup from all component symbols: mangled_name -> size."""
|
||||
return {
|
||||
symbol: size
|
||||
for symbols in self._component_symbols.values()
|
||||
for symbol, _, size, _ in symbols
|
||||
}
|
||||
|
||||
def _format_call_row(
|
||||
self, index: int, mangled: str, count: int, symbol_sizes: dict[str, int]
|
||||
) -> str:
|
||||
"""Format a single row for call frequency tables."""
|
||||
demangled = self._demangle_cache.get(mangled, mangled)
|
||||
if len(demangled) > 80:
|
||||
demangled = f"{demangled[:77]}..."
|
||||
size = symbol_sizes.get(mangled)
|
||||
size_str = f"{size:>5,} B" if size is not None else " ?"
|
||||
return f"{index:>3} {count:>5} {size_str} {demangled}"
|
||||
|
||||
def _add_call_table_header(self, lines: list[str]) -> None:
|
||||
"""Add the header row for call frequency tables."""
|
||||
lines.append(f"{'#':>3} {'Calls':>5} {'Size':>7} Function")
|
||||
lines.append(f"{'---':>3} {'-----':>5} {'-------':>7} {'-' * 60}")
|
||||
|
||||
def _add_function_call_analysis(self, lines: list[str]) -> None:
|
||||
"""Add function call frequency analysis section.
|
||||
|
||||
Shows the most frequently called functions by call site count.
|
||||
"""
|
||||
self._add_section_header(lines, "Top Called Functions")
|
||||
|
||||
symbol_sizes = self._build_symbol_sizes()
|
||||
|
||||
# Sort by call count descending
|
||||
sorted_calls = sorted(
|
||||
self._function_call_counts.items(), key=lambda x: x[1], reverse=True
|
||||
)
|
||||
|
||||
self._add_call_table_header(lines)
|
||||
|
||||
for i, (mangled, count) in enumerate(sorted_calls[: self.TOP_CALLS_LIMIT]):
|
||||
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
|
||||
|
||||
total_calls = sum(self._function_call_counts.values())
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"Total: {len(self._function_call_counts)} unique targets, "
|
||||
f"{total_calls:,} call sites"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
def _add_inline_candidates(self, lines: list[str]) -> None:
|
||||
"""Add inlining candidates section.
|
||||
|
||||
Shows frequently called functions that are small enough to benefit
|
||||
from inlining (< 16 bytes). These are the best candidates for
|
||||
reducing call overhead.
|
||||
"""
|
||||
self._add_section_header(
|
||||
lines,
|
||||
f"Inlining Candidates (<{self.INLINE_SIZE_THRESHOLD} B, by call count)",
|
||||
)
|
||||
|
||||
symbol_sizes = self._build_symbol_sizes()
|
||||
|
||||
# Filter to small functions with known size, sort by call count
|
||||
candidates = sorted(
|
||||
(
|
||||
(mangled, count)
|
||||
for mangled, count in self._function_call_counts.items()
|
||||
if mangled in symbol_sizes
|
||||
and symbol_sizes[mangled] < self.INLINE_SIZE_THRESHOLD
|
||||
),
|
||||
key=lambda x: x[1],
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
if not candidates:
|
||||
lines.append("No candidates found.")
|
||||
lines.append("")
|
||||
return
|
||||
|
||||
self._add_call_table_header(lines)
|
||||
|
||||
for i, (mangled, count) in enumerate(
|
||||
candidates[: self.INLINE_CANDIDATES_LIMIT]
|
||||
):
|
||||
lines.append(self._format_call_row(i + 1, mangled, count, symbol_sizes))
|
||||
|
||||
lines.append("")
|
||||
lines.append(
|
||||
f"Showing top {min(len(candidates), self.INLINE_CANDIDATES_LIMIT)} "
|
||||
f"of {len(candidates)} functions under "
|
||||
f"{self.INLINE_SIZE_THRESHOLD} B"
|
||||
)
|
||||
lines.append("")
|
||||
|
||||
def generate_report(self, detailed: bool = False) -> str:
|
||||
"""Generate a formatted memory report."""
|
||||
components = sorted(
|
||||
@@ -637,11 +533,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
if self._cswtch_symbols:
|
||||
self._add_cswtch_analysis(lines)
|
||||
|
||||
# Function call frequency analysis
|
||||
if self._function_call_counts:
|
||||
self._add_function_call_analysis(lines)
|
||||
self._add_inline_candidates(lines)
|
||||
|
||||
lines.append(
|
||||
"Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
|
||||
)
|
||||
|
||||
@@ -408,6 +408,7 @@ SYMBOL_PATTERNS = {
|
||||
],
|
||||
"arduino_core": [
|
||||
"pinMode",
|
||||
"resetPins",
|
||||
"millis",
|
||||
"micros",
|
||||
"delay(", # More specific - Arduino delay function with parenthesis
|
||||
|
||||
@@ -1,29 +1,22 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "absolute_humidity.h"
|
||||
|
||||
namespace esphome::absolute_humidity {
|
||||
namespace esphome {
|
||||
namespace absolute_humidity {
|
||||
|
||||
static const char *const TAG{"absolute_humidity.sensor"};
|
||||
static const char *const TAG = "absolute_humidity.sensor";
|
||||
|
||||
void AbsoluteHumidityComponent::setup() {
|
||||
this->temperature_sensor_->add_on_state_callback([this](float state) {
|
||||
this->temperature_ = state;
|
||||
this->enable_loop();
|
||||
});
|
||||
ESP_LOGD(TAG, " Added callback for temperature '%s'", this->temperature_sensor_->get_name().c_str());
|
||||
// Get initial value
|
||||
this->temperature_sensor_->add_on_state_callback([this](float state) { this->temperature_callback_(state); });
|
||||
if (this->temperature_sensor_->has_state()) {
|
||||
this->temperature_ = this->temperature_sensor_->get_state();
|
||||
this->temperature_callback_(this->temperature_sensor_->get_state());
|
||||
}
|
||||
|
||||
this->humidity_sensor_->add_on_state_callback([this](float state) {
|
||||
this->humidity_ = state;
|
||||
this->enable_loop();
|
||||
});
|
||||
ESP_LOGD(TAG, " Added callback for relative humidity '%s'", this->humidity_sensor_->get_name().c_str());
|
||||
// Get initial value
|
||||
this->humidity_sensor_->add_on_state_callback([this](float state) { this->humidity_callback_(state); });
|
||||
if (this->humidity_sensor_->has_state()) {
|
||||
this->humidity_ = this->humidity_sensor_->get_state();
|
||||
this->humidity_callback_(this->humidity_sensor_->get_state());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,12 +46,14 @@ void AbsoluteHumidityComponent::dump_config() {
|
||||
}
|
||||
|
||||
void AbsoluteHumidityComponent::loop() {
|
||||
// Only run once
|
||||
this->disable_loop();
|
||||
if (!this->next_update_) {
|
||||
return;
|
||||
}
|
||||
this->next_update_ = false;
|
||||
|
||||
// Ensure we have source data
|
||||
const bool no_temperature{std::isnan(this->temperature_)};
|
||||
const bool no_humidity{std::isnan(this->humidity_)};
|
||||
const bool no_temperature = std::isnan(this->temperature_);
|
||||
const bool no_humidity = std::isnan(this->humidity_);
|
||||
if (no_temperature || no_humidity) {
|
||||
if (no_temperature) {
|
||||
ESP_LOGW(TAG, "No valid state from temperature sensor!");
|
||||
@@ -72,9 +67,9 @@ void AbsoluteHumidityComponent::loop() {
|
||||
}
|
||||
|
||||
// Convert to desired units
|
||||
const float temperature_c{this->temperature_};
|
||||
const float temperature_k{temperature_c + 273.15f};
|
||||
const float hr{this->humidity_ / 100.0f};
|
||||
const float temperature_c = this->temperature_;
|
||||
const float temperature_k = temperature_c + 273.15;
|
||||
const float hr = this->humidity_ / 100;
|
||||
|
||||
// Calculate saturation vapor pressure
|
||||
float es;
|
||||
@@ -95,7 +90,7 @@ void AbsoluteHumidityComponent::loop() {
|
||||
}
|
||||
|
||||
// Calculate absolute humidity
|
||||
const float absolute_humidity{vapor_density(es, hr, temperature_k)};
|
||||
const float absolute_humidity = vapor_density(es, hr, temperature_k);
|
||||
|
||||
ESP_LOGD(TAG, "Saturation vapor pressure %f kPa, absolute humidity %f g/m³", es, absolute_humidity);
|
||||
|
||||
@@ -108,16 +103,16 @@ void AbsoluteHumidityComponent::loop() {
|
||||
// More accurate than Tetens in normal meteorologic conditions
|
||||
float AbsoluteHumidityComponent::es_buck(float temperature_c) {
|
||||
float a, b, c, d;
|
||||
if (temperature_c >= 0.0f) {
|
||||
a = 0.61121f;
|
||||
b = 18.678f;
|
||||
c = 234.5f;
|
||||
d = 257.14f;
|
||||
if (temperature_c >= 0) {
|
||||
a = 0.61121;
|
||||
b = 18.678;
|
||||
c = 234.5;
|
||||
d = 257.14;
|
||||
} else {
|
||||
a = 0.61115f;
|
||||
b = 18.678f;
|
||||
c = 233.7f;
|
||||
d = 279.82f;
|
||||
a = 0.61115;
|
||||
b = 18.678;
|
||||
c = 233.7;
|
||||
d = 279.82;
|
||||
}
|
||||
return a * expf((b - (temperature_c / c)) * (temperature_c / (d + temperature_c)));
|
||||
}
|
||||
@@ -125,14 +120,14 @@ float AbsoluteHumidityComponent::es_buck(float temperature_c) {
|
||||
// Tetens equation (https://en.wikipedia.org/wiki/Tetens_equation)
|
||||
float AbsoluteHumidityComponent::es_tetens(float temperature_c) {
|
||||
float a, b;
|
||||
if (temperature_c >= 0.0f) {
|
||||
a = 17.27f;
|
||||
b = 237.3f;
|
||||
if (temperature_c >= 0) {
|
||||
a = 17.27;
|
||||
b = 237.3;
|
||||
} else {
|
||||
a = 21.875f;
|
||||
b = 265.5f;
|
||||
a = 21.875;
|
||||
b = 265.5;
|
||||
}
|
||||
return 0.61078f * expf((a * temperature_c) / (temperature_c + b));
|
||||
return 0.61078 * expf((a * temperature_c) / (temperature_c + b));
|
||||
}
|
||||
|
||||
// Wobus equation
|
||||
@@ -151,18 +146,18 @@ float AbsoluteHumidityComponent::es_wobus(float t) {
|
||||
//
|
||||
// Baker, Schlatter 17-MAY-1982 Original version.
|
||||
|
||||
constexpr float c0{+0.99999683e+00f};
|
||||
constexpr float c1{-0.90826951e-02f};
|
||||
constexpr float c2{+0.78736169e-04f};
|
||||
constexpr float c3{-0.61117958e-06f};
|
||||
constexpr float c4{+0.43884187e-08f};
|
||||
constexpr float c5{-0.29883885e-10f};
|
||||
constexpr float c6{+0.21874425e-12f};
|
||||
constexpr float c7{-0.17892321e-14f};
|
||||
constexpr float c8{+0.11112018e-16f};
|
||||
constexpr float c9{-0.30994571e-19f};
|
||||
const float p{c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))))};
|
||||
return 0.61078f / powf(p, 8.0f);
|
||||
const float c0 = +0.99999683e00;
|
||||
const float c1 = -0.90826951e-02;
|
||||
const float c2 = +0.78736169e-04;
|
||||
const float c3 = -0.61117958e-06;
|
||||
const float c4 = +0.43884187e-08;
|
||||
const float c5 = -0.29883885e-10;
|
||||
const float c6 = +0.21874425e-12;
|
||||
const float c7 = -0.17892321e-14;
|
||||
const float c8 = +0.11112018e-16;
|
||||
const float c9 = -0.30994571e-19;
|
||||
const float p = c0 + t * (c1 + t * (c2 + t * (c3 + t * (c4 + t * (c5 + t * (c6 + t * (c7 + t * (c8 + t * (c9)))))))));
|
||||
return 0.61078 / pow(p, 8);
|
||||
}
|
||||
|
||||
// From https://www.environmentalbiophysics.org/chalk-talk-how-to-calculate-absolute-humidity/
|
||||
@@ -173,10 +168,11 @@ float AbsoluteHumidityComponent::vapor_density(float es, float hr, float ta) {
|
||||
// hr = relative humidity [0-1]
|
||||
// ta = absolute temperature (K)
|
||||
|
||||
const float ea{hr * es * 1000.0f}; // vapor pressure of the air (Pa)
|
||||
const float mw{18.01528f}; // molar mass of water (g⋅mol⁻¹)
|
||||
const float r{8.31446261815324f}; // molar gas constant (J⋅K⁻¹)
|
||||
const float ea = hr * es * 1000; // vapor pressure of the air (Pa)
|
||||
const float mw = 18.01528; // molar mass of water (g⋅mol⁻¹)
|
||||
const float r = 8.31446261815324; // molar gas constant (J⋅K⁻¹)
|
||||
return (ea * mw) / (r * ta);
|
||||
}
|
||||
|
||||
} // namespace esphome::absolute_humidity
|
||||
} // namespace absolute_humidity
|
||||
} // namespace esphome
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
namespace esphome::absolute_humidity {
|
||||
namespace esphome {
|
||||
namespace absolute_humidity {
|
||||
|
||||
/// Enum listing all implemented saturation vapor pressure equations.
|
||||
enum SaturationVaporPressureEquation {
|
||||
@@ -15,6 +16,8 @@ enum SaturationVaporPressureEquation {
|
||||
/// This class implements calculation of absolute humidity from temperature and relative humidity.
|
||||
class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
public:
|
||||
AbsoluteHumidityComponent() = default;
|
||||
|
||||
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
|
||||
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
|
||||
void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; }
|
||||
@@ -24,6 +27,15 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
void loop() override;
|
||||
|
||||
protected:
|
||||
void temperature_callback_(float state) {
|
||||
this->next_update_ = true;
|
||||
this->temperature_ = state;
|
||||
}
|
||||
void humidity_callback_(float state) {
|
||||
this->next_update_ = true;
|
||||
this->humidity_ = state;
|
||||
}
|
||||
|
||||
/** Buck equation for saturation vapor pressure in kPa.
|
||||
*
|
||||
* @param temperature_c Air temperature in °C.
|
||||
@@ -45,15 +57,19 @@ class AbsoluteHumidityComponent : public sensor::Sensor, public Component {
|
||||
* @param es Saturation vapor pressure in kPa.
|
||||
* @param hr Relative humidity 0 to 1.
|
||||
* @param ta Absolute temperature in K.
|
||||
* @param heater_duration The duration in ms that the heater should turn on for when measuring.
|
||||
*/
|
||||
static float vapor_density(float es, float hr, float ta);
|
||||
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *humidity_sensor_{nullptr};
|
||||
|
||||
bool next_update_{false};
|
||||
|
||||
float temperature_{NAN};
|
||||
float humidity_{NAN};
|
||||
SaturationVaporPressureEquation equation_;
|
||||
};
|
||||
|
||||
} // namespace esphome::absolute_humidity
|
||||
} // namespace absolute_humidity
|
||||
} // namespace esphome
|
||||
|
||||
@@ -22,8 +22,7 @@ namespace adc {
|
||||
|
||||
#ifdef USE_ESP32
|
||||
// clang-format off
|
||||
#if ESP_IDF_VERSION_MAJOR >= 6 || \
|
||||
(ESP_IDF_VERSION_MAJOR == 5 && \
|
||||
#if (ESP_IDF_VERSION_MAJOR == 5 && \
|
||||
((ESP_IDF_VERSION_MINOR == 0 && ESP_IDF_VERSION_PATCH >= 5) || \
|
||||
(ESP_IDF_VERSION_MINOR == 1 && ESP_IDF_VERSION_PATCH >= 3) || \
|
||||
(ESP_IDF_VERSION_MINOR >= 2)) \
|
||||
|
||||
@@ -51,6 +51,22 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
|
||||
}
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) {
|
||||
this->cleared_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
|
||||
this->chime_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
|
||||
this->ready_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
|
||||
const char *code) {
|
||||
auto call = this->make_call();
|
||||
|
||||
@@ -37,24 +37,25 @@ class AlarmControlPanel : public EntityBase {
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
void add_on_state_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when the state of the alarm_control_panel clears from triggered. */
|
||||
template<typename F> void add_on_cleared_callback(F &&callback) {
|
||||
this->cleared_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
/** Add a callback for when the state of the alarm_control_panel clears from triggered
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_cleared_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when a chime zone goes from closed to open. */
|
||||
template<typename F> void add_on_chime_callback(F &&callback) {
|
||||
this->chime_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
/** Add a callback for when a chime zone goes from closed to open
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_chime_callback(std::function<void()> &&callback);
|
||||
|
||||
/** Add a callback for when a ready state changes. */
|
||||
template<typename F> void add_on_ready_callback(F &&callback) {
|
||||
this->ready_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
/** Add a callback for when a ready state changes
|
||||
*
|
||||
* @param callback The callback function
|
||||
*/
|
||||
void add_on_ready_callback(std::function<void()> &&callback);
|
||||
|
||||
/** A numeric representation of the supported features as per HomeAssistant
|
||||
*
|
||||
|
||||
@@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
|
||||
uint8_t current_sensor_;
|
||||
// The AM43 often gets into a state where it spams loads of battery update
|
||||
// notifications. Here we will limit to no more than every 10s.
|
||||
uint32_t last_battery_update_;
|
||||
uint8_t last_battery_update_;
|
||||
};
|
||||
|
||||
} // namespace am43
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/automation.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/binary_sensor/binary_sensor.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
|
||||
@@ -251,11 +251,11 @@ void APDS9960::read_gesture_data_() {
|
||||
|
||||
uint8_t buf[128];
|
||||
for (uint8_t pos = 0; pos < fifo_level * 4; pos += 32) {
|
||||
// Read in 32-byte chunks due to ESP8266 I2C buffer limit.
|
||||
// Always read from 0xFC — the FIFO auto-increments through 0xFC-0xFF
|
||||
// and advances its internal pointer after every 4th byte.
|
||||
// The ESP's i2c driver has a limited buffer size.
|
||||
// This way of retrieving the data should be wrong according to the datasheet
|
||||
// but it seems to work.
|
||||
uint8_t read = std::min(32, fifo_level * 4 - pos);
|
||||
APDS9960_WARNING_CHECK(this->read_bytes(0xFC, buf + pos, read), "Reading FIFO buffer failed.");
|
||||
APDS9960_WARNING_CHECK(this->read_bytes(0xFC + pos, buf + pos, read), "Reading FIFO buffer failed.");
|
||||
}
|
||||
|
||||
if (millis() - this->gesture_start_ > 500) {
|
||||
|
||||
@@ -301,12 +301,11 @@ CONFIG_SCHEMA = cv.All(
|
||||
# Maximum queued send buffers per connection before dropping connection
|
||||
# Each buffer uses ~8-12 bytes overhead plus actual message size
|
||||
# Platform defaults based on available RAM and typical message rates:
|
||||
# CONF_MAX_SEND_QUEUE defaults are power of 2 for efficient modulo
|
||||
cv.SplitDefault(
|
||||
CONF_MAX_SEND_QUEUE,
|
||||
esp8266=4, # Limited RAM, need to fail fast
|
||||
esp8266=5, # Limited RAM, need to fail fast
|
||||
esp32=8, # More RAM, can buffer more
|
||||
rp2040=8, # Moderate RAM
|
||||
rp2040=5, # Limited RAM
|
||||
bk72xx=8, # Moderate RAM
|
||||
nrf52=8, # Moderate RAM
|
||||
rtl87xx=8, # Moderate RAM
|
||||
@@ -455,9 +454,6 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
cg.add_define("USE_API_NOISE")
|
||||
cg.add_library("esphome/noise-c", "0.1.11")
|
||||
# Enable optimized memzero/memcmp in libsodium instead of volatile byte loops
|
||||
cg.add_build_flag("-DHAVE_WEAK_SYMBOLS=1")
|
||||
cg.add_build_flag("-DHAVE_INLINE_ASM=1")
|
||||
else:
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
|
||||
|
||||
@@ -69,6 +69,9 @@ service APIConnection {
|
||||
rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {}
|
||||
rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {}
|
||||
|
||||
rpc zigbee_proxy_frame(ZigbeeProxyFrame) returns (void) {}
|
||||
rpc zigbee_proxy_request(ZigbeeProxyRequest) returns (void) {}
|
||||
|
||||
rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {}
|
||||
|
||||
rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {}
|
||||
@@ -281,6 +284,10 @@ message DeviceInfoResponse {
|
||||
|
||||
// Serial proxy instance metadata
|
||||
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
|
||||
|
||||
// Indicates if Zigbee proxy support is available and features supported
|
||||
uint32 zigbee_proxy_feature_flags = 26 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
|
||||
uint64 zigbee_ieee_address = 27 [(field_ifdef) = "USE_ZIGBEE_PROXY"];
|
||||
}
|
||||
|
||||
message ListEntitiesRequest {
|
||||
@@ -2669,3 +2676,29 @@ message BluetoothSetConnectionParamsResponse {
|
||||
uint64 address = 1;
|
||||
int32 error = 2;
|
||||
}
|
||||
|
||||
// ==================== ZIGBEE ====================
|
||||
|
||||
message ZigbeeProxyFrame {
|
||||
option (id) = 148;
|
||||
option (source) = SOURCE_BOTH;
|
||||
option (ifdef) = "USE_ZIGBEE_PROXY";
|
||||
option (no_delay) = true;
|
||||
|
||||
bytes data = 1;
|
||||
}
|
||||
|
||||
enum ZigbeeProxyRequestType {
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0;
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1;
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 2;
|
||||
}
|
||||
|
||||
message ZigbeeProxyRequest {
|
||||
option (id) = 149;
|
||||
option (source) = SOURCE_BOTH;
|
||||
option (ifdef) = "USE_ZIGBEE_PROXY";
|
||||
|
||||
ZigbeeProxyRequestType type = 1;
|
||||
bytes data = 2;
|
||||
}
|
||||
|
||||
@@ -44,12 +44,6 @@ class APIBuffer {
|
||||
this->reserve(n);
|
||||
this->size_ = n; // no zero-fill
|
||||
}
|
||||
/// Reserve capacity for max(reserve_size, new_size) bytes, then set size to new_size.
|
||||
/// Single grow_ check regardless of argument order.
|
||||
inline void reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
|
||||
this->reserve(std::max(reserve_size, new_size));
|
||||
this->size_ = new_size;
|
||||
}
|
||||
uint8_t *data() { return this->data_.get(); }
|
||||
const uint8_t *data() const { return this->data_.get(); }
|
||||
size_t size() const { return this->size_; }
|
||||
|
||||
@@ -43,6 +43,9 @@
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
#include "esphome/components/zwave_proxy/zwave_proxy.h"
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
#include "esphome/components/zigbee_proxy/zigbee_proxy.h"
|
||||
#endif
|
||||
#ifdef USE_WATER_HEATER
|
||||
#include "esphome/components/water_heater/water_heater.h"
|
||||
#endif
|
||||
@@ -64,11 +67,7 @@ static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS *
|
||||
// A stalled handshake from a buggy client or network glitch holds a connection
|
||||
// slot, which can prevent legitimate clients from reconnecting. Also hardens
|
||||
// against the less likely case of intentional connection slot exhaustion.
|
||||
//
|
||||
// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak
|
||||
// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to
|
||||
// 28-30s. See https://github.com/esphome/esphome/issues/14999
|
||||
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000;
|
||||
static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000;
|
||||
|
||||
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
|
||||
|
||||
@@ -1321,6 +1320,16 @@ void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void APIConnection::on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) {
|
||||
zigbee_proxy::global_zigbee_proxy->zigbee_proxy_frame(this, msg);
|
||||
}
|
||||
|
||||
void APIConnection::on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) {
|
||||
zigbee_proxy::global_zigbee_proxy->zigbee_proxy_request(this, msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
|
||||
return this->send_message_smart_(a_alarm_control_panel, AlarmControlPanelStateResponse::MESSAGE_TYPE,
|
||||
@@ -1634,6 +1643,11 @@ void APIConnection::complete_authentication_() {
|
||||
zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
if (zigbee_proxy::global_zigbee_proxy != nullptr) {
|
||||
zigbee_proxy::global_zigbee_proxy->api_connection_authenticated(this);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
@@ -1775,6 +1789,10 @@ bool APIConnection::send_device_info_response_() {
|
||||
info.port_type = proxy->get_port_type();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
resp.zigbee_proxy_feature_flags = zigbee_proxy::global_zigbee_proxy->get_feature_flags();
|
||||
resp.zigbee_ieee_address = zigbee_proxy::global_zigbee_proxy->get_ieee_address();
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
resp.api_encryption_supported = true;
|
||||
#endif
|
||||
@@ -2029,7 +2047,8 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode
|
||||
// Batch message second or later
|
||||
// Add padding for previous message footer + this message header
|
||||
size_t current_size = shared_buf.size();
|
||||
shared_buf.reserve_and_resize(current_size + total_calculated_size, current_size + footer_size + header_padding);
|
||||
shared_buf.reserve(current_size + total_calculated_size);
|
||||
shared_buf.resize(current_size + footer_size + header_padding);
|
||||
}
|
||||
|
||||
// Pre-resize buffer to include payload, then encode through raw pointer
|
||||
|
||||
@@ -180,6 +180,12 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override;
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
void on_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) override;
|
||||
void on_zigbee_proxy_request(const ZigbeeProxyRequest &msg) override;
|
||||
void send_zigbee_proxy_frame(const ZigbeeProxyFrame &msg) { this->send_message(msg); }
|
||||
#endif
|
||||
|
||||
#ifdef USE_ALARM_CONTROL_PANEL
|
||||
bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel);
|
||||
void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override;
|
||||
@@ -305,9 +311,9 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
// Reserve space for header padding + message + footer
|
||||
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
|
||||
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
|
||||
// Reserve full size but only set initial size to header padding
|
||||
// so message encoding starts at the correct position
|
||||
shared_buf.reserve_and_resize(total_size, header_padding);
|
||||
shared_buf.reserve(total_size);
|
||||
// Resize to add header padding so message encoding starts at the correct position
|
||||
shared_buf.resize(header_padding);
|
||||
}
|
||||
|
||||
// Convenience overload - computes frame overhead internally
|
||||
|
||||
@@ -100,61 +100,149 @@ const LogString *api_error_to_logstr(APIError err) {
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
APIError APIFrameHelper::drain_overflow_and_handle_errors_() {
|
||||
if (this->overflow_buf_.try_drain(this->socket_.get()) == -1) {
|
||||
int err = errno;
|
||||
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
|
||||
HELPER_LOG("Socket write failed with errno %d", err);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
// Default implementation for loop - handles sending buffered data
|
||||
APIError APIFrameHelper::loop() {
|
||||
if (this->tx_buf_count_ > 0) {
|
||||
APIError err = try_send_tx_buf_();
|
||||
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
return APIError::OK;
|
||||
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
|
||||
}
|
||||
|
||||
// Write data to socket, overflow to backlog buffer if LWIP TCP send buffer is full.
|
||||
// Returns OK if all data was sent or successfully queued.
|
||||
// Returns SOCKET_WRITE_FAILED on hard error (sets state to FAILED).
|
||||
// Common socket write error handling
|
||||
APIError APIFrameHelper::handle_socket_write_error_() {
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
HELPER_LOG("Socket write failed with errno %d", errno);
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
|
||||
// Helper method to buffer data from IOVs
|
||||
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
|
||||
uint16_t offset) {
|
||||
// Check if queue is full
|
||||
if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
|
||||
HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
|
||||
this->state_ = State::FAILED;
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t buffer_size = total_write_len - offset;
|
||||
auto &buffer = this->tx_buf_[this->tx_buf_tail_];
|
||||
buffer = std::make_unique<SendBuffer>(SendBuffer{
|
||||
.data = std::make_unique<uint8_t[]>(buffer_size),
|
||||
.size = buffer_size,
|
||||
.offset = 0,
|
||||
});
|
||||
|
||||
uint16_t to_skip = offset;
|
||||
uint16_t write_pos = 0;
|
||||
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
if (to_skip >= iov[i].iov_len) {
|
||||
// Skip this entire segment
|
||||
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
|
||||
} else {
|
||||
// Include this segment (partially or fully)
|
||||
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
|
||||
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
|
||||
std::memcpy(buffer->data.get() + write_pos, src, len);
|
||||
write_pos += len;
|
||||
to_skip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Update circular buffer tracking
|
||||
this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->tx_buf_count_++;
|
||||
}
|
||||
|
||||
// This method writes data to socket or buffers it
|
||||
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
|
||||
// Returns APIError::OK if successful (or would block, but data has been buffered)
|
||||
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
|
||||
|
||||
if (iovcnt == 0)
|
||||
return APIError::OK; // Nothing to do, success
|
||||
|
||||
#ifdef HELPER_LOG_PACKETS
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
LOG_PACKET_SENDING(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t skip = 0;
|
||||
// Try to send any existing buffered data first if there is any
|
||||
if (this->tx_buf_count_ > 0) {
|
||||
APIError send_result = try_send_tx_buf_();
|
||||
// If real error occurred (not just WOULD_BLOCK), return it
|
||||
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
|
||||
return send_result;
|
||||
}
|
||||
|
||||
// Drain any existing backlog first
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
APIError err = this->drain_overflow_and_handle_errors_();
|
||||
if (err != APIError::OK)
|
||||
return err;
|
||||
}
|
||||
|
||||
// If backlog is clear, try direct send
|
||||
if (this->overflow_buf_.empty()) [[likely]] {
|
||||
ssize_t sent =
|
||||
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
|
||||
if (sent == -1) [[unlikely]] {
|
||||
int err = errno;
|
||||
if (this->check_socket_write_err_(err) != APIError::WOULD_BLOCK) {
|
||||
HELPER_LOG("Socket write failed with errno %d", err);
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
} else if (static_cast<uint16_t>(sent) >= total_write_len) [[likely]] {
|
||||
return APIError::OK;
|
||||
} else {
|
||||
skip = static_cast<uint16_t>(sent);
|
||||
// If there is still data in the buffer, we can't send, buffer
|
||||
// the new data and return
|
||||
if (this->tx_buf_count_ > 0) {
|
||||
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
|
||||
return APIError::OK; // Success, data buffered
|
||||
}
|
||||
}
|
||||
|
||||
// Queue unsent data into overflow buffer
|
||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, skip)) {
|
||||
HELPER_LOG("Overflow buffer full, dropping connection");
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
// Try to send directly if no buffered data
|
||||
// Optimize for single iovec case (common for plaintext API)
|
||||
ssize_t sent =
|
||||
(iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
|
||||
|
||||
if (sent == -1) {
|
||||
APIError err = this->handle_socket_write_error_();
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
// Socket would block, buffer the data
|
||||
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
|
||||
return APIError::OK; // Success, data buffered
|
||||
}
|
||||
return err; // Socket write failed
|
||||
} else if (static_cast<uint16_t>(sent) < total_write_len) {
|
||||
// Partially sent, buffer the remaining data
|
||||
this->buffer_data_from_iov_(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent));
|
||||
}
|
||||
return APIError::OK;
|
||||
|
||||
return APIError::OK; // Success, all data sent or buffered
|
||||
}
|
||||
|
||||
// Common implementation for trying to send buffered data
|
||||
// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
|
||||
APIError APIFrameHelper::try_send_tx_buf_() {
|
||||
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
|
||||
while (this->tx_buf_count_ > 0) {
|
||||
// Get the first buffer in the queue
|
||||
SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
|
||||
|
||||
// Try to send the remaining data in this buffer
|
||||
ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
|
||||
|
||||
if (sent == -1) {
|
||||
return this->handle_socket_write_error_();
|
||||
} else if (sent == 0) {
|
||||
// Nothing sent but not an error
|
||||
return APIError::WOULD_BLOCK;
|
||||
} else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
|
||||
// Partially sent, update offset
|
||||
// Cast to ensure no overflow issues with uint16_t
|
||||
front_buffer->offset += static_cast<uint16_t>(sent);
|
||||
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
|
||||
} else {
|
||||
// Buffer completely sent, remove it from the queue
|
||||
this->tx_buf_[this->tx_buf_head_].reset();
|
||||
this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->tx_buf_count_--;
|
||||
// Continue loop to try sending the next buffer
|
||||
}
|
||||
}
|
||||
|
||||
return APIError::OK; // All buffers sent successfully
|
||||
}
|
||||
|
||||
const char *APIFrameHelper::get_peername_to(std::span<char, socket::SOCKADDR_STR_LEN> buf) const {
|
||||
@@ -190,12 +278,11 @@ APIError APIFrameHelper::init_common_() {
|
||||
|
||||
APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
|
||||
if (received == -1) {
|
||||
const int err = errno;
|
||||
if (err == EWOULDBLOCK || err == EAGAIN) {
|
||||
if (errno == EWOULDBLOCK || errno == EAGAIN) {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Socket read failed with errno %d", err);
|
||||
HELPER_LOG("Socket read failed with errno %d", errno);
|
||||
return APIError::SOCKET_READ_FAILED;
|
||||
} else if (received == 0) {
|
||||
state_ = State::FAILED;
|
||||
|
||||
@@ -9,11 +9,9 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_API
|
||||
#include "esphome/components/api/api_buffer.h"
|
||||
#include "esphome/components/api/api_overflow_buffer.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "proto.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -39,6 +37,8 @@ static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
|
||||
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
|
||||
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
|
||||
|
||||
class ProtoWriteBuffer;
|
||||
|
||||
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
|
||||
static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32;
|
||||
|
||||
@@ -105,9 +105,9 @@ class APIFrameHelper {
|
||||
}
|
||||
virtual ~APIFrameHelper() = default;
|
||||
virtual APIError init() = 0;
|
||||
virtual APIError loop() = 0;
|
||||
virtual APIError loop();
|
||||
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
|
||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->overflow_buf_.empty(); }
|
||||
bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
|
||||
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
|
||||
APIError close() {
|
||||
if (state_ == State::CLOSED)
|
||||
@@ -147,28 +147,25 @@ class APIFrameHelper {
|
||||
//
|
||||
void set_nodelay_for_message(bool is_log_message) {
|
||||
if (!is_log_message) {
|
||||
if (this->nodelay_counter_) {
|
||||
if (this->nodelay_state_ != NODELAY_ON) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_counter_ = 0;
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Log message: enable Nagle on first, flush after LOG_NAGLE_COUNT
|
||||
if (!this->nodelay_counter_)
|
||||
|
||||
// Log messages: state transitions -1 -> 1 -> ... -> LOG_NAGLE_COUNT -> -1 (flush)
|
||||
if (this->nodelay_state_ == NODELAY_ON) {
|
||||
this->set_nodelay_raw_(false);
|
||||
if (++this->nodelay_counter_ > LOG_NAGLE_COUNT) {
|
||||
this->nodelay_state_ = 1;
|
||||
} else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) {
|
||||
this->set_nodelay_raw_(true);
|
||||
this->nodelay_counter_ = 0;
|
||||
this->nodelay_state_ = NODELAY_ON;
|
||||
} else {
|
||||
this->nodelay_state_++;
|
||||
}
|
||||
}
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
// Resize buffer to include footer space if needed (e.g. Noise MAC)
|
||||
if (frame_footer_size_)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
||||
MessageInfo msg{type, 0,
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
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
|
||||
// The buffer contains all messages with appropriate padding before each
|
||||
@@ -190,23 +187,28 @@ class APIFrameHelper {
|
||||
}
|
||||
|
||||
protected:
|
||||
// Drain backlogged overflow data to the socket and handle errors.
|
||||
// Called when overflow_buf_.empty() is false. Out-of-line to keep the
|
||||
// fast path (empty check) inline at call sites.
|
||||
// Returns OK for transient errors (WOULD_BLOCK), SOCKET_WRITE_FAILED for hard errors.
|
||||
APIError drain_overflow_and_handle_errors_();
|
||||
// Buffer containing data to be sent
|
||||
struct SendBuffer {
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
uint16_t size{0}; // Total size of the buffer
|
||||
uint16_t offset{0}; // Current offset within the buffer
|
||||
|
||||
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
|
||||
uint16_t remaining() const { return size - offset; }
|
||||
const uint8_t *current_data() const { return data.get() + offset; }
|
||||
};
|
||||
|
||||
// Common implementation for writing raw data to socket
|
||||
APIError write_raw_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
|
||||
|
||||
// Check if a socket write errno is a hard error (not WOULD_BLOCK/EAGAIN).
|
||||
// Returns WOULD_BLOCK for transient errors, SOCKET_WRITE_FAILED for hard errors.
|
||||
APIError check_socket_write_err_(int err) {
|
||||
if (err == EWOULDBLOCK || err == EAGAIN)
|
||||
return APIError::WOULD_BLOCK;
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
// Try to send data from the tx buffer
|
||||
APIError try_send_tx_buf_();
|
||||
|
||||
// Helper method to buffer data from IOVs
|
||||
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len, uint16_t offset);
|
||||
|
||||
// Common socket write error handling
|
||||
APIError handle_socket_write_error_();
|
||||
|
||||
// Socket ownership (4 bytes on 32-bit, 8 bytes on 64-bit)
|
||||
std::unique_ptr<socket::Socket> socket_;
|
||||
@@ -241,8 +243,8 @@ class APIFrameHelper {
|
||||
return APIError::WOULD_BLOCK;
|
||||
}
|
||||
|
||||
// Backlog for unsent data when TCP send buffer is full (rarely used in production)
|
||||
APIOverflowBuffer overflow_buf_;
|
||||
// Containers (size varies, but typically 12+ bytes on 32-bit)
|
||||
std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
|
||||
APIBuffer rx_buf_;
|
||||
|
||||
// Client name buffer - stores name from Hello message or initial peername
|
||||
@@ -253,17 +255,21 @@ class APIFrameHelper {
|
||||
State state_{State::INITIALIZE};
|
||||
uint8_t frame_header_padding_{0};
|
||||
uint8_t frame_footer_size_{0};
|
||||
// Nagle batching counter for log messages. 0 means NODELAY is enabled (immediate send).
|
||||
// Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we flush by re-enabling NODELAY and resetting to 0.
|
||||
uint8_t tx_buf_head_{0};
|
||||
uint8_t tx_buf_tail_{0};
|
||||
uint8_t tx_buf_count_{0};
|
||||
// Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled
|
||||
// (immediate send). Values 1..LOG_NAGLE_COUNT count log messages in the current Nagle batch.
|
||||
// After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset.
|
||||
// ESP8266 has the tightest TCP send buffer (2×MSS) and needs conservative batching.
|
||||
// ESP32 (4×MSS+), RP2040 (8×MSS), and LibreTiny (4×MSS) can coalesce more.
|
||||
static constexpr int8_t NODELAY_ON = -1;
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr uint8_t LOG_NAGLE_COUNT = 2;
|
||||
static constexpr int8_t LOG_NAGLE_COUNT = 2;
|
||||
#else
|
||||
static constexpr uint8_t LOG_NAGLE_COUNT = 3;
|
||||
static constexpr int8_t LOG_NAGLE_COUNT = 3;
|
||||
#endif
|
||||
uint8_t nodelay_counter_{0};
|
||||
int8_t nodelay_state_{NODELAY_ON};
|
||||
|
||||
// Internal helper to set TCP_NODELAY socket option
|
||||
void set_nodelay_raw_(bool enable) {
|
||||
|
||||
@@ -153,10 +153,8 @@ APIError APINoiseFrameHelper::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
return this->drain_overflow_and_handle_errors_();
|
||||
}
|
||||
return APIError::OK;
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_.
|
||||
@@ -452,6 +450,14 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = type;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
// Resize to include MAC space (required for Noise encryption)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
|
||||
MessageInfo msg{type, 0,
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
|
||||
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
if (aerr != APIError::OK)
|
||||
|
||||
@@ -22,6 +22,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -64,10 +64,8 @@ APIError APIPlaintextFrameHelper::loop() {
|
||||
if (state_ != State::DATA) {
|
||||
return APIError::BAD_STATE;
|
||||
}
|
||||
if (!this->overflow_buf_.empty()) [[unlikely]] {
|
||||
return this->drain_overflow_and_handle_errors_();
|
||||
}
|
||||
return APIError::OK;
|
||||
// Use base class implementation for buffer sending
|
||||
return APIFrameHelper::loop();
|
||||
}
|
||||
|
||||
/** Read a packet into the rx_buf_.
|
||||
@@ -237,6 +235,11 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
buffer->type = this->rx_header_parsed_type_;
|
||||
return APIError::OK;
|
||||
}
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
|
||||
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
|
||||
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
|
||||
}
|
||||
|
||||
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
|
||||
std::span<const MessageInfo> messages) {
|
||||
APIError aerr = this->check_data_state_();
|
||||
@@ -254,11 +257,9 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
|
||||
uint16_t total_write_len = 0;
|
||||
|
||||
for (const auto &msg : messages) {
|
||||
// Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead
|
||||
uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE
|
||||
? 1
|
||||
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
|
||||
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
|
||||
// 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 total_header_len = 1 + size_varint_len + type_varint_len;
|
||||
|
||||
// Calculate where to start writing the header
|
||||
@@ -280,8 +281,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
|
||||
//
|
||||
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
|
||||
// [0] - 0x00 indicator byte
|
||||
// [1-3] - Payload size varint (3 bytes, for sizes 16384-65535)
|
||||
// [4-5] - Message type varint (2 bytes, for types 128-16383)
|
||||
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
|
||||
// [4-5] - Message type varint (2 bytes, for types 128-32767)
|
||||
// [6...] - Actual payload data
|
||||
//
|
||||
// The message starts at offset + frame_header_padding_
|
||||
|
||||
@@ -19,6 +19,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
APIError init() override;
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#include "api_overflow_buffer.h"
|
||||
#ifdef USE_API
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
APIOverflowBuffer::~APIOverflowBuffer() {
|
||||
for (auto *entry : this->queue_) {
|
||||
if (entry != nullptr)
|
||||
Entry::destroy(entry);
|
||||
}
|
||||
}
|
||||
|
||||
ssize_t APIOverflowBuffer::try_drain(socket::Socket *socket) {
|
||||
while (this->count_ > 0) {
|
||||
Entry *front = this->queue_[this->head_];
|
||||
|
||||
ssize_t sent = socket->write(front->current_data(), front->remaining());
|
||||
|
||||
if (sent <= 0) {
|
||||
// -1 = error (caller checks errno for EWOULDBLOCK vs hard error)
|
||||
// 0 = nothing sent (treat as no progress)
|
||||
return sent;
|
||||
}
|
||||
|
||||
if (static_cast<uint16_t>(sent) < front->remaining()) {
|
||||
// Partially sent, update offset and stop
|
||||
front->offset += static_cast<uint16_t>(sent);
|
||||
return sent;
|
||||
}
|
||||
|
||||
// Entry fully sent — free it and advance
|
||||
Entry::destroy(front);
|
||||
this->queue_[this->head_] = nullptr;
|
||||
this->head_ = (this->head_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->count_--;
|
||||
}
|
||||
|
||||
return 0; // All drained
|
||||
}
|
||||
|
||||
bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip) {
|
||||
if (this->count_ >= API_MAX_SEND_QUEUE)
|
||||
return false;
|
||||
|
||||
uint16_t buffer_size = total_len - skip;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
|
||||
auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0};
|
||||
this->queue_[this->tail_] = entry;
|
||||
|
||||
uint16_t to_skip = skip;
|
||||
uint16_t write_pos = 0;
|
||||
|
||||
for (int i = 0; i < iovcnt; i++) {
|
||||
if (to_skip >= iov[i].iov_len) {
|
||||
to_skip -= static_cast<uint16_t>(iov[i].iov_len);
|
||||
} else {
|
||||
const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
|
||||
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
|
||||
std::memcpy(entry->data + write_pos, src, len);
|
||||
write_pos += len;
|
||||
to_skip = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->count_++;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
#endif // USE_API
|
||||
@@ -1,76 +0,0 @@
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_API
|
||||
|
||||
#include "esphome/components/socket/headers.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
/// Circular queue of heap-allocated byte buffers used as a TCP send backlog.
|
||||
///
|
||||
/// Under normal operation this buffer is **never used** — data goes straight
|
||||
/// from the frame helper to the socket. It only fills when the LWIP TCP
|
||||
/// send buffer is full (slow client, congested network, heavy logging).
|
||||
/// The queue drains automatically on subsequent write/loop calls once the
|
||||
/// socket becomes writable again.
|
||||
///
|
||||
/// Capacity is compile-time-fixed via API_MAX_SEND_QUEUE (set from Python
|
||||
/// config). If the queue fills completely the connection is marked failed.
|
||||
class APIOverflowBuffer {
|
||||
public:
|
||||
/// A single heap-allocated send-backlog entry.
|
||||
/// Lifetime is manually managed — see destroy().
|
||||
struct Entry {
|
||||
uint8_t *data;
|
||||
uint16_t size; // Total size of the buffer
|
||||
uint16_t offset; // Current send offset within the buffer
|
||||
|
||||
uint16_t remaining() const { return this->size - this->offset; }
|
||||
const uint8_t *current_data() const { return this->data + this->offset; }
|
||||
|
||||
/// Free this entry and its data buffer.
|
||||
static ESPHOME_ALWAYS_INLINE void destroy(Entry *entry) {
|
||||
delete[] entry->data;
|
||||
delete entry; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
};
|
||||
|
||||
~APIOverflowBuffer();
|
||||
|
||||
/// True when no backlogged data is waiting.
|
||||
bool empty() const { return this->count_ == 0; }
|
||||
|
||||
/// True when the queue has no room for another entry.
|
||||
bool full() const { return this->count_ >= API_MAX_SEND_QUEUE; }
|
||||
|
||||
/// Number of entries currently queued.
|
||||
uint8_t count() const { return this->count_; }
|
||||
|
||||
/// Try to drain queued data to the socket.
|
||||
/// Returns bytes-written > 0 on success/partial, 0 if all drained or no progress,
|
||||
/// -1 on error (caller must check errno to distinguish EWOULDBLOCK from hard errors).
|
||||
/// Callers only need to act on -1; 0 and positive values both mean "no error".
|
||||
/// Frees entries as they are fully sent.
|
||||
ssize_t try_drain(socket::Socket *socket);
|
||||
|
||||
/// Enqueue unsent IOV data into the backlog.
|
||||
/// Copies iov data starting at byte offset `skip` into a new entry.
|
||||
/// Returns false if the queue is full (caller should fail the connection).
|
||||
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
|
||||
|
||||
protected:
|
||||
std::array<Entry *, API_MAX_SEND_QUEUE> queue_{};
|
||||
uint8_t head_{0};
|
||||
uint8_t tail_{0};
|
||||
uint8_t count_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
#endif // USE_API
|
||||
@@ -142,6 +142,12 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const {
|
||||
buffer.encode_sub_message(25, it);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
buffer.encode_uint32(26, this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
buffer.encode_uint64(27, this->zigbee_ieee_address);
|
||||
#endif
|
||||
}
|
||||
uint32_t DeviceInfoResponse::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
@@ -202,6 +208,12 @@ uint32_t DeviceInfoResponse::calculate_size() const {
|
||||
for (const auto &it : this->serial_proxies) {
|
||||
size += ProtoSize::calc_message_force(2, it.calculate_size());
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
size += ProtoSize::calc_uint32(2, this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
size += ProtoSize::calc_uint64(2, this->zigbee_ieee_address);
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
@@ -3889,5 +3901,57 @@ uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const {
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
bool ZigbeeProxyFrame::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 1: {
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ZigbeeProxyFrame::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bytes(1, this->data, this->data_len); }
|
||||
uint32_t ZigbeeProxyFrame::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_length(1, this->data_len);
|
||||
return size;
|
||||
}
|
||||
bool ZigbeeProxyRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->type = static_cast<enums::ZigbeeProxyRequestType>(value);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool ZigbeeProxyRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
|
||||
switch (field_id) {
|
||||
case 2: {
|
||||
this->data = value.data();
|
||||
this->data_len = value.size();
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
void ZigbeeProxyRequest::encode(ProtoWriteBuffer &buffer) const {
|
||||
buffer.encode_uint32(1, static_cast<uint32_t>(this->type));
|
||||
buffer.encode_bytes(2, this->data, this->data_len);
|
||||
}
|
||||
uint32_t ZigbeeProxyRequest::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_uint32(1, static_cast<uint32_t>(this->type));
|
||||
size += ProtoSize::calc_length(1, this->data_len);
|
||||
return size;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -341,6 +341,13 @@ enum SerialProxyStatus : uint32_t {
|
||||
SERIAL_PROXY_STATUS_NOT_SUPPORTED = 4,
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
enum ZigbeeProxyRequestType : uint32_t {
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE = 0,
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1,
|
||||
ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO = 2,
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace enums
|
||||
|
||||
@@ -518,7 +525,7 @@ class SerialProxyInfo final : public ProtoMessage {
|
||||
class DeviceInfoResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 10;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 309;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 319;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "device_info_response"; }
|
||||
#endif
|
||||
@@ -573,6 +580,12 @@ class DeviceInfoResponse final : public ProtoMessage {
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
uint32_t zigbee_proxy_feature_flags{0};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
uint64_t zigbee_ieee_address{0};
|
||||
#endif
|
||||
void encode(ProtoWriteBuffer &buffer) const;
|
||||
uint32_t calculate_size() const;
|
||||
@@ -3285,5 +3298,45 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
|
||||
protected:
|
||||
};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
class ZigbeeProxyFrame final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 148;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 19;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "zigbee_proxy_frame"; }
|
||||
#endif
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
void encode(ProtoWriteBuffer &buffer) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
};
|
||||
class ZigbeeProxyRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 149;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 21;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *message_name() const override { return "zigbee_proxy_request"; }
|
||||
#endif
|
||||
enums::ZigbeeProxyRequestType type{};
|
||||
const uint8_t *data{nullptr};
|
||||
uint16_t data_len{0};
|
||||
void encode(ProtoWriteBuffer &buffer) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
|
||||
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
|
||||
};
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -3,10 +3,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_BLUETOOTH_PROXY
|
||||
#ifndef USE_API_VARINT64
|
||||
#define USE_API_VARINT64
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace esphome::api {} // namespace esphome::api
|
||||
|
||||
@@ -806,6 +806,20 @@ template<> const char *proto_enum_to_string<enums::SerialProxyStatus>(enums::Ser
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
template<> const char *proto_enum_to_string<enums::ZigbeeProxyRequestType>(enums::ZigbeeProxyRequestType value) {
|
||||
switch (value) {
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE:
|
||||
return "ZIGBEE_PROXY_REQUEST_TYPE_SUBSCRIBE";
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
|
||||
return "ZIGBEE_PROXY_REQUEST_TYPE_UNSUBSCRIBE";
|
||||
case enums::ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO:
|
||||
return "ZIGBEE_PROXY_REQUEST_TYPE_NETWORK_INFO";
|
||||
default:
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
const char *HelloRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "HelloRequest");
|
||||
@@ -930,6 +944,12 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
dump_field(out, "zigbee_proxy_feature_flags", this->zigbee_proxy_feature_flags);
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
dump_field(out, "zigbee_ieee_address", this->zigbee_ieee_address);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
@@ -2651,6 +2671,19 @@ const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
const char *ZigbeeProxyFrame::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ZigbeeProxyFrame");
|
||||
dump_bytes_field(out, "data", this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ZigbeeProxyRequest::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, "ZigbeeProxyRequest");
|
||||
dump_field(out, "type", static_cast<enums::ZigbeeProxyRequestType>(this->type));
|
||||
dump_bytes_field(out, "data", this->data, this->data_len);
|
||||
return out.c_str();
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
|
||||
@@ -700,6 +700,28 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
|
||||
this->on_bluetooth_set_connection_params_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
case ZigbeeProxyFrame::MESSAGE_TYPE: {
|
||||
ZigbeeProxyFrame msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_zigbee_proxy_frame"), msg);
|
||||
#endif
|
||||
this->on_zigbee_proxy_frame(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
case ZigbeeProxyRequest::MESSAGE_TYPE: {
|
||||
ZigbeeProxyRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_zigbee_proxy_request"), msg);
|
||||
#endif
|
||||
this->on_zigbee_proxy_request(msg);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
default:
|
||||
break;
|
||||
|
||||
@@ -238,6 +238,12 @@ class APIServerConnectionBase : public ProtoService {
|
||||
virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){};
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
virtual void on_zigbee_proxy_frame(const ZigbeeProxyFrame &value){};
|
||||
#endif
|
||||
#ifdef USE_ZIGBEE_PROXY
|
||||
virtual void on_zigbee_proxy_request(const ZigbeeProxyRequest &value){};
|
||||
#endif
|
||||
protected:
|
||||
void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override;
|
||||
};
|
||||
|
||||
@@ -36,11 +36,11 @@ struct SavedNoisePsk {
|
||||
} PACKED; // NOLINT
|
||||
#endif
|
||||
|
||||
class APIServer final : public Component,
|
||||
public Controller
|
||||
class APIServer : public Component,
|
||||
public Controller
|
||||
#ifdef USE_CAMERA
|
||||
,
|
||||
public camera::CameraListener
|
||||
public camera::CameraListener
|
||||
#endif
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -136,9 +136,8 @@ class CustomAPIDevice {
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(StringRef), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto *obj = static_cast<T *>(this);
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
|
||||
[obj, callback](StringRef state) { (obj->*callback)(state); });
|
||||
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).
|
||||
@@ -149,12 +148,10 @@ class CustomAPIDevice {
|
||||
ESPDEPRECATED("Use void callback(StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto *obj = static_cast<T *>(this);
|
||||
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 &)>(
|
||||
[obj, callback](const std::string &state) { (obj->*callback)(state); }));
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
|
||||
std::function<void(const std::string &)>(f));
|
||||
}
|
||||
|
||||
/** Subscribe to the state (or attribute state) of an entity from Home Assistant.
|
||||
@@ -179,10 +176,8 @@ class CustomAPIDevice {
|
||||
template<typename T>
|
||||
void subscribe_homeassistant_state(void (T::*callback)(const std::string &, StringRef), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto *obj = static_cast<T *>(this);
|
||||
global_api_server->subscribe_home_assistant_state(
|
||||
entity_id, optional<std::string>(attribute),
|
||||
[obj, callback, entity_id](StringRef state) { (obj->*callback)(entity_id, state); });
|
||||
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).
|
||||
@@ -193,12 +188,10 @@ class CustomAPIDevice {
|
||||
ESPDEPRECATED("Use void callback(const std::string &, StringRef) instead. Will be removed in 2027.1.0.", "2026.1.0")
|
||||
void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
|
||||
const std::string &attribute = "") {
|
||||
auto *obj = static_cast<T *>(this);
|
||||
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 &)>(
|
||||
[obj, callback, entity_id](const std::string &state) { (obj->*callback)(entity_id, state); }));
|
||||
global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute),
|
||||
std::function<void(const std::string &)>(f));
|
||||
}
|
||||
#else
|
||||
template<typename T>
|
||||
|
||||
@@ -442,12 +442,8 @@ class ProtoMessage {
|
||||
virtual const char *message_name() const { return "unknown"; }
|
||||
#endif
|
||||
|
||||
#ifndef USE_HOST
|
||||
protected:
|
||||
#endif
|
||||
// Non-virtual destructor is protected to prevent polymorphic deletion.
|
||||
// On host platform, made public to allow value-initialization of std::array
|
||||
// members (e.g. DeviceInfoResponse::devices) without clang errors.
|
||||
~ProtoMessage() = default;
|
||||
};
|
||||
|
||||
@@ -477,12 +473,6 @@ class ProtoDecodableMessage : public ProtoMessage {
|
||||
|
||||
class ProtoSize {
|
||||
public:
|
||||
// Varint encoding thresholds: values below each threshold fit in N bytes
|
||||
static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128
|
||||
static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384
|
||||
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
|
||||
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
|
||||
|
||||
/**
|
||||
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
|
||||
*
|
||||
@@ -490,7 +480,7 @@ class ProtoSize {
|
||||
* @return The number of bytes needed to encode the value
|
||||
*/
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) {
|
||||
if (value < VARINT_THRESHOLD_1_BYTE) [[likely]]
|
||||
if (value < 128) [[likely]]
|
||||
return 1; // Fast path: 7 bits, most common case
|
||||
if (__builtin_is_constant_evaluated())
|
||||
return varint_wide(value);
|
||||
@@ -502,11 +492,11 @@ class ProtoSize {
|
||||
static uint32_t varint_slow(uint32_t value) __attribute__((noinline));
|
||||
// Shared cascade for values >= 128 (used by both constexpr and noinline paths)
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) {
|
||||
if (value < VARINT_THRESHOLD_2_BYTE)
|
||||
if (value < 16384)
|
||||
return 2;
|
||||
if (value < VARINT_THRESHOLD_3_BYTE)
|
||||
if (value < 2097152)
|
||||
return 3;
|
||||
if (value < VARINT_THRESHOLD_4_BYTE)
|
||||
if (value < 268435456)
|
||||
return 4;
|
||||
return 5;
|
||||
}
|
||||
@@ -612,7 +602,7 @@ class ProtoSize {
|
||||
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
|
||||
return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
|
||||
}
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) {
|
||||
static constexpr uint32_t calc_sint32_force(uint32_t field_id_size, int32_t value) {
|
||||
return field_id_size + varint(encode_zigzag32(value));
|
||||
}
|
||||
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
|
||||
@@ -624,13 +614,13 @@ class ProtoSize {
|
||||
static constexpr uint32_t calc_uint64(uint32_t field_id_size, uint64_t value) {
|
||||
return value ? field_id_size + varint(value) : 0;
|
||||
}
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
|
||||
static constexpr uint32_t calc_uint64_force(uint32_t field_id_size, uint64_t value) {
|
||||
return field_id_size + varint(value);
|
||||
}
|
||||
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
|
||||
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
|
||||
}
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_length_force(uint32_t field_id_size, size_t len) {
|
||||
static constexpr uint32_t calc_length_force(uint32_t field_id_size, size_t len) {
|
||||
return field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len);
|
||||
}
|
||||
static constexpr uint32_t calc_sint64(uint32_t field_id_size, int64_t value) {
|
||||
@@ -648,8 +638,7 @@ class ProtoSize {
|
||||
static constexpr uint32_t calc_message(uint32_t field_id_size, uint32_t nested_size) {
|
||||
return nested_size ? field_id_size + varint(nested_size) + nested_size : 0;
|
||||
}
|
||||
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_message_force(uint32_t field_id_size,
|
||||
uint32_t nested_size) {
|
||||
static constexpr uint32_t calc_message_force(uint32_t field_id_size, uint32_t nested_size) {
|
||||
return field_id_size + varint(nested_size) + nested_size;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -41,7 +41,7 @@ enum AS3935RegisterMasks {
|
||||
INT_MASK = 0xF0,
|
||||
THRESH_MASK = 0x0F,
|
||||
R_SPIKE_MASK = 0xF0,
|
||||
ENERGY_MASK = 0xE0,
|
||||
ENERGY_MASK = 0xF0,
|
||||
CAP_MASK = 0xF0,
|
||||
LIGHT_MASK = 0xCF,
|
||||
DISTURB_MASK = 0xDF,
|
||||
|
||||
@@ -52,12 +52,11 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
|
||||
connect_cb_(connect_arg_, this);
|
||||
return true;
|
||||
}
|
||||
const int saved_errno = errno;
|
||||
if (saved_errno != EINPROGRESS) {
|
||||
ESP_LOGE(TAG, "Connect failed: %d", saved_errno);
|
||||
if (errno != EINPROGRESS) {
|
||||
ESP_LOGE(TAG, "Connect failed: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, saved_errno);
|
||||
error_cb_(error_arg_, this, errno);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -80,12 +79,11 @@ size_t AsyncClient::write(const char *data, size_t len) {
|
||||
|
||||
ssize_t sent = socket_->write(data, len);
|
||||
if (sent < 0) {
|
||||
const int err = errno;
|
||||
if (err != EAGAIN && err != EWOULDBLOCK) {
|
||||
ESP_LOGE(TAG, "Write error: %d", err);
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
ESP_LOGE(TAG, "Write error: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, err);
|
||||
error_cb_(error_arg_, this, errno);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -131,11 +129,10 @@ void AsyncClient::loop() {
|
||||
error_cb_(error_arg_, this, error);
|
||||
}
|
||||
} else if (ret < 0) {
|
||||
const int err = errno;
|
||||
ESP_LOGE(TAG, "Select error: %d", err);
|
||||
ESP_LOGE(TAG, "Select error: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, err);
|
||||
error_cb_(error_arg_, this, errno);
|
||||
}
|
||||
} else if (connected_) {
|
||||
// For connected sockets, use the Application's select() results
|
||||
@@ -151,14 +148,11 @@ void AsyncClient::loop() {
|
||||
} else if (len > 0) {
|
||||
if (data_cb_)
|
||||
data_cb_(data_arg_, this, buf, len);
|
||||
} else {
|
||||
const int err = errno;
|
||||
if (err != EAGAIN && err != EWOULDBLOCK) {
|
||||
ESP_LOGW(TAG, "Read error: %d", err);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, err);
|
||||
}
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
ESP_LOGW(TAG, "Read error: %d", errno);
|
||||
close();
|
||||
if (error_cb_)
|
||||
error_cb_(error_arg_, this, errno);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +183,7 @@ class BedjetCodec {
|
||||
|
||||
BedjetPacket packet_;
|
||||
|
||||
BedjetStatusPacket *status_packet_{nullptr};
|
||||
BedjetStatusPacket *status_packet_;
|
||||
BedjetStatusPacket buf_;
|
||||
};
|
||||
|
||||
|
||||
@@ -96,7 +96,8 @@ class MultiClickTrigger : public Trigger<>, public Component {
|
||||
|
||||
void setup() override {
|
||||
this->last_state_ = this->parent_->get_state_default(false);
|
||||
this->parent_->add_on_state_callback([this](bool state) { this->on_state_(state); });
|
||||
auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1);
|
||||
this->parent_->add_on_state_callback(f);
|
||||
}
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
@@ -37,7 +37,7 @@ class TimeoutFilter : public Filter, public Component {
|
||||
TemplatableValue<uint32_t> timeout_delay_{};
|
||||
};
|
||||
|
||||
class DelayedOnOffFilter final : public Filter, public Component {
|
||||
class DelayedOnOffFilter : public Filter, public Component {
|
||||
public:
|
||||
optional<bool> new_value(bool value) override;
|
||||
|
||||
|
||||
@@ -47,8 +47,6 @@ void BLEClientRSSISensor::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
|
||||
switch (event) {
|
||||
// server response on RSSI request:
|
||||
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
|
||||
if (!this->parent()->check_addr(param->read_rssi_cmpl.remote_addr))
|
||||
return;
|
||||
if (param->read_rssi_cmpl.status == ESP_BT_STATUS_SUCCESS) {
|
||||
int8_t rssi = param->read_rssi_cmpl.rssi;
|
||||
ESP_LOGI(TAG, "ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT RSSI: %d", rssi);
|
||||
|
||||
@@ -102,10 +102,6 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.value_len == 0) {
|
||||
ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str());
|
||||
break;
|
||||
}
|
||||
ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
|
||||
param->notify.handle, param->notify.value[0]);
|
||||
if (param->notify.handle != this->handle)
|
||||
@@ -135,10 +131,8 @@ float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
|
||||
if (this->has_data_to_value_) {
|
||||
std::vector<uint8_t> data(value, value + value_len);
|
||||
return this->data_to_value_func_(data);
|
||||
} else if (value_len > 0) {
|
||||
return value[0];
|
||||
} else {
|
||||
return NAN;
|
||||
return value[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -104,10 +104,6 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
case ESP_GATTC_NOTIFY_EVT: {
|
||||
if (param->notify.handle != this->handle)
|
||||
break;
|
||||
if (param->notify.value_len == 0) {
|
||||
ESP_LOGW(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: empty value", this->get_name().c_str());
|
||||
break;
|
||||
}
|
||||
ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
|
||||
param->notify.handle, param->notify.value[0]);
|
||||
this->publish_state(reinterpret_cast<const char *>(param->notify.value), param->notify.value_len);
|
||||
|
||||
@@ -67,14 +67,14 @@ bool BLENUS::read_array(uint8_t *data, size_t len) {
|
||||
|
||||
// First, use the peek buffer if available
|
||||
if (this->has_peek_) {
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
|
||||
#endif
|
||||
data[0] = this->peek_buffer_;
|
||||
this->has_peek_ = false;
|
||||
data++;
|
||||
if (--len == 0) { // Decrement len first, then check it...
|
||||
return true; // No more to read
|
||||
#ifdef USE_UART_DEBUGGER
|
||||
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
|
||||
#endif
|
||||
return true; // No more to read
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -521,7 +521,7 @@ int BME680BSECComponent::reinit_bsec_lib_() {
|
||||
}
|
||||
|
||||
void BME680BSECComponent::load_state_() {
|
||||
uint32_t hash = fnv1_hash_extend(fnv1_hash("bme680_bsec_state_"), this->device_id_);
|
||||
uint32_t hash = fnv1_hash("bme680_bsec_state_" + this->device_id_);
|
||||
this->bsec_state_ = global_preferences->make_preference<uint8_t[BSEC_MAX_STATE_BLOB_SIZE]>(hash, true);
|
||||
|
||||
if (!this->bsec_state_.load(&this->bsec_state_data_)) {
|
||||
|
||||
@@ -186,8 +186,8 @@ async def to_code_base(config):
|
||||
cg.add_library("SPI", None)
|
||||
cg.add_library(
|
||||
"BME68x Sensor library",
|
||||
None,
|
||||
"https://github.com/boschsensortec/Bosch-BME68x-Library#v1.3.40408",
|
||||
"1.3.40408",
|
||||
"https://github.com/boschsensortec/Bosch-BME68x-Library",
|
||||
)
|
||||
cg.add_library(
|
||||
"BSEC2 Software Library",
|
||||
|
||||
@@ -279,8 +279,7 @@ void BME68xBSEC2Component::run_() {
|
||||
uint32_t meas_dur = 0;
|
||||
meas_dur = bme68x_get_meas_dur(this->op_mode_, &bme68x_conf, &this->bme68x_);
|
||||
ESP_LOGV(TAG, "Queueing read in %uus", meas_dur);
|
||||
this->trigger_time_ns_ = curr_time_ns;
|
||||
this->set_timeout("read", meas_dur / 1000, [this]() { this->read_(this->trigger_time_ns_); });
|
||||
this->set_timeout("read", meas_dur / 1000, [this, curr_time_ns]() { this->read_(curr_time_ns); });
|
||||
} else {
|
||||
ESP_LOGV(TAG, "Measurement not required");
|
||||
this->read_(curr_time_ns);
|
||||
|
||||
@@ -116,8 +116,6 @@ class BME68xBSEC2Component : public Component {
|
||||
int8_t bme68x_status_{BME68X_OK};
|
||||
|
||||
int64_t last_time_ms_{0};
|
||||
int64_t trigger_time_ns_{0}; // Stored for set_timeout lambda to help avoid heap allocation on supported 32-bit
|
||||
// toolchains with small std::function SBO
|
||||
uint32_t millis_overflow_counter_{0};
|
||||
|
||||
std::queue<std::function<void()>> queue_;
|
||||
|
||||
@@ -10,12 +10,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
#include <psa/crypto.h>
|
||||
#else
|
||||
#include "mbedtls/ccm.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
namespace bthome_mithermometer {
|
||||
@@ -201,37 +196,6 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da
|
||||
const uint8_t *ciphertext = data.data() + 1;
|
||||
const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE;
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// PSA AEAD expects ciphertext + tag concatenated
|
||||
// BLE advertisement max payload is 31 bytes, so this is always sufficient
|
||||
static constexpr size_t MAX_CT_WITH_TAG = 32;
|
||||
uint8_t ct_with_tag[MAX_CT_WITH_TAG];
|
||||
size_t ct_with_tag_size = ciphertext_size + BTHOME_MIC_SIZE;
|
||||
memcpy(ct_with_tag, ciphertext, ciphertext_size);
|
||||
memcpy(ct_with_tag + ciphertext_size, mic, BTHOME_MIC_SIZE);
|
||||
|
||||
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
|
||||
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
|
||||
psa_set_key_bits(&attributes, BTHOME_BINDKEY_SIZE * 8);
|
||||
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
|
||||
psa_set_key_algorithm(&attributes, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE));
|
||||
|
||||
mbedtls_svc_key_id_t key_id;
|
||||
if (psa_import_key(&attributes, this->bindkey_, BTHOME_BINDKEY_SIZE, &key_id) != PSA_SUCCESS) {
|
||||
ESP_LOGVV(TAG, "psa_import_key() failed.");
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t plaintext_length;
|
||||
psa_status_t status = psa_aead_decrypt(key_id, PSA_ALG_AEAD_WITH_SHORTENED_TAG(PSA_ALG_CCM, BTHOME_MIC_SIZE),
|
||||
nonce.data(), nonce.size(), nullptr, 0, ct_with_tag, ct_with_tag_size,
|
||||
payload.data(), ciphertext_size, &plaintext_length);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || plaintext_length != ciphertext_size) {
|
||||
ESP_LOGVV(TAG, "BTHome decryption failed.");
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
mbedtls_ccm_context ctx;
|
||||
mbedtls_ccm_init(&ctx);
|
||||
|
||||
@@ -249,7 +213,6 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da
|
||||
ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,5 +20,6 @@ void Button::press() {
|
||||
this->press_action();
|
||||
this->press_callback_.call();
|
||||
}
|
||||
void Button::add_on_press_callback(std::function<void()> &&callback) { this->press_callback_.add(std::move(callback)); }
|
||||
|
||||
} // namespace esphome::button
|
||||
|
||||
@@ -34,9 +34,7 @@ class Button : public EntityBase {
|
||||
*
|
||||
* @param callback The void() callback.
|
||||
*/
|
||||
template<typename F> void add_on_press_callback(F &&callback) {
|
||||
this->press_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
void add_on_press_callback(std::function<void()> &&callback);
|
||||
|
||||
protected:
|
||||
/** You should implement this virtual method if you want to create your own button.
|
||||
|
||||
@@ -50,7 +50,7 @@ async def to_code(config: ConfigType) -> None:
|
||||
buffer = cg.new_Pvariable(config[CONF_ENCODER_BUFFER_ID])
|
||||
cg.add(buffer.set_buffer_size(config[CONF_BUFFER_SIZE]))
|
||||
if config[CONF_TYPE] == ESP32_CAMERA_ENCODER:
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
|
||||
cg.add_define("USE_ESP32_CAMERA_JPEG_ENCODER")
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
|
||||
@@ -91,7 +91,10 @@ class Canbus : public Component {
|
||||
* - rtr If this is a remote transmission request
|
||||
* - data The message data
|
||||
*/
|
||||
template<typename F> void add_callback(F &&callback) { this->callback_manager_.add(std::forward<F>(callback)); }
|
||||
void add_callback(
|
||||
std::function<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)> callback) {
|
||||
this->callback_manager_.add(std::move(callback));
|
||||
}
|
||||
|
||||
protected:
|
||||
template<typename... Ts> friend class CanbusSendAction;
|
||||
|
||||
@@ -100,9 +100,8 @@ void DNSServer::process_next_request() {
|
||||
&client_addr_len);
|
||||
|
||||
if (len < 0) {
|
||||
const int err = errno;
|
||||
if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) {
|
||||
ESP_LOGE(TAG, "recvfrom failed: %d", err);
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
|
||||
ESP_LOGE(TAG, "recvfrom failed: %d", errno);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -356,6 +356,14 @@ ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode)
|
||||
return *this;
|
||||
}
|
||||
|
||||
void Climate::add_on_state_callback(std::function<void(Climate &)> &&callback) {
|
||||
this->state_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void Climate::add_on_control_callback(std::function<void(ClimateCall &)> &&callback) {
|
||||
this->control_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
// Random 32bit value; If this changes existing restore preferences are invalidated
|
||||
static const uint32_t RESTORE_STATE_VERSION = 0x848EA6ADUL;
|
||||
|
||||
|
||||
@@ -192,9 +192,7 @@ class Climate : public EntityBase {
|
||||
*
|
||||
* @param callback The callback to call.
|
||||
*/
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
void add_on_state_callback(std::function<void(Climate &)> &&callback);
|
||||
|
||||
/**
|
||||
* Add a callback for the climate device configuration; each time the configuration parameters of a climate device
|
||||
@@ -202,9 +200,7 @@ class Climate : public EntityBase {
|
||||
*
|
||||
* @param callback The callback to call.
|
||||
*/
|
||||
template<typename F> void add_on_control_callback(F &&callback) {
|
||||
this->control_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
void add_on_control_callback(std::function<void(ClimateCall &)> &&callback);
|
||||
|
||||
/** Make a climate device control call, this is used to control the climate device, see the ClimateCall description
|
||||
* for more info.
|
||||
|
||||
@@ -21,10 +21,6 @@ CONF_REQUEST_HEADERS = "request_headers"
|
||||
CONF_ROWS = "rows"
|
||||
CONF_STOP_BITS = "stop_bits"
|
||||
CONF_USE_PSRAM = "use_psram"
|
||||
CONF_VOLUME_INCREMENT = "volume_increment"
|
||||
CONF_VOLUME_INITIAL = "volume_initial"
|
||||
CONF_VOLUME_MAX = "volume_max"
|
||||
CONF_VOLUME_MIN = "volume_min"
|
||||
|
||||
ICON_CURRENT_DC = "mdi:current-dc"
|
||||
ICON_SOLAR_PANEL = "mdi:solar-panel"
|
||||
|
||||
@@ -105,18 +105,17 @@ template<typename... Ts> using CoverIsClosedCondition = CoverPositionCondition<f
|
||||
|
||||
template<bool OPEN> class CoverPositionTrigger : public Trigger<> {
|
||||
public:
|
||||
CoverPositionTrigger(Cover *a_cover) : cover_(a_cover) {
|
||||
a_cover->add_on_state_callback([this]() {
|
||||
if (this->cover_->position != this->last_position_) {
|
||||
this->last_position_ = this->cover_->position;
|
||||
if (this->cover_->position == (OPEN ? COVER_OPEN : COVER_CLOSED))
|
||||
CoverPositionTrigger(Cover *a_cover) {
|
||||
a_cover->add_on_state_callback([this, a_cover]() {
|
||||
if (a_cover->position != this->last_position_) {
|
||||
this->last_position_ = a_cover->position;
|
||||
if (a_cover->position == (OPEN ? COVER_OPEN : COVER_CLOSED))
|
||||
this->trigger();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected:
|
||||
Cover *cover_;
|
||||
float last_position_{NAN};
|
||||
};
|
||||
|
||||
@@ -125,9 +124,9 @@ using CoverClosedTrigger = CoverPositionTrigger<false>;
|
||||
|
||||
template<CoverOperation OP> class CoverTrigger : public Trigger<> {
|
||||
public:
|
||||
CoverTrigger(Cover *a_cover) : cover_(a_cover) {
|
||||
a_cover->add_on_state_callback([this]() {
|
||||
auto current_op = this->cover_->current_operation;
|
||||
CoverTrigger(Cover *a_cover) {
|
||||
a_cover->add_on_state_callback([this, a_cover]() {
|
||||
auto current_op = a_cover->current_operation;
|
||||
if (current_op == OP) {
|
||||
if (!this->last_operation_.has_value() || this->last_operation_.value() != OP) {
|
||||
this->trigger();
|
||||
@@ -138,7 +137,6 @@ template<CoverOperation OP> class CoverTrigger : public Trigger<> {
|
||||
}
|
||||
|
||||
protected:
|
||||
Cover *cover_;
|
||||
optional<CoverOperation> last_operation_{};
|
||||
};
|
||||
} // namespace esphome::cover
|
||||
|
||||
@@ -139,6 +139,7 @@ bool CoverCall::get_stop() const { return this->stop_; }
|
||||
|
||||
CoverCall Cover::make_call() { return {this}; }
|
||||
|
||||
void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
|
||||
void Cover::publish_state(bool save) {
|
||||
this->position = clamp(this->position, 0.0f, 1.0f);
|
||||
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
|
||||
|
||||
@@ -125,7 +125,7 @@ class Cover : public EntityBase {
|
||||
/// Construct a new cover call used to control the cover.
|
||||
CoverCall make_call();
|
||||
|
||||
template<typename F> void add_on_state_callback(F &&f) { this->state_callback_.add(std::forward<F>(f)); }
|
||||
void add_on_state_callback(std::function<void()> &&f);
|
||||
|
||||
/** Publish the current state of the cover.
|
||||
*
|
||||
|
||||
@@ -136,9 +136,6 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
|
||||
float DallasTemperatureSensor::get_temp_c_() {
|
||||
int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0];
|
||||
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) {
|
||||
if (this->scratch_pad_[7] == 0) {
|
||||
return NAN;
|
||||
}
|
||||
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
|
||||
}
|
||||
switch (this->resolution_) {
|
||||
|
||||
@@ -14,9 +14,7 @@ class DateTimeBase : public EntityBase {
|
||||
public:
|
||||
virtual ESPTime state_as_esptime() const = 0;
|
||||
|
||||
template<typename F> void add_on_state_callback(F &&callback) {
|
||||
this->state_callback_.add(std::forward<F>(callback));
|
||||
}
|
||||
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
|
||||
|
||||
#ifdef USE_TIME
|
||||
void set_rtc(time::RealTimeClock *rtc) { this->rtc_ = rtc; }
|
||||
@@ -33,12 +31,9 @@ class DateTimeBase : public EntityBase {
|
||||
|
||||
class DateTimeStateTrigger : public Trigger<ESPTime> {
|
||||
public:
|
||||
explicit DateTimeStateTrigger(DateTimeBase *parent) : parent_(parent) {
|
||||
parent->add_on_state_callback([this]() { this->trigger(this->parent_->state_as_esptime()); });
|
||||
explicit DateTimeStateTrigger(DateTimeBase *parent) {
|
||||
parent->add_on_state_callback([this, parent]() { this->trigger(parent->state_as_esptime()); });
|
||||
}
|
||||
|
||||
protected:
|
||||
DateTimeBase *parent_;
|
||||
};
|
||||
|
||||
} // namespace esphome::datetime
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace debug {
|
||||
|
||||
static constexpr size_t DEVICE_INFO_BUFFER_SIZE = 256;
|
||||
static constexpr size_t RESET_REASON_BUFFER_SIZE = 128;
|
||||
static constexpr size_t WAKEUP_CAUSE_BUFFER_SIZE = 128;
|
||||
|
||||
// buf_append_printf is now provided by esphome/core/helpers.h
|
||||
|
||||
@@ -95,7 +94,7 @@ class DebugComponent : public PollingComponent {
|
||||
#endif // USE_TEXT_SENSOR
|
||||
|
||||
const char *get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer);
|
||||
const char *get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer);
|
||||
const char *get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer);
|
||||
uint32_t get_free_heap_();
|
||||
size_t get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE> buffer, size_t pos);
|
||||
void update_platform_();
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include <esp_sleep.h>
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
#include <esp_heap_caps.h>
|
||||
#include <esp_system.h>
|
||||
@@ -49,8 +48,7 @@ static const size_t REBOOT_MAX_LEN = 24;
|
||||
void DebugComponent::on_shutdown() {
|
||||
auto *component = App.get_current_component();
|
||||
char buffer[REBOOT_MAX_LEN]{};
|
||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN,
|
||||
fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str()));
|
||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
|
||||
if (component != nullptr) {
|
||||
strncpy(buffer, LOG_STR_ARG(component->get_component_log_str()), REBOOT_MAX_LEN - 1);
|
||||
buffer[REBOOT_MAX_LEN - 1] = '\0';
|
||||
@@ -67,8 +65,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
unsigned reason = esp_reset_reason();
|
||||
if (reason < sizeof(RESET_REASONS) / sizeof(RESET_REASONS[0])) {
|
||||
if (reason == ESP_RST_SW) {
|
||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN,
|
||||
fnv1_hash_extend(fnv1_hash(REBOOT_KEY), App.get_name().c_str()));
|
||||
auto pref = global_preferences->make_preference(REBOOT_MAX_LEN, fnv1_hash(REBOOT_KEY + App.get_name()));
|
||||
char reboot_source[REBOOT_MAX_LEN]{};
|
||||
if (pref.load(&reboot_source)) {
|
||||
reboot_source[REBOOT_MAX_LEN - 1] = '\0';
|
||||
@@ -85,74 +82,32 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return buf;
|
||||
}
|
||||
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
static const char *const WAKEUP_CAUSES[] = {
|
||||
"undefined", // ESP_SLEEP_WAKEUP_UNDEFINED (0)
|
||||
"undefined", // ESP_SLEEP_WAKEUP_ALL (1)
|
||||
"external signal using RTC_IO", // ESP_SLEEP_WAKEUP_EXT0 (2)
|
||||
"external signal using RTC_CNTL", // ESP_SLEEP_WAKEUP_EXT1 (3)
|
||||
"timer", // ESP_SLEEP_WAKEUP_TIMER (4)
|
||||
"touchpad", // ESP_SLEEP_WAKEUP_TOUCHPAD (5)
|
||||
"ULP program", // ESP_SLEEP_WAKEUP_ULP (6)
|
||||
"GPIO", // ESP_SLEEP_WAKEUP_GPIO (7)
|
||||
"UART", // ESP_SLEEP_WAKEUP_UART (8)
|
||||
"UART1", // ESP_SLEEP_WAKEUP_UART1 (9)
|
||||
"UART2", // ESP_SLEEP_WAKEUP_UART2 (10)
|
||||
"WIFI", // ESP_SLEEP_WAKEUP_WIFI (11)
|
||||
"COCPU int", // ESP_SLEEP_WAKEUP_COCPU (12)
|
||||
"COCPU crash", // ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG (13)
|
||||
"BT", // ESP_SLEEP_WAKEUP_BT (14)
|
||||
"VAD", // ESP_SLEEP_WAKEUP_VAD (15)
|
||||
"VBAT under voltage", // ESP_SLEEP_WAKEUP_VBAT_UNDER_VOLT (16)
|
||||
"undefined",
|
||||
"undefined",
|
||||
"external signal using RTC_IO",
|
||||
"external signal using RTC_CNTL",
|
||||
"timer",
|
||||
"touchpad",
|
||||
"ULP program",
|
||||
"GPIO",
|
||||
"UART",
|
||||
"WIFI",
|
||||
"COCPU int",
|
||||
"COCPU crash",
|
||||
"BT",
|
||||
};
|
||||
#else
|
||||
static const char *const WAKEUP_CAUSES[] = {
|
||||
"undefined", // ESP_SLEEP_WAKEUP_UNDEFINED (0)
|
||||
"undefined", // ESP_SLEEP_WAKEUP_ALL (1)
|
||||
"external signal using RTC_IO", // ESP_SLEEP_WAKEUP_EXT0 (2)
|
||||
"external signal using RTC_CNTL", // ESP_SLEEP_WAKEUP_EXT1 (3)
|
||||
"timer", // ESP_SLEEP_WAKEUP_TIMER (4)
|
||||
"touchpad", // ESP_SLEEP_WAKEUP_TOUCHPAD (5)
|
||||
"ULP program", // ESP_SLEEP_WAKEUP_ULP (6)
|
||||
"GPIO", // ESP_SLEEP_WAKEUP_GPIO (7)
|
||||
"UART", // ESP_SLEEP_WAKEUP_UART (8)
|
||||
"WIFI", // ESP_SLEEP_WAKEUP_WIFI (9)
|
||||
"COCPU int", // ESP_SLEEP_WAKEUP_COCPU (10)
|
||||
"COCPU crash", // ESP_SLEEP_WAKEUP_COCPU_TRAP_TRIG (11)
|
||||
"BT", // ESP_SLEEP_WAKEUP_BT (12)
|
||||
};
|
||||
#endif
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
|
||||
static constexpr auto NUM_CAUSES = sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0]);
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// IDF 6.0+ returns a bitmap of all wakeup sources
|
||||
uint32_t causes = esp_sleep_get_wakeup_causes();
|
||||
if (causes == 0) {
|
||||
return WAKEUP_CAUSES[0]; // "undefined"
|
||||
}
|
||||
char *p = buffer.data();
|
||||
char *end = p + buffer.size();
|
||||
*p = '\0';
|
||||
const char *sep = "";
|
||||
for (unsigned i = 0; i < NUM_CAUSES && p < end; i++) {
|
||||
if (causes & (1U << i)) {
|
||||
size_t needed = strlen(sep) + strlen(WAKEUP_CAUSES[i]);
|
||||
if (p + needed >= end) {
|
||||
break;
|
||||
}
|
||||
p += snprintf(p, end - p, "%s%s", sep, WAKEUP_CAUSES[i]);
|
||||
sep = ", ";
|
||||
}
|
||||
}
|
||||
return buffer.data();
|
||||
#else
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
const char *wake_reason;
|
||||
unsigned reason = esp_sleep_get_wakeup_cause();
|
||||
if (reason < NUM_CAUSES) {
|
||||
return WAKEUP_CAUSES[reason];
|
||||
if (reason < sizeof(WAKEUP_CAUSES) / sizeof(WAKEUP_CAUSES[0])) {
|
||||
wake_reason = WAKEUP_CAUSES[reason];
|
||||
} else {
|
||||
wake_reason = "unknown source";
|
||||
}
|
||||
return "unknown source";
|
||||
#endif
|
||||
// Return the static string directly - no need to copy to buffer
|
||||
return wake_reason;
|
||||
}
|
||||
|
||||
void DebugComponent::log_partition_info_() {
|
||||
@@ -241,10 +196,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
uint32_t cpu_freq_mhz = arch_get_cpu_freq_hz() / 1000000;
|
||||
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32 " MHz", cpu_freq_mhz);
|
||||
|
||||
char reset_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
char wakeup_buffer[WAKEUP_CAUSE_BUFFER_SIZE];
|
||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reset_buffer));
|
||||
const char *wakeup_cause = get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE>(wakeup_buffer));
|
||||
char reason_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
const char *wakeup_cause = get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
|
||||
uint8_t mac[6];
|
||||
get_mac_address_raw(mac);
|
||||
|
||||
@@ -91,7 +91,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
// ESP8266 doesn't have detailed wakeup cause like ESP32
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace debug {
|
||||
|
||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return INT_MAX; }
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return lt_get_reboot_reason_name(lt_get_reboot_reason());
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return lt_heap_get_free(); }
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) { return ""; }
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) { return ""; }
|
||||
|
||||
uint32_t DebugComponent::get_free_heap_() { return ::rp2040.getFreeHeap(); }
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@ const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFE
|
||||
return buf;
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, WAKEUP_CAUSE_BUFFER_SIZE> buffer) {
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
// Zephyr doesn't have detailed wakeup cause like ESP32
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "driver/gpio.h"
|
||||
#include "deep_sleep_component.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <esp_idf_version.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace deep_sleep {
|
||||
@@ -27,7 +26,7 @@ namespace deep_sleep {
|
||||
// - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup)
|
||||
// - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup)
|
||||
// - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup)
|
||||
// - GPIO wakeup: GPIO wakeup for RTC pins
|
||||
// - GPIO wakeup: GPIO wakeup for RTC pins (esp_deep_sleep_enable_gpio_wakeup)
|
||||
|
||||
static const char *const TAG = "deep_sleep";
|
||||
|
||||
@@ -136,13 +135,8 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
}
|
||||
// Internal pullup/pulldown resistors are enabled automatically, when
|
||||
// ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS is set (by default it is)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << this->wakeup_pin_->get_pin(),
|
||||
static_cast<esp_sleep_gpio_wake_up_mode_t>(level));
|
||||
#else
|
||||
esp_deep_sleep_enable_gpio_wakeup(1ULL << this->wakeup_pin_->get_pin(),
|
||||
esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(),
|
||||
static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -51,8 +51,8 @@ class DFPlayer : public uart::UARTDevice, public Component {
|
||||
bool is_playing() { return is_playing_; }
|
||||
void dump_config() override;
|
||||
|
||||
template<typename F> void add_on_finished_playback_callback(F &&callback) {
|
||||
this->on_finished_playback_callback_.add(std::forward<F>(callback));
|
||||
void add_on_finished_playback_callback(std::function<void()> callback) {
|
||||
this->on_finished_playback_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
protected:
|
||||
|
||||
@@ -96,52 +96,37 @@ template<typename... Ts> class IsActiveCondition : public Condition<Ts...> {
|
||||
|
||||
class DisplayMenuOnEnterTrigger : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_enter_callback([this]() { this->trigger(this->parent_); });
|
||||
explicit DisplayMenuOnEnterTrigger(MenuItem *parent) {
|
||||
parent->add_on_enter_callback([this, parent]() { this->trigger(parent); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnLeaveTrigger : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_leave_callback([this]() { this->trigger(this->parent_); });
|
||||
explicit DisplayMenuOnLeaveTrigger(MenuItem *parent) {
|
||||
parent->add_on_leave_callback([this, parent]() { this->trigger(parent); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnValueTrigger : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnValueTrigger(MenuItem *parent) : parent_(parent) {
|
||||
parent->add_on_value_callback([this]() { this->trigger(this->parent_); });
|
||||
explicit DisplayMenuOnValueTrigger(MenuItem *parent) {
|
||||
parent->add_on_value_callback([this, parent]() { this->trigger(parent); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItem *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnNextTrigger : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) : parent_(parent) {
|
||||
parent->add_on_next_callback([this]() { this->trigger(this->parent_); });
|
||||
explicit DisplayMenuOnNextTrigger(MenuItemCustom *parent) {
|
||||
parent->add_on_next_callback([this, parent]() { this->trigger(parent); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItemCustom *parent_;
|
||||
};
|
||||
|
||||
class DisplayMenuOnPrevTrigger : public Trigger<const MenuItem *> {
|
||||
public:
|
||||
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) : parent_(parent) {
|
||||
parent->add_on_prev_callback([this]() { this->trigger(this->parent_); });
|
||||
explicit DisplayMenuOnPrevTrigger(MenuItemCustom *parent) {
|
||||
parent->add_on_prev_callback([this, parent]() { this->trigger(parent); });
|
||||
}
|
||||
|
||||
protected:
|
||||
MenuItemCustom *parent_;
|
||||
};
|
||||
|
||||
} // namespace display_menu_base
|
||||
|
||||
@@ -44,9 +44,9 @@ class MenuItem {
|
||||
MenuItemMenu *get_parent() { return this->parent_; }
|
||||
MenuItemType get_type() const { return this->item_type_; }
|
||||
template<typename V> void set_text(V val) { this->text_ = val; }
|
||||
template<typename F> void add_on_enter_callback(F &&cb) { this->on_enter_callbacks_.add(std::forward<F>(cb)); }
|
||||
template<typename F> void add_on_leave_callback(F &&cb) { this->on_leave_callbacks_.add(std::forward<F>(cb)); }
|
||||
template<typename F> void add_on_value_callback(F &&cb) { this->on_value_callbacks_.add(std::forward<F>(cb)); }
|
||||
void add_on_enter_callback(std::function<void()> &&cb) { this->on_enter_callbacks_.add(std::move(cb)); }
|
||||
void add_on_leave_callback(std::function<void()> &&cb) { this->on_leave_callbacks_.add(std::move(cb)); }
|
||||
void add_on_value_callback(std::function<void()> &&cb) { this->on_value_callbacks_.add(std::move(cb)); }
|
||||
|
||||
std::string get_text() const { return const_cast<MenuItem *>(this)->text_.value(this); }
|
||||
virtual bool get_immediate_edit() const { return false; }
|
||||
@@ -170,8 +170,8 @@ class MenuItemCommand : public MenuItem {
|
||||
class MenuItemCustom : public MenuItemEditable {
|
||||
public:
|
||||
explicit MenuItemCustom() : MenuItemEditable(MENU_ITEM_CUSTOM) {}
|
||||
template<typename F> void add_on_next_callback(F &&cb) { this->on_next_callbacks_.add(std::forward<F>(cb)); }
|
||||
template<typename F> void add_on_prev_callback(F &&cb) { this->on_prev_callbacks_.add(std::forward<F>(cb)); }
|
||||
void add_on_next_callback(std::function<void()> &&cb) { this->on_next_callbacks_.add(std::move(cb)); }
|
||||
void add_on_prev_callback(std::function<void()> &&cb) { this->on_prev_callbacks_.add(std::move(cb)); }
|
||||
|
||||
bool has_value() const override { return this->value_getter_.has_value(); }
|
||||
std::string get_value_text() const override;
|
||||
|
||||
@@ -3,14 +3,9 @@
|
||||
#if defined(USE_ESP8266_FRAMEWORK_ARDUINO)
|
||||
#include <bearssl/bearssl.h>
|
||||
#elif defined(USE_ESP32)
|
||||
#include <esp_idf_version.h>
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
#include <psa/crypto.h>
|
||||
#else
|
||||
#include "mbedtls/esp_config.h"
|
||||
#include "mbedtls/gcm.h"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace esphome::dlms_meter {
|
||||
|
||||
@@ -245,35 +240,6 @@ bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t m
|
||||
br_gcm_flip(&gcm_ctx);
|
||||
br_gcm_run(&gcm_ctx, 0, payload_ptr, message_length);
|
||||
#elif defined(USE_ESP32)
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// PSA Crypto multipart AEAD (no tag verification, matching legacy behavior)
|
||||
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
|
||||
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
|
||||
psa_set_key_bits(&attributes, this->decryption_key_.size() * 8);
|
||||
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_DECRYPT);
|
||||
psa_set_key_algorithm(&attributes, PSA_ALG_GCM);
|
||||
|
||||
mbedtls_svc_key_id_t key_id;
|
||||
bool decrypt_failed = true;
|
||||
if (psa_import_key(&attributes, this->decryption_key_.data(), this->decryption_key_.size(), &key_id) == PSA_SUCCESS) {
|
||||
psa_aead_operation_t op = PSA_AEAD_OPERATION_INIT;
|
||||
if (psa_aead_decrypt_setup(&op, key_id, PSA_ALG_GCM) == PSA_SUCCESS &&
|
||||
psa_aead_set_nonce(&op, iv, sizeof(iv)) == PSA_SUCCESS) {
|
||||
size_t outlen = 0;
|
||||
if (psa_aead_update(&op, payload_ptr, message_length, payload_ptr, message_length, &outlen) == PSA_SUCCESS &&
|
||||
outlen == message_length) {
|
||||
decrypt_failed = false;
|
||||
}
|
||||
}
|
||||
psa_aead_abort(&op);
|
||||
psa_destroy_key(key_id);
|
||||
}
|
||||
if (decrypt_failed) {
|
||||
ESP_LOGE(TAG, "Decryption failed");
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
size_t outlen = 0;
|
||||
mbedtls_gcm_context gcm_ctx;
|
||||
mbedtls_gcm_init(&gcm_ctx);
|
||||
@@ -286,7 +252,6 @@ bool DlmsMeterComponent::decrypt_(std::vector<uint8_t> &mbus_payload, uint16_t m
|
||||
this->receive_buffer_.clear();
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
#else
|
||||
#error "Invalid Platform"
|
||||
#endif
|
||||
|
||||
@@ -127,7 +127,8 @@ void DPS310Component::read_() {
|
||||
this->update_in_progress_ = false;
|
||||
this->status_clear_warning();
|
||||
} else {
|
||||
this->set_timeout("dps310", 10, [this]() { this->read_(); });
|
||||
auto f = std::bind(&DPS310Component::read_, this);
|
||||
this->set_timeout("dps310", 10, f);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ void EE895Component::setup() {
|
||||
this->read(serial_number, 20);
|
||||
|
||||
crc16_check = (serial_number[19] << 8) + serial_number[18];
|
||||
if (crc16_check != calc_crc16_(serial_number, 18)) {
|
||||
if (crc16_check != calc_crc16_(serial_number, 19)) {
|
||||
this->error_code_ = CRC_CHECK_FAILED;
|
||||
this->mark_failed();
|
||||
return;
|
||||
@@ -84,7 +84,7 @@ void EE895Component::write_command_(uint16_t addr, uint16_t reg_cnt) {
|
||||
address[2] = addr & 0xFF;
|
||||
address[3] = (reg_cnt >> 8) & 0xFF;
|
||||
address[4] = reg_cnt & 0xFF;
|
||||
crc16 = calc_crc16_(address, 5);
|
||||
crc16 = calc_crc16_(address, 6);
|
||||
address[5] = crc16 & 0xFF;
|
||||
address[6] = (crc16 >> 8) & 0xFF;
|
||||
this->write(address, 7);
|
||||
@@ -95,7 +95,7 @@ float EE895Component::read_float_() {
|
||||
uint8_t i2c_response[8];
|
||||
this->read(i2c_response, 8);
|
||||
crc16_check = (i2c_response[7] << 8) + i2c_response[6];
|
||||
if (crc16_check != calc_crc16_(i2c_response, 6)) {
|
||||
if (crc16_check != calc_crc16_(i2c_response, 7)) {
|
||||
this->error_code_ = CRC_CHECK_FAILED;
|
||||
this->status_set_warning();
|
||||
return 0;
|
||||
@@ -107,9 +107,12 @@ float EE895Component::read_float_() {
|
||||
}
|
||||
|
||||
uint16_t EE895Component::calc_crc16_(const uint8_t buf[], uint8_t len) {
|
||||
uint8_t addr = this->address_;
|
||||
uint16_t crc = crc16(&addr, 1);
|
||||
return crc16(buf, len, crc);
|
||||
uint8_t crc_check_buf[22];
|
||||
for (int i = 0; i < len; i++) {
|
||||
crc_check_buf[i + 1] = buf[i];
|
||||
}
|
||||
crc_check_buf[0] = this->address_;
|
||||
return crc16(crc_check_buf, len);
|
||||
}
|
||||
} // namespace ee895
|
||||
} // namespace esphome
|
||||
|
||||
@@ -28,7 +28,6 @@ from esphome.const import (
|
||||
CONF_PLATFORMIO_OPTIONS,
|
||||
CONF_REF,
|
||||
CONF_SAFE_MODE,
|
||||
CONF_SIZE,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
CONF_VARIANT,
|
||||
@@ -60,7 +59,6 @@ from .const import ( # noqa
|
||||
KEY_EXTRA_BUILD_FILES,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_FULL_CERT_BUNDLE,
|
||||
KEY_IDF_VERSION,
|
||||
KEY_PATH,
|
||||
KEY_REF,
|
||||
KEY_REPO,
|
||||
@@ -97,7 +95,6 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
CONF_RELEASE = "release"
|
||||
CONF_SUBTYPE = "subtype"
|
||||
|
||||
ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32"
|
||||
ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}"
|
||||
@@ -423,20 +420,9 @@ def set_core_data(config):
|
||||
CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS] = excluded
|
||||
# Initialize Arduino library tracking - cg.add_library() auto-enables libraries
|
||||
CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES] = set()
|
||||
framework_ver = cv.Version.parse(config[CONF_FRAMEWORK][CONF_VERSION])
|
||||
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = framework_ver
|
||||
|
||||
# Store the underlying IDF version for framework-agnostic checks
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = framework_ver
|
||||
elif (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||
CORE.data[KEY_ESP32][KEY_IDF_VERSION] = idf_ver
|
||||
else:
|
||||
raise cv.Invalid(
|
||||
f"Arduino version {framework_ver} has no known ESP-IDF version mapping. "
|
||||
"Please update ARDUINO_IDF_VERSION_LOOKUP.",
|
||||
path=[CONF_FRAMEWORK, CONF_VERSION],
|
||||
)
|
||||
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
|
||||
config[CONF_FRAMEWORK][CONF_VERSION]
|
||||
)
|
||||
|
||||
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
|
||||
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
|
||||
@@ -614,12 +600,10 @@ def _format_framework_espidf_version(
|
||||
ext = "tar.xz"
|
||||
else:
|
||||
ext = "zip"
|
||||
# Build version string with extra separator based on type:
|
||||
# numeric extra uses dot (e.g., "5.5.3.1"), string extra uses dash (e.g., "6.0.0-rc1")
|
||||
# Build version string with dot-separated extra (e.g., "5.5.3.1" not "5.5.3-1")
|
||||
ver_str = f"{ver.major}.{ver.minor}.{ver.patch}"
|
||||
if ver.extra:
|
||||
sep = "." if str(ver.extra).isdigit() else "-"
|
||||
ver_str += f"{sep}{ver.extra}"
|
||||
ver_str += f".{ver.extra}"
|
||||
if release:
|
||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}.{release}/esp-idf-v{ver_str}.{ext}"
|
||||
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}/esp-idf-v{ver_str}.{ext}"
|
||||
@@ -990,7 +974,6 @@ KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
|
||||
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
|
||||
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
|
||||
KEY_FATFS_REQUIRED = "fatfs_required"
|
||||
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
|
||||
|
||||
|
||||
def require_vfs_select() -> None:
|
||||
@@ -1060,25 +1043,6 @@ def require_mbedtls_pkcs7() -> None:
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
|
||||
|
||||
|
||||
def require_mbedtls_sha512() -> None:
|
||||
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
|
||||
|
||||
Call this from components that need to verify TLS certificates or signatures
|
||||
using SHA-384 or SHA-512 algorithms. This prevents CONFIG_MBEDTLS_SHA384_C
|
||||
and CONFIG_MBEDTLS_SHA512_C from being disabled.
|
||||
"""
|
||||
CORE.data[KEY_ESP32][KEY_MBEDTLS_SHA512_REQUIRED] = True
|
||||
|
||||
|
||||
def idf_version() -> cv.Version:
|
||||
"""Return the underlying ESP-IDF version regardless of framework choice.
|
||||
|
||||
For ESP-IDF builds this is the framework version directly.
|
||||
For Arduino builds this is the mapped IDF version from ARDUINO_IDF_VERSION_LOOKUP.
|
||||
"""
|
||||
return CORE.data[KEY_ESP32][KEY_IDF_VERSION]
|
||||
|
||||
|
||||
def require_fatfs() -> None:
|
||||
"""Mark that FATFS support is required by a component.
|
||||
|
||||
@@ -1260,43 +1224,6 @@ def _set_default_framework(config):
|
||||
return config
|
||||
|
||||
|
||||
RESERVED_PARTITION_NAMES = {
|
||||
"nvs",
|
||||
"app0",
|
||||
"app1",
|
||||
"otadata",
|
||||
"eeprom",
|
||||
"spiffs",
|
||||
"phy_init",
|
||||
}
|
||||
|
||||
VALID_APP_SUBTYPES = {"factory", "test"}
|
||||
VALID_DATA_SUBTYPES = {
|
||||
"nvs",
|
||||
"nvs_keys",
|
||||
"spiffs",
|
||||
"coredump",
|
||||
"efuse",
|
||||
"fat",
|
||||
"undefined",
|
||||
"littlefs",
|
||||
}
|
||||
|
||||
|
||||
def _validate_custom_partition(config: ConfigType) -> ConfigType:
|
||||
"""Voluptuous validator for custom partition schema."""
|
||||
try:
|
||||
_validate_partition(
|
||||
config[CONF_NAME],
|
||||
config[CONF_TYPE],
|
||||
config[CONF_SUBTYPE],
|
||||
config[CONF_SIZE],
|
||||
)
|
||||
except ValueError as e:
|
||||
raise cv.Invalid(str(e)) from e
|
||||
return config
|
||||
|
||||
|
||||
FLASH_SIZES = [
|
||||
"2MB",
|
||||
"4MB",
|
||||
@@ -1319,28 +1246,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_FLASH_SIZE, default="4MB"): cv.one_of(
|
||||
*FLASH_SIZES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_PARTITIONS): cv.Any(
|
||||
cv.file_,
|
||||
cv.ensure_list(
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_NAME): cv.string_strict,
|
||||
cv.Required(CONF_TYPE): cv.All(
|
||||
cv.Any(cv.string_strict, cv.int_range(0x40, 0xFE)),
|
||||
cv.int_to_hex_string,
|
||||
),
|
||||
cv.Required(CONF_SUBTYPE): cv.All(
|
||||
cv.Any(cv.string_strict, cv.int_range(0, 0xFE)),
|
||||
cv.int_to_hex_string,
|
||||
),
|
||||
cv.Required(CONF_SIZE): cv.int_range(min=0x1000),
|
||||
}
|
||||
),
|
||||
_validate_custom_partition,
|
||||
),
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_PARTITIONS): cv.file_,
|
||||
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
|
||||
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
|
||||
}
|
||||
@@ -1809,18 +1715,9 @@ async def to_code(config):
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
if CONF_PARTITIONS in config:
|
||||
if isinstance(config[CONF_PARTITIONS], list):
|
||||
for partition in config[CONF_PARTITIONS]:
|
||||
add_partition(
|
||||
partition[CONF_NAME],
|
||||
partition[CONF_TYPE],
|
||||
partition[CONF_SUBTYPE],
|
||||
partition[CONF_SIZE],
|
||||
)
|
||||
else:
|
||||
add_extra_build_file(
|
||||
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
|
||||
)
|
||||
add_extra_build_file(
|
||||
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
|
||||
)
|
||||
|
||||
if assertion_level := advanced.get(CONF_ASSERTION_LEVEL):
|
||||
for key, flag in ASSERTION_LEVELS.items():
|
||||
@@ -1905,33 +1802,6 @@ async def to_code(config):
|
||||
elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]:
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False)
|
||||
|
||||
# Disable SHA-384 and SHA-512 in mbedTLS
|
||||
# ESPHome doesn't use either algorithm. SHA-384 shares the same
|
||||
# compression function as SHA-512 (mbedtls_internal_sha512_process),
|
||||
# so both must be disabled to eliminate the ~3KB software fallback
|
||||
# that IDF 6.0's PSA parallel engine always links in.
|
||||
# On IDF < 6.0 these are a single config and hardware-only (no
|
||||
# software fallback), so there was no code size cost to leaving
|
||||
# them enabled.
|
||||
# Components that need SHA-384/SHA-512 can call require_mbedtls_sha512()
|
||||
if idf_version() >= cv.Version(6, 0, 0) and not CORE.data[KEY_ESP32].get(
|
||||
KEY_MBEDTLS_SHA512_REQUIRED, False
|
||||
):
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False)
|
||||
|
||||
# Disable PicolibC Newlib compatibility shim on IDF 6.0+
|
||||
# IDF 6.0 switched from Newlib to PicolibC. The shim provides thread-local
|
||||
# stdin/stdout/stderr and getreent() for code compiled against Newlib.
|
||||
# ESPHome doesn't link against Newlib-built libraries that use stdio.
|
||||
# If a component needs it (e.g. precompiled Newlib binaries), re-enable via:
|
||||
# esp32:
|
||||
# framework:
|
||||
# sdkconfig_options:
|
||||
# CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY: "y"
|
||||
if idf_version() >= cv.Version(6, 0, 0):
|
||||
add_idf_sdkconfig_option("CONFIG_LIBC_PICOLIBC_NEWLIB_COMPATIBILITY", False)
|
||||
|
||||
# Disable regi2c control functions in IRAM
|
||||
# Only needed if using analog peripherals (ADC, DAC, etc.) from ISRs while cache is disabled
|
||||
if advanced[CONF_DISABLE_REGI2C_IN_IRAM]:
|
||||
@@ -1966,175 +1836,45 @@ async def to_code(config):
|
||||
CORE.add_job(_write_arduino_libraries_sdkconfig)
|
||||
|
||||
|
||||
KEY_CUSTOM_PARTITIONS = "custom_partitions"
|
||||
|
||||
|
||||
@dataclass
|
||||
class PartitionEntry:
|
||||
name: str
|
||||
type: str
|
||||
subtype: str
|
||||
size: int
|
||||
|
||||
|
||||
# Partition sizes (offsets auto-placed by gen_esp32part.py).
|
||||
# These constants are the single source of truth — used in both
|
||||
# the CSV generation and the overhead calculation.
|
||||
BOOTLOADER_SIZE = 0x8000
|
||||
PARTITION_TABLE_SIZE = 0x1000
|
||||
FIRST_PARTITION_OFFSET = BOOTLOADER_SIZE + PARTITION_TABLE_SIZE
|
||||
OTADATA_SIZE = 0x2000
|
||||
PHY_INIT_SIZE = 0x1000
|
||||
EEPROM_SIZE = 0x1000 # Arduino only
|
||||
SPIFFS_SIZE = 0xF000 # Arduino only
|
||||
ARDUINO_NVS_SIZE = 0x60000
|
||||
IDF_NVS_SIZE = 0x70000
|
||||
|
||||
|
||||
def _get_partition_overhead() -> int:
|
||||
"""Total non-app partition budget (system partitions + nvs + padding).
|
||||
|
||||
Custom partitions are appended at the end and steal from app.
|
||||
"""
|
||||
# otadata + phy_init are followed by app0 which requires 64KB alignment,
|
||||
# so pad up to the next 64KB boundary.
|
||||
overhead = (
|
||||
FIRST_PARTITION_OFFSET + OTADATA_SIZE + PHY_INIT_SIZE + 0xFFFF
|
||||
) & ~0xFFFF
|
||||
if CORE.using_arduino:
|
||||
overhead += EEPROM_SIZE + SPIFFS_SIZE + ARDUINO_NVS_SIZE
|
||||
else:
|
||||
overhead += IDF_NVS_SIZE
|
||||
return overhead
|
||||
|
||||
|
||||
VALID_SUBTYPES: dict[str, set[str]] = {
|
||||
"app": VALID_APP_SUBTYPES,
|
||||
"data": VALID_DATA_SUBTYPES,
|
||||
APP_PARTITION_SIZES = {
|
||||
"2MB": 0x0C0000, # 768 KB
|
||||
"4MB": 0x1C0000, # 1792 KB
|
||||
"8MB": 0x3C0000, # 3840 KB
|
||||
"16MB": 0x7C0000, # 7936 KB
|
||||
"32MB": 0xFC0000, # 16128 KB
|
||||
}
|
||||
|
||||
|
||||
def _validate_partition(
|
||||
name: str, p_type: str | int, subtype: str | int, size: int
|
||||
) -> None:
|
||||
"""Validate partition parameters. Raises ValueError on invalid input."""
|
||||
if name in RESERVED_PARTITION_NAMES:
|
||||
raise ValueError(f"Partition name '{name}' is reserved.")
|
||||
if size % 0x1000 != 0:
|
||||
raise ValueError("Partition size must be 4KB (0x1000) aligned.")
|
||||
# Numeric or already-normalized hex types/subtypes skip string validation
|
||||
if not isinstance(p_type, str) or p_type.startswith("0x"):
|
||||
return
|
||||
if p_type not in VALID_SUBTYPES:
|
||||
raise ValueError(
|
||||
f"Type '{p_type}' is invalid. Only 'app' and 'data' are allowed."
|
||||
" Use numbers for custom types."
|
||||
)
|
||||
if not isinstance(subtype, str) or subtype.startswith("0x"):
|
||||
return
|
||||
valid = VALID_SUBTYPES[p_type]
|
||||
if subtype not in valid:
|
||||
raise ValueError(
|
||||
f"Subtype '{subtype}' is invalid for {p_type} type."
|
||||
f" Only {', '.join(sorted(valid))} are allowed."
|
||||
" Use numbers for custom subtypes."
|
||||
)
|
||||
def get_arduino_partition_csv(flash_size: str):
|
||||
app_partition_size = APP_PARTITION_SIZES[flash_size]
|
||||
eeprom_partition_size = 0x1000 # 4 KB
|
||||
spiffs_partition_size = 0xF000 # 60 KB
|
||||
|
||||
app0_partition_start = 0x010000 # 64 KB
|
||||
app1_partition_start = app0_partition_start + app_partition_size
|
||||
eeprom_partition_start = app1_partition_start + app_partition_size
|
||||
spiffs_partition_start = eeprom_partition_start + eeprom_partition_size
|
||||
|
||||
return f"""\
|
||||
nvs, data, nvs, 0x9000, 0x5000,
|
||||
otadata, data, ota, 0xE000, 0x2000,
|
||||
app0, app, ota_0, 0x{app0_partition_start:X}, 0x{app_partition_size:X},
|
||||
app1, app, ota_1, 0x{app1_partition_start:X}, 0x{app_partition_size:X},
|
||||
eeprom, data, 0x99, 0x{eeprom_partition_start:X}, 0x{eeprom_partition_size:X},
|
||||
spiffs, data, spiffs, 0x{spiffs_partition_start:X}, 0x{spiffs_partition_size:X}
|
||||
"""
|
||||
|
||||
|
||||
def add_partition(name: str, p_type: str | int, subtype: str | int, size: int) -> None:
|
||||
"""Register a custom partition to be appended to the partition table.
|
||||
def get_idf_partition_csv(flash_size: str):
|
||||
app_partition_size = APP_PARTITION_SIZES[flash_size]
|
||||
|
||||
Called from component to_code() to request additional flash partitions.
|
||||
Size must be 4KB aligned. Integer types/subtypes are converted to hex strings.
|
||||
"""
|
||||
if name in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}):
|
||||
raise ValueError(f"Partition name '{name}' is already defined.")
|
||||
_validate_partition(name, p_type, subtype, size)
|
||||
p_type_str = f"0x{p_type:X}" if isinstance(p_type, int) else p_type
|
||||
subtype_str = f"0x{subtype:X}" if isinstance(subtype, int) else subtype
|
||||
custom_partitions = CORE.data[KEY_ESP32].setdefault(KEY_CUSTOM_PARTITIONS, {})
|
||||
custom_partitions[name] = PartitionEntry(
|
||||
name=name, type=p_type_str, subtype=subtype_str, size=size
|
||||
)
|
||||
|
||||
|
||||
def _flash_size_to_bytes(flash_size_mb: str) -> int:
|
||||
"""Convert flash size string (e.g. '4MB') to bytes."""
|
||||
return int(flash_size_mb.removesuffix("MB")) * 1024 * 1024
|
||||
|
||||
|
||||
def _get_custom_partitions_total_size() -> int:
|
||||
"""Total size of custom partitions including alignment padding."""
|
||||
size = 0
|
||||
for partition in CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values():
|
||||
if partition.type == "app":
|
||||
size = (size + 0xFFFF) & ~0xFFFF # align to 64KB
|
||||
size += partition.size
|
||||
return size
|
||||
|
||||
|
||||
def _get_app_partition_size(flash_size_mb: str) -> int:
|
||||
flash_bytes = _flash_size_to_bytes(flash_size_mb)
|
||||
custom_total = _get_custom_partitions_total_size()
|
||||
# Align down to 64KB — app partitions require 64KB-aligned offsets,
|
||||
# so the size must also be aligned to avoid unbudgeted padding.
|
||||
raw_size = (flash_bytes - _get_partition_overhead() - custom_total) // 2
|
||||
app_size = raw_size & ~0xFFFF
|
||||
wasted = (raw_size - app_size) * 2
|
||||
if wasted:
|
||||
_LOGGER.info(
|
||||
"Custom partitions cause %dKB of wasted flash due to 64KB app partition alignment.",
|
||||
wasted // 1024,
|
||||
)
|
||||
if app_size <= 0x10000: # 64 KB
|
||||
raise ValueError(
|
||||
"Custom partitions are too large to fit in the available flash size. "
|
||||
"Reduce custom partition sizes."
|
||||
)
|
||||
if app_size <= 0x80000: # 512 KB
|
||||
_LOGGER.warning(
|
||||
"App partition size is only %dKB. This may be too small for firmware with "
|
||||
"many components. Consider reducing custom partition sizes or using a "
|
||||
"larger flash chip.",
|
||||
app_size // 1024,
|
||||
)
|
||||
return app_size
|
||||
|
||||
|
||||
def get_partition_csv(flash_size_mb: str) -> str:
|
||||
app_size = _get_app_partition_size(flash_size_mb)
|
||||
|
||||
partitions: list[PartitionEntry] = [
|
||||
PartitionEntry(name="otadata", type="data", subtype="ota", size=OTADATA_SIZE),
|
||||
PartitionEntry(name="phy_init", type="data", subtype="phy", size=PHY_INIT_SIZE),
|
||||
PartitionEntry(name="app0", type="app", subtype="ota_0", size=app_size),
|
||||
PartitionEntry(name="app1", type="app", subtype="ota_1", size=app_size),
|
||||
]
|
||||
if CORE.using_arduino:
|
||||
partitions.append(
|
||||
PartitionEntry(name="eeprom", type="data", subtype="0x99", size=EEPROM_SIZE)
|
||||
)
|
||||
partitions.append(
|
||||
PartitionEntry(
|
||||
name="spiffs", type="data", subtype="spiffs", size=SPIFFS_SIZE
|
||||
)
|
||||
)
|
||||
partitions.append(
|
||||
PartitionEntry(
|
||||
name="nvs", type="data", subtype="nvs", size=ARDUINO_NVS_SIZE
|
||||
)
|
||||
)
|
||||
else:
|
||||
partitions.append(
|
||||
PartitionEntry(name="nvs", type="data", subtype="nvs", size=IDF_NVS_SIZE)
|
||||
)
|
||||
partitions.extend(CORE.data[KEY_ESP32].get(KEY_CUSTOM_PARTITIONS, {}).values())
|
||||
|
||||
csv = "".join(
|
||||
f"{p.name}, {p.type}, {p.subtype}, , 0x{p.size:X},\n" for p in partitions
|
||||
)
|
||||
_LOGGER.debug("Partition table:\n%s", csv)
|
||||
return csv
|
||||
return f"""\
|
||||
otadata, data, ota, , 0x2000,
|
||||
phy_init, data, phy, , 0x1000,
|
||||
app0, app, ota_0, , 0x{app_partition_size:X},
|
||||
app1, app, ota_1, , 0x{app_partition_size:X},
|
||||
nvs, data, nvs, , 0x6D000,
|
||||
"""
|
||||
|
||||
|
||||
def _format_sdkconfig_val(value: SdkconfigValueType) -> str:
|
||||
@@ -2241,10 +1981,16 @@ def copy_files():
|
||||
|
||||
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
|
||||
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
get_partition_csv(flash_size),
|
||||
)
|
||||
if CORE.using_arduino:
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
get_arduino_partition_csv(flash_size),
|
||||
)
|
||||
else:
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
get_idf_partition_csv(flash_size),
|
||||
)
|
||||
# IDF build scripts look for version string to put in the build.
|
||||
# However, if the build path does not have an initialized git repo,
|
||||
# and no version.txt file exists, the CMake script fails for some setups.
|
||||
|
||||
@@ -15,7 +15,6 @@ KEY_PATH = "path"
|
||||
KEY_SUBMODULES = "submodules"
|
||||
KEY_EXTRA_BUILD_FILES = "extra_build_files"
|
||||
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
|
||||
KEY_IDF_VERSION = "idf_version"
|
||||
|
||||
VARIANT_ESP32 = "ESP32"
|
||||
VARIANT_ESP32C2 = "ESP32C2"
|
||||
|
||||
@@ -53,6 +53,9 @@ void arch_init() {
|
||||
}
|
||||
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
|
||||
|
||||
uint8_t progmem_read_byte(const uint8_t *addr) { return *addr; }
|
||||
const char *progmem_read_ptr(const char *const *addr) { return *addr; }
|
||||
uint16_t progmem_read_uint16(const uint16_t *addr) { return *addr; }
|
||||
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
|
||||
uint32_t arch_get_cpu_freq_hz() {
|
||||
uint32_t freq = 0;
|
||||
|
||||
@@ -14,11 +14,18 @@
|
||||
|
||||
namespace esphome {
|
||||
|
||||
uint32_t random_uint32() { return esp_random(); }
|
||||
bool random_bytes(uint8_t *data, size_t len) {
|
||||
esp_fill_random(data, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
|
||||
Mutex::~Mutex() {}
|
||||
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
|
||||
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
|
||||
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
|
||||
|
||||
// only affects the executing core
|
||||
// so should not be used as a mutex lock, only to get accurate timing
|
||||
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
class ESP32PreferenceBackend final {
|
||||
public:
|
||||
bool save(const uint8_t *data, size_t len);
|
||||
bool load(uint8_t *data, size_t len);
|
||||
|
||||
uint32_t key;
|
||||
uint32_t nvs_handle;
|
||||
};
|
||||
|
||||
class ESP32Preferences;
|
||||
ESP32Preferences *get_preferences();
|
||||
|
||||
} // namespace esphome::esp32
|
||||
|
||||
namespace esphome {
|
||||
using PreferenceBackend = esp32::ESP32PreferenceBackend;
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
@@ -1,16 +1,18 @@
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "preferences.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include <nvs_flash.h>
|
||||
#include <cinttypes>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome::esp32 {
|
||||
namespace esphome {
|
||||
namespace esp32 {
|
||||
|
||||
static const char *const TAG = "preferences";
|
||||
static const char *const TAG = "esp32.preferences";
|
||||
|
||||
// Buffer size for converting uint32_t to string: max "4294967295" (10 chars) + null terminator + 1 padding
|
||||
static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
@@ -22,175 +24,185 @@ struct NVSData {
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
public:
|
||||
uint32_t key;
|
||||
uint32_t nvs_handle;
|
||||
bool save(const uint8_t *data, size_t len) override {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
return true;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
if (actual_len != len) {
|
||||
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
|
||||
return false;
|
||||
}
|
||||
err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return false;
|
||||
} else {
|
||||
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ESP32Preferences::open() {
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err == 0)
|
||||
return;
|
||||
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
nvs_flash_init();
|
||||
|
||||
err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
|
||||
if (err != 0) {
|
||||
this->nvs_handle = 0;
|
||||
}
|
||||
}
|
||||
|
||||
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
|
||||
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->nvs_handle = this->nvs_handle;
|
||||
pref->key = type;
|
||||
|
||||
return ESPPreferenceObject(pref);
|
||||
}
|
||||
|
||||
bool ESP32Preferences::sync() {
|
||||
if (s_pending_save.empty())
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
bool load(uint8_t *data, size_t len) override {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||
int cached = 0, written = 0, failed = 0;
|
||||
esp_err_t last_err = ESP_OK;
|
||||
uint32_t last_key = 0;
|
||||
|
||||
for (const auto &save : s_pending_save) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
continue;
|
||||
}
|
||||
written++;
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, this->key);
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(this->nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
if (actual_len != len) {
|
||||
ESP_LOGVV(TAG, "NVS length does not match (%zu!=%zu)", actual_len, len);
|
||||
return false;
|
||||
}
|
||||
err = nvs_get_blob(this->nvs_handle, key_str, data, &len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return false;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
cached++;
|
||||
ESP_LOGVV(TAG, "nvs_get_blob: key: %s, len: %zu", key_str, len);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
class ESP32Preferences : public ESPPreferences {
|
||||
public:
|
||||
uint32_t nvs_handle;
|
||||
|
||||
void open() {
|
||||
nvs_flash_init();
|
||||
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &nvs_handle);
|
||||
if (err == 0)
|
||||
return;
|
||||
|
||||
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
nvs_flash_init();
|
||||
|
||||
err = nvs_open("esphome", NVS_READWRITE, &nvs_handle);
|
||||
if (err != 0) {
|
||||
nvs_handle = 0;
|
||||
}
|
||||
}
|
||||
s_pending_save.clear();
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
|
||||
return this->make_preference(length, type);
|
||||
}
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
|
||||
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
|
||||
pref->nvs_handle = this->nvs_handle;
|
||||
pref->key = type;
|
||||
|
||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||
failed);
|
||||
if (failed > 0) {
|
||||
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err),
|
||||
last_key);
|
||||
return ESPPreferenceObject(pref);
|
||||
}
|
||||
|
||||
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
|
||||
esp_err_t err = nvs_commit(this->nvs_handle);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
bool sync() override {
|
||||
if (s_pending_save.empty())
|
||||
return true;
|
||||
|
||||
ESP_LOGV(TAG, "Saving %zu items...", s_pending_save.size());
|
||||
int cached = 0, written = 0, failed = 0;
|
||||
esp_err_t last_err = ESP_OK;
|
||||
uint32_t last_key = 0;
|
||||
|
||||
for (const auto &save : s_pending_save) {
|
||||
char key_str[KEY_BUFFER_SIZE];
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
continue;
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
cached++;
|
||||
}
|
||||
}
|
||||
s_pending_save.clear();
|
||||
|
||||
ESP_LOGD(TAG, "Writing %d items: %d cached, %d written, %d failed", cached + written + failed, cached, written,
|
||||
failed);
|
||||
if (failed > 0) {
|
||||
ESP_LOGE(TAG, "Writing %d items failed. Last error=%s for key=%" PRIu32, failed, esp_err_to_name(last_err),
|
||||
last_key);
|
||||
}
|
||||
|
||||
// note: commit on esp-idf currently is a no-op, nvs_set_blob always writes
|
||||
esp_err_t err = nvs_commit(this->nvs_handle);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_commit() failed: %s", esp_err_to_name(err));
|
||||
return false;
|
||||
}
|
||||
|
||||
return failed == 0;
|
||||
}
|
||||
|
||||
return failed == 0;
|
||||
}
|
||||
protected:
|
||||
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.data.size()) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
SmallBufferWithHeapFallback<256> stored_data(actual_len);
|
||||
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
||||
}
|
||||
|
||||
bool ESP32Preferences::is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str) {
|
||||
size_t actual_len;
|
||||
esp_err_t err = nvs_get_blob(nvs_handle, key_str, nullptr, &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s'): %s - the key might not be set yet", key_str, esp_err_to_name(err));
|
||||
bool reset() override {
|
||||
ESP_LOGD(TAG, "Erasing storage");
|
||||
s_pending_save.clear();
|
||||
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
// Make the handle invalid to prevent any saves until restart
|
||||
nvs_handle = 0;
|
||||
return true;
|
||||
}
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.data.size()) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
SmallBufferWithHeapFallback<256> stored_data(actual_len);
|
||||
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
||||
}
|
||||
|
||||
bool ESP32Preferences::reset() {
|
||||
ESP_LOGD(TAG, "Erasing storage");
|
||||
s_pending_save.clear();
|
||||
|
||||
nvs_flash_deinit();
|
||||
nvs_flash_erase();
|
||||
// Make the handle invalid to prevent any saves until restart
|
||||
this->nvs_handle = 0;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
static ESP32Preferences s_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
ESP32Preferences *get_preferences() { return &s_preferences; }
|
||||
|
||||
void setup_preferences() {
|
||||
s_preferences.open();
|
||||
global_preferences = &s_preferences;
|
||||
}
|
||||
|
||||
} // namespace esphome::esp32
|
||||
} // namespace esp32
|
||||
|
||||
namespace esphome {
|
||||
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -1,33 +1,12 @@
|
||||
#pragma once
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include "esphome/core/preference_backend.h"
|
||||
|
||||
namespace esphome::esp32 {
|
||||
|
||||
struct NVSData;
|
||||
|
||||
class ESP32Preferences final : public PreferencesMixin<ESP32Preferences> {
|
||||
public:
|
||||
using PreferencesMixin<ESP32Preferences>::make_preference;
|
||||
void open();
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) {
|
||||
return this->make_preference(length, type);
|
||||
}
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type);
|
||||
bool sync();
|
||||
bool reset();
|
||||
|
||||
uint32_t nvs_handle;
|
||||
|
||||
protected:
|
||||
bool is_changed_(uint32_t nvs_handle, const NVSData &to_save, const char *key_str);
|
||||
};
|
||||
namespace esphome {
|
||||
namespace esp32 {
|
||||
|
||||
void setup_preferences();
|
||||
|
||||
} // namespace esphome::esp32
|
||||
|
||||
DECLARE_PREFERENCE_ALIASES(esphome::esp32::ESP32Preferences)
|
||||
} // namespace esp32
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
@@ -81,6 +81,8 @@ void ESP32BLE::disable() {
|
||||
this->state_ = BLE_COMPONENT_STATE_DISABLE;
|
||||
}
|
||||
|
||||
bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
|
||||
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
void ESP32BLE::advertising_start() {
|
||||
this->advertising_init_();
|
||||
@@ -573,9 +575,8 @@ template<typename... Args> void enqueue_ble_event(Args... args) {
|
||||
load_ble_event(event, args...);
|
||||
|
||||
// Push the event to the queue
|
||||
// Push always succeeds: pool is sized to queue capacity (N-1), so if
|
||||
// allocate() returned non-null, the queue is guaranteed to have room.
|
||||
global_ble->ble_events_.push(event);
|
||||
// Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
|
||||
}
|
||||
|
||||
// Explicit template instantiations for the friend function
|
||||
|
||||
@@ -135,7 +135,7 @@ class ESP32BLE : public Component {
|
||||
|
||||
void enable();
|
||||
void disable();
|
||||
ESPHOME_ALWAYS_INLINE bool is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
|
||||
bool is_active();
|
||||
void setup() override;
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
@@ -221,13 +221,7 @@ class ESP32BLE : public Component {
|
||||
|
||||
// Large objects (size depends on template parameters, but typically aligned to 4 bytes)
|
||||
esphome::LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
|
||||
// Pool sized to queue capacity (SIZE-1) because LockFreeQueue<T,N> is a ring
|
||||
// buffer that holds N-1 elements (one slot distinguishes full from empty).
|
||||
// This guarantees allocate() returns nullptr before push() can fail, which:
|
||||
// 1. Prevents leaking a pool slot (the Nth allocate succeeds but push fails)
|
||||
// 2. Avoids needing release() on the producer path after a failed push(),
|
||||
// preserving the SPSC contract on the pool's internal free list
|
||||
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE - 1> ble_event_pool_;
|
||||
esphome::EventPool<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_event_pool_;
|
||||
|
||||
// 4-byte aligned members
|
||||
#ifdef USE_ESP32_BLE_ADVERTISING
|
||||
|
||||
@@ -16,9 +16,13 @@ BLECharacteristic::~BLECharacteristic() {
|
||||
for (auto *descriptor : this->descriptors_) {
|
||||
delete descriptor; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
vSemaphoreDelete(this->set_value_lock_);
|
||||
}
|
||||
|
||||
BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties) : uuid_(uuid) {
|
||||
this->set_value_lock_ = xSemaphoreCreateBinary();
|
||||
xSemaphoreGive(this->set_value_lock_);
|
||||
|
||||
this->properties_ = (esp_gatt_char_prop_t) 0;
|
||||
|
||||
this->set_broadcast_property((properties & PROPERTY_BROADCAST) != 0);
|
||||
@@ -31,7 +35,11 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
|
||||
|
||||
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
|
||||
|
||||
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) { this->value_ = std::move(buffer); }
|
||||
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
|
||||
xSemaphoreTake(this->set_value_lock_, 0L);
|
||||
this->value_ = std::move(buffer);
|
||||
xSemaphoreGive(this->set_value_lock_);
|
||||
}
|
||||
|
||||
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
|
||||
this->set_value(std::vector<uint8_t>(data)); // Delegate to move overload
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include <esp_gattc_api.h>
|
||||
#include <esp_gatts_api.h>
|
||||
#include <esp_bt_defs.h>
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/semphr.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace esp32_ble_server {
|
||||
@@ -82,6 +84,8 @@ class BLECharacteristic {
|
||||
|
||||
uint16_t value_read_offset_{0};
|
||||
std::vector<uint8_t> value_;
|
||||
SemaphoreHandle_t set_value_lock_;
|
||||
|
||||
std::vector<BLEDescriptor *> descriptors_;
|
||||
|
||||
struct ClientNotificationEntry {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <algorithm>
|
||||
#include <nvs_flash.h>
|
||||
#include <freertos/FreeRTOSConfig.h>
|
||||
#include <esp_bt_main.h>
|
||||
@@ -38,17 +39,16 @@ void BLEServer::loop() {
|
||||
case RUNNING: {
|
||||
// Start all services that are pending to start
|
||||
if (!this->services_to_start_.empty()) {
|
||||
size_t write_idx = 0;
|
||||
for (auto *service : this->services_to_start_) {
|
||||
for (auto &service : this->services_to_start_) {
|
||||
if (service->is_created()) {
|
||||
service->start(); // Needs to be called once per characteristic in the service
|
||||
}
|
||||
// Remove services that have started or are starting
|
||||
if (!service->is_starting() && !service->is_running()) {
|
||||
this->services_to_start_[write_idx++] = service;
|
||||
}
|
||||
}
|
||||
this->services_to_start_.erase(this->services_to_start_.begin() + write_idx, this->services_to_start_.end());
|
||||
// Remove services that have been started
|
||||
this->services_to_start_.erase(
|
||||
std::remove_if(this->services_to_start_.begin(), this->services_to_start_.end(),
|
||||
[](BLEService *service) { return service->is_starting() || service->is_running(); }),
|
||||
this->services_to_start_.end());
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -91,6 +91,8 @@ void BLEServer::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
bool BLEServer::is_running() { return this->parent_->is_active() && this->state_ == RUNNING; }
|
||||
|
||||
bool BLEServer::can_proceed() { return this->is_running() || !this->parent_->is_active(); }
|
||||
|
||||
void BLEServer::restart_advertising_() {
|
||||
|
||||
@@ -32,7 +32,7 @@ class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEv
|
||||
float get_setup_priority() const override;
|
||||
bool can_proceed() override;
|
||||
|
||||
ESPHOME_ALWAYS_INLINE bool is_running() { return this->parent_->is_active() && this->state_ == RUNNING; }
|
||||
bool is_running();
|
||||
|
||||
void set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->manufacturer_data_ = data;
|
||||
|
||||
@@ -27,14 +27,8 @@
|
||||
#include <esp_coexist.h>
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32_BLE_DEVICE
|
||||
#ifdef USE_BLE_TRACKER_PSA_AES
|
||||
#include <psa/crypto.h>
|
||||
#else
|
||||
#define MBEDTLS_AES_ALT
|
||||
#include <aes_alt.h>
|
||||
#endif
|
||||
#endif // USE_ESP32_BLE_DEVICE
|
||||
|
||||
// bt_trace.h
|
||||
#undef TAG
|
||||
@@ -744,48 +738,23 @@ void ESP32BLETracker::print_bt_device_info(const ESPBTDevice &device) {
|
||||
}
|
||||
|
||||
bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
|
||||
static constexpr size_t AES_BLOCK_SIZE = 16;
|
||||
static constexpr size_t AES_KEY_BITS = 128;
|
||||
|
||||
uint8_t ecb_key[AES_BLOCK_SIZE];
|
||||
uint8_t ecb_plaintext[AES_BLOCK_SIZE];
|
||||
uint8_t ecb_ciphertext[AES_BLOCK_SIZE];
|
||||
uint8_t ecb_key[16];
|
||||
uint8_t ecb_plaintext[16];
|
||||
uint8_t ecb_ciphertext[16];
|
||||
|
||||
uint64_t addr64 = esp32_ble::ble_addr_to_uint64(this->address_);
|
||||
|
||||
memcpy(&ecb_key, irk, AES_BLOCK_SIZE);
|
||||
memset(&ecb_plaintext, 0, AES_BLOCK_SIZE);
|
||||
memcpy(&ecb_key, irk, 16);
|
||||
memset(&ecb_plaintext, 0, 16);
|
||||
|
||||
ecb_plaintext[13] = (addr64 >> 40) & 0xff;
|
||||
ecb_plaintext[14] = (addr64 >> 32) & 0xff;
|
||||
ecb_plaintext[15] = (addr64 >> 24) & 0xff;
|
||||
|
||||
#ifdef USE_BLE_TRACKER_PSA_AES
|
||||
// Use PSA Crypto API (mbedtls 4.0 / IDF 6.0+)
|
||||
psa_key_attributes_t attributes = PSA_KEY_ATTRIBUTES_INIT;
|
||||
psa_set_key_type(&attributes, PSA_KEY_TYPE_AES);
|
||||
psa_set_key_bits(&attributes, AES_KEY_BITS);
|
||||
psa_set_key_usage_flags(&attributes, PSA_KEY_USAGE_ENCRYPT);
|
||||
psa_set_key_algorithm(&attributes, PSA_ALG_ECB_NO_PADDING);
|
||||
|
||||
mbedtls_svc_key_id_t key_id;
|
||||
if (psa_import_key(&attributes, ecb_key, AES_BLOCK_SIZE, &key_id) != PSA_SUCCESS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t output_length;
|
||||
psa_status_t status = psa_cipher_encrypt(key_id, PSA_ALG_ECB_NO_PADDING, ecb_plaintext, AES_BLOCK_SIZE,
|
||||
ecb_ciphertext, AES_BLOCK_SIZE, &output_length);
|
||||
psa_destroy_key(key_id);
|
||||
if (status != PSA_SUCCESS || output_length != AES_BLOCK_SIZE) {
|
||||
return false;
|
||||
}
|
||||
#else
|
||||
// Use legacy mbedtls AES API (IDF < 6.0)
|
||||
mbedtls_aes_context ctx = {0, 0, {0}};
|
||||
mbedtls_aes_init(&ctx);
|
||||
|
||||
if (mbedtls_aes_setkey_enc(&ctx, ecb_key, AES_KEY_BITS) != 0) {
|
||||
if (mbedtls_aes_setkey_enc(&ctx, ecb_key, 128) != 0) {
|
||||
mbedtls_aes_free(&ctx);
|
||||
return false;
|
||||
}
|
||||
@@ -796,7 +765,6 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
|
||||
}
|
||||
|
||||
mbedtls_aes_free(&ctx);
|
||||
#endif
|
||||
|
||||
return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
|
||||
ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
|
||||
|
||||
@@ -12,13 +12,6 @@
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
|
||||
// mbedtls 4.0 (IDF 6.0) removed the legacy mbedtls AES API.
|
||||
// Use the PSA Crypto API instead.
|
||||
#define USE_BLE_TRACKER_PSA_AES
|
||||
#endif
|
||||
|
||||
#include <esp_bt_defs.h>
|
||||
#include <esp_gap_ble_api.h>
|
||||
#include <esp_gattc_api.h>
|
||||
|
||||
@@ -400,7 +400,7 @@ async def to_code(config):
|
||||
if config[CONF_JPEG_QUALITY] != 0 and config[CONF_PIXEL_FORMAT] != "JPEG":
|
||||
cg.add_define("USE_ESP32_CAMERA_JPEG_CONVERSION")
|
||||
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.5")
|
||||
add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
|
||||
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_NEW", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SCCB_HARDWARE_I2C_DRIVER_LEGACY", False)
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user