mirror of
https://github.com/esphome/esphome.git
synced 2026-09-05 12:36:07 +00:00
Compare commits
155
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8d43616507 | ||
|
|
a5bbac084a | ||
|
|
44cea5c0ed | ||
|
|
28ab280c7b | ||
|
|
f2f9b9cbff | ||
|
|
f32e3e75f4 | ||
|
|
5494190d97 | ||
|
|
36468022ea | ||
|
|
19f5641706 | ||
|
|
900dd63883 | ||
|
|
bca4c9dbba | ||
|
|
e1595cb7ef | ||
|
|
86c10ac0c8 | ||
|
|
7b17651837 | ||
|
|
cb7cfa5340 | ||
|
|
9ca77cd7c7 | ||
|
|
da5f43ce41 | ||
|
|
736218e747 | ||
|
|
6fce48f1d5 | ||
|
|
e3820e62bc | ||
|
|
92e663821c | ||
|
|
da16c01351 | ||
|
|
6099ac7b53 | ||
|
|
b8480b8424 | ||
|
|
8bef5b22e1 | ||
|
|
379e077b5f | ||
|
|
3f68930001 | ||
|
|
0aff9e1c54 | ||
|
|
f9824ee83f | ||
|
|
68ffd5a773 | ||
|
|
fe3788ff47 | ||
|
|
d6758377d1 | ||
|
|
5dbc8ffe4c | ||
|
|
0f982f03b2 | ||
|
|
afb0022dd0 | ||
|
|
081ef3d30d | ||
|
|
ca44db107c | ||
|
|
d437869373 | ||
|
|
c20b619684 | ||
|
|
ebe6c2d049 | ||
|
|
78beb75b3b | ||
|
|
fc9afcb201 | ||
|
|
7a784d1135 | ||
|
|
24af3fd834 | ||
|
|
61e37cfc86 | ||
|
|
ea1e0c6f68 | ||
|
|
1ba1aebfa1 | ||
|
|
5d56517e14 | ||
|
|
ad69718ecc | ||
|
|
06d477bea7 | ||
|
|
bbe806f1e6 | ||
|
|
813c000684 | ||
|
|
2890afe0e5 | ||
|
|
5e58b312e2 | ||
|
|
575a540c9f | ||
|
|
404da26a37 | ||
|
|
be54a5c403 | ||
|
|
8b5c06add6 | ||
|
|
7b97c87390 | ||
|
|
fbe306f00b | ||
|
|
0e4f460013 | ||
|
|
4e0a5e5a5b | ||
|
|
20d4fe1a41 | ||
|
|
8929fc43d8 | ||
|
|
c75252d425 | ||
|
|
ae460b430c | ||
|
|
82ca5365c9 | ||
|
|
a5d8c45b67 | ||
|
|
67f7532940 | ||
|
|
fc977b7b5e | ||
|
|
2f1d3f8299 | ||
|
|
cb54e57d84 | ||
|
|
fb65096ea3 | ||
|
|
0607c228f5 | ||
|
|
b9eda644cd | ||
|
|
a811aa840c | ||
|
|
cd28a8a03e | ||
|
|
6957576867 | ||
|
|
3fea080ed8 | ||
|
|
06bc3d70c2 | ||
|
|
2576a0e340 | ||
|
|
ce163b8258 | ||
|
|
bcec1d6cb8 | ||
|
|
1c44cec343 | ||
|
|
c9848d8fa6 | ||
|
|
0dc0cf83de | ||
|
|
ed3429d372 | ||
|
|
dfac9e1f11 | ||
|
|
7255315ce2 | ||
|
|
346c250901 | ||
|
|
5fc9bff371 | ||
|
|
97f643574c | ||
|
|
e8d76b735b | ||
|
|
03147bc3b1 | ||
|
|
d4348335dd | ||
|
|
246670e22b | ||
|
|
0dce7f4845 | ||
|
|
1623fe0852 | ||
|
|
4333870590 | ||
|
|
cc41fd0beb | ||
|
|
59397b4e28 | ||
|
|
768ab5b672 | ||
|
|
8db07d0de5 | ||
|
|
a1515ec662 | ||
|
|
2031be0c23 | ||
|
|
708b7bfb53 | ||
|
|
7782bc11c6 | ||
|
|
a9a66baeba | ||
|
|
16d0fa1165 | ||
|
|
25d5cd3e14 | ||
|
|
9598449b6b | ||
|
|
a8c7279b3b | ||
|
|
c3c69c730c | ||
|
|
a8c8827a5b | ||
|
|
18b7002604 | ||
|
|
51105a25db | ||
|
|
fb132b4421 | ||
|
|
20b38ac8b0 | ||
|
|
cdd892a526 | ||
|
|
b99e7f5ae2 | ||
|
|
e55a8aeabe | ||
|
|
a8094ed548 | ||
|
|
2104096f02 | ||
|
|
e53870085b | ||
|
|
b62305793d | ||
|
|
a1b29b0d0b | ||
|
|
e458a38f89 | ||
|
|
39177402dd | ||
|
|
3361d031de | ||
|
|
5df1c7f1d3 | ||
|
|
9508165797 | ||
|
|
5878406918 | ||
|
|
fedd46e648 | ||
|
|
d28b17c619 | ||
|
|
f17ef133f3 | ||
|
|
34e7453677 | ||
|
|
8a89f3075f | ||
|
|
5328813814 | ||
|
|
1cbe3a49b2 | ||
|
|
5c79c92c06 | ||
|
|
7b4894da03 | ||
|
|
4eb85a24c2 | ||
|
|
1e7c48e2cf | ||
|
|
6ef13cc8de | ||
|
|
e37a540fb7 | ||
|
|
9baff76520 | ||
|
|
612d58ec37 | ||
|
|
cb981c2930 | ||
|
|
8b8de0c9c6 | ||
|
|
9e4c52989c | ||
|
|
7ff56c62f9 | ||
|
|
370e8fffed | ||
|
|
c272c4c1a6 | ||
|
|
150f75d8f6 | ||
|
|
cf5ec2d277 |
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"hooks": {
|
||||
"SessionStart": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "d=\"${CLAUDE_PROJECT_DIR:-.}\"; [ -x \"$d/venv/bin/python\" ] || { mkdir -p \"$d/.temp\"; env -u VIRTUAL_ENV \"$d/script/setup\" >\"$d/.temp/setup.log\" 2>&1 || echo '{\"systemMessage\":\"script/setup failed; see .temp/setup.log\"}'; }",
|
||||
"statusMessage": "Setting up dev environment (script/setup)...",
|
||||
"timeout": 900
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
name: Cache clang-tidy idedata
|
||||
description: >
|
||||
Cache the clang-tidy idedata and the headers it references under .temp
|
||||
(headers only, about 30MB per env). Run after restore-python and cache-esp-idf.
|
||||
inputs:
|
||||
environment:
|
||||
description: 'clang-tidy environment (e.g. esp32-idf-tidy).'
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Compute cache key
|
||||
id: key
|
||||
shell: bash
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
[ -n "${{ inputs.environment }}" ] || { echo "::error::cache-clang-tidy-idedata: 'environment' input is empty"; exit 1; }
|
||||
hash=$(python -c 'import sys; sys.path.insert(0, "script"); from clang_tidy_hash import idedata_cache_hash; print(idedata_cache_hash("${{ inputs.environment }}"))')
|
||||
pyver=$(python -c 'import platform; print(platform.python_version())')
|
||||
# Generating idedata is what installs ESP-IDF; never skip it over a missing
|
||||
# install. This also skips the save, so a dev run that installs ESP-IDF
|
||||
# warms the idedata cache on the next run.
|
||||
if [ -d ~/.esphome-idf/frameworks ]; then
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "ESP-IDF install missing, not using the clang-tidy idedata cache"
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
echo "key=${{ runner.os }}-tidy-idedata-${{ inputs.environment }}-$hash-py$pyver" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
echo "path<<EOF"
|
||||
printf '%s\n' '.temp/idedata-*.json' '.temp/idedata-*.hash'
|
||||
printf '.temp/**/*.%s\n' h hpp hh hxx inc inl ipp tpp
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
# Mirror cache-esp-idf: write on dev, restore-only on PRs. The post-step
|
||||
# save only runs when the job succeeded, so a failed generation is never saved.
|
||||
# Extend the extension list if a component ships extensionless headers.
|
||||
- name: Cache clang-tidy idedata (write on dev)
|
||||
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && steps.key.outputs.skip != 'true'
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.key.outputs.path }}
|
||||
key: ${{ steps.key.outputs.key }}
|
||||
- name: Cache clang-tidy idedata (restore-only off dev)
|
||||
if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') && steps.key.outputs.skip != 'true'
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ steps.key.outputs.path }}
|
||||
key: ${{ steps.key.outputs.key }}
|
||||
@@ -26,6 +26,9 @@ runs:
|
||||
# The native-IDF version is pinned in code, not in any file that feeds the
|
||||
# other cache keys, so resolve it explicitly. Keying on it means the cache
|
||||
# invalidates on a version bump (actions/cache never overwrites a key).
|
||||
# Also key on the Python version: the cached IDF venv links to the
|
||||
# runner's toolcache interpreter and is reinstalled every run after a
|
||||
# runner image bump.
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -36,19 +39,32 @@ runs:
|
||||
version=$(python -c 'from esphome.components.esp32 import ESP_IDF_FRAMEWORK_VERSION_LOOKUP as L; print(L["recommended"])')
|
||||
fi
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
echo "python-version=$(python -c 'import platform; print(platform.python_version())')" >> "$GITHUB_OUTPUT"
|
||||
# Mirror the adjacent PlatformIO cache: only dev-branch runs write the
|
||||
# shared cache (so it lives in the default-branch scope readable by all
|
||||
# PRs), and PRs are restore-only -- they never push multi-GB artifacts into
|
||||
# their own scope / the repo quota (e.g. on a version-bump PR).
|
||||
# their own scope / the repo quota (e.g. on a version-bump PR). The
|
||||
# ci-cache-write label lets a PR write into its own scope to test the hit path;
|
||||
# that costs about 1GB of the repo cache quota per run, so remove it when done.
|
||||
# -slim: bump when prune-esp-idf changes what it removes; a key is never overwritten.
|
||||
- name: Cache ESP-IDF install (write on dev)
|
||||
if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
|
||||
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && inputs.restore-only != 'true'
|
||||
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.esphome-idf
|
||||
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
|
||||
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim
|
||||
- name: Cache ESP-IDF install (restore-only off dev)
|
||||
if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
|
||||
if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') || inputs.restore-only == 'true'
|
||||
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||
with:
|
||||
path: ~/.esphome-idf
|
||||
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}
|
||||
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim
|
||||
# Install explicitly so the prune below sees the toolchains on a cache miss
|
||||
# too, instead of the install happening inside the first build step.
|
||||
- name: Install ESP-IDF
|
||||
shell: bash
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
python -c 'from esphome.espidf.framework import check_esp_idf_install; check_esp_idf_install("${{ steps.version.outputs.version }}")'
|
||||
- name: Prune ESP-IDF install
|
||||
uses: ./.github/actions/prune-esp-idf
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Prune ESP-IDF install
|
||||
description: >
|
||||
Remove the picolibc sysroots (1.1GB of the 3.9GB install) from the native
|
||||
ESP-IDF toolchains; IDF 5.x links newlib. Skipped when an IDF 6 install is
|
||||
present, which links picolibc (see esp32/__init__.py).
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Prune picolibc
|
||||
shell: bash
|
||||
run: |
|
||||
shopt -s nullglob
|
||||
prefix="${ESPHOME_ESP_IDF_PREFIX:-$HOME/.esphome-idf}"
|
||||
prefix="${prefix/#\~/$HOME}"
|
||||
for fw in "$prefix"/frameworks/*/; do
|
||||
case "$(basename "$fw")" in
|
||||
[6-9].*) echo "IDF $(basename "$fw") installed, keeping picolibc"; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
n=0
|
||||
for dir in "$prefix"/tools/*-esp-elf/*/*-esp-elf/picolibc; do
|
||||
echo "Removing $dir ($(du -sh "$dir" | cut -f1))"
|
||||
rm -rf "$dir"
|
||||
n=$((n + 1))
|
||||
done
|
||||
# The marker rides along in the cache entry so a restored slim tree stays quiet.
|
||||
if [ "$n" -gt 0 ]; then
|
||||
touch "$prefix/.picolibc-pruned"
|
||||
elif [ -d "$prefix/tools" ] && [ ! -f "$prefix/.picolibc-pruned" ]; then
|
||||
echo "::warning::no picolibc sysroots matched under $prefix/tools"
|
||||
fi
|
||||
if [ -d "$prefix" ]; then
|
||||
du -sh "$prefix"
|
||||
fi
|
||||
@@ -119,16 +119,22 @@ jobs:
|
||||
# pushed image) keeps it working for fork PRs, which never push to ghcr.io.
|
||||
- name: Export image for compile-test
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
||||
run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | gzip > compile-test-image.tar.gz
|
||||
# zstd over gzip: docker save is on the critical path for every
|
||||
# compile-test job, and zstd -T0 is multithreaded (export 50s -> 9s).
|
||||
# docker load auto-detects the format; its time is layer extraction,
|
||||
# not decompression, so it is unchanged. shell: bash adds pipefail so
|
||||
# a failed docker save cannot upload a truncated artifact.
|
||||
shell: bash
|
||||
run: docker save "ghcr.io/esphome/esphome-amd64:${{ steps.tag.outputs.tag }}" | zstd -T0 -3 > compile-test-image.tar.zst
|
||||
|
||||
- name: Upload compile-test image artifact
|
||||
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
# The tar is already gzipped, so upload it as-is. archive: false skips
|
||||
# the redundant zip and makes the file name the artifact name (the
|
||||
# `name` input is ignored in that mode).
|
||||
path: compile-test-image.tar.gz
|
||||
# The tar is already compressed, so upload it as-is. archive: false
|
||||
# skips the redundant zip and makes the file name the artifact name
|
||||
# (the `name` input is ignored in that mode).
|
||||
path: compile-test-image.tar.zst
|
||||
retention-days: 1
|
||||
archive: false
|
||||
|
||||
@@ -182,8 +188,6 @@ jobs:
|
||||
contents: read # actions/checkout to load the test configs
|
||||
strategy:
|
||||
fail-fast: false
|
||||
# Modest cap so this smoke test leaves room on the shared runner pool.
|
||||
max-parallel: 8
|
||||
matrix:
|
||||
# One entry per distinct toolchain. ESP32 variants (c3/c6/s2/s3/p4)
|
||||
# share a toolchain bundle, so esp32 is exercised on the base variant
|
||||
@@ -208,9 +212,9 @@ jobs:
|
||||
- name: Download image artifact
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: compile-test-image.tar.gz
|
||||
name: compile-test-image.tar.zst
|
||||
- name: Load image
|
||||
run: docker load --input compile-test-image.tar.gz
|
||||
run: docker load --input compile-test-image.tar.zst
|
||||
- name: Compile ${{ matrix.id }}
|
||||
run: |
|
||||
docker run --rm \
|
||||
|
||||
@@ -92,6 +92,7 @@ jobs:
|
||||
outputs:
|
||||
core-ci: ${{ steps.determine.outputs.core-ci }}
|
||||
integration-tests: ${{ steps.determine.outputs.integration-tests }}
|
||||
integration-run-all: ${{ steps.determine.outputs.integration-run-all }}
|
||||
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
|
||||
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
|
||||
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
|
||||
@@ -152,6 +153,9 @@ jobs:
|
||||
# Extract individual fields
|
||||
echo "core-ci=$(echo "$output" | jq -r '.core_ci')" >> $GITHUB_OUTPUT
|
||||
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
|
||||
# A missing key must fail here, not silently disable the junit upload
|
||||
run_all=$(echo "$output" | jq -r 'if has("integration_run_all") then .integration_run_all else error("integration_run_all missing") end')
|
||||
echo "integration-run-all=${run_all}" >> $GITHUB_OUTPUT
|
||||
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $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
|
||||
@@ -355,6 +359,15 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
|
||||
env:
|
||||
# What the cache steps persist; libdeps is excluded (keyed per xdist
|
||||
# worker and env, it never crosses runs).
|
||||
INTEGRATION_PIO_CACHE_PATH: |
|
||||
~/.esphome-integration-tests/platformio/platforms
|
||||
~/.esphome-integration-tests/platformio/packages
|
||||
~/.esphome-integration-tests/platformio/appstate.json
|
||||
~/.esphome-integration-tests/platformio/.cache
|
||||
~/.esphome-integration-tests/platformio/.esphome.pio.stamp.json
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -373,6 +386,14 @@ jobs:
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- name: Restore integration PlatformIO cache
|
||||
# Native platform + toolchain installed by shared_platformio_cache in
|
||||
# tests/integration/conftest.py; a miss self-heals, so no restore-keys.
|
||||
id: pio-cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
|
||||
key: integration-pio-v1-${{ runner.os }}-py${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'tests/integration/fixtures/cache_init.yaml', 'esphome/components/host/__init__.py') }}
|
||||
- name: Restore Python virtual environment
|
||||
id: cache-venv
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
@@ -410,12 +431,36 @@ jobs:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]')
|
||||
if [ "${#test_files[@]}" -eq 0 ]; then
|
||||
echo "::error::Empty integration test bucket; pytest would collect the whole tree"
|
||||
exit 1
|
||||
fi
|
||||
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
|
||||
pytest -vv --no-cov --tb=native --durations=30 -n auto "${test_files[@]}"
|
||||
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
|
||||
--junitxml=junit-integration.xml "${test_files[@]}"
|
||||
- name: Upload junit timings
|
||||
# Consumed by sync-integration-durations.yml through
|
||||
# script/update_integration_test_durations.py; only full matrix dev
|
||||
# runs produce usable data.
|
||||
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: junit-integration-${{ strategy.job-index }}
|
||||
path: junit-integration.xml
|
||||
if-no-files-found: error
|
||||
# A full cron period of margin for the weekly refresh
|
||||
retention-days: 14
|
||||
- name: Print ccache statistics
|
||||
# esphome stores the PlatformIO ccache under the machine-global cache
|
||||
# dir (see _ccache_env() in esphome/platformio/toolchain.py).
|
||||
run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s
|
||||
- name: Save integration PlatformIO cache
|
||||
# Bucket 0 only; the others would race the same immutable key.
|
||||
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.pio-cache.outputs.cache-hit != 'true'
|
||||
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
|
||||
key: ${{ steps.pio-cache.outputs.cache-primary-key }}
|
||||
|
||||
import-time:
|
||||
name: Check import esphome.__main__ time
|
||||
@@ -536,7 +581,7 @@ jobs:
|
||||
apt-get install -y libc6-dbg
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@4296e51e7041e24dadb86d1d6e8b9320d223dbe8 # v5.0.3
|
||||
uses: CodSpeedHQ/action@373d6868929f444bc08d901fd0eb0ad52a8875ea # v5.2.1
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -649,6 +694,12 @@ jobs:
|
||||
with:
|
||||
framework: arduino
|
||||
|
||||
- name: Cache clang-tidy idedata
|
||||
if: matrix.cache_idf
|
||||
uses: ./.github/actions/cache-clang-tidy-idedata
|
||||
with:
|
||||
environment: esp32-arduino-tidy
|
||||
|
||||
- name: Cache nRF Connect SDK install
|
||||
if: matrix.cache_sdk_nrf
|
||||
uses: ./.github/actions/cache-sdk-nrf
|
||||
@@ -698,6 +749,10 @@ jobs:
|
||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||
|
||||
- name: Prune ESP-IDF install before cache save
|
||||
if: matrix.cache_idf && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
|
||||
uses: ./.github/actions/prune-esp-idf
|
||||
|
||||
- name: Suggested changes
|
||||
run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||
# yamllint disable-line rule:line-length
|
||||
@@ -730,6 +785,11 @@ jobs:
|
||||
- name: Cache ESP-IDF install
|
||||
uses: ./.github/actions/cache-esp-idf
|
||||
|
||||
- name: Cache clang-tidy idedata
|
||||
uses: ./.github/actions/cache-clang-tidy-idedata
|
||||
with:
|
||||
environment: esp32-idf-tidy
|
||||
|
||||
- name: Register problem matchers
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||
@@ -764,6 +824,10 @@ jobs:
|
||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||
|
||||
- name: Prune ESP-IDF install before cache save
|
||||
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
|
||||
uses: ./.github/actions/prune-esp-idf
|
||||
|
||||
- name: Suggested changes
|
||||
run: script/ci-suggest-changes
|
||||
if: always()
|
||||
@@ -809,6 +873,11 @@ jobs:
|
||||
- name: Cache ESP-IDF install
|
||||
uses: ./.github/actions/cache-esp-idf
|
||||
|
||||
- name: Cache clang-tidy idedata
|
||||
uses: ./.github/actions/cache-clang-tidy-idedata
|
||||
with:
|
||||
environment: esp32-idf-tidy
|
||||
|
||||
- name: Register problem matchers
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||
@@ -843,6 +912,10 @@ jobs:
|
||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||
PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
|
||||
|
||||
- name: Prune ESP-IDF install before cache save
|
||||
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
|
||||
uses: ./.github/actions/prune-esp-idf
|
||||
|
||||
- name: Suggested changes
|
||||
run: script/ci-suggest-changes
|
||||
if: always()
|
||||
@@ -866,16 +939,19 @@ jobs:
|
||||
name: Run script/clang-tidy for ESP32 S3
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
|
||||
tidy_environment: esp32s3-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 P4
|
||||
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
|
||||
# so their code paths differ -- lint them under the P4 build too.
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
|
||||
tidy_environment: esp32p4-idf-tidy
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 C6
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32c6-idf-tidy --grep SOC_LP_I2C_SUPPORTED --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
|
||||
tidy_environment: esp32c6-idf-tidy
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
@@ -893,6 +969,11 @@ jobs:
|
||||
- name: Cache ESP-IDF install
|
||||
uses: ./.github/actions/cache-esp-idf
|
||||
|
||||
- name: Cache clang-tidy idedata
|
||||
uses: ./.github/actions/cache-clang-tidy-idedata
|
||||
with:
|
||||
environment: ${{ matrix.tidy_environment }}
|
||||
|
||||
- name: Register problem matchers
|
||||
run: |
|
||||
echo "::add-matcher::.github/workflows/matchers/gcc.json"
|
||||
@@ -926,6 +1007,10 @@ jobs:
|
||||
script/clang-tidy --fix --changed ${{ matrix.options }}
|
||||
fi
|
||||
|
||||
- name: Prune ESP-IDF install before cache save
|
||||
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
|
||||
uses: ./.github/actions/prune-esp-idf
|
||||
|
||||
- name: Suggested changes
|
||||
run: script/ci-suggest-changes
|
||||
if: always()
|
||||
@@ -946,7 +1031,6 @@ jobs:
|
||||
ESPHOME_SDK_NRF_PREFIX: ~/.esphome-sdk-nrf
|
||||
strategy:
|
||||
fail-fast: false
|
||||
max-parallel: ${{ needs.determine-jobs.outputs.release-pr == 'true' && 32 || 16 }}
|
||||
matrix:
|
||||
batch: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
|
||||
steps:
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
@@ -84,6 +84,6 @@ jobs:
|
||||
exit 1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
uses: github/codeql-action/analyze@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
name: Refresh integration test durations
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "45 5 * * 1"
|
||||
|
||||
# Repo writes (branch push, PR open) happen via the App token minted below,
|
||||
# so the workflow's GITHUB_TOKEN does not need any write scopes.
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read # gh api / gh run download for the CI junit artifacts
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
name: Refresh integration test durations
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'esphome/esphome'
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
|
||||
with:
|
||||
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
|
||||
permission-contents: write # push the sync branch
|
||||
permission-pull-requests: write # open or refresh the sync PR
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Refresh from the newest usable dev run
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Only full matrix dev runs upload junit-integration-* artifacts
|
||||
# (see the integration-tests job); the merge script re-checks
|
||||
# coverage regardless.
|
||||
# Newest-first candidates via their bucket-0 artifact. Fork PRs run
|
||||
# their own ci.yml, so name and branch are spoofable; require
|
||||
# same-repo. Assignment failures trip set -e and fail loudly.
|
||||
candidates=$(
|
||||
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=junit-integration-0&per_page=100" \
|
||||
--jq '.artifacts[] | select(.expired | not) | .workflow_run
|
||||
| select(.head_branch == "dev" and .head_repository_id != null
|
||||
and .head_repository_id == .repository_id)
|
||||
| .id'
|
||||
)
|
||||
# Green runs first, then the rest newest first; a run missing a
|
||||
# bucket fails the coverage check and the next one is tried
|
||||
green=""
|
||||
rest=""
|
||||
for id in ${candidates}; do
|
||||
conclusion=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${id}" --jq '.conclusion // ""')
|
||||
if [ "${conclusion}" = "success" ]; then
|
||||
green="${green} ${id}"
|
||||
elif [ -n "${conclusion}" ]; then
|
||||
rest="${rest} ${id}"
|
||||
fi
|
||||
done
|
||||
# helpers.py imports colorama; the script needs nothing else
|
||||
pip install colorama
|
||||
for id in ${green} ${rest}; do
|
||||
rm -rf /tmp/junit
|
||||
if ! gh run download "${id}" --repo "${GITHUB_REPOSITORY}" -p "junit-integration-*" -D /tmp/junit; then
|
||||
echo "::warning::Could not download artifacts for run ${id}; trying the next"
|
||||
continue
|
||||
fi
|
||||
status=0
|
||||
python script/update_integration_test_durations.py /tmp/junit || status=$?
|
||||
if [ "${status}" -eq 0 ]; then
|
||||
echo "Refreshed from run ${id}"
|
||||
exit 0
|
||||
fi
|
||||
# Only EXIT_LOW_COVERAGE (3) from the script advances to the next run
|
||||
[ "${status}" -eq 3 ] || exit 1
|
||||
echo "::warning::Run ${id} covers too few test files; trying the next"
|
||||
done
|
||||
echo "::error::No dev CI run with usable junit artifacts in range; the feed is starved"
|
||||
exit 1
|
||||
|
||||
- name: Commit changes
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
commit-message: "[ci] Refresh integration test durations"
|
||||
committer: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
|
||||
author: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
|
||||
branch: sync/integration-durations
|
||||
delete-branch: true
|
||||
title: "[ci] Refresh integration test durations"
|
||||
body-path: .github/PULL_REQUEST_TEMPLATE.md
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
@@ -44,6 +44,16 @@ This document provides essential context for AI models interacting with this pro
|
||||
|
||||
## 4. Coding Conventions & Style Guide
|
||||
|
||||
**Read the developer documentation before writing a component.** https://developers.esphome.io covers the
|
||||
component lifecycle, the main loop, and the reasoning behind the rules below in far more depth than this
|
||||
file does, and it is the authority when they disagree. The most useful starting points:
|
||||
|
||||
* https://developers.esphome.io/architecture/components/ - component lifecycle, `setup()`, `loop()`,
|
||||
setup priorities, and how a component is registered.
|
||||
* https://developers.esphome.io/architecture/components/advanced/ - choosing between `loop()`,
|
||||
`set_interval`, `set_timeout` and `defer`; waking the loop from another thread; the RAM cost of each.
|
||||
* https://developers.esphome.io/contributing/code/ - contribution rules, public API and breaking changes.
|
||||
|
||||
* **Formatting:**
|
||||
* **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
|
||||
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
|
||||
@@ -142,6 +152,47 @@ This document provides essential context for AI models interacting with this pro
|
||||
* **Indentation:** Use spaces (two per indentation level), not tabs
|
||||
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
|
||||
* **Line length:** Wrap lines at no more than 120 characters
|
||||
* **Timing in `loop()`:** Never call `millis()` in a `loop()` body. The current tick's timestamp is
|
||||
already cached - use `App.get_loop_component_start_time()` (from `esphome/core/application.h`).
|
||||
Only reach for `millis()` when you genuinely need sub-tick resolution inside a long operation.
|
||||
* **The main loop runs every 16 ms.** A rate-limit gate shorter than that does nothing: the check
|
||||
passes on essentially every pass of the loop, so it costs a comparison and buys nothing. Pick an
|
||||
interval comfortably coarser than 16 ms, or drop the gate entirely and accept running every loop.
|
||||
```cpp
|
||||
// Bad - a 10ms gate against a 16ms loop never holds anything back
|
||||
static constexpr uint32_t POLL_INTERVAL_MS = 10;
|
||||
const uint32_t now = millis();
|
||||
if (now - this->last_poll_ < POLL_INTERVAL_MS)
|
||||
return;
|
||||
this->last_poll_ = now;
|
||||
```
|
||||
```cpp
|
||||
// Good - an interval that actually rate limits, off the cached timestamp
|
||||
static constexpr uint32_t POLL_INTERVAL_MS = 100;
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_poll_ < POLL_INTERVAL_MS)
|
||||
return;
|
||||
this->last_poll_ = now;
|
||||
```
|
||||
Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use
|
||||
`set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms:
|
||||
https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb
|
||||
* **Don't override a default with the same value:** if a base class method already returns what you
|
||||
want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a
|
||||
component that wants `DATA` should simply leave it alone.
|
||||
```cpp
|
||||
// Bad - this is exactly what the base class already does
|
||||
float get_setup_priority() const override { return setup_priority::DATA; }
|
||||
```
|
||||
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
|
||||
can be stored in flash rather than RAM.
|
||||
```cpp
|
||||
// Bad
|
||||
ESP_LOGV(TAG, "Key %u %s", key, pressed ? "pressed" : "released");
|
||||
|
||||
// Good
|
||||
ESP_LOGV(TAG, "Key %u %s", key, pressed ? LOG_STR_LITERAL("pressed") : LOG_STR_LITERAL("released"));
|
||||
```
|
||||
* **Constructor parameters vs setters:** Component properties that are both **required** and **invariant**
|
||||
(never change after construction) should be constructor parameters rather than set via setter methods.
|
||||
This makes the dependency explicit and prevents use of the object in an incompletely-initialized state.
|
||||
@@ -562,6 +613,33 @@ This document provides essential context for AI models interacting with this pro
|
||||
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
|
||||
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
|
||||
|
||||
**Listener and child-entity registration lists are the most common case, and the most commonly
|
||||
missed.** A `register_*()` method called once per child at code generation time has a count that
|
||||
is known at compile time, so it should never be a `std::vector`. Use `cg.slot_counter()`: it
|
||||
returns a function that each consumer calls once per slot it will occupy, and after every
|
||||
`to_code` has run it emits the define with the final count. When nothing registers, no define is
|
||||
emitted and the storage plus its registration method compile out entirely.
|
||||
```python
|
||||
# hub component's __init__.py
|
||||
_request_listener_slot = cg.slot_counter("MY_COMPONENT_LISTENER_COUNT")
|
||||
|
||||
|
||||
async def register_listener(hub: MockObj, var: MockObj) -> None:
|
||||
_request_listener_slot()
|
||||
cg.add(hub.register_listener(var))
|
||||
```
|
||||
```cpp
|
||||
#ifdef MY_COMPONENT_LISTENER_COUNT
|
||||
void register_listener(MyComponentListener *listener);
|
||||
#endif
|
||||
protected:
|
||||
#ifdef MY_COMPONENT_LISTENER_COUNT
|
||||
StaticVector<MyComponentListener *, MY_COMPONENT_LISTENER_COUNT> listeners_;
|
||||
#endif
|
||||
```
|
||||
Request slots from `to_code`, not from a job that runs after `CoroPriority.FINAL` - a late
|
||||
request raises rather than silently undercounting.
|
||||
|
||||
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
|
||||
```cpp
|
||||
// Bad - generates STL realloc code (_M_realloc_insert)
|
||||
@@ -599,9 +677,25 @@ This document provides essential context for AI models interacting with this pro
|
||||
```
|
||||
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
|
||||
|
||||
5. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
|
||||
5. **Strings set once from configuration:** Use `StringRef` (`esphome/core/string_ref.h`) rather than
|
||||
`std::string`. Code generation passes a string literal that lives in flash for the life of the
|
||||
program, so storing a `std::string` copies it onto the heap for nothing. `StringRef` is a
|
||||
non-owning pointer plus length; it does not copy, and it must only ever refer to storage that
|
||||
outlives it (a string literal, or a buffer owned elsewhere).
|
||||
```cpp
|
||||
// Bad - heap copy of a literal that is already in flash
|
||||
void set_keys(std::string keys) { this->keys_ = std::move(keys); }
|
||||
std::string keys_;
|
||||
```
|
||||
```cpp
|
||||
// Good - no allocation
|
||||
void set_keys(const char *keys) { this->keys_ = StringRef(keys); }
|
||||
StringRef keys_;
|
||||
```
|
||||
|
||||
6. **Detection:** Look for these patterns in compiler output:
|
||||
6. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
|
||||
|
||||
7. **Detection:** Look for these patterns in compiler output:
|
||||
- Large code sections with STL symbols (vector, map, set)
|
||||
- `alloc`, `realloc`, `dealloc` in symbol names
|
||||
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
|
||||
|
||||
@@ -131,6 +131,7 @@ esphome/components/cst816/* @clydebarrow
|
||||
esphome/components/cst9220/* @clydebarrow
|
||||
esphome/components/ct_clamp/* @jesserockz
|
||||
esphome/components/current_based/* @djwmarcx
|
||||
esphome/components/d01/* @ch604
|
||||
esphome/components/dac7678/* @NickB1
|
||||
esphome/components/daikin_arc/* @MagicBear
|
||||
esphome/components/daikin_brc/* @hagak
|
||||
@@ -148,6 +149,7 @@ esphome/components/display_menu_base/* @numo68
|
||||
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
|
||||
esphome/components/dps310/* @kbx81
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/ds1603l/* @JakeLC15
|
||||
esphome/components/ds2484/* @mrk-its
|
||||
esphome/components/ds248x/* @tomwellnitz
|
||||
esphome/components/dsmr/* @glmnet @PolarGoose
|
||||
@@ -350,6 +352,7 @@ esphome/components/mipi_spi/* @clydebarrow
|
||||
esphome/components/mitsubishi/* @RubyBailey
|
||||
esphome/components/mitsubishi_cn105/* @crnjan
|
||||
esphome/components/mixer/speaker/* @kahrendt
|
||||
esphome/components/mk2pvrouter/* @FredM67
|
||||
esphome/components/mlx90393/* @functionpointer
|
||||
esphome/components/mlx90614/* @jesserockz
|
||||
esphome/components/mmc5603/* @benhoff
|
||||
@@ -577,6 +580,7 @@ esphome/components/tuya/select/* @bearpawmaxim
|
||||
esphome/components/tuya/sensor/* @jesserockz
|
||||
esphome/components/tuya/switch/* @jesserockz
|
||||
esphome/components/tuya/text_sensor/* @dentra
|
||||
esphome/components/tuya/water_heater/* @iago-veiga
|
||||
esphome/components/uart/* @esphome/core
|
||||
esphome/components/uart/button/* @ssieb
|
||||
esphome/components/uart/event/* @eoasmxd
|
||||
|
||||
+35
-3
@@ -23,7 +23,8 @@ For this repository there are two trusted inputs by design:
|
||||
1. **The configuration.** Anyone who can supply or edit a YAML config is trusted
|
||||
(see below).
|
||||
2. **Authenticated peers of a running device** — clients holding the device's
|
||||
API encryption key / password, OTA password, or web server credentials.
|
||||
API/OTA encryption key, API password, OTA password, or web server
|
||||
credentials.
|
||||
|
||||
The security boundary is therefore **unauthenticated network traffic vs. those
|
||||
trusted inputs.** A bug that lets an unauthenticated attacker cross it is a
|
||||
@@ -76,8 +77,8 @@ These *are* security bugs in this repo, and we want to hear about them privately
|
||||
captive portal, etc.) **without** valid credentials.
|
||||
- Authentication or encryption bypass on the device — reaching API calls, OTA
|
||||
updates, or the web server without the configured key/password.
|
||||
- Flaws that weaken the device's API encryption (Noise), OTA, or web server auth
|
||||
below their documented guarantees.
|
||||
- Flaws that weaken the device's API or OTA encryption (Noise), OTA auth, or
|
||||
web server auth below their documented guarantees.
|
||||
|
||||
## The web server is an open HTTP API by design
|
||||
|
||||
@@ -121,6 +122,37 @@ and any memory-safety or protocol bug in the server reachable without credential
|
||||
This section documents the current design and scope; it is not a judgment that the
|
||||
design is optimal or that it will not change.
|
||||
|
||||
## OTA update encryption
|
||||
|
||||
The `esphome` OTA platform optionally encrypts updates with the same Noise
|
||||
`NNpsk0` pattern the native API uses; one key protects the device. With an
|
||||
`encryption:` block configured the guarantees are: the firmware image is
|
||||
confidential in transit, the uploader is authenticated by the pre-shared key,
|
||||
and the plaintext negotiation preceding the handshake is bound into the
|
||||
handshake prologue, so stripping or tampering with it fails the first MAC.
|
||||
Both ends fail closed with no override: a device built with a key refuses
|
||||
plaintext uploads, and the CLI refuses to send plaintext when a key is
|
||||
configured.
|
||||
|
||||
Defeating any of that without the key is in scope: a keyed device accepting a
|
||||
plaintext or downgraded upload, getting past the MAC, or recovering image
|
||||
contents from captured traffic.
|
||||
|
||||
The following are **not** vulnerabilities, by design:
|
||||
|
||||
- Plaintext OTA on a device with no `encryption:` block. That is the
|
||||
documented default, authenticated (if at all) by the OTA password.
|
||||
- The enablement window: turning encryption on takes one last upload of the
|
||||
encryption-enabled firmware over the existing plaintext channel, with the
|
||||
pre-existing plaintext exposure.
|
||||
- The web OTA `/update` endpoint alongside encryption. The `web_server`
|
||||
component keeps it always reachable, and `captive_portal:` auto-loads it
|
||||
for the fallback AP window; validation warns about both combinations, and
|
||||
the operator keeps the recovery path.
|
||||
- CLI retry behavior on transport or MAC failures; every attempt renegotiates
|
||||
a fresh handshake with fresh ephemerals, so retrying does not weaken
|
||||
authentication.
|
||||
|
||||
## Explicitly out of scope
|
||||
|
||||
- Local attackers who already have shell access on the host that runs `esphome`.
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
|
||||
CHANNEL_DEV = "dev"
|
||||
@@ -64,7 +65,10 @@ def main():
|
||||
|
||||
suffix = f"-{args.suffix}" if args.suffix else ""
|
||||
|
||||
image_name = f"esphome/esphome{suffix}"
|
||||
repository = (
|
||||
(os.environ.get("GITHUB_REPOSITORY") or "esphome/esphome").strip().lower()
|
||||
)
|
||||
image_name = f"{repository}{suffix}"
|
||||
|
||||
print(f"channel={channel}")
|
||||
|
||||
|
||||
+53
-26
@@ -26,7 +26,9 @@ from esphome.const import (
|
||||
CONF_DEASSERT_RTS_DTR,
|
||||
CONF_DISABLED,
|
||||
CONF_DISCOVER_IP,
|
||||
CONF_ENCRYPTION,
|
||||
CONF_ESPHOME,
|
||||
CONF_KEY,
|
||||
CONF_LEVEL,
|
||||
CONF_LOG,
|
||||
CONF_LOG_TOPIC,
|
||||
@@ -1336,6 +1338,19 @@ def _upload_via_native_api(
|
||||
|
||||
remote_port = int(ota_conf[CONF_PORT])
|
||||
password = ota_conf.get(CONF_PASSWORD)
|
||||
# Fail closed: an encryption block whose key did not resolve must never
|
||||
# fall back to a plaintext upload
|
||||
noise_psk = None
|
||||
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
|
||||
noise_psk = encryption_conf.get(CONF_KEY)
|
||||
if not noise_psk:
|
||||
raise EsphomeError(
|
||||
"OTA encryption is configured but no key was resolved; "
|
||||
"set the key under 'ota: encryption:' or 'api: encryption:'"
|
||||
)
|
||||
# Ensure the key is a string, as required by the underlying OTA implementation.
|
||||
# It arrives here as a SensitiveStr which aioesphomeapi rejects.
|
||||
noise_psk = str(noise_psk)
|
||||
|
||||
def check_partition_access(option_string: str) -> None:
|
||||
if not ota_conf.get("allow_partition_access"):
|
||||
@@ -1366,7 +1381,9 @@ def _upload_via_native_api(
|
||||
if ota_type == espota2.OTA_TYPE_UPDATE_BOOTLOADER:
|
||||
_validate_bootloader_binary(binary)
|
||||
|
||||
return espota2.run_ota(network_devices, remote_port, password, binary, ota_type)
|
||||
return espota2.run_ota(
|
||||
network_devices, remote_port, password, binary, ota_type, noise_psk
|
||||
)
|
||||
|
||||
|
||||
def _upload_via_web_server(
|
||||
@@ -1375,6 +1392,16 @@ def _upload_via_web_server(
|
||||
from esphome import web_server_ota
|
||||
from esphome.web_server_helpers import get_web_server_connection
|
||||
|
||||
if any(
|
||||
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
|
||||
and ota_item.get(CONF_ENCRYPTION) is not None
|
||||
for ota_item in config.get(CONF_OTA, [])
|
||||
):
|
||||
_LOGGER.warning(
|
||||
"This config has OTA encryption, but the web_server OTA path sends "
|
||||
"the image over plaintext HTTP; use the esphome OTA platform to "
|
||||
"keep it confidential"
|
||||
)
|
||||
remote_port, username, password = get_web_server_connection(config)
|
||||
return web_server_ota.run_ota(
|
||||
network_devices, remote_port, username, password, binary
|
||||
@@ -1670,20 +1697,26 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
if CORE.is_host:
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
program_path = str(toolchain.get_elf_path())
|
||||
else:
|
||||
from esphome.platformio.toolchain import get_idedata
|
||||
|
||||
program_path = str(get_idedata(config).firmware_elf_path)
|
||||
_LOGGER.info("Successfully compiled program to path '%s'", program_path)
|
||||
_LOGGER.info(
|
||||
"Successfully compiled program to path '%s'", _host_program_path(config)
|
||||
)
|
||||
else:
|
||||
_LOGGER.info("Successfully compiled program.")
|
||||
return 0
|
||||
|
||||
|
||||
def _host_program_path(config: ConfigType) -> str:
|
||||
"""Return the compiled host ELF path."""
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
return str(toolchain.get_elf_path())
|
||||
from esphome.platformio.toolchain import get_idedata
|
||||
|
||||
# Memoized by compile_program's own call; this is a dict lookup
|
||||
return str(get_idedata(config).firmware_elf_path)
|
||||
|
||||
|
||||
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
# Get devices, resolving special identifiers like OTA
|
||||
devices = choose_upload_log_host(
|
||||
@@ -1728,14 +1761,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
return exit_code
|
||||
_LOGGER.info("Successfully compiled program.")
|
||||
if CORE.is_host:
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain
|
||||
|
||||
program_path = str(toolchain.get_elf_path())
|
||||
else:
|
||||
from esphome.platformio.toolchain import get_idedata
|
||||
|
||||
program_path = str(get_idedata(config).firmware_elf_path)
|
||||
program_path = _host_program_path(config)
|
||||
_LOGGER.info("Running program from path '%s'", program_path)
|
||||
return run_external_process(program_path)
|
||||
|
||||
@@ -2734,10 +2760,14 @@ def run_esphome(argv):
|
||||
# Skipped when -s overrides are passed, since the cache was written
|
||||
# against the previous substitution set.
|
||||
config: ConfigType | None = None
|
||||
cache_eligible = (
|
||||
cache_write_eligible = (
|
||||
args.command in ("upload", "logs") and not command_line_substitutions
|
||||
)
|
||||
if cache_eligible:
|
||||
# An explicit --toolchain must re-run the per-platform validators, so
|
||||
# gate only the cache read; the refresh below saves the result unless
|
||||
# the sidecar records a different toolchain.
|
||||
cache_read_eligible = cache_write_eligible and args.toolchain is None
|
||||
if cache_read_eligible:
|
||||
from esphome.compiled_config import load_compiled_config
|
||||
|
||||
config = load_compiled_config(conf_path)
|
||||
@@ -2761,17 +2791,14 @@ def run_esphome(argv):
|
||||
return 2
|
||||
CORE.config = config
|
||||
|
||||
# Fallback for platforms whose validators didn't set the toolchain
|
||||
# (only the esp32 component reads esp32.framework.toolchain). All
|
||||
# other platforms only support PlatformIO today. Must run before the
|
||||
# cache refresh below so its sidecar records the same toolchain a
|
||||
# compile would.
|
||||
# The cache fast path skips validation, and legacy sidecars lack the
|
||||
# toolchain field. Must run before the cache refresh below.
|
||||
if CORE.toolchain is None:
|
||||
CORE.toolchain = Toolchain.PLATFORMIO
|
||||
|
||||
# Refresh the cache so the next upload/logs hits the fast path
|
||||
# instead of re-running read_config.
|
||||
if cache_eligible and cache_missed:
|
||||
if cache_write_eligible and cache_missed:
|
||||
from esphome.compiled_config import save_compiled_config_and_sidecar
|
||||
|
||||
save_compiled_config_and_sidecar(config)
|
||||
|
||||
@@ -0,0 +1,531 @@
|
||||
"""Arduino-core backend for the shared PlatformIO library converter.
|
||||
|
||||
Bundled names build straight from the framework tree; everything else goes
|
||||
through ``esphome.platformio.library``. Mirrors ``lib_ldf_mode=off``: each
|
||||
library builds its own archive; all include dirs join one global path.
|
||||
|
||||
Deviations from PlatformIO: flat-layout libraries get the recursive default
|
||||
source filter; ``dot_a_linkage`` is honored; bundled libraries never run a
|
||||
manifest ``extraScript``; manifest ``-I`` flags join the global include path;
|
||||
``precompiled``/``ldflags`` properties are refused by name.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from esphome.core import CORE, EsphomeError, Library
|
||||
from esphome.helpers import walk_files
|
||||
from esphome.platformio.extra_script import apply_extra_script
|
||||
from esphome.platformio.library import (
|
||||
DEFAULT_BUILD_INCLUDE_DIR,
|
||||
DEFAULT_BUILD_SRC_FILTER,
|
||||
ESPHOME_DATA_KEY,
|
||||
ESPHOME_DATA_LINK_FLAGS_KEY,
|
||||
LIBRARY_HEADER_SUFFIXES,
|
||||
SRC_FILE_EXTENSIONS,
|
||||
ConvertedLibrary,
|
||||
IncompatiblePlatform,
|
||||
InvalidLibrary,
|
||||
LibraryBackend,
|
||||
_url_or_none,
|
||||
check_library_data,
|
||||
collect_filtered_files,
|
||||
convert_libraries,
|
||||
ensure_list,
|
||||
is_lib_ignored,
|
||||
lex_build_flags,
|
||||
lib_ignore_set,
|
||||
normalize_dependencies,
|
||||
parse_library_json,
|
||||
parse_library_properties,
|
||||
warn_properties_depends,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ArduinoLibrary:
|
||||
"""One resolved library, ready for the ninja generator."""
|
||||
|
||||
name: str
|
||||
sources: list[Path] = field(default_factory=list)
|
||||
include_dirs: list[Path] = field(default_factory=list)
|
||||
# Extra compile flags private to this library's own sources
|
||||
flags: list[str] = field(default_factory=list)
|
||||
# PlatformIO's build.libArchive / Arduino's dot_a_linkage: when False the
|
||||
# objects go to the linker directly (symbols nothing references survive)
|
||||
lib_archive: bool = True
|
||||
# Link inputs the library contributes (-L dirs / -l libs, e.g. from
|
||||
# precompiled vendor blobs) and -Wl, options for the firmware link
|
||||
link_dirs: list[Path] = field(default_factory=list)
|
||||
link_libs: list[str] = field(default_factory=list)
|
||||
link_flags: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
# Source-like suffixes the case-sensitive suffix map rejects
|
||||
_UNMAPPED_SOURCE_SUFFIXES = frozenset(
|
||||
{s.lower() for s in SRC_FILE_EXTENSIONS} | {".ino"}
|
||||
)
|
||||
|
||||
# Filename-plain names: an allowlist excludes separators, drive colons,
|
||||
# and dot-only names by shape
|
||||
_SAFE_LIBRARY_NAME_RE = re.compile(r"[A-Za-z0-9_][A-Za-z0-9_. +-]*\Z")
|
||||
|
||||
|
||||
def _is_safe_library_name(name: object) -> bool:
|
||||
"""Whether a name may be joined under the framework's libraries dir."""
|
||||
return isinstance(name, str) and _SAFE_LIBRARY_NAME_RE.fullmatch(name) is not None
|
||||
|
||||
|
||||
def _manifest_build(name: str, data: object) -> dict:
|
||||
"""The manifest's ``build`` section; malformed manifests fail by name."""
|
||||
build = data.get("build", {}) if isinstance(data, dict) else None
|
||||
if not isinstance(build, dict):
|
||||
raise EsphomeError(f"Library {name} has a malformed manifest")
|
||||
return build
|
||||
|
||||
|
||||
def _resolve_src_dir(name: str, read_path: Path, build: dict) -> str:
|
||||
"""Resolve PIO's source dir: manifest srcDir, else src/Src, else the root."""
|
||||
if "srcDir" not in build:
|
||||
return next((d for d in ("src", "Src") if (read_path / d).is_dir()), ".")
|
||||
# A declared srcDir (falsy included) that does not resolve is a manifest error
|
||||
src_dir = build["srcDir"]
|
||||
if not (isinstance(src_dir, str) and src_dir and (read_path / src_dir).is_dir()):
|
||||
raise EsphomeError(
|
||||
f"Library {name} declares srcDir {src_dir!r} which does not exist"
|
||||
)
|
||||
return src_dir
|
||||
|
||||
|
||||
def _reject_unsupported_link_fields(name: str, data: dict) -> None:
|
||||
# PIO honors these; ignoring them would fail at link with no stated
|
||||
# cause. Property values are strings, so "false" is not a declaration.
|
||||
precompiled = data.get("precompiled")
|
||||
if precompiled and str(precompiled).strip().lower() != "false":
|
||||
raise EsphomeError(
|
||||
f"Library {name} declares precompiled, which this backend does not support"
|
||||
)
|
||||
if data.get("ldflags"):
|
||||
raise EsphomeError(
|
||||
f"Library {name} declares ldflags, which this backend does not support"
|
||||
)
|
||||
|
||||
|
||||
def _resolve_lib_archive(name: str, data: dict, build: dict) -> bool:
|
||||
"""build.libArchive, else dot_a_linkage (an Arduino IDE property PIO
|
||||
ignores; a deliberate extra), else archive."""
|
||||
|
||||
# Strict parse: bool("false") is True
|
||||
def _parse(key: str, raw: object) -> bool:
|
||||
if isinstance(raw, bool):
|
||||
return raw
|
||||
value = str(raw).strip().lower()
|
||||
if value in ("true", "false"):
|
||||
return value == "true"
|
||||
raise EsphomeError(f"Library {name} has a malformed {key} value {raw!r}")
|
||||
|
||||
if "libArchive" in build:
|
||||
return _parse("libArchive", build["libArchive"])
|
||||
if "dot_a_linkage" in data:
|
||||
return _parse("dot_a_linkage", data["dot_a_linkage"])
|
||||
return True
|
||||
|
||||
|
||||
def _classify_build_flags(
|
||||
name: str, read_path: Path, lib: ArduinoLibrary, flag_tokens: list[str]
|
||||
) -> list[str]:
|
||||
"""Route the lexed build.flags into the library's flag lists.
|
||||
|
||||
Returns the ``-I`` arguments for the include-dir resolution.
|
||||
"""
|
||||
include_flags: list[str] = []
|
||||
for tok in flag_tokens:
|
||||
if tok.startswith("-I"):
|
||||
include_flags.append(tok[2:])
|
||||
elif tok.startswith("-L"):
|
||||
link_dir = (read_path / tok[2:]).resolve()
|
||||
if not link_dir.is_dir():
|
||||
# Kept (the linker ignores missing -L dirs); the warning
|
||||
# names the culprit before a bare "cannot find -lfoo"
|
||||
_LOGGER.warning(
|
||||
"Library %s declares library dir %s which does not exist",
|
||||
name,
|
||||
tok[2:],
|
||||
)
|
||||
lib.link_dirs.append(link_dir)
|
||||
elif tok.startswith("-l"):
|
||||
lib.link_libs.append(tok[2:])
|
||||
elif tok.startswith("-Wl,"):
|
||||
lib.link_flags.append(tok)
|
||||
else:
|
||||
lib.flags.append(tok)
|
||||
return include_flags
|
||||
|
||||
|
||||
def _resolve_include_dirs(
|
||||
name: str,
|
||||
read_path: Path,
|
||||
lib: ArduinoLibrary,
|
||||
build: dict,
|
||||
src_dir: str,
|
||||
include_flags: list[str],
|
||||
) -> None:
|
||||
include_dir = build.get("includeDir", DEFAULT_BUILD_INCLUDE_DIR)
|
||||
if not isinstance(include_dir, str):
|
||||
raise EsphomeError(f"Library {name} has a malformed includeDir")
|
||||
for d, explicit in [
|
||||
(include_dir, "includeDir" in build),
|
||||
(src_dir, False), # _resolve_src_dir already validated it
|
||||
*((flag, True) for flag in include_flags),
|
||||
]:
|
||||
if (path := (read_path / d)).is_dir():
|
||||
lib.include_dirs.append(path.resolve())
|
||||
elif explicit:
|
||||
# Warn-and-drop (unlike srcDir): a missing include dir is
|
||||
# harmless until a header is needed, and the compile names it
|
||||
_LOGGER.warning(
|
||||
"Library %s declares include dir %s which does not exist", name, d
|
||||
)
|
||||
|
||||
|
||||
def _collect_lib_sources(
|
||||
name: str,
|
||||
read_path: Path,
|
||||
lib: ArduinoLibrary,
|
||||
src_dir: str,
|
||||
src_filter: list[str],
|
||||
) -> None:
|
||||
sources: list[Path] = []
|
||||
dropped: list[str] = []
|
||||
saw_header = False
|
||||
for f in collect_filtered_files(read_path / src_dir, src_filter):
|
||||
path = Path(f)
|
||||
suffix = path.suffix
|
||||
if suffix in SRC_FILE_EXTENSIONS:
|
||||
# resolve() per file: srcFilter patterns may escape src_dir
|
||||
sources.append(path.resolve())
|
||||
elif suffix.lower() in _UNMAPPED_SOURCE_SUFFIXES:
|
||||
# A source-like suffix the case-sensitive map rejects (.CPP,
|
||||
# .ino) is a dropped compilation unit; headers fall through
|
||||
dropped.append(path.name)
|
||||
elif suffix.lower() in LIBRARY_HEADER_SUFFIXES:
|
||||
saw_header = True
|
||||
lib.sources = sorted(sources)
|
||||
if dropped:
|
||||
_LOGGER.warning(
|
||||
"Library %s: %d file(s) with unmapped source suffixes are not compiled: %s",
|
||||
name,
|
||||
len(dropped),
|
||||
", ".join(sorted(dropped)),
|
||||
)
|
||||
if not lib.sources and not saw_header:
|
||||
# Matched headers mean header-only; a filter matching nothing is
|
||||
# a manifest/tree problem (a truly empty tree raises elsewhere)
|
||||
_LOGGER.warning("Library %s: no source files matched", name)
|
||||
|
||||
|
||||
def _library_info(name: str, read_path: Path, data: dict) -> ArduinoLibrary:
|
||||
"""Resolve one library's sources, include dirs, and flags (PIO semantics)."""
|
||||
build = _manifest_build(name, data)
|
||||
_reject_unsupported_link_fields(name, data)
|
||||
src_dir = _resolve_src_dir(name, read_path, build)
|
||||
src_filter = ensure_list(build.get("srcFilter", DEFAULT_BUILD_SRC_FILTER))
|
||||
if not all(isinstance(entry, str) for entry in src_filter):
|
||||
raise EsphomeError(f"Library {name} has a malformed srcFilter")
|
||||
lib = ArduinoLibrary(name=name, lib_archive=_resolve_lib_archive(name, data, build))
|
||||
# PlatformIO shell-lexes each build.flags entry
|
||||
include_flags = _classify_build_flags(
|
||||
name, read_path, lib, lex_build_flags(build.get("flags", []), f"library {name}")
|
||||
)
|
||||
_resolve_include_dirs(name, read_path, lib, build, src_dir, include_flags)
|
||||
_collect_lib_sources(name, read_path, lib, src_dir, src_filter)
|
||||
return lib
|
||||
|
||||
|
||||
def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary:
|
||||
"""A library bundled with the Arduino core, read from the framework tree.
|
||||
|
||||
``library.json`` wins over ``library.properties`` when both exist, as in
|
||||
PlatformIO's LibBuilderFactory; only the JSON manifest can carry a
|
||||
``build`` section (srcDir, srcFilter, flags).
|
||||
"""
|
||||
lib_dir = framework_path / "libraries" / name
|
||||
manifest_json = lib_dir / "library.json"
|
||||
if manifest_json.is_file():
|
||||
try:
|
||||
data = parse_library_json(manifest_json)
|
||||
except ValueError as err: # JSONDecodeError
|
||||
raise EsphomeError(
|
||||
f"Bundled library {name} has a corrupt library.json ({err}); "
|
||||
"the framework install may be incomplete (run 'esphome clean-all')"
|
||||
) from err
|
||||
elif (manifest := lib_dir / "library.properties").is_file():
|
||||
data = parse_library_properties(manifest)
|
||||
else:
|
||||
# Debug, not warning: the legacy manifest-less layout is legal and
|
||||
# the 3.1.2 core ships one such library (FSTools), so a warning
|
||||
# would be unactionable noise on every build using it
|
||||
_LOGGER.debug("Bundled library %s has no manifest; using defaults", name)
|
||||
data = {}
|
||||
if isinstance(data, dict):
|
||||
# Bundled manifest deps are never walked; make the skip visible
|
||||
if data.get("dependencies"):
|
||||
_LOGGER.warning(
|
||||
"Bundled library %s declares dependencies, which are not "
|
||||
"resolved automatically; add them with add_library() if needed",
|
||||
name,
|
||||
)
|
||||
warn_properties_depends(name, data)
|
||||
build = data.get("build")
|
||||
if isinstance(build, dict) and build.get("extraScript"):
|
||||
# Scripts only run on the converted path; building without
|
||||
# the script's flags would miscompile
|
||||
raise EsphomeError(
|
||||
f"Bundled library {name} declares an extraScript, which is "
|
||||
"not run for bundled libraries"
|
||||
)
|
||||
lib = _library_info(name, lib_dir, data)
|
||||
_assert_tree_has_code(
|
||||
name,
|
||||
lib_dir,
|
||||
"the framework install may be incomplete (run 'esphome clean-all')",
|
||||
)
|
||||
return lib
|
||||
|
||||
|
||||
def _assert_tree_has_code(name: str, root: Path, hint: str) -> None:
|
||||
"""An empty or half-extracted tree can never link; fail by name (a
|
||||
warning would scroll away and resurface as undefined symbols)."""
|
||||
if not any(
|
||||
Path(p).suffix in SRC_FILE_EXTENSIONS
|
||||
or Path(p).suffix.lower() in LIBRARY_HEADER_SUFFIXES
|
||||
for p in walk_files(root)
|
||||
):
|
||||
raise EsphomeError(f"Library {name} has no sources or headers; {hint}")
|
||||
|
||||
|
||||
def _external_short_name(name: str) -> str:
|
||||
"""The short library name of a requested spec.
|
||||
|
||||
"owner/Name" and plain names take the last path segment; "Name=<url>"
|
||||
takes the declared name. Git tails (".git", "#ref") are stripped like
|
||||
the walk's URL normalization; the comparand is a manifest dependency
|
||||
name, never a spec.
|
||||
"""
|
||||
head, sep, tail = name.partition("=")
|
||||
if sep and "://" in tail:
|
||||
return head
|
||||
short = name.rsplit("/", maxsplit=1)[-1]
|
||||
return short.partition("#")[0].removesuffix(".git")
|
||||
|
||||
|
||||
def _check_unfulfilled_provides(
|
||||
provided_requests: set[str], satisfied: set[str], still_requested: set[str]
|
||||
) -> None:
|
||||
"""Fail by name when a walk-skipped dependency was never added.
|
||||
|
||||
An unfulfilled provides() promise only surfaces as undefined symbols
|
||||
at link. The walk records across re-resolutions, so a name no final
|
||||
manifest still requests is stale state, never a failure.
|
||||
"""
|
||||
if missing := sorted((provided_requests & still_requested) - satisfied):
|
||||
raise EsphomeError(
|
||||
"provides() skipped these dependencies but nothing added them: "
|
||||
f"{', '.join(missing)}; the build is missing libraries"
|
||||
)
|
||||
|
||||
|
||||
def resolve_libraries(
|
||||
framework_path: Path, *, pio_platform: str, board_mcu: str, cache_key: str
|
||||
) -> list[ArduinoLibrary]:
|
||||
"""Resolve every ``cg.add_library()`` entry into an :class:`ArduinoLibrary`.
|
||||
|
||||
``pio_platform``/``board_mcu`` filter manifests the way PlatformIO would
|
||||
for that core (e.g. ``espressif8266``/``esp8266``); ``cache_key`` keys the
|
||||
shared converter's download cache.
|
||||
|
||||
The returned list is not topologically sorted, so the caller must link
|
||||
the archives inside one ``--start-group``/``--end-group`` pair (the
|
||||
bundled-first grouping is incidental).
|
||||
"""
|
||||
bundled: list[ArduinoLibrary] = []
|
||||
external: list[Library] = []
|
||||
# PlatformIO's lib_ignore covers framework-bundled libraries too; the
|
||||
# shared converter only filters the registry/git ones.
|
||||
lib_ignore = lib_ignore_set()
|
||||
# Exact directory names keep membership case-sensitive everywhere
|
||||
# (an is_dir() probe would match "wire" on macOS/Windows and build
|
||||
# the bundled Wire twice)
|
||||
libraries_dir = framework_path / "libraries"
|
||||
if not libraries_dir.is_dir():
|
||||
# A registry fallback would fail later with a misleading
|
||||
# package-not-found error per bundled name
|
||||
raise EsphomeError(
|
||||
f"{libraries_dir} is missing; the framework install may be "
|
||||
"incomplete (run 'esphome clean-all')"
|
||||
)
|
||||
bundled_dir_names = frozenset(p.name for p in libraries_dir.iterdir() if p.is_dir())
|
||||
|
||||
def _provided(name: object) -> bool:
|
||||
return _is_safe_library_name(name) and name in bundled_dir_names
|
||||
|
||||
for library in CORE.platformio_libraries.values():
|
||||
if is_lib_ignored(library.name, lib_ignore):
|
||||
continue
|
||||
# Bundled only for a bare name with a matching framework dir; pinned
|
||||
# or unmatched names resolve from the registry, as under PlatformIO.
|
||||
if not library.repository and not library.version and _provided(library.name):
|
||||
# Bundled manifest deps are not walked; _bundled_library warns
|
||||
bundled.append(_bundled_library(framework_path, library.name))
|
||||
else:
|
||||
external.append(library)
|
||||
|
||||
converted: list[ArduinoLibrary] = []
|
||||
bundled_names = {lib.name for lib in bundled}
|
||||
converted_manifest_names: set[str] = set()
|
||||
# Bundled candidates skipped on purpose (platform filter); the
|
||||
# provides() reconciliation must count them as satisfied
|
||||
knowingly_skipped: set[str] = set()
|
||||
# Dependency names of the manifests actually emitted; a walk recording
|
||||
# for a since-re-resolved manifest must not fail the reconciliation
|
||||
final_dep_names: set[str] = set()
|
||||
# Ordered set of bundled dependency names to add once conversion is done
|
||||
pending_bundled: dict[str, None] = {}
|
||||
# Deps matching a separately-requested external are already in the build
|
||||
# (a duplicate archive means duplicate-symbol link errors)
|
||||
external_short_names = {
|
||||
_external_short_name(lib.name) for lib in external if lib.name
|
||||
}
|
||||
|
||||
def _add_bundled_dependencies(component: ConvertedLibrary) -> None:
|
||||
# A version-less bare name ("Hash") is a core-bundled library the
|
||||
# shared converter cannot resolve from the registry
|
||||
for dep in normalize_dependencies(
|
||||
component.data.get("dependencies"), component.name
|
||||
):
|
||||
# normalize_dependencies guarantees a non-empty str name
|
||||
name = dep["name"]
|
||||
final_dep_names.add(name)
|
||||
if "/" in name:
|
||||
owner, _, pkg = name.partition("/")
|
||||
if _is_safe_library_name(owner) and _is_safe_library_name(pkg):
|
||||
# Owner-qualified; the converter resolves it from the registry
|
||||
continue
|
||||
if not _is_safe_library_name(name):
|
||||
# The name becomes a path component; never join a traversal
|
||||
_LOGGER.warning(
|
||||
"Ignoring malformed dependency entry %r of library %s",
|
||||
dep,
|
||||
component.name,
|
||||
)
|
||||
continue
|
||||
if name in external_short_names:
|
||||
if _provided(name):
|
||||
# A bundled copy is suppressed; a coincidental name
|
||||
# collision would surface as link errors
|
||||
_LOGGER.warning(
|
||||
"Dependency %s of %s is assumed satisfied by a "
|
||||
"requested external library; the bundled copy is "
|
||||
"not added",
|
||||
name,
|
||||
component.name,
|
||||
)
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Dependency %s of %s assumed satisfied by a requested "
|
||||
"external library",
|
||||
name,
|
||||
component.name,
|
||||
)
|
||||
continue
|
||||
if name in bundled_names or is_lib_ignored(name, lib_ignore):
|
||||
continue
|
||||
if _url_or_none(dep.get("version")) is not None:
|
||||
# A URL names one specific source; never add the bundled copy
|
||||
continue
|
||||
if dep.get("owner") or not _provided(name):
|
||||
# Only owner-less framework-tree names take the bundled
|
||||
# copy (PIO's process_dependencies); the walk reports drops
|
||||
continue
|
||||
try:
|
||||
# framework=None: the walk already warned for non-platform
|
||||
# causes; debug keeps one fault from warning twice (pinned
|
||||
# by test_nonplatform_rejection_warns_once_through_real_converter)
|
||||
check_library_data(dep, pio_platform, None)
|
||||
except IncompatiblePlatform as err:
|
||||
# A knowing skip (platform filter), not a broken promise
|
||||
knowingly_skipped.add(name)
|
||||
_LOGGER.debug("Skip bundled candidate %s: %s", name, err)
|
||||
continue
|
||||
except InvalidLibrary as err:
|
||||
# Malformed manifest data never counts as satisfied; the
|
||||
# walk owns the warning (see the warns-once test above)
|
||||
_LOGGER.debug("Skip malformed bundled candidate %s: %s", name, err)
|
||||
continue
|
||||
# Deferred: a later manifest name may satisfy this
|
||||
pending_bundled.setdefault(name)
|
||||
|
||||
def _emit(component: ConvertedLibrary) -> None:
|
||||
apply_extra_script(
|
||||
component, board_mcu=lambda: board_mcu, pio_platform=pio_platform
|
||||
)
|
||||
_assert_tree_has_code(
|
||||
component.get_require_name(),
|
||||
component.source_dir,
|
||||
"the download may be incomplete (run 'esphome clean-all')",
|
||||
)
|
||||
if isinstance(manifest_name := component.data.get("name"), str):
|
||||
converted_manifest_names.add(manifest_name)
|
||||
lib = _library_info(
|
||||
component.get_require_name(), component.source_dir, component.data
|
||||
)
|
||||
# Extra-script LINKFLAGS travel outside build.flags; dropping
|
||||
# them would link wrong with no stated cause
|
||||
lib.link_flags.extend(
|
||||
component.data.get(ESPHOME_DATA_KEY, {}).get(
|
||||
ESPHOME_DATA_LINK_FLAGS_KEY, []
|
||||
)
|
||||
)
|
||||
converted.append(lib)
|
||||
_add_bundled_dependencies(component)
|
||||
|
||||
backend = LibraryBackend(
|
||||
platform=pio_platform,
|
||||
framework="arduino",
|
||||
emit=_emit,
|
||||
cache_key=cache_key,
|
||||
# The walk must not resolve bundled names from the registry;
|
||||
# _add_bundled_dependencies adds them after emit
|
||||
provides=_provided,
|
||||
)
|
||||
if external:
|
||||
convert_libraries(external, backend)
|
||||
for name in pending_bundled:
|
||||
if name in converted_manifest_names:
|
||||
# The converted library is this one; the bundled copy would
|
||||
# double the archive. Warn like the external_short_names twin.
|
||||
_LOGGER.warning(
|
||||
"Dependency %s is assumed satisfied by a converted library's "
|
||||
"manifest name; the bundled copy is not added",
|
||||
name,
|
||||
)
|
||||
continue
|
||||
bundled_names.add(name)
|
||||
bundled.append(_bundled_library(framework_path, name))
|
||||
|
||||
_check_unfulfilled_provides(
|
||||
backend.provided_requests,
|
||||
bundled_names
|
||||
| converted_manifest_names
|
||||
| external_short_names
|
||||
| knowingly_skipped,
|
||||
final_dep_names,
|
||||
)
|
||||
|
||||
return bundled + converted
|
||||
@@ -0,0 +1,9 @@
|
||||
"""Native (PlatformIO-free) build support for the ESP8266 Arduino core.
|
||||
|
||||
This package downloads the Arduino ESP8266 core and the xtensa-lx106
|
||||
toolchain, generates a ninja build for them plus the ESPHome sources, and
|
||||
drives the build directly — the ESP8266 equivalent of ``esphome.espidf``.
|
||||
|
||||
Deliberately importable without the esp8266 component to avoid circular
|
||||
imports; the component wires these modules in via lazy imports.
|
||||
"""
|
||||
@@ -0,0 +1,164 @@
|
||||
"""Download and install the Arduino ESP8266 core, toolchain, and ninja.
|
||||
|
||||
Artifacts land in a machine-global cache (shared across projects, like the
|
||||
ESP-IDF install in ``esphome.espidf.framework``):
|
||||
|
||||
<cache>/arduino8266/frameworks/<version>/ framework-arduinoespressif8266
|
||||
<cache>/arduino8266/toolchains/<version>/ toolchain-xtensa (gcc 10.3)
|
||||
|
||||
Packages come from the PlatformIO registry (identical bits to the PlatformIO
|
||||
backend); ``ESPHOME_ARDUINO8266_*_MIRRORS`` overrides the URLs. ninja comes
|
||||
from PATH or the ninja PyPI wheel.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import NamedTuple
|
||||
|
||||
from esphome.build_helpers.ccache import ccache_defaults_env
|
||||
from esphome.build_helpers.ninja import find_ninja
|
||||
from esphome.build_helpers.tools_cache import ARDUINO8266_TOOLS_CACHE, tools_cache_path
|
||||
from esphome.core import EsphomeError, Version
|
||||
from esphome.framework_helpers import str_to_lst_of_str
|
||||
from esphome.platformio.registry import install_package, prefetch_packages
|
||||
|
||||
FRAMEWORK_PACKAGE = "framework-arduinoespressif8266"
|
||||
TOOLCHAIN_PACKAGE = "toolchain-xtensa"
|
||||
# gcc 10.3, the toolchain Arduino core 3.x builds with; the build
|
||||
# generator's compile flags are tuned to it.
|
||||
TOOLCHAIN_VERSION = "2.100300.220621"
|
||||
|
||||
ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS = str_to_lst_of_str(
|
||||
os.environ.get("ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS", "")
|
||||
)
|
||||
ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS = str_to_lst_of_str(
|
||||
os.environ.get("ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS", "")
|
||||
)
|
||||
|
||||
|
||||
def get_arduino8266_tools_path() -> Path:
|
||||
# Machine-global so all projects share one install; see
|
||||
# espidf.framework.get_idf_tools_path for the location rationale.
|
||||
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
|
||||
|
||||
|
||||
# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the
|
||||
# encoder below cannot name 3.0.0/3.0.1 either (see its docstring)
|
||||
MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
|
||||
|
||||
|
||||
def framework_package_version(ver: Version) -> str:
|
||||
"""Map an Arduino core version to its registry package version (3.1.2 ->
|
||||
3.30102.0; the leading 3 is the package major).
|
||||
|
||||
Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor
|
||||
at MIN_FRAMEWORK_VERSION.
|
||||
"""
|
||||
if ver.major > 3:
|
||||
raise EsphomeError(
|
||||
f"Arduino core {ver} is not supported yet; "
|
||||
"the newest known core series is 3.x"
|
||||
)
|
||||
if ver <= Version(2, 6, 2):
|
||||
# Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same
|
||||
# boundary as _format_framework_arduino_version's era guard)
|
||||
raise EsphomeError(
|
||||
f"Arduino core {ver} uses an older package encoding than this "
|
||||
"helper implements (newer than 2.6.2)"
|
||||
)
|
||||
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
|
||||
|
||||
def get_framework_path(package_version: str) -> Path:
|
||||
return get_arduino8266_tools_path() / "frameworks" / package_version
|
||||
|
||||
|
||||
def get_toolchain_path() -> Path:
|
||||
return get_arduino8266_tools_path() / "toolchains" / TOOLCHAIN_VERSION
|
||||
|
||||
|
||||
class InstalledPaths(NamedTuple):
|
||||
"""Locations of the installed framework, toolchain, and ninja binary."""
|
||||
|
||||
framework: Path
|
||||
toolchain: Path
|
||||
ninja: Path
|
||||
|
||||
|
||||
def check_and_install(framework_version: Version) -> InstalledPaths:
|
||||
"""Ensure framework, toolchain, and ninja are installed; return their paths."""
|
||||
if framework_version < MIN_FRAMEWORK_VERSION:
|
||||
# Config validation enforces this too; keep the module honest when
|
||||
# called directly.
|
||||
raise EsphomeError(
|
||||
f"The native toolchain requires the Arduino core "
|
||||
f">= {MIN_FRAMEWORK_VERSION}, got {framework_version}"
|
||||
)
|
||||
# Probe the cheap local dependency before ~110 MB of downloads
|
||||
ninja_path = find_ninja()
|
||||
package_version = framework_package_version(framework_version)
|
||||
framework_path = get_framework_path(package_version)
|
||||
downloads_dir = get_arduino8266_tools_path() / "downloads"
|
||||
toolchain_path = get_toolchain_path()
|
||||
# One spec per package: the prefetch and the installs must agree
|
||||
specs = (
|
||||
(
|
||||
FRAMEWORK_PACKAGE,
|
||||
package_version,
|
||||
framework_path,
|
||||
ESPHOME_ARDUINO8266_FRAMEWORK_MIRRORS,
|
||||
("cores/esp8266", "tools/sdk", "libraries"),
|
||||
),
|
||||
(
|
||||
TOOLCHAIN_PACKAGE,
|
||||
TOOLCHAIN_VERSION,
|
||||
toolchain_path,
|
||||
ESPHOME_ARDUINO8266_TOOLCHAIN_MIRRORS,
|
||||
# xtensa-lx106-elf pins the target: every gcc package has a bin/
|
||||
("bin", "xtensa-lx106-elf"),
|
||||
),
|
||||
)
|
||||
# Fetch both archives at once; the installs below verify and extract
|
||||
prefetch_packages([spec[:4] for spec in specs], downloads_dir)
|
||||
for name, version, dest, mirrors, expect in specs:
|
||||
install_package(name, version, dest, mirrors, downloads_dir, expect=expect)
|
||||
return InstalledPaths(
|
||||
framework=framework_path, toolchain=toolchain_path, ninja=ninja_path
|
||||
)
|
||||
|
||||
|
||||
def toolchain_tool(toolchain_path: Path, name: str) -> Path:
|
||||
"""Path to one toolchain tool (gcc, g++, ar, size, addr2line, ...).
|
||||
|
||||
The single owner of the ``bin/xtensa-lx106-elf-<name>`` layout and the
|
||||
Windows suffix, so a toolchain package bump touches one spot.
|
||||
"""
|
||||
suffix = ".exe" if os.name == "nt" else ""
|
||||
return toolchain_path / "bin" / f"xtensa-lx106-elf-{name}{suffix}"
|
||||
|
||||
|
||||
def get_build_env(toolchain_path: Path, ccache: str | None) -> dict[str, str]:
|
||||
env = os.environ.copy()
|
||||
# Drop empty entries: a trailing separator from an absent PATH would
|
||||
# make the shell search the current directory for tools
|
||||
parts = [
|
||||
str(toolchain_path / "bin"),
|
||||
*filter(None, env.get("PATH", "").split(os.pathsep)),
|
||||
]
|
||||
env["PATH"] = os.pathsep.join(parts)
|
||||
env.update(ccache_env(ccache))
|
||||
return env
|
||||
|
||||
|
||||
def ccache_env(ccache: str | None) -> dict[str, str]:
|
||||
"""Return ccache settings for the build subprocess (not os.environ).
|
||||
|
||||
``ccache`` is the pre-resolved binary (resolve_ccache_path), or None
|
||||
when disabled. Values the user already set in the environment are
|
||||
respected.
|
||||
"""
|
||||
if ccache is None:
|
||||
return {}
|
||||
return ccache_defaults_env(get_arduino8266_tools_path() / "ccache")
|
||||
@@ -0,0 +1,108 @@
|
||||
"""Tiny cross-platform build steps invoked from the generated ninja file.
|
||||
|
||||
Plain script (not ``python -m``): it runs from ninja with whatever Python
|
||||
started esphome and must not depend on the package being importable.
|
||||
|
||||
Subcommands:
|
||||
ar <ar-binary> <archive> <rspfile> remove stale archive, then ``ar rcs``
|
||||
copy <src> <dst> copy a file
|
||||
|
||||
The ar rspfile carries one object path per line (the generating rule must
|
||||
use ``$in_newline``, never ``$in``).
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def _read_rspfile(rspfile: str) -> list[str]:
|
||||
r"""The object paths listed in ``rspfile``, unquoted.
|
||||
|
||||
GNU ar treats backslashes in response files as escapes (corrupts
|
||||
Windows paths), so the caller expands the list into argv; strip the
|
||||
simple surrounding quote ninja adds to special paths, then undo
|
||||
ninja's POSIX escape for an embedded quote ('a'\\''b.o' -> a'b.o).
|
||||
"""
|
||||
return [
|
||||
line[1:-1].replace("'\\''", "'")
|
||||
if len(line) >= 2 and line[0] == line[-1] and line[0] in "'\""
|
||||
else line
|
||||
for line in Path(rspfile).read_text(encoding="utf-8").splitlines()
|
||||
if line
|
||||
]
|
||||
|
||||
|
||||
def _run_ar(ar: str, archive: str, rspfile: str) -> int:
|
||||
# Remove first: ``ar rcs`` replaces members but never drops ones whose
|
||||
# source was removed from the build, which would leak stale objects.
|
||||
Path(archive).unlink(missing_ok=True)
|
||||
objects = _read_rspfile(rspfile)
|
||||
if not objects:
|
||||
# An empty archive would "succeed" here and fail far away at link
|
||||
print(f"ar: no objects listed in {rspfile} for {archive}", file=sys.stderr)
|
||||
return 1
|
||||
# Batch by argv length: expanding the rspfile gives back the Windows
|
||||
# 32767-char command-line limit it existed to avoid. "rcs" creates,
|
||||
# "qs" appends; the s keeps the symbol index explicit on every ar.
|
||||
op = "rcs"
|
||||
ok = False
|
||||
try:
|
||||
while objects:
|
||||
batch = [objects.pop(0)]
|
||||
batch_len = len(batch[0])
|
||||
while objects and batch_len + len(objects[0]) < 25000:
|
||||
batch_len += len(objects[0]) + 1
|
||||
batch.append(objects.pop(0))
|
||||
rc = subprocess.run(
|
||||
[ar, op, archive, *batch], check=False, close_fds=False
|
||||
).returncode
|
||||
if rc != 0:
|
||||
return rc
|
||||
op = "qs"
|
||||
ok = True
|
||||
return 0
|
||||
finally:
|
||||
if not ok:
|
||||
# Any failure (bad exit, missing ar binary, interrupt) must not
|
||||
# leave a truncated archive behind
|
||||
Path(archive).unlink(missing_ok=True)
|
||||
|
||||
|
||||
def _run_copy(src: str, dst: str) -> int:
|
||||
try:
|
||||
shutil.copyfile(src, dst)
|
||||
except OSError as err:
|
||||
# Never leave a partially written output (e.g. a firmware image);
|
||||
# SameFileError means dst IS src, where unlinking destroys the input
|
||||
if not isinstance(err, shutil.SameFileError):
|
||||
Path(dst).unlink(missing_ok=True)
|
||||
print(f"copy: {src} -> {dst} failed: {err}", file=sys.stderr)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
# mode -> (handler, expected operand count); surplus argv means a
|
||||
# mis-specified ninja rule and must error, not silently drop operands
|
||||
_MODES = {"ar": (_run_ar, 3), "copy": (_run_copy, 2)}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
mode = sys.argv[1] if len(sys.argv) > 1 else ""
|
||||
if entry := _MODES.get(mode):
|
||||
handler, argc = entry
|
||||
args = sys.argv[2:]
|
||||
if len(args) != argc:
|
||||
print(
|
||||
f"build_tool {mode}: expected {argc} arguments, got {len(args)}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
return 1
|
||||
return handler(*args)
|
||||
print(f"unknown build_tool mode: {mode}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Shared ccache policy for build backends: env-knob parsing, binary
|
||||
resolution, and default ``CCACHE_*`` values."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
|
||||
from esphome.helpers import FALSY_ENV_STRINGS, TRUTHY_ENV_STRINGS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ccache_runs(ccache: str) -> bool:
|
||||
"""Return True when the ``ccache`` found on PATH actually runs."""
|
||||
return tool_version_runs(
|
||||
ccache,
|
||||
"Ignoring ccache at %s because it failed to run; compiling without ccache",
|
||||
)
|
||||
|
||||
|
||||
def parse_enable_env(name: str) -> bool | None:
|
||||
"""Strictly parse an on/off environment knob; None when unset or invalid.
|
||||
|
||||
``bool(str)`` truthiness would flip ``no``/``off`` to enabled, so only
|
||||
1/true/yes/on and 0/false/no/off count; anything else warns and reads
|
||||
as unset so the caller's default policy applies.
|
||||
"""
|
||||
raw = os.environ.get(name)
|
||||
if raw is None:
|
||||
return None
|
||||
lowered = raw.strip().lower()
|
||||
if not lowered:
|
||||
# ENV KNOB= (Docker/CI) has always read as a disable
|
||||
return False
|
||||
if lowered in TRUTHY_ENV_STRINGS:
|
||||
return True
|
||||
if lowered in FALSY_ENV_STRINGS:
|
||||
return False
|
||||
_LOGGER.warning("Ignoring unrecognized %s=%r; use 1 or 0", name, raw)
|
||||
return None
|
||||
|
||||
|
||||
def resolve_ccache_path() -> str | None:
|
||||
"""The ccache binary to wrap compiles with, or None when disabled.
|
||||
|
||||
An explicit ``ESPHOME_CCACHE_ENABLE=1`` skips the runnability probe; the
|
||||
Windows extended-length prefix is stripped before probing (#18399).
|
||||
"""
|
||||
import shutil
|
||||
|
||||
explicit = parse_enable_env("ESPHOME_CCACHE_ENABLE")
|
||||
if explicit is False:
|
||||
return None
|
||||
ccache = shutil.which("ccache")
|
||||
if ccache is None:
|
||||
if explicit:
|
||||
_LOGGER.warning(
|
||||
"ESPHOME_CCACHE_ENABLE is set but no ccache binary is on PATH; "
|
||||
"compiling without ccache"
|
||||
)
|
||||
return None
|
||||
ccache = strip_win_long_path_prefix(ccache)
|
||||
if not explicit and not _ccache_runs(ccache):
|
||||
return None
|
||||
return ccache
|
||||
|
||||
|
||||
def ccache_defaults_env(cache_dir: Path) -> dict[str, str]:
|
||||
"""Default ``CCACHE_*`` values for a build subprocess (not os.environ).
|
||||
|
||||
Values the user already set in the environment are respected. Depend
|
||||
mode is on: both native backends emit depfiles (-MMD / CMake), which
|
||||
keeps cache-miss overhead low.
|
||||
"""
|
||||
from esphome.core import CORE
|
||||
|
||||
# An unset build_path means the env was built before preload; fail loudly
|
||||
# rather than silently drop CCACHE_BASEDIR.
|
||||
if CORE.build_path is None:
|
||||
raise ValueError(
|
||||
"CORE.build_path must be set before constructing the build environment"
|
||||
)
|
||||
defaults = {
|
||||
"CCACHE_DIR": str(cache_dir),
|
||||
"CCACHE_NOHASHDIR": "true",
|
||||
"CCACHE_DEPEND": "1",
|
||||
"CCACHE_BASEDIR": str(Path(CORE.build_path).resolve()),
|
||||
}
|
||||
return {k: v for k, v in defaults.items() if k not in os.environ}
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Platform-neutral helpers for ninja-driven native builds."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
|
||||
from esphome.core import EsphomeError
|
||||
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _ninja_runs(binary: str) -> bool:
|
||||
"""Whether the ninja found on PATH actually runs (see tool_version_runs)."""
|
||||
return tool_version_runs(
|
||||
binary,
|
||||
"Ignoring ninja at %s because it failed to run; "
|
||||
"falling back to the bundled wheel",
|
||||
)
|
||||
|
||||
|
||||
def find_ninja() -> Path:
|
||||
"""Locate the ninja binary: a runnable PATH hit first, else the ninja
|
||||
PyPI wheel."""
|
||||
if binary := shutil.which("ninja"):
|
||||
binary = strip_win_long_path_prefix(binary)
|
||||
if _ninja_runs(binary):
|
||||
return Path(binary)
|
||||
import_error: ImportError | None = None
|
||||
try:
|
||||
import ninja
|
||||
except ImportError as err:
|
||||
import_error = err
|
||||
wheel_binary = None
|
||||
else:
|
||||
wheel_binary = Path(ninja.BIN_DIR) / (
|
||||
"ninja.exe" if os.name == "nt" else "ninja"
|
||||
)
|
||||
if wheel_binary is None or not wheel_binary.is_file():
|
||||
raise EsphomeError(
|
||||
"ninja not found on PATH or in the ninja package; reinstall the "
|
||||
"esphome Python environment"
|
||||
) from import_error
|
||||
return wheel_binary
|
||||
|
||||
|
||||
def escape(value: Path | str) -> str:
|
||||
"""Escape a path or token for a ninja file."""
|
||||
return str(value).replace("$", "$$").replace(":", "$:").replace(" ", "$ ")
|
||||
|
||||
|
||||
def quote_arg(tok: str) -> str:
|
||||
"""Quote with the CreateProcess argv rule (as ``subprocess.list2cmdline``):
|
||||
backslash runs double only before a quote. Windows-only; ``$`` must
|
||||
already be doubled for ninja.
|
||||
"""
|
||||
quoted = re.sub(r'(\\*)"', lambda m: m.group(1) * 2 + '\\"', tok)
|
||||
quoted = re.sub(r"(\\+)\Z", lambda m: m.group(1) * 2, quoted)
|
||||
return f'"{quoted}"'
|
||||
|
||||
|
||||
# Force-quote any token containing a character outside the shlex.quote-style
|
||||
# safe set: ninja hands POSIX commands to /bin/sh -c, so bare (, ;, <, *, `
|
||||
# and friends would be re-parsed as shell syntax.
|
||||
_NEEDS_QUOTE = re.compile(r"[^\w@%+=:,./-]")
|
||||
|
||||
|
||||
def shell_token(tok: str, force: bool = False) -> str:
|
||||
"""Re-quote a lexed token for the platform shell; ``force`` always quotes.
|
||||
|
||||
Single quotes on POSIX (/bin/sh), the argv rule on Windows
|
||||
(CreateProcess). ``$`` is doubled first because ninja expands it before
|
||||
the command reaches the shell.
|
||||
"""
|
||||
tok = tok.replace("$", "$$") # ninja would expand a bare $ to nothing
|
||||
if not (force or not tok or _NEEDS_QUOTE.search(tok)):
|
||||
return tok
|
||||
# An empty token must become '' / "" or it vanishes from the argv
|
||||
if os.name == "nt":
|
||||
return quote_arg(tok)
|
||||
# shlex.quote's rule; inlined because the $-doubled token must not be
|
||||
# re-examined for safe characters
|
||||
return "'" + tok.replace("'", "'\"'\"'") + "'"
|
||||
|
||||
|
||||
def quote_path(value: Path | str) -> str:
|
||||
"""Force-quote a path for the ninja command line (shell/CreateProcess)."""
|
||||
return shell_token(str(value), force=True)
|
||||
@@ -0,0 +1,36 @@
|
||||
"""Machine-global tools cache location shared by the native backends."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def tools_cache_path(env_var: str, subdir: str) -> Path:
|
||||
"""A backend's machine-global tools directory, with an env override.
|
||||
|
||||
A blank/whitespace override is treated as unset: ``Path("")`` resolves
|
||||
to the CWD, which ``clean-all`` would then delete.
|
||||
"""
|
||||
import platformdirs
|
||||
|
||||
from esphome.helpers import get_str_env
|
||||
|
||||
if prefix := get_str_env(env_var, "").strip():
|
||||
# resolve(): symlinked prefixes otherwise trip idf.py's
|
||||
# venv-mismatch warning on every build
|
||||
return Path(prefix).expanduser().resolve()
|
||||
# appauthor=False keeps the Windows path short (no vendor segment);
|
||||
# deep IDF trees run into MAX_PATH otherwise
|
||||
return (
|
||||
Path(platformdirs.user_cache_dir("esphome", appauthor=False)) / subdir
|
||||
).resolve()
|
||||
|
||||
|
||||
# (env override, cache subdir) per native backend. writer.clean_all wipes
|
||||
# every entry via tools_cache_path, so listing a cache here is the single
|
||||
# step that registers it for removal; the backends' own path getters use
|
||||
# the same named pairs so the two cannot drift.
|
||||
IDF_TOOLS_CACHE = ("ESPHOME_ESP_IDF_PREFIX", "idf")
|
||||
SDK_NRF_TOOLS_CACHE = ("ESPHOME_SDK_NRF_PREFIX", "sdk-nrf")
|
||||
ARDUINO8266_TOOLS_CACHE = ("ESPHOME_ARDUINO8266_PREFIX", "arduino8266")
|
||||
TOOLS_CACHE_SPECS = (IDF_TOOLS_CACHE, SDK_NRF_TOOLS_CACHE, ARDUINO8266_TOOLS_CACHE)
|
||||
@@ -78,6 +78,7 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
StringRef,
|
||||
arduino_json_ns,
|
||||
bool_,
|
||||
char,
|
||||
const_char_ptr,
|
||||
double,
|
||||
esphome_ns,
|
||||
|
||||
@@ -100,6 +100,21 @@ def _refresh_sidecar() -> bool:
|
||||
)
|
||||
return False
|
||||
if old is not None and old.can_apply_to_core():
|
||||
if (
|
||||
old.toolchain is not None
|
||||
and CORE.toolchain is not None
|
||||
and old.toolchain != CORE.toolchain.value
|
||||
):
|
||||
# Platforms normalize toolchain-sensitive keys differently;
|
||||
# never cache a config validated under a different toolchain
|
||||
# than the compile's
|
||||
_LOGGER.debug(
|
||||
"Not caching: config validated with toolchain %r but the "
|
||||
"last compile used %r",
|
||||
CORE.toolchain.value,
|
||||
old.toolchain,
|
||||
)
|
||||
return False
|
||||
# Compile-written; nothing to refresh.
|
||||
return True
|
||||
if CORE.build_path is not None and CORE.build_path.exists():
|
||||
|
||||
@@ -13,6 +13,7 @@ from esphome.components.esp32 import (
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32S31,
|
||||
get_esp32_variant,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
@@ -156,6 +157,17 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
|
||||
9: adc_channel_t.ADC_CHANNEL_8,
|
||||
10: adc_channel_t.ADC_CHANNEL_9,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h
|
||||
VARIANT_ESP32S31: {
|
||||
42: adc_channel_t.ADC_CHANNEL_0,
|
||||
43: adc_channel_t.ADC_CHANNEL_1,
|
||||
44: adc_channel_t.ADC_CHANNEL_2,
|
||||
45: adc_channel_t.ADC_CHANNEL_3,
|
||||
46: adc_channel_t.ADC_CHANNEL_4,
|
||||
47: adc_channel_t.ADC_CHANNEL_5,
|
||||
48: adc_channel_t.ADC_CHANNEL_6,
|
||||
49: adc_channel_t.ADC_CHANNEL_7,
|
||||
},
|
||||
}
|
||||
|
||||
# pin to adc2 channel mapping
|
||||
@@ -225,6 +237,17 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
|
||||
19: adc_channel_t.ADC_CHANNEL_8,
|
||||
20: adc_channel_t.ADC_CHANNEL_9,
|
||||
},
|
||||
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h
|
||||
VARIANT_ESP32S31: {
|
||||
50: adc_channel_t.ADC_CHANNEL_0,
|
||||
51: adc_channel_t.ADC_CHANNEL_1,
|
||||
52: adc_channel_t.ADC_CHANNEL_2,
|
||||
53: adc_channel_t.ADC_CHANNEL_3,
|
||||
54: adc_channel_t.ADC_CHANNEL_4,
|
||||
55: adc_channel_t.ADC_CHANNEL_5,
|
||||
56: adc_channel_t.ADC_CHANNEL_6,
|
||||
57: adc_channel_t.ADC_CHANNEL_7,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -74,9 +74,7 @@ void ADCSensor::setup() {
|
||||
if (this->calibration_handle_ == nullptr) {
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
// RISC-V variants (except C2) and S3 use curve fitting calibration
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
@@ -94,7 +92,7 @@ void ADCSensor::setup() {
|
||||
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
|
||||
this->setup_flags_.calibration_complete = false;
|
||||
}
|
||||
#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_line_fitting_config_t cali_config = {
|
||||
.unit_id = this->adc_unit_,
|
||||
.atten = this->attenuation_,
|
||||
@@ -112,7 +110,11 @@ void ADCSensor::setup() {
|
||||
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
|
||||
this->setup_flags_.calibration_complete = false;
|
||||
}
|
||||
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
#else // No calibration scheme available
|
||||
(void) handle;
|
||||
ESP_LOGD(TAG, "No calibration scheme for this variant, readings are uncalibrated");
|
||||
this->setup_flags_.calibration_complete = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
this->setup_flags_.init_complete = true;
|
||||
@@ -121,23 +123,28 @@ void ADCSensor::setup() {
|
||||
void ADCSensor::dump_config() {
|
||||
LOG_SENSOR("", "ADC Sensor", this);
|
||||
LOG_PIN(" Pin: ", this->pin_);
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
" Channel: %d\n"
|
||||
" Unit: %s\n"
|
||||
" Attenuation: %s\n"
|
||||
" Samples: %i\n"
|
||||
" Sampling mode: %s\n"
|
||||
" Setup Status:\n"
|
||||
" Handle Init: %s\n"
|
||||
" Config: %s\n"
|
||||
" Calibration: %s\n"
|
||||
" Overall Init: %s",
|
||||
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
|
||||
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
|
||||
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
|
||||
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
|
||||
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) || defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
const char *calibration_status = this->setup_flags_.calibration_complete ? "OK" : "FAILED";
|
||||
#else
|
||||
const char *calibration_status = "N/A"; // This variant has no calibration scheme
|
||||
#endif
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Channel: %d\n"
|
||||
" Unit: %s\n"
|
||||
" Attenuation: %s\n"
|
||||
" Samples: %i\n"
|
||||
" Sampling mode: %s\n"
|
||||
" Setup Status:\n"
|
||||
" Handle Init: %s\n"
|
||||
" Config: %s\n"
|
||||
" Calibration: %s\n"
|
||||
" Overall Init: %s",
|
||||
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
|
||||
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
|
||||
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
|
||||
this->setup_flags_.handle_init_complete ? "OK" : "FAILED",
|
||||
this->setup_flags_.config_complete ? "OK" : "FAILED", calibration_status,
|
||||
this->setup_flags_.init_complete ? "OK" : "FAILED");
|
||||
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
}
|
||||
@@ -184,12 +191,11 @@ float ADCSensor::sample_fixed_attenuation_() {
|
||||
} else {
|
||||
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else // Other ESP32 variants use line fitting calibration
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
|
||||
#endif
|
||||
this->calibration_handle_ = nullptr;
|
||||
}
|
||||
}
|
||||
@@ -217,10 +223,9 @@ float ADCSensor::sample_autorange_() {
|
||||
// Need to recalibrate for the new attenuation
|
||||
if (this->calibration_handle_ != nullptr) {
|
||||
// Delete old calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
|
||||
#else
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
|
||||
#endif
|
||||
this->calibration_handle_ = nullptr;
|
||||
@@ -229,8 +234,7 @@ float ADCSensor::sample_autorange_() {
|
||||
// Create new calibration handle for this attenuation
|
||||
adc_cali_handle_t handle = nullptr;
|
||||
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_curve_fitting_config_t cali_config = {};
|
||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
|
||||
cali_config.chan = this->channel_;
|
||||
@@ -242,7 +246,7 @@ float ADCSensor::sample_autorange_() {
|
||||
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
|
||||
ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten,
|
||||
(err == ESP_OK) ? "SUCCESS" : "FAILED", err);
|
||||
#else
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_line_fitting_config_t cali_config = {
|
||||
.unit_id = this->adc_unit_,
|
||||
.atten = atten,
|
||||
@@ -264,10 +268,9 @@ float ADCSensor::sample_autorange_() {
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
|
||||
if (handle != nullptr) {
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
#endif
|
||||
}
|
||||
@@ -286,10 +289,9 @@ float ADCSensor::sample_autorange_() {
|
||||
ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage);
|
||||
}
|
||||
// Clean up calibration handle
|
||||
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
|
||||
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
|
||||
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_curve_fitting(handle);
|
||||
#else
|
||||
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
|
||||
adc_cali_delete_scheme_line_fitting(handle);
|
||||
#endif
|
||||
} else {
|
||||
|
||||
@@ -3,6 +3,7 @@ import logging
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, voltage_sampler
|
||||
from esphome.components.esp32 import (
|
||||
VARIANT_ESP32S31,
|
||||
get_esp32_variant,
|
||||
include_builtin_idf_component,
|
||||
require_adc_oneshot_iram,
|
||||
@@ -56,6 +57,14 @@ def validate_config(config: ConfigType) -> ConfigType:
|
||||
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
|
||||
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
|
||||
|
||||
# The S31 ADC supports a single attenuation level (SOC_ADC_ATTEN_NUM is 1)
|
||||
if (
|
||||
CORE.is_esp32
|
||||
and get_esp32_variant() == VARIANT_ESP32S31
|
||||
and config.get(CONF_ATTENUATION, "0db") != "0db"
|
||||
):
|
||||
raise cv.Invalid("ESP32-S31 only supports 'attenuation: 0db'")
|
||||
|
||||
if config.get(CONF_ATTENUATION, None) == "auto" and config.get(CONF_SAMPLES, 1) > 1:
|
||||
raise cv.Invalid(
|
||||
"Automatic attenuation cannot be used when multisampling is set"
|
||||
|
||||
@@ -162,8 +162,9 @@ void Alpha3::send_request_(uint8_t *request, size_t len) {
|
||||
auto status =
|
||||
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
|
||||
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (status)
|
||||
if (status) {
|
||||
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
|
||||
}
|
||||
}
|
||||
|
||||
void Alpha3::update() {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import logging
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from esphome import automation
|
||||
from esphome.automation import Condition
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_DESCRIPTION
|
||||
from esphome.components.logger import request_log_listener
|
||||
|
||||
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
|
||||
@@ -40,10 +42,12 @@ from esphome.const import (
|
||||
CONF_TAG,
|
||||
CONF_THEN,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
CONF_VARIABLES,
|
||||
)
|
||||
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
|
||||
from esphome.cpp_generator import MockObj, TemplateArgsType
|
||||
from esphome.helpers import fnv1_hash
|
||||
from esphome.types import ConfigFragmentType, ConfigType
|
||||
|
||||
# Compat alias: downstream consumers (e.g. device-builder) referenced the
|
||||
@@ -124,6 +128,7 @@ SERVICE_ARG_FALLBACK_TYPES: dict[str, MockObj] = {
|
||||
}
|
||||
CONF_BATCH_DELAY = "batch_delay"
|
||||
CONF_CUSTOM_SERVICES = "custom_services"
|
||||
CONF_EXAMPLE = "example"
|
||||
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
|
||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
|
||||
CONF_LISTEN_BACKLOG = "listen_backlog"
|
||||
@@ -227,14 +232,30 @@ def _validate_supports_response(value: Any) -> str:
|
||||
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
|
||||
|
||||
|
||||
# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small
|
||||
ESP8266_ACTION_STRINGS_MAX_TOTAL = 384
|
||||
|
||||
VARIABLE_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
|
||||
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
|
||||
cv.Optional(CONF_EXAMPLE): cv.string_strict,
|
||||
}
|
||||
)
|
||||
|
||||
# Accepts the plain `name: type` shorthand or the full mapping form
|
||||
validate_variable = cv.maybe_simple_value(VARIABLE_SCHEMA, key=CONF_TYPE)
|
||||
|
||||
|
||||
ACTIONS_SCHEMA = automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
|
||||
cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name,
|
||||
cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name,
|
||||
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
|
||||
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
|
||||
{
|
||||
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
|
||||
cv.validate_id_name: validate_variable,
|
||||
}
|
||||
),
|
||||
# No default - auto-detected by _auto_detect_supports_response
|
||||
@@ -351,6 +372,85 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
|
||||
|
||||
def _has_action_metadata(actions: list[ConfigType]) -> bool:
|
||||
# Empty strings count as unset, matching _action_strings
|
||||
return any(
|
||||
conf.get(CONF_DESCRIPTION)
|
||||
or any(
|
||||
var_.get(CONF_DESCRIPTION) or var_.get(CONF_EXAMPLE)
|
||||
for var_ in conf[CONF_VARIABLES].values()
|
||||
)
|
||||
for conf in actions
|
||||
)
|
||||
|
||||
|
||||
def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
|
||||
"""Strings of one action in the table order UserServiceStatic (user_services.h) expects."""
|
||||
# An empty description or example is treated as unset
|
||||
strings: list[str | None] = [conf[CONF_ACTION]]
|
||||
if has_metadata:
|
||||
strings.append(conf.get(CONF_DESCRIPTION) or None)
|
||||
for name, var_ in conf[CONF_VARIABLES].items():
|
||||
strings.append(name)
|
||||
if has_metadata:
|
||||
strings += [
|
||||
var_.get(CONF_DESCRIPTION) or None,
|
||||
var_.get(CONF_EXAMPLE) or None,
|
||||
]
|
||||
return strings
|
||||
|
||||
|
||||
def _action_strings_size(strings: list[str | None]) -> int:
|
||||
"""Bytes needed to copy every string out of flash, each with its terminator."""
|
||||
return sum(
|
||||
len(string.encode("utf-8")) + 1 for string in strings if string is not None
|
||||
)
|
||||
|
||||
|
||||
def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
|
||||
if not CORE.is_esp8266:
|
||||
return config
|
||||
actions = config.get(CONF_ACTIONS, [])
|
||||
has_metadata = _has_action_metadata(actions)
|
||||
for conf in actions:
|
||||
size = _action_strings_size(_action_strings(conf, has_metadata))
|
||||
if size > ESP8266_ACTION_STRINGS_MAX_TOTAL:
|
||||
raise cv.Invalid(
|
||||
f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, "
|
||||
f"description and example text; ESP8266 allows at most "
|
||||
f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _validate_esp8266_action_strings
|
||||
|
||||
|
||||
def _add_action_strings(
|
||||
index: int, strings: list[str | None], interned: dict[str, MockObj]
|
||||
) -> MockObj:
|
||||
"""Emit the PROGMEM string table for one action.
|
||||
|
||||
Each string is its own PROGMEM array because on ESP8266 .rodata is RAM, and identical
|
||||
strings are shared between actions through `interned`.
|
||||
"""
|
||||
entries: list[MockObj] = []
|
||||
for string in strings:
|
||||
if string is None:
|
||||
entries.append(cg.nullptr)
|
||||
continue
|
||||
if (var := interned.get(string)) is None:
|
||||
var = interned[string] = cg.progmem_array(
|
||||
ID(f"api_action_str{len(interned)}", is_declaration=True, type=cg.char),
|
||||
string,
|
||||
)
|
||||
entries.append(var)
|
||||
return cg.progmem_array(
|
||||
ID(f"api_action{index}_strings", is_declaration=True, type=cg.const_char_ptr),
|
||||
entries,
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.WEB)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -370,8 +470,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
|
||||
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
|
||||
|
||||
actions = config.get(CONF_ACTIONS, [])
|
||||
has_user_actions = bool(actions) or config[CONF_CUSTOM_SERVICES]
|
||||
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
|
||||
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
|
||||
if has_user_actions:
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTIONS")
|
||||
|
||||
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
|
||||
@@ -384,10 +486,17 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config[CONF_HOMEASSISTANT_STATES]:
|
||||
cg.add_define("USE_API_HOMEASSISTANT_STATES")
|
||||
|
||||
if actions := config.get(CONF_ACTIONS, []):
|
||||
scratch_size = 0
|
||||
if actions:
|
||||
# Metadata is compiled in for every action once any action declares it, because the
|
||||
# string table layout is fixed by the define rather than per action
|
||||
has_metadata = _has_action_metadata(actions)
|
||||
if has_metadata:
|
||||
cg.add_define("USE_API_USER_DEFINED_ACTION_METADATA")
|
||||
interned_strings: dict[str, MockObj] = {}
|
||||
# Collect all triggers first, then register all at once with initializer_list
|
||||
triggers: list[cg.MockObj] = []
|
||||
for conf in actions:
|
||||
for index, conf in enumerate(actions):
|
||||
func_args: list[tuple[MockObj, str]] = []
|
||||
service_template_args: list[MockObj] = [] # User service argument types
|
||||
|
||||
@@ -420,22 +529,23 @@ async def to_code(config: ConfigType) -> None:
|
||||
conf.get(CONF_THEN, [])
|
||||
)
|
||||
|
||||
service_arg_names: list[str] = []
|
||||
for name, var_ in conf[CONF_VARIABLES].items():
|
||||
if has_non_synchronous and var_ in SERVICE_ARG_FALLBACK_TYPES:
|
||||
native = SERVICE_ARG_FALLBACK_TYPES[var_]
|
||||
var_type = var_[CONF_TYPE]
|
||||
if has_non_synchronous and var_type in SERVICE_ARG_FALLBACK_TYPES:
|
||||
native = SERVICE_ARG_FALLBACK_TYPES[var_type]
|
||||
else:
|
||||
native = SERVICE_ARG_NATIVE_TYPES[var_]
|
||||
native = SERVICE_ARG_NATIVE_TYPES[var_type]
|
||||
service_template_args.append(native)
|
||||
func_args.append((native, name))
|
||||
service_arg_names.append(name)
|
||||
strings = _action_strings(conf, has_metadata)
|
||||
table = _add_action_strings(index, strings, interned_strings)
|
||||
if CORE.is_esp8266:
|
||||
scratch_size = max(scratch_size, _action_strings_size(strings))
|
||||
# Template args: supports_response mode, then user service arg types
|
||||
templ = cg.TemplateArguments(supports_response, *service_template_args)
|
||||
# Key is hashed here because the name is not readable at runtime on ESP8266
|
||||
trigger = cg.new_Pvariable(
|
||||
conf[CONF_TRIGGER_ID],
|
||||
templ,
|
||||
conf[CONF_ACTION],
|
||||
service_arg_names,
|
||||
conf[CONF_TRIGGER_ID], templ, table, fnv1_hash(conf[CONF_ACTION])
|
||||
)
|
||||
triggers.append(trigger)
|
||||
auto = await automation.build_automation(trigger, func_args, conf)
|
||||
@@ -457,6 +567,9 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add(auto.add_actions([unregister_action]))
|
||||
# Register all services at once - single allocation, no reallocations
|
||||
cg.add(var.initialize_user_services(triggers))
|
||||
if CORE.is_esp8266 and has_user_actions:
|
||||
# Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action
|
||||
cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1))
|
||||
|
||||
if CONF_ON_CLIENT_CONNECTED in config:
|
||||
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
|
||||
@@ -499,6 +612,40 @@ async def to_code(config: ConfigType) -> None:
|
||||
|
||||
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
|
||||
|
||||
_ID_CALL_PROG = re.compile(r"\bid\s*\(")
|
||||
|
||||
|
||||
# Remove before 2027.3.0: untagged strings that look like lambda source keep
|
||||
# being compiled as lambdas during the deprecation window
|
||||
def _coerce_implicit_lambda(value: Any) -> Any:
|
||||
if not isinstance(value, str):
|
||||
return value
|
||||
if cv.looks_like_returning_lambda(value):
|
||||
_LOGGER.warning(
|
||||
"[api] The 'variables' value '%s' looks like a lambda but is "
|
||||
"missing the !lambda tag. It is compiled as a lambda for now but "
|
||||
"will be sent as literal text from 2027.3.0. Add !lambda to keep "
|
||||
"it evaluated; literal text belongs under 'data:'.",
|
||||
value,
|
||||
)
|
||||
# cv.templatable runs returning_lambda on the coerced Lambda
|
||||
return cv.lambda_(value)
|
||||
if _ID_CALL_PROG.search(value):
|
||||
# lambda source without a return: issue 5394's mistake class
|
||||
_LOGGER.warning(
|
||||
"[api] The 'variables' value '%s' is sent as literal text; wrap "
|
||||
"it in !lambda 'return ...;' to evaluate it instead.",
|
||||
value,
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
# Static strings or !lambda values. cv.templatable stays introspectable for
|
||||
# schema tooling; removing the shim leaves KEY_VALUE_SCHEMA.
|
||||
VARIABLES_SCHEMA = cv.Schema(
|
||||
{cv.string: cv.All(_coerce_implicit_lambda, cv.templatable(cv.string_strict))}
|
||||
)
|
||||
|
||||
|
||||
def _validate_response_config(config: ConfigType) -> ConfigType:
|
||||
# Validate dependencies:
|
||||
@@ -535,9 +682,7 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA,
|
||||
cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA,
|
||||
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
|
||||
{cv.string: cv.returning_lambda}
|
||||
),
|
||||
cv.Optional(CONF_VARIABLES, default={}): VARIABLES_SCHEMA,
|
||||
cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
|
||||
cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True),
|
||||
@@ -598,6 +743,8 @@ async def homeassistant_service_to_code(
|
||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||
for key, value in config[CONF_VARIABLES].items():
|
||||
templ = await cg.templatable(value, args, None)
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
if on_error := config.get(CONF_ON_ERROR):
|
||||
@@ -652,7 +799,7 @@ HOMEASSISTANT_EVENT_ACTION_SCHEMA = cv.Schema(
|
||||
cv.Required(CONF_EVENT): validate_homeassistant_event,
|
||||
cv.Optional(CONF_DATA, default={}): KEY_VALUE_SCHEMA,
|
||||
cv.Optional(CONF_DATA_TEMPLATE, default={}): KEY_VALUE_SCHEMA,
|
||||
cv.Optional(CONF_VARIABLES, default={}): KEY_VALUE_SCHEMA,
|
||||
cv.Optional(CONF_VARIABLES, default={}): VARIABLES_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -698,6 +845,8 @@ async def homeassistant_event_to_code(
|
||||
cg.add(var.init_variables(len(config[CONF_VARIABLES])))
|
||||
for key, value in config[CONF_VARIABLES].items():
|
||||
templ = await cg.templatable(value, args, None)
|
||||
if isinstance(templ, str):
|
||||
templ = cg.FlashStringLiteral(templ)
|
||||
cg.add(var.add_variable(cg.FlashStringLiteral(key), templ))
|
||||
|
||||
return var
|
||||
|
||||
@@ -1034,6 +1034,8 @@ message ListEntitiesServicesArgument {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
string name = 1;
|
||||
ServiceArgType type = 2;
|
||||
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
|
||||
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
|
||||
}
|
||||
message ListEntitiesServicesResponse {
|
||||
option (id) = 41;
|
||||
@@ -1044,6 +1046,7 @@ message ListEntitiesServicesResponse {
|
||||
fixed32 key = 2 [(force) = true];
|
||||
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
|
||||
SupportsResponseType supports_response = 4;
|
||||
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
|
||||
}
|
||||
message ExecuteServiceArgument {
|
||||
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
|
||||
@@ -1654,7 +1657,8 @@ message ListEntitiesMediaPlayerResponse {
|
||||
bool disabled_by_default = 6;
|
||||
EntityCategory entity_category = 7;
|
||||
|
||||
bool supports_pause = 8;
|
||||
// Deprecated in ESPHome 2026.9.0; use feature_flags instead.
|
||||
bool supports_pause = 8 [deprecated = true];
|
||||
|
||||
repeated MediaPlayerSupportedFormat supported_formats = 9;
|
||||
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
#include "api_buffer.h"
|
||||
#include <new>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
void APIBuffer::grow_(size_t n) {
|
||||
auto new_data = make_buffer(n);
|
||||
bool APIBuffer::grow_(size_t n) {
|
||||
// nothrow (no zero-fill) so OOM is reportable; plain new aborts instead
|
||||
// (NEW_OOM_ABORT on ESP8266 Arduino, exception stub on ESP-IDF).
|
||||
// RAMAllocator is no fit here: unique_ptr needs delete[]-compatible memory.
|
||||
std::unique_ptr<uint8_t[]> new_data(new (std::nothrow) uint8_t[n]);
|
||||
if (new_data == nullptr)
|
||||
return false;
|
||||
if (this->size_)
|
||||
std::memcpy(new_data.get(), this->data_.get(), this->size_);
|
||||
this->data_ = std::move(new_data);
|
||||
this->capacity_ = n;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -9,16 +9,6 @@
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
/// Helper to use make_unique_for_overwrite where available (skips zero-fill),
|
||||
/// falling back to make_unique on older GCC (ESP8266, LibreTiny).
|
||||
inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) {
|
||||
#if defined(USE_ESP8266) || defined(USE_LIBRETINY)
|
||||
return std::make_unique<uint8_t[]>(n);
|
||||
#else
|
||||
return std::make_unique_for_overwrite<uint8_t[]>(n);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// Byte buffer that skips zero-initialization on resize().
|
||||
///
|
||||
/// std::vector<uint8_t>::resize() zero-fills new bytes via memset. For the
|
||||
@@ -36,23 +26,23 @@ inline std::unique_ptr<uint8_t[]> make_buffer(size_t n) {
|
||||
class APIBuffer {
|
||||
public:
|
||||
void clear() { this->size_ = 0; }
|
||||
inline void reserve(size_t n) ESPHOME_ALWAYS_INLINE {
|
||||
if (n > this->capacity_)
|
||||
this->grow_(n);
|
||||
}
|
||||
inline void resize(size_t n) ESPHOME_ALWAYS_INLINE {
|
||||
this->reserve(n);
|
||||
this->size_ = n; // no zero-fill
|
||||
}
|
||||
/// Returns false if allocation fails; the buffer is left unchanged.
|
||||
[[nodiscard]] inline bool reserve(size_t n) ESPHOME_ALWAYS_INLINE { return n <= this->capacity_ || this->grow_(n); }
|
||||
/// Returns false if allocation fails; the buffer is left unchanged. No zero-fill.
|
||||
[[nodiscard]] inline bool resize(size_t n) ESPHOME_ALWAYS_INLINE { return this->reserve_and_resize(n, n); }
|
||||
/// 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));
|
||||
/// Returns false if allocation fails; the buffer is left unchanged.
|
||||
[[nodiscard]] inline bool reserve_and_resize(size_t reserve_size, size_t new_size) ESPHOME_ALWAYS_INLINE {
|
||||
if (!this->reserve(std::max(reserve_size, new_size)))
|
||||
return false;
|
||||
this->size_ = new_size;
|
||||
return true;
|
||||
}
|
||||
uint8_t *data() { return this->data_.get(); }
|
||||
const uint8_t *data() const { return this->data_.get(); }
|
||||
size_t size() const { return this->size_; }
|
||||
size_t capacity() const { return this->capacity_; }
|
||||
bool empty() const { return this->size_ == 0; }
|
||||
uint8_t &operator[](size_t i) { return this->data_[i]; }
|
||||
const uint8_t &operator[](size_t i) const { return this->data_[i]; }
|
||||
@@ -64,7 +54,7 @@ class APIBuffer {
|
||||
}
|
||||
|
||||
protected:
|
||||
void grow_(size_t n);
|
||||
bool grow_(size_t n);
|
||||
std::unique_ptr<uint8_t[]> data_;
|
||||
size_t size_{0};
|
||||
size_t capacity_{0};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#include "api_connection.h"
|
||||
#ifdef USE_API
|
||||
#include "api_connection_buffer.h" // for encode_to_buffer / get_batch_delay_ms_ inlines
|
||||
#include "api_connection_buffer.h" // for the APIServer-dependent APIConnection inlines
|
||||
#ifdef USE_API_NOISE
|
||||
#include "api_frame_helper_noise.h"
|
||||
#endif
|
||||
@@ -1099,7 +1099,6 @@ uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnec
|
||||
auto *media_player = static_cast<media_player::MediaPlayer *>(entity);
|
||||
ListEntitiesMediaPlayerResponse msg;
|
||||
auto traits = media_player->get_traits();
|
||||
msg.supports_pause = traits.get_supports_pause();
|
||||
msg.feature_flags = traits.get_feature_flags();
|
||||
for (auto &supported_format : traits.get_supported_formats()) {
|
||||
msg.supported_formats.emplace_back();
|
||||
@@ -2240,10 +2239,17 @@ bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type,
|
||||
this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
|
||||
}
|
||||
#endif
|
||||
if (!this->prepare_first_message_buffer(payload_size)) [[unlikely]] {
|
||||
this->fatal_out_of_memory_();
|
||||
return false;
|
||||
}
|
||||
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
this->prepare_first_message_buffer(shared_buf, payload_size);
|
||||
size_t write_start = shared_buf.size();
|
||||
shared_buf.resize(write_start + payload_size);
|
||||
#ifdef ESPHOME_DEBUG_API
|
||||
assert(shared_buf.capacity() >= write_start + payload_size);
|
||||
#endif
|
||||
// Capacity reserved above, cannot fail
|
||||
(void) shared_buf.resize(write_start + payload_size);
|
||||
ProtoWriteBuffer buffer{&shared_buf, write_start};
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
|
||||
@@ -2279,6 +2285,9 @@ void APIConnection::on_no_setup_connection() {
|
||||
this->on_fatal_error();
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
|
||||
}
|
||||
void APIConnection::fatal_out_of_memory_() {
|
||||
this->fatal_error_with_log_(LOG_STR("Out of memory"), APIError::OUT_OF_MEMORY);
|
||||
}
|
||||
void APIConnection::on_fatal_error() {
|
||||
// Don't close socket here - keep it open so getpeername() works for logging
|
||||
// Socket will be closed when client is removed from the list in APIServer::loop()
|
||||
@@ -2293,16 +2302,25 @@ bool APIConnection::schedule_message_front_(EntityBase *entity, uint16_t message
|
||||
bool APIConnection::send_message_smart_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
|
||||
uint8_t aux_data_index) {
|
||||
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
|
||||
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
this->prepare_first_message_buffer(shared_buf, estimated_size);
|
||||
// No local for the shared buffer here: keeping it live across
|
||||
// dispatch_message_ costs a register and spills message_type into the
|
||||
// batching path's dedup loop (measured on x86 GCC -Os)
|
||||
if (!this->prepare_first_message_buffer(estimated_size)) [[unlikely]] {
|
||||
this->fatal_out_of_memory_();
|
||||
return false;
|
||||
}
|
||||
DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index};
|
||||
if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) &&
|
||||
this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type)) {
|
||||
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_batch_item_(item);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
// An OOM during the immediate attempt marks the connection for removal;
|
||||
// don't queue more work (schedule_message_'s push_back may allocate again)
|
||||
if (this->flags_.remove) [[unlikely]]
|
||||
return false;
|
||||
}
|
||||
return this->schedule_message_(entity, message_type, estimated_size, aux_data_index);
|
||||
}
|
||||
@@ -2352,7 +2370,11 @@ void APIConnection::process_batch_() {
|
||||
total_estimated_size = MAX_BATCH_PACKET_SIZE;
|
||||
}
|
||||
|
||||
this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
|
||||
if (!this->prepare_first_message_buffer(header_padding, total_estimated_size)) [[unlikely]] {
|
||||
this->fatal_out_of_memory_();
|
||||
this->clear_batch_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Fast path for single message - buffer already allocated above
|
||||
if (num_items == 1) {
|
||||
@@ -2367,8 +2389,11 @@ void APIConnection::process_batch_() {
|
||||
#endif
|
||||
this->clear_batch_();
|
||||
} else if (payload_size == 0) {
|
||||
// Message too large to fit in available space
|
||||
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
|
||||
// payload_size == 0 with remove set means encoding hit OOM and the
|
||||
// connection is being dropped; warn only for a genuinely oversized message
|
||||
if (!this->flags_.remove) {
|
||||
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
|
||||
}
|
||||
this->clear_batch_();
|
||||
}
|
||||
return;
|
||||
@@ -2431,8 +2456,10 @@ void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items
|
||||
|
||||
if (items_processed > 0) {
|
||||
// Add footer space for the last message (for Noise protocol MAC)
|
||||
if (footer_size > 0) {
|
||||
shared_buf.resize(shared_buf.size() + footer_size);
|
||||
if (footer_size > 0 && !shared_buf.resize(shared_buf.size() + footer_size)) [[unlikely]] {
|
||||
this->fatal_out_of_memory_();
|
||||
this->clear_batch_();
|
||||
return;
|
||||
}
|
||||
|
||||
// Send all collected messages
|
||||
|
||||
@@ -352,22 +352,13 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
}
|
||||
}
|
||||
|
||||
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t header_padding, size_t total_size) {
|
||||
shared_buf.clear();
|
||||
// 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);
|
||||
}
|
||||
/// Clear the shared write buffer and reserve space for the first message.
|
||||
/// Returns false if the allocation fails (out of memory).
|
||||
/// Defined in api_connection_buffer.h (needs APIServer complete).
|
||||
[[nodiscard]] bool prepare_first_message_buffer(size_t header_padding, size_t total_size);
|
||||
|
||||
// Convenience overload - computes frame overhead internally
|
||||
void prepare_first_message_buffer(APIBuffer &shared_buf, size_t payload_size) {
|
||||
const uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
const uint8_t footer_size = this->helper_->frame_footer_size();
|
||||
this->prepare_first_message_buffer(shared_buf, header_padding, payload_size + header_padding + footer_size);
|
||||
}
|
||||
[[nodiscard]] bool prepare_first_message_buffer(size_t payload_size);
|
||||
|
||||
bool try_to_clear_buffer(bool log_out_of_space) {
|
||||
if (this->flags_.remove)
|
||||
@@ -853,6 +844,9 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
this->on_fatal_error();
|
||||
this->log_warning_(message, err);
|
||||
}
|
||||
// Shared cold path for buffer allocation failures — noinline keeps the
|
||||
// OOM handling out of the hot send paths
|
||||
void __attribute__((noinline)) fatal_out_of_memory_();
|
||||
};
|
||||
|
||||
} // namespace esphome::api
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_API
|
||||
|
||||
// Inline APIConnection methods that need APIServer complete. Include this
|
||||
// instead of api_connection.h when calling encode_to_buffer or get_batch_delay_ms_.
|
||||
// Inline APIConnection members that need APIServer complete. Include this
|
||||
// instead of api_connection.h when calling them.
|
||||
|
||||
#include "api_connection.h"
|
||||
#include "api_server.h"
|
||||
@@ -41,7 +41,10 @@ inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t c
|
||||
return 0;
|
||||
|
||||
auto &shared_buf = conn->parent_->get_shared_buffer_ref();
|
||||
shared_buf.resize(shared_buf.size() + to_add);
|
||||
if (!shared_buf.resize(shared_buf.size() + to_add)) [[unlikely]] {
|
||||
conn->fatal_out_of_memory_();
|
||||
return 0;
|
||||
}
|
||||
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
|
||||
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
|
||||
|
||||
@@ -50,5 +53,22 @@ inline uint16_t ESPHOME_ALWAYS_INLINE APIConnection::encode_to_buffer(uint32_t c
|
||||
|
||||
inline uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
|
||||
|
||||
inline bool APIConnection::prepare_first_message_buffer(size_t header_padding, size_t total_size) {
|
||||
auto &shared_buf = this->parent_->get_shared_buffer_ref();
|
||||
shared_buf.clear();
|
||||
// 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
|
||||
return shared_buf.reserve_and_resize(total_size, header_padding);
|
||||
}
|
||||
|
||||
inline bool APIConnection::prepare_first_message_buffer(size_t payload_size) {
|
||||
const uint8_t header_padding = this->helper_->frame_header_padding();
|
||||
const uint8_t footer_size = this->helper_->frame_footer_size();
|
||||
return this->prepare_first_message_buffer(header_padding, payload_size + header_padding + footer_size);
|
||||
}
|
||||
|
||||
} // namespace esphome::api
|
||||
#endif
|
||||
|
||||
@@ -172,7 +172,7 @@ APIError APIFrameHelper::write_raw_iov_(const struct iovec *iov, int iovcnt, uin
|
||||
|
||||
// Queue unsent data into overflow buffer
|
||||
if (!this->overflow_buf_.enqueue_iov(iov, iovcnt, total_write_len, static_cast<uint16_t>(sent))) {
|
||||
HELPER_LOG("Overflow buffer full, dropping connection");
|
||||
HELPER_LOG("Overflow buffer full or out of memory, dropping connection");
|
||||
this->state_ = State::FAILED;
|
||||
return APIError::SOCKET_WRITE_FAILED;
|
||||
}
|
||||
|
||||
@@ -68,7 +68,10 @@ APIError APINoiseFrameHelper::init() {
|
||||
|
||||
// init prologue
|
||||
size_t old_size = prologue_.size();
|
||||
prologue_.resize(old_size + PROLOGUE_INIT_LEN);
|
||||
if (!prologue_.resize(old_size + PROLOGUE_INIT_LEN)) [[unlikely]] {
|
||||
state_ = State::FAILED;
|
||||
return APIError::OUT_OF_MEMORY;
|
||||
}
|
||||
#ifdef USE_ESP8266
|
||||
memcpy_P(prologue_.data() + old_size, PROLOGUE_INIT, PROLOGUE_INIT_LEN);
|
||||
#else
|
||||
@@ -202,7 +205,10 @@ APIError APINoiseFrameHelper::try_read_frame_() {
|
||||
// During handshake, rx_buf_.size() is used in prologue construction, so
|
||||
// the buffer must be exactly msg_size to avoid prologue mismatch.)
|
||||
uint16_t alloc_size = msg_size + (is_data ? RX_BUF_NULL_TERMINATOR : 0);
|
||||
this->rx_buf_.resize(alloc_size);
|
||||
if (!this->rx_buf_.resize(alloc_size)) [[unlikely]] {
|
||||
state_ = State::FAILED;
|
||||
return APIError::OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
if (rx_buf_len_ < msg_size) {
|
||||
// more data to read
|
||||
@@ -269,7 +275,10 @@ APIError APINoiseFrameHelper::state_action_client_hello_() {
|
||||
// Resize for: existing prologue + 2 size bytes + frame data
|
||||
size_t old_size = this->prologue_.size();
|
||||
size_t rx_size = this->rx_buf_.size();
|
||||
this->prologue_.resize(old_size + 2 + rx_size);
|
||||
if (!this->prologue_.resize(old_size + 2 + rx_size)) [[unlikely]] {
|
||||
state_ = State::FAILED;
|
||||
return APIError::OUT_OF_MEMORY;
|
||||
}
|
||||
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
|
||||
this->prologue_[old_size + 1] = (uint8_t) rx_size;
|
||||
if (rx_size > 0) {
|
||||
@@ -477,13 +486,15 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
|
||||
assert(this->state_ == State::DATA);
|
||||
#endif
|
||||
|
||||
APIBuffer *buf = buffer.get_buffer();
|
||||
// Resize buffer to include footer space for Noise MAC
|
||||
if (this->frame_footer_size_)
|
||||
buffer.get_buffer()->resize(buffer.get_buffer()->size() + this->frame_footer_size_);
|
||||
if (this->frame_footer_size_ && !buf->resize(buf->size() + this->frame_footer_size_)) [[unlikely]] {
|
||||
state_ = State::FAILED;
|
||||
return APIError::OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
uint16_t payload_size =
|
||||
static_cast<uint16_t>(buffer.get_buffer()->size() - HEADER_PADDING - this->frame_footer_size_);
|
||||
uint8_t *buf_start = buffer.get_buffer()->data();
|
||||
uint16_t payload_size = static_cast<uint16_t>(buf->size() - HEADER_PADDING - this->frame_footer_size_);
|
||||
uint8_t *buf_start = buf->data();
|
||||
uint16_t encrypted_len;
|
||||
APIError aerr = this->encrypt_noise_message_(buf_start, payload_size, type, encrypted_len);
|
||||
if (aerr != APIError::OK)
|
||||
|
||||
@@ -172,7 +172,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
||||
|
||||
// Reserve space for body (+ null terminator so protobuf StringRef fields
|
||||
// can be safely null-terminated in-place after decode)
|
||||
this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR);
|
||||
if (!this->rx_buf_.resize(this->rx_header_parsed_len_ + RX_BUF_NULL_TERMINATOR)) [[unlikely]] {
|
||||
state_ = State::FAILED;
|
||||
return APIError::OUT_OF_MEMORY;
|
||||
}
|
||||
|
||||
if (rx_buf_len_ < rx_header_parsed_len_) {
|
||||
// more data to read
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "api_overflow_buffer.h"
|
||||
#ifdef USE_API
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
@@ -61,9 +62,18 @@ bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_
|
||||
return false;
|
||||
|
||||
uint16_t buffer_size = total_len - skip;
|
||||
// nothrow: a failed allocation returns nullptr so the connection is dropped
|
||||
// cleanly instead of plain new's crash or abort on OOM
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
|
||||
auto *entry = new Entry{new uint8_t[buffer_size], buffer_size, 0};
|
||||
this->queue_[this->tail_] = entry;
|
||||
auto *data = new (std::nothrow) uint8_t[buffer_size];
|
||||
if (data == nullptr)
|
||||
return false;
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
|
||||
auto *entry = new (std::nothrow) Entry{data, buffer_size, 0};
|
||||
if (entry == nullptr) {
|
||||
delete[] data;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint16_t to_skip = skip;
|
||||
uint16_t write_pos = 0;
|
||||
@@ -80,6 +90,8 @@ bool APIOverflowBuffer::enqueue_iov(const struct iovec *iov, int iovcnt, uint16_
|
||||
}
|
||||
}
|
||||
|
||||
// Publish only after the copy completes so a half-built entry is never reachable
|
||||
this->queue_[this->tail_] = entry;
|
||||
this->tail_ = (this->tail_ + 1) % API_MAX_SEND_QUEUE;
|
||||
this->count_++;
|
||||
return true;
|
||||
|
||||
@@ -61,7 +61,7 @@ class APIOverflowBuffer {
|
||||
|
||||
/// 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).
|
||||
/// Returns false if the queue is full or allocation fails (caller should fail the connection).
|
||||
bool enqueue_iov(const struct iovec *iov, int iovcnt, uint16_t total_len, uint16_t skip);
|
||||
|
||||
protected:
|
||||
|
||||
@@ -1275,12 +1275,24 @@ uint8_t *ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer PROTO_ENC
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->type));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->description);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->example);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
uint32_t ListEntitiesServicesArgument::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += ProtoSize::calc_length(1, this->name.size());
|
||||
size += this->type ? 2 : 0;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
size += ProtoSize::calc_length(1, this->description.size());
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
size += ProtoSize::calc_length(1, this->example.size());
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
@@ -1291,6 +1303,9 @@ uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENC
|
||||
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it);
|
||||
}
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(this->supports_response));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->description);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
uint32_t ListEntitiesServicesResponse::calculate_size() const {
|
||||
@@ -1303,6 +1318,9 @@ uint32_t ListEntitiesServicesResponse::calculate_size() const {
|
||||
}
|
||||
}
|
||||
size += this->supports_response ? 2 : 0;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
size += ProtoSize::calc_length(1, this->description.size());
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
@@ -2323,7 +2341,6 @@ uint8_t *ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer PROTO_
|
||||
#endif
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 6, this->disabled_by_default);
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, static_cast<uint32_t>(this->entity_category));
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 8, this->supports_pause);
|
||||
for (auto &it : this->supported_formats) {
|
||||
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 9, it);
|
||||
}
|
||||
@@ -2343,7 +2360,6 @@ uint32_t ListEntitiesMediaPlayerResponse::calculate_size() const {
|
||||
#endif
|
||||
size += ProtoSize::calc_bool(1, this->disabled_by_default);
|
||||
size += this->entity_category ? 2 : 0;
|
||||
size += ProtoSize::calc_bool(1, this->supports_pause);
|
||||
if (!this->supported_formats.empty()) {
|
||||
for (const auto &it : this->supported_formats) {
|
||||
size += ProtoSize::calc_message_force(1, it.calculate_size());
|
||||
|
||||
@@ -1317,6 +1317,12 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
public:
|
||||
StringRef name{};
|
||||
enums::ServiceArgType type{};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
StringRef description{};
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
StringRef example{};
|
||||
#endif
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1328,7 +1334,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
|
||||
class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 41;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 50;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 59;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
|
||||
#endif
|
||||
@@ -1336,6 +1342,9 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
|
||||
uint32_t key{0};
|
||||
FixedVector<ListEntitiesServicesArgument> args{};
|
||||
enums::SupportsResponseType supports_response{};
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
StringRef description{};
|
||||
#endif
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
@@ -1911,11 +1920,10 @@ class MediaPlayerSupportedFormat final : public ProtoMessage {
|
||||
class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage {
|
||||
public:
|
||||
static constexpr uint16_t MESSAGE_TYPE = 63;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 80;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 78;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("list_entities_media_player_response"); }
|
||||
#endif
|
||||
bool supports_pause{false};
|
||||
std::vector<MediaPlayerSupportedFormat> supported_formats{};
|
||||
uint32_t feature_flags{0};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
|
||||
@@ -1500,6 +1500,12 @@ const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument"));
|
||||
dump_field(out, ESPHOME_PSTR("name"), this->name);
|
||||
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
dump_field(out, ESPHOME_PSTR("description"), this->description);
|
||||
#endif
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
dump_field(out, ESPHOME_PSTR("example"), this->example);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
|
||||
@@ -1512,6 +1518,9 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
|
||||
out.append("\n");
|
||||
}
|
||||
dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response));
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
dump_field(out, ESPHOME_PSTR("description"), this->description);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
const char *ExecuteServiceArgument::dump_to(DumpBuffer &out) const {
|
||||
@@ -1962,7 +1971,6 @@ const char *ListEntitiesMediaPlayerResponse::dump_to(DumpBuffer &out) const {
|
||||
#endif
|
||||
dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default);
|
||||
dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category));
|
||||
dump_field(out, ESPHOME_PSTR("supports_pause"), this->supports_pause);
|
||||
for (const auto &it : this->supported_formats) {
|
||||
out.append(4, ' ').append_p(ESPHOME_PSTR("supported_formats")).append(": ");
|
||||
it.dump_to(out);
|
||||
|
||||
@@ -433,8 +433,10 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
|
||||
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
|
||||
// fired right at connection time (on_client_connected, on_time_sync, ...) can
|
||||
// arrive before the subscription and are lost - warn instead of failing silently.
|
||||
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", call.is_event ? "event" : "action", call.service.c_str(),
|
||||
this->is_connected() ? "client has not subscribed to actions (yet)" : "no client connected");
|
||||
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s",
|
||||
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(),
|
||||
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
|
||||
: LOG_STR_LITERAL("no client connected"));
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
|
||||
@@ -99,7 +99,8 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
|
||||
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
|
||||
|
||||
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
|
||||
auto resp = service->encode_list_service_response();
|
||||
UserActionScratch scratch;
|
||||
auto resp = service->encode_list_service_response(scratch);
|
||||
if (!this->client_->send_message(resp))
|
||||
return false;
|
||||
// at_ is this service's index
|
||||
|
||||
@@ -1,9 +1,52 @@
|
||||
#include "user_services.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
|
||||
namespace esphome::api {
|
||||
|
||||
StringRef UserServiceStatic::str_(size_t idx, std::span<char> &scratch) const {
|
||||
const char *s = progmem_read_ptr(&this->strings_[idx]);
|
||||
if (s == nullptr)
|
||||
return {};
|
||||
#ifdef USE_ESP8266
|
||||
// Codegen sizes the scratch buffer for the largest service; the bound only guards other callers
|
||||
if (scratch.empty())
|
||||
return {};
|
||||
size_t len = strnlen_P(s, scratch.size() - 1);
|
||||
progmem_memcpy(scratch.data(), s, len);
|
||||
scratch[len] = '\0';
|
||||
StringRef ref(scratch.data(), len);
|
||||
scratch = scratch.subspan(len + 1);
|
||||
return ref;
|
||||
#else
|
||||
return StringRef(s);
|
||||
#endif
|
||||
}
|
||||
|
||||
ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
|
||||
std::span<const enums::ServiceArgType> arg_types, std::span<char> scratch) const {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.name = this->str_(0, scratch);
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = this->supports_response_;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
msg.description = this->str_(1, scratch);
|
||||
#endif
|
||||
msg.args.init(arg_types.size());
|
||||
for (size_t i = 0; i < arg_types.size(); i++) {
|
||||
size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS;
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.name = this->str_(base, scratch);
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
arg.description = this->str_(base + 1, scratch);
|
||||
arg.example = this->str_(base + 2, scratch);
|
||||
#endif
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; }
|
||||
template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &arg) {
|
||||
if (arg.legacy_int != 0)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <span>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -19,7 +20,9 @@ class APIServer;
|
||||
|
||||
class UserServiceDescriptor {
|
||||
public:
|
||||
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
|
||||
/// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into
|
||||
/// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it.
|
||||
virtual ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) = 0;
|
||||
|
||||
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
|
||||
@@ -34,29 +37,51 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
|
||||
|
||||
template<typename T> enums::ServiceArgType to_service_arg_type();
|
||||
|
||||
// Base class for YAML-defined services (most common case)
|
||||
// Stores only pointers to string literals in flash - no heap allocation
|
||||
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
public:
|
||||
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names,
|
||||
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
|
||||
: name_(name), arg_names_(arg_names), supports_response_(supports_response) {
|
||||
this->key_ = fnv1_hash(name);
|
||||
}
|
||||
// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
|
||||
#ifdef USE_ESP8266
|
||||
using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
|
||||
#else
|
||||
using UserActionScratch = std::array<char, 0>;
|
||||
#endif
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.name = StringRef(this->name_);
|
||||
msg.key = this->key_;
|
||||
msg.supports_response = this->supports_response_;
|
||||
// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
|
||||
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
|
||||
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
|
||||
// each argument name; with USE_API_USER_DEFINED_ACTION_METADATA the action description follows
|
||||
// the name and every argument is (name, description, example). Unset metadata is nullptr.
|
||||
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
|
||||
static constexpr size_t USER_ACTION_HEADER_STRINGS = 2;
|
||||
static constexpr size_t USER_ACTION_ARG_STRINGS = 3;
|
||||
#else
|
||||
static constexpr size_t USER_ACTION_HEADER_STRINGS = 1;
|
||||
static constexpr size_t USER_ACTION_ARG_STRINGS = 1;
|
||||
#endif
|
||||
class UserServiceStatic : public UserServiceDescriptor {
|
||||
public:
|
||||
UserServiceStatic(const char *const *strings, uint32_t key,
|
||||
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
|
||||
: strings_(strings), key_(key), supports_response_(supports_response) {}
|
||||
|
||||
protected:
|
||||
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
|
||||
std::span<char> scratch) const;
|
||||
/// Reference table entry `idx`; nullptr gives an empty StringRef.
|
||||
/// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span
|
||||
/// is advanced past the copy.
|
||||
StringRef str_(size_t idx, std::span<char> &scratch) const;
|
||||
|
||||
const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
|
||||
uint32_t key_;
|
||||
enums::SupportsResponseType supports_response_;
|
||||
};
|
||||
|
||||
template<typename... Ts> class UserServiceBase : public UserServiceStatic {
|
||||
public:
|
||||
using UserServiceStatic::UserServiceStatic;
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) override {
|
||||
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
|
||||
msg.args.init(sizeof...(Ts));
|
||||
for (size_t i = 0; i < sizeof...(Ts); i++) {
|
||||
auto &arg = msg.args.emplace_back();
|
||||
arg.type = arg_types[i];
|
||||
arg.name = StringRef(this->arg_names_[i]);
|
||||
}
|
||||
return msg;
|
||||
return this->encode_list_service_response_(arg_types, scratch);
|
||||
}
|
||||
|
||||
bool execute_service(const ExecuteServiceRequest &req) override {
|
||||
@@ -89,12 +114,6 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
|
||||
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
|
||||
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
|
||||
}
|
||||
|
||||
// Pointers to string literals in flash - no heap allocation
|
||||
const char *name_;
|
||||
std::array<const char *, sizeof...(Ts)> arg_names_;
|
||||
uint32_t key_{0};
|
||||
enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE};
|
||||
};
|
||||
|
||||
// Separate class for custom_api_device services (rare case)
|
||||
@@ -106,7 +125,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
|
||||
this->key_ = fnv1_hash(this->name_.c_str());
|
||||
}
|
||||
|
||||
ListEntitiesServicesResponse encode_list_service_response() override {
|
||||
ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
|
||||
ListEntitiesServicesResponse msg;
|
||||
msg.name = StringRef(this->name_);
|
||||
msg.key = this->key_;
|
||||
@@ -167,8 +186,8 @@ template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> final : public UserServiceBase<Ts...>,
|
||||
public Trigger<Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {}
|
||||
UserServiceTrigger(const char *const *strings, uint32_t key)
|
||||
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_NONE) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
|
||||
@@ -179,8 +198,8 @@ template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> final : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, bool, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
|
||||
UserServiceTrigger(const char *const *strings, uint32_t key)
|
||||
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool return_response, Ts... x) override {
|
||||
@@ -193,8 +212,8 @@ template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> final : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {}
|
||||
UserServiceTrigger(const char *const *strings, uint32_t key)
|
||||
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_ONLY) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
|
||||
@@ -205,8 +224,8 @@ template<typename... Ts>
|
||||
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> final : public UserServiceBase<Ts...>,
|
||||
public Trigger<uint32_t, Ts...> {
|
||||
public:
|
||||
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
|
||||
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {}
|
||||
UserServiceTrigger(const char *const *strings, uint32_t key)
|
||||
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_STATUS) {}
|
||||
|
||||
protected:
|
||||
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
|
||||
|
||||
@@ -23,8 +23,10 @@ void AQISensor::setup() {
|
||||
|
||||
void AQISensor::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "AQI Sensor:");
|
||||
ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI");
|
||||
ESP_LOGCONFIG(TAG, " Extended Range: %s", this->extended_range_ ? "enabled" : "disabled");
|
||||
ESP_LOGCONFIG(TAG, " Calculation Type: %s",
|
||||
this->aqi_calc_type_ == AQI_TYPE ? LOG_STR_LITERAL("AQI") : LOG_STR_LITERAL("CAQI"));
|
||||
ESP_LOGCONFIG(TAG, " Extended Range: %s",
|
||||
this->extended_range_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
|
||||
if (this->pm_2_5_sensor_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str());
|
||||
}
|
||||
|
||||
@@ -100,13 +100,15 @@ void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) {
|
||||
}
|
||||
this->is_valid_ = false;
|
||||
this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
ESP_LOGV(TAG, "Multi Click: You can now %s the button.",
|
||||
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
|
||||
this->is_valid_ = true;
|
||||
});
|
||||
}
|
||||
void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) {
|
||||
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
|
||||
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.",
|
||||
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
|
||||
this->is_valid_ = false;
|
||||
this->schedule_cooldown_();
|
||||
});
|
||||
|
||||
@@ -62,8 +62,9 @@ BdkActivityState bdk_scan_state(uint8_t activity_idx) {
|
||||
|
||||
uint8_t bdk_scan_acquire_activity() {
|
||||
uint8_t idx = app_ble_get_idle_actv_idx_handle(SCAN_ACTV);
|
||||
if (idx == INVALID_ACTIVITY_IDX)
|
||||
if (idx == INVALID_ACTIVITY_IDX) {
|
||||
ESP_LOGE(TAG, "Scan start failed: no idle activity handle");
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
|
||||
@@ -181,8 +181,9 @@ void BK72xxBLE::enable() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!bdaddr_live)
|
||||
if (!bdaddr_live) {
|
||||
ESP_LOGW(TAG, "Controller address still unset after init; BLE stack may not have started");
|
||||
}
|
||||
#endif
|
||||
|
||||
this->state_ = BLEComponentState::ACTIVE;
|
||||
@@ -210,8 +211,9 @@ void BK72xxBLE::loop() {
|
||||
// Re-check a settled scan; scan_start() refills the bring-up budget.
|
||||
// WARN: the only report of a drop that recovers inside its budget.
|
||||
if (this->scan_start(this->requested_.interval, this->requested_.window, this->requested_.active) !=
|
||||
ScanOpResult::SETTLED)
|
||||
ScanOpResult::SETTLED) {
|
||||
ESP_LOGW(TAG, "Controller dropped the scan; restarting");
|
||||
}
|
||||
}
|
||||
|
||||
// Drain the lock-free ring filled by the BLE task; all per-report work runs
|
||||
@@ -230,8 +232,9 @@ void BK72xxBLE::loop() {
|
||||
// Log dropped reports — only reachable when reports were processed; drops can
|
||||
// only occur while the queue is full, and only this loop drains it.
|
||||
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
|
||||
if (dropped > 0)
|
||||
if (dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u scan reports due to queue overflow", dropped);
|
||||
}
|
||||
}
|
||||
|
||||
void BK72xxBLE::get_mac_lsb_first(uint8_t out[MAC_ADDRESS_SIZE]) const {
|
||||
@@ -449,8 +452,9 @@ ScanOpResult BK72xxBLE::advance_stop_(BdkActivityState state, bool ready) {
|
||||
if (!ready) {
|
||||
// Acting mid-operation could delete an activity whose start lands
|
||||
// afterwards, leaking the slot with the radio on; wait.
|
||||
if (this->last_result_ == ScanOpResult::SETTLED)
|
||||
if (this->last_result_ == ScanOpResult::SETTLED) {
|
||||
ESP_LOGD(TAG, "Scan stop deferred (controller busy)");
|
||||
}
|
||||
return ScanOpResult::PENDING;
|
||||
}
|
||||
// Settled, so CREATED unambiguously means "never started".
|
||||
@@ -474,8 +478,9 @@ ScanOpResult BK72xxBLE::advance_start_(BdkActivityState state, bool ready) {
|
||||
return ScanOpResult::PENDING;
|
||||
}
|
||||
if (!ready) {
|
||||
if (this->last_result_ == ScanOpResult::SETTLED)
|
||||
if (this->last_result_ == ScanOpResult::SETTLED) {
|
||||
ESP_LOGD(TAG, "Scan start deferred (controller busy)");
|
||||
}
|
||||
return ScanOpResult::PENDING;
|
||||
}
|
||||
if (state == BdkActivityState::CREATED) {
|
||||
|
||||
@@ -69,8 +69,9 @@ void BK72xxBLETracker::on_ota_global_state(ota::OTAState state, float progress,
|
||||
this->stop_scan();
|
||||
// The transfer starves the loop; a deferred stop would leave the radio
|
||||
// scanning for the whole update, so drain it here, bounded.
|
||||
if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS))
|
||||
if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS)) {
|
||||
ESP_LOGE(TAG, "Scan still stopping at OTA start; the radio may contend with the update");
|
||||
}
|
||||
} else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
|
||||
// On success the device reboots, so restore only on a failed/aborted update;
|
||||
// loop() restarts the scan on its next iteration (continuous idle branch).
|
||||
|
||||
@@ -80,8 +80,9 @@ void BLEBinaryOutput::write_state(bool state) {
|
||||
esp_err_t err =
|
||||
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_,
|
||||
sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE);
|
||||
if (err != ESP_GATT_OK)
|
||||
if (err != ESP_GATT_OK) {
|
||||
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_str(char_buf), err);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ble_client
|
||||
|
||||
@@ -327,10 +327,12 @@ void BME680Component::read_data_() {
|
||||
|
||||
ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%% gas_resistance=%.1fΩ", temperature, pressure,
|
||||
humidity, gas_resistance);
|
||||
if (!gas_valid)
|
||||
if (!gas_valid) {
|
||||
ESP_LOGW(TAG, "Gas measurement unsuccessful, reading invalid!");
|
||||
if (!heat_stable)
|
||||
}
|
||||
if (!heat_stable) {
|
||||
ESP_LOGW(TAG, "Heater unstable, reading invalid! (Normal for a few readings after a power cycle)");
|
||||
}
|
||||
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(temperature);
|
||||
|
||||
@@ -162,8 +162,9 @@ void BME680BSECComponent::dump_config() {
|
||||
" Supply Voltage: %sV\n"
|
||||
" Sample Rate: %s\n"
|
||||
" State Save Interval: %" PRIu32 "ms",
|
||||
this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile",
|
||||
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8",
|
||||
this->temperature_offset_,
|
||||
this->iaq_mode_ == IAQ_MODE_STATIC ? LOG_STR_LITERAL("Static") : LOG_STR_LITERAL("Mobile"),
|
||||
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? LOG_STR_LITERAL("3.3") : LOG_STR_LITERAL("1.8"),
|
||||
BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_);
|
||||
|
||||
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
|
||||
|
||||
@@ -7,146 +7,146 @@ namespace esphome::captive_portal {
|
||||
|
||||
#ifdef USE_CAPTIVE_PORTAL_GZIP
|
||||
constexpr uint8_t INDEX_GZ[] PROGMEM = {
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7f,
|
||||
0x05, 0x8f, 0x4d, 0x1b, 0xa9, 0xb1, 0xa8, 0x87, 0xd7, 0xde, 0x44, 0x96, 0x54, 0xa4, 0x7b, 0x2d, 0x5a, 0xa0, 0x69,
|
||||
0x03, 0xec, 0x36, 0xf7, 0x21, 0x08, 0xb0, 0x34, 0x39, 0xb2, 0x98, 0xa5, 0x48, 0x1d, 0x49, 0xbf, 0x62, 0xf8, 0x7e,
|
||||
0xfb, 0x81, 0x92, 0xec, 0xf5, 0x2e, 0x9a, 0x03, 0x0e, 0x86, 0x85, 0x19, 0xce, 0x7b, 0x38, 0x0f, 0x16, 0xff, 0xe0,
|
||||
0x9a, 0xb9, 0x7d, 0x07, 0xa8, 0x71, 0xad, 0xac, 0x0a, 0xff, 0x45, 0x92, 0xaa, 0x55, 0x09, 0xaa, 0x2a, 0x1a, 0xa0,
|
||||
0xbc, 0x2a, 0x5a, 0x70, 0x14, 0xb1, 0x86, 0x1a, 0x0b, 0xae, 0xfc, 0xeb, 0xee, 0x97, 0xe8, 0x75, 0x55, 0x48, 0xa1,
|
||||
0x1e, 0x90, 0x01, 0x59, 0x0a, 0xa6, 0x15, 0x6a, 0x0c, 0xd4, 0x25, 0xa7, 0x8e, 0xe6, 0xa2, 0xa5, 0x2b, 0x18, 0x45,
|
||||
0x14, 0x6d, 0xa1, 0xdc, 0x08, 0xd8, 0x76, 0xda, 0x38, 0xc4, 0xb4, 0x72, 0xa0, 0x5c, 0x89, 0xb7, 0x82, 0xbb, 0xa6,
|
||||
0xe4, 0xb0, 0x11, 0x0c, 0xa2, 0x1e, 0x99, 0x08, 0x25, 0x9c, 0xa0, 0x32, 0xb2, 0x8c, 0x4a, 0x28, 0xd3, 0xc9, 0xda,
|
||||
0x82, 0xe9, 0x11, 0xba, 0x94, 0x50, 0x2a, 0x8d, 0xab, 0xc2, 0x32, 0x23, 0x3a, 0x87, 0xbc, 0xab, 0x65, 0xab, 0xf9,
|
||||
0x5a, 0x42, 0x15, 0xc7, 0xd4, 0x5a, 0x70, 0x36, 0x16, 0x8a, 0xc3, 0x8e, 0xd0, 0x6b, 0xb8, 0xa6, 0x2c, 0x4d, 0xc8,
|
||||
0x67, 0xfb, 0x0d, 0xd7, 0x6c, 0xdd, 0x82, 0x72, 0x44, 0x6a, 0x46, 0x9d, 0xd0, 0x8a, 0x58, 0xa0, 0x86, 0x35, 0x65,
|
||||
0x59, 0xe2, 0x1f, 0x2d, 0xdd, 0x00, 0xfe, 0xfe, 0xfb, 0xe0, 0xcc, 0xb4, 0x02, 0xf7, 0xb3, 0x04, 0x0f, 0xda, 0x9f,
|
||||
0xf6, 0x77, 0x74, 0xf5, 0x07, 0x6d, 0x21, 0xc0, 0xd4, 0x0a, 0x0e, 0x38, 0xfc, 0x98, 0x7c, 0x22, 0xd6, 0xed, 0x25,
|
||||
0x10, 0x2e, 0x6c, 0x27, 0xe9, 0xbe, 0xc4, 0x4b, 0xa9, 0xd9, 0x03, 0x0e, 0x17, 0xf5, 0x5a, 0x31, 0xaf, 0x1c, 0xe9,
|
||||
0x00, 0xc2, 0x83, 0x04, 0x87, 0x5c, 0xf9, 0x8e, 0xba, 0x86, 0xb4, 0x74, 0x17, 0x0c, 0x80, 0x50, 0x41, 0xf6, 0x43,
|
||||
0x00, 0xaf, 0xd2, 0x24, 0x09, 0x27, 0xfd, 0x27, 0x09, 0xe3, 0x34, 0x49, 0x16, 0x06, 0xdc, 0xda, 0x28, 0x44, 0x83,
|
||||
0xfb, 0xa2, 0xa3, 0xae, 0x41, 0xbc, 0xc4, 0xef, 0xd2, 0x0c, 0xa5, 0x6f, 0x48, 0x36, 0xfb, 0x9d, 0x5c, 0xa3, 0x2b,
|
||||
0x92, 0xcd, 0xd8, 0x75, 0x34, 0x43, 0xe9, 0x55, 0x34, 0x43, 0x59, 0x46, 0x66, 0x28, 0xf9, 0x82, 0x51, 0x2d, 0xa4,
|
||||
0x2c, 0xb1, 0xd2, 0x0a, 0x30, 0xb2, 0xce, 0xe8, 0x07, 0x28, 0x31, 0x5b, 0x1b, 0x03, 0xca, 0xdd, 0x68, 0xa9, 0x0d,
|
||||
0x8e, 0xab, 0x6f, 0xfe, 0x2f, 0x85, 0xce, 0x50, 0x65, 0x6b, 0x6d, 0xda, 0x12, 0xf7, 0xd9, 0x0f, 0x5e, 0x1c, 0xdc,
|
||||
0x11, 0xf9, 0x4f, 0x78, 0x41, 0x8c, 0xb4, 0x11, 0x2b, 0xa1, 0x4a, 0xec, 0x35, 0xbe, 0xc6, 0x71, 0x75, 0x1f, 0x1e,
|
||||
0xcf, 0xd1, 0x53, 0x1f, 0xfd, 0x18, 0x0f, 0x0f, 0x3e, 0xde, 0x17, 0x76, 0xb3, 0x42, 0xbb, 0x56, 0x2a, 0x5b, 0xe2,
|
||||
0xc6, 0xb9, 0x2e, 0x8f, 0xe3, 0xed, 0x76, 0x4b, 0xb6, 0x53, 0xa2, 0xcd, 0x2a, 0xce, 0x92, 0x24, 0x89, 0xed, 0x66,
|
||||
0x85, 0xd1, 0x50, 0x08, 0x38, 0xbb, 0xc2, 0xa8, 0x01, 0xb1, 0x6a, 0x5c, 0x0f, 0x57, 0x2f, 0x0e, 0x70, 0x2c, 0x3c,
|
||||
0x47, 0x75, 0xff, 0xe9, 0xc2, 0x8a, 0xb9, 0xb0, 0x02, 0x3f, 0xd2, 0x00, 0x9f, 0xc2, 0x7c, 0xd9, 0x87, 0x79, 0x4d,
|
||||
0x33, 0x94, 0xa1, 0xa4, 0xff, 0x65, 0x91, 0x87, 0x47, 0x2c, 0x7a, 0x86, 0xa1, 0x0b, 0xcc, 0x43, 0xed, 0x3c, 0x7a,
|
||||
0x73, 0x96, 0x4d, 0xfd, 0xc9, 0x26, 0x4d, 0x1e, 0x0f, 0xbc, 0xc0, 0xaf, 0xf3, 0x4b, 0x3c, 0xca, 0x3e, 0x5c, 0x32,
|
||||
0x78, 0x6b, 0x4d, 0xfa, 0x61, 0x4e, 0x67, 0x68, 0x36, 0x9e, 0xcc, 0x22, 0x0f, 0x9f, 0x31, 0x34, 0xdb, 0x64, 0x4d,
|
||||
0xda, 0x46, 0xf3, 0x68, 0x46, 0xa7, 0x68, 0x3a, 0x3a, 0x32, 0x45, 0xd3, 0x4d, 0xd6, 0xcc, 0x3f, 0xcc, 0x2f, 0xcf,
|
||||
0xa2, 0xe9, 0x97, 0x97, 0x71, 0x85, 0xc3, 0x1c, 0xe3, 0xc7, 0xc8, 0xf9, 0x65, 0xe4, 0xe4, 0xb3, 0x16, 0x2a, 0xc0,
|
||||
0x38, 0x3c, 0xd6, 0xe0, 0x58, 0x13, 0xe0, 0x98, 0x69, 0x55, 0x8b, 0x15, 0xf9, 0x6c, 0xb5, 0xc2, 0x21, 0x71, 0x0d,
|
||||
0xa8, 0xe0, 0x24, 0xea, 0x05, 0xa1, 0xa7, 0x04, 0xcf, 0x29, 0x2e, 0x3c, 0x9c, 0xeb, 0xdf, 0x09, 0x27, 0xa1, 0x74,
|
||||
0xc4, 0x37, 0xec, 0xe4, 0x6f, 0xba, 0xe2, 0xa7, 0xfd, 0x6f, 0x3c, 0xc0, 0x2d, 0x65, 0x38, 0x24, 0x42, 0x29, 0x30,
|
||||
0x77, 0xb0, 0x73, 0x25, 0x7e, 0xf7, 0xf6, 0x06, 0xbd, 0xe5, 0xdc, 0x80, 0xb5, 0x39, 0xc2, 0xaf, 0x1c, 0x69, 0x29,
|
||||
0xfb, 0xba, 0x78, 0x93, 0x3e, 0x95, 0xfe, 0x97, 0xf8, 0x45, 0xa0, 0x3f, 0xc0, 0x6d, 0xb5, 0x79, 0x18, 0xe5, 0xbd,
|
||||
0xfd, 0x85, 0x6f, 0x23, 0x56, 0x7e, 0x55, 0x8d, 0x02, 0x87, 0xc3, 0x89, 0xf8, 0x3a, 0x83, 0xb5, 0x82, 0xe3, 0x70,
|
||||
0x22, 0xbf, 0xce, 0xd1, 0x59, 0xdf, 0xbc, 0x8e, 0xd0, 0xce, 0x12, 0x2b, 0x05, 0x83, 0x20, 0x0d, 0x49, 0xad, 0xcd,
|
||||
0xcf, 0x94, 0x35, 0x8f, 0x09, 0xb2, 0x43, 0x47, 0xab, 0x47, 0x3d, 0xcc, 0x00, 0x75, 0x30, 0xaa, 0x0a, 0x30, 0x17,
|
||||
0x1b, 0x1c, 0x2e, 0x14, 0x61, 0x92, 0x5a, 0xeb, 0x47, 0x46, 0xe9, 0x9d, 0xf3, 0xe1, 0xe0, 0x89, 0x1a, 0x22, 0xfd,
|
||||
0xf5, 0xee, 0xdd, 0xef, 0xe5, 0x7d, 0x41, 0x87, 0x01, 0x89, 0xbf, 0xc5, 0xa8, 0x67, 0x3e, 0x33, 0x46, 0x12, 0x6a,
|
||||
0xe7, 0x2b, 0x5e, 0x07, 0x96, 0x18, 0x6b, 0x45, 0x78, 0x2c, 0x6c, 0x47, 0xd5, 0x73, 0xb6, 0x3e, 0xa6, 0xaa, 0x88,
|
||||
0x3d, 0xad, 0x2a, 0x62, 0x5a, 0xbd, 0x38, 0x98, 0xc0, 0xfa, 0xe1, 0xf6, 0x10, 0x1e, 0xef, 0x27, 0x8a, 0xfc, 0x7b,
|
||||
0x0d, 0x66, 0x7f, 0x0b, 0x12, 0x98, 0xd3, 0x26, 0xc0, 0xe4, 0x89, 0x60, 0x48, 0x1c, 0xec, 0xdc, 0xcd, 0x38, 0x7f,
|
||||
0x2d, 0xf1, 0x87, 0x13, 0x45, 0xb4, 0x62, 0x52, 0xb0, 0x87, 0xf2, 0x1c, 0x71, 0x78, 0x10, 0x64, 0x43, 0xe5, 0x1a,
|
||||
0x4e, 0x3c, 0x92, 0xd4, 0x9a, 0xad, 0x6d, 0x10, 0x1e, 0x27, 0x8c, 0xd0, 0xae, 0x03, 0xc5, 0x6f, 0x1a, 0x21, 0x79,
|
||||
0xa0, 0xc2, 0x63, 0xf8, 0x78, 0xd3, 0xcf, 0x8c, 0xfb, 0xd5, 0xf0, 0xd1, 0x80, 0xfc, 0x4f, 0xf9, 0xd2, 0x2f, 0x87,
|
||||
0x97, 0x9f, 0x70, 0x48, 0xfa, 0xf8, 0xef, 0x1f, 0x37, 0x84, 0x6f, 0xef, 0x57, 0xbb, 0x56, 0x4e, 0x7c, 0xe8, 0xd1,
|
||||
0x7c, 0x16, 0x1e, 0xef, 0x8f, 0xe1, 0x31, 0x5c, 0x14, 0xf1, 0x30, 0xe7, 0xab, 0xa2, 0x1f, 0xb9, 0xd5, 0x0f, 0x87,
|
||||
0xa5, 0xde, 0x45, 0x56, 0x7c, 0x11, 0x6a, 0x95, 0x0b, 0xd5, 0x80, 0x11, 0xee, 0xc8, 0xc5, 0x66, 0x22, 0x54, 0xb7,
|
||||
0x76, 0x87, 0x8e, 0x72, 0xee, 0x29, 0xb3, 0x6e, 0xb7, 0xa8, 0xb5, 0x72, 0x9e, 0x13, 0xf2, 0x14, 0xda, 0xe3, 0x40,
|
||||
0xef, 0x27, 0x4c, 0xfe, 0x66, 0xf6, 0xdd, 0x71, 0xa9, 0xf9, 0xfe, 0xe0, 0xd3, 0x10, 0x51, 0x29, 0x56, 0x2a, 0x67,
|
||||
0xa0, 0x1c, 0x98, 0x41, 0xa8, 0xa6, 0xad, 0x90, 0xfb, 0xdc, 0x52, 0x65, 0x23, 0x0b, 0x46, 0xd4, 0xc7, 0xe5, 0xda,
|
||||
0x39, 0xad, 0x0e, 0x4b, 0x6d, 0x38, 0x98, 0x3c, 0x59, 0x0c, 0x40, 0x64, 0x28, 0x17, 0x6b, 0x9b, 0x93, 0xa9, 0x81,
|
||||
0x76, 0xb1, 0xa4, 0xec, 0x61, 0x65, 0xf4, 0x5a, 0xf1, 0x88, 0xf9, 0xc9, 0x9b, 0x7f, 0x9b, 0xd6, 0x74, 0x0a, 0x6c,
|
||||
0x31, 0x62, 0x75, 0x5d, 0x2f, 0xa4, 0x50, 0x10, 0x0d, 0xb3, 0x2d, 0xcf, 0xc8, 0x95, 0x17, 0xbb, 0x70, 0x93, 0x64,
|
||||
0xfe, 0x60, 0xf0, 0x31, 0x4d, 0x92, 0xef, 0x16, 0xa7, 0x70, 0x92, 0x05, 0x5b, 0x1b, 0xab, 0x4d, 0xde, 0x69, 0xe1,
|
||||
0xdd, 0x3c, 0xb6, 0x54, 0xa8, 0x4b, 0xef, 0x7d, 0xd9, 0x2c, 0xc6, 0x75, 0x94, 0x0b, 0xd5, 0x9b, 0xe9, 0x97, 0xd2,
|
||||
0xa2, 0x15, 0x6a, 0xd8, 0xa9, 0x79, 0x36, 0x4f, 0xba, 0xdd, 0xf1, 0x54, 0x09, 0x87, 0x13, 0x77, 0x2d, 0x61, 0xb7,
|
||||
0xf8, 0xbc, 0xb6, 0x4e, 0xd4, 0xfb, 0x68, 0xdc, 0xc9, 0xb9, 0xed, 0x28, 0x83, 0x68, 0x09, 0x6e, 0x0b, 0xa0, 0x16,
|
||||
0xbd, 0x8d, 0x48, 0x38, 0x68, 0xed, 0x98, 0xa7, 0xb3, 0x9a, 0xbe, 0x60, 0x9f, 0xea, 0xfa, 0x5f, 0xdc, 0xbe, 0x8a,
|
||||
0x0e, 0x2d, 0x35, 0x2b, 0xa1, 0xa2, 0xa5, 0x76, 0x4e, 0xb7, 0x79, 0x74, 0xdd, 0xed, 0x16, 0xe3, 0x91, 0x57, 0x96,
|
||||
0xa7, 0xde, 0xcd, 0x7e, 0xd7, 0x9e, 0xf2, 0x9d, 0x76, 0x3b, 0x64, 0xb5, 0x14, 0x7c, 0xe4, 0xeb, 0x59, 0x50, 0x72,
|
||||
0x4e, 0x4f, 0x3a, 0xeb, 0x76, 0xc8, 0x9f, 0x9d, 0x52, 0x7d, 0x55, 0xbf, 0xa6, 0x69, 0xf2, 0x37, 0x37, 0xc2, 0xeb,
|
||||
0x3a, 0x5b, 0xd6, 0xe7, 0x4c, 0xf9, 0xb5, 0xe9, 0x57, 0x4b, 0x5f, 0x5a, 0x45, 0x3c, 0xbc, 0x6e, 0x7c, 0x65, 0x54,
|
||||
0x85, 0xcf, 0x70, 0x55, 0x34, 0x29, 0x12, 0xbc, 0x6c, 0x29, 0xab, 0x2e, 0x66, 0x5b, 0x11, 0x37, 0xe9, 0x89, 0xd4,
|
||||
0xa4, 0xd5, 0x93, 0xb9, 0x35, 0xd0, 0x7a, 0xef, 0xab, 0x1b, 0xad, 0x14, 0x30, 0x27, 0xd4, 0x0a, 0x39, 0x8d, 0xc6,
|
||||
0x14, 0x10, 0x42, 0x8a, 0xa5, 0xa9, 0xde, 0x4b, 0xa0, 0x16, 0xd0, 0x96, 0x0a, 0x47, 0x8a, 0x78, 0xe0, 0x1f, 0x3a,
|
||||
0x5d, 0xf0, 0x52, 0x81, 0x3b, 0xf7, 0x76, 0x33, 0x1d, 0x0c, 0xdc, 0x82, 0xf3, 0x9a, 0xbc, 0x81, 0x69, 0x55, 0xf8,
|
||||
0x15, 0x8c, 0x68, 0xdf, 0xa5, 0x65, 0xbc, 0x15, 0xb5, 0xf0, 0x4f, 0x98, 0xaa, 0xe8, 0x8b, 0xdc, 0x6b, 0xf0, 0x79,
|
||||
0x1e, 0x9e, 0x5b, 0x3d, 0x24, 0x41, 0xad, 0x5c, 0x53, 0x4e, 0x33, 0xd4, 0x49, 0xca, 0xa0, 0xd1, 0x92, 0x83, 0x29,
|
||||
0x6f, 0x6f, 0x7f, 0xfb, 0x67, 0xe5, 0x9d, 0x79, 0x94, 0xeb, 0xec, 0xc3, 0x20, 0xe6, 0x81, 0x51, 0x6a, 0x7e, 0x35,
|
||||
0x3c, 0xb2, 0x3a, 0x6a, 0xed, 0x56, 0x1b, 0xfe, 0x44, 0xc7, 0xfb, 0xf1, 0x70, 0xd0, 0xd3, 0xff, 0xfb, 0x56, 0xa9,
|
||||
0x6e, 0xe9, 0x06, 0x8a, 0x78, 0x44, 0x8a, 0xd8, 0x3b, 0x3c, 0xd0, 0x9b, 0x91, 0xaf, 0x49, 0xab, 0x3f, 0xef, 0xde,
|
||||
0xa2, 0xbf, 0x3a, 0x4e, 0x1d, 0x0c, 0x69, 0xeb, 0xa3, 0x6a, 0xc1, 0x35, 0x9a, 0x97, 0xef, 0xff, 0xbc, 0xbd, 0x3b,
|
||||
0x47, 0xb8, 0xee, 0x99, 0x10, 0x28, 0x36, 0x3c, 0xf7, 0xd6, 0xd2, 0x89, 0x8e, 0x1a, 0xd7, 0xab, 0x8d, 0xfc, 0x14,
|
||||
0x39, 0xc5, 0xd0, 0xd3, 0x6b, 0x21, 0x61, 0x08, 0x63, 0x10, 0xac, 0xd0, 0xc9, 0xab, 0x93, 0xb5, 0x67, 0x7e, 0xc5,
|
||||
0xc3, 0x6d, 0xc7, 0xc3, 0xd5, 0xc7, 0xfd, 0xcb, 0xf7, 0xbf, 0x81, 0xdb, 0x13, 0xb5, 0x09, 0x0b, 0x00, 0x00};
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x56, 0x6d, 0x8f, 0xdb, 0x36, 0x0c, 0xfe, 0xbe,
|
||||
0x5f, 0xa1, 0x79, 0xdd, 0x6a, 0xaf, 0xb1, 0xfc, 0x92, 0x4b, 0xda, 0x3a, 0x96, 0x8b, 0xee, 0xd6, 0x62, 0x03, 0xd6,
|
||||
0xad, 0xc0, 0xdd, 0xba, 0x0f, 0x45, 0x01, 0x2b, 0x32, 0x1d, 0xab, 0x27, 0x4b, 0x9e, 0xa4, 0xbc, 0x35, 0xc8, 0x7e,
|
||||
0xfb, 0x20, 0xdb, 0xc9, 0xe5, 0x8a, 0x16, 0xd8, 0x10, 0xc4, 0xa0, 0x44, 0xf2, 0xe1, 0x8b, 0x28, 0x52, 0xf9, 0xb7,
|
||||
0x95, 0x62, 0x76, 0xdf, 0x01, 0x6a, 0x6c, 0x2b, 0x8a, 0xdc, 0x7d, 0x91, 0xa0, 0x72, 0x45, 0x40, 0x16, 0x79, 0x03,
|
||||
0xb4, 0x2a, 0xf2, 0x16, 0x2c, 0x45, 0xac, 0xa1, 0xda, 0x80, 0x25, 0x7f, 0xde, 0xbe, 0x0e, 0x9f, 0x15, 0xb9, 0xe0,
|
||||
0xf2, 0x0e, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0xa8,
|
||||
0x22, 0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0xc4, 0xdb, 0xf2, 0xca, 0x36,
|
||||
0xa4, 0x82, 0x0d, 0x67, 0x10, 0xf6, 0x8b, 0x09, 0x97, 0xdc, 0x72, 0x2a, 0x42, 0xc3, 0xa8, 0x00, 0x92, 0x4c, 0xd6,
|
||||
0x06, 0x74, 0xbf, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0xaf, 0xc8, 0x0d, 0xd3, 0xbc, 0xb3, 0xc8, 0xb9, 0x4a, 0x5a, 0x55,
|
||||
0xad, 0x05, 0x20, 0xa6, 0x95, 0x31, 0x4a, 0xf3, 0x15, 0x97, 0x45, 0xa5, 0xd8, 0xba, 0x05, 0x69, 0xb1, 0x50, 0x8c,
|
||||
0x5a, 0xae, 0x24, 0x36, 0x40, 0x35, 0x6b, 0x08, 0x21, 0xe5, 0x0b, 0x43, 0x37, 0x50, 0xfe, 0xf0, 0x83, 0x7f, 0x16,
|
||||
0x5a, 0x81, 0x7d, 0x25, 0xc0, 0x91, 0xe6, 0xa7, 0xfd, 0x2d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x4b, 0x6a, 0x78, 0x05,
|
||||
0x65, 0xf0, 0x3e, 0xfe, 0x80, 0x8d, 0xdd, 0x0b, 0xc0, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x52, 0x2e, 0x85, 0x62, 0x77,
|
||||
0x65, 0xb0, 0xa8, 0xd7, 0x92, 0x39, 0x70, 0x04, 0x3e, 0x04, 0x07, 0x01, 0x16, 0x49, 0xf2, 0x86, 0xda, 0x06, 0xb7,
|
||||
0x74, 0xe7, 0x0f, 0x04, 0x97, 0x7e, 0xfa, 0xa3, 0x0f, 0x4f, 0x92, 0x38, 0x0e, 0x26, 0xfd, 0x27, 0x0e, 0xa2, 0x24,
|
||||
0x8e, 0x17, 0x1a, 0xec, 0x5a, 0x4b, 0x64, 0xfd, 0x32, 0xef, 0xa8, 0x6d, 0x50, 0x45, 0xbc, 0x37, 0x49, 0x8a, 0x92,
|
||||
0xe7, 0x38, 0x9d, 0xfd, 0x86, 0x9f, 0xa2, 0x2b, 0x9c, 0xce, 0xd8, 0xd3, 0x70, 0x86, 0x92, 0xab, 0x70, 0x86, 0xd2,
|
||||
0x14, 0xcf, 0x50, 0xfc, 0xc9, 0x43, 0x35, 0x17, 0x82, 0x78, 0x52, 0x49, 0xf0, 0x90, 0xb1, 0x5a, 0xdd, 0x01, 0xf1,
|
||||
0xd8, 0x5a, 0x6b, 0x90, 0xf6, 0x5a, 0x09, 0xa5, 0xbd, 0xa8, 0xf8, 0xe6, 0x7f, 0x01, 0x5a, 0x4d, 0xa5, 0xa9, 0x95,
|
||||
0x6e, 0x89, 0xd7, 0xa7, 0xdb, 0x7f, 0x74, 0x90, 0x47, 0xe4, 0x3e, 0xc1, 0x05, 0x33, 0x1c, 0xf2, 0x4a, 0x3c, 0x87,
|
||||
0xf8, 0xcc, 0x8b, 0x8a, 0x32, 0x38, 0x9e, 0xa3, 0xb7, 0x2e, 0xfa, 0x31, 0x1e, 0xed, 0xbf, 0x2f, 0x73, 0xb3, 0x59,
|
||||
0xa1, 0x5d, 0x2b, 0xa4, 0x21, 0x5e, 0x63, 0x6d, 0x97, 0x45, 0xd1, 0x76, 0xbb, 0xc5, 0xdb, 0x29, 0x56, 0x7a, 0x15,
|
||||
0xa5, 0x71, 0x1c, 0x47, 0x66, 0xb3, 0xf2, 0xd0, 0x70, 0xf2, 0x5e, 0x7a, 0xe5, 0xa1, 0x06, 0xf8, 0xaa, 0xb1, 0x3d,
|
||||
0x5d, 0x3c, 0x3a, 0xc0, 0x31, 0x77, 0x12, 0x45, 0xf9, 0xe1, 0xc2, 0x8a, 0xbc, 0xb0, 0x02, 0x2f, 0x2e, 0xf2, 0xf6,
|
||||
0xb8, 0x0f, 0xf3, 0x29, 0x4d, 0x51, 0x8a, 0xe2, 0xfe, 0x97, 0x86, 0x8e, 0x1e, 0x57, 0xe1, 0x67, 0x2b, 0x74, 0xb1,
|
||||
0x72, 0x54, 0x3b, 0x0f, 0x9f, 0x9f, 0x75, 0x13, 0xb7, 0xb3, 0x49, 0xe2, 0xfb, 0x0d, 0xa7, 0xf0, 0xcb, 0xfc, 0x72,
|
||||
0x1d, 0xa6, 0xef, 0x2e, 0x05, 0x9c, 0xb5, 0x26, 0x79, 0x37, 0xa7, 0x33, 0x34, 0x1b, 0x77, 0x66, 0xa1, 0xa3, 0xcf,
|
||||
0x2b, 0x34, 0xdb, 0xa4, 0x4d, 0xd2, 0x86, 0xf3, 0x70, 0x46, 0xa7, 0x68, 0x3a, 0x3a, 0x32, 0x45, 0xd3, 0x4d, 0xda,
|
||||
0xcc, 0xdf, 0xcd, 0x2f, 0xf7, 0xc2, 0xe9, 0xa7, 0xc7, 0x2e, 0xb9, 0x59, 0x59, 0xde, 0x47, 0xae, 0x2f, 0x23, 0xc7,
|
||||
0x1f, 0x15, 0x97, 0x7e, 0xe9, 0xf2, 0x0f, 0x96, 0x35, 0x7e, 0x19, 0x31, 0x25, 0x6b, 0xbe, 0xc2, 0x1f, 0x8d, 0x92,
|
||||
0x65, 0x80, 0x6d, 0x03, 0xd2, 0x3f, 0xa9, 0xfa, 0x36, 0x38, 0xd8, 0x9e, 0xe3, 0x7f, 0x81, 0x73, 0xae, 0x7f, 0xcb,
|
||||
0xad, 0x00, 0x62, 0xb1, 0xbb, 0xa1, 0x93, 0x2f, 0xdc, 0x8a, 0x9f, 0xf6, 0xbf, 0x56, 0x7e, 0xd9, 0x52, 0x56, 0x06,
|
||||
0x98, 0x4b, 0x09, 0xfa, 0x16, 0x76, 0x96, 0x94, 0x6f, 0x5e, 0x5e, 0xa3, 0x97, 0x55, 0xa5, 0xc1, 0x98, 0x0c, 0x95,
|
||||
0x4f, 0x2c, 0x6e, 0x29, 0xfb, 0xba, 0x7a, 0x93, 0x3c, 0xd4, 0xfe, 0x8b, 0xbf, 0xe6, 0xe8, 0x77, 0xb0, 0x5b, 0xa5,
|
||||
0xef, 0x46, 0x7d, 0x67, 0x7f, 0xe1, 0xae, 0x91, 0x26, 0x5f, 0x85, 0x91, 0x60, 0xcb, 0x60, 0xc2, 0xbf, 0x2e, 0x60,
|
||||
0x0c, 0xaf, 0xca, 0x60, 0x42, 0xbf, 0x2e, 0xd1, 0x19, 0x77, 0x79, 0x2d, 0xa6, 0x9d, 0xc1, 0x46, 0x70, 0x06, 0x7e,
|
||||
0x12, 0xe0, 0x5a, 0xe9, 0x57, 0x94, 0x35, 0x0f, 0x12, 0xe4, 0x5c, 0x51, 0xf7, 0x38, 0x4c, 0x03, 0xb5, 0x30, 0x42,
|
||||
0xf9, 0x65, 0xc5, 0x37, 0x65, 0xb0, 0x50, 0x98, 0x09, 0x6a, 0x8c, 0x6b, 0x19, 0xc4, 0x39, 0xe7, 0xc2, 0x29, 0x27,
|
||||
0x6a, 0x88, 0xf4, 0x97, 0xdb, 0x37, 0xbf, 0x91, 0x32, 0xa7, 0x43, 0x47, 0xf4, 0xbe, 0xf3, 0x50, 0x2f, 0x4c, 0xbc,
|
||||
0x51, 0x30, 0x14, 0x50, 0xdb, 0xbe, 0xe2, 0x7d, 0x8b, 0xb5, 0x31, 0x3c, 0x38, 0xe6, 0xa6, 0xa3, 0xf2, 0x73, 0x31,
|
||||
0x17, 0x93, 0x57, 0xe4, 0x91, 0xe3, 0x15, 0x79, 0x44, 0x8b, 0x47, 0x07, 0xe9, 0xf7, 0xcd, 0xed, 0x2e, 0x38, 0x3a,
|
||||
0x6b, 0x7f, 0xaf, 0x41, 0xef, 0x6f, 0x40, 0x00, 0xb3, 0x4a, 0xfb, 0x25, 0xbe, 0x54, 0x74, 0x45, 0x01, 0x3b, 0x7b,
|
||||
0x3d, 0x36, 0x5c, 0x8b, 0xdd, 0xe6, 0x44, 0x61, 0x25, 0x99, 0xe0, 0xec, 0x8e, 0x9c, 0x23, 0x0e, 0x0e, 0x1c, 0x6f,
|
||||
0xa8, 0x58, 0xc3, 0x49, 0x86, 0xe2, 0x5a, 0xb1, 0xb5, 0xf1, 0x83, 0xe3, 0x44, 0x63, 0xda, 0x75, 0x20, 0xab, 0xeb,
|
||||
0x86, 0x8b, 0xca, 0x57, 0xc1, 0x31, 0xb8, 0x3f, 0xe9, 0xcf, 0x8c, 0xbb, 0x59, 0xf0, 0x5e, 0x83, 0xf8, 0x87, 0x3c,
|
||||
0x76, 0xd3, 0xe0, 0xf1, 0x87, 0x32, 0xc0, 0x7d, 0xfc, 0xe5, 0xfd, 0x48, 0x70, 0xd7, 0xfb, 0xc9, 0xae, 0x15, 0x13,
|
||||
0x17, 0x7a, 0x38, 0x9f, 0x05, 0xc7, 0xf2, 0x18, 0x1c, 0x83, 0x45, 0x1e, 0x0d, 0x8d, 0xbd, 0xc8, 0xfb, 0x96, 0xdb,
|
||||
0x8f, 0x94, 0x9e, 0x32, 0x0d, 0x80, 0x7d, 0xd0, 0xe2, 0x7f, 0x3c, 0x2c, 0xd5, 0x2e, 0x34, 0xfc, 0x13, 0x97, 0xab,
|
||||
0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0x26, 0x5c, 0x76, 0x6b, 0x7b, 0xe8, 0x68, 0x55, 0x39, 0xce, 0xac,
|
||||
0xdb, 0x2d, 0x6a, 0x25, 0xad, 0x93, 0x84, 0x2c, 0x81, 0xf6, 0x38, 0xf0, 0xfb, 0xe6, 0x93, 0x3d, 0x9f, 0x7d, 0x7f,
|
||||
0x5c, 0xaa, 0x6a, 0x7f, 0x70, 0x19, 0x0a, 0xa9, 0xe0, 0x2b, 0x99, 0x31, 0x90, 0x16, 0xf4, 0xa0, 0x54, 0xd3, 0x96,
|
||||
0x8b, 0x7d, 0x66, 0xa8, 0x34, 0xa1, 0x01, 0xcd, 0xeb, 0xe3, 0x72, 0x6d, 0xad, 0x92, 0x07, 0xe6, 0x9a, 0x6d, 0xf6,
|
||||
0x5d, 0x5d, 0xd7, 0x0b, 0xb6, 0xd6, 0x46, 0xe9, 0xac, 0x53, 0xbc, 0xd7, 0x5b, 0x52, 0x76, 0xb7, 0xd2, 0x6a, 0x2d,
|
||||
0xab, 0x70, 0x14, 0x4a, 0x6a, 0x3a, 0x05, 0xb6, 0x58, 0x2a, 0x5d, 0x81, 0xce, 0xe2, 0x91, 0x08, 0x35, 0xad, 0xf8,
|
||||
0xda, 0x64, 0x78, 0xaa, 0xa1, 0x5d, 0x0c, 0xee, 0x24, 0x71, 0xfc, 0xfd, 0xe2, 0xe4, 0x79, 0x7c, 0xe9, 0x37, 0x4e,
|
||||
0x9d, 0x94, 0xe0, 0x12, 0xc2, 0xa1, 0x57, 0x66, 0x29, 0xbe, 0xd2, 0xd0, 0x1e, 0x5b, 0xca, 0xe5, 0xa5, 0xf7, 0xae,
|
||||
0xa2, 0x16, 0x2d, 0x97, 0xc3, 0x28, 0xcd, 0xd2, 0x79, 0xdc, 0xed, 0x16, 0xe3, 0xe4, 0xca, 0xb8, 0xec, 0x11, 0xfa,
|
||||
0xf9, 0x75, 0x3c, 0x15, 0xc9, 0xe1, 0xe3, 0xda, 0x58, 0x5e, 0xef, 0xc3, 0x71, 0x24, 0x67, 0xa6, 0xa3, 0x0c, 0xc2,
|
||||
0x25, 0xd8, 0x2d, 0x80, 0x5c, 0xf4, 0xb0, 0x21, 0xb7, 0xd0, 0x9a, 0x53, 0x6a, 0x4e, 0x70, 0xb5, 0x80, 0xdd, 0x19,
|
||||
0xa6, 0xaf, 0xe5, 0xc3, 0x7f, 0x96, 0x76, 0x05, 0x76, 0x68, 0xa9, 0x5e, 0x71, 0x19, 0x2e, 0x95, 0xb5, 0xaa, 0xcd,
|
||||
0xc2, 0xa7, 0xdd, 0x6e, 0x31, 0x6e, 0x39, 0xb0, 0x2c, 0x89, 0xbb, 0xdd, 0xb1, 0x1f, 0xc3, 0xa7, 0x7c, 0x5f, 0xd5,
|
||||
0xcf, 0x68, 0x12, 0x7f, 0x21, 0xc7, 0x55, 0x5d, 0xa7, 0xcb, 0xfa, 0x94, 0xe3, 0xa4, 0xdb, 0x21, 0xa3, 0x04, 0xaf,
|
||||
0x46, 0xb8, 0x1e, 0x09, 0xc5, 0xe7, 0xd4, 0x26, 0xb3, 0x6e, 0x87, 0x92, 0xcb, 0xcc, 0xb8, 0x89, 0xea, 0xa6, 0x8e,
|
||||
0xab, 0xb5, 0x22, 0x8f, 0x86, 0x97, 0x8e, 0xab, 0x8c, 0x22, 0x77, 0x19, 0x2e, 0xf2, 0x26, 0x41, 0xbc, 0x22, 0x2d,
|
||||
0x65, 0xc5, 0x45, 0xdb, 0xcb, 0xa3, 0x26, 0x39, 0xb1, 0x9a, 0xa4, 0x78, 0xd0, 0xd2, 0x06, 0x5e, 0xef, 0x7d, 0x71,
|
||||
0xad, 0xa4, 0x04, 0x66, 0xb9, 0x5c, 0x21, 0xab, 0xd0, 0x98, 0x02, 0x8c, 0x71, 0xbe, 0xd4, 0xc5, 0x5b, 0x01, 0xd4,
|
||||
0x00, 0xda, 0x52, 0x6e, 0x71, 0x1e, 0x0d, 0xf2, 0x43, 0x13, 0xe0, 0x15, 0x91, 0x60, 0xcf, 0xd7, 0xbe, 0x99, 0x0e,
|
||||
0x06, 0x6e, 0xc0, 0x3a, 0x24, 0x67, 0x60, 0x5a, 0xe4, 0x6e, 0x3a, 0x23, 0xda, 0x5f, 0x60, 0x12, 0x6d, 0x79, 0xcd,
|
||||
0xdd, 0xeb, 0xa6, 0xc8, 0xfb, 0x22, 0x77, 0x08, 0x2e, 0xcf, 0xc3, 0xd3, 0xab, 0xa7, 0x04, 0xc8, 0x95, 0x6d, 0xc8,
|
||||
0x34, 0x45, 0x9d, 0xa0, 0x0c, 0x1a, 0x25, 0x2a, 0xd0, 0xe4, 0xe6, 0xe6, 0xd7, 0x9f, 0x0b, 0xe7, 0xcc, 0xbd, 0x5e,
|
||||
0x67, 0xee, 0x06, 0x35, 0x47, 0x8c, 0x5a, 0xf3, 0xab, 0xe1, 0xc1, 0xd5, 0x51, 0x63, 0xb6, 0x4a, 0x57, 0x0f, 0x30,
|
||||
0xde, 0x8e, 0x9b, 0x03, 0x4e, 0xff, 0xef, 0xaf, 0x4a, 0x71, 0x43, 0x37, 0x90, 0x47, 0xe3, 0x22, 0x8f, 0x9c, 0xc3,
|
||||
0x03, 0xbf, 0x19, 0xe5, 0x9a, 0xa4, 0xf8, 0xe3, 0xf6, 0x25, 0xfa, 0xb3, 0xab, 0xa8, 0x85, 0x21, 0x6d, 0x7d, 0x54,
|
||||
0x2d, 0xd8, 0x46, 0x55, 0xe4, 0xed, 0x1f, 0x37, 0xb7, 0xe7, 0x08, 0xd7, 0xbd, 0x10, 0x02, 0xc9, 0x86, 0xa7, 0xdf,
|
||||
0x5a, 0x58, 0xde, 0x51, 0x6d, 0x7b, 0xd8, 0xd0, 0x35, 0x98, 0x53, 0x0c, 0x3d, 0xbf, 0xe6, 0x02, 0x86, 0x30, 0x06,
|
||||
0xc5, 0x02, 0x9d, 0xbc, 0x3a, 0x59, 0xfb, 0xcc, 0xaf, 0x68, 0x38, 0xed, 0x68, 0x38, 0xfa, 0xa8, 0x7f, 0x05, 0xff,
|
||||
0x0b, 0x54, 0xcf, 0x54, 0x8b, 0x15, 0x0b, 0x00, 0x00};
|
||||
|
||||
#else // Brotli (default, smaller)
|
||||
constexpr uint8_t INDEX_BR[] PROGMEM = {
|
||||
0x1b, 0x08, 0x0b, 0x00, 0xe4, 0x7f, 0x9b, 0xad, 0xbb, 0x97, 0x53, 0xde, 0xb7, 0x25, 0x0e, 0x69, 0xd4, 0x69, 0x89,
|
||||
0xba, 0xa5, 0x55, 0x22, 0x04, 0x27, 0xeb, 0x00, 0x52, 0xac, 0xbc, 0xec, 0x5f, 0xfb, 0xb5, 0x7a, 0x12, 0x0a, 0xd6,
|
||||
0x48, 0x84, 0x4a, 0x48, 0x5a, 0xcb, 0xbd, 0xb7, 0x72, 0x66, 0x4e, 0x62, 0xd8, 0xbd, 0x7f, 0x88, 0x68, 0x86, 0x28,
|
||||
0xd6, 0xf1, 0xda, 0x2c, 0xa2, 0x32, 0x5c, 0xdd, 0xab, 0xb2, 0x15, 0xbc, 0x24, 0x13, 0xe6, 0xbd, 0x0c, 0x52, 0x63,
|
||||
0x82, 0x88, 0x6d, 0x74, 0x71, 0x51, 0x9c, 0xe2, 0x40, 0x56, 0xea, 0xb0, 0x5c, 0xb7, 0x1d, 0x81, 0x3a, 0x0c, 0xf2,
|
||||
0xd7, 0xa8, 0x5c, 0x35, 0x2d, 0x43, 0xb3, 0x42, 0x37, 0x7c, 0x60, 0xd8, 0xc1, 0x95, 0x91, 0x94, 0x3c, 0x29, 0x20,
|
||||
0x96, 0x20, 0xf6, 0x24, 0xb7, 0x83, 0x4a, 0x06, 0xf1, 0xc3, 0x2f, 0x88, 0x50, 0x55, 0x35, 0x2d, 0xe8, 0xb6, 0x21,
|
||||
0x38, 0x61, 0x8e, 0x44, 0x2a, 0xac, 0x39, 0xcf, 0x74, 0xaa, 0x31, 0x7f, 0x62, 0x32, 0x9b, 0x99, 0x42, 0x0a, 0xf6,
|
||||
0x6f, 0xd8, 0x89, 0x3f, 0x49, 0xb1, 0xa2, 0x52, 0x0a, 0x8e, 0xb3, 0x27, 0x0b, 0x07, 0x07, 0x58, 0xb0, 0x8f, 0xaa,
|
||||
0xdc, 0x68, 0x12, 0x0f, 0x95, 0xbf, 0xc7, 0x36, 0xd5, 0x60, 0x5a, 0xc7, 0x80, 0xac, 0x52, 0xf7, 0xa7, 0x2d, 0xb6,
|
||||
0x64, 0xda, 0xda, 0x11, 0x8d, 0x6a, 0xe5, 0xba, 0xe9, 0xda, 0xdc, 0x62, 0xc3, 0xcb, 0xae, 0xc1, 0xe1, 0x11, 0xb6,
|
||||
0x33, 0x29, 0x04, 0x09, 0xfe, 0x09, 0x08, 0xc2, 0x1f, 0x98, 0x16, 0x26, 0x0d, 0xce, 0xd7, 0x75, 0xfb, 0xd7, 0xa5,
|
||||
0x82, 0xd7, 0x32, 0x44, 0x72, 0xc1, 0xc2, 0xe4, 0x15, 0xcb, 0x50, 0xfc, 0xd3, 0x58, 0x64, 0x34, 0x41, 0x32, 0xbe,
|
||||
0x1f, 0x08, 0x43, 0x16, 0x14, 0xf7, 0x70, 0x2c, 0x1c, 0x61, 0x09, 0x78, 0x73, 0xd1, 0x30, 0xf6, 0xed, 0x85, 0x55,
|
||||
0x50, 0x02, 0xf0, 0x68, 0x50, 0x5c, 0x1a, 0xd7, 0x3b, 0x8e, 0xf2, 0x23, 0xc8, 0x88, 0x39, 0xd0, 0x84, 0xf7, 0xa6,
|
||||
0xd1, 0xa3, 0xfb, 0xe5, 0x44, 0xc0, 0x4c, 0x2f, 0x6f, 0x19, 0x70, 0x75, 0xf6, 0x1c, 0xb8, 0xce, 0x89, 0x4f, 0x01,
|
||||
0x8d, 0xa1, 0x71, 0xf2, 0x97, 0xf8, 0xe7, 0xd3, 0xf1, 0xe1, 0xfa, 0xfc, 0xc8, 0x03, 0x78, 0x14, 0xa0, 0x3d, 0x28,
|
||||
0xb7, 0xeb, 0x26, 0x52, 0x59, 0xa8, 0xb5, 0x0d, 0x5c, 0x8a, 0x6b, 0xe5, 0xf6, 0x30, 0xd6, 0xf7, 0x71, 0x31, 0xe8,
|
||||
0xbd, 0xc9, 0xfa, 0xf5, 0x71, 0x9d, 0xff, 0xf6, 0x69, 0x62, 0x5f, 0xb5, 0xc7, 0x06, 0x43, 0x54, 0xb5, 0x87, 0xf1,
|
||||
0xcc, 0x84, 0xe8, 0xb7, 0x56, 0x38, 0x43, 0x6a, 0xa6, 0x2f, 0x53, 0xd1, 0x89, 0x48, 0x8d, 0x72, 0x75, 0x4a, 0x17,
|
||||
0xf6, 0x8d, 0xb2, 0xeb, 0xb1, 0x6b, 0x29, 0x1e, 0xd5, 0x74, 0xc7, 0xb3, 0xf4, 0xf1, 0x04, 0x0d, 0xbf, 0x08, 0x81,
|
||||
0x8f, 0xfe, 0x8d, 0xfc, 0xf2, 0x35, 0x92, 0xa0, 0x24, 0x88, 0x12, 0x6a, 0xe6, 0x2f, 0x01, 0x94, 0x5c, 0x4b, 0xf9,
|
||||
0x6b, 0x9a, 0xf6, 0x1a, 0x35, 0x11, 0x8a, 0xc6, 0x04, 0x8d, 0x50, 0x64, 0x48, 0x2d, 0x8b, 0xdf, 0xdf, 0xa1, 0xd1,
|
||||
0xfd, 0x21, 0xd7, 0x40, 0x96, 0x00, 0xb1, 0x9f, 0x54, 0xc2, 0xe1, 0x00, 0x79, 0x15, 0x80, 0xf8, 0xca, 0x8e, 0xc5,
|
||||
0x06, 0x03, 0xdf, 0x72, 0x49, 0xc0, 0x08, 0x27, 0x90, 0xe3, 0x14, 0xc2, 0xac, 0xc3, 0x83, 0xc9, 0xf6, 0x9d, 0x12,
|
||||
0xab, 0x46, 0xae, 0x03, 0x74, 0x1d, 0x27, 0xce, 0x91, 0x1d, 0x4e, 0xb4, 0xbe, 0x80, 0xe7, 0x6a, 0x6d, 0x0a, 0x20,
|
||||
0x47, 0x6b, 0x00, 0x4e, 0x25, 0x52, 0xe6, 0xf5, 0xe9, 0x43, 0x84, 0xc1, 0x0d, 0x4b, 0x04, 0xb3, 0x91, 0x49, 0xf5,
|
||||
0xe9, 0xc4, 0x2e, 0x1b, 0xf9, 0x98, 0xf9, 0xea, 0x9e, 0xb8, 0xf6, 0x53, 0xf8, 0x42, 0xc2, 0x60, 0x5c, 0xa9, 0xd2,
|
||||
0xfe, 0x0a, 0xe5, 0xd4, 0x7d, 0x36, 0x76, 0x04, 0x12, 0x38, 0xa1, 0xc4, 0x30, 0xb8, 0xf2, 0x69, 0x86, 0x5b, 0xe7,
|
||||
0xe5, 0xa0, 0xc0, 0xfe, 0x91, 0x19, 0x03, 0x1e, 0xa9, 0x93, 0x26, 0x49, 0x58, 0xd5, 0xf6, 0x33, 0x4e, 0x0a, 0x89,
|
||||
0x64, 0x1e, 0xb4, 0x7a, 0x5d, 0x0d, 0x12, 0xcf, 0x2b, 0xdd, 0x35, 0x90, 0x55, 0xc3, 0x0e, 0xcd, 0x0e, 0xad, 0x6b,
|
||||
0x8f, 0x43, 0xd0, 0xb4, 0x6a, 0xdb, 0x4b, 0xe5, 0xa4, 0x9b, 0x09, 0x23, 0x1a, 0x43, 0xa9, 0xff, 0x61, 0x8b, 0x07,
|
||||
0xd6, 0x0f, 0x83, 0x23, 0xbe, 0x8a, 0x66, 0xa2, 0x10, 0x2f, 0x11, 0x01, 0x0d, 0xc6, 0x96, 0xba, 0x87, 0xaa, 0xa8,
|
||||
0x44, 0x7c, 0x1e, 0x34, 0xdb, 0xba, 0x41, 0x90, 0xf0, 0x7a, 0xdb, 0x63, 0x60, 0x96, 0x8d, 0x64, 0xcb, 0x2f, 0x28,
|
||||
0x96, 0x04, 0xd5, 0xc0, 0x7a, 0xe8, 0xec, 0xd1, 0x61, 0x1b, 0x09, 0x4b, 0x4c, 0x6e, 0x1b, 0x31, 0x98, 0x9c, 0x6d,
|
||||
0xdb, 0xd0, 0xec, 0xa4, 0x0f, 0x0a, 0xe0, 0xc8, 0x35, 0xc4, 0x93, 0xdc, 0xee, 0x19, 0x00, 0x80, 0x07, 0xf5, 0xcf,
|
||||
0xe0, 0x7f, 0x75, 0xf8, 0x52, 0x3a, 0xfc, 0x0d, 0x84, 0xa5, 0xc1, 0xb2, 0x1c, 0x25, 0x40, 0xc5, 0x8b, 0xb3, 0xdb,
|
||||
0x7a, 0x1b, 0x44, 0xff, 0x79, 0x9a, 0x26, 0xc4, 0xe7, 0x9e, 0x78, 0x4c, 0xa5, 0x94, 0xab, 0x78, 0x34, 0x9d, 0xb5,
|
||||
0xb7, 0x24, 0x26, 0xe7, 0x9a, 0xf3, 0xe5, 0x2a, 0x35, 0x4b, 0xbe, 0x74, 0xd7, 0x01, 0xbc, 0x71, 0x72, 0x03, 0x5c,
|
||||
0x40, 0x24, 0x17, 0x61, 0x5b, 0x7b, 0x19, 0xc2, 0x7f, 0x4e, 0x5b, 0x24, 0xfb, 0x8b, 0xc3, 0x51, 0x00, 0x3d, 0x09,
|
||||
0x04, 0x05, 0x72, 0x64, 0xf6, 0xf0, 0x6b, 0x99, 0x38, 0x93, 0x5b, 0xac, 0x0c, 0xff, 0x40, 0x7b, 0x53, 0xba, 0xab,
|
||||
0x61, 0xc9, 0xa2, 0xde, 0xd6, 0x2b, 0x0c, 0xbd, 0x85, 0xac, 0x4c, 0x64, 0x8b, 0xe6, 0x69, 0x2b, 0x56, 0x10, 0x1b,
|
||||
0x08, 0x59, 0x6c, 0x55, 0x0a, 0xaa, 0x93, 0x85, 0x1d, 0x45, 0xda, 0xc6, 0x39, 0xe6, 0x6a, 0xab, 0x71, 0x73, 0x46,
|
||||
0x0f, 0xf7, 0xab, 0x4e, 0x88, 0xae, 0x8c, 0xe0, 0x91, 0xda, 0x35, 0x8c, 0xd3, 0x18, 0x92, 0x3d, 0xab, 0x2f, 0x0d,
|
||||
0xd6, 0xc9, 0x06, 0xdb, 0xc2, 0x62, 0xcb, 0x8a, 0x23, 0x5b, 0x28, 0x2e, 0x9b, 0x96, 0xc4, 0xc1, 0x42, 0xa9, 0x8d,
|
||||
0x69, 0xe5, 0x8f, 0x89, 0x12, 0x11, 0x9a, 0x56, 0x3b, 0x3c, 0x2b, 0xb4, 0xb2, 0x7b, 0xfd, 0xdb, 0x80, 0x92, 0xb4,
|
||||
0xca, 0x44, 0x37, 0x8c, 0x94, 0x2f, 0x4a, 0xb4, 0xc4, 0x28, 0x83, 0x8a, 0x78, 0xcb, 0xd2, 0x5c, 0x5c, 0x35, 0x29,
|
||||
0x95, 0x35, 0x2f, 0x99, 0x28, 0x4f, 0x22, 0x18, 0x70, 0x85, 0xee, 0x2c, 0xb9, 0xef, 0x14, 0x57, 0x73, 0x23, 0x45,
|
||||
0xae, 0xdc, 0x54, 0x56, 0x55, 0x78, 0x56, 0xad, 0x48, 0x26, 0x27, 0xbf, 0xcb, 0xd7, 0x54, 0xa9, 0xa8, 0xd7, 0xb5,
|
||||
0x71, 0x9c, 0xe7, 0x79, 0x89, 0x5c, 0xa9, 0x6a, 0x53, 0xe8, 0xfa, 0x0d, 0x69, 0x36, 0xed, 0xad, 0x33, 0xc9, 0x75,
|
||||
0x17, 0xe9, 0x8f, 0x31, 0x58, 0xa6, 0x27, 0xfc, 0x79, 0xc6, 0xb6, 0xd5, 0x65, 0x90, 0x31, 0xc6, 0x9d, 0x29, 0x95,
|
||||
0x83, 0xe5, 0x4e, 0xbb, 0xd7, 0xdc, 0x8e, 0xd0, 0x3a, 0x54, 0x27, 0xce, 0xf5, 0xdb, 0xbd, 0x89, 0x3c, 0x61, 0xb3,
|
||||
0x71, 0x23, 0xc7, 0x55, 0x9e, 0xea, 0x50, 0xe4, 0x37, 0xae, 0x72, 0x34, 0x66, 0xb9, 0x6e, 0x2c, 0x0a, 0x69, 0x8d,
|
||||
0x94, 0x8b, 0x98, 0x08, 0x05, 0x3c, 0x45, 0x45, 0xe1, 0x6c, 0x22, 0xc5, 0x8f, 0x1f, 0x9f, 0x3f, 0xd2, 0x01, 0x12,
|
||||
0xec, 0x86, 0x2f, 0x87, 0x8b, 0x47, 0x30, 0xd0, 0x77, 0x77, 0x1a, 0x13, 0x2d, 0xc6, 0x31, 0x65, 0x77, 0xd8, 0x8c,
|
||||
0xe7, 0xe0, 0x16, 0xf9, 0x4b, 0xd4, 0xc5, 0xa8, 0x67, 0x79, 0xe7, 0xc3, 0x07, 0x94, 0x46, 0xa3, 0x8c, 0x67, 0xd3,
|
||||
0xff, 0x5f, 0x96, 0xfa, 0xed, 0xa7, 0xd3, 0xdd, 0x4f, 0x27, 0x49, 0x27, 0xcf, 0xa4, 0x02, 0xc3, 0x27, 0x3c, 0x96,
|
||||
0x64, 0x24, 0x55, 0xc8, 0x36, 0x45, 0x68, 0x90, 0xea, 0x03, 0x0b, 0x46, 0xa7, 0x8d, 0xb4, 0x26, 0x91, 0x07, 0x4c,
|
||||
0x80, 0x7b, 0x93, 0xa8, 0x10, 0xcb, 0x5e, 0x8d, 0x42, 0x86, 0x3e, 0x2a, 0x7c, 0x99, 0x79, 0x8e, 0xcb, 0x43, 0x28};
|
||||
0x1b, 0x14, 0x0b, 0x00, 0xe4, 0x6f, 0xcd, 0xfc, 0x7b, 0x2e, 0x27, 0xd2, 0x21, 0x2b, 0x58, 0xea, 0x16, 0xf8, 0xa5,
|
||||
0xb6, 0x47, 0x14, 0xb3, 0x4c, 0x56, 0xcc, 0x20, 0x5b, 0x1d, 0xfe, 0x7d, 0xfb, 0xb5, 0x7a, 0x12, 0x0a, 0xd6, 0x48,
|
||||
0x84, 0xa2, 0x21, 0x69, 0x0d, 0x77, 0x33, 0xf3, 0x77, 0xcf, 0x4c, 0x21, 0xf1, 0xde, 0xee, 0x7d, 0x44, 0xbc, 0xd3,
|
||||
0xc4, 0x33, 0x89, 0x1a, 0x69, 0x68, 0x48, 0x57, 0xa7, 0x17, 0x0b, 0x4d, 0x91, 0xac, 0x30, 0x48, 0x21, 0x4d, 0x4c,
|
||||
0x10, 0x57, 0x46, 0x14, 0x17, 0xd5, 0x21, 0x0e, 0xb4, 0x50, 0x87, 0xad, 0x62, 0xda, 0x11, 0x68, 0xc3, 0x20, 0x7f,
|
||||
0x2d, 0xdc, 0x57, 0xeb, 0x2c, 0x36, 0xdb, 0xc4, 0x84, 0x0f, 0xac, 0xec, 0x08, 0x91, 0xa3, 0x92, 0x27, 0x45, 0xc4,
|
||||
0x1e, 0xa4, 0x9e, 0x72, 0x3b, 0xc2, 0xe3, 0x20, 0x7d, 0xf8, 0x05, 0x09, 0xea, 0xe3, 0xa6, 0x3f, 0x11, 0x8b, 0x16,
|
||||
0x11, 0x6b, 0x26, 0x91, 0x1a, 0x2c, 0x19, 0x24, 0xf7, 0x5d, 0x44, 0x82, 0x49, 0x44, 0x15, 0xce, 0x39, 0xdc, 0xcb,
|
||||
0x8f, 0x5b, 0xe1, 0xe2, 0x42, 0x96, 0xa2, 0x10, 0x3c, 0x20, 0xa5, 0x65, 0x85, 0x49, 0x6a, 0x06, 0x08, 0xd1, 0xcb,
|
||||
0x41, 0xb8, 0x49, 0x20, 0x73, 0x71, 0xfe, 0x55, 0x61, 0x45, 0xc6, 0x95, 0x72, 0xc8, 0xf0, 0xa5, 0xe9, 0xe6, 0x3a,
|
||||
0xb9, 0xc3, 0x8a, 0x9f, 0xb5, 0xc1, 0xc9, 0x15, 0x56, 0x93, 0x38, 0x8a, 0x48, 0xf0, 0x4f, 0x38, 0x22, 0xe1, 0x8d,
|
||||
0x55, 0xbb, 0x8c, 0xc3, 0xb0, 0x68, 0xcc, 0x7f, 0x6f, 0xf8, 0xc9, 0x9b, 0x38, 0x41, 0xf1, 0x94, 0x25, 0xf9, 0x6b,
|
||||
0x56, 0xa2, 0xec, 0xa7, 0xbd, 0x2e, 0x69, 0x8e, 0xe2, 0xec, 0x36, 0x94, 0x24, 0x2c, 0x12, 0x1d, 0x4e, 0x76, 0x7e,
|
||||
0x23, 0xcc, 0xf2, 0x6e, 0x35, 0x1a, 0x9c, 0xed, 0x6f, 0x15, 0x3f, 0xc9, 0x72, 0xdc, 0xfd, 0x13, 0x0f, 0x16, 0x8a,
|
||||
0x23, 0x4f, 0xf9, 0x2e, 0x63, 0x44, 0x91, 0x77, 0xe0, 0xb3, 0xd1, 0x78, 0x74, 0xdb, 0x4a, 0x2c, 0xd8, 0xe8, 0xf9,
|
||||
0x2c, 0x03, 0xbe, 0xae, 0xac, 0x4e, 0x42, 0x01, 0xc4, 0x4b, 0x40, 0xe7, 0x94, 0x34, 0x85, 0x2c, 0xfe, 0xf5, 0x74,
|
||||
0x75, 0xd8, 0xdc, 0xec, 0x6a, 0x00, 0x3f, 0x3f, 0x81, 0x77, 0xa8, 0xcd, 0xde, 0x6d, 0x5a, 0x47, 0xa1, 0x99, 0x36,
|
||||
0x87, 0xb6, 0x78, 0x35, 0x3c, 0x9a, 0x64, 0x15, 0x7c, 0x56, 0x76, 0x22, 0xce, 0x46, 0xe5, 0x17, 0x57, 0x05, 0xfc,
|
||||
0x09, 0x69, 0xae, 0x39, 0xaa, 0xee, 0xc9, 0x4e, 0x7f, 0x99, 0x2a, 0x65, 0x82, 0x7e, 0xeb, 0x23, 0x4f, 0x42, 0xd5,
|
||||
0xca, 0xcb, 0x42, 0x74, 0x2e, 0xd2, 0xa2, 0x62, 0x57, 0xd0, 0xa9, 0x7b, 0x4b, 0xac, 0x7b, 0x65, 0x13, 0x47, 0x0f,
|
||||
0x2d, 0x3d, 0xf6, 0xbc, 0x78, 0x5c, 0xa3, 0xc9, 0x57, 0x4b, 0x10, 0x62, 0x68, 0x19, 0x7f, 0xfd, 0x1a, 0xcb, 0x51,
|
||||
0x1e, 0x41, 0x39, 0x55, 0xf3, 0x97, 0x30, 0xca, 0x37, 0xb6, 0x42, 0x1d, 0x2d, 0x8c, 0xc6, 0x65, 0x8a, 0xd2, 0x89,
|
||||
0x88, 0xa6, 0x28, 0xbd, 0x56, 0x7c, 0x2d, 0x5e, 0x0d, 0x2f, 0x9e, 0xc3, 0xa5, 0x80, 0xaf, 0xcc, 0x00, 0xde, 0xe6,
|
||||
0x5a, 0x8b, 0xda, 0xff, 0x1f, 0x16, 0xc8, 0x83, 0x5e, 0xe5, 0xea, 0x25, 0x86, 0x70, 0x55, 0x25, 0x01, 0x14, 0x0c,
|
||||
0x77, 0xd8, 0x31, 0x21, 0xcc, 0x79, 0x15, 0x3b, 0x32, 0x3a, 0xd3, 0xc5, 0x30, 0xaf, 0x03, 0xd8, 0x3e, 0x44, 0xb8,
|
||||
0x63, 0xb5, 0x74, 0x4f, 0x41, 0x4b, 0xf0, 0x24, 0x74, 0xb2, 0x06, 0xb2, 0x7b, 0x06, 0x60, 0xdc, 0xc1, 0x3e, 0x0e,
|
||||
0x6f, 0x1e, 0x3c, 0x42, 0xa0, 0xdb, 0x36, 0x43, 0x30, 0x71, 0xcc, 0xd6, 0x1a, 0xbd, 0x38, 0x61, 0x19, 0x3f, 0xf2,
|
||||
0xdf, 0xf4, 0x53, 0x3d, 0xc1, 0x14, 0xbe, 0x50, 0x1c, 0x2c, 0xf3, 0xaa, 0x74, 0x36, 0xcb, 0xbd, 0x7a, 0x4b, 0xa3,
|
||||
0x1c, 0x90, 0x40, 0x5b, 0x4a, 0x0f, 0x83, 0x6e, 0x9e, 0x96, 0x28, 0x3d, 0x77, 0x43, 0x05, 0x0e, 0x39, 0x26, 0x15,
|
||||
0xb8, 0x6b, 0x4e, 0x3a, 0x62, 0xc2, 0xda, 0xde, 0x8e, 0x29, 0x29, 0x0b, 0x09, 0xa2, 0xb3, 0xa7, 0x1e, 0x9a, 0x0c,
|
||||
0x8f, 0xa1, 0x46, 0x6f, 0x92, 0xf3, 0x9e, 0xb5, 0xc5, 0x7e, 0x0e, 0x8d, 0xeb, 0x55, 0x08, 0xfa, 0xc9, 0xd8, 0x4e,
|
||||
0xe3, 0xd0, 0xca, 0x2a, 0x96, 0x11, 0x7e, 0xb1, 0xd4, 0x7f, 0x8f, 0x1d, 0xb3, 0xc3, 0xa0, 0x89, 0x6f, 0x93, 0x99,
|
||||
0x55, 0x48, 0x97, 0x08, 0x79, 0x66, 0xe9, 0x4a, 0x6b, 0xa0, 0x29, 0xf2, 0x10, 0x1f, 0x22, 0xa2, 0x1b, 0x41, 0xdf,
|
||||
0xa3, 0xde, 0x62, 0x60, 0x8e, 0xa1, 0x60, 0xc4, 0xd5, 0xce, 0x81, 0x47, 0x84, 0x3b, 0x66, 0x60, 0x74, 0xa7, 0x74,
|
||||
0xcc, 0x48, 0xe0, 0xe1, 0xd5, 0x0c, 0x15, 0x98, 0x3d, 0xa7, 0x9c, 0xb2, 0xec, 0xba, 0x0f, 0x2c, 0x52, 0x14, 0x7b,
|
||||
0xe2, 0x49, 0x6e, 0x0f, 0x8c, 0x00, 0xe0, 0x81, 0xf6, 0x57, 0xe4, 0x3f, 0xbf, 0x7c, 0xa9, 0x5e, 0xfe, 0x01, 0xc2,
|
||||
0x64, 0xb0, 0x05, 0x60, 0x01, 0xaa, 0x78, 0x65, 0xb2, 0xeb, 0x56, 0x41, 0xf2, 0xbf, 0xa3, 0x45, 0x4e, 0x3c, 0x78,
|
||||
0xe2, 0xa1, 0x0d, 0xa9, 0x2a, 0xc0, 0x8a, 0xc0, 0x6f, 0xe4, 0x66, 0xbe, 0x72, 0x35, 0x5e, 0xf7, 0x3b, 0x42, 0x53,
|
||||
0xd4, 0xe6, 0x66, 0xb6, 0x78, 0xcd, 0xaa, 0x6f, 0xf4, 0x26, 0x80, 0x3a, 0x4e, 0x74, 0x80, 0x17, 0x88, 0x44, 0x23,
|
||||
0xa6, 0x3a, 0x6f, 0x87, 0xb8, 0xd0, 0x4d, 0xd3, 0xfc, 0x7c, 0xd6, 0x38, 0x2a, 0x00, 0x28, 0x01, 0xa2, 0x40, 0x94,
|
||||
0x6c, 0x1e, 0x8a, 0xed, 0xe3, 0x92, 0x9d, 0x90, 0xe3, 0x0d, 0x02, 0x4e, 0x15, 0x30, 0xed, 0x8f, 0x5b, 0x99, 0xaa,
|
||||
0x7a, 0x4e, 0xb9, 0xec, 0x91, 0xe2, 0x9f, 0xd4, 0xca, 0x46, 0xaf, 0x87, 0x19, 0x4b, 0xad, 0xea, 0xe6, 0x6c, 0x8d,
|
||||
0x53, 0x4b, 0x29, 0xee, 0x1e, 0x96, 0xd8, 0x94, 0x30, 0x3a, 0x9c, 0xb0, 0x4c, 0xdb, 0xe2, 0xa1, 0x7f, 0xc7, 0x11,
|
||||
0xdd, 0xe3, 0x9d, 0x36, 0x44, 0xd3, 0x93, 0x14, 0x9c, 0x4c, 0x9d, 0xdd, 0x3a, 0x7c, 0x41, 0xb1, 0x8f, 0x14, 0xd9,
|
||||
0x4e, 0x61, 0xd9, 0x3a, 0xe3, 0x0d, 0x76, 0xca, 0x6c, 0xac, 0x73, 0xaf, 0xad, 0x94, 0x87, 0x30, 0xf1, 0x30, 0x2f,
|
||||
0x8b, 0xed, 0x4a, 0xed, 0xbc, 0xc2, 0xf2, 0x7c, 0x30, 0xa3, 0x0b, 0x28, 0x64, 0x3b, 0x8c, 0xd4, 0xc3, 0x42, 0xb9,
|
||||
0xa3, 0x44, 0x51, 0x80, 0x07, 0x5a, 0x3d, 0x14, 0x33, 0x99, 0xbf, 0x2a, 0x6b, 0x2b, 0x19, 0x47, 0x72, 0x9e, 0xd4,
|
||||
0xb4, 0x6d, 0x72, 0xdd, 0x8a, 0x4b, 0x33, 0x55, 0xbc, 0xb4, 0xcd, 0xc8, 0x2b, 0x17, 0x2f, 0x74, 0xeb, 0x22, 0x17,
|
||||
0x94, 0x08, 0x27, 0x27, 0xc2, 0x5b, 0x17, 0xb4, 0xa9, 0x22, 0x16, 0x9d, 0xd4, 0xfc, 0xc7, 0x15, 0xa3, 0x9b, 0x86,
|
||||
0x1f, 0xad, 0x45, 0xd3, 0x87, 0x94, 0x5b, 0x31, 0x36, 0xaa, 0xe4, 0x66, 0x8d, 0xcc, 0x31, 0x05, 0x5b, 0xc4, 0x40,
|
||||
0xc0, 0xb8, 0xeb, 0x91, 0x18, 0x22, 0x8c, 0x31, 0x1e, 0xad, 0xd0, 0x3a, 0x98, 0x07, 0xb5, 0x6f, 0x11, 0xba, 0x11,
|
||||
0xa6, 0x14, 0x35, 0x5a, 0xe7, 0x55, 0xdf, 0xb7, 0x4c, 0x03, 0x61, 0xa3, 0x74, 0x23, 0xdf, 0x55, 0x1f, 0x02, 0x51,
|
||||
0x09, 0xb7, 0xba, 0xd5, 0x0c, 0x67, 0xab, 0x98, 0x70, 0x14, 0x64, 0x8d, 0xf4, 0x8b, 0x54, 0x44, 0x07, 0x6f, 0xe0,
|
||||
0x69, 0x32, 0xca, 0x48, 0xe5, 0xd3, 0xa7, 0x17, 0x8f, 0x45, 0x84, 0x04, 0xb7, 0xd1, 0xbb, 0xe1, 0xf6, 0x01, 0x0a,
|
||||
0xf6, 0xee, 0x2b, 0x32, 0xd2, 0xc5, 0xf8, 0xa6, 0xec, 0x0f, 0x1b, 0x09, 0x1d, 0xfc, 0xa2, 0xbf, 0x54, 0x5d, 0x2c,
|
||||
0x62, 0xf4, 0x77, 0xde, 0xad, 0xa0, 0x50, 0x6a, 0xb4, 0xe3, 0x5f, 0xda, 0xff, 0x6b, 0xb1, 0x78, 0xf7, 0xf9, 0xc1,
|
||||
0xa6, 0xa8, 0x93, 0xe8, 0xe4, 0x11, 0x56, 0xa0, 0x5b, 0x85, 0xa7, 0x92, 0x7a, 0x58, 0x45, 0x95, 0xa9, 0x63, 0x83,
|
||||
0xb4, 0x1f, 0x18, 0x31, 0x7a, 0x6d, 0xa1, 0x8d, 0x8c, 0xdc, 0x91, 0x02, 0x3c, 0x9c, 0x92, 0x42, 0x8e, 0x03, 0x02,
|
||||
0xc5, 0x0c, 0x43, 0x54, 0xf9, 0xb2, 0x85, 0x39, 0x2e, 0x77, 0xad, 0x00};
|
||||
|
||||
// Backwards compatibility alias
|
||||
#define INDEX_GZ INDEX_BR
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include "esphome/components/wifi/scan_list.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#ifdef USE_PROVISIONING
|
||||
#include "esphome/components/provisioning/provisioning.h"
|
||||
#endif
|
||||
#include "captive_index.h"
|
||||
|
||||
namespace esphome::captive_portal {
|
||||
@@ -78,6 +81,20 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
|
||||
void CaptivePortal::setup() {
|
||||
// Disable loop by default - will be enabled when captive portal starts
|
||||
this->disable_loop();
|
||||
#ifdef USE_PROVISIONING
|
||||
// The captive portal is a provisioning surface: once the provisioning window
|
||||
// has closed, stop serving it. WiFi's own closed-callback shuts down the
|
||||
// access point the portal runs on, and the gated fallback in WiFiComponent's
|
||||
// loop() ensures neither is started again afterwards.
|
||||
if (provisioning::global_provisioning_manager != nullptr) {
|
||||
provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
|
||||
if (this->active_) {
|
||||
ESP_LOGD(TAG, "Provisioning window closed; stopping captive portal");
|
||||
this->end();
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
}
|
||||
void CaptivePortal::start() {
|
||||
this->base_->init();
|
||||
|
||||
@@ -551,7 +551,14 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
|
||||
|
||||
void ClimateDeviceRestoreState::apply(Climate *climate) {
|
||||
auto traits = climate->get_traits();
|
||||
climate->mode = this->mode;
|
||||
// A saved mode the device no longer offers cannot be selected again, so skip it and leave the
|
||||
// entity on the mode it already has. The other saved fields are still restored.
|
||||
if (traits.supports_mode(this->mode)) {
|
||||
climate->mode = this->mode;
|
||||
} else {
|
||||
ESP_LOGW(TAG, "'%s' - Saved mode %s is no longer supported, keeping %s", climate->get_name().c_str(),
|
||||
LOG_STR_ARG(climate_mode_to_string(this->mode)), LOG_STR_ARG(climate_mode_to_string(climate->mode)));
|
||||
}
|
||||
if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
|
||||
CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
|
||||
climate->target_temperature_low = this->target_temperature_low;
|
||||
@@ -742,33 +749,39 @@ void Climate::dump_traits_(const char *tag) {
|
||||
}
|
||||
if (!traits.get_supported_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported modes:");
|
||||
for (ClimateMode m : traits.get_supported_modes())
|
||||
for (ClimateMode m : traits.get_supported_modes()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
|
||||
}
|
||||
}
|
||||
if (!traits.get_supported_fan_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported fan modes:");
|
||||
for (ClimateFanMode m : traits.get_supported_fan_modes())
|
||||
for (ClimateFanMode m : traits.get_supported_fan_modes()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
|
||||
}
|
||||
}
|
||||
if (!traits.get_supported_custom_fan_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
|
||||
for (const char *s : traits.get_supported_custom_fan_modes())
|
||||
for (const char *s : traits.get_supported_custom_fan_modes()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", s);
|
||||
}
|
||||
}
|
||||
if (!traits.get_supported_presets().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported presets:");
|
||||
for (ClimatePreset p : traits.get_supported_presets())
|
||||
for (ClimatePreset p : traits.get_supported_presets()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
|
||||
}
|
||||
}
|
||||
if (!traits.get_supported_custom_presets().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported custom presets:");
|
||||
for (const char *s : traits.get_supported_custom_presets())
|
||||
for (const char *s : traits.get_supported_custom_presets()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", s);
|
||||
}
|
||||
}
|
||||
if (!traits.get_supported_swing_modes().empty()) {
|
||||
ESP_LOGCONFIG(tag, " Supported swing modes:");
|
||||
for (ClimateSwingMode m : traits.get_supported_swing_modes())
|
||||
for (ClimateSwingMode m : traits.get_supported_swing_modes()) {
|
||||
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,9 +13,11 @@ CONF_HEADER_LOW = "header_low"
|
||||
CONF_BIT_HIGH = "bit_high"
|
||||
CONF_BIT_ONE_LOW = "bit_one_low"
|
||||
CONF_BIT_ZERO_LOW = "bit_zero_low"
|
||||
CONF_ADVANCED_COMMANDS_SUPPORT = "advanced_commands_support"
|
||||
|
||||
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend(
|
||||
{
|
||||
cv.Optional(CONF_ADVANCED_COMMANDS_SUPPORT, default=False): cv.boolean,
|
||||
cv.Optional(
|
||||
CONF_HEADER_HIGH, default="8000us"
|
||||
): cv.positive_time_period_microseconds,
|
||||
@@ -38,6 +40,7 @@ CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(LgIrClimate).extend(
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await climate_ir.new_climate_ir(config)
|
||||
|
||||
cg.add(var.set_advanced_commands_support(config[CONF_ADVANCED_COMMANDS_SUPPORT]))
|
||||
cg.add(var.set_header_high(config[CONF_HEADER_HIGH]))
|
||||
cg.add(var.set_header_low(config[CONF_HEADER_LOW]))
|
||||
cg.add(var.set_bit_high(config[CONF_BIT_HIGH]))
|
||||
|
||||
@@ -5,11 +5,85 @@ namespace esphome::climate_ir_lg {
|
||||
|
||||
static const char *const TAG = "climate.climate_ir_lg";
|
||||
|
||||
// Commands
|
||||
const uint32_t COMMAND_MASK = 0xFF000;
|
||||
const uint32_t COMMAND_OFF = 0xC0000;
|
||||
const uint32_t COMMAND_SWING = 0x10000;
|
||||
// All codes provided here are missing the checksum (last 4 bits)
|
||||
// this checksum needs to be calculated before sending (look at `calc_checksum_()`)
|
||||
|
||||
const uint32_t LG_HEADER = 0x8800000;
|
||||
|
||||
// Commands
|
||||
const uint32_t COMMAND_HEADER_MASK = 0xFF000;
|
||||
const uint32_t COMMAND_DATA_MASK = 0x00FF0;
|
||||
const uint32_t CHECKSUM_MASK = 0xF;
|
||||
|
||||
enum CommandBasic : uint32_t {
|
||||
HEADER_BASIC = 0x10000,
|
||||
BASIC_SWING_TOGGLE = 0x000,
|
||||
|
||||
// JET MODE (only for cooling/drying/heating modes)
|
||||
// For 30 minutes: max airflow (stronger than F5 aka FAN_MAX) + PO (min/min/max temperature respectively)
|
||||
// After 30 minutes: F5 aka FAN_MAX + min/min/max temperature respectively
|
||||
BASIC_JET = 0x080,
|
||||
};
|
||||
|
||||
enum CommandSys : uint32_t {
|
||||
HEADER_SYS = 0xC0000,
|
||||
|
||||
COMMAND_OFF = 0x050,
|
||||
|
||||
// Also known as 'auto-dry'
|
||||
AUTO_CLEAN_ON = 0x0B0,
|
||||
AUTO_CLEAN_OFF = 0x0C0,
|
||||
|
||||
PURIFY_ON = 0x000, // From either OFF or Mode -> Purify
|
||||
PURIFY_OFF = 0x080, // From Mode + Purify -> Mode
|
||||
|
||||
QUIET_OUTDOOR_ON = 0xA60,
|
||||
QUIET_OUTDOOR_OFF = 0xA70,
|
||||
|
||||
// ENERGY CTRL (only in Cooling mode)
|
||||
COOL_ENERG_CTRL_80 = 0x7D0, // 80%
|
||||
COOL_ENERG_CTRL_60 = 0x7E0, // 60%
|
||||
COOL_ENERG_CTRL_40 = 0x800, // 40%
|
||||
COOL_ENERG_CTRL_OFF = 0x7F0, // OFF
|
||||
|
||||
DISPLAY_KW = 0x460,
|
||||
LIGHT_ON_OFF = 0x0A0,
|
||||
|
||||
TEMP_UNIT_F = 0x170,
|
||||
TEMP_UNIT_C = 0x160,
|
||||
};
|
||||
|
||||
enum CommandAdvSwing : uint32_t {
|
||||
HEADER_ADV_SWING = 0x13000,
|
||||
|
||||
// Only 5 bits are relevant, I got 0x13952 once - not sure what is the 8th bit so ignoring that.
|
||||
ADV_SWING_DATA_MASK = 0x1F0,
|
||||
|
||||
// Commands for Advanced Vertical Control: Swing + 6 fixed positions
|
||||
VERT_FIX_1 = 0x040, // Down
|
||||
VERT_FIX_2 = 0x050,
|
||||
VERT_FIX_3 = 0x060,
|
||||
VERT_FIX_4 = 0x070,
|
||||
VERT_FIX_5 = 0x080,
|
||||
VERT_FIX_6 = 0x090, // Up
|
||||
VERT_SWING_ON = 0x140, // Swing between 1 and 6
|
||||
VERT_SWING_OFF = 0x150, // Stops immediately
|
||||
|
||||
// Commands for Advanced Horizontal Control: Swing (3 modes) + 5 fixed positions
|
||||
HORI_FIX_1 = 0x0B0, // Left
|
||||
HORI_FIX_2 = 0x0C0,
|
||||
HORI_FIX_3 = 0x0D0,
|
||||
HORI_FIX_4 = 0x0E0,
|
||||
HORI_FIX_5 = 0x0F0, // Right
|
||||
HORI_SWING_ON_LEFT = 0x100, // Swing between 1 and 3
|
||||
HORI_SWING_ON_RIGHT = 0x110, // Swing between 3 and 5
|
||||
HORI_SWING_ON_FULL = 0x160, // Swing between 1 and 5
|
||||
HORI_SWING_OFF = 0x170, // Stops immediately
|
||||
};
|
||||
|
||||
// Following commands contain mode, fan speed and temperature
|
||||
|
||||
// Modes
|
||||
const uint32_t COMMAND_ON_COOL = 0x00000;
|
||||
const uint32_t COMMAND_ON_DRY = 0x01000;
|
||||
const uint32_t COMMAND_ON_FAN_ONLY = 0x02000;
|
||||
@@ -23,11 +97,13 @@ const uint32_t COMMAND_AI = 0x0B000;
|
||||
const uint32_t COMMAND_HEAT = 0x0C000;
|
||||
|
||||
// Fan speed
|
||||
const uint32_t FAN_MASK = 0xF0;
|
||||
const uint32_t FAN_SPEED_MASK = 0xF0;
|
||||
const uint32_t FAN_AUTO = 0x50;
|
||||
const uint32_t FAN_MIN = 0x00;
|
||||
const uint32_t FAN_MED = 0x20;
|
||||
const uint32_t FAN_MAX = 0x40;
|
||||
const uint32_t FAN_MIN = 0x00; // AKA F1
|
||||
const uint32_t FAN_F2 = 0x90;
|
||||
const uint32_t FAN_MED = 0x20; // AKA F3
|
||||
const uint32_t FAN_F4 = 0xA0;
|
||||
const uint32_t FAN_MAX = 0x40; // AKA F5
|
||||
|
||||
// Temperature
|
||||
const uint8_t TEMP_RANGE = TEMP_MAX - TEMP_MIN + 1;
|
||||
@@ -37,16 +113,37 @@ const uint32_t TEMP_SHIFT = 8;
|
||||
const uint16_t BITS = 28;
|
||||
|
||||
void LgIrClimate::transmit_state() {
|
||||
uint32_t remote_state = 0x8800000;
|
||||
uint32_t remote_state = LG_HEADER;
|
||||
|
||||
// ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", modeBefore_);
|
||||
// ESP_LOGD(TAG, "climate_lg_ir mode_before_ code: 0x%02X", this->modeBefore_);
|
||||
|
||||
// Set command
|
||||
if (this->send_swing_cmd_) {
|
||||
this->send_swing_cmd_ = false;
|
||||
remote_state |= COMMAND_SWING;
|
||||
} else {
|
||||
bool climate_is_off = (this->mode_before_ == climate::CLIMATE_MODE_OFF);
|
||||
if (this->advanced_commands_support_) {
|
||||
switch (this->swing_mode) {
|
||||
case climate::CLIMATE_SWING_VERTICAL:
|
||||
ESP_LOGD(TAG, "setting swing vertical");
|
||||
remote_state |= CommandAdvSwing::HEADER_ADV_SWING;
|
||||
remote_state |= CommandAdvSwing::VERT_SWING_ON;
|
||||
break;
|
||||
case climate::CLIMATE_SWING_OFF:
|
||||
ESP_LOGD(TAG, "setting swing off");
|
||||
remote_state |= CommandAdvSwing::HEADER_ADV_SWING;
|
||||
remote_state |= CommandAdvSwing::VERT_SWING_OFF;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
this->transmit_(remote_state);
|
||||
this->publish_state();
|
||||
return;
|
||||
} else { // just toggle swing when advanced_commands_support is not set
|
||||
remote_state |= HEADER_BASIC;
|
||||
remote_state |= BASIC_SWING_TOGGLE;
|
||||
}
|
||||
} else { // Mode commands
|
||||
const bool climate_is_off = (this->mode_before_ == climate::CLIMATE_MODE_OFF);
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
remote_state |= climate_is_off ? COMMAND_ON_COOL : COMMAND_COOL;
|
||||
@@ -65,8 +162,8 @@ void LgIrClimate::transmit_state() {
|
||||
break;
|
||||
case climate::CLIMATE_MODE_OFF:
|
||||
default:
|
||||
remote_state |= COMMAND_OFF;
|
||||
break;
|
||||
remote_state |= CommandSys::HEADER_SYS;
|
||||
remote_state |= CommandSys::COMMAND_OFF;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,9 +172,8 @@ void LgIrClimate::transmit_state() {
|
||||
ESP_LOGD(TAG, "climate_lg_ir mode code: 0x%02X", this->mode);
|
||||
|
||||
// Set fan speed
|
||||
if (this->mode == climate::CLIMATE_MODE_OFF) {
|
||||
remote_state |= FAN_AUTO;
|
||||
} else {
|
||||
if (this->mode !=
|
||||
climate::CLIMATE_MODE_OFF) { // https://github.com/esphome/esphome/pull/10875#issuecomment-5042765948
|
||||
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
|
||||
case climate::CLIMATE_FAN_HIGH:
|
||||
remote_state |= FAN_MAX;
|
||||
@@ -95,10 +191,20 @@ void LgIrClimate::transmit_state() {
|
||||
}
|
||||
}
|
||||
|
||||
// Set temperature
|
||||
if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) {
|
||||
auto temp = (uint8_t) roundf(clamp<float>(this->target_temperature, TEMP_MIN, TEMP_MAX));
|
||||
remote_state |= ((temp - 15) << TEMP_SHIFT);
|
||||
uint8_t temp;
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
if (!this->advanced_commands_support_) { // Keep previous behavior
|
||||
break;
|
||||
}
|
||||
[[fallthrough]];
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
temp = static_cast<uint8_t>(roundf(clamp<float>(this->target_temperature, TEMP_MIN, TEMP_MAX)));
|
||||
remote_state |= (temp - 15) << TEMP_SHIFT;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this->transmit_(remote_state);
|
||||
@@ -124,62 +230,134 @@ bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) {
|
||||
}
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Decoded 0x%02" PRIX32, remote_state);
|
||||
if ((remote_state & 0xFF00000) != 0x8800000)
|
||||
ESP_LOGD(TAG, "Received 0x%02" PRIX32, remote_state);
|
||||
if ((remote_state & 0xFF00000) != LG_HEADER)
|
||||
return false;
|
||||
|
||||
// Get command
|
||||
if ((remote_state & COMMAND_MASK) == COMMAND_OFF) {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
} else if ((remote_state & COMMAND_MASK) == COMMAND_SWING) {
|
||||
this->swing_mode =
|
||||
this->swing_mode == climate::CLIMATE_SWING_OFF ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
|
||||
} else {
|
||||
switch (remote_state & COMMAND_MASK) {
|
||||
case COMMAND_DRY:
|
||||
case COMMAND_ON_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
case COMMAND_FAN_ONLY:
|
||||
case COMMAND_ON_FAN_ONLY:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
case COMMAND_AI:
|
||||
case COMMAND_ON_AI:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
case COMMAND_HEAT:
|
||||
case COMMAND_ON_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
case COMMAND_COOL:
|
||||
case COMMAND_ON_COOL:
|
||||
default:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
}
|
||||
|
||||
// Get fan speed
|
||||
if (this->mode == climate::CLIMATE_MODE_HEAT_COOL) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
} else if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_DRY ||
|
||||
this->mode == climate::CLIMATE_MODE_FAN_ONLY || this->mode == climate::CLIMATE_MODE_HEAT) {
|
||||
if ((remote_state & FAN_MASK) == FAN_AUTO) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
} else if ((remote_state & FAN_MASK) == FAN_MIN) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
} else if ((remote_state & FAN_MASK) == FAN_MED) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
} else if ((remote_state & FAN_MASK) == FAN_MAX) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
// Decode commands
|
||||
switch (remote_state & COMMAND_HEADER_MASK) {
|
||||
case CommandSys::HEADER_SYS:
|
||||
ESP_LOGD(TAG, "Got system command! With data: 0x%02" PRIX32, remote_state & COMMAND_DATA_MASK);
|
||||
if ((remote_state & COMMAND_DATA_MASK) == CommandSys::COMMAND_OFF) {
|
||||
this->mode = climate::CLIMATE_MODE_OFF;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case CommandAdvSwing::HEADER_ADV_SWING:
|
||||
ESP_LOGD(TAG, "Got advanced swing command! With data: 0x%02" PRIX32,
|
||||
remote_state & CommandAdvSwing::ADV_SWING_DATA_MASK);
|
||||
switch (remote_state & CommandAdvSwing::ADV_SWING_DATA_MASK) {
|
||||
case CommandAdvSwing::VERT_SWING_ON:
|
||||
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||
break;
|
||||
case CommandAdvSwing::VERT_SWING_OFF:
|
||||
case CommandAdvSwing::VERT_FIX_1:
|
||||
case CommandAdvSwing::VERT_FIX_2:
|
||||
case CommandAdvSwing::VERT_FIX_3:
|
||||
case CommandAdvSwing::VERT_FIX_4:
|
||||
case CommandAdvSwing::VERT_FIX_5:
|
||||
case CommandAdvSwing::VERT_FIX_6:
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
break;
|
||||
default:
|
||||
return false; // Ignore all other (horizontal) swing commands
|
||||
}
|
||||
}
|
||||
|
||||
// Get temperature
|
||||
if (this->mode == climate::CLIMATE_MODE_COOL || this->mode == climate::CLIMATE_MODE_HEAT) {
|
||||
this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15;
|
||||
}
|
||||
this->publish_state();
|
||||
return true;
|
||||
|
||||
case HEADER_BASIC:
|
||||
if ((remote_state & COMMAND_DATA_MASK) == BASIC_JET) {
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
case climate::CLIMATE_MODE_DRY:
|
||||
this->target_temperature =
|
||||
this->mode == climate::CLIMATE_MODE_HEAT ? this->maximum_temperature_ : this->minimum_temperature_;
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
// When enabling PO(WER) also known as JET mode, swing is set to VERT_3, but after 30 mins it will switch
|
||||
// back to what it was before, so let's just not change it here it at all
|
||||
this->publish_state();
|
||||
return true;
|
||||
default:
|
||||
ESP_LOGD(TAG, "Got jet command, but current mode does not support it! Ignoring.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Keep previous behavior in case of other BASIC command
|
||||
if (this->swing_mode == climate::CLIMATE_SWING_OFF) { // Just flip between vertical and off
|
||||
this->swing_mode = climate::CLIMATE_SWING_VERTICAL;
|
||||
} else {
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
}
|
||||
this->publish_state();
|
||||
return true;
|
||||
// Following commands also contain fan speed and temperature, so no 'return' in these cases
|
||||
case COMMAND_DRY:
|
||||
case COMMAND_ON_DRY:
|
||||
this->mode = climate::CLIMATE_MODE_DRY;
|
||||
break;
|
||||
case COMMAND_FAN_ONLY:
|
||||
case COMMAND_ON_FAN_ONLY:
|
||||
this->mode = climate::CLIMATE_MODE_FAN_ONLY;
|
||||
break;
|
||||
case COMMAND_AI:
|
||||
case COMMAND_ON_AI:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT_COOL;
|
||||
break;
|
||||
case COMMAND_HEAT:
|
||||
case COMMAND_ON_HEAT:
|
||||
this->mode = climate::CLIMATE_MODE_HEAT;
|
||||
break;
|
||||
case COMMAND_COOL:
|
||||
case COMMAND_ON_COOL:
|
||||
this->mode = climate::CLIMATE_MODE_COOL;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGD(TAG, "Got unknown command! Ignoring!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decode fan speed
|
||||
switch (remote_state & FAN_SPEED_MASK) {
|
||||
case FAN_AUTO:
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
break;
|
||||
case FAN_MIN:
|
||||
case FAN_F2:
|
||||
this->fan_mode = climate::CLIMATE_FAN_LOW;
|
||||
break;
|
||||
case FAN_MED:
|
||||
case FAN_F4:
|
||||
this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
|
||||
break;
|
||||
case FAN_MAX:
|
||||
this->fan_mode = climate::CLIMATE_FAN_HIGH;
|
||||
break;
|
||||
default:
|
||||
ESP_LOGD(TAG, "Got unknown fan speed! Ignoring!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Keep previous behavior
|
||||
if (this->mode == climate::CLIMATE_MODE_HEAT_COOL && !(this->advanced_commands_support_)) {
|
||||
this->fan_mode = climate::CLIMATE_FAN_AUTO;
|
||||
}
|
||||
|
||||
// Decode temperature for modes that support it
|
||||
switch (this->mode) {
|
||||
case climate::CLIMATE_MODE_HEAT_COOL:
|
||||
case climate::CLIMATE_MODE_COOL:
|
||||
case climate::CLIMATE_MODE_HEAT:
|
||||
this->target_temperature = ((remote_state & TEMP_MASK) >> TEMP_SHIFT) + 15;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
this->mode_before_ = this->mode;
|
||||
this->publish_state();
|
||||
|
||||
return true;
|
||||
@@ -207,14 +385,14 @@ void LgIrClimate::transmit_(uint32_t value) {
|
||||
data->mark(this->bit_high_);
|
||||
transmit.perform();
|
||||
}
|
||||
|
||||
void LgIrClimate::calc_checksum_(uint32_t &value) {
|
||||
uint32_t mask = 0xF;
|
||||
uint32_t sum = 0;
|
||||
for (uint8_t i = 1; i < 8; i++) {
|
||||
sum += (value & (mask << (i * 4))) >> (i * 4);
|
||||
sum += (value & (CHECKSUM_MASK << (i * 4))) >> (i * 4);
|
||||
}
|
||||
|
||||
value |= (sum & mask);
|
||||
value |= (sum & CHECKSUM_MASK);
|
||||
}
|
||||
|
||||
} // namespace esphome::climate_ir_lg
|
||||
|
||||
@@ -21,12 +21,13 @@ class LgIrClimate final : public climate_ir::ClimateIR {
|
||||
/// Override control to change settings of the climate device.
|
||||
void control(const climate::ClimateCall &call) override {
|
||||
this->send_swing_cmd_ = call.get_swing_mode().has_value();
|
||||
// swing resets after unit powered off
|
||||
// swing resets after unit powered off, except when advanced_commands_support_ is set
|
||||
auto mode = call.get_mode();
|
||||
if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF)
|
||||
if (mode.has_value() && *mode == climate::CLIMATE_MODE_OFF && !(this->advanced_commands_support_))
|
||||
this->swing_mode = climate::CLIMATE_SWING_OFF;
|
||||
climate_ir::ClimateIR::control(call);
|
||||
}
|
||||
void set_advanced_commands_support(bool value) { this->advanced_commands_support_ = value; }
|
||||
void set_header_high(uint32_t header_high) { this->header_high_ = header_high; }
|
||||
void set_header_low(uint32_t header_low) { this->header_low_ = header_low; }
|
||||
void set_bit_high(uint32_t bit_high) { this->bit_high_ = bit_high; }
|
||||
@@ -44,6 +45,7 @@ class LgIrClimate final : public climate_ir::ClimateIR {
|
||||
void calc_checksum_(uint32_t &value);
|
||||
void transmit_(uint32_t value);
|
||||
|
||||
bool advanced_commands_support_{false};
|
||||
uint32_t header_high_;
|
||||
uint32_t header_low_;
|
||||
uint32_t bit_high_;
|
||||
|
||||
@@ -14,8 +14,10 @@ CONF_CHANNEL_COLORS = "channel_colors"
|
||||
CONF_CLIMATE_ID = "climate_id"
|
||||
CONF_CO2_EQUIVALENT = "co2_equivalent"
|
||||
CONF_COLOR_DEPTH = "color_depth"
|
||||
CONF_COLUMNS = "columns"
|
||||
CONF_CRC_ENABLE = "crc_enable"
|
||||
CONF_DATA_BITS = "data_bits"
|
||||
CONF_DESCRIPTION = "description"
|
||||
CONF_DRAW_ROUNDING = "draw_rounding"
|
||||
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
|
||||
CONF_ENABLED = "enabled"
|
||||
@@ -24,6 +26,7 @@ CONF_GYROSCOPE_RANGE = "gyroscope_range"
|
||||
CONF_IAQ = "iaq"
|
||||
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
|
||||
CONF_IS_WRGB = "is_wrgb"
|
||||
CONF_KEYS = "keys"
|
||||
CONF_LABEL = "label"
|
||||
CONF_LIBRETINY = "libretiny"
|
||||
CONF_LOOP = "loop"
|
||||
|
||||
@@ -249,7 +249,7 @@ bool CS5460AComponent::check_status_() {
|
||||
bool dir = status & (1 << 21);
|
||||
if (current_gain_ < 0)
|
||||
dir = !dir;
|
||||
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? "negative" : "positive");
|
||||
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? LOG_STR_LITERAL("negative") : LOG_STR_LITERAL("positive"));
|
||||
clear |= 1 << 22;
|
||||
}
|
||||
|
||||
@@ -319,7 +319,9 @@ void CS5460AComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"CS5460A:\n"
|
||||
" Init status: %s",
|
||||
state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other"));
|
||||
state == COMPONENT_STATE_LOOP
|
||||
? LOG_STR_LITERAL("OK")
|
||||
: (state == COMPONENT_STATE_FAILED ? LOG_STR_LITERAL("failed") : LOG_STR_LITERAL("other")));
|
||||
LOG_PIN(" CS Pin: ", cs_);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Samples / cycle: %" PRIu32 "\n"
|
||||
@@ -330,9 +332,10 @@ void CS5460AComponent::dump_config() {
|
||||
" Current HPF: %s\n"
|
||||
" Voltage HPF: %s\n"
|
||||
" Pulse energy: %.2f Wh",
|
||||
samples_, phase_offset_, pga_gain_ == CS5460A_PGA_GAIN_50X ? "50x" : "10x", current_gain_,
|
||||
voltage_gain_, current_hpf_ ? "enabled" : "disabled", voltage_hpf_ ? "enabled" : "disabled",
|
||||
pulse_energy_wh_);
|
||||
samples_, phase_offset_,
|
||||
pga_gain_ == CS5460A_PGA_GAIN_50X ? LOG_STR_LITERAL("50x") : LOG_STR_LITERAL("10x"), current_gain_,
|
||||
voltage_gain_, current_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"),
|
||||
voltage_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"), pulse_energy_wh_);
|
||||
LOG_SENSOR(" ", "Voltage", voltage_sensor_);
|
||||
LOG_SENSOR(" ", "Current", current_sensor_);
|
||||
LOG_SENSOR(" ", "Power", power_sensor_);
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#include "d01.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
// uart specification for d01 sensor from https://manuals.plus/ae/1005006417362019:
|
||||
//
|
||||
// A frame of serial output data includes 4 bytes, formatted as follows:
|
||||
// __Characteristic byte: Fixed value 0xA5.
|
||||
// __Data byte: DATAH is the high 7 bits of the concentration value, and DATAL is the low 7 bits of the concentration
|
||||
// value.
|
||||
// __Check byte: The low 7 bits of the sum of all bytes before the check byte.
|
||||
//
|
||||
// If the serial output is 4 bytes of data: 0*A5 0*01 0*2C 0*52, then DATAH = 0*01 = 1, DATAL = 0*2C = 44.
|
||||
// Concentration value = 1*128 + 44 = 172 µg/m³.
|
||||
//
|
||||
// The PM2.5 dust concentration value obtained from the dust sensor needs to be calibrated with a K value coefficient
|
||||
// based on the TSI instrument's photometric method. It is generally recommended to use 0.4.
|
||||
|
||||
namespace esphome::d01 {
|
||||
|
||||
static const char *const TAG = "d01";
|
||||
|
||||
static const uint8_t D01_FRAME_HEADER = 0xA5;
|
||||
|
||||
void D01SensorComponent::dump_config() { LOG_SENSOR(" ", "D01 PM2.5", this); }
|
||||
|
||||
void D01SensorComponent::loop() {
|
||||
uint8_t buf[4];
|
||||
while (this->available() >= 4) {
|
||||
if (this->peek() != D01_FRAME_HEADER) {
|
||||
this->read();
|
||||
continue;
|
||||
}
|
||||
this->read_array(buf, 4);
|
||||
uint8_t sum = (buf[0] + buf[1] + buf[2]) & 0x7F;
|
||||
if (sum != buf[3]) {
|
||||
ESP_LOGW(TAG, "checksum mismatch");
|
||||
continue;
|
||||
}
|
||||
uint16_t latest_concentration = (buf[1] & 0x7F) * 128 + (buf[2] & 0x7F);
|
||||
ESP_LOGV(TAG, "Unadjusted PM2.5 Concentration: %d µg/m³", latest_concentration);
|
||||
this->publish_state(latest_concentration);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::d01
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
|
||||
namespace esphome::d01 {
|
||||
|
||||
class D01SensorComponent final : public sensor::Sensor, public Component, public uart::UARTDevice {
|
||||
public:
|
||||
void dump_config() override;
|
||||
void loop() override;
|
||||
};
|
||||
|
||||
} // namespace esphome::d01
|
||||
@@ -0,0 +1,45 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_PM25,
|
||||
ICON_BLUR,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@ch604"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
d01_ns = cg.esphome_ns.namespace("d01")
|
||||
D01SensorComponent = d01_ns.class_(
|
||||
"D01SensorComponent", sensor.Sensor, uart.UARTDevice, cg.Component
|
||||
)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
D01SensorComponent,
|
||||
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
|
||||
icon=ICON_BLUR,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_PM25,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"d01",
|
||||
baud_rate=9600,
|
||||
require_rx=True,
|
||||
require_tx=False,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -20,8 +20,8 @@ void DHT::dump_config() {
|
||||
"DHT:\n"
|
||||
" %sModel: %s\n"
|
||||
" Internal pull-up: %s",
|
||||
this->is_auto_detect_ ? "Auto-detected " : "",
|
||||
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent",
|
||||
this->is_auto_detect_ ? LOG_STR_LITERAL("Auto-detected ") : "",
|
||||
this->model_ == DHT_MODEL_DHT11 ? LOG_STR_LITERAL("DHT11") : LOG_STR_LITERAL("DHT22 or equivalent"),
|
||||
ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
|
||||
LOG_PIN(" Pin: ", this->t_pin_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
@@ -154,8 +154,9 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
|
||||
}
|
||||
}
|
||||
if (error_code != 0) {
|
||||
if (report_errors)
|
||||
if (report_errors) {
|
||||
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
#include "ds1603l.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ds1603l {
|
||||
|
||||
static const char *const TAG = "ds1603l.sensor";
|
||||
|
||||
void DS1603L::loop() {
|
||||
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
|
||||
uint8_t byte;
|
||||
while (this->available() > 0 && this->read_byte(&byte)) {
|
||||
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
|
||||
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
|
||||
continue;
|
||||
}
|
||||
|
||||
this->rx_buffer_[this->rx_count_++] = byte;
|
||||
if (this->rx_count_ < FRAME_SIZE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this->parse_data_()) {
|
||||
this->rx_count_ = 0;
|
||||
} else {
|
||||
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
|
||||
this->resync_();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
|
||||
|
||||
bool DS1603L::parse_data_() {
|
||||
uint8_t header = this->rx_buffer_[0];
|
||||
uint8_t data_h = this->rx_buffer_[1];
|
||||
uint8_t data_l = this->rx_buffer_[2];
|
||||
uint8_t checksum = this->rx_buffer_[3];
|
||||
|
||||
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
|
||||
|
||||
ESP_LOGV(TAG, "Data: Header=0x%02X, Data_H=0x%02X, Data_L=0x%02X, Checksum=0x%02X", header, data_h, data_l, checksum);
|
||||
|
||||
if (checksum != computed_checksum) {
|
||||
ESP_LOGW(TAG, "Checksum mismatch: received 0x%02X, expected 0x%02X", checksum, computed_checksum);
|
||||
return false;
|
||||
}
|
||||
|
||||
this->publish_state(encode_uint16(data_h, data_l));
|
||||
return true;
|
||||
}
|
||||
|
||||
void DS1603L::resync_() {
|
||||
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
|
||||
size_t start = 1;
|
||||
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
|
||||
start++;
|
||||
}
|
||||
this->rx_count_ -= start;
|
||||
if (this->rx_count_ > 0) {
|
||||
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::ds1603l
|
||||
@@ -0,0 +1,30 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "esphome/components/sensor/sensor.h"
|
||||
#include "esphome/components/uart/uart.h"
|
||||
#include "esphome/core/component.h"
|
||||
|
||||
namespace esphome::ds1603l {
|
||||
|
||||
class DS1603L final : public sensor::Sensor, public Component, public uart::UARTDevice {
|
||||
public:
|
||||
void loop() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
static constexpr uint8_t HEADER_BYTE = 0xFF;
|
||||
static constexpr size_t FRAME_SIZE = 4;
|
||||
|
||||
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
|
||||
bool parse_data_();
|
||||
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
|
||||
void resync_();
|
||||
|
||||
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
|
||||
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
|
||||
};
|
||||
|
||||
} // namespace esphome::ds1603l
|
||||
@@ -0,0 +1,43 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor, uart
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
DEVICE_CLASS_DISTANCE,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
UNIT_MILLIMETER,
|
||||
)
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@JakeLC15"]
|
||||
DEPENDENCIES = ["uart"]
|
||||
|
||||
ds1603l_ns = cg.esphome_ns.namespace("ds1603l")
|
||||
DS1603L = ds1603l_ns.class_("DS1603L", sensor.Sensor, cg.Component, uart.UARTDevice)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
sensor.sensor_schema(
|
||||
DS1603L,
|
||||
unit_of_measurement=UNIT_MILLIMETER,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_DISTANCE,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
.extend(uart.UART_DEVICE_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
|
||||
"ds1603l",
|
||||
baud_rate=9600,
|
||||
require_tx=False,
|
||||
require_rx=True,
|
||||
data_bits=8,
|
||||
stop_bits=1,
|
||||
)
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
@@ -93,7 +93,7 @@ void Emc2101Component::dump_config() {
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
|
||||
}
|
||||
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? "DAC" : "PWM");
|
||||
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? LOG_STR_LITERAL("DAC") : LOG_STR_LITERAL("PWM"));
|
||||
if (this->dac_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " DAC Conversion Rate: %X", this->dac_conversion_rate_);
|
||||
} else {
|
||||
|
||||
@@ -116,14 +116,16 @@ _CALLBACK_AUTOMATIONS = (
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
|
||||
# Initialize sensor storage with count from final_validate
|
||||
# Initialize sensor storage with count from final_validate before any
|
||||
# await, so platform to_code() calls always see it initialized
|
||||
# regardless of YAML key order.
|
||||
sensor_count = _get_data().sensor_counts.get(str(config[CONF_ID]), 0)
|
||||
if sensor_count > 0:
|
||||
cg.add(var.init_sensors(sensor_count))
|
||||
|
||||
await cg.register_component(var, config)
|
||||
await uart.register_uart_device(var, config)
|
||||
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
|
||||
|
||||
|
||||
|
||||
@@ -216,7 +216,7 @@ void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status)
|
||||
// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems.
|
||||
bool ENS210Component::set_low_power_(bool enable) {
|
||||
uint8_t low_power_cmd = enable ? 0x01 : 0x00;
|
||||
ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false");
|
||||
ESP_LOGD(TAG, "Enable low power: %s", enable ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
|
||||
bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd);
|
||||
delay(ENS210_BOOTING_MS);
|
||||
return result;
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
#include "epaper_spi_uc8179.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
static constexpr const char *const TAG = "epaper_spi.uc8179";
|
||||
|
||||
bool EPaperUC8179::initialise(bool partial) {
|
||||
EPaperBase::initialise(partial); // send the model init sequence
|
||||
this->partial_ = partial;
|
||||
ESP_LOGV(TAG, "Power on");
|
||||
// POWER ON must precede the waveform/mode registers and the data transfer
|
||||
// (the original driver powers on and busy-waits before writing them).
|
||||
// The state machine busy-waits before entering TRANSFER_DATA.
|
||||
this->command(0x04);
|
||||
// Give the busy line time to assert before the state machine polls it
|
||||
this->next_delay_ = 100;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Set up the refresh mode. Must be called after power-on has completed.
|
||||
void EPaperUC8179::set_refresh_mode_() {
|
||||
if (!this->is_using_partial_update_()) {
|
||||
return; // plain full refresh uses the mode set by the init sequence
|
||||
}
|
||||
// Fast and partial refresh use flipped data polarity and a floating border
|
||||
this->cmd_data(0x50, {0xA9, 0x07});
|
||||
// Force the waveform via the temperature registers: 0x5A selects the fast
|
||||
// full-refresh waveform, 0x6E the partial-refresh waveform
|
||||
this->cmd_data(0xE0, {0x02});
|
||||
if (this->partial_) {
|
||||
this->cmd_data(0xE5, {0x6E});
|
||||
this->command(0x91); // enter partial mode
|
||||
// Set the partial window to the full screen
|
||||
const uint16_t x_end = this->width_ - 1;
|
||||
const uint16_t y_end = this->height_ - 1;
|
||||
this->cmd_data(0x90, {0x00, 0x00, static_cast<uint8_t>(x_end >> 8), static_cast<uint8_t>(x_end & 0xFF), 0x00, 0x00,
|
||||
static_cast<uint8_t>(y_end >> 8), static_cast<uint8_t>(y_end & 0xFF), 0x01});
|
||||
} else {
|
||||
this->cmd_data(0xE5, {0x5A});
|
||||
this->command(0x92); // exit partial mode
|
||||
}
|
||||
}
|
||||
|
||||
bool HOT EPaperUC8179::transfer_data() {
|
||||
const uint32_t start_time = millis();
|
||||
const size_t buffer_length = this->buffer_length_;
|
||||
if (this->current_data_index_ == 0) {
|
||||
this->set_refresh_mode_();
|
||||
}
|
||||
// Fast full refresh sends the previous-image plane as well, so that every pixel transitions
|
||||
const bool two_pass = this->is_using_partial_update_() && !this->partial_;
|
||||
// Plain full refresh sends inverted data (buffer is 1=white, the wire wants 0=white);
|
||||
// in fast/partial mode the data polarity is flipped via the VCOM/data-interval
|
||||
// register instead, so the new-image plane is sent unmodified
|
||||
const bool invert_new_data = !this->is_using_partial_update_();
|
||||
|
||||
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
|
||||
|
||||
// Phase 1 (fast full refresh only): previous image via 0x10 (DTM1), inverse of the new image
|
||||
if (two_pass && this->current_data_index_ < buffer_length) {
|
||||
if (this->current_data_index_ == 0) {
|
||||
this->command(0x10); // DATA START TRANSMISSION 1 (previous image)
|
||||
}
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ < buffer_length) {
|
||||
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, buffer_length - this->current_data_index_);
|
||||
for (size_t i = 0; i < bytes_to_copy; i++) {
|
||||
bytes_to_send[i] = ~this->buffer_[this->current_data_index_ + i];
|
||||
}
|
||||
this->write_array(bytes_to_send, bytes_to_copy);
|
||||
this->current_data_index_ += bytes_to_copy;
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
// Phase 2: new image via 0x13 (DTM2)
|
||||
const size_t offset = two_pass ? buffer_length : 0;
|
||||
const size_t total = offset + buffer_length;
|
||||
if (this->current_data_index_ < total) {
|
||||
if (this->current_data_index_ == offset) {
|
||||
this->command(0x13); // DATA START TRANSMISSION 2 (new image)
|
||||
}
|
||||
this->start_data_();
|
||||
while (this->current_data_index_ < total) {
|
||||
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, total - this->current_data_index_);
|
||||
const size_t data_idx = this->current_data_index_ - offset;
|
||||
for (size_t i = 0; i < bytes_to_copy; i++) {
|
||||
const uint8_t byte = this->buffer_[data_idx + i];
|
||||
bytes_to_send[i] = invert_new_data ? ~byte : byte;
|
||||
}
|
||||
this->write_array(bytes_to_send, bytes_to_copy);
|
||||
this->current_data_index_ += bytes_to_copy;
|
||||
if (millis() - start_time > MAX_TRANSFER_TIME) {
|
||||
this->disable();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this->disable();
|
||||
}
|
||||
|
||||
this->current_data_index_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
void EPaperUC8179::power_on() {
|
||||
// Power-on is sent at the end of initialise() instead, because the
|
||||
// waveform/mode registers and the data transfer must follow it
|
||||
}
|
||||
|
||||
void EPaperUC8179::refresh_screen(bool /*partial*/) {
|
||||
ESP_LOGV(TAG, "Refresh");
|
||||
this->command(0x12); // DISPLAY REFRESH
|
||||
// Delay the next busy poll: the busy line takes a short time to assert after
|
||||
// the refresh command, and polling too early would read it as already idle
|
||||
this->next_delay_ = 100;
|
||||
}
|
||||
|
||||
void EPaperUC8179::power_off() {
|
||||
ESP_LOGV(TAG, "Power off");
|
||||
this->command(0x02); // POWER OFF
|
||||
}
|
||||
|
||||
void EPaperUC8179::deep_sleep() {
|
||||
// Deep sleep loses the previous-image RAM that partial refresh compares against
|
||||
if (!this->is_using_partial_update_()) {
|
||||
ESP_LOGV(TAG, "Deep sleep");
|
||||
this->cmd_data(0x07, {0xA5}); // DEEP SLEEP with check code
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,52 @@
|
||||
#pragma once
|
||||
|
||||
#include "epaper_spi.h"
|
||||
|
||||
namespace esphome::epaper_spi {
|
||||
|
||||
/**
|
||||
* Monochrome e-paper displays using the UC8179 controller.
|
||||
* Supports: 7.5" V2 (EPD_7in5_V2), 800x480 pixels, as used by the
|
||||
* Waveshare 7.5" V2 HAT and the Seeed reTerminal E1001.
|
||||
*
|
||||
* Buffer layout: 1 bit per pixel, 1=white, 0=black (the base class default).
|
||||
*
|
||||
* The INITIALISE state sends the panel configuration followed by power-on
|
||||
* (0x04); the state machine busy-waits for power-on to complete before
|
||||
* TRANSFER_DATA, which first writes the waveform/mode registers (these are
|
||||
* only accepted while powered) and then the image data. The state machine
|
||||
* busy-waits again before triggering REFRESH_SCREEN (0x12).
|
||||
*
|
||||
* Three refresh modes are used, following the Waveshare EPD_7in5_V2 examples:
|
||||
* - full_update_every == 1: plain full refresh. The new image is sent
|
||||
* inverted to DTM2 (0x13) and the controller uses its normal waveform.
|
||||
* - full_update_every > 1, full update: fast full refresh. The data polarity
|
||||
* is flipped via the VCOM/data-interval register, a fast waveform is forced
|
||||
* via the temperature registers, and the image is sent to both DTM1 (0x10,
|
||||
* inverted) and DTM2 (0x13) so that every pixel transitions.
|
||||
* - full_update_every > 1, partial update: partial refresh. A partial-update
|
||||
* waveform is forced, partial mode is entered with a full-screen window and
|
||||
* only DTM2 is sent; the controller compares against its previous-image RAM.
|
||||
*/
|
||||
class EPaperUC8179 final : public EPaperBase {
|
||||
public:
|
||||
EPaperUC8179(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
|
||||
size_t init_sequence_length)
|
||||
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
|
||||
this->buffer_length_ = this->row_width_ * height;
|
||||
}
|
||||
|
||||
protected:
|
||||
bool initialise(bool partial) override;
|
||||
bool transfer_data() override;
|
||||
void refresh_screen(bool partial) override;
|
||||
void power_on() override;
|
||||
void power_off() override;
|
||||
void deep_sleep() override;
|
||||
void set_refresh_mode_();
|
||||
|
||||
// Set by initialise() so transfer_data() knows which planes to send
|
||||
bool partial_{};
|
||||
};
|
||||
|
||||
} // namespace esphome::epaper_spi
|
||||
@@ -0,0 +1,93 @@
|
||||
"""Monochrome e-paper displays using the UC8179 controller.
|
||||
|
||||
Supported models:
|
||||
- waveshare-7.5in-v2: 7.5" mono display, 800x480 pixels (EPD_7in5_V2)
|
||||
- seeed-reterminal-e1001: Seeed reTerminal E1001, which uses the same
|
||||
7.5" 800x480 panel on an integrated ESP32-S3 board
|
||||
|
||||
Panel configuration and power-on (0x04) are both sent during the INITIALISE
|
||||
state; the state machine's built-in busy wait then covers the power-on delay
|
||||
before the waveform/mode registers and image data are transferred.
|
||||
|
||||
These displays support fast full and partial refresh: set ``full_update_every``
|
||||
greater than 1 to enable it. Every ``full_update_every``-th update is a fast
|
||||
full refresh, with partial refreshes in between.
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
|
||||
from esphome.const import CONF_DATA_RATE
|
||||
|
||||
from . import EpaperModel
|
||||
|
||||
|
||||
class UC8179(EpaperModel):
|
||||
"""EpaperModel class for monochrome displays using the UC8179 controller."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
class_name: str = "EPaperUC8179",
|
||||
data_rate: str = "10MHz",
|
||||
**defaults: Any,
|
||||
) -> None:
|
||||
defaults.setdefault(CONF_DATA_RATE, data_rate)
|
||||
super().__init__(name, class_name, **defaults)
|
||||
|
||||
def get_init_sequence(self, config: dict) -> tuple:
|
||||
"""Generate the initialization sequence for UC8179 mono displays.
|
||||
|
||||
Panel configuration only — the driver appends power-on (0x04) at the
|
||||
end of the INITIALISE state, and the state machine busy-waits for it
|
||||
to complete before the data transfer starts.
|
||||
"""
|
||||
width, height = self.get_dimensions(config)
|
||||
return (
|
||||
# POWER SETTING
|
||||
(0x01, 0x07, 0x07, 0x3F, 0x3F),
|
||||
# BOOSTER SOFT START
|
||||
(0x06, 0x17, 0x17, 0x28, 0x17),
|
||||
# PANEL SETTING (black/white mode, LUT from OTP)
|
||||
(0x00, 0x1F),
|
||||
# RESOLUTION SETTING (width x height)
|
||||
(
|
||||
0x61,
|
||||
(width >> 8) & 0xFF,
|
||||
width & 0xFF,
|
||||
(height >> 8) & 0xFF,
|
||||
height & 0xFF,
|
||||
),
|
||||
# DUAL SPI MODE (disabled)
|
||||
(0x15, 0x00),
|
||||
# VCOM AND DATA INTERVAL SETTING
|
||||
(0x50, 0x10, 0x07),
|
||||
# TCON SETTING
|
||||
(0x60, 0x22),
|
||||
)
|
||||
|
||||
|
||||
uc8179 = UC8179("uc8179")
|
||||
|
||||
# Waveshare 7.5" V2 mono (EPD_7in5_V2) — 800x480, UC8179 controller
|
||||
waveshare_7_5_v2 = uc8179.extend(
|
||||
"waveshare-7.5in-v2",
|
||||
width=800,
|
||||
height=480,
|
||||
)
|
||||
|
||||
# Seeed reTerminal E1001 — 7.5" mono e-paper (800x480), same panel as the
|
||||
# Waveshare 7.5" V2, driven by an integrated ESP32-S3 board
|
||||
waveshare_7_5_v2.extend(
|
||||
"seeed-reterminal-e1001",
|
||||
cs_pin=10,
|
||||
dc_pin=11,
|
||||
reset_pin=12,
|
||||
busy_pin={
|
||||
"number": 13,
|
||||
"inverted": True,
|
||||
"mode": {
|
||||
"input": True,
|
||||
"pullup": True,
|
||||
},
|
||||
},
|
||||
)
|
||||
@@ -182,6 +182,13 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = {
|
||||
VARIANT_ESP32,
|
||||
}
|
||||
|
||||
# Variants that support execution from PSRAM
|
||||
PSRAM_XIP_VARIANTS = {
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S31,
|
||||
}
|
||||
|
||||
# NVS encryption (HMAC peripheral scheme) is only available on variants that
|
||||
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
|
||||
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
|
||||
@@ -214,11 +221,13 @@ COMPILER_OPTIMIZATIONS = {
|
||||
# builds that need them.
|
||||
DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"app_trace", # CPU trace/SystemView support - unused by ESPHome
|
||||
"bt", # Bluetooth stack - re-included by request_bluetooth(); its REQUIRES pulls the WiFi stack back
|
||||
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
|
||||
"console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured
|
||||
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
|
||||
"esp-tls", # TLS wrapper - re-included by http_request, mqtt, web_server_idf
|
||||
"esp_adc", # ADC driver - only needed by adc component
|
||||
"esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back
|
||||
"esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back
|
||||
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
|
||||
"esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, opentherm, Arduino BLE libs
|
||||
@@ -236,6 +245,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"esp_driver_twai", # TWAI/CAN driver - only needed by esp32_can component
|
||||
"esp_eth", # Ethernet driver - only needed by ethernet component
|
||||
"esp_gdbstub", # GDB stub panic handler - unused by ESPHome; bt pulls it back
|
||||
"esp_hal_ieee802154", # 802.15.4 HAL - ieee802154 pulls it back
|
||||
"esp_hid", # HID host/device support - ESPHome doesn't implement HID functionality
|
||||
"esp_http_client", # HTTP client - only needed by http_request component
|
||||
"esp_http_server", # HTTP server - re-included by web_server_idf, esp32_camera_web_server
|
||||
@@ -243,8 +253,11 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"esp_https_server", # HTTPS server - ESPHome has its own web server
|
||||
"esp_lcd", # LCD controller drivers - only needed by display component
|
||||
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
|
||||
"esp_phy", # RF PHY - re-included by internal_temperature on the original ESP32; esp_wifi/bt/ieee802154 pull it back
|
||||
"esp_wifi", # WiFi stack - re-included by request_wifi(), espnow; bt pulls it back for BLE builds
|
||||
"espcoredump", # Core dump support - ESPHome has its own debug component
|
||||
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
|
||||
"ieee802154", # 802.15.4 radio - IDF openthread and the Zigbee libs pull it back
|
||||
"json", # cJSON library - ESPHome uses ArduinoJson instead
|
||||
"mqtt", # ESP-IDF MQTT library - ESPHome has its own MQTT implementation
|
||||
"nvs_sec_provider", # NVS encryption key provider - re-included when CONFIG_NVS_ENCRYPTION is set
|
||||
@@ -260,6 +273,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
|
||||
"unity", # Unit testing framework - ESPHome doesn't use IDF's testing
|
||||
"wear_levelling", # Flash wear levelling for fatfs - unused since fatfs unused
|
||||
"wifi_provisioning", # WiFi provisioning - ESPHome uses its own improv implementation
|
||||
"wpa_supplicant", # WPA supplicant - re-included by request_wifi() for esp_eap_client.h
|
||||
)
|
||||
|
||||
# Additional IDF managed components to exclude for Arduino framework builds
|
||||
@@ -709,6 +723,9 @@ def request_wifi(ap: bool = False) -> None:
|
||||
net.wifi = True
|
||||
if ap:
|
||||
net.wifi_ap = True
|
||||
include_builtin_idf_component("esp_wifi")
|
||||
# wifi_component.cpp includes esp_eap_client.h/esp_wpa2.h
|
||||
include_builtin_idf_component("wpa_supplicant")
|
||||
|
||||
|
||||
def request_ethernet() -> None:
|
||||
@@ -720,11 +737,14 @@ def request_bluetooth() -> None:
|
||||
"""Request the Bluetooth controller."""
|
||||
net = _network_sdkconfig()
|
||||
net.bluetooth = True
|
||||
include_builtin_idf_component("bt")
|
||||
|
||||
|
||||
def request_software_coexistence() -> None:
|
||||
"""Request WiFi/BT software coexistence (only valid alongside WiFi)."""
|
||||
_network_sdkconfig().software_coexistence = True
|
||||
# Callers include esp_coexist.h directly.
|
||||
include_builtin_idf_component("esp_coex")
|
||||
|
||||
|
||||
def add_idf_component(
|
||||
@@ -1105,19 +1125,11 @@ def _check_esp_idf_versions(config: ConfigType) -> ConfigType:
|
||||
return config
|
||||
|
||||
|
||||
def _validate_toolchain(value) -> Toolchain:
|
||||
return Toolchain(
|
||||
cv.one_of(Toolchain.PLATFORMIO, Toolchain.ESP_IDF, lower=True)(value)
|
||||
)
|
||||
|
||||
|
||||
def _resolve_toolchain(value: ConfigType) -> ConfigType:
|
||||
# Resolve toolchain: CLI (already on CORE.toolchain) > YAML > default.
|
||||
# Runs before _detect_variant so downstream validators can rely on
|
||||
# CORE.toolchain instead of re-resolving it from the config dict.
|
||||
if CORE.toolchain is None:
|
||||
CORE.toolchain = value.get(CONF_TOOLCHAIN, Toolchain.ESP_IDF)
|
||||
return value
|
||||
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.ESP_IDF)
|
||||
_validate_toolchain = cv.toolchain_enum(_TOOLCHAINS)
|
||||
# Runs before _detect_variant so downstream validators can rely on
|
||||
# CORE.toolchain instead of re-resolving it from the config dict.
|
||||
_resolve_toolchain = cv.resolve_toolchain("ESP32", _TOOLCHAINS, Toolchain.ESP_IDF)
|
||||
|
||||
|
||||
def _check_versions(config: ConfigType) -> ConfigType:
|
||||
@@ -1518,7 +1530,7 @@ def final_validate(config) -> None:
|
||||
)
|
||||
)
|
||||
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
||||
if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}:
|
||||
if config[CONF_VARIANT] not in PSRAM_XIP_VARIANTS:
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant",
|
||||
@@ -2312,6 +2324,8 @@ async def _reconcile_network_sdkconfig() -> None:
|
||||
|
||||
# WiFi stack: disable only when Ethernet is present and WiFi is not. WiFi
|
||||
# relies on the IDF default (enabled), so it is never written True here.
|
||||
# esp_wifi is excluded by default on IDF, so this only matters for Arduino
|
||||
# or when bt pulls it back.
|
||||
wifi_disabled = net.ethernet and not net.wifi
|
||||
if wifi_disabled:
|
||||
set_idf_sdkconfig_default("CONFIG_ESP_WIFI_ENABLED", False)
|
||||
@@ -2720,13 +2734,7 @@ async def to_code(config):
|
||||
_configure_lwip_max_sockets(conf)
|
||||
|
||||
if advanced[CONF_EXECUTE_FROM_PSRAM]:
|
||||
if variant == VARIANT_ESP32S3:
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
|
||||
elif variant == VARIANT_ESP32P4:
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
|
||||
else:
|
||||
raise ValueError("Unhandled ESP32 variant")
|
||||
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
|
||||
|
||||
# Apply LWIP core locking for better socket performance
|
||||
# This is already enabled by default in Arduino framework, where it provides
|
||||
@@ -3242,7 +3250,13 @@ def _write_sdkconfig():
|
||||
if write_file_if_changed(internal_path, contents):
|
||||
# internal changed, update real one
|
||||
write_file_if_changed(sdk_path, contents)
|
||||
clean_build(clear_pio_cache=False)
|
||||
if not CORE.using_toolchain_esp_idf:
|
||||
# PIO's dependency tracking under-declares sdkconfig inputs
|
||||
# (ldgen, linker scripts); without a clean the image can be
|
||||
# unbootable (esphome#15336). The esp-idf toolchain tracks
|
||||
# sdkconfig via IDF's cmake and has_outdated_files(), so a
|
||||
# reconfigure suffices there; everything else fails safe.
|
||||
clean_build(clear_pio_cache=False)
|
||||
|
||||
|
||||
def _write_idf_component_yml():
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "preferences.h"
|
||||
#include <freertos/FreeRTOS.h>
|
||||
#include <freertos/task.h>
|
||||
@@ -29,6 +30,13 @@ void loop_task(void *pv_params) {
|
||||
}
|
||||
|
||||
extern "C" void app_main() {
|
||||
// Apply the custom eFuse MAC (if burned and valid) as the base MAC before any
|
||||
// interface (Wi-Fi, Ethernet, Bluetooth, 802.15.4) derives its address from it.
|
||||
// The logger does not exist yet, so only log-free helpers may be used here.
|
||||
uint8_t mac[MAC_ADDRESS_SIZE];
|
||||
if (get_custom_mac_address(mac)) {
|
||||
set_mac_address(mac);
|
||||
}
|
||||
initArduino();
|
||||
esp32::setup_preferences();
|
||||
#if CONFIG_FREERTOS_UNICORE
|
||||
|
||||
@@ -5,11 +5,14 @@ import esphome.config_validation as cv
|
||||
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
|
||||
from esphome.pins import check_strapping_pin
|
||||
|
||||
# Per the ESP32-S31 datasheet (page 96):
|
||||
# Per the ESP32-S31 IDF DOCS and datasheet:
|
||||
# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html
|
||||
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
|
||||
_ESP32S31_SPI_FLASH_PINS: set[int] = {27, 28, 29, 31, 32, 33}
|
||||
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source.
|
||||
_ESP32S31_STRAPPING_PINS: set[int] = {37, 60, 61}
|
||||
_ESP32S31_SPI_FLASH_PINS: set[int] = {26, 27, 28, 30, 31, 32}
|
||||
_ESP32S31_INVALID_PINS: set[int] = {29, 41}
|
||||
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source;
|
||||
# GPIO36 sets the VDD_SPI voltage.
|
||||
_ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61}
|
||||
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
|
||||
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
|
||||
|
||||
@@ -19,6 +22,8 @@ _LOGGER = logging.getLogger(__name__)
|
||||
def esp32_s31_validate_gpio_pin(value: int) -> int:
|
||||
if value < 0 or value > 61:
|
||||
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)")
|
||||
if value in _ESP32S31_INVALID_PINS:
|
||||
raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.")
|
||||
if value in _ESP32S31_SPI_FLASH_PINS:
|
||||
raise cv.Invalid(
|
||||
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
|
||||
@@ -33,6 +38,10 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
if num < 0 or num > 61:
|
||||
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)")
|
||||
# Checked here as well so ignore_pin_validation_error cannot bypass it;
|
||||
# these pins are not bonded and can never work
|
||||
if num in _ESP32S31_INVALID_PINS:
|
||||
raise cv.Invalid(f"GPIO{num} does not exist on ESP32-S31.")
|
||||
if is_input:
|
||||
# All ESP32 pins support input mode
|
||||
pass
|
||||
|
||||
@@ -71,23 +71,32 @@ static bool read_valid_mac(uint8_t *mac, esp_err_t err) { return err == ESP_OK &
|
||||
|
||||
static constexpr size_t MAC_ADDRESS_SIZE_BITS = MAC_ADDRESS_SIZE * 8; // 48 bits
|
||||
|
||||
// Must not use the ESPHome logger (may run before it exists, e.g. from app_main()).
|
||||
bool get_custom_mac_address(uint8_t *mac) {
|
||||
// has_custom_mac_address() checks the raw eFuse field, while the reads below select their
|
||||
// method differently and may still fail (CRC), so the result must be validated again.
|
||||
if (!has_custom_mac_address())
|
||||
return false;
|
||||
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
|
||||
return read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS));
|
||||
#else
|
||||
return read_valid_mac(mac, esp_efuse_mac_get_custom(mac));
|
||||
#endif
|
||||
}
|
||||
|
||||
void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parameter)
|
||||
if (get_custom_mac_address(mac)) {
|
||||
return;
|
||||
}
|
||||
#if defined(CONFIG_SOC_IEEE802154_SUPPORTED)
|
||||
// When CONFIG_SOC_IEEE802154_SUPPORTED is defined, esp_efuse_mac_get_default
|
||||
// returns the 802.15.4 EUI-64 address, so we read directly from eFuse instead.
|
||||
// Both paths already read raw eFuse bytes, so there is no CRC-bypass fallback
|
||||
// This already reads raw eFuse bytes, so there is no CRC-bypass fallback
|
||||
// (unlike the non-IEEE802154 path where esp_efuse_mac_get_default does CRC checks).
|
||||
if (has_custom_mac_address() &&
|
||||
read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_CUSTOM, mac, MAC_ADDRESS_SIZE_BITS))) {
|
||||
return;
|
||||
}
|
||||
if (read_valid_mac(mac, esp_efuse_read_field_blob(ESP_EFUSE_MAC_FACTORY, mac, MAC_ADDRESS_SIZE_BITS))) {
|
||||
return;
|
||||
}
|
||||
#else
|
||||
if (has_custom_mac_address() && read_valid_mac(mac, esp_efuse_mac_get_custom(mac))) {
|
||||
return;
|
||||
}
|
||||
if (read_valid_mac(mac, esp_efuse_mac_get_default(mac))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,8 +210,9 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
|
||||
if (!image) {
|
||||
// A shutdown is not a lost frame: wait_for_image_() returns empty as soon
|
||||
// as running_ clears, and the loop condition below ends the stream anyway.
|
||||
if (this->running_)
|
||||
if (this->running_) {
|
||||
ESP_LOGW(TAG, "STREAM: failed to acquire frame");
|
||||
}
|
||||
res = ESP_FAIL;
|
||||
}
|
||||
if (res == ESP_OK) {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "esp32_improv_component.h"
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "esphome/components/bytebuffer/bytebuffer.h"
|
||||
#include "esphome/components/esp32_ble/ble.h"
|
||||
#include "esphome/components/esp32_ble_server/ble_2902.h"
|
||||
@@ -19,7 +21,13 @@ using namespace bytebuffer;
|
||||
|
||||
static const char *const TAG = "esp32_improv.component";
|
||||
static constexpr size_t IMPROV_MAX_LOG_BYTES = 128;
|
||||
static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
|
||||
static constexpr char ESPHOME_MY_LINK[] = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
|
||||
// command + data length + trailing byte
|
||||
static constexpr size_t RPC_RESPONSE_OVERHEAD = 3;
|
||||
// Reserves the ESPHOME_MY_LINK entry; a maximal next URL displaces only the
|
||||
// lower value web server URL
|
||||
static constexpr size_t MAX_NEXT_URL_LEN =
|
||||
improv::RPC_RESPONSE_MAX_SIZE - RPC_RESPONSE_OVERHEAD - 1 - sizeof(ESPHOME_MY_LINK);
|
||||
static constexpr uint16_t STOP_ADVERTISING_DELAY =
|
||||
10000; // Delay (ms) before stopping service to allow BLE clients to read the final state
|
||||
static constexpr uint16_t NAME_ADVERTISING_INTERVAL = 60000; // Advertise name every 60 seconds
|
||||
@@ -285,8 +293,9 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
|
||||
}
|
||||
}
|
||||
|
||||
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) {
|
||||
this->rpc_response_->set_value(std::move(response));
|
||||
void ESP32ImprovComponent::send_response_(std::span<const uint8_t> response) {
|
||||
// The BLE characteristic owns its value, so one exact-size copy is required here
|
||||
this->rpc_response_->set_value(std::vector<uint8_t>(response.begin(), response.end()));
|
||||
if (this->state_ != improv::STATE_STOPPED)
|
||||
this->rpc_response_->notify();
|
||||
}
|
||||
@@ -430,40 +439,35 @@ void ESP32ImprovComponent::check_wifi_connection_() {
|
||||
this->connecting_sta_ = {};
|
||||
this->cancel_timeout("wifi-connect-timeout");
|
||||
|
||||
// Build URL list with minimal allocations
|
||||
// Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL
|
||||
std::string url_strings[3];
|
||||
size_t url_count = 0;
|
||||
// Build the URL list directly into a stack buffer with no heap allocation
|
||||
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
|
||||
improv::RpcResponseBuilder builder(buf, improv::WIFI_SETTINGS);
|
||||
|
||||
#ifdef USE_ESP32_IMPROV_NEXT_URL
|
||||
// Add next_url if configured (should be first per Improv BLE spec)
|
||||
{
|
||||
char url_buffer[384];
|
||||
size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
|
||||
if (len > 0) {
|
||||
url_strings[url_count++] = std::string(url_buffer, len);
|
||||
}
|
||||
}
|
||||
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
|
||||
#endif
|
||||
|
||||
// Add default URLs for backward compatibility
|
||||
url_strings[url_count++] = ESPHOME_MY_LINK;
|
||||
// Add default URLs for backward compatibility; MAX_NEXT_URL_LEN reserves this
|
||||
// entry's space, so it always fits
|
||||
builder.add_string(ESPHOME_MY_LINK, sizeof(ESPHOME_MY_LINK) - 1);
|
||||
#ifdef USE_WEBSERVER
|
||||
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
|
||||
if (ip.is_ip4()) {
|
||||
// "http://" (7) + IPv4 max (15) + ":" (1) + port max (5) + null = 29
|
||||
char url_buffer[32];
|
||||
memcpy(url_buffer, "http://", 7); // NOLINT(bugprone-not-null-terminated-result) - str_to null-terminates
|
||||
ip.str_to(url_buffer + 7);
|
||||
size_t len = strlen(url_buffer);
|
||||
snprintf(url_buffer + len, sizeof(url_buffer) - len, ":%d", USE_WEBSERVER_PORT);
|
||||
url_strings[url_count++] = url_buffer;
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ip.str_to(ip_buf);
|
||||
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
|
||||
char webserver_url[7 + network::IP_ADDRESS_BUFFER_SIZE + 1 + 5 + 1];
|
||||
size_t len =
|
||||
buf_append_printf(webserver_url, sizeof(webserver_url), 0, "http://%s:%u", ip_buf, USE_WEBSERVER_PORT);
|
||||
if (!builder.add_string(webserver_url, len)) {
|
||||
ESP_LOGW(TAG, "Response full; URL dropped");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS,
|
||||
std::vector<std::string>(url_strings, url_strings + url_count)));
|
||||
this->send_response_(builder.finish());
|
||||
} else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
|
||||
ESP_LOGD(TAG, "WiFi provisioned externally");
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "esphome/components/output/binary_output.h"
|
||||
#endif
|
||||
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_ESP32
|
||||
@@ -109,7 +110,7 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
|
||||
void set_state_(improv::State state, bool update_advertising = true);
|
||||
void set_error_(improv::Error error);
|
||||
improv::State get_initial_state_() const;
|
||||
void send_response_(std::vector<uint8_t> &&response);
|
||||
void send_response_(std::span<const uint8_t> response);
|
||||
void process_incoming_data_();
|
||||
void on_wifi_connect_timeout_();
|
||||
void check_wifi_connection_();
|
||||
|
||||
@@ -137,7 +137,16 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
|
||||
return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
if ver <= cv.Version(2, 6, 2):
|
||||
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
|
||||
# Same encoding the native toolchain uses for its package download, so a
|
||||
# version bump cannot drift between the two paths.
|
||||
from esphome.arduino8266.framework import framework_package_version
|
||||
|
||||
try:
|
||||
return f"~{framework_package_version(ver)}"
|
||||
except EsphomeError as err:
|
||||
# Anchor the 4.x rejection to the framework version line instead of
|
||||
# aborting with a bare traceback-level error
|
||||
raise cv.Invalid(str(err), path=[CONF_VERSION]) from err
|
||||
|
||||
|
||||
# NOTE: Keep this in mind when updating the recommended version:
|
||||
@@ -247,6 +256,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_ENABLE_SCANF_FLOAT): cv.boolean,
|
||||
}
|
||||
),
|
||||
# Until the native toolchain lands, PlatformIO is the only backend;
|
||||
# reject a --toolchain this platform cannot serve yet.
|
||||
cv.require_platformio_toolchain("ESP8266"),
|
||||
set_core_data,
|
||||
)
|
||||
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
import logging
|
||||
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.noise import (
|
||||
decode_encryption_key,
|
||||
encryption_schema,
|
||||
is_reserved_key,
|
||||
)
|
||||
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
|
||||
from esphome.config_helpers import merge_config
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_API,
|
||||
CONF_ENCRYPTION,
|
||||
CONF_ESPHOME,
|
||||
CONF_ID,
|
||||
CONF_KEY,
|
||||
CONF_NUM_ATTEMPTS,
|
||||
CONF_OTA,
|
||||
CONF_PASSWORD,
|
||||
@@ -15,6 +23,7 @@ from esphome.const import (
|
||||
CONF_REBOOT_TIMEOUT,
|
||||
CONF_SAFE_MODE,
|
||||
CONF_VERSION,
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.coroutine import CoroPriority
|
||||
@@ -22,6 +31,7 @@ import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONF_ALLOW_PARTITION_ACCESS = "allow_partition_access"
|
||||
CONF_CAPTIVE_PORTAL = "captive_portal"
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -30,7 +40,15 @@ CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["network"]
|
||||
|
||||
|
||||
AUTO_LOAD = ["sha256", "socket"]
|
||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
|
||||
"""Auto-load noise only when encryption is configured."""
|
||||
base = ["sha256", "socket"]
|
||||
# A falsy config is a tooling probe for the maximal set (None from
|
||||
# dependency resolution, {} from the components-graph platform probe);
|
||||
# a validated config always carries defaults, never empty
|
||||
if not config or CONF_ENCRYPTION in config:
|
||||
return base + ["noise"]
|
||||
return base
|
||||
|
||||
|
||||
esphome = cg.esphome_ns.namespace("esphome")
|
||||
@@ -67,11 +85,24 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
|
||||
CONF_PASSWORD in merged_ota_esphome_configs_by_port[conf_port]
|
||||
and CONF_PASSWORD in ota_conf
|
||||
and merged_ota_esphome_configs_by_port[conf_port][CONF_PASSWORD]
|
||||
!= ota_conf.get(CONF_PASSWORD)
|
||||
!= ota_conf[CONF_PASSWORD]
|
||||
):
|
||||
raise cv.Invalid(
|
||||
f"Found multiple configurations but {CONF_PASSWORD} is inconsistent"
|
||||
)
|
||||
# Encryption blocks conflict only when both pin a key; a bare
|
||||
# `encryption:` (a package/device split) is compatible with a
|
||||
# keyed one, and merge_config yields the keyed result
|
||||
merged_key = (
|
||||
merged_ota_esphome_configs_by_port[conf_port]
|
||||
.get(CONF_ENCRYPTION, {})
|
||||
.get(CONF_KEY)
|
||||
)
|
||||
other_key = ota_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY)
|
||||
if merged_key and other_key and merged_key != other_key:
|
||||
raise cv.Invalid(
|
||||
f"Found multiple configurations but {CONF_ENCRYPTION} is inconsistent"
|
||||
)
|
||||
|
||||
ports_with_merged_configs.append(conf_port)
|
||||
merged_ota_esphome_configs_by_port[conf_port] = merge_config(
|
||||
@@ -94,6 +125,20 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
|
||||
|
||||
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
|
||||
|
||||
api_conf = full_conf.get(CONF_API) or {}
|
||||
for ota_conf in merged_ota_esphome_configs_by_port.values():
|
||||
# Merging same-port blocks can combine a password from one block with
|
||||
# encryption from another; re-check the exclusion on the merged result.
|
||||
_validate_no_password_with_encryption(ota_conf)
|
||||
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
|
||||
_resolve_encryption_key(encryption_conf, api_conf)
|
||||
if any(
|
||||
conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf
|
||||
) and any(
|
||||
CONF_ENCRYPTION in conf for conf in merged_ota_esphome_configs_by_port.values()
|
||||
):
|
||||
_warn_web_server_ota(full_conf)
|
||||
|
||||
full_conf[CONF_OTA] = new_ota_conf
|
||||
fv.full_config.set(full_conf)
|
||||
|
||||
@@ -107,6 +152,73 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
|
||||
)
|
||||
|
||||
|
||||
def _warn_web_server_ota(full_conf: ConfigType) -> None:
|
||||
"""The web_server ota platform accepts the same image over plaintext HTTP
|
||||
with basic auth, bypassing the encryption; warn rather than fail so the
|
||||
operator keeps the recovery path."""
|
||||
if CONF_CAPTIVE_PORTAL in full_conf and CONF_WEB_SERVER not in full_conf:
|
||||
# The captive_portal auto-load: the endpoint only exists while the
|
||||
# fallback AP is active
|
||||
_LOGGER.warning(
|
||||
"OTA encryption does not cover the %s OTA platform (auto-loaded "
|
||||
"by captive_portal); the plaintext /update endpoint stays "
|
||||
"reachable while the fallback AP is active",
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
"OTA encryption does not cover the %s OTA platform; its "
|
||||
"plaintext /update endpoint accepts the same image",
|
||||
CONF_WEB_SERVER,
|
||||
)
|
||||
|
||||
|
||||
def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) -> None:
|
||||
"""Resolve the one encryption key per device into the ota block.
|
||||
|
||||
An explicit ota key must match the api key, a bare block inherits it,
|
||||
a runtime provisioned api key cannot be inherited, and the all-zeros
|
||||
provisioning sentinel is rejected (the device treats it as no key).
|
||||
"""
|
||||
api_key = api_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY)
|
||||
if ota_key := encryption_conf.get(CONF_KEY):
|
||||
if api_key and ota_key != api_key:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY} must match the "
|
||||
f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY}; omit the "
|
||||
f"'{CONF_OTA}' {CONF_KEY} to use the '{CONF_API}' one"
|
||||
)
|
||||
elif not api_key:
|
||||
if CONF_ENCRYPTION in api_conf:
|
||||
raise cv.Invalid(
|
||||
f"the '{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} is provisioned at "
|
||||
f"runtime and cannot be inherited at build time; set an explicit "
|
||||
f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY}"
|
||||
)
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_OTA}' {CONF_ENCRYPTION} has no {CONF_KEY} and there is no "
|
||||
f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} to inherit; set one of them"
|
||||
)
|
||||
else:
|
||||
encryption_conf[CONF_KEY] = api_key
|
||||
if is_reserved_key(encryption_conf[CONF_KEY]):
|
||||
raise cv.Invalid(
|
||||
f"The all-zeros {CONF_KEY} is reserved and provides no protection; "
|
||||
f"generate a real key with: openssl rand -base64 32"
|
||||
)
|
||||
|
||||
|
||||
# Also called on merged same-port configs in final validate, where schemas
|
||||
# do not run
|
||||
def _validate_no_password_with_encryption(config: ConfigType) -> ConfigType:
|
||||
if CONF_PASSWORD in config and CONF_ENCRYPTION in config:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_PASSWORD}' cannot be combined with '{CONF_ENCRYPTION}'; the "
|
||||
f"encryption key already authenticates the uploader, remove '{CONF_PASSWORD}'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
|
||||
"""Register socket needs for OTA component."""
|
||||
from esphome.components import socket
|
||||
@@ -134,6 +246,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
): cv.port,
|
||||
cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PASSWORD): cv.sensitive(),
|
||||
cv.Optional(CONF_ENCRYPTION): encryption_schema,
|
||||
cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid(
|
||||
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
|
||||
),
|
||||
@@ -147,12 +260,24 @@ CONFIG_SCHEMA = cv.All(
|
||||
)
|
||||
.extend(BASE_OTA_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
_validate_no_password_with_encryption,
|
||||
_consume_ota_sockets,
|
||||
)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
|
||||
|
||||
|
||||
def FILTER_SOURCE_FILES() -> list[str]:
|
||||
"""Filter out the noise transport when no ota entry configures encryption."""
|
||||
for ota_conf in CORE.config.get(CONF_OTA, []):
|
||||
if (
|
||||
ota_conf.get(CONF_PLATFORM) == CONF_ESPHOME
|
||||
and ota_conf.get(CONF_ENCRYPTION) is not None
|
||||
):
|
||||
return []
|
||||
return ["ota_esphome_noise.cpp"]
|
||||
|
||||
|
||||
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
@@ -171,6 +296,12 @@ async def to_code(config: ConfigType) -> None:
|
||||
if config.get(CONF_ALLOW_PARTITION_ACCESS):
|
||||
cg.add_define("USE_OTA_PARTITIONS")
|
||||
|
||||
if (encryption_conf := config.get(CONF_ENCRYPTION)) is not None:
|
||||
# A missing key was resolved from the api component in final validate.
|
||||
key = encryption_conf[CONF_KEY]
|
||||
cg.add_define("USE_OTA_ENCRYPTION")
|
||||
cg.add(var.set_noise_psk(list(decode_encryption_key(key))))
|
||||
|
||||
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
|
||||
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace esphome {
|
||||
|
||||
static const char *const TAG = "esphome.ota";
|
||||
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
|
||||
static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
|
||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
|
||||
|
||||
@@ -105,6 +104,11 @@ void ESPHomeOTAComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, " Password configured");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ctx_.has_psk()) {
|
||||
ESP_LOGCONFIG(TAG, " Encryption configured");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Partition access allowed\n"
|
||||
@@ -128,7 +132,8 @@ void ESPHomeOTAComponent::dump_config() {
|
||||
esp_partition_iterator_release(it);
|
||||
esp_bootloader_desc_t bootloader_desc;
|
||||
esp_err_t err = esp_ota_get_bootloader_description(nullptr, &bootloader_desc);
|
||||
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s", (err == ESP_OK) ? bootloader_desc.idf_ver : "version unknown");
|
||||
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s",
|
||||
(err == ESP_OK) ? bootloader_desc.idf_ver : LOG_STR_LITERAL("version unknown"));
|
||||
#endif // USE_ESP32
|
||||
#endif // USE_OTA_PARTITIONS
|
||||
}
|
||||
@@ -148,8 +153,10 @@ void ESPHomeOTAComponent::loop() {
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
|
||||
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08;
|
||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01;
|
||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02;
|
||||
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04;
|
||||
|
||||
void ESPHomeOTAComponent::handle_handshake_() {
|
||||
/// Handle the OTA handshake and authentication.
|
||||
@@ -201,8 +208,7 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
}
|
||||
|
||||
// Validate magic bytes
|
||||
static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
|
||||
if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) {
|
||||
if (memcmp(this->handshake_buf_, MAGIC_BYTES, sizeof(MAGIC_BYTES)) != 0) {
|
||||
ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
|
||||
this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
|
||||
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC);
|
||||
@@ -234,6 +240,19 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
}
|
||||
this->ota_features_ = this->handshake_buf_[0];
|
||||
ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
|
||||
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
// Fail closed: with a PSK configured the client must negotiate encryption
|
||||
// (which requires the extended protocol); refuse plaintext uploads.
|
||||
static constexpr uint8_t NOISE_REQUIRED_FEATURES =
|
||||
CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL;
|
||||
if (this->noise_ctx_.has_psk() && (this->ota_features_ & NOISE_REQUIRED_FEATURES) != NOISE_REQUIRED_FEATURES) {
|
||||
ESP_LOGW(TAG, "Client does not support encryption");
|
||||
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
this->transition_ota_state_(OTAState::FEATURE_ACK);
|
||||
|
||||
const bool supports_compression =
|
||||
@@ -249,6 +268,11 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
|
||||
#endif
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ctx_.has_psk()) {
|
||||
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE;
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
this->handshake_buf_[0] =
|
||||
@@ -264,6 +288,20 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
if (!this->try_write_(ack_size, LOG_STR("ack feature"))) {
|
||||
return;
|
||||
}
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
// With a PSK configured the rest of the session runs inside the noise
|
||||
// transport; the client sends the first handshake frame next, so there
|
||||
// is nothing to do until data arrives.
|
||||
if (this->noise_ctx_.has_psk()) {
|
||||
// handshake_buf_ still holds the feature ack composed above; a
|
||||
// would-block re-entry lands here without rebuilding it
|
||||
if (!this->noise_start_session_(this->handshake_buf_[1])) {
|
||||
return;
|
||||
}
|
||||
this->transition_ota_state_(OTAState::NOISE_HANDSHAKE);
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
// If password is set, move to auth phase
|
||||
if (!this->password_.empty()) {
|
||||
@@ -301,6 +339,16 @@ void ESPHomeOTAComponent::handle_handshake_() {
|
||||
this->handle_data_();
|
||||
return;
|
||||
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
case OTAState::NOISE_HANDSHAKE:
|
||||
if (!this->handle_noise_handshake_()) {
|
||||
return;
|
||||
}
|
||||
this->transition_ota_state_(OTAState::DATA);
|
||||
this->handle_data_();
|
||||
return;
|
||||
#endif
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -339,6 +387,8 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
/// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses
|
||||
/// wakeable_delay() in read();
|
||||
/// write() always returns immediately
|
||||
// Backend calls overwrite this with OK; reset to UNKNOWN before any
|
||||
// goto error that follows a successful begin()/write()
|
||||
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
size_t total = 0;
|
||||
uint32_t last_progress = 0;
|
||||
@@ -360,11 +410,11 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
this->client_->setblocking(true);
|
||||
|
||||
// Acknowledge auth OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
|
||||
|
||||
if (this->extended_proto_) {
|
||||
// Read ota type, 1 byte
|
||||
if (!this->readall_(buf, 1)) {
|
||||
if (!this->data_readall_(buf, 1)) {
|
||||
this->log_read_error_(LOG_STR("OTA type"));
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
@@ -373,7 +423,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type);
|
||||
|
||||
// Read size, 4 bytes MSB first
|
||||
if (!this->readall_(buf, 4)) {
|
||||
if (!this->data_readall_(buf, 4)) {
|
||||
this->log_read_error_(LOG_STR("size"));
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
@@ -404,11 +454,12 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
|
||||
// Acknowledge prepare OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
|
||||
|
||||
// Read binary MD5, 32 bytes
|
||||
if (!this->readall_(buf, 32)) {
|
||||
if (!this->data_readall_(buf, 32)) {
|
||||
this->log_read_error_(LOG_STR("MD5 checksum"));
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
sbuf[32] = '\0';
|
||||
@@ -416,7 +467,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
this->backend_->set_update_md5(sbuf);
|
||||
|
||||
// Acknowledge MD5 OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
|
||||
|
||||
// Track when we last received data so a silently-vanished peer (no FIN/RST
|
||||
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
|
||||
@@ -432,19 +483,35 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
}
|
||||
size_t remaining = ota_size - total;
|
||||
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
|
||||
ssize_t read = this->client_->read(buf, requested);
|
||||
if (read == -1) {
|
||||
const int err = errno;
|
||||
if (this->would_block_(err)) {
|
||||
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
|
||||
App.feed_wdt();
|
||||
continue;
|
||||
ssize_t read;
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ != nullptr) {
|
||||
// One frame per call; noise_read_data_ waits internally (readall_), so
|
||||
// there is no would-block retry here and failures are already logged.
|
||||
read = this->noise_read_data_(buf, requested);
|
||||
if (read <= 0) {
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
read = this->client_->read(buf, requested);
|
||||
if (read == -1) {
|
||||
const int err = errno;
|
||||
if (this->would_block_(err)) {
|
||||
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
|
||||
App.feed_wdt();
|
||||
continue;
|
||||
}
|
||||
ESP_LOGW(TAG, "Read err %d", err);
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
} else if (read == 0) {
|
||||
ESP_LOGW(TAG, "Remote closed");
|
||||
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
ESP_LOGW(TAG, "Read err %d", err);
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
} else if (read == 0) {
|
||||
ESP_LOGW(TAG, "Remote closed");
|
||||
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
|
||||
}
|
||||
|
||||
last_data_ms = millis();
|
||||
@@ -456,7 +523,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
total += read;
|
||||
#if USE_OTA_VERSION == 2
|
||||
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
|
||||
this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
|
||||
size_acknowledged += OTA_BLOCK_SIZE;
|
||||
}
|
||||
#endif
|
||||
@@ -475,7 +542,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
}
|
||||
|
||||
// Acknowledge receive OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
|
||||
|
||||
error_code = this->backend_->end();
|
||||
if (error_code != ota::OTA_RESPONSE_OK) {
|
||||
@@ -484,10 +551,10 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
}
|
||||
|
||||
// Acknowledge Update end OK - 1 byte
|
||||
this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
|
||||
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
|
||||
|
||||
// Read ACK
|
||||
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
||||
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
|
||||
this->log_read_error_(LOG_STR("ack"));
|
||||
// do not go to error, this is not fatal
|
||||
}
|
||||
@@ -510,7 +577,7 @@ void ESPHomeOTAComponent::handle_data_() {
|
||||
App.safe_reboot();
|
||||
|
||||
error:
|
||||
this->write_byte_(static_cast<uint8_t>(error_code));
|
||||
this->data_write_byte_(static_cast<uint8_t>(error_code));
|
||||
|
||||
// Abort backend before cleanup - cleanup_connection_() destroys the backend.
|
||||
// Always call abort() unconditionally: backends register external partitions before
|
||||
@@ -677,6 +744,9 @@ void ESPHomeOTAComponent::cleanup_connection_() {
|
||||
this->backend_ = nullptr;
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
this->cleanup_auth_();
|
||||
#endif
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
this->noise_ = nullptr;
|
||||
#endif
|
||||
// Intentionally no disable_loop() — letting loop() run one more iteration catches
|
||||
// any connection that queued on the listener mid-session (otherwise the wake flag,
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
#ifdef USE_OTA
|
||||
#include "esphome/components/ota/ota_backend_factory.h"
|
||||
#include "esphome/components/socket/socket.h"
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
#include "esphome/components/noise/noise_handshake.h"
|
||||
#endif
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
@@ -24,7 +27,10 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
AUTH_SEND, // Sending authentication request
|
||||
AUTH_READ, // Reading authentication data
|
||||
#endif // USE_OTA_PASSWORD
|
||||
DATA, // BLOCKING! Processing OTA data (update, etc.)
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
NOISE_HANDSHAKE, // Exchanging Noise handshake frames
|
||||
#endif
|
||||
DATA, // BLOCKING! Processing OTA data (update, etc.)
|
||||
};
|
||||
#ifdef USE_OTA_PASSWORD
|
||||
void set_auth_password(const std::string &password) { password_ = password; }
|
||||
@@ -38,6 +44,10 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
}
|
||||
#endif // USE_OTA_PASSWORD
|
||||
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
|
||||
#endif
|
||||
|
||||
/// Manually set the port OTA should listen on
|
||||
void set_port(uint16_t port) { this->port_ = port; }
|
||||
|
||||
@@ -63,6 +73,48 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
bool writeall_(const uint8_t *buf, size_t len);
|
||||
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
|
||||
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
// Heap-allocated only while an encrypted OTA session is active.
|
||||
struct NoiseSession {
|
||||
~NoiseSession();
|
||||
noise::NoiseResponderHandshake handshake;
|
||||
NoiseCipherState *send_cipher{nullptr};
|
||||
NoiseCipherState *recv_cipher{nullptr};
|
||||
uint16_t frame_len{0}; // total frame size once the header is parsed, 0 until then
|
||||
uint16_t frame_pos{0}; // bytes read or written so far
|
||||
bool writing{false}; // a produced handshake frame is still being flushed
|
||||
uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE];
|
||||
};
|
||||
bool noise_start_session_(uint8_t server_feature_flags);
|
||||
bool handle_noise_handshake_();
|
||||
bool noise_try_read_frame_();
|
||||
bool noise_try_write_frame_();
|
||||
void noise_send_reject_(const LogString *reason);
|
||||
ssize_t noise_decrypt_(uint8_t *buf, size_t len);
|
||||
ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext);
|
||||
bool noise_readall_(uint8_t *buf, size_t len);
|
||||
ssize_t noise_read_data_(uint8_t *buf, size_t capacity);
|
||||
bool noise_write_byte_(uint8_t byte);
|
||||
#endif // USE_OTA_ENCRYPTION
|
||||
|
||||
// Data-phase I/O dispatch: through the noise transport when a session is
|
||||
// active, straight to the socket otherwise.
|
||||
inline bool data_write_byte_(uint8_t byte) {
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ != nullptr)
|
||||
return this->noise_write_byte_(byte);
|
||||
#endif
|
||||
return this->write_byte_(byte);
|
||||
}
|
||||
// When encrypted, buf must have room for len + noise::MAC_SIZE bytes.
|
||||
inline bool data_readall_(uint8_t *buf, size_t len) {
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
if (this->noise_ != nullptr)
|
||||
return this->noise_readall_(buf, len);
|
||||
#endif
|
||||
return this->readall_(buf, len);
|
||||
}
|
||||
|
||||
bool try_read_(size_t to_read, const LogString *desc);
|
||||
bool try_write_(size_t to_write, const LogString *desc);
|
||||
|
||||
@@ -91,6 +143,10 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
std::string password_;
|
||||
std::unique_ptr<uint8_t[]> auth_buf_;
|
||||
#endif // USE_OTA_PASSWORD
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
noise::NoiseContext noise_ctx_;
|
||||
std::unique_ptr<NoiseSession> noise_;
|
||||
#endif // USE_OTA_ENCRYPTION
|
||||
|
||||
socket::ListenSocket *server_{nullptr};
|
||||
std::unique_ptr<socket::Socket> client_;
|
||||
@@ -98,6 +154,18 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
|
||||
|
||||
uint32_t client_connect_time_{0};
|
||||
static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
|
||||
// Buffer size for OTA data transfer. The upload client derives its maximum
|
||||
// encrypted frame plaintext from this (espota2.NOISE_MAX_PLAINTEXT is this
|
||||
// minus the 16-byte MAC); both must change together.
|
||||
static constexpr size_t OTA_BUFFER_SIZE = 1040;
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
// espota2.NOISE_MAX_PLAINTEXT; shrinking the buffer would reject every
|
||||
// frame a current CLI sends
|
||||
static constexpr size_t NOISE_CLIENT_MAX_PLAINTEXT = 1024;
|
||||
static_assert(OTA_BUFFER_SIZE >= NOISE_CLIENT_MAX_PLAINTEXT + noise::MAC_SIZE,
|
||||
"OTA_BUFFER_SIZE must fit a full encrypted data frame");
|
||||
#endif
|
||||
static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
|
||||
#ifdef USE_OTA_PARTITIONS
|
||||
uint32_t running_app_offset_{0};
|
||||
size_t running_app_size_{0};
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
#include "ota_esphome.h"
|
||||
#ifdef USE_OTA
|
||||
#ifdef USE_OTA_ENCRYPTION
|
||||
#include "esphome/components/noise/noise.h"
|
||||
#include "esphome/components/ota/ota_backend.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
#include <pgmspace.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
static const char *const TAG = "esphome.ota";
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
static constexpr char OTA_NOISE_PROLOGUE_INIT[] PROGMEM = "NoiseOTAInit";
|
||||
#else
|
||||
static constexpr char OTA_NOISE_PROLOGUE_INIT[] = "NoiseOTAInit";
|
||||
#endif
|
||||
static constexpr size_t OTA_NOISE_PROLOGUE_INIT_LEN = sizeof(OTA_NOISE_PROLOGUE_INIT) - 1;
|
||||
|
||||
ESPHomeOTAComponent::NoiseSession::~NoiseSession() {
|
||||
if (this->send_cipher != nullptr) {
|
||||
noise_cipherstate_free(this->send_cipher);
|
||||
}
|
||||
if (this->recv_cipher != nullptr) {
|
||||
noise_cipherstate_free(this->recv_cipher);
|
||||
}
|
||||
}
|
||||
|
||||
/** Allocate the session and start the responder handshake.
|
||||
*
|
||||
* The prologue binds the whole plaintext preamble, so any tampering with the
|
||||
* negotiation (a stripped feature flag, a changed version) breaks the first
|
||||
* handshake MAC on either side:
|
||||
* "NoiseOTAInit" | magic(5) | OK,version | client_features | FEATURE_FLAGS,server_flags
|
||||
*/
|
||||
bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
|
||||
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
|
||||
this->noise_ = std::unique_ptr<NoiseSession>(new (std::nothrow) NoiseSession());
|
||||
if (this->noise_ == nullptr) {
|
||||
ESP_LOGW(TAG, "Session allocation failed");
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
|
||||
static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version
|
||||
static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1;
|
||||
static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags
|
||||
uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + sizeof(MAGIC_BYTES) + PROLOGUE_ACK_LEN + PROLOGUE_CLIENT_FEATURES_LEN +
|
||||
PROLOGUE_FEATURE_ACK_LEN];
|
||||
#ifdef USE_ESP8266
|
||||
memcpy_P(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
|
||||
#else
|
||||
std::memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
|
||||
#endif
|
||||
uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN;
|
||||
// Magic bytes, already validated in MAGIC_READ
|
||||
std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES));
|
||||
p += sizeof(MAGIC_BYTES);
|
||||
// Our magic ack
|
||||
*p++ = ota::OTA_RESPONSE_OK;
|
||||
*p++ = USE_OTA_VERSION;
|
||||
// The feature byte the client sent
|
||||
*p++ = this->ota_features_;
|
||||
// The feature ack we sent (noise requires the extended protocol)
|
||||
*p++ = ota::OTA_RESPONSE_FEATURE_FLAGS;
|
||||
*p++ = server_feature_flags;
|
||||
|
||||
int err = this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue));
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Handshake init: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/** Drive the non-blocking handshake from loop(); returns true once the
|
||||
* transport ciphers are ready. A would-block returns false and the next
|
||||
* loop() resumes from the NoiseSession cursors; on failure the connection
|
||||
* is cleaned up.
|
||||
*/
|
||||
bool ESPHomeOTAComponent::handle_noise_handshake_() {
|
||||
NoiseSession &s = *this->noise_;
|
||||
while (true) {
|
||||
if (s.writing) {
|
||||
if (!this->noise_try_write_frame_()) {
|
||||
return false; // would block, or errored and cleaned up
|
||||
}
|
||||
s.writing = false;
|
||||
s.frame_pos = 0;
|
||||
s.frame_len = 0;
|
||||
}
|
||||
switch (s.handshake.action()) {
|
||||
case noise::NoiseResponderHandshake::Action::ACTION_READ: {
|
||||
if (!this->noise_try_read_frame_()) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t payload_len = s.frame_len - noise::FRAME_HEADER_SIZE;
|
||||
s.frame_pos = 0;
|
||||
s.frame_len = 0;
|
||||
if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) {
|
||||
ESP_LOGW(TAG, "Bad handshake error byte: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]);
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Handshake read: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
this->noise_send_reject_(noise::reject_reason_for(err));
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case noise::NoiseResponderHandshake::Action::ACTION_WRITE: {
|
||||
size_t msg_len = 0;
|
||||
int err =
|
||||
s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Handshake write: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
const uint16_t payload_len = msg_len + 1;
|
||||
noise::write_frame_header(s.frame_buf, payload_len);
|
||||
s.frame_buf[noise::FRAME_HEADER_SIZE] = noise::HANDSHAKE_STATUS_OK;
|
||||
s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
|
||||
s.frame_pos = 0;
|
||||
s.writing = true;
|
||||
break;
|
||||
}
|
||||
case noise::NoiseResponderHandshake::Action::ACTION_SPLIT: {
|
||||
int err = s.handshake.split(s.send_cipher, s.recv_cipher);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Handshake split: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Noise handshake complete");
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
ESP_LOGW(TAG, "Bad handshake state");
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Non-blocking read of one handshake frame into the session buffer.
|
||||
bool ESPHomeOTAComponent::noise_try_read_frame_() {
|
||||
NoiseSession &s = *this->noise_;
|
||||
while (s.frame_pos < noise::FRAME_HEADER_SIZE) {
|
||||
ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, noise::FRAME_HEADER_SIZE - s.frame_pos);
|
||||
if (!this->handle_read_error_(read, LOG_STR("read noise header"))) {
|
||||
return false;
|
||||
}
|
||||
s.frame_pos += read;
|
||||
}
|
||||
if (s.frame_len == 0) {
|
||||
const uint16_t payload_len = encode_uint16(s.frame_buf[1], s.frame_buf[2]);
|
||||
if (s.frame_buf[0] != noise::FRAME_INDICATOR || payload_len < 1 || payload_len > 1 + noise::MAX_HANDSHAKE_SIZE) {
|
||||
ESP_LOGW(TAG, "Bad handshake frame: 0x%02X, %u bytes", s.frame_buf[0], payload_len);
|
||||
this->cleanup_connection_();
|
||||
return false;
|
||||
}
|
||||
s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
|
||||
}
|
||||
while (s.frame_pos < s.frame_len) {
|
||||
ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
|
||||
if (!this->handle_read_error_(read, LOG_STR("read noise frame"))) {
|
||||
return false;
|
||||
}
|
||||
s.frame_pos += read;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Non-blocking write of the pending session-buffer frame.
|
||||
bool ESPHomeOTAComponent::noise_try_write_frame_() {
|
||||
NoiseSession &s = *this->noise_;
|
||||
while (s.frame_pos < s.frame_len) {
|
||||
ssize_t written = this->client_->write(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
|
||||
if (!this->handle_write_error_(written, LOG_STR("write noise frame"))) {
|
||||
return false;
|
||||
}
|
||||
s.frame_pos += written;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Best-effort explicit reject frame so the client can log a readable reason.
|
||||
void ESPHomeOTAComponent::noise_send_reject_(const LogString *reason) {
|
||||
// Every reason here comes from noise::reject_reason_for(), so the exported
|
||||
// floor is the exact capacity needed
|
||||
uint8_t data[noise::FRAME_HEADER_SIZE + noise::MAC_FAILURE_PAYLOAD_SIZE];
|
||||
const size_t payload_len =
|
||||
noise::format_reject_payload(data + noise::FRAME_HEADER_SIZE, sizeof(data) - noise::FRAME_HEADER_SIZE, reason);
|
||||
noise::write_frame_header(data, payload_len);
|
||||
this->client_->write(data, noise::FRAME_HEADER_SIZE + payload_len); // Best effort, non-blocking
|
||||
}
|
||||
|
||||
/// Decrypt a ciphertext in place; returns the plaintext size or -1.
|
||||
ssize_t ESPHomeOTAComponent::noise_decrypt_(uint8_t *buf, size_t len) {
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, buf, len, len);
|
||||
int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Decrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
return -1;
|
||||
}
|
||||
return mbuf.size;
|
||||
}
|
||||
|
||||
/** Blocking read of one frame whose ciphertext size must be within the given
|
||||
* bounds, decrypted in place; returns the plaintext size, or -1 on error.
|
||||
* buf needs max_ciphertext capacity.
|
||||
*/
|
||||
ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext) {
|
||||
uint8_t header[noise::FRAME_HEADER_SIZE];
|
||||
if (!this->readall_(header, sizeof(header))) {
|
||||
return -1;
|
||||
}
|
||||
const size_t ciphertext_len = encode_uint16(header[1], header[2]);
|
||||
if (header[0] != noise::FRAME_INDICATOR || ciphertext_len < min_ciphertext || ciphertext_len > max_ciphertext) {
|
||||
ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], ciphertext_len);
|
||||
return -1;
|
||||
}
|
||||
if (!this->readall_(buf, ciphertext_len)) {
|
||||
return -1;
|
||||
}
|
||||
return this->noise_decrypt_(buf, ciphertext_len);
|
||||
}
|
||||
|
||||
/** Blocking read of one frame whose plaintext must be exactly len bytes
|
||||
* (control units are one unit per frame). buf needs len + noise::MAC_SIZE
|
||||
* capacity; the plaintext lands at buf[0..len).
|
||||
*/
|
||||
bool ESPHomeOTAComponent::noise_readall_(uint8_t *buf, size_t len) {
|
||||
return this->noise_read_frame_blocking_(buf, len + noise::MAC_SIZE, len + noise::MAC_SIZE) == (ssize_t) len;
|
||||
}
|
||||
|
||||
/** Blocking read of one data-phase frame, decrypted in place; returns the
|
||||
* plaintext size, or -1 on error. buf is the OTA_BUFFER_SIZE data buffer.
|
||||
* The ciphertext must fit that buffer and its plaintext must fit what the
|
||||
* caller accepts (the remaining image bytes).
|
||||
*/
|
||||
ssize_t ESPHomeOTAComponent::noise_read_data_(uint8_t *buf, size_t capacity) {
|
||||
const size_t max_ciphertext = std::min(capacity + noise::MAC_SIZE, OTA_BUFFER_SIZE);
|
||||
return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
|
||||
}
|
||||
|
||||
/// Blocking write of one response byte as an encrypted frame.
|
||||
bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) {
|
||||
uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE];
|
||||
frame[noise::FRAME_HEADER_SIZE] = byte;
|
||||
NoiseBuffer mbuf;
|
||||
noise_buffer_init(mbuf);
|
||||
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE);
|
||||
int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
|
||||
if (err != 0) {
|
||||
ESP_LOGW(TAG, "Encrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
|
||||
return false;
|
||||
}
|
||||
noise::write_frame_header(frame, mbuf.size);
|
||||
return this->writeall_(frame, noise::FRAME_HEADER_SIZE + mbuf.size);
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
#endif // USE_OTA_ENCRYPTION
|
||||
#endif // USE_OTA
|
||||
@@ -155,6 +155,11 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_ESPNOW")
|
||||
cg.add_define("USE_ESPNOW_MAX_PAYLOAD_SIZE", config[CONF_MAX_PAYLOAD_SIZE])
|
||||
|
||||
if CORE.is_esp32:
|
||||
from esphome.components.esp32 import include_builtin_idf_component
|
||||
|
||||
include_builtin_idf_component("esp_wifi")
|
||||
|
||||
if CONF_WIFI in CORE.config:
|
||||
# Track the Wi-Fi channel via connect events instead of polling every loop
|
||||
wifi.request_wifi_connect_state_listener()
|
||||
|
||||
@@ -71,6 +71,33 @@ def _process_git_config(config: dict[str, Any], refresh: TimePeriodSeconds) -> P
|
||||
return components_dir
|
||||
|
||||
|
||||
def _log_overridden_components(
|
||||
conf: dict[str, Any], component_names: list[str]
|
||||
) -> None:
|
||||
overridden = [
|
||||
name
|
||||
for name in component_names
|
||||
if (loader.CORE_COMPONENTS_PATH / name / "__init__.py").is_file()
|
||||
]
|
||||
if not overridden:
|
||||
return
|
||||
if conf[CONF_TYPE] == TYPE_GIT:
|
||||
source = conf[CONF_URL]
|
||||
if ref := conf.get(CONF_REF):
|
||||
source = f"{source}@{ref}"
|
||||
if path := conf.get(CONF_PATH):
|
||||
source = f"{source} ({path})"
|
||||
else:
|
||||
source = conf[CONF_PATH]
|
||||
_LOGGER.info(
|
||||
"External components are overriding built-in components:\n"
|
||||
" source: %s\n"
|
||||
" components: %s",
|
||||
source,
|
||||
", ".join(sorted(overridden)),
|
||||
)
|
||||
|
||||
|
||||
def _process_single_config(config: dict[str, Any]) -> None:
|
||||
conf = config[CONF_SOURCE]
|
||||
if conf[CONF_TYPE] == TYPE_GIT:
|
||||
@@ -84,8 +111,8 @@ def _process_single_config(config: dict[str, Any]) -> None:
|
||||
raise NotImplementedError
|
||||
|
||||
if config[CONF_COMPONENTS] == "all":
|
||||
num_components = len(list(components_dir.glob("*/__init__.py")))
|
||||
if num_components > 100:
|
||||
component_names = [p.parent.name for p in components_dir.glob("*/__init__.py")]
|
||||
if len(component_names) > 100:
|
||||
# Prevent accidentally including all components from an esphome fork/branch
|
||||
# In this case force the user to manually specify which components they want to include
|
||||
raise cv.Invalid(
|
||||
@@ -102,6 +129,9 @@ def _process_single_config(config: dict[str, Any]) -> None:
|
||||
[CONF_COMPONENTS, i],
|
||||
)
|
||||
allowed_components = config[CONF_COMPONENTS]
|
||||
component_names = allowed_components
|
||||
|
||||
_log_overridden_components(conf, component_names)
|
||||
|
||||
loader.install_meta_finder(components_dir, allowed_components=allowed_components)
|
||||
|
||||
|
||||
@@ -334,8 +334,9 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
|
||||
}
|
||||
if (traits.supports_preset_modes()) {
|
||||
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
|
||||
for (const char *s : traits.supported_preset_modes())
|
||||
for (const char *s : traits.supported_preset_modes()) {
|
||||
ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,8 @@ void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback)
|
||||
|
||||
// setup callbacks to react to sensor changes
|
||||
open_feedback->add_on_state_callback([this](bool state) {
|
||||
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
|
||||
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(),
|
||||
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
|
||||
this->recompute_position_();
|
||||
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) {
|
||||
this->endstop_reached_(true);
|
||||
@@ -106,7 +107,8 @@ void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback
|
||||
this->close_feedback_ = close_feedback;
|
||||
|
||||
close_feedback->add_on_state_callback([this](bool state) {
|
||||
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
|
||||
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(),
|
||||
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
|
||||
this->recompute_position_();
|
||||
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
|
||||
this->endstop_reached_(false);
|
||||
@@ -144,7 +146,8 @@ void FeedbackCover::endstop_reached_(bool open_endstop) {
|
||||
// from a position slightly past the endpoint
|
||||
if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) {
|
||||
float dur = (now - this->start_dir_time_) / 1e3f;
|
||||
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur);
|
||||
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
|
||||
open_endstop ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
|
||||
|
||||
// if there is no external mechanism, stop the cover
|
||||
if (!this->has_built_in_endstop_) {
|
||||
@@ -366,7 +369,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
|
||||
// the case when an obstacle appears while moving is handled in the callback
|
||||
if (obstacle != nullptr && obstacle->state) {
|
||||
ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(),
|
||||
dir == COVER_OPERATION_OPENING ? "Open" : "Close");
|
||||
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"));
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
@@ -383,9 +386,9 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
|
||||
this->set_current_operation_(dir, true);
|
||||
this->prev_command_trigger_ = trig;
|
||||
ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(),
|
||||
dir == COVER_OPERATION_OPENING ? "OPEN"
|
||||
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
|
||||
: "STOP");
|
||||
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
|
||||
: dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
|
||||
: LOG_STR_LITERAL("STOP"));
|
||||
trig->trigger();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,8 +547,8 @@ void FingerprintGrowComponent::dump_config() {
|
||||
" System Identifier Code: 0x%.4X\n"
|
||||
" Touch Sensing Pin: %s\n"
|
||||
" Sensor Power Pin: %s",
|
||||
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
|
||||
this->has_power_pin_ ? power_pin_buf : "None");
|
||||
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : LOG_STR_LITERAL("None"),
|
||||
this->has_power_pin_ ? power_pin_buf : LOG_STR_LITERAL("None"));
|
||||
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
|
||||
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
|
||||
} else {
|
||||
|
||||
@@ -35,18 +35,20 @@ void GraphicalDisplayMenu::setup() {
|
||||
}
|
||||
|
||||
void GraphicalDisplayMenu::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Graphical Display Menu\n"
|
||||
" Has Display: %s\n"
|
||||
" Popup Mode: %s\n"
|
||||
" Advanced Drawing Mode: %s\n"
|
||||
" Has Font: %s\n"
|
||||
" Mode: %s\n"
|
||||
" Active: %s\n"
|
||||
" Menu items:",
|
||||
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
|
||||
YESNO(this->font_ != nullptr),
|
||||
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_));
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
"Graphical Display Menu\n"
|
||||
" Has Display: %s\n"
|
||||
" Popup Mode: %s\n"
|
||||
" Advanced Drawing Mode: %s\n"
|
||||
" Has Font: %s\n"
|
||||
" Mode: %s\n"
|
||||
" Active: %s\n"
|
||||
" Menu items:",
|
||||
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
|
||||
YESNO(this->font_ != nullptr),
|
||||
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? LOG_STR_LITERAL("Rotary") : LOG_STR_LITERAL("Joystick"),
|
||||
YESNO(this->active_));
|
||||
for (size_t i = 0; i < this->displayed_item_->items_size(); i++) {
|
||||
auto *item = this->displayed_item_->get_item(i);
|
||||
ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(),
|
||||
|
||||
@@ -1,105 +1,68 @@
|
||||
#include "growatt_solar.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::growatt_solar {
|
||||
|
||||
namespace helpers = modbus::helpers;
|
||||
|
||||
static const char *const TAG = "growatt_solar";
|
||||
|
||||
static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95}; // indexed with enum GrowattProtocolVersion
|
||||
|
||||
void GrowattSolar::loop() {
|
||||
// If update() was unable to send we retry until we can send.
|
||||
if (!this->waiting_to_update_)
|
||||
return;
|
||||
update();
|
||||
}
|
||||
void GrowattSolar::update() { this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]); }
|
||||
|
||||
void GrowattSolar::update() {
|
||||
// If our last send has had no reply yet, and it wasn't that long ago, do nothing.
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_send_ < this->get_update_interval() / 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The bus might be slow, or there might be other devices, or other components might be talking to our device.
|
||||
if (!this->ready_for_immediate_send()) {
|
||||
this->waiting_to_update_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
this->waiting_to_update_ = false;
|
||||
this->read_input_registers(0, MODBUS_REGISTER_COUNT[this->protocol_version_]);
|
||||
this->last_send_ = millis();
|
||||
}
|
||||
|
||||
void GrowattSolar::on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) {
|
||||
auto data = modbus::helpers::server_pdu_payload(response_pdu);
|
||||
// Other components might be sending commands to our device. But we don't get called with enough
|
||||
// context to know what is what. So if we didn't do a send, we ignore the data.
|
||||
if (!this->last_send_)
|
||||
return;
|
||||
this->last_send_ = 0;
|
||||
|
||||
// Also ignore the data if the message is too short. Otherwise we will publish invalid values.
|
||||
if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2)
|
||||
void GrowattSolar::on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status))
|
||||
return;
|
||||
|
||||
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void {
|
||||
// Publish a sensor if its register(s) are in this response; skipping absent registers keeps this
|
||||
// correct for any read range, so the poll may be split into multiple requests.
|
||||
auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
float value = encode_uint16(data[i * 2], data[i * 2 + 1]) * unit;
|
||||
sensor->publish_state(value);
|
||||
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
|
||||
sensor->publish_state(*value * unit);
|
||||
};
|
||||
|
||||
auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, size_t reg1, size_t reg2, float unit) -> void {
|
||||
float value = ((encode_uint16(data[reg1 * 2], data[reg1 * 2 + 1]) << 16) +
|
||||
encode_uint16(data[reg2 * 2], data[reg2 * 2 + 1])) *
|
||||
unit;
|
||||
if (sensor != nullptr)
|
||||
sensor->publish_state(value);
|
||||
auto publish_2_reg_sensor_state = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
if (auto value = helpers::value_at<helpers::SensorValueType::U_DWORD>(registers, start_address, reg))
|
||||
sensor->publish_state(*value * unit);
|
||||
};
|
||||
|
||||
switch (this->protocol_version_) {
|
||||
case RTU: {
|
||||
publish_1_reg_sensor_state(this->inverter_status_, RTU_INVERTER_STATUS, 1);
|
||||
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, RTU_PV_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU_PV_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU_PV1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU_PV1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, RTU_PV1_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU_PV1_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU_PV2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU_PV2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, RTU_PV2_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU_PV2_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, RTU_GRID_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU_GRID_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU_GRID_FREQUENCY, TWO_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU_PHASE1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU_PHASE1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER,
|
||||
RTU_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU_PHASE1_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU_PHASE2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU_PHASE2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER,
|
||||
RTU_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU_PHASE2_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU_PHASE3_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU_PHASE3_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER,
|
||||
RTU_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU_PHASE3_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, RTU_TODAY_PRODUCTION + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION,
|
||||
RTU_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->today_production_, RTU_TODAY_PRODUCTION, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, RTU_TOTAL_ENERGY_PRODUCTION, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, RTU_INVERTER_MODULE_TEMP, ONE_DEC_UNIT);
|
||||
break;
|
||||
@@ -107,42 +70,33 @@ void GrowattSolar::on_response(std::span<const uint8_t> request_pdu, std::span<c
|
||||
case RTU2: {
|
||||
publish_1_reg_sensor_state(this->inverter_status_, RTU2_INVERTER_STATUS, 1);
|
||||
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, RTU2_PV_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pv_active_power_sensor_, RTU2_PV_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[0].voltage_sensor_, RTU2_PV1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[0].current_sensor_, RTU2_PV1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, RTU2_PV1_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[0].active_power_sensor_, RTU2_PV1_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->pvs_[1].voltage_sensor_, RTU2_PV2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->pvs_[1].current_sensor_, RTU2_PV2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, RTU2_PV2_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->pvs_[1].active_power_sensor_, RTU2_PV2_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, RTU2_GRID_ACTIVE_POWER + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->grid_active_power_sensor_, RTU2_GRID_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->grid_frequency_sensor_, RTU2_GRID_FREQUENCY, TWO_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[0].voltage_sensor_, RTU2_PHASE1_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[0].current_sensor_, RTU2_PHASE1_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER,
|
||||
RTU2_PHASE1_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[0].active_power_sensor_, RTU2_PHASE1_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[1].voltage_sensor_, RTU2_PHASE2_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[1].current_sensor_, RTU2_PHASE2_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER,
|
||||
RTU2_PHASE2_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[1].active_power_sensor_, RTU2_PHASE2_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->phases_[2].voltage_sensor_, RTU2_PHASE3_VOLTAGE, ONE_DEC_UNIT);
|
||||
publish_1_reg_sensor_state(this->phases_[2].current_sensor_, RTU2_PHASE3_CURRENT, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER,
|
||||
RTU2_PHASE3_ACTIVE_POWER + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->phases_[2].active_power_sensor_, RTU2_PHASE3_ACTIVE_POWER, ONE_DEC_UNIT);
|
||||
|
||||
publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, RTU2_TODAY_PRODUCTION + 1,
|
||||
ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION,
|
||||
RTU2_TOTAL_ENERGY_PRODUCTION + 1, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->today_production_, RTU2_TODAY_PRODUCTION, ONE_DEC_UNIT);
|
||||
publish_2_reg_sensor_state(this->total_energy_production_, RTU2_TOTAL_ENERGY_PRODUCTION, ONE_DEC_UNIT);
|
||||
|
||||
publish_1_reg_sensor_state(this->inverter_module_temp_, RTU2_INVERTER_MODULE_TEMP, ONE_DEC_UNIT);
|
||||
break;
|
||||
|
||||
@@ -17,59 +17,59 @@ enum GrowattProtocolVersion {
|
||||
};
|
||||
|
||||
// Register addresses for the RTU protocol.
|
||||
constexpr size_t RTU_INVERTER_STATUS = 0; // length = 1
|
||||
constexpr size_t RTU_PV_ACTIVE_POWER = 1; // length = 2
|
||||
constexpr size_t RTU_PV1_VOLTAGE = 3; // length = 1
|
||||
constexpr size_t RTU_PV1_CURRENT = 4; // length = 1
|
||||
constexpr size_t RTU_PV1_ACTIVE_POWER = 5; // length = 2
|
||||
constexpr size_t RTU_PV2_VOLTAGE = 7; // length = 1
|
||||
constexpr size_t RTU_PV2_CURRENT = 8; // length = 1
|
||||
constexpr size_t RTU_PV2_ACTIVE_POWER = 9; // length = 2
|
||||
constexpr size_t RTU_GRID_ACTIVE_POWER = 11; // length = 2
|
||||
constexpr size_t RTU_GRID_FREQUENCY = 13; // length = 1
|
||||
constexpr size_t RTU_PHASE1_VOLTAGE = 14; // length = 1
|
||||
constexpr size_t RTU_PHASE1_CURRENT = 15; // length = 1
|
||||
constexpr size_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2
|
||||
constexpr size_t RTU_PHASE2_VOLTAGE = 18; // length = 1
|
||||
constexpr size_t RTU_PHASE2_CURRENT = 19; // length = 1
|
||||
constexpr size_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2
|
||||
constexpr size_t RTU_PHASE3_VOLTAGE = 22; // length = 1
|
||||
constexpr size_t RTU_PHASE3_CURRENT = 23; // length = 1
|
||||
constexpr size_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2
|
||||
constexpr size_t RTU_TODAY_PRODUCTION = 26; // length = 2
|
||||
constexpr size_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2
|
||||
constexpr size_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1
|
||||
constexpr uint16_t RTU_INVERTER_STATUS = 0; // length = 1
|
||||
constexpr uint16_t RTU_PV_ACTIVE_POWER = 1; // length = 2
|
||||
constexpr uint16_t RTU_PV1_VOLTAGE = 3; // length = 1
|
||||
constexpr uint16_t RTU_PV1_CURRENT = 4; // length = 1
|
||||
constexpr uint16_t RTU_PV1_ACTIVE_POWER = 5; // length = 2
|
||||
constexpr uint16_t RTU_PV2_VOLTAGE = 7; // length = 1
|
||||
constexpr uint16_t RTU_PV2_CURRENT = 8; // length = 1
|
||||
constexpr uint16_t RTU_PV2_ACTIVE_POWER = 9; // length = 2
|
||||
constexpr uint16_t RTU_GRID_ACTIVE_POWER = 11; // length = 2
|
||||
constexpr uint16_t RTU_GRID_FREQUENCY = 13; // length = 1
|
||||
constexpr uint16_t RTU_PHASE1_VOLTAGE = 14; // length = 1
|
||||
constexpr uint16_t RTU_PHASE1_CURRENT = 15; // length = 1
|
||||
constexpr uint16_t RTU_PHASE1_ACTIVE_POWER = 16; // length = 2
|
||||
constexpr uint16_t RTU_PHASE2_VOLTAGE = 18; // length = 1
|
||||
constexpr uint16_t RTU_PHASE2_CURRENT = 19; // length = 1
|
||||
constexpr uint16_t RTU_PHASE2_ACTIVE_POWER = 20; // length = 2
|
||||
constexpr uint16_t RTU_PHASE3_VOLTAGE = 22; // length = 1
|
||||
constexpr uint16_t RTU_PHASE3_CURRENT = 23; // length = 1
|
||||
constexpr uint16_t RTU_PHASE3_ACTIVE_POWER = 24; // length = 2
|
||||
constexpr uint16_t RTU_TODAY_PRODUCTION = 26; // length = 2
|
||||
constexpr uint16_t RTU_TOTAL_ENERGY_PRODUCTION = 28; // length = 2
|
||||
constexpr uint16_t RTU_INVERTER_MODULE_TEMP = 32; // length = 1
|
||||
|
||||
// Input register addresses for the RTU2 protocol as described
|
||||
// in the "GROWATT INVERTER MODBUS PROTOCOL_II V1.39" document.
|
||||
constexpr size_t RTU2_INVERTER_STATUS = 0; // length = 1
|
||||
constexpr size_t RTU2_PV_ACTIVE_POWER = 1; // length = 2
|
||||
constexpr size_t RTU2_PV1_VOLTAGE = 3; // length = 1
|
||||
constexpr size_t RTU2_PV1_CURRENT = 4; // length = 1
|
||||
constexpr size_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2
|
||||
constexpr size_t RTU2_PV2_VOLTAGE = 7; // length = 1
|
||||
constexpr size_t RTU2_PV2_CURRENT = 8; // length = 1
|
||||
constexpr size_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2
|
||||
constexpr size_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2
|
||||
constexpr size_t RTU2_GRID_FREQUENCY = 37; // length = 1
|
||||
constexpr size_t RTU2_PHASE1_VOLTAGE = 38; // length = 1
|
||||
constexpr size_t RTU2_PHASE1_CURRENT = 39; // length = 1
|
||||
constexpr size_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2
|
||||
constexpr size_t RTU2_PHASE2_VOLTAGE = 42; // length = 1
|
||||
constexpr size_t RTU2_PHASE2_CURRENT = 43; // length = 1
|
||||
constexpr size_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2
|
||||
constexpr size_t RTU2_PHASE3_VOLTAGE = 46; // length = 1
|
||||
constexpr size_t RTU2_PHASE3_CURRENT = 47; // length = 1
|
||||
constexpr size_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2
|
||||
constexpr size_t RTU2_TODAY_PRODUCTION = 53; // length = 2
|
||||
constexpr size_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2
|
||||
constexpr size_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1
|
||||
constexpr uint16_t RTU2_INVERTER_STATUS = 0; // length = 1
|
||||
constexpr uint16_t RTU2_PV_ACTIVE_POWER = 1; // length = 2
|
||||
constexpr uint16_t RTU2_PV1_VOLTAGE = 3; // length = 1
|
||||
constexpr uint16_t RTU2_PV1_CURRENT = 4; // length = 1
|
||||
constexpr uint16_t RTU2_PV1_ACTIVE_POWER = 5; // length = 2
|
||||
constexpr uint16_t RTU2_PV2_VOLTAGE = 7; // length = 1
|
||||
constexpr uint16_t RTU2_PV2_CURRENT = 8; // length = 1
|
||||
constexpr uint16_t RTU2_PV2_ACTIVE_POWER = 9; // length = 2
|
||||
constexpr uint16_t RTU2_GRID_ACTIVE_POWER = 35; // length = 2
|
||||
constexpr uint16_t RTU2_GRID_FREQUENCY = 37; // length = 1
|
||||
constexpr uint16_t RTU2_PHASE1_VOLTAGE = 38; // length = 1
|
||||
constexpr uint16_t RTU2_PHASE1_CURRENT = 39; // length = 1
|
||||
constexpr uint16_t RTU2_PHASE1_ACTIVE_POWER = 40; // length = 2
|
||||
constexpr uint16_t RTU2_PHASE2_VOLTAGE = 42; // length = 1
|
||||
constexpr uint16_t RTU2_PHASE2_CURRENT = 43; // length = 1
|
||||
constexpr uint16_t RTU2_PHASE2_ACTIVE_POWER = 44; // length = 2
|
||||
constexpr uint16_t RTU2_PHASE3_VOLTAGE = 46; // length = 1
|
||||
constexpr uint16_t RTU2_PHASE3_CURRENT = 47; // length = 1
|
||||
constexpr uint16_t RTU2_PHASE3_ACTIVE_POWER = 48; // length = 2
|
||||
constexpr uint16_t RTU2_TODAY_PRODUCTION = 53; // length = 2
|
||||
constexpr uint16_t RTU2_TOTAL_ENERGY_PRODUCTION = 55; // length = 2
|
||||
constexpr uint16_t RTU2_INVERTER_MODULE_TEMP = 93; // length = 1
|
||||
|
||||
class GrowattSolar final : public PollingComponent, public modbus::ModbusClientDevice {
|
||||
public:
|
||||
void loop() override;
|
||||
void update() override;
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_read_input_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
void dump_config() override;
|
||||
|
||||
void set_protocol_version(GrowattProtocolVersion protocol_version) { this->protocol_version_ = protocol_version; }
|
||||
@@ -104,9 +104,6 @@ class GrowattSolar final : public PollingComponent, public modbus::ModbusClientD
|
||||
}
|
||||
|
||||
protected:
|
||||
bool waiting_to_update_{false};
|
||||
uint32_t last_send_{0};
|
||||
|
||||
struct GrowattPhase {
|
||||
sensor::Sensor *voltage_sensor_{nullptr};
|
||||
sensor::Sensor *current_sensor_{nullptr};
|
||||
|
||||
@@ -69,10 +69,12 @@ void GT911Touchscreen::setup_internal_() {
|
||||
// Direct MCU pin: attach a hardware interrupt, no polling needed.
|
||||
this->attach_interrupt_(static_cast<InternalGPIOPin *>(this->interrupt_pin_),
|
||||
active_high ? gpio::INTERRUPT_RISING_EDGE : gpio::INTERRUPT_FALLING_EDGE);
|
||||
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s", active_high ? "HIGH" : "LOW");
|
||||
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s",
|
||||
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
|
||||
} else {
|
||||
// IO expander pin: leave as output for configuration only.
|
||||
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", active_high ? "HIGH" : "LOW");
|
||||
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s",
|
||||
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,7 +248,8 @@ void HaierClimateBase::setup() {
|
||||
|
||||
void HaierClimateBase::dump_config() {
|
||||
LOG_CLIMATE("", "Haier Climate", this);
|
||||
ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none");
|
||||
ESP_LOGCONFIG(TAG, " Device communication status: %s",
|
||||
this->valid_connection() ? LOG_STR_LITERAL("established") : LOG_STR_LITERAL("none"));
|
||||
}
|
||||
|
||||
void HaierClimateBase::loop() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user