mirror of
https://github.com/esphome/esphome.git
synced 2026-09-04 12:06:01 +00:00
Compare commits
94
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
421c5e5d32 | ||
|
|
b8480b8424 | ||
|
|
8bef5b22e1 | ||
|
|
379e077b5f | ||
|
|
3f68930001 | ||
|
|
0aff9e1c54 | ||
|
|
f9824ee83f | ||
|
|
68ffd5a773 | ||
|
|
fe3788ff47 | ||
|
|
d6758377d1 | ||
|
|
5dbc8ffe4c | ||
|
|
0f982f03b2 | ||
|
|
afb0022dd0 | ||
|
|
081ef3d30d | ||
|
|
ab59822c1f | ||
|
|
ca44db107c | ||
|
|
d437869373 | ||
|
|
c20b619684 | ||
|
|
ebe6c2d049 | ||
|
|
78beb75b3b | ||
|
|
fc9afcb201 | ||
|
|
7a784d1135 | ||
|
|
24af3fd834 | ||
|
|
61e37cfc86 | ||
|
|
fa5180bd51 | ||
|
|
8582bf194a | ||
|
|
328e83ad64 | ||
|
|
6877513195 | ||
|
|
03104b894b | ||
|
|
26a308a82d | ||
|
|
ea1e0c6f68 | ||
|
|
938f598709 | ||
|
|
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 | ||
|
|
2b4a196cf8 | ||
|
|
0dce7f4845 | ||
|
|
1623fe0852 | ||
|
|
4333870590 | ||
|
|
cc41fd0beb | ||
|
|
59397b4e28 | ||
|
|
768ab5b672 | ||
|
|
8db07d0de5 | ||
|
|
a1515ec662 | ||
|
|
3f65f5c12c | ||
|
|
7cc892ea14 | ||
|
|
9e4bec7e49 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
|
||||
+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,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}")
|
||||
|
||||
|
||||
+44
-18
@@ -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)
|
||||
|
||||
|
||||
@@ -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,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())
|
||||
@@ -78,6 +78,7 @@ from esphome.cpp_types import ( # noqa: F401
|
||||
StringRef,
|
||||
arduino_json_ns,
|
||||
bool_,
|
||||
char,
|
||||
const_char_ptr,
|
||||
double,
|
||||
esphome_ns,
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -41,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
|
||||
@@ -125,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"
|
||||
@@ -228,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
|
||||
@@ -352,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])
|
||||
@@ -371,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
|
||||
@@ -385,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
|
||||
|
||||
@@ -421,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)
|
||||
@@ -458,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")
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -2391,8 +2391,9 @@ void APIConnection::process_batch_() {
|
||||
} else if (payload_size == 0) {
|
||||
// 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)
|
||||
if (!this->flags_.remove) {
|
||||
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
|
||||
}
|
||||
this->clear_batch_();
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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_);
|
||||
|
||||
@@ -749,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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
@@ -1510,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",
|
||||
@@ -2304,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)
|
||||
@@ -2712,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
|
||||
@@ -3234,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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -343,11 +343,11 @@ void HonClimate::dump_config() {
|
||||
this->hvac_hardware_info_.value().software_version_,
|
||||
this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_);
|
||||
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
|
||||
(this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""),
|
||||
(this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""),
|
||||
(this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""),
|
||||
(this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""),
|
||||
(this->hvac_hardware_info_.value().functions_[4] ? " role" : ""));
|
||||
(this->hvac_hardware_info_.value().functions_[0] ? LOG_STR_LITERAL(" interactive") : ""),
|
||||
(this->hvac_hardware_info_.value().functions_[1] ? LOG_STR_LITERAL(" controller-device") : ""),
|
||||
(this->hvac_hardware_info_.value().functions_[2] ? LOG_STR_LITERAL(" crc") : ""),
|
||||
(this->hvac_hardware_info_.value().functions_[3] ? LOG_STR_LITERAL(" multinode") : ""),
|
||||
(this->hvac_hardware_info_.value().functions_[4] ? LOG_STR_LITERAL(" role") : ""));
|
||||
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,124 +1,71 @@
|
||||
#include "havells_solar.h"
|
||||
#include "havells_solar_registers.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::havells_solar {
|
||||
|
||||
namespace helpers = modbus::helpers;
|
||||
|
||||
static const char *const TAG = "havells_solar";
|
||||
|
||||
static const uint8_t MODBUS_REGISTER_COUNT = 48; // 48 x 16-bit registers
|
||||
|
||||
void HavellsSolar::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);
|
||||
if (data.size() < MODBUS_REGISTER_COUNT * 2) {
|
||||
ESP_LOGW(TAG, "Invalid size for HavellsSolar!");
|
||||
return;
|
||||
}
|
||||
void HavellsSolar::on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status))
|
||||
return; // the hub already logs exception responses
|
||||
|
||||
/* Usage: returns the float value of 1 register read by modbus
|
||||
Arg1: Register address * number of bytes per register
|
||||
Arg2: Multiplier for final register value
|
||||
*/
|
||||
auto havells_solar_get_2_registers = [&](size_t i, float unit) -> float {
|
||||
uint32_t temp = encode_uint32(data[i], data[i + 1], data[i + 2], data[i + 3]);
|
||||
return temp * unit;
|
||||
// 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_register = [&](sensor::Sensor *sensor, uint16_t reg, float unit) -> void {
|
||||
if (sensor == nullptr)
|
||||
return;
|
||||
if (auto value = helpers::value_at<helpers::SensorValueType::U_WORD>(registers, start_address, reg))
|
||||
sensor->publish_state(*value * unit);
|
||||
};
|
||||
|
||||
/* Usage: returns the float value of 2 registers read by modbus
|
||||
Arg1: Register address * number of bytes per register
|
||||
Arg2: Multiplier for final register value
|
||||
*/
|
||||
auto havells_solar_get_1_register = [&](size_t i, float unit) -> float {
|
||||
uint16_t temp = encode_uint16(data[i], data[i + 1]);
|
||||
return temp * unit;
|
||||
auto publish_2_registers = [&](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);
|
||||
};
|
||||
|
||||
for (uint8_t i = 0; i < 3; i++) {
|
||||
auto phase = this->phases_[i];
|
||||
auto &phase = this->phases_[i];
|
||||
if (!phase.setup)
|
||||
continue;
|
||||
|
||||
float voltage = havells_solar_get_1_register(HAVELLS_PHASE_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT);
|
||||
float current = havells_solar_get_1_register(HAVELLS_PHASE_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT);
|
||||
|
||||
if (phase.voltage_sensor_ != nullptr)
|
||||
phase.voltage_sensor_->publish_state(voltage);
|
||||
if (phase.current_sensor_ != nullptr)
|
||||
phase.current_sensor_->publish_state(current);
|
||||
publish_1_register(phase.voltage_sensor_, HAVELLS_PHASE_1_VOLTAGE + i * 2, ONE_DEC_UNIT);
|
||||
publish_1_register(phase.current_sensor_, HAVELLS_PHASE_1_CURRENT + i * 2, TWO_DEC_UNIT);
|
||||
}
|
||||
|
||||
for (uint8_t i = 0; i < 2; i++) {
|
||||
auto pv = this->pvs_[i];
|
||||
auto &pv = this->pvs_[i];
|
||||
if (!pv.setup)
|
||||
continue;
|
||||
|
||||
float voltage = havells_solar_get_1_register(HAVELLS_PV_1_VOLTAGE * 2 + (i * 4), ONE_DEC_UNIT);
|
||||
float current = havells_solar_get_1_register(HAVELLS_PV_1_CURRENT * 2 + (i * 4), TWO_DEC_UNIT);
|
||||
float active_power = havells_solar_get_1_register(HAVELLS_PV_1_POWER * 2 + (i * 2), MULTIPLY_TEN_UNIT);
|
||||
float voltage_sampled_by_secondary_cpu =
|
||||
havells_solar_get_1_register(HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU * 2 + (i * 2), ONE_DEC_UNIT);
|
||||
float insulation_of_p_to_ground =
|
||||
havells_solar_get_1_register(HAVELLS_PV1_INSULATION_OF_P_TO_GROUND * 2 + (i * 2), NO_DEC_UNIT);
|
||||
|
||||
if (pv.voltage_sensor_ != nullptr)
|
||||
pv.voltage_sensor_->publish_state(voltage);
|
||||
if (pv.current_sensor_ != nullptr)
|
||||
pv.current_sensor_->publish_state(current);
|
||||
if (pv.active_power_sensor_ != nullptr)
|
||||
pv.active_power_sensor_->publish_state(active_power);
|
||||
if (pv.voltage_sampled_by_secondary_cpu_sensor_ != nullptr)
|
||||
pv.voltage_sampled_by_secondary_cpu_sensor_->publish_state(voltage_sampled_by_secondary_cpu);
|
||||
if (pv.insulation_of_p_to_ground_sensor_ != nullptr)
|
||||
pv.insulation_of_p_to_ground_sensor_->publish_state(insulation_of_p_to_ground);
|
||||
publish_1_register(pv.voltage_sensor_, HAVELLS_PV_1_VOLTAGE + i * 2, ONE_DEC_UNIT);
|
||||
publish_1_register(pv.current_sensor_, HAVELLS_PV_1_CURRENT + i * 2, TWO_DEC_UNIT);
|
||||
publish_1_register(pv.active_power_sensor_, HAVELLS_PV_1_POWER + i, MULTIPLY_TEN_UNIT);
|
||||
publish_1_register(pv.voltage_sampled_by_secondary_cpu_sensor_, HAVELLS_PV1_VOLTAGE_SAMPLED_BY_SECONDARY_CPU + i,
|
||||
ONE_DEC_UNIT);
|
||||
publish_1_register(pv.insulation_of_p_to_ground_sensor_, HAVELLS_PV1_INSULATION_OF_P_TO_GROUND + i, NO_DEC_UNIT);
|
||||
}
|
||||
|
||||
float frequency = havells_solar_get_1_register(HAVELLS_GRID_FREQUENCY * 2, TWO_DEC_UNIT);
|
||||
float active_power = havells_solar_get_1_register(HAVELLS_SYSTEM_ACTIVE_POWER * 2, MULTIPLY_TEN_UNIT);
|
||||
float reactive_power = havells_solar_get_1_register(HAVELLS_SYSTEM_REACTIVE_POWER * 2, TWO_DEC_UNIT);
|
||||
float today_production = havells_solar_get_1_register(HAVELLS_TODAY_PRODUCTION * 2, TWO_DEC_UNIT);
|
||||
float total_energy_production = havells_solar_get_2_registers(HAVELLS_TOTAL_ENERGY_PRODUCTION * 2, NO_DEC_UNIT);
|
||||
float total_generation_time = havells_solar_get_2_registers(HAVELLS_TOTAL_GENERATION_TIME * 2, NO_DEC_UNIT);
|
||||
float today_generation_time = havells_solar_get_1_register(HAVELLS_TODAY_GENERATION_TIME * 2, NO_DEC_UNIT);
|
||||
float inverter_module_temp = havells_solar_get_1_register(HAVELLS_INVERTER_MODULE_TEMP * 2, NO_DEC_UNIT);
|
||||
float inverter_inner_temp = havells_solar_get_1_register(HAVELLS_INVERTER_INNER_TEMP * 2, NO_DEC_UNIT);
|
||||
float inverter_bus_voltage = havells_solar_get_1_register(HAVELLS_INVERTER_BUS_VOLTAGE * 2, NO_DEC_UNIT);
|
||||
float insulation_pv_n_to_ground = havells_solar_get_1_register(HAVELLS_INSULATION_OF_PV_N_TO_GROUND * 2, NO_DEC_UNIT);
|
||||
float gfci_value = havells_solar_get_1_register(HAVELLS_GFCI_VALUE * 2, NO_DEC_UNIT);
|
||||
float dci_of_r = havells_solar_get_1_register(HAVELLS_DCI_OF_R * 2, NO_DEC_UNIT);
|
||||
float dci_of_s = havells_solar_get_1_register(HAVELLS_DCI_OF_S * 2, NO_DEC_UNIT);
|
||||
float dci_of_t = havells_solar_get_1_register(HAVELLS_DCI_OF_T * 2, NO_DEC_UNIT);
|
||||
|
||||
if (this->frequency_sensor_ != nullptr)
|
||||
this->frequency_sensor_->publish_state(frequency);
|
||||
if (this->active_power_sensor_ != nullptr)
|
||||
this->active_power_sensor_->publish_state(active_power);
|
||||
if (this->reactive_power_sensor_ != nullptr)
|
||||
this->reactive_power_sensor_->publish_state(reactive_power);
|
||||
if (this->today_production_sensor_ != nullptr)
|
||||
this->today_production_sensor_->publish_state(today_production);
|
||||
if (this->total_energy_production_sensor_ != nullptr)
|
||||
this->total_energy_production_sensor_->publish_state(total_energy_production);
|
||||
if (this->total_generation_time_sensor_ != nullptr)
|
||||
this->total_generation_time_sensor_->publish_state(total_generation_time);
|
||||
if (this->today_generation_time_sensor_ != nullptr)
|
||||
this->today_generation_time_sensor_->publish_state(today_generation_time);
|
||||
if (this->inverter_module_temp_sensor_ != nullptr)
|
||||
this->inverter_module_temp_sensor_->publish_state(inverter_module_temp);
|
||||
if (this->inverter_inner_temp_sensor_ != nullptr)
|
||||
this->inverter_inner_temp_sensor_->publish_state(inverter_inner_temp);
|
||||
if (this->inverter_bus_voltage_sensor_ != nullptr)
|
||||
this->inverter_bus_voltage_sensor_->publish_state(inverter_bus_voltage);
|
||||
if (this->insulation_pv_n_to_ground_sensor_ != nullptr)
|
||||
this->insulation_pv_n_to_ground_sensor_->publish_state(insulation_pv_n_to_ground);
|
||||
if (this->gfci_value_sensor_ != nullptr)
|
||||
this->gfci_value_sensor_->publish_state(gfci_value);
|
||||
if (this->dci_of_r_sensor_ != nullptr)
|
||||
this->dci_of_r_sensor_->publish_state(dci_of_r);
|
||||
if (this->dci_of_s_sensor_ != nullptr)
|
||||
this->dci_of_s_sensor_->publish_state(dci_of_s);
|
||||
if (this->dci_of_t_sensor_ != nullptr)
|
||||
this->dci_of_t_sensor_->publish_state(dci_of_t);
|
||||
publish_1_register(this->frequency_sensor_, HAVELLS_GRID_FREQUENCY, TWO_DEC_UNIT);
|
||||
publish_1_register(this->active_power_sensor_, HAVELLS_SYSTEM_ACTIVE_POWER, MULTIPLY_TEN_UNIT);
|
||||
publish_1_register(this->reactive_power_sensor_, HAVELLS_SYSTEM_REACTIVE_POWER, TWO_DEC_UNIT);
|
||||
publish_1_register(this->today_production_sensor_, HAVELLS_TODAY_PRODUCTION, TWO_DEC_UNIT);
|
||||
publish_2_registers(this->total_energy_production_sensor_, HAVELLS_TOTAL_ENERGY_PRODUCTION, NO_DEC_UNIT);
|
||||
publish_2_registers(this->total_generation_time_sensor_, HAVELLS_TOTAL_GENERATION_TIME, NO_DEC_UNIT);
|
||||
publish_1_register(this->today_generation_time_sensor_, HAVELLS_TODAY_GENERATION_TIME, NO_DEC_UNIT);
|
||||
publish_1_register(this->inverter_module_temp_sensor_, HAVELLS_INVERTER_MODULE_TEMP, NO_DEC_UNIT);
|
||||
publish_1_register(this->inverter_inner_temp_sensor_, HAVELLS_INVERTER_INNER_TEMP, NO_DEC_UNIT);
|
||||
publish_1_register(this->inverter_bus_voltage_sensor_, HAVELLS_INVERTER_BUS_VOLTAGE, NO_DEC_UNIT);
|
||||
publish_1_register(this->insulation_pv_n_to_ground_sensor_, HAVELLS_INSULATION_OF_PV_N_TO_GROUND, NO_DEC_UNIT);
|
||||
publish_1_register(this->gfci_value_sensor_, HAVELLS_GFCI_VALUE, NO_DEC_UNIT);
|
||||
publish_1_register(this->dci_of_r_sensor_, HAVELLS_DCI_OF_R, NO_DEC_UNIT);
|
||||
publish_1_register(this->dci_of_s_sensor_, HAVELLS_DCI_OF_S, NO_DEC_UNIT);
|
||||
publish_1_register(this->dci_of_t_sensor_, HAVELLS_DCI_OF_T, NO_DEC_UNIT);
|
||||
}
|
||||
|
||||
void HavellsSolar::update() { this->read_holding_registers(0, MODBUS_REGISTER_COUNT); }
|
||||
|
||||
@@ -77,7 +77,8 @@ class HavellsSolar final : public PollingComponent, public modbus::ModbusClientD
|
||||
|
||||
void update() override;
|
||||
|
||||
void on_response(std::span<const uint8_t> request_pdu, std::span<const uint8_t> response_pdu) override;
|
||||
void on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
|
||||
@@ -29,8 +29,9 @@ void HBridgeSwitch::dump_config() {
|
||||
LOG_PIN(" On Pin: ", this->on_pin_);
|
||||
LOG_PIN(" Off Pin: ", this->off_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Pulse length: %" PRId32 " ms", this->pulse_length_);
|
||||
if (this->wait_time_)
|
||||
if (this->wait_time_) {
|
||||
ESP_LOGCONFIG(TAG, " Wait time %" PRId32 " ms", this->wait_time_);
|
||||
}
|
||||
}
|
||||
|
||||
void HBridgeSwitch::write_state(bool state) {
|
||||
|
||||
@@ -38,7 +38,7 @@ void HDC302XComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"HDC302x:\n"
|
||||
" Heater: %s",
|
||||
this->heater_active_ ? "active" : "inactive");
|
||||
this->heater_active_ ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("inactive"));
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Temperature", this->temp_sensor_);
|
||||
|
||||
@@ -44,8 +44,9 @@ void HE60rCover::dump_config() {
|
||||
" Close Duration: %.1fs",
|
||||
this->open_duration_ / 1e3f, this->close_duration_ / 1e3f);
|
||||
auto restore = this->restore_state_();
|
||||
if (restore.has_value())
|
||||
if (restore.has_value()) {
|
||||
ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f));
|
||||
}
|
||||
}
|
||||
|
||||
void HE60rCover::endstop_reached_(CoverOperation operation) {
|
||||
@@ -59,7 +60,7 @@ void HE60rCover::endstop_reached_(CoverOperation operation) {
|
||||
if (this->last_command_ == operation) {
|
||||
float dur = (float) (now - this->start_dir_time_) / 1e3f;
|
||||
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
|
||||
operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur);
|
||||
operation == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
|
||||
}
|
||||
this->publish_state();
|
||||
}
|
||||
@@ -77,8 +78,9 @@ void HE60rCover::process_rx_(uint8_t data) {
|
||||
ESP_LOGV(TAG, "Process RX data %X", data);
|
||||
if (!this->query_seen_) {
|
||||
this->query_seen_ = data == QUERY_BYTE;
|
||||
if (!this->query_seen_)
|
||||
if (!this->query_seen_) {
|
||||
ESP_LOGD(TAG, "RX Byte %02X", data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (data) {
|
||||
@@ -213,9 +215,9 @@ void HE60rCover::start_direction_(CoverOperation dir) {
|
||||
if (this->current_operation == dir)
|
||||
return;
|
||||
ESP_LOGD(TAG, "'%s' - Direction '%s' requested.", 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"));
|
||||
|
||||
if (dir == this->next_direction_) {
|
||||
// either moving and needs to stop, or stopped and will move correctly on one trigger
|
||||
|
||||
@@ -336,7 +336,8 @@ void HlkFm22xComponent::dump_config() {
|
||||
}
|
||||
if (this->enrolling_binary_sensor_) {
|
||||
LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF");
|
||||
ESP_LOGCONFIG(TAG, " Current Value: %s",
|
||||
this->enrolling_binary_sensor_->state ? LOG_STR_LITERAL("ON") : LOG_STR_LITERAL("OFF"));
|
||||
}
|
||||
if (this->face_count_sensor_) {
|
||||
LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
|
||||
|
||||
@@ -97,7 +97,8 @@ void HLW8012Component::update() {
|
||||
|
||||
if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) {
|
||||
this->current_mode_ = !this->current_mode_;
|
||||
ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE");
|
||||
ESP_LOGV(TAG, "Changing mode to %s mode",
|
||||
this->current_mode_ ? LOG_STR_LITERAL("CURRENT") : LOG_STR_LITERAL("VOLTAGE"));
|
||||
this->change_mode_at_ = 0;
|
||||
this->sel_pin_->digital_write(this->current_mode_);
|
||||
}
|
||||
|
||||
@@ -257,8 +257,9 @@ void HoermannHcp::on_state_reg_(uint16_t value) {
|
||||
}
|
||||
}
|
||||
// The low byte can change on its own, so only report a state we cannot decode once.
|
||||
if (state != (previous >> 8))
|
||||
if (state != (previous >> 8)) {
|
||||
ESP_LOGW(TAG, "Unknown door state 0x%02X", state);
|
||||
}
|
||||
}
|
||||
|
||||
// Low byte of register 6: bit 0x10 is the lamp, bit 0x04 the relay. The reference implementation records
|
||||
|
||||
@@ -116,8 +116,8 @@ void ILI9XXXDisplay::dump_config() {
|
||||
" Mirror_x: %s\n"
|
||||
" Mirror_y: %s\n"
|
||||
" Invert colors: %s",
|
||||
this->color_order_ == display::COLOR_ORDER_BGR ? "BGR" : "RGB", YESNO(this->swap_xy_),
|
||||
YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->pre_invertcolors_));
|
||||
this->color_order_ == display::COLOR_ORDER_BGR ? LOG_STR_LITERAL("BGR") : LOG_STR_LITERAL("RGB"),
|
||||
YESNO(this->swap_xy_), YESNO(this->mirror_x_), YESNO(this->mirror_y_), YESNO(this->pre_invertcolors_));
|
||||
|
||||
if (this->is_failed()) {
|
||||
ESP_LOGCONFIG(TAG, " => Failed to init Memory: YES!");
|
||||
|
||||
@@ -16,7 +16,7 @@ from esphome.types import ConfigType
|
||||
|
||||
AUTO_LOAD = ["improv_base"]
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
DEPENDENCIES = ["logger", "wifi"]
|
||||
DEPENDENCIES = ["logger", "network"]
|
||||
|
||||
improv_serial_ns = cg.esphome_ns.namespace("improv_serial")
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
#include "improv_serial_component.h"
|
||||
#ifdef USE_WIFI
|
||||
#ifdef USE_IMPROV_SERIAL
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -7,7 +7,10 @@
|
||||
#include "esphome/core/version.h"
|
||||
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#ifdef USE_WIFI
|
||||
#include "esphome/components/wifi/scan_list.h"
|
||||
#endif
|
||||
|
||||
#include <array>
|
||||
|
||||
@@ -26,13 +29,17 @@ void ImprovSerialComponent::setup() {
|
||||
this->hw_serial_ = logger::global_logger->get_hw_serial();
|
||||
#endif
|
||||
|
||||
if (wifi::global_wifi_component->has_sta()) {
|
||||
// The Improv state machine tracks Wi-Fi provisioning only. General device
|
||||
// connectivity (e.g. Ethernet) is reported separately via GET_NETWORK_STATE.
|
||||
#ifdef USE_WIFI
|
||||
if (wifi::global_wifi_component != nullptr && wifi::global_wifi_component->has_sta()) {
|
||||
this->state_ = improv::STATE_PROVISIONED;
|
||||
} else if (!wifi::global_wifi_component->is_disabled()) {
|
||||
} else if (wifi::global_wifi_component != nullptr && !wifi::global_wifi_component->is_disabled()) {
|
||||
// Respect Wi-Fi's disabled state; forcing a scan while disabled throws
|
||||
// the wifi component into an invalid state from which it cannot recover.
|
||||
wifi::global_wifi_component->start_scanning();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::loop() {
|
||||
@@ -55,8 +62,14 @@ void ImprovSerialComponent::loop() {
|
||||
}
|
||||
}
|
||||
|
||||
if (this->state_ == improv::STATE_PROVISIONING) {
|
||||
if (wifi::global_wifi_component->is_connected()) {
|
||||
#ifdef USE_WIFI
|
||||
if (this->state_ == improv::STATE_PROVISIONING && wifi::global_wifi_component != nullptr &&
|
||||
wifi::global_wifi_component->is_connected()) {
|
||||
// Being connected is not enough: re-provisioning a device that is already online leaves the
|
||||
// prior network up until it drops, so check that the joined network is the requested one
|
||||
// before reporting success. Same test as the wifi.connect action.
|
||||
char ssid_buf[wifi::SSID_BUFFER_SIZE];
|
||||
if (strcmp(wifi::global_wifi_component->wifi_ssid_to(ssid_buf), this->connecting_sta_.get_ssid().c_str()) == 0) {
|
||||
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
|
||||
this->connecting_sta_.get_password());
|
||||
this->connecting_sta_ = {};
|
||||
@@ -66,6 +79,7 @@ void ImprovSerialComponent::loop() {
|
||||
this->send_settings_response_(improv::WIFI_SETTINGS);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
|
||||
@@ -143,15 +157,17 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size)
|
||||
#endif
|
||||
}
|
||||
|
||||
void ImprovSerialComponent::send_settings_response_(improv::Command command) {
|
||||
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
|
||||
improv::RpcResponseBuilder builder(buf, command);
|
||||
#ifdef USE_IMPROV_SERIAL_NEXT_URL
|
||||
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
|
||||
if (ip.is_ip4()) {
|
||||
void ImprovSerialComponent::add_webserver_urls_(improv::RpcResponseBuilder &builder, [[maybe_unused]] bool wifi_first) {
|
||||
// The webserver listens on every interface, so advertise each one that has a usable IPv4.
|
||||
// network::get_ip_addresses() can't be used here: it returns only the highest-priority
|
||||
// interface's addresses, which are all-unset (0.0.0.0) when e.g. Ethernet has no link while
|
||||
// the device is online via Wi-Fi, and 0.0.0.0 must not become the advertised URL. OpenThread
|
||||
// is omitted: it only ever has IPv6 addresses, which cannot form an IPv4 http:// URL.
|
||||
const auto append_urls = [&builder](const network::IPAddresses &addresses) {
|
||||
for (const auto &ip : addresses) {
|
||||
if (!ip.is_ip4() || !ip.is_set())
|
||||
continue;
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
ip.str_to(ip_buf);
|
||||
// "http://" (7) + IP (40) + ":" (1) + port (5) + null (1) = 54
|
||||
@@ -162,9 +178,43 @@ void ImprovSerialComponent::send_settings_response_(improv::Command command) {
|
||||
if (!builder.add_string(webserver_url, len)) {
|
||||
ESP_LOGW(TAG, "Response full; URL dropped");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
#ifdef USE_WIFI
|
||||
// Clients redirect to the first URL, so the interface the client just configured has to lead:
|
||||
// another interface's address can be on a subnet that client cannot reach.
|
||||
const auto append_wifi_urls = [&append_urls]() {
|
||||
if (wifi::global_wifi_component != nullptr)
|
||||
append_urls(wifi::global_wifi_component->get_ip_addresses());
|
||||
};
|
||||
if (wifi_first)
|
||||
append_wifi_urls();
|
||||
#endif
|
||||
#ifdef USE_ETHERNET
|
||||
if (ethernet::global_eth_component != nullptr)
|
||||
append_urls(ethernet::global_eth_component->get_ip_addresses());
|
||||
#endif
|
||||
#ifdef USE_MODEM
|
||||
if (modem::global_modem_component != nullptr)
|
||||
append_urls(modem::global_modem_component->get_ip_addresses());
|
||||
#endif
|
||||
#ifdef USE_WIFI
|
||||
if (!wifi_first)
|
||||
append_wifi_urls();
|
||||
#endif
|
||||
}
|
||||
#endif // USE_WEBSERVER
|
||||
|
||||
void ImprovSerialComponent::send_settings_response_(improv::Command command) {
|
||||
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
|
||||
improv::RpcResponseBuilder builder(buf, command);
|
||||
#ifdef USE_IMPROV_SERIAL_NEXT_URL
|
||||
this->add_next_url_(builder, MAX_NEXT_URL_LEN);
|
||||
#endif
|
||||
#ifdef USE_WEBSERVER
|
||||
// This response only ever answers Wi-Fi provisioning, so lead with the Wi-Fi URL as it did
|
||||
// before other interfaces were reported.
|
||||
this->add_webserver_urls_(builder, /*wifi_first=*/true);
|
||||
#endif
|
||||
this->send_response_(builder.finish(false));
|
||||
}
|
||||
@@ -231,7 +281,8 @@ bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) {
|
||||
bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command) {
|
||||
switch (command.command) {
|
||||
case improv::WIFI_SETTINGS: {
|
||||
if (wifi::global_wifi_component->is_disabled()) {
|
||||
#ifdef USE_WIFI
|
||||
if (wifi::global_wifi_component == nullptr || wifi::global_wifi_component->is_disabled()) {
|
||||
// Wi-Fi is disabled, so we can't provision. Respond immediately
|
||||
// instead of letting the client wait out its provisioning timeout.
|
||||
ESP_LOGW(TAG, "Wi-Fi is disabled; cannot provision");
|
||||
@@ -243,21 +294,32 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
sta.set_password(command.password.c_str());
|
||||
this->connecting_sta_ = sta;
|
||||
|
||||
// Sampled before start_connecting(): the old connection drops asynchronously after it.
|
||||
const bool switching = wifi::global_wifi_component->is_connected();
|
||||
wifi::global_wifi_component->set_sta(sta);
|
||||
wifi::global_wifi_component->start_connecting(sta);
|
||||
this->set_state_(improv::STATE_PROVISIONING);
|
||||
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
|
||||
command.password.c_str());
|
||||
|
||||
this->set_timeout("wifi-connect-timeout", 30000, [this]() { this->on_wifi_connect_timeout_(); });
|
||||
this->set_timeout("wifi-connect-timeout", switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS,
|
||||
[this]() { this->on_wifi_connect_timeout_(); });
|
||||
#else
|
||||
// No Wi-Fi support compiled in; there is nothing to provision.
|
||||
ESP_LOGW(TAG, "Wi-Fi not supported; cannot provision");
|
||||
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
case improv::GET_CURRENT_STATE:
|
||||
if (wifi::global_wifi_component->is_disabled()) {
|
||||
// Wi-Fi is disabled; report the Improv "stopped" state so a client can tell
|
||||
// the user that provisioning is unavailable. Reported transiently without
|
||||
// disturbing our internal provisioning state machine, so a later `wifi.enable`
|
||||
// still reports the correct state.
|
||||
case improv::GET_CURRENT_STATE: {
|
||||
// This state machine tracks Wi-Fi provisioning only. When Wi-Fi is disabled or not
|
||||
// compiled in, provisioning is unavailable -> report STOPPED so the client doesn't
|
||||
// offer a Wi-Fi form. General connectivity (e.g. Ethernet) is reported separately
|
||||
// via GET_NETWORK_STATE.
|
||||
#ifdef USE_WIFI
|
||||
if (wifi::global_wifi_component == nullptr || wifi::global_wifi_component->is_disabled()) {
|
||||
// Reported transiently without disturbing our internal provisioning state machine,
|
||||
// so a later `wifi.enable` still reports the correct state.
|
||||
this->send_current_state_(improv::STATE_STOPPED);
|
||||
return true;
|
||||
}
|
||||
@@ -265,14 +327,20 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
if (this->state_ == improv::STATE_PROVISIONED) {
|
||||
this->send_settings_response_(improv::GET_CURRENT_STATE);
|
||||
}
|
||||
#else
|
||||
this->send_current_state_(improv::STATE_STOPPED);
|
||||
#endif
|
||||
return true;
|
||||
}
|
||||
case improv::GET_DEVICE_INFO: {
|
||||
this->send_version_info_();
|
||||
return true;
|
||||
}
|
||||
case improv::GET_WIFI_NETWORKS: {
|
||||
const auto &results = wifi::global_wifi_component->get_scan_result();
|
||||
// Declared out here because the terminating empty response is sent with or without Wi-Fi
|
||||
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
|
||||
#ifdef USE_WIFI
|
||||
const auto &results = wifi::global_wifi_component->get_scan_result();
|
||||
for (const auto &scan : results) {
|
||||
bool with_auth = false;
|
||||
if (!wifi::should_show_scan_entry(results, scan, with_auth))
|
||||
@@ -289,11 +357,52 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
|
||||
builder.add_string(YESNO(with_auth));
|
||||
this->send_response_(builder.finish(false));
|
||||
}
|
||||
#endif // USE_WIFI
|
||||
// Send empty response to signify the end of the list.
|
||||
improv::RpcResponseBuilder builder(buf, improv::GET_WIFI_NETWORKS);
|
||||
this->send_response_(builder.finish(false));
|
||||
return true;
|
||||
}
|
||||
case improv::GET_NETWORK_STATE: {
|
||||
// Reports general device connectivity and which network interfaces are present, decoupled
|
||||
// from the Wi-Fi-only provisioning state machine. data[0] is a decimal flags byte;
|
||||
// when online, the reachable device URL(s) follow.
|
||||
uint8_t flags = 0;
|
||||
if (network::is_connected())
|
||||
flags |= improv::NETWORK_IS_ONLINE;
|
||||
#ifdef USE_WIFI
|
||||
flags |= improv::NETWORK_SUPPORTS_WIFI;
|
||||
#endif
|
||||
#ifdef USE_ETHERNET
|
||||
flags |= improv::NETWORK_SUPPORTS_ETHERNET;
|
||||
#endif
|
||||
#ifdef USE_OPENTHREAD
|
||||
flags |= improv::NETWORK_SUPPORTS_THREAD;
|
||||
#endif
|
||||
#ifdef USE_MODEM
|
||||
flags |= improv::NETWORK_SUPPORTS_MODEM;
|
||||
#endif
|
||||
std::array<uint8_t, improv::RPC_RESPONSE_MAX_SIZE> buf;
|
||||
improv::RpcResponseBuilder builder(buf, improv::GET_NETWORK_STATE);
|
||||
// Every flag bit fits int8_t's positive range, so int8_to_str renders the byte
|
||||
static_assert(improv::NETWORK_SUPPORTS_MODEM <= 0x7F, "network flags no longer fit int8_to_str");
|
||||
char flags_buf[4]; // uint8_t: max "255" + null
|
||||
char *flags_end = int8_to_str(flags_buf, static_cast<int8_t>(flags));
|
||||
builder.add_string(flags_buf, flags_end - flags_buf);
|
||||
#ifdef USE_WEBSERVER
|
||||
// Not tied to one interface, so follow the configured priority the way
|
||||
// network::get_ip_addresses() does: a wifi-first network priority list leads with Wi-Fi.
|
||||
if (flags & improv::NETWORK_IS_ONLINE) {
|
||||
#if defined(USE_NETWORK_PRIMARY_INTERFACE_WIFI) && defined(USE_WIFI)
|
||||
this->add_webserver_urls_(builder, /*wifi_first=*/true);
|
||||
#else
|
||||
this->add_webserver_urls_(builder, /*wifi_first=*/false);
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
this->send_response_(builder.finish(false));
|
||||
return true;
|
||||
}
|
||||
default: {
|
||||
ESP_LOGW(TAG, "Unknown payload");
|
||||
this->set_error_(improv::ERROR_UNKNOWN_RPC);
|
||||
@@ -331,12 +440,14 @@ void ImprovSerialComponent::send_response_(std::span<const uint8_t> response) {
|
||||
this->write_data_(response.data(), response.size());
|
||||
}
|
||||
|
||||
#ifdef USE_WIFI
|
||||
void ImprovSerialComponent::on_wifi_connect_timeout_() {
|
||||
this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
|
||||
this->set_state_(improv::STATE_AUTHORIZED);
|
||||
ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
|
||||
wifi::global_wifi_component->clear_sta();
|
||||
}
|
||||
#endif
|
||||
|
||||
ImprovSerialComponent *global_improv_serial_component = // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
@@ -2,15 +2,19 @@
|
||||
|
||||
#include "esphome/components/improv_base/improv_base.h"
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#include "esphome/components/network/util.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#ifdef USE_WIFI
|
||||
#ifdef USE_IMPROV_SERIAL
|
||||
#include <improv.h>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_WIFI
|
||||
#include "esphome/components/wifi/wifi_component.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_IMPROV_SERIAL_UART
|
||||
#include "esphome/components/uart/uart_component.h"
|
||||
#elif defined(USE_ESP32)
|
||||
@@ -48,13 +52,22 @@ enum ImprovSerialType : uint8_t {
|
||||
static const uint16_t IMPROV_SERIAL_TIMEOUT = 100;
|
||||
static const uint8_t IMPROV_SERIAL_VERSION = 1;
|
||||
|
||||
#ifdef USE_WIFI
|
||||
// Wi-Fi connect failure timers: a fresh provision reports at 30 s (stock behavior), while
|
||||
// switching networks on an already-connected device (disconnect + reconnect) can legitimately
|
||||
// take longer; 90 s matches esp32_improv's default wifi_timeout.
|
||||
static const uint32_t WIFI_CONNECT_TIMEOUT_MS = 30000;
|
||||
static const uint32_t WIFI_SWITCH_TIMEOUT_MS = 90000;
|
||||
#endif
|
||||
|
||||
// The serial frame length field is one byte
|
||||
static constexpr size_t MAX_SERIAL_RESPONSE = 255;
|
||||
// command + data length + trailing byte
|
||||
static constexpr size_t RPC_RESPONSE_OVERHEAD = 3;
|
||||
static constexpr size_t MAX_SERIAL_PAYLOAD = MAX_SERIAL_RESPONSE - RPC_RESPONSE_OVERHEAD;
|
||||
#ifdef USE_WEBSERVER
|
||||
// length byte + "http://" + IPv4 + ":" + port
|
||||
// length byte + "http://" + IPv4 + ":" + port. Reserves the first URL only; a device with
|
||||
// several interfaces online adds the rest best-effort and warns if one no longer fits.
|
||||
static constexpr size_t WEBSERVER_URL_RESERVE = 1 + 7 + 15 + 1 + 5;
|
||||
#else
|
||||
static constexpr size_t WEBSERVER_URL_RESERVE = 0;
|
||||
@@ -84,8 +97,15 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
|
||||
void send_current_state_(improv::State state);
|
||||
void set_error_(improv::Error error);
|
||||
void send_response_(std::span<const uint8_t> response);
|
||||
#ifdef USE_WIFI
|
||||
void on_wifi_connect_timeout_();
|
||||
#endif
|
||||
|
||||
#ifdef USE_WEBSERVER
|
||||
/// Append one web server URL per interface that has a usable IPv4. With wifi_first the Wi-Fi
|
||||
/// URL leads, for responses to Wi-Fi provisioning; otherwise interfaces go in priority order.
|
||||
void add_webserver_urls_(improv::RpcResponseBuilder &builder, [[maybe_unused]] bool wifi_first);
|
||||
#endif
|
||||
void send_settings_response_(improv::Command command);
|
||||
void send_version_info_();
|
||||
|
||||
@@ -167,7 +187,9 @@ class ImprovSerialComponent final : public Component, public improv_base::Improv
|
||||
|
||||
std::vector<uint8_t> rx_buffer_;
|
||||
uint32_t last_read_byte_{0};
|
||||
#ifdef USE_WIFI
|
||||
wifi::WiFiAP connecting_sta_;
|
||||
#endif
|
||||
improv::State state_{improv::STATE_AUTHORIZED};
|
||||
};
|
||||
|
||||
|
||||
@@ -209,7 +209,8 @@ void INA2XX::dump_config() {
|
||||
" CURRENT_LSB = %f\n"
|
||||
" SHUNT_CAL = %d",
|
||||
this->shunt_resistance_ohm_, this->max_current_a_, this->shunt_tempco_ppm_c_,
|
||||
(uint8_t) this->adc_range_, this->adc_range_ ? "±40.96 mV" : "±163.84 mV", this->current_lsb_,
|
||||
(uint8_t) this->adc_range_,
|
||||
this->adc_range_ ? LOG_STR_LITERAL("±40.96 mV") : LOG_STR_LITERAL("±163.84 mV"), this->current_lsb_,
|
||||
this->shunt_cal_);
|
||||
|
||||
ESP_LOGCONFIG(TAG, " ADC Samples = %d; ADC times: Bus = %d μs, Shunt = %d μs, Temp = %d μs",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import sensor
|
||||
from esphome.components.esp32 import get_esp32_variant, include_builtin_idf_component
|
||||
from esphome.components.esp32.const import VARIANT_ESP32
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
from esphome.config_helpers import filter_source_files_from_platform
|
||||
import esphome.config_validation as cv
|
||||
@@ -48,6 +50,10 @@ async def to_code(config: ConfigType) -> None:
|
||||
var = await sensor.new_sensor(config)
|
||||
await cg.register_component(var, config)
|
||||
|
||||
if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32:
|
||||
# temprature_sens_read() lives in the esp_phy blob, which is excluded by default
|
||||
include_builtin_idf_component("esp_phy")
|
||||
|
||||
if CORE.using_zephyr and CORE.is_nrf52:
|
||||
zephyr_add_prj_conf("SENSOR", True)
|
||||
zephyr_add_prj_conf("TEMP_NRF5", True)
|
||||
|
||||
@@ -740,7 +740,7 @@ bool IT8951Display::prepare_update_region_(UpdateMode &mode) {
|
||||
this->reset_dirty_region_();
|
||||
|
||||
ESP_LOGV(TAG, "Update: %ux%u@%u,%u mode=%u (%s)", width, height, x, y, static_cast<unsigned>(mode),
|
||||
this->grayscale_ ? "grayscale" : "mono");
|
||||
this->grayscale_ ? LOG_STR_LITERAL("grayscale") : LOG_STR_LITERAL("mono"));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1063,25 +1063,27 @@ void IT8951Display::dump_config() {
|
||||
strncpy(force_temperature, "(controller default)", sizeof(force_temperature));
|
||||
force_temperature[sizeof(force_temperature) - 1] = '\0';
|
||||
}
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Model preset: %s"
|
||||
"\n Dimensions: %dx%d"
|
||||
"\n Buffer: %u bytes"
|
||||
"\n Image buffer addr: 0x%04X%04X"
|
||||
"\n VCOM: %.02fV (set selector 0x%04X)"
|
||||
"\n Force temperature: %s"
|
||||
"\n Display command: %s"
|
||||
"\n Sleep when done: %s"
|
||||
"\n Full update every: %u"
|
||||
"\n Inverted colors: %s"
|
||||
"\n Pixel format: %s"
|
||||
"\n Reset duration: %" PRIu32 "ms",
|
||||
this->name_ != nullptr ? this->name_ : "(unknown)", this->get_width_internal(),
|
||||
this->get_height_internal(), static_cast<unsigned>(this->buffer_length_), this->img_buf_addr_h_,
|
||||
this->img_buf_addr_l_, static_cast<float>(this->vcom_) / 1000.0f, this->vcom_register_,
|
||||
force_temperature, this->use_legacy_dpy_area_ ? "DPY_AREA (0x0034, legacy)" : "DPY_BUF_AREA (0x0037)",
|
||||
YESNO(this->sleep_when_done_), this->full_update_every_, YESNO(this->invert_colors_),
|
||||
this->grayscale_ ? "4bpp grayscale" : "1bpp monochrome", this->reset_duration_);
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
" Model preset: %s"
|
||||
"\n Dimensions: %dx%d"
|
||||
"\n Buffer: %u bytes"
|
||||
"\n Image buffer addr: 0x%04X%04X"
|
||||
"\n VCOM: %.02fV (set selector 0x%04X)"
|
||||
"\n Force temperature: %s"
|
||||
"\n Display command: %s"
|
||||
"\n Sleep when done: %s"
|
||||
"\n Full update every: %u"
|
||||
"\n Inverted colors: %s"
|
||||
"\n Pixel format: %s"
|
||||
"\n Reset duration: %" PRIu32 "ms",
|
||||
this->name_ != nullptr ? this->name_ : LOG_STR_LITERAL("(unknown)"), this->get_width_internal(),
|
||||
this->get_height_internal(), static_cast<unsigned>(this->buffer_length_), this->img_buf_addr_h_,
|
||||
this->img_buf_addr_l_, static_cast<float>(this->vcom_) / 1000.0f, this->vcom_register_, force_temperature,
|
||||
this->use_legacy_dpy_area_ ? LOG_STR_LITERAL("DPY_AREA (0x0034, legacy)")
|
||||
: LOG_STR_LITERAL("DPY_BUF_AREA (0x0037)"),
|
||||
YESNO(this->sleep_when_done_), this->full_update_every_, YESNO(this->invert_colors_),
|
||||
this->grayscale_ ? LOG_STR_LITERAL("4bpp grayscale") : LOG_STR_LITERAL("1bpp monochrome"), this->reset_duration_);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" Busy Pin: ", this->busy_pin_);
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
|
||||
@@ -16,26 +16,33 @@ void KeyCollector::loop() {
|
||||
void KeyCollector::dump_config() {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_CONFIG
|
||||
ESP_LOGCONFIG(TAG, "Key Collector:");
|
||||
if (this->min_length_ > 0)
|
||||
if (this->min_length_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " min length: %d", this->min_length_);
|
||||
if (this->max_length_ > 0)
|
||||
}
|
||||
if (this->max_length_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " max length: %d", this->max_length_);
|
||||
if (!this->back_keys_.empty())
|
||||
}
|
||||
if (!this->back_keys_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " erase keys '%s'", this->back_keys_.c_str());
|
||||
if (!this->clear_keys_.empty())
|
||||
}
|
||||
if (!this->clear_keys_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " clear keys '%s'", this->clear_keys_.c_str());
|
||||
if (!this->start_keys_.empty())
|
||||
}
|
||||
if (!this->start_keys_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " start keys '%s'", this->start_keys_.c_str());
|
||||
}
|
||||
if (!this->end_keys_.empty()) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" end keys '%s'\n"
|
||||
" end key is required: %s",
|
||||
this->end_keys_.c_str(), ONOFF(this->end_key_required_));
|
||||
}
|
||||
if (!this->allowed_keys_.empty())
|
||||
if (!this->allowed_keys_.empty()) {
|
||||
ESP_LOGCONFIG(TAG, " allowed keys '%s'", this->allowed_keys_.c_str());
|
||||
if (this->timeout_ > 0)
|
||||
}
|
||||
if (this->timeout_ > 0) {
|
||||
ESP_LOGCONFIG(TAG, " entry timeout: %0.1f", this->timeout_ / 1000.0);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -1,87 +1,74 @@
|
||||
#include "kuntze.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/application.h"
|
||||
|
||||
namespace esphome::kuntze {
|
||||
|
||||
static const char *const TAG = "kuntze";
|
||||
|
||||
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
|
||||
static constexpr uint16_t REGISTER_PH = 4136;
|
||||
static constexpr uint16_t REGISTER_TEMPERATURE = 4160;
|
||||
static constexpr uint16_t REGISTER_DIS1 = 4680;
|
||||
static constexpr uint16_t REGISTER_DIS2 = 6000;
|
||||
static constexpr uint16_t REGISTER_REDOX = 4688;
|
||||
static constexpr uint16_t REGISTER_EC = 4728;
|
||||
static constexpr uint16_t REGISTER_OCI = 5832;
|
||||
static constexpr uint16_t REGISTER[] = {REGISTER_PH, REGISTER_TEMPERATURE, REGISTER_DIS1, REGISTER_DIS2,
|
||||
REGISTER_REDOX, REGISTER_EC, REGISTER_OCI};
|
||||
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
|
||||
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
|
||||
void Kuntze::on_read_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) {
|
||||
if (!modbus::succeeded(status) || registers.size() < 2)
|
||||
return;
|
||||
|
||||
void Kuntze::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);
|
||||
auto get_16bit = [&](int i) -> uint16_t { return (uint16_t(data[i * 2]) << 8) | uint16_t(data[i * 2 + 1]); };
|
||||
// Each value is a register pair: the reading, then the number of decimal places in its low byte.
|
||||
float value = registers[0];
|
||||
for (uint16_t i = 0; i < (registers[1] & 0xFF); i++)
|
||||
value /= 10.0f;
|
||||
|
||||
this->waiting_ = false;
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(KUNTZE_MAX_LOG_BYTES)];
|
||||
#endif
|
||||
ESP_LOGV(TAG, "Data: %s", format_hex_pretty_to(hex_buf, data.data(), data.size()));
|
||||
|
||||
float value = (float) get_16bit(0);
|
||||
for (int i = 0; i < data[3]; i++)
|
||||
value /= 10.0;
|
||||
switch (this->state_) {
|
||||
case 1:
|
||||
switch (start_address) {
|
||||
case REGISTER_PH:
|
||||
ESP_LOGD(TAG, "pH=%.1f", value);
|
||||
if (this->ph_sensor_ != nullptr)
|
||||
this->ph_sensor_->publish_state(value);
|
||||
break;
|
||||
case 2:
|
||||
case REGISTER_TEMPERATURE:
|
||||
ESP_LOGD(TAG, "temperature=%.1f", value);
|
||||
if (this->temperature_sensor_ != nullptr)
|
||||
this->temperature_sensor_->publish_state(value);
|
||||
break;
|
||||
case 3:
|
||||
case REGISTER_DIS1:
|
||||
ESP_LOGD(TAG, "DIS1=%.1f", value);
|
||||
if (this->dis1_sensor_ != nullptr)
|
||||
this->dis1_sensor_->publish_state(value);
|
||||
break;
|
||||
case 4:
|
||||
case REGISTER_DIS2:
|
||||
ESP_LOGD(TAG, "DIS2=%.1f", value);
|
||||
if (this->dis2_sensor_ != nullptr)
|
||||
this->dis2_sensor_->publish_state(value);
|
||||
break;
|
||||
case 5:
|
||||
case REGISTER_REDOX:
|
||||
ESP_LOGD(TAG, "REDOX=%.1f", value);
|
||||
if (this->redox_sensor_ != nullptr)
|
||||
this->redox_sensor_->publish_state(value);
|
||||
break;
|
||||
case 6:
|
||||
case REGISTER_EC:
|
||||
ESP_LOGD(TAG, "EC=%.1f", value);
|
||||
if (this->ec_sensor_ != nullptr)
|
||||
this->ec_sensor_->publish_state(value);
|
||||
break;
|
||||
case 7:
|
||||
case REGISTER_OCI:
|
||||
ESP_LOGD(TAG, "OCI=%.1f", value);
|
||||
if (this->oci_sensor_ != nullptr)
|
||||
this->oci_sensor_->publish_state(value);
|
||||
break;
|
||||
}
|
||||
if (++this->state_ > 7)
|
||||
this->state_ = 0;
|
||||
}
|
||||
|
||||
void Kuntze::loop() {
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
// timeout after 15 seconds
|
||||
if (this->waiting_ && (now - this->last_send_ > 15000)) {
|
||||
ESP_LOGW(TAG, "timed out waiting for response");
|
||||
this->waiting_ = false;
|
||||
}
|
||||
if (this->waiting_ || (this->state_ == 0))
|
||||
return;
|
||||
this->last_send_ = now;
|
||||
this->read_holding_registers(REGISTER[this->state_ - 1], 2);
|
||||
this->waiting_ = true;
|
||||
void Kuntze::update() {
|
||||
for (uint16_t reg : REGISTER)
|
||||
this->read_holding_registers(reg, 2);
|
||||
}
|
||||
|
||||
void Kuntze::update() { this->state_ = 1; }
|
||||
|
||||
void Kuntze::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"Kuntze:\n"
|
||||
|
||||
@@ -18,18 +18,14 @@ class Kuntze final : public PollingComponent, public modbus::ModbusClientDevice
|
||||
void set_ec_sensor(sensor::Sensor *ec_sensor) { ec_sensor_ = ec_sensor; }
|
||||
void set_oci_sensor(sensor::Sensor *oci_sensor) { oci_sensor_ = oci_sensor; }
|
||||
|
||||
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_holding_registers(uint16_t start_address, std::span<const uint16_t> registers,
|
||||
modbus::ResponseStatus status) override;
|
||||
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
int state_{0};
|
||||
bool waiting_{false};
|
||||
uint32_t last_send_{0};
|
||||
|
||||
sensor::Sensor *ph_sensor_{nullptr};
|
||||
sensor::Sensor *temperature_sensor_{nullptr};
|
||||
sensor::Sensor *dis1_sensor_{nullptr};
|
||||
|
||||
@@ -150,7 +150,8 @@ void Lc709203f::dump_config() {
|
||||
" Pack Size: %d mAH\n"
|
||||
" Pack APA: 0x%02X\n"
|
||||
" Pack Rated Voltage: 3.%sV",
|
||||
this->pack_size_, this->apa_, this->pack_voltage_ == 0x0000 ? "8" : "7");
|
||||
this->pack_size_, this->apa_,
|
||||
this->pack_voltage_ == 0x0000 ? LOG_STR_LITERAL("8") : LOG_STR_LITERAL("7"));
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
LOG_SENSOR(" ", "Voltage", this->voltage_sensor_);
|
||||
|
||||
@@ -701,7 +701,8 @@ uint8_t LD2420Component::set_config_mode(bool enable) {
|
||||
cmd_frame.data_length += sizeof(CMD_PROTOCOL_VER);
|
||||
}
|
||||
cmd_frame.footer = CMD_FRAME_FOOTER;
|
||||
ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? "enable" : "disable", cmd_frame.command);
|
||||
ESP_LOGV(TAG, "Sending set config %s command: %2X", enable ? LOG_STR_LITERAL("enable") : LOG_STR_LITERAL("disable"),
|
||||
cmd_frame.command);
|
||||
return this->send_cmd_from_array(cmd_frame);
|
||||
}
|
||||
|
||||
|
||||
@@ -437,7 +437,8 @@ void LD6002BComponent::dump_config() {
|
||||
"HLK-LD6002B:\n"
|
||||
" Auto wake: %s\n"
|
||||
" Max data length: %u",
|
||||
this->auto_wake_ ? "true" : "false", static_cast<unsigned>(this->max_data_len_));
|
||||
this->auto_wake_ ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"),
|
||||
static_cast<unsigned>(this->max_data_len_));
|
||||
if (this->wakeup_pin_ != nullptr) {
|
||||
LOG_PIN(" Wake-up Pin: ", this->wakeup_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Wake Pulse: %" PRIu32 "ms", this->wakeup_pulse_ms_);
|
||||
|
||||
@@ -5,7 +5,11 @@ namespace esphome::light {
|
||||
uint8_t ESPColorCorrection::gamma_correct_(uint8_t value) const {
|
||||
if (this->gamma_table_ == nullptr)
|
||||
return value;
|
||||
return static_cast<uint8_t>((progmem_read_uint16(&this->gamma_table_[value]) + 128) / 257);
|
||||
uint16_t table_value = progmem_read_uint16(&this->gamma_table_[value]);
|
||||
uint8_t result = (table_value + 128) / 257;
|
||||
if (result == 0 && table_value != 0)
|
||||
return 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
uint8_t ESPColorCorrection::gamma_uncorrect_(uint8_t value) const {
|
||||
|
||||
@@ -333,8 +333,9 @@ void LN882HBLE::loop() {
|
||||
// the queue empty — from the very first report on. Checking here keeps that
|
||||
// failure visible instead of producing a scanner that is silently dead.
|
||||
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
|
||||
if (dropped > 0)
|
||||
if (dropped > 0) {
|
||||
ESP_LOGW(TAG, "Dropped %u scan reports (queue full or out of memory for a report slot)", dropped);
|
||||
}
|
||||
// Drain the lock-free ring filled by the rw task; all per-report work runs
|
||||
// here on the main task, then the report returns to the pool.
|
||||
BLEScanReport *report = this->report_queue_.pop();
|
||||
|
||||
@@ -483,6 +483,7 @@ LV_ANIM = LvConstant(
|
||||
|
||||
LV_GRAD_DIR = LvConstant("LV_GRAD_DIR_", "NONE", "HOR", "VER")
|
||||
LV_DITHER = LvConstant("LV_DITHER_", "NONE", "ORDERED", "ERR_DIFF")
|
||||
LV_GRAD_EXTEND = LvConstant("LV_GRAD_EXTEND_", "PAD", "REPEAT", "REFLECT")
|
||||
|
||||
LV_LOG_LEVELS = {
|
||||
"VERBOSE": "TRACE",
|
||||
@@ -627,10 +628,6 @@ OBJ_FLAGS = (
|
||||
"send_draw_task_events",
|
||||
"widget_1",
|
||||
"widget_2",
|
||||
"user_1",
|
||||
"user_2",
|
||||
"user_3",
|
||||
"user_4",
|
||||
)
|
||||
LV_OBJ_FLAG = LvConstant("LV_OBJ_FLAG_", *OBJ_FLAGS)
|
||||
|
||||
@@ -904,7 +901,7 @@ LV_COLOR_FORMATS = (
|
||||
|
||||
LV_DEFINES = (
|
||||
"LV_USE_FREERTOS_TASK_NOTIFY", "LV_DRAW_BUF_STRIDE_ALIGN", "LV_USE_DRAW_SW", "LV_DRAW_SW_DRAW_UNIT_CNT",
|
||||
"LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D",
|
||||
"LV_DRAW_SW_COMPLEX", "LV_USE_DRAW_SW_COMPLEX_GRADIENTS", "LV_USE_DRAW_PXP", "LV_USE_PXP_DRAW_THREAD", "LV_USE_DRAW_G2D",
|
||||
"LV_USE_G2D_DRAW_THREAD", "LV_VG_LITE_USE_BOX_SHADOW", "LV_VG_LITE_THORVG_16PIXELS_ALIGN", "LV_LOG_USE_TIMESTAMP",
|
||||
"LV_LOG_USE_FILE_LINE", "LV_USE_OBJ_ID_BUILTIN", "LV_USE_OBJ_PROPERTY_NAME", "LV_ATTRIBUTE_MEM_ALIGN_SIZE",
|
||||
"LV_FONT_MONTSERRAT_14", "LV_USE_FONT_PLACEHOLDER", "LV_WIDGETS_HAS_DEFAULT_VALUE", "LV_USE_ARCLABEL",
|
||||
|
||||
@@ -13,18 +13,40 @@ from esphome.core import ID
|
||||
from esphome.cpp_generator import MockObj
|
||||
|
||||
from .defines import (
|
||||
CONF_END_ANGLE,
|
||||
CONF_GRADIENTS,
|
||||
CONF_OPA,
|
||||
CONF_START_ANGLE,
|
||||
LV_DITHER,
|
||||
LV_GRAD_EXTEND,
|
||||
add_define,
|
||||
add_lv_use,
|
||||
add_warning,
|
||||
)
|
||||
from .lv_validation import lv_color, lv_percentage, opacity
|
||||
from .lv_validation import (
|
||||
lv_angle_degrees,
|
||||
lv_color,
|
||||
lv_percentage,
|
||||
opacity,
|
||||
pixels_or_percent,
|
||||
)
|
||||
from .lvcode import lv
|
||||
from .types import lv_color_t, lv_gradient_t, lv_opa_t
|
||||
|
||||
CONF_STOPS = "stops"
|
||||
CONF_LINEAR = "linear"
|
||||
CONF_RADIAL = "radial"
|
||||
CONF_CONICAL = "conical"
|
||||
CONF_EXTEND = "extend"
|
||||
CONF_FROM_X = "from_x"
|
||||
CONF_FROM_Y = "from_y"
|
||||
CONF_TO_X = "to_x"
|
||||
CONF_TO_Y = "to_y"
|
||||
CONF_CENTER_X = "center_x"
|
||||
CONF_CENTER_Y = "center_y"
|
||||
CONF_FOCAL_X = "focal_x"
|
||||
CONF_FOCAL_Y = "focal_y"
|
||||
CONF_FOCAL_RADIUS = "focal_radius"
|
||||
|
||||
|
||||
def min_stops(value):
|
||||
@@ -33,27 +55,109 @@ def min_stops(value):
|
||||
return value
|
||||
|
||||
|
||||
STOPS_SCHEMA = cv.All(
|
||||
[
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_COLOR): lv_color,
|
||||
cv.Optional(CONF_OPA, default=1.0): opacity,
|
||||
cv.Required(CONF_POSITION): lv_percentage,
|
||||
}
|
||||
)
|
||||
],
|
||||
min_stops,
|
||||
)
|
||||
|
||||
LINEAR_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_FROM_X): pixels_or_percent,
|
||||
cv.Required(CONF_FROM_Y): pixels_or_percent,
|
||||
cv.Required(CONF_TO_X): pixels_or_percent,
|
||||
cv.Required(CONF_TO_Y): pixels_or_percent,
|
||||
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
|
||||
}
|
||||
)
|
||||
|
||||
RADIAL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CENTER_X): pixels_or_percent,
|
||||
cv.Required(CONF_CENTER_Y): pixels_or_percent,
|
||||
cv.Required(CONF_TO_X): pixels_or_percent,
|
||||
cv.Required(CONF_TO_Y): pixels_or_percent,
|
||||
cv.Optional(CONF_FOCAL_X): pixels_or_percent,
|
||||
cv.Optional(CONF_FOCAL_Y): pixels_or_percent,
|
||||
# No default: gradient_validator() must be able to tell whether this was actually
|
||||
# given, to require it alongside focal_x/focal_y rather than silently drop it.
|
||||
# LVGL's lv_grad_radial_set_focal() takes this as a scalar, not lv_pct() -
|
||||
# unlike every other coordinate here, a percentage is not accepted.
|
||||
cv.Optional(CONF_FOCAL_RADIUS): cv.positive_int,
|
||||
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
|
||||
}
|
||||
)
|
||||
|
||||
CONICAL_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_CENTER_X): pixels_or_percent,
|
||||
cv.Required(CONF_CENTER_Y): pixels_or_percent,
|
||||
cv.Optional(CONF_START_ANGLE, default=0): lv_angle_degrees,
|
||||
cv.Optional(CONF_END_ANGLE, default=360): lv_angle_degrees,
|
||||
cv.Optional(CONF_EXTEND, default="PAD"): LV_GRAD_EXTEND.one_of,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def gradient_validator(config):
|
||||
direction = config[CONF_DIRECTION]
|
||||
for gradient_direction, key in (
|
||||
("LINEAR", CONF_LINEAR),
|
||||
("RADIAL", CONF_RADIAL),
|
||||
("CONICAL", CONF_CONICAL),
|
||||
):
|
||||
if direction == gradient_direction:
|
||||
if key not in config:
|
||||
raise cv.Invalid(
|
||||
f"'{key}' is required for {gradient_direction} gradient direction"
|
||||
)
|
||||
elif key in config:
|
||||
raise cv.Invalid(
|
||||
f"'{key}' is only valid with 'direction: {gradient_direction}'"
|
||||
)
|
||||
if CONF_RADIAL in config:
|
||||
radial = config[CONF_RADIAL]
|
||||
has_focal_x = CONF_FOCAL_X in radial
|
||||
has_focal_y = CONF_FOCAL_Y in radial
|
||||
has_focal_radius = CONF_FOCAL_RADIUS in radial
|
||||
if has_focal_x != has_focal_y or (has_focal_radius and not has_focal_x):
|
||||
raise cv.Invalid(
|
||||
"'focal_x', 'focal_y' and 'focal_radius' must be specified together "
|
||||
"in 'radial'"
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
GRADIENT_SCHEMA = cv.ensure_list(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
|
||||
cv.Required(CONF_DIRECTION): cv.one_of(
|
||||
"HOR", "HORIZONTAL", "VER", "VERTICAL", upper=True
|
||||
),
|
||||
cv.Optional(CONF_DITHER): LV_DITHER.one_of,
|
||||
cv.Required(CONF_STOPS): cv.All(
|
||||
[
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_COLOR): lv_color,
|
||||
cv.Optional(CONF_OPA, default=1.0): opacity,
|
||||
cv.Required(CONF_POSITION): lv_percentage,
|
||||
}
|
||||
)
|
||||
],
|
||||
min_stops,
|
||||
),
|
||||
}
|
||||
cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.GenerateID(CONF_ID): cv.declare_id(lv_gradient_t),
|
||||
cv.Required(CONF_DIRECTION): cv.one_of(
|
||||
"HOR",
|
||||
"HORIZONTAL",
|
||||
"VER",
|
||||
"VERTICAL",
|
||||
"LINEAR",
|
||||
"RADIAL",
|
||||
"CONICAL",
|
||||
upper=True,
|
||||
),
|
||||
cv.Optional(CONF_DITHER): LV_DITHER.one_of,
|
||||
cv.Optional(CONF_LINEAR): LINEAR_SCHEMA,
|
||||
cv.Optional(CONF_RADIAL): RADIAL_SCHEMA,
|
||||
cv.Optional(CONF_CONICAL): CONICAL_SCHEMA,
|
||||
cv.Required(CONF_STOPS): STOPS_SCHEMA,
|
||||
}
|
||||
),
|
||||
gradient_validator,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -65,15 +169,60 @@ async def gradients_to_code(config):
|
||||
add_warning(
|
||||
"The 'dither' option for gradients is not supported by LVGL 9.x and will be ignored"
|
||||
)
|
||||
if any(
|
||||
x[CONF_DIRECTION] in ("LINEAR", "RADIAL", "CONICAL")
|
||||
for x in config.get(CONF_GRADIENTS, ())
|
||||
):
|
||||
# LVGL's software renderer only draws these gradient types when this is enabled; without
|
||||
# it they silently fall back to a plain horizontal gradient.
|
||||
add_define("LV_USE_DRAW_SW_COMPLEX_GRADIENTS")
|
||||
for gradient in config.get(CONF_GRADIENTS, ()):
|
||||
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
|
||||
idbase = gradient[CONF_ID].id
|
||||
stops = sorted(gradient[CONF_STOPS], key=itemgetter(CONF_POSITION))
|
||||
max_stops = max(max_stops, len(stops))
|
||||
if gradient[CONF_DIRECTION].startswith("VER"):
|
||||
direction = gradient[CONF_DIRECTION]
|
||||
if direction.startswith("VER"):
|
||||
lv.grad_vertical_init(var)
|
||||
else:
|
||||
elif direction.startswith("HOR"):
|
||||
lv.grad_horizontal_init(var)
|
||||
elif direction == "LINEAR":
|
||||
linear = gradient[CONF_LINEAR]
|
||||
lv.grad_linear_init(
|
||||
var,
|
||||
await pixels_or_percent.process(linear[CONF_FROM_X]),
|
||||
await pixels_or_percent.process(linear[CONF_FROM_Y]),
|
||||
await pixels_or_percent.process(linear[CONF_TO_X]),
|
||||
await pixels_or_percent.process(linear[CONF_TO_Y]),
|
||||
await LV_GRAD_EXTEND.process(linear[CONF_EXTEND]),
|
||||
)
|
||||
elif direction == "RADIAL":
|
||||
radial = gradient[CONF_RADIAL]
|
||||
lv.grad_radial_init(
|
||||
var,
|
||||
await pixels_or_percent.process(radial[CONF_CENTER_X]),
|
||||
await pixels_or_percent.process(radial[CONF_CENTER_Y]),
|
||||
await pixels_or_percent.process(radial[CONF_TO_X]),
|
||||
await pixels_or_percent.process(radial[CONF_TO_Y]),
|
||||
await LV_GRAD_EXTEND.process(radial[CONF_EXTEND]),
|
||||
)
|
||||
if CONF_FOCAL_X in radial:
|
||||
lv.grad_radial_set_focal(
|
||||
var,
|
||||
await pixels_or_percent.process(radial[CONF_FOCAL_X]),
|
||||
await pixels_or_percent.process(radial[CONF_FOCAL_Y]),
|
||||
radial.get(CONF_FOCAL_RADIUS, 0),
|
||||
)
|
||||
elif direction == "CONICAL":
|
||||
conical = gradient[CONF_CONICAL]
|
||||
lv.grad_conical_init(
|
||||
var,
|
||||
await pixels_or_percent.process(conical[CONF_CENTER_X]),
|
||||
await pixels_or_percent.process(conical[CONF_CENTER_Y]),
|
||||
await lv_angle_degrees.process(conical[CONF_START_ANGLE]),
|
||||
await lv_angle_degrees.process(conical[CONF_END_ANGLE]),
|
||||
await LV_GRAD_EXTEND.process(conical[CONF_EXTEND]),
|
||||
)
|
||||
stop_colors = cg.static_const_array(
|
||||
ID(idbase + "_colors_", type=lv_color_t),
|
||||
[await lv_color.process(x[CONF_COLOR]) for x in stops],
|
||||
|
||||
@@ -1059,8 +1059,9 @@ static void *lv_alloc_draw_buf(size_t size, bool internal) {
|
||||
void *buffer;
|
||||
size = LV_ROUND_UP(size, LV_DRAW_BUF_ALIGN);
|
||||
buffer = heap_caps_aligned_alloc(LV_DRAW_BUF_ALIGN, size, internal ? MALLOC_CAP_8BIT : cap_bits); // NOLINT
|
||||
if (buffer == nullptr)
|
||||
if (buffer == nullptr) {
|
||||
ESP_LOGW(esphome::lvgl::TAG, "Failed to allocate %zu bytes for %sdraw buffer", size, internal ? "internal " : "");
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ from contextlib import ExitStack
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_ROWS
|
||||
from esphome.components.const import CONF_COLUMNS, CONF_ROWS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ITEMS, CONF_ROW, CONF_TEXT, CONF_WIDTH
|
||||
from esphome.core import ID
|
||||
@@ -20,7 +20,6 @@ from .label import CONF_LABEL
|
||||
|
||||
CONF_TABLE = "table"
|
||||
CONF_CELLS = "cells"
|
||||
CONF_COLUMNS = "columns"
|
||||
CONF_ROW_COUNT = "row_count"
|
||||
CONF_COLUMN_COUNT = "column_count"
|
||||
CONF_MERGE_RIGHT = "merge_right"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from esphome import automation, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import key_provider
|
||||
from esphome.components.const import CONF_ROWS
|
||||
from esphome.components.const import CONF_COLUMNS, CONF_KEYS, CONF_ROWS
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_ON_KEY, CONF_PIN, CONF_TRIGGER_ID
|
||||
from esphome.types import ConfigType
|
||||
@@ -21,8 +21,6 @@ MatrixKeyTrigger = matrix_keypad_ns.class_(
|
||||
)
|
||||
|
||||
CONF_KEYPAD_ID = "keypad_id"
|
||||
CONF_COLUMNS = "columns"
|
||||
CONF_KEYS = "keys"
|
||||
CONF_DEBOUNCE_TIME = "debounce_time"
|
||||
CONF_HAS_DIODES = "has_diodes"
|
||||
CONF_HAS_PULLDOWNS = "has_pulldowns"
|
||||
|
||||
@@ -23,8 +23,10 @@ void MAX31856Sensor::setup() {
|
||||
void MAX31856Sensor::dump_config() {
|
||||
LOG_SENSOR("", "MAX31856", this);
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
ESP_LOGCONFIG(TAG, " Mains Filter: %s",
|
||||
(filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!")));
|
||||
ESP_LOGCONFIG(
|
||||
TAG, " Mains Filter: %s",
|
||||
(filter_ == FILTER_60HZ ? LOG_STR_LITERAL("60 Hz")
|
||||
: (filter_ == FILTER_50HZ ? LOG_STR_LITERAL("50 Hz") : LOG_STR_LITERAL("Unknown!"))));
|
||||
if (this->thermocouple_type_ < 0 || this->thermocouple_type_ > 7) {
|
||||
ESP_LOGCONFIG(TAG, " Thermocouple Type: Unknown");
|
||||
} else {
|
||||
|
||||
@@ -80,12 +80,14 @@ void MAX31865Sensor::dump_config() {
|
||||
LOG_SENSOR("", "MAX31865", this);
|
||||
LOG_PIN(" CS Pin: ", this->cs_);
|
||||
LOG_UPDATE_INTERVAL(this);
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Reference Resistance: %.2fΩ\n"
|
||||
" RTD: %u-wire %.2fΩ\n"
|
||||
" Mains Filter: %s",
|
||||
reference_resistance_, rtd_wires_, rtd_nominal_resistance_,
|
||||
(filter_ == FILTER_60HZ ? "60 Hz" : (filter_ == FILTER_50HZ ? "50 Hz" : "Unknown!")));
|
||||
ESP_LOGCONFIG(
|
||||
TAG,
|
||||
" Reference Resistance: %.2fΩ\n"
|
||||
" RTD: %u-wire %.2fΩ\n"
|
||||
" Mains Filter: %s",
|
||||
reference_resistance_, rtd_wires_, rtd_nominal_resistance_,
|
||||
(filter_ == FILTER_60HZ ? LOG_STR_LITERAL("60 Hz")
|
||||
: (filter_ == FILTER_50HZ ? LOG_STR_LITERAL("50 Hz") : LOG_STR_LITERAL("Unknown!"))));
|
||||
}
|
||||
|
||||
void MAX31865Sensor::read_data_() {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user