mirror of
https://github.com/esphome/esphome.git
synced 2026-08-31 01:56:01 +00:00
Merge remote-tracking branch 'origin/dev' into integration
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
# Normalize line endings to LF in the repository
|
||||
* text eol=lf
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.apng binary
|
||||
|
||||
@@ -32,9 +32,12 @@ runs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
|
||||
# downstream jobs rely on is preserved.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
# reuse; dev pushes seed the shared copy instead.
|
||||
save-cache: ${{ github.event_name != 'pull_request' }}
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
# manifest from raw.githubusercontent.com on every cache
|
||||
# miss; that fetch flakes on Windows runners.
|
||||
|
||||
@@ -29,9 +29,12 @@ jobs:
|
||||
- name: Set up uv
|
||||
# ``--system`` (below) installs into the setup-python interpreter;
|
||||
# no venv is created or restored by this workflow.
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull-request-only workflow: a save could never be shared and
|
||||
# would only consume quota.
|
||||
save-cache: "false"
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
# manifest from raw.githubusercontent.com on every cache
|
||||
# miss; that fetch flakes on Windows runners.
|
||||
|
||||
+88
-14
@@ -49,9 +49,12 @@ jobs:
|
||||
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
|
||||
# that ``. venv/bin/activate`` see an identical layout.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
# reuse; dev pushes seed the shared copy instead.
|
||||
save-cache: ${{ github.event_name != 'pull_request' }}
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
# manifest from raw.githubusercontent.com on every cache
|
||||
# miss; that fetch flakes on Windows runners.
|
||||
@@ -171,9 +174,12 @@ jobs:
|
||||
# install step (order-of-magnitude faster on cold boots,
|
||||
# with its own wheel cache). actions/setup-python still
|
||||
# provides the interpreter.
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
# reuse; dev pushes seed the shared copy instead.
|
||||
save-cache: ${{ github.event_name != 'pull_request' }}
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
# manifest from raw.githubusercontent.com on every cache
|
||||
# miss; that fetch flakes on Windows runners.
|
||||
@@ -372,9 +378,12 @@ jobs:
|
||||
- name: Set up uv
|
||||
# Only needed on cache miss to populate the venv.
|
||||
if: steps.cache-venv.outputs.cache-hit != 'true'
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pull request saves land in per-PR scopes nothing else can
|
||||
# reuse; dev pushes seed the shared copy instead.
|
||||
save-cache: ${{ github.event_name != 'pull_request' }}
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
# manifest from raw.githubusercontent.com on every cache
|
||||
# miss; that fetch flakes on Windows runners.
|
||||
@@ -456,7 +465,7 @@ jobs:
|
||||
echo "binary=$BINARY" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Run CodSpeed benchmarks
|
||||
uses: CodSpeedHQ/action@a4a36bb07c0638b0b4ca52bf1f3dad1b4289e52f # v4.18.1
|
||||
uses: CodSpeedHQ/action@f99becdce5e5d51fd556489ebef684f4ecfd6286 # v4.18.5
|
||||
with:
|
||||
run: |
|
||||
. venv/bin/activate
|
||||
@@ -495,6 +504,15 @@ jobs:
|
||||
options: --environment nrf52-tidy --grep USE_ZEPHYR --grep USE_NRF52
|
||||
cache_sdk_nrf: true
|
||||
ignore_errors: false
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for RP2
|
||||
options: --environment rp2-tidy --grep USE_RP2
|
||||
pio_cache_key: tidyrp2
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for LibreTiny
|
||||
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
|
||||
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
|
||||
pio_cache_key: tidylibretiny
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
@@ -558,10 +576,21 @@ jobs:
|
||||
. venv/bin/activate
|
||||
if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
|
||||
echo "Running FULL clang-tidy scan (reason: ${{ steps.check_full_scan.outputs.reason }})"
|
||||
script/clang-tidy --all-headers --fix ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||
changed=""
|
||||
else
|
||||
echo "Running clang-tidy on changed files only"
|
||||
script/clang-tidy --all-headers --fix --changed ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||
changed="--changed"
|
||||
fi
|
||||
if [ -n "${{ matrix.environments }}" ]; then
|
||||
rc=0
|
||||
for env in ${{ matrix.environments }}; do
|
||||
echo "::group::clang-tidy $env"
|
||||
script/clang-tidy --all-headers --fix $changed --environment "$env" ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }} || rc=1
|
||||
echo "::endgroup::"
|
||||
done
|
||||
exit $rc
|
||||
else
|
||||
script/clang-tidy --all-headers --fix $changed ${{ matrix.options }} ${{ matrix.ignore_errors && '|| true' || '' }}
|
||||
fi
|
||||
env:
|
||||
# Also cache libdeps, store them in a ~/.platformio subfolder
|
||||
@@ -735,7 +764,8 @@ jobs:
|
||||
include:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 S3
|
||||
options: --environment esp32s3-idf-tidy --grep USE_ESP32_VARIANT_ESP32S3
|
||||
# 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
|
||||
- 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,
|
||||
@@ -745,7 +775,7 @@ jobs:
|
||||
- id: clang-tidy
|
||||
name: Run script/clang-tidy for ESP32 C6
|
||||
# yamllint disable-line rule:line-length
|
||||
options: --environment esp32c6-idf-tidy --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
|
||||
options: --environment esp32c6-idf-tidy --grep SOC_LP_I2C_SUPPORTED --grep USE_ESP32_VARIANT_ESP32C6 --grep USE_OPENTHREAD --grep USE_ZIGBEE
|
||||
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
@@ -828,11 +858,12 @@ jobs:
|
||||
- name: List components
|
||||
run: echo ${{ matrix.batch.components }}
|
||||
|
||||
- name: Cache apt packages
|
||||
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
|
||||
with:
|
||||
packages: libsdl2-dev ccache
|
||||
version: 1.1
|
||||
- name: Install apt packages
|
||||
# Not cached: this job is pull-request-only, so a cache save could
|
||||
# never be shared and would only consume quota.
|
||||
run: |
|
||||
sudo apt-get update -qq
|
||||
sudo apt-get install -y --no-install-recommends libsdl2-dev ccache
|
||||
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
@@ -1006,6 +1037,36 @@ jobs:
|
||||
# Arduino framework via PlatformIO (only components with an esp32-ard test are built):
|
||||
python3 script/test_build_components.py -e compile -t esp32-ard -c "$TEST_COMPONENTS" -f --toolchain platformio
|
||||
|
||||
pre-commit-seed-cache:
|
||||
name: Seed pre-commit cache
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- common
|
||||
# Saves a dev-scoped pre-commit cache that pull request runs can
|
||||
# restore, since pre-commit.ci lite itself never runs on dev pushes.
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
|
||||
steps:
|
||||
- name: Check out code from GitHub
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- name: Restore Python
|
||||
uses: ./.github/actions/restore-python
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- name: Cache pre-commit environments
|
||||
id: cache-pre-commit
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
# Must match the restore key in pre-commit-ci-lite
|
||||
# yamllint disable-line rule:line-length
|
||||
key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Install pre-commit hook environments
|
||||
if: steps.cache-pre-commit.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
python -m pip install pre-commit
|
||||
pre-commit install-hooks
|
||||
|
||||
pre-commit-ci-lite:
|
||||
name: pre-commit.ci lite
|
||||
runs-on: ubuntu-latest
|
||||
@@ -1021,9 +1082,22 @@ jobs:
|
||||
with:
|
||||
python-version: ${{ env.DEFAULT_PYTHON }}
|
||||
cache-key: ${{ needs.common.outputs.cache-key }}
|
||||
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
|
||||
# Inlined from esphome/pre-commit-action with a restore-only cache
|
||||
# step: the pre-commit-seed-cache job owns saving this cache, so
|
||||
# pull request runs never write per-PR copies.
|
||||
- name: Restore pre-commit cache
|
||||
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: ~/.cache/pre-commit
|
||||
# Must match the key pre-commit-seed-cache saves
|
||||
# yamllint disable-line rule:line-length
|
||||
key: pre-commit-3|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }}
|
||||
- name: Run pre-commit
|
||||
env:
|
||||
SKIP: pylint,ci-custom
|
||||
run: |
|
||||
python -m pip install pre-commit
|
||||
pre-commit run --show-diff-on-failure --color=always --all-files
|
||||
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
|
||||
if: always()
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
uses: github/codeql-action/init@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
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@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
uses: github/codeql-action/analyze@7188fc363630916deb702c7fdcf4e481b751f97a # v4.37.1
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -14,4 +14,4 @@ jobs:
|
||||
permissions:
|
||||
issues: write # issues.lock on closed issues
|
||||
pull-requests: write # issues.lock on closed pull requests
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1
|
||||
uses: esphome/workflows/.github/workflows/lock.yml@9f6577fd37b5cf773ab1b9be929714a0dcd15661 # 2026.7.0
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Stale
|
||||
uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||
uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
||||
with:
|
||||
debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
|
||||
remove-stale-when-updated: true
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
# setup-python interpreter so subsequent ``pre-commit`` /
|
||||
# ``script/run-in-env.py`` steps find the deps without a
|
||||
# ``uv run`` prefix.
|
||||
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v8.3.0
|
||||
uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
|
||||
with:
|
||||
enable-cache: true
|
||||
# Pin uv version so the action does not have to fetch the
|
||||
|
||||
@@ -145,6 +145,7 @@ esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
|
||||
esphome/components/dps310/* @kbx81
|
||||
esphome/components/ds1307/* @badbadc0ffee
|
||||
esphome/components/ds2484/* @mrk-its
|
||||
esphome/components/ds248x/* @tomwellnitz
|
||||
esphome/components/dsmr/* @glmnet @PolarGoose
|
||||
esphome/components/duty_time/* @dudanov
|
||||
esphome/components/ee895/* @Stock-M
|
||||
@@ -187,6 +188,7 @@ esphome/components/ezo_pmp/* @carlos-sarmiento
|
||||
esphome/components/factory_reset/* @anatoly-savchenkov
|
||||
esphome/components/fastled_base/* @OttoWinter
|
||||
esphome/components/feedback/* @ianchi
|
||||
esphome/components/file/* @esphome/core
|
||||
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
|
||||
esphome/components/font/* @clydebarrow @esphome/core
|
||||
esphome/components/fs3000/* @kahrendt
|
||||
@@ -208,6 +210,7 @@ esphome/components/gree/switch/* @nagyrobi
|
||||
esphome/components/grove_gas_mc_v2/* @YorkshireIoT
|
||||
esphome/components/grove_tb6612fng/* @max246
|
||||
esphome/components/growatt_solar/* @leeuwte
|
||||
esphome/components/gsl3670/* @clydebarrow
|
||||
esphome/components/gt911/* @clydebarrow @jesserockz
|
||||
esphome/components/haier/* @paveldn
|
||||
esphome/components/haier/binary_sensor/* @paveldn
|
||||
@@ -404,6 +407,7 @@ esphome/components/pn7160_i2c/* @jesserockz @kbx81
|
||||
esphome/components/pn7160_spi/* @jesserockz @kbx81
|
||||
esphome/components/power_supply/* @esphome/core
|
||||
esphome/components/preferences/* @esphome/core
|
||||
esphome/components/provisioning/* @esphome/core
|
||||
esphome/components/psram/* @esphome/core
|
||||
esphome/components/pulse_meter/* @cstaahl @stevebaxter @TrentHouliston
|
||||
esphome/components/pvvx_mithermometer/* @pasiz
|
||||
|
||||
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
|
||||
# could be handy for archiving the generated documentation or if some version
|
||||
# control system is used.
|
||||
|
||||
PROJECT_NUMBER = 2026.7.0-dev
|
||||
PROJECT_NUMBER = 2026.8.0-dev
|
||||
|
||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
|
||||
# for a project that appears at the top of each page and should give viewer a
|
||||
|
||||
@@ -6,3 +6,4 @@ recursive-include esphome *.cpp *.h *.tcc *.c
|
||||
recursive-include esphome *.py.script
|
||||
recursive-include esphome *.jinja
|
||||
recursive-include esphome LICENSE.txt
|
||||
recursive-include esphome requirements.txt
|
||||
|
||||
@@ -79,6 +79,48 @@ These *are* security bugs in this repo, and we want to hear about them privately
|
||||
- Flaws that weaken the device's API encryption (Noise), OTA, or web server auth
|
||||
below their documented guarantees.
|
||||
|
||||
## The web server is an open HTTP API by design
|
||||
|
||||
The `web_server` component exposes a plain HTTP interface for viewing and
|
||||
controlling entities, and, when the `web_server` OTA platform is enabled, for
|
||||
uploading firmware at `/update`. Its only access controls are the optional
|
||||
`web_server` `auth:` credentials and the network the device sits on.
|
||||
|
||||
When `auth:` is not configured, every endpoint is reachable by any client that
|
||||
can reach the device. This is intentional; enabling `web_server` without `auth:`
|
||||
is choosing an open control surface, in the same way that running native OTA
|
||||
without a password leaves OTA open. The API is documented and is meant to be
|
||||
called by other devices, scripts, and pages.
|
||||
|
||||
As defense-in-depth, the web server checks the `Origin` header on browser requests
|
||||
to its entity control and state endpoints: a request whose `Origin` does not match
|
||||
the address the device is served on is rejected, and the `allowed_origins` option
|
||||
widens that list. This blocks the common "confused deputy" (CSRF) case where a page
|
||||
the operator visits drives the device through their browser. It is **not** an
|
||||
authentication boundary: it only constrains browsers. Any client that omits the
|
||||
`Origin` header — `curl`, scripts, or other non-browser callers on the same
|
||||
network — reaches every endpoint exactly as before. The check also does not cover
|
||||
the web OTA `/update` endpoint. The device performs no CSRF-token or `Referer`
|
||||
validation. The following are therefore **not** vulnerabilities in this repository:
|
||||
|
||||
- Requests without an `Origin` header (for example `curl`) reaching the control
|
||||
endpoints, whether or not `web_server` `auth:` is set.
|
||||
- Requests from an origin the operator added to `allowed_origins`.
|
||||
- Cross-origin or CSRF firmware upload through the web OTA endpoint (`/update`) when
|
||||
web OTA is enabled without `web_server` `auth:`. The `/update` endpoint is not
|
||||
covered by the `Origin` check; this is the same exposure as running OTA without a
|
||||
password.
|
||||
|
||||
The supported defenses are `web_server` `auth:`, protecting OTA (a web password or
|
||||
a native OTA password), and keeping devices on a trusted, segmented network. See
|
||||
the security best practices guide linked above.
|
||||
|
||||
What remains in scope is bypassing `web_server` `auth:` when it *is* configured,
|
||||
and any memory-safety or protocol bug in the server reachable without credentials.
|
||||
|
||||
This section documents the current design and scope; it is not a judgment that the
|
||||
design is optimal or that it will not change.
|
||||
|
||||
## Explicitly out of scope
|
||||
|
||||
- Local attackers who already have shell access on the host that runs `esphome`.
|
||||
@@ -86,6 +128,10 @@ These *are* security bugs in this repo, and we want to hear about them privately
|
||||
- Operator-supplied hostile YAML (covered above — config authoring is trusted).
|
||||
- Attacks that require an already-authenticated device peer (someone who already
|
||||
holds the API key / OTA / web credentials).
|
||||
- Access to the device web server or its web OTA endpoint by non-browser clients
|
||||
(those that send no `Origin` header). The web server is an open HTTP API by
|
||||
design (see above); browser cross-origin requests are blocked by default, but the
|
||||
real controls are `web_server` `auth:` and network isolation.
|
||||
- Anything in the dashboard / device-builder — report that in its own repository
|
||||
(linked at the top).
|
||||
- Deployments where the operator removed protections or exposed credentials. See
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ RUN \
|
||||
-r /requirements.txt
|
||||
|
||||
# Install the ESPHome Device Builder dashboard.
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.2.0
|
||||
RUN uv pip install --no-cache-dir esphome-device-builder==1.6.4
|
||||
|
||||
RUN \
|
||||
platformio settings set enable_telemetry No \
|
||||
|
||||
@@ -21,6 +21,7 @@ from . import (
|
||||
RAM_SECTIONS,
|
||||
MemoryAnalyzer,
|
||||
)
|
||||
from .toolchain import find_elf_path, find_idedata_path, idedata_candidates
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import ComponentMemory
|
||||
@@ -760,45 +761,25 @@ def main():
|
||||
print(f"Error: {build_path} is not a directory", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Find firmware.elf
|
||||
elf_file = None
|
||||
for elf_candidate in [
|
||||
build_path / "firmware.elf",
|
||||
build_path / ".pioenvs" / build_path.name / "firmware.elf",
|
||||
]:
|
||||
if elf_candidate.exists():
|
||||
elf_file = str(elf_candidate)
|
||||
break
|
||||
|
||||
if not elf_file:
|
||||
print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr)
|
||||
elf_path = find_elf_path(build_path)
|
||||
if not elf_path:
|
||||
print(f"Error: no firmware ELF found in {build_dir}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Find idedata.json - check current directory first, then home
|
||||
device_name = build_path.name
|
||||
idedata_candidates = [
|
||||
Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json",
|
||||
Path.home() / ".esphome" / "idedata" / f"{device_name}.json",
|
||||
]
|
||||
elf_file = str(elf_path)
|
||||
|
||||
idedata = None
|
||||
for idedata_path in idedata_candidates:
|
||||
if not idedata_path.exists():
|
||||
continue
|
||||
if idedata_path := find_idedata_path(build_path):
|
||||
try:
|
||||
with idedata_path.open(encoding="utf-8") as f:
|
||||
raw_data = json.load(f)
|
||||
idedata = IDEData(raw_data)
|
||||
print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
|
||||
break
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
|
||||
|
||||
if not idedata:
|
||||
print(
|
||||
f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})",
|
||||
file=sys.stderr,
|
||||
)
|
||||
searched = "\n ".join(str(p) for p in idedata_candidates(build_path))
|
||||
print(f"Warning: idedata not found, searched:\n {searched}", file=sys.stderr)
|
||||
|
||||
analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata)
|
||||
analyzer.analyze()
|
||||
|
||||
@@ -23,6 +23,78 @@ TOOLCHAIN_PREFIXES = [
|
||||
]
|
||||
|
||||
|
||||
def find_elf_path(build_path: Path) -> Path | None:
|
||||
"""Locate the firmware ELF inside an ESPHome build directory.
|
||||
|
||||
The layout depends on the toolchain that produced the build, so try each
|
||||
known one in turn.
|
||||
|
||||
Args:
|
||||
build_path: Path to an ESPHome build directory
|
||||
|
||||
Returns:
|
||||
Path to the ELF file, or None if no known layout matches
|
||||
"""
|
||||
name = build_path.name
|
||||
for candidate in (
|
||||
# Native ESP-IDF: idf.py writes build/<name>.elf, which ESPHome copies
|
||||
# to build/firmware.elf (see espidf.toolchain.create_elf_copy)
|
||||
build_path / "build" / "firmware.elf",
|
||||
# PlatformIO
|
||||
build_path / "firmware.elf",
|
||||
build_path / ".pioenvs" / name / "firmware.elf",
|
||||
# LibreTiny uses raw_firmware.elf
|
||||
build_path / "raw_firmware.elf",
|
||||
build_path / ".pioenvs" / name / "raw_firmware.elf",
|
||||
# Zephyr (nRF52); the SDK nests the artifacts one level deeper from 2.9.2
|
||||
build_path / ".pioenvs" / name / "zephyr" / "zephyr" / "zephyr.elf",
|
||||
build_path / ".pioenvs" / name / "zephyr" / "zephyr.elf",
|
||||
):
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def idedata_candidates(build_path: Path) -> list[Path]:
|
||||
"""Return the idedata locations searched for a build directory, in order.
|
||||
|
||||
Exposed so a caller reporting "not found" can name the paths it tried
|
||||
without keeping its own copy of the list.
|
||||
|
||||
Args:
|
||||
build_path: Path to an ESPHome build directory
|
||||
|
||||
Returns:
|
||||
The candidate idedata JSON paths, most specific first
|
||||
"""
|
||||
name = build_path.name
|
||||
return [
|
||||
# In .pioenvs for test builds
|
||||
build_path / ".pioenvs" / name / "idedata.json",
|
||||
# Both toolchains cache it in the data dir, which holds this build dir:
|
||||
# <data_dir>/idedata/<name>.json next to <data_dir>/build/<name>
|
||||
build_path.parent.parent / "idedata" / f"{name}.json",
|
||||
# Regular builds, invoked from the config dir or from anywhere
|
||||
Path.cwd() / ".esphome" / "idedata" / f"{name}.json",
|
||||
Path.home() / ".esphome" / "idedata" / f"{name}.json",
|
||||
]
|
||||
|
||||
|
||||
def find_idedata_path(build_path: Path) -> Path | None:
|
||||
"""Locate the idedata JSON belonging to an ESPHome build directory.
|
||||
|
||||
Args:
|
||||
build_path: Path to an ESPHome build directory
|
||||
|
||||
Returns:
|
||||
Path to the idedata JSON, or None if it was not found
|
||||
"""
|
||||
for candidate in idedata_candidates(build_path):
|
||||
if candidate.is_file():
|
||||
return candidate
|
||||
return None
|
||||
|
||||
|
||||
def _find_in_platformio_packages(tool_name: str) -> str | None:
|
||||
"""Search for a tool in PlatformIO package directories.
|
||||
|
||||
|
||||
+35
-2
@@ -7,7 +7,7 @@ and compiled directly: ``esphome compile my_device.esphomebundle.tar.gz``
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum
|
||||
import io
|
||||
import json
|
||||
@@ -32,6 +32,8 @@ from esphome.core import CORE, EsphomeError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "bundle"
|
||||
|
||||
BUNDLE_EXTENSION = ".esphomebundle.tar.gz"
|
||||
MANIFEST_FILENAME = "manifest.json"
|
||||
CURRENT_MANIFEST_VERSION = 1
|
||||
@@ -120,6 +122,32 @@ def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]:
|
||||
return keys
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleData:
|
||||
"""Files components asked to include, keyed under DOMAIN in CORE.data."""
|
||||
|
||||
extra_files: list[Path] = field(default_factory=list)
|
||||
|
||||
|
||||
def _get_data() -> BundleData:
|
||||
if DOMAIN not in CORE.data:
|
||||
CORE.data[DOMAIN] = BundleData()
|
||||
return CORE.data[DOMAIN]
|
||||
|
||||
|
||||
def add_bundle_file(path: Path) -> None:
|
||||
"""Register a file that a bundle must include.
|
||||
|
||||
Bundle discovery walks the validated config, so it only finds files the config
|
||||
names. Components call this during validation for files it cannot see, such as a
|
||||
file that is referenced from inside another file.
|
||||
|
||||
A relative path is taken as relative to the config directory. Files outside the
|
||||
config directory are skipped when the bundle is built.
|
||||
"""
|
||||
_get_data().extra_files.append(CORE.relative_config_path(path))
|
||||
|
||||
|
||||
@dataclass
|
||||
class BundleFile:
|
||||
"""A file to include in the bundle."""
|
||||
@@ -286,13 +314,18 @@ class ConfigBundleCreator:
|
||||
with known file extensions are also resolved and checked.
|
||||
|
||||
Core ESPHome concepts that use relative paths or directories
|
||||
are handled explicitly.
|
||||
are handled explicitly. Files the config does not name at all are
|
||||
registered by their component with add_bundle_file().
|
||||
"""
|
||||
config = self._config
|
||||
|
||||
# Generic walk: find all file paths in the validated config
|
||||
self._walk_config_for_files(config)
|
||||
|
||||
# Files registered by components during validation
|
||||
for extra_file in _get_data().extra_files:
|
||||
self._add_file(extra_file)
|
||||
|
||||
# --- Core ESPHome concepts needing explicit handling ---
|
||||
|
||||
# esphome.includes / includes_c - can be relative paths and directories
|
||||
|
||||
@@ -1,114 +1,36 @@
|
||||
import logging
|
||||
# ---------------------------------------------------------------------------
|
||||
# Legacy top-level `animation:` deprecation shim -- REMOVE this whole file after
|
||||
# 2027.1.0.
|
||||
#
|
||||
# Animations are now a platform of the `image:` component (`platform:
|
||||
# animation`); the real schema, actions and codegen live in `image.py`. This
|
||||
# module only keeps the deprecated top-level `animation:` key working during the
|
||||
# deprecation window: it reuses that schema/codegen and adds a one-shot
|
||||
# deprecation warning (with a pasteable migrated `image:` block) at validation
|
||||
# time. Deleting this file drops the top-level form entirely.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_LOOP
|
||||
import esphome.components.image as espImage
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_REPEAT
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
from .image import ANIMATION_CONFIG_SCHEMA, setup_animation
|
||||
|
||||
AUTO_LOAD = ["image"]
|
||||
AUTO_LOAD = ["image", "file"]
|
||||
CODEOWNERS = ["@syndlex"]
|
||||
DEPENDENCIES = ["display"]
|
||||
MULTI_CONF = True
|
||||
MULTI_CONF_NO_DEFAULT = True
|
||||
|
||||
CONF_START_FRAME = "start_frame"
|
||||
CONF_END_FRAME = "end_frame"
|
||||
CONF_FRAME = "frame"
|
||||
DOMAIN = "animation"
|
||||
|
||||
animation_ns = cg.esphome_ns.namespace("animation")
|
||||
LEGACY_REMOVAL_VERSION = "2027.1.0"
|
||||
|
||||
Animation_ = animation_ns.class_("Animation", espImage.Image_)
|
||||
|
||||
# Actions
|
||||
NextFrameAction = animation_ns.class_(
|
||||
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
PrevFrameAction = animation_ns.class_(
|
||||
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
_capture_legacy_entry, _warn_legacy_animation = (
|
||||
espImage.legacy_platform_migration_warning(DOMAIN, DOMAIN, LEGACY_REMOVAL_VERSION)
|
||||
)
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
espImage.IMAGE_SCHEMA.extend(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(Animation_),
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||
}
|
||||
),
|
||||
},
|
||||
),
|
||||
espImage.validate_settings,
|
||||
)
|
||||
CONFIG_SCHEMA = cv.All(_capture_legacy_entry, ANIMATION_CONFIG_SCHEMA)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _warn_legacy_animation
|
||||
|
||||
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Animation_),
|
||||
}
|
||||
)
|
||||
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Animation_),
|
||||
}
|
||||
)
|
||||
SET_FRAME_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Animation_),
|
||||
cv.Required(CONF_FRAME): cv.uint16_t,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
async def animation_action_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
if (frame := config.get(CONF_FRAME)) is not None:
|
||||
template_ = await cg.templatable(frame, args, cg.uint16)
|
||||
cg.add(var.set_frame(template_))
|
||||
return var
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
(
|
||||
prog_arr,
|
||||
width,
|
||||
height,
|
||||
image_type,
|
||||
trans_value,
|
||||
frame_count,
|
||||
) = await espImage.write_image(config, all_frames=True)
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
prog_arr,
|
||||
width,
|
||||
height,
|
||||
frame_count,
|
||||
image_type,
|
||||
trans_value,
|
||||
)
|
||||
if loop_config := config.get(CONF_LOOP):
|
||||
start = loop_config[CONF_START_FRAME]
|
||||
end = loop_config.get(CONF_END_FRAME, frame_count)
|
||||
count = loop_config.get(CONF_REPEAT, -1)
|
||||
cg.add(var.set_loop(start, end, count))
|
||||
to_code = setup_animation
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
from esphome import automation
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_LOOP
|
||||
from esphome.components.file.image import image_schema, write_image
|
||||
from esphome.components.image import Image_, validate_settings
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_REPEAT
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@syndlex"]
|
||||
AUTO_LOAD = ["file"]
|
||||
DEPENDENCIES = ["display"]
|
||||
|
||||
CONF_START_FRAME = "start_frame"
|
||||
CONF_END_FRAME = "end_frame"
|
||||
CONF_FRAME = "frame"
|
||||
|
||||
animation_ns = cg.esphome_ns.namespace("animation")
|
||||
|
||||
Animation_ = animation_ns.class_("Animation", Image_)
|
||||
|
||||
# Actions
|
||||
NextFrameAction = animation_ns.class_(
|
||||
"AnimationNextFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
PrevFrameAction = animation_ns.class_(
|
||||
"AnimationPrevFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
SetFrameAction = animation_ns.class_(
|
||||
"AnimationSetFrameAction", automation.Action, cg.Parented.template(Animation_)
|
||||
)
|
||||
|
||||
ANIMATION_SCHEMA = image_schema(Animation_).extend(
|
||||
{
|
||||
cv.Optional(CONF_LOOP): cv.All(
|
||||
{
|
||||
cv.Optional(CONF_START_FRAME, default=0): cv.positive_int,
|
||||
cv.Optional(CONF_END_FRAME): cv.positive_int,
|
||||
cv.Optional(CONF_REPEAT): cv.positive_int,
|
||||
}
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
# Shared schema used by both the (deprecated) top-level `animation:` key and the
|
||||
# `image:` `platform: animation` entry.
|
||||
ANIMATION_CONFIG_SCHEMA = cv.All(ANIMATION_SCHEMA, validate_settings)
|
||||
|
||||
|
||||
NEXT_FRAME_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Animation_),
|
||||
}
|
||||
)
|
||||
PREV_FRAME_SCHEMA = automation.maybe_simple_id(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Animation_),
|
||||
}
|
||||
)
|
||||
SET_FRAME_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.use_id(Animation_),
|
||||
cv.Required(CONF_FRAME): cv.uint16_t,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"animation.next_frame", NextFrameAction, NEXT_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"animation.prev_frame", PrevFrameAction, PREV_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
@automation.register_action(
|
||||
"animation.set_frame", SetFrameAction, SET_FRAME_SCHEMA, synchronous=True
|
||||
)
|
||||
async def animation_action_to_code(config, action_id, template_arg, args):
|
||||
paren = await cg.get_variable(config[CONF_ID])
|
||||
var = cg.new_Pvariable(action_id, template_arg, paren)
|
||||
|
||||
if (frame := config.get(CONF_FRAME)) is not None:
|
||||
template_ = await cg.templatable(frame, args, cg.uint16)
|
||||
cg.add(var.set_frame(template_))
|
||||
return var
|
||||
|
||||
|
||||
async def setup_animation(config: ConfigType) -> None:
|
||||
(
|
||||
prog_arr,
|
||||
width,
|
||||
height,
|
||||
image_type,
|
||||
trans_value,
|
||||
frame_count,
|
||||
) = await write_image(config, all_frames=True)
|
||||
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID],
|
||||
prog_arr,
|
||||
width,
|
||||
height,
|
||||
frame_count,
|
||||
image_type,
|
||||
trans_value,
|
||||
)
|
||||
if loop_config := config.get(CONF_LOOP):
|
||||
start = loop_config[CONF_START_FRAME]
|
||||
end = loop_config.get(CONF_END_FRAME, frame_count)
|
||||
count = loop_config.get(CONF_REPEAT, -1)
|
||||
cg.add(var.set_loop(start, end, count))
|
||||
|
||||
|
||||
CONFIG_SCHEMA = ANIMATION_CONFIG_SCHEMA
|
||||
|
||||
to_code = setup_animation
|
||||
@@ -112,6 +112,23 @@ CONF_MAX_SEND_QUEUE = "max_send_queue"
|
||||
CONF_STATE_SUBSCRIPTION_ONLY = "state_subscription_only"
|
||||
|
||||
|
||||
def _register_provisioning_source(config: ConfigType) -> ConfigType:
|
||||
"""Register the API as a provisioning source when encryption is enabled.
|
||||
|
||||
With no ``key`` the device boots unprovisioned and is set up on first
|
||||
connection; a YAML ``key`` means it is born provisioned. Either way the API
|
||||
drives the provisioning manager, so it counts as a source for `provisioning:`.
|
||||
A hardcoded ``key`` is reported so `provisioning:` can warn about it.
|
||||
"""
|
||||
if (encryption := config.get(CONF_ENCRYPTION)) is not None:
|
||||
from esphome.components import provisioning
|
||||
|
||||
provisioning.register_source("api")
|
||||
if CONF_KEY in encryption:
|
||||
provisioning.report_hardcoded_credentials("api")
|
||||
return config
|
||||
|
||||
|
||||
def validate_encryption_key(value):
|
||||
value = cv.string_strict(value)
|
||||
try:
|
||||
@@ -337,6 +354,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
|
||||
_consume_api_sockets,
|
||||
_register_provisioning_source,
|
||||
)
|
||||
|
||||
|
||||
@@ -470,8 +488,11 @@ async def to_code(config: ConfigType) -> None:
|
||||
cg.add_define("USE_API_NOISE_PSK_FROM_YAML")
|
||||
else:
|
||||
# No key provided, but encryption desired
|
||||
# This will allow a plaintext client to provide a noise key,
|
||||
# send it to the device, and then switch to noise.
|
||||
# Until a key is set, the device accepts both Noise connections
|
||||
# using the well-known all-zeros PSK (preferred: the key travels
|
||||
# encrypted, protecting against passive sniffing) and plaintext
|
||||
# connections (deprecated, remove after 2027.2.0) so a client can
|
||||
# provide a noise key and the device then switches to noise only.
|
||||
# The key will be saved in flash and used for future connections
|
||||
# and plaintext disabled. Only a factory reset can remove it.
|
||||
cg.add_define("USE_API_PLAINTEXT")
|
||||
|
||||
@@ -158,6 +158,16 @@ message AuthenticationResponse {
|
||||
bool invalid_password = 1;
|
||||
}
|
||||
|
||||
// Reason a party is requesting the connection be closed.
|
||||
enum DisconnectReason {
|
||||
// No specific reason / not provided (default for older peers).
|
||||
DISCONNECT_REASON_UNSPECIFIED = 0;
|
||||
// The device's provisioning window has expired. The device must be reset
|
||||
// (power-cycled) to reopen the provisioning window before it will accept a
|
||||
// connection again.
|
||||
DISCONNECT_REASON_PROVISIONING_CLOSED = 1;
|
||||
}
|
||||
|
||||
// Request to close the connection.
|
||||
// Can be sent by both the client and server
|
||||
message DisconnectRequest {
|
||||
@@ -166,6 +176,10 @@ message DisconnectRequest {
|
||||
option (no_delay) = true;
|
||||
|
||||
// Do not close the connection before the acknowledgement arrives
|
||||
|
||||
// Optional reason the connection is being closed. Older peers that do not
|
||||
// send this field will report DISCONNECT_REASON_UNSPECIFIED (0).
|
||||
DisconnectReason reason = 1;
|
||||
}
|
||||
|
||||
message DisconnectResponse {
|
||||
@@ -296,6 +310,11 @@ message DeviceInfoResponse {
|
||||
|
||||
// Serial proxy instance metadata
|
||||
repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
|
||||
|
||||
// Device is unprovisioned and accepts Noise handshakes with the well-known
|
||||
// all-zeros PSK, so the api encryption key can be provisioned without being
|
||||
// sent in plaintext (protects against passive sniffing, not active MITM)
|
||||
bool api_encryption_provisionable = 26 [(field_ifdef) = "USE_API_NOISE"];
|
||||
}
|
||||
|
||||
message ListEntitiesRequest {
|
||||
|
||||
@@ -25,6 +25,9 @@
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/version.h"
|
||||
#ifdef USE_PROVISIONING
|
||||
#include "esphome/components/provisioning/provisioning.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_DEEP_SLEEP
|
||||
#include "esphome/components/deep_sleep/deep_sleep_component.h"
|
||||
@@ -195,6 +198,29 @@ APIConnection::~APIConnection() {
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
||||
void APIConnection::upgrade_helper_to_noise_() {
|
||||
// The client opened with a Noise hello while this device has no encryption
|
||||
// key set. Replace the plaintext helper with a Noise helper so the key can
|
||||
// be provisioned over an encrypted channel: the noise context PSK is all
|
||||
// zeros when unprovisioned, and NNpsk0 still runs a fresh ephemeral X25519
|
||||
// exchange, so a passive listener cannot read the session. A publicly known
|
||||
// PSK authenticates nobody; this protects against sniffing only.
|
||||
auto *plaintext = static_cast<APIPlaintextFrameHelper *>(this->helper_.get());
|
||||
uint8_t header[3];
|
||||
uint8_t header_len = plaintext->get_consumed_header(header);
|
||||
auto *noise = new APINoiseFrameHelper(plaintext->release_socket_for_switch(), this->parent_->get_noise_ctx());
|
||||
// Carry over the peername-based client name (Hello has not arrived yet)
|
||||
const char *name = plaintext->get_client_name();
|
||||
noise->set_client_name(name, strlen(name));
|
||||
this->helper_.reset(noise); // destroys the plaintext helper
|
||||
APIError err = noise->init_from_handoff(header, header_len);
|
||||
if (err != APIError::OK) {
|
||||
this->fatal_error_with_log_(LOG_STR("Noise handoff failed"), err);
|
||||
}
|
||||
}
|
||||
#endif // USE_API_NOISE && USE_API_PLAINTEXT
|
||||
|
||||
void APIConnection::destroy_active_iterator_() {
|
||||
switch (this->active_iterator_) {
|
||||
case ActiveIterator::LIST_ENTITIES:
|
||||
@@ -253,6 +279,15 @@ void APIConnection::loop() {
|
||||
// No more data available
|
||||
break;
|
||||
} else if (err != APIError::OK) {
|
||||
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
||||
// Checked inside the error branch to keep the hot err == OK path
|
||||
// free of it; this can only fire on the first bytes of a plaintext
|
||||
// helper on an unprovisioned device
|
||||
if (err == APIError::PROTOCOL_SWITCH_TO_NOISE) {
|
||||
this->upgrade_helper_to_noise_();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
|
||||
return;
|
||||
} else {
|
||||
@@ -1348,7 +1383,7 @@ void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSet
|
||||
|
||||
#ifdef USE_ZWAVE_PROXY
|
||||
void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
|
||||
zwave_proxy::global_zwave_proxy->send_frame(msg.data, msg.data_len);
|
||||
zwave_proxy::global_zwave_proxy->send_frame(this, msg.data, msg.data_len);
|
||||
}
|
||||
|
||||
void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
|
||||
@@ -1711,12 +1746,6 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
|
||||
this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
|
||||
|
||||
// TODO: Remove before 2026.8.0 (one version after get_object_id backward compat removal)
|
||||
if (!this->client_supports_api_version(1, 14)) {
|
||||
ESP_LOGW(TAG, "'%s' using outdated API %" PRIu16 ".%" PRIu16 ", update to 1.14+", this->helper_->get_client_name(),
|
||||
this->client_api_version_major_, this->client_api_version_minor_);
|
||||
}
|
||||
|
||||
HelloResponse resp;
|
||||
resp.api_version_major = 1;
|
||||
resp.api_version_minor = 14;
|
||||
@@ -1724,6 +1753,19 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
|
||||
resp.server_info = ESPHOME_VERSION_REF;
|
||||
resp.name = StringRef(App.get_name());
|
||||
|
||||
#ifdef USE_PROVISIONING
|
||||
if (provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed()) {
|
||||
// The provisioning window has closed without the device being provisioned.
|
||||
// Acknowledge the hello so the client can read the server name, then request
|
||||
// disconnect with the reason. Authentication is intentionally not completed.
|
||||
this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Provisioning closed; rejecting connection"));
|
||||
this->send_message(resp);
|
||||
DisconnectRequest req;
|
||||
req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED;
|
||||
return this->send_message(req);
|
||||
}
|
||||
#endif
|
||||
|
||||
// Auto-authenticate - password auth was removed in ESPHome 2026.1.0
|
||||
this->complete_authentication_();
|
||||
|
||||
@@ -1844,6 +1886,12 @@ bool APIConnection::send_device_info_response_() {
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
resp.api_encryption_supported = true;
|
||||
#ifndef USE_API_NOISE_PSK_FROM_YAML
|
||||
// No key from YAML: while no key is set, the key can be provisioned over a
|
||||
// zero-PSK Noise connection. Gated on the YAML define (not the plaintext
|
||||
// one) so this advertisement survives the plaintext removal in 2027.2.0.
|
||||
resp.api_encryption_provisionable = !this->parent_->get_noise_ctx().has_psk();
|
||||
#endif
|
||||
#endif
|
||||
#ifdef USE_DEVICES
|
||||
size_t device_index = 0;
|
||||
@@ -1874,7 +1922,8 @@ void APIConnection::on_hello_request(const HelloRequest &msg) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
}
|
||||
void APIConnection::on_disconnect_request() {
|
||||
void APIConnection::on_disconnect_request(const DisconnectRequest & /*msg*/) {
|
||||
// The reason is informational when a client disconnects us; we always ack and close.
|
||||
if (!this->send_disconnect_response_()) {
|
||||
this->on_fatal_error();
|
||||
}
|
||||
@@ -2002,6 +2051,15 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
||||
NoiseEncryptionSetKeyResponse resp;
|
||||
resp.success = false;
|
||||
|
||||
#ifdef USE_PROVISIONING
|
||||
// Refuse to set a key once the provisioning window has closed (defense in depth;
|
||||
// such connections are already rejected at hello).
|
||||
if (provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed()) {
|
||||
ESP_LOGW(TAG, "Provisioning closed; rejecting key set");
|
||||
return this->send_message(resp);
|
||||
}
|
||||
#endif
|
||||
|
||||
psk_t psk{};
|
||||
if (msg.key_len == 0) {
|
||||
if (this->parent_->clear_noise_psk(true)) {
|
||||
@@ -2011,10 +2069,21 @@ bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptio
|
||||
}
|
||||
} else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
|
||||
ESP_LOGW(TAG, "Invalid encryption key length");
|
||||
} else if (APINoiseContext::is_all_zeros(psk)) {
|
||||
// Accepting the reserved provisioning PSK would report success without
|
||||
// enabling encryption (or silently clear an existing key)
|
||||
ESP_LOGW(TAG, "Rejecting all-zero encryption key");
|
||||
} else if (!this->parent_->save_noise_psk(psk, true)) {
|
||||
ESP_LOGW(TAG, "Failed to save encryption key");
|
||||
} else {
|
||||
resp.success = true;
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
if (this->helper_->frame_footer_size() == 0) {
|
||||
// Plaintext transport has no frame footer; Noise always has the MAC footer.
|
||||
// Remove after 2027.2.0 together with plaintext support on keyless devices.
|
||||
ESP_LOGW(TAG, "Key received over plaintext; deprecated, will be removed in 2027.2.0");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return this->send_message(resp);
|
||||
|
||||
@@ -166,10 +166,14 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
#endif
|
||||
bool try_send_log_message(int level, const char *tag, const char *line, size_t message_len);
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
// Returns whether this client has subscribed to Home Assistant actions; the message
|
||||
// is only handed to the send path when subscribed. A true return does not guarantee
|
||||
// delivery - it lets the caller warn when no connected client has the subscription.
|
||||
bool send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
if (!this->flags_.service_call_subscription)
|
||||
return;
|
||||
return false;
|
||||
this->send_message(call);
|
||||
return true;
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
void on_homeassistant_action_response(const HomeassistantActionResponse &msg);
|
||||
@@ -259,7 +263,7 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void on_get_time_response(const GetTimeResponse &value);
|
||||
#endif
|
||||
void on_hello_request(const HelloRequest &msg);
|
||||
void on_disconnect_request();
|
||||
void on_disconnect_request(const DisconnectRequest &msg);
|
||||
void on_ping_request();
|
||||
void on_device_info_request();
|
||||
void on_list_entities_request() { this->begin_iterator_(ActiveIterator::LIST_ENTITIES); }
|
||||
@@ -626,6 +630,11 @@ class APIConnection final : public APIServerConnectionBase {
|
||||
void destroy_active_iterator_();
|
||||
void begin_iterator_(ActiveIterator type);
|
||||
void finalize_iterator_sync_();
|
||||
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
||||
// Swap the plaintext helper for a Noise helper after the client opened
|
||||
// with a Noise hello on an unprovisioned device (zero-PSK provisioning).
|
||||
void upgrade_helper_to_noise_();
|
||||
#endif
|
||||
#ifdef USE_CAMERA
|
||||
std::unique_ptr<camera::CameraImageReader> image_reader_;
|
||||
#endif
|
||||
|
||||
@@ -97,6 +97,8 @@ const LogString *api_error_to_logstr(APIError err) {
|
||||
return LOG_STR("BAD_HANDSHAKE_ERROR_BYTE");
|
||||
}
|
||||
#endif
|
||||
// PROTOCOL_SWITCH_TO_NOISE is intercepted in APIConnection::loop() before
|
||||
// any logging can happen, so it intentionally has no entry here.
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,11 @@ enum class APIError : uint16_t {
|
||||
HANDSHAKESTATE_SPLIT_FAILED = 1020,
|
||||
BAD_HANDSHAKE_ERROR_BYTE = 1021,
|
||||
#endif
|
||||
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
||||
// Not an error: an unprovisioned device received a Noise client hello on a
|
||||
// plaintext connection; the caller must hand the socket off to a Noise helper.
|
||||
PROTOCOL_SWITCH_TO_NOISE = 1023,
|
||||
#endif
|
||||
};
|
||||
|
||||
const LogString *api_error_to_logstr(APIError err);
|
||||
@@ -200,6 +205,12 @@ class APIFrameHelper {
|
||||
// or track that they stopped early and retry without this check.
|
||||
// See Socket::ready() for details.
|
||||
bool is_socket_ready() const { return socket_ != nullptr && socket_->ready(); }
|
||||
#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
|
||||
// Move the socket out of this helper so a replacement helper can take it
|
||||
// over (plaintext to Noise handoff on unprovisioned devices). The drained
|
||||
// helper must be destroyed right after.
|
||||
std::unique_ptr<socket::Socket> release_socket_for_switch() { return std::move(this->socket_); }
|
||||
#endif
|
||||
// Release excess memory from internal buffers after initial sync
|
||||
void release_buffers() {
|
||||
// rx_buf_: Safe to clear only if no partial read in progress.
|
||||
|
||||
@@ -109,6 +109,40 @@ APIError APINoiseFrameHelper::init() {
|
||||
state_ = State::CLIENT_HELLO;
|
||||
return APIError::OK;
|
||||
}
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
APIError APINoiseFrameHelper::init_from_handoff(const uint8_t *header, uint8_t header_len) {
|
||||
APIError err = this->init();
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
}
|
||||
// Seed the header bytes the plaintext helper consumed before detecting the
|
||||
// Noise indicator; try_read_frame_ resumes from rx_header_buf_len_.
|
||||
std::memcpy(this->rx_header_buf_, header, header_len);
|
||||
this->rx_header_buf_len_ = header_len;
|
||||
// Pump the handshake without gating on socket_->ready(): on LWIP the
|
||||
// plaintext helper's partial read can drain rcvevent while the rest of the
|
||||
// client hello sits in the lastdata cache, so ready() may report false even
|
||||
// though data is available.
|
||||
return this->pump_handshake_();
|
||||
}
|
||||
#endif // USE_API_PLAINTEXT
|
||||
|
||||
/// Drive the handshake state machine until DATA, WOULD_BLOCK, or a fatal
|
||||
/// error. WOULD_BLOCK is not an error: reads stop naturally on EWOULDBLOCK
|
||||
/// and resume on the next loop().
|
||||
APIError APINoiseFrameHelper::pump_handshake_() {
|
||||
while (this->state_ != State::DATA) {
|
||||
APIError err = this->state_action_();
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
break;
|
||||
}
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
}
|
||||
}
|
||||
return APIError::OK;
|
||||
}
|
||||
|
||||
// Helper for handling handshake frame errors
|
||||
APIError APINoiseFrameHelper::handle_handshake_frame_error_(APIError aerr) {
|
||||
if (aerr == APIError::BAD_INDICATOR) {
|
||||
@@ -131,16 +165,13 @@ APIError APINoiseFrameHelper::handle_noise_error_(int err, const LogString *func
|
||||
|
||||
/// Run through handshake messages (if in that phase)
|
||||
APIError APINoiseFrameHelper::loop() {
|
||||
// Cache ready() outside the loop. On ESP8266 LWIP raw TCP, ready() returns false once
|
||||
// the rx buffer is consumed. Re-checking each iteration would block handshake writes
|
||||
// that must follow reads, deadlocking the handshake. state_action() will return
|
||||
// WOULD_BLOCK when no more data is available to read.
|
||||
bool socket_ready = this->socket_->ready();
|
||||
while (state_ != State::DATA && socket_ready) {
|
||||
APIError err = state_action_();
|
||||
if (err == APIError::WOULD_BLOCK) {
|
||||
break;
|
||||
}
|
||||
// Check ready() once, not per state transition. On ESP8266 LWIP raw TCP,
|
||||
// ready() returns false once the rx buffer is consumed. Re-checking each
|
||||
// iteration would block handshake writes that must follow reads,
|
||||
// deadlocking the handshake. pump_handshake_() stops on WOULD_BLOCK when
|
||||
// no more data is available to read.
|
||||
if (state_ != State::DATA && this->socket_->ready()) {
|
||||
APIError err = this->pump_handshake_();
|
||||
if (err != APIError::OK) {
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -22,12 +22,20 @@ class APINoiseFrameHelper final : public APIFrameHelper {
|
||||
}
|
||||
~APINoiseFrameHelper() override;
|
||||
APIError init() override;
|
||||
#ifdef USE_API_PLAINTEXT
|
||||
// Take over a connection whose first bytes were consumed by a plaintext
|
||||
// helper on an unprovisioned device (see APIError::PROTOCOL_SWITCH_TO_NOISE).
|
||||
// Seeds the already-read header bytes and pumps the handshake state machine
|
||||
// until it would block.
|
||||
APIError init_from_handoff(const uint8_t *header, uint8_t header_len);
|
||||
#endif
|
||||
APIError loop() override;
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
|
||||
protected:
|
||||
APIError pump_handshake_();
|
||||
APIError state_action_();
|
||||
APIError state_action_client_hello_();
|
||||
APIError state_action_server_hello_();
|
||||
|
||||
@@ -89,6 +89,17 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
|
||||
// If this was the first read, validate the indicator byte
|
||||
if (rx_header_buf_pos_ == 0 && received > 0) {
|
||||
if (rx_header_buf_[0] != 0x00) {
|
||||
#ifdef USE_API_NOISE
|
||||
// Dual build (encryption supported but no key set): a 0x01 first byte
|
||||
// is a Noise client hello. Hand the connection off to a Noise helper
|
||||
// running the all-zeros provisioning PSK so the encryption key can be
|
||||
// set without crossing the wire in plaintext. Preserve the bytes we
|
||||
// already consumed; they are the start of the Noise 3-byte header.
|
||||
if (rx_header_buf_[0] == 0x01) {
|
||||
rx_header_buf_pos_ = static_cast<uint8_t>(received);
|
||||
return APIError::PROTOCOL_SWITCH_TO_NOISE;
|
||||
}
|
||||
#endif
|
||||
state_ = State::FAILED;
|
||||
HELPER_LOG("Bad indicator byte %u", rx_header_buf_[0]);
|
||||
return APIError::BAD_INDICATOR;
|
||||
|
||||
@@ -23,6 +23,15 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
|
||||
APIError read_packet(ReadPacketBuffer *buffer) override;
|
||||
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
|
||||
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
|
||||
#ifdef USE_API_NOISE
|
||||
// After try_read_frame_ returned PROTOCOL_SWITCH_TO_NOISE: copy out the
|
||||
// header bytes already consumed from the socket (at most 3, the size of the
|
||||
// Noise fixed header) so the replacement Noise helper can be seeded with them.
|
||||
uint8_t get_consumed_header(uint8_t out[3]) const {
|
||||
memcpy(out, this->rx_header_buf_, this->rx_header_buf_pos_);
|
||||
return this->rx_header_buf_pos_;
|
||||
}
|
||||
#endif
|
||||
|
||||
protected:
|
||||
APIError try_read_frame_();
|
||||
|
||||
@@ -10,13 +10,20 @@ using psk_t = std::array<uint8_t, 32>;
|
||||
|
||||
class APINoiseContext {
|
||||
public:
|
||||
// The all-zeros PSK is reserved: it marks the device as unprovisioned and
|
||||
// doubles as the well-known provisioning PSK that unprovisioned devices
|
||||
// accept for Noise handshakes (passive-sniffing protection only, no
|
||||
// authentication). It is never a valid real key.
|
||||
static bool is_all_zeros(const psk_t &psk) {
|
||||
uint8_t acc = 0;
|
||||
for (uint8_t b : psk) {
|
||||
acc |= b;
|
||||
}
|
||||
return acc == 0;
|
||||
}
|
||||
void set_psk(psk_t psk) {
|
||||
this->psk_ = psk;
|
||||
bool has_psk = false;
|
||||
for (auto i : psk) {
|
||||
has_psk |= i;
|
||||
}
|
||||
this->has_psk_ = has_psk;
|
||||
this->has_psk_ = !is_all_zeros(psk);
|
||||
}
|
||||
const psk_t &get_psk() const { return this->psk_; }
|
||||
bool has_psk() const { return this->has_psk_; }
|
||||
|
||||
@@ -47,6 +47,26 @@ uint32_t HelloResponse::calculate_size() const {
|
||||
size += 2 + this->name.size();
|
||||
return size;
|
||||
}
|
||||
bool DisconnectRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
|
||||
switch (field_id) {
|
||||
case 1:
|
||||
this->reason = static_cast<enums::DisconnectReason>(value);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
uint8_t *DisconnectRequest::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, static_cast<uint32_t>(this->reason));
|
||||
return pos;
|
||||
}
|
||||
uint32_t DisconnectRequest::calculate_size() const {
|
||||
uint32_t size = 0;
|
||||
size += this->reason ? 2 : 0;
|
||||
return size;
|
||||
}
|
||||
#ifdef USE_AREAS
|
||||
uint8_t *AreaInfo::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
|
||||
uint8_t *__restrict__ pos = buffer.get_pos();
|
||||
@@ -150,6 +170,9 @@ uint8_t *DeviceInfoResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_
|
||||
for (const auto &it : this->serial_proxies) {
|
||||
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 25, it);
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 26, this->api_encryption_provisionable);
|
||||
#endif
|
||||
return pos;
|
||||
}
|
||||
@@ -212,6 +235,9 @@ uint32_t DeviceInfoResponse::calculate_size() const {
|
||||
for (const auto &it : this->serial_proxies) {
|
||||
size += ProtoSize::calc_message_force(2, it.calculate_size());
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
size += ProtoSize::calc_bool(2, this->api_encryption_provisionable);
|
||||
#endif
|
||||
return size;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ namespace esphome::api {
|
||||
|
||||
namespace enums {
|
||||
|
||||
enum DisconnectReason : uint32_t {
|
||||
DISCONNECT_REASON_UNSPECIFIED = 0,
|
||||
DISCONNECT_REASON_PROVISIONING_CLOSED = 1,
|
||||
};
|
||||
enum SerialProxyPortType : uint32_t {
|
||||
SERIAL_PROXY_PORT_TYPE_TTL = 0,
|
||||
SERIAL_PROXY_PORT_TYPE_RS232 = 1,
|
||||
@@ -427,18 +431,22 @@ class HelloResponse final : public ProtoMessage {
|
||||
|
||||
protected:
|
||||
};
|
||||
class DisconnectRequest final : public ProtoMessage {
|
||||
class DisconnectRequest final : public ProtoDecodableMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 5;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 0;
|
||||
static constexpr uint8_t ESTIMATED_SIZE = 2;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("disconnect_request"); }
|
||||
#endif
|
||||
enums::DisconnectReason reason{};
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const char *dump_to(DumpBuffer &out) const override;
|
||||
#endif
|
||||
|
||||
protected:
|
||||
bool decode_varint(uint32_t field_id, proto_varint_value_t value) override;
|
||||
};
|
||||
class DisconnectResponse final : public ProtoMessage {
|
||||
public:
|
||||
@@ -525,7 +533,7 @@ class SerialProxyInfo final : public ProtoMessage {
|
||||
class DeviceInfoResponse final : public ProtoMessage {
|
||||
public:
|
||||
static constexpr uint8_t MESSAGE_TYPE = 10;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 309;
|
||||
static constexpr uint16_t ESTIMATED_SIZE = 312;
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
const LogString *message_name() const override { return LOG_STR("device_info_response"); }
|
||||
#endif
|
||||
@@ -580,6 +588,9 @@ class DeviceInfoResponse final : public ProtoMessage {
|
||||
#endif
|
||||
#ifdef USE_SERIAL_PROXY
|
||||
std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{};
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
bool api_encryption_provisionable{false};
|
||||
#endif
|
||||
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
|
||||
uint32_t calculate_size() const;
|
||||
|
||||
@@ -125,6 +125,16 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint
|
||||
}
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
template<> const char *proto_enum_to_string<enums::DisconnectReason>(enums::DisconnectReason value) {
|
||||
switch (value) {
|
||||
case enums::DISCONNECT_REASON_UNSPECIFIED:
|
||||
return ESPHOME_PSTR("DISCONNECT_REASON_UNSPECIFIED");
|
||||
case enums::DISCONNECT_REASON_PROVISIONING_CLOSED:
|
||||
return ESPHOME_PSTR("DISCONNECT_REASON_PROVISIONING_CLOSED");
|
||||
default:
|
||||
return ESPHOME_PSTR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
template<> const char *proto_enum_to_string<enums::SerialProxyPortType>(enums::SerialProxyPortType value) {
|
||||
switch (value) {
|
||||
case enums::SERIAL_PROXY_PORT_TYPE_TTL:
|
||||
@@ -864,7 +874,8 @@ const char *HelloResponse::dump_to(DumpBuffer &out) const {
|
||||
return out.c_str();
|
||||
}
|
||||
const char *DisconnectRequest::dump_to(DumpBuffer &out) const {
|
||||
out.append_p(ESPHOME_PSTR("DisconnectRequest {}"));
|
||||
MessageDumpHelper helper(out, ESPHOME_PSTR("DisconnectRequest"));
|
||||
dump_field(out, ESPHOME_PSTR("reason"), static_cast<enums::DisconnectReason>(this->reason));
|
||||
return out.c_str();
|
||||
}
|
||||
const char *DisconnectResponse::dump_to(DumpBuffer &out) const {
|
||||
@@ -971,6 +982,9 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const {
|
||||
it.dump_to(out);
|
||||
out.append("\n");
|
||||
}
|
||||
#endif
|
||||
#ifdef USE_API_NOISE
|
||||
dump_field(out, ESPHOME_PSTR("api_encryption_provisionable"), this->api_encryption_provisionable);
|
||||
#endif
|
||||
return out.c_str();
|
||||
}
|
||||
|
||||
@@ -51,10 +51,12 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
|
||||
break;
|
||||
}
|
||||
case DisconnectRequest::MESSAGE_TYPE: {
|
||||
DisconnectRequest msg;
|
||||
msg.decode(msg_data, msg_size);
|
||||
#ifdef HAS_PROTO_MESSAGE_DUMP
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_request"));
|
||||
this->log_receive_message_(LOG_STR("on_disconnect_request"), msg);
|
||||
#endif
|
||||
this->on_disconnect_request();
|
||||
this->on_disconnect_request(msg);
|
||||
break;
|
||||
}
|
||||
case DisconnectResponse::MESSAGE_TYPE: {
|
||||
|
||||
@@ -21,7 +21,7 @@ class APIServerConnectionBase {
|
||||
|
||||
void on_hello_request(const HelloRequest &value){};
|
||||
|
||||
void on_disconnect_request(){};
|
||||
void on_disconnect_request(const DisconnectRequest &value){};
|
||||
void on_disconnect_response(){};
|
||||
void on_ping_request(){};
|
||||
void on_ping_response(){};
|
||||
|
||||
@@ -107,8 +107,30 @@ void APIServer::setup() {
|
||||
|
||||
// Initialize last_connected_ for reboot timeout tracking
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
// Set warning status if reboot timeout is enabled
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
|
||||
// Register with the provisioning manager (provisioning:) as a source and
|
||||
// report our current state (provisioned == an encryption key is set). When the
|
||||
// window closes, disconnect any client still attempting to provision so it learns
|
||||
// the reason. The manager owns the timeout, window state and on_timeout automation.
|
||||
if (provisioning::global_provisioning_manager != nullptr) {
|
||||
this->provisioning_source_ = provisioning::global_provisioning_manager->register_source();
|
||||
provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_,
|
||||
this->noise_ctx_.has_psk());
|
||||
provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
|
||||
for (auto &c : this->active_clients()) {
|
||||
DisconnectRequest req;
|
||||
req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED;
|
||||
// Best-effort: if the send buffer is full the reason is dropped, but the
|
||||
// client still learns the window is closed when it reconnects (rejected at
|
||||
// hello) or via the socket close.
|
||||
c->send_message(req);
|
||||
}
|
||||
});
|
||||
}
|
||||
#endif
|
||||
// Set warning status if reboot timeout is enabled (suppressed while provisioning
|
||||
// is pending so the device waits to be onboarded instead of rebooting).
|
||||
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
||||
this->status_set_warning(LOG_STR("waiting for client connection"));
|
||||
}
|
||||
}
|
||||
@@ -121,8 +143,10 @@ void APIServer::loop() {
|
||||
|
||||
if (this->api_connection_count_ == 0) {
|
||||
// Check reboot timeout - done in loop to avoid scheduler heap churn
|
||||
// (cancelled scheduler items sit in heap memory until their scheduled time)
|
||||
if (this->reboot_timeout_ != 0) {
|
||||
// (cancelled scheduler items sit in heap memory until their scheduled time).
|
||||
// Suppressed while a provisioning window is pending so the device waits to be
|
||||
// onboarded / reset instead of rebooting itself; resumes once provisioned.
|
||||
if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
||||
const uint32_t now = App.get_loop_component_start_time();
|
||||
if (now - this->last_connected_ > this->reboot_timeout_) {
|
||||
ESP_LOGE(TAG, "No clients; rebooting");
|
||||
@@ -194,7 +218,8 @@ void APIServer::remove_client_(uint8_t client_index) {
|
||||
this->clients_[last_index].reset();
|
||||
|
||||
// Last client disconnected - set warning and start tracking for reboot timeout
|
||||
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
|
||||
// (suppressed while provisioning is pending - see loop()).
|
||||
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
||||
this->status_set_warning(LOG_STR("waiting for client connection"));
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
@@ -232,7 +257,7 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
|
||||
conn->start();
|
||||
|
||||
// First client connected - clear warning and update timestamp
|
||||
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0) {
|
||||
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) {
|
||||
this->status_clear_warning();
|
||||
this->last_connected_ = App.get_loop_component_start_time();
|
||||
}
|
||||
@@ -401,8 +426,16 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat
|
||||
|
||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
|
||||
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
|
||||
bool has_subscriber = false;
|
||||
for (auto &client : this->active_clients()) {
|
||||
client->send_homeassistant_action(call);
|
||||
has_subscriber |= client->send_homeassistant_action(call);
|
||||
}
|
||||
if (!has_subscriber) {
|
||||
// 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");
|
||||
}
|
||||
}
|
||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
|
||||
@@ -572,8 +605,16 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
|
||||
}
|
||||
|
||||
SavedNoisePsk new_saved_psk{psk};
|
||||
return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
|
||||
make_active);
|
||||
bool result = this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"),
|
||||
make_active);
|
||||
#ifdef USE_PROVISIONING
|
||||
// The device now has a key; report provisioned so the provisioning window is
|
||||
// satisfied and the reboot timeout resumes normal operation.
|
||||
if (result && provisioning::global_provisioning_manager != nullptr) {
|
||||
provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, true);
|
||||
}
|
||||
#endif
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
bool APIServer::clear_noise_psk(bool make_active) {
|
||||
@@ -584,8 +625,16 @@ bool APIServer::clear_noise_psk(bool make_active) {
|
||||
return false;
|
||||
#else
|
||||
SavedNoisePsk empty_psk{};
|
||||
return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
|
||||
make_active);
|
||||
bool result = this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"),
|
||||
make_active);
|
||||
#ifdef USE_PROVISIONING
|
||||
// The key was cleared; report unprovisioned so a subsequent reboot reopens the
|
||||
// provisioning window.
|
||||
if (result && provisioning::global_provisioning_manager != nullptr) {
|
||||
provisioning::global_provisioning_manager->set_source_provisioned(this->provisioning_source_, false);
|
||||
}
|
||||
#endif
|
||||
return result;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
#include "esphome/core/controller.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#ifdef USE_PROVISIONING
|
||||
#include "esphome/components/provisioning/provisioning.h"
|
||||
#endif
|
||||
#ifdef USE_LOGGER
|
||||
#include "esphome/components/logger/logger.h"
|
||||
#endif
|
||||
@@ -255,6 +258,19 @@ class APIServer final : public Component,
|
||||
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
|
||||
void __attribute__((noinline)) remove_client_(uint8_t client_index);
|
||||
|
||||
#ifdef USE_PROVISIONING
|
||||
// True while a configured provisioning window is still pending (the device is
|
||||
// unprovisioned). Suppresses the reboot timeout and its warning so the device is
|
||||
// not auto-rebooted while waiting to be provisioned. False when no provisioning
|
||||
// window is configured.
|
||||
bool provisioning_pending_() const {
|
||||
return provisioning::global_provisioning_manager != nullptr &&
|
||||
provisioning::global_provisioning_manager->window_pending();
|
||||
}
|
||||
#else
|
||||
bool provisioning_pending_() const { return false; }
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
|
||||
bool make_active);
|
||||
@@ -332,7 +348,10 @@ class APIServer final : public Component,
|
||||
uint8_t listen_backlog_{4};
|
||||
bool shutting_down_ = false;
|
||||
uint8_t api_connection_count_{0};
|
||||
// 7 bytes used, 1 byte padding
|
||||
#if defined(USE_PROVISIONING) && defined(USE_API_NOISE)
|
||||
// Index assigned by the provisioning manager for reporting this transport's state.
|
||||
uint8_t provisioning_source_{0};
|
||||
#endif
|
||||
|
||||
#ifdef USE_API_NOISE
|
||||
APINoiseContext noise_ctx_;
|
||||
|
||||
@@ -18,7 +18,7 @@ with warnings.catch_warnings():
|
||||
import contextlib
|
||||
|
||||
from esphome.const import CONF_KEY, CONF_PORT, __version__
|
||||
from esphome.core import CORE, EsphomeError
|
||||
from esphome.core import CORE
|
||||
from esphome.util import safe_print
|
||||
|
||||
from . import CONF_ENCRYPTION
|
||||
@@ -36,15 +36,17 @@ class _LogLineProcessor:
|
||||
"""Feeds incoming log lines to the stack-trace decoder.
|
||||
|
||||
Two responsibilities beyond just calling the decoder:
|
||||
1. Catch EsphomeError. on_log runs inside an asyncio protocol
|
||||
callback; if an exception escapes, the loop tears the transport
|
||||
down with "Fatal error: protocol.data_received() call failed."
|
||||
and ReconnectLogic immediately reconnects, the device replays
|
||||
the same crash trace, and we loop forever.
|
||||
2. Disable decoding after the first failure. _decode_pc shells out
|
||||
to PlatformIO via _run_idedata, which is expensive; a single
|
||||
crash dump can contain many PC/BT lines and we don't want to
|
||||
retry the failing subprocess for each one.
|
||||
1. Catch everything the decoder can raise. aioesphomeapi isolates
|
||||
exceptions raised by log handlers, so an escaping one no longer
|
||||
kills the session, but it does log a full traceback per line. A
|
||||
crash dump carries a PC line plus one per backtrace frame, so the
|
||||
tracebacks bury the dump the user is trying to read. Decoding is a
|
||||
diagnostic nicety; nothing it raises is worth that noise.
|
||||
2. Disable decoding after the first failure. _decode_pc shells out to
|
||||
the toolchain to resolve addr2line, which is expensive; a single
|
||||
crash dump can contain many PC/BT lines and we don't want to retry
|
||||
the failing subprocess for each one. This only works if every
|
||||
failure is caught, which is why 1 is not narrowed to EsphomeError.
|
||||
"""
|
||||
|
||||
def __init__(self, config: dict[str, Any], platform_handler: Any | None) -> None:
|
||||
@@ -61,12 +63,13 @@ class _LogLineProcessor:
|
||||
self.backtrace_state = self._platform_handler(
|
||||
self._config, raw_line, self.backtrace_state
|
||||
)
|
||||
except EsphomeError as exc:
|
||||
except Exception as exc: # noqa: BLE001 # pylint: disable=broad-except
|
||||
self._decode_enabled = False
|
||||
self.backtrace_state = False
|
||||
# _run_idedata raises EsphomeError with no message; fall back
|
||||
# to a generic explanation when str(exc) is empty.
|
||||
detail = str(exc) or "build artifacts not found locally"
|
||||
_LOGGER.debug("Stack-trace decoding failed", exc_info=True)
|
||||
_LOGGER.warning(
|
||||
"Crash trace decoding unavailable: %s. "
|
||||
"Run 'esphome compile' for this device to enable PC decoding.",
|
||||
@@ -152,6 +155,9 @@ async def async_run_logs(
|
||||
name=name,
|
||||
subscribe_states=subscribe_states,
|
||||
allow_plaintext_fallback=True,
|
||||
# A top-level ``deep_sleep:`` block means the device is only awake
|
||||
# briefly; cap the reconnect backoff so a wake window is not missed.
|
||||
deep_sleep="deep_sleep" in config,
|
||||
)
|
||||
try:
|
||||
await asyncio.Event().wait()
|
||||
|
||||
@@ -7,6 +7,7 @@ AQICalculatorType = aqi_ns.enum("AQICalculatorType")
|
||||
|
||||
CONF_AQI = "aqi"
|
||||
CONF_CALCULATION_TYPE = "calculation_type"
|
||||
CONF_EXTENDED_RANGE = "extended_range"
|
||||
|
||||
AQI_CALCULATION_TYPE = {
|
||||
"CAQI": AQICalculatorType.CAQI_TYPE,
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace esphome::aqi {
|
||||
|
||||
class AbstractAQICalculator {
|
||||
public:
|
||||
virtual uint16_t get_aqi(float pm2_5_value, float pm10_0_value) = 0;
|
||||
virtual uint16_t get_aqi(float pm2_5_value, float pm10_0_value, bool extended_range) = 0;
|
||||
};
|
||||
|
||||
} // namespace esphome::aqi
|
||||
|
||||
@@ -11,10 +11,12 @@ namespace esphome::aqi {
|
||||
|
||||
class AQICalculator : public AbstractAQICalculator {
|
||||
public:
|
||||
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
uint16_t get_aqi(float pm2_5_value, float pm10_0_value, bool extended_range) override {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID, extended_range);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID, extended_range);
|
||||
float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f});
|
||||
// extended_range lets the index run past the standard maximum, so clamp to the sensor's range.
|
||||
aqi = std::min(aqi, static_cast<float>(std::numeric_limits<uint16_t>::max()));
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
@@ -30,7 +32,7 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
{35.5f, 55.5f},
|
||||
{55.5f, 125.5f},
|
||||
{125.5f, 225.5f},
|
||||
{225.5f, std::numeric_limits<float>::max()}
|
||||
{225.5f, 500.4f} // EPA 2024: AQI 301-500 maps to PM2.5 225.5-500.4 ug/m3
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
@@ -41,11 +43,11 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
{155.0f, 255.0f},
|
||||
{255.0f, 355.0f},
|
||||
{355.0f, 425.0f},
|
||||
{425.0f, std::numeric_limits<float>::max()}
|
||||
{425.0f, 604.0f} // EPA: AQI 301-500 maps to PM10 425-604 ug/m3 (top of the 401-500 band)
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
static float calculate_index(float value, const float array[NUM_LEVELS][2], bool extended_range) {
|
||||
int grid_index = get_grid_index(value, array);
|
||||
if (grid_index == -1) {
|
||||
return -1.0f;
|
||||
@@ -55,14 +57,22 @@ class AQICalculator : public AbstractAQICalculator {
|
||||
float conc_lo = array[grid_index][0];
|
||||
float conc_hi = array[grid_index][1];
|
||||
|
||||
return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
|
||||
float index = (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
|
||||
|
||||
// Concentrations above the highest breakpoint run the linear fit past aqi_hi. By default we
|
||||
// clamp to the standard maximum; with extended_range we keep the extrapolated "over-range"
|
||||
// value so heavy pollution reports numbers beyond what the standard defines.
|
||||
if (grid_index == NUM_LEVELS - 1 && !extended_range && index > aqi_hi) {
|
||||
return aqi_hi;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
const bool in_range =
|
||||
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
|
||||
: (value < array[i][1])); // others exclusive on hi
|
||||
// The top band is open-ended: any value at or above its lower breakpoint falls into it,
|
||||
// and calculate_index() decides whether to clamp or extrapolate.
|
||||
const bool in_range = (value >= array[i][0]) && (i == NUM_LEVELS - 1 || value < array[i][1]);
|
||||
if (in_range) {
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ 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");
|
||||
if (this->pm_2_5_sensor_ != nullptr) {
|
||||
ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str());
|
||||
}
|
||||
@@ -44,7 +45,7 @@ void AQISensor::calculate_aqi_() {
|
||||
return;
|
||||
}
|
||||
|
||||
uint16_t aqi = calculator->get_aqi(this->pm_2_5_value_, this->pm_10_0_value_);
|
||||
uint16_t aqi = calculator->get_aqi(this->pm_2_5_value_, this->pm_10_0_value_, this->extended_range_);
|
||||
this->publish_state(aqi);
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ class AQISensor final : public sensor::Sensor, public Component {
|
||||
void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; }
|
||||
void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; }
|
||||
void set_aqi_calculation_type(AQICalculatorType type) { this->aqi_calc_type_ = type; }
|
||||
void set_extended_range(bool extended_range) { this->extended_range_ = extended_range; }
|
||||
|
||||
protected:
|
||||
void calculate_aqi_();
|
||||
@@ -21,6 +22,7 @@ class AQISensor final : public sensor::Sensor, public Component {
|
||||
sensor::Sensor *pm_2_5_sensor_{nullptr};
|
||||
sensor::Sensor *pm_10_0_sensor_{nullptr};
|
||||
AQICalculatorType aqi_calc_type_{AQI_TYPE};
|
||||
bool extended_range_{false};
|
||||
AQICalculatorFactory aqi_calculator_factory_;
|
||||
|
||||
float pm_2_5_value_{NAN};
|
||||
|
||||
@@ -9,25 +9,28 @@ namespace esphome::aqi {
|
||||
|
||||
class CAQICalculator : public AbstractAQICalculator {
|
||||
public:
|
||||
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
|
||||
// The CAQI (CITEAIR) scale defines no maximum: its top "Very high" class is simply ">100". We
|
||||
// therefore always extrapolate the top band past 100 without limit, so the extended_range flag
|
||||
// (which lifts the AQI calculator's fixed 500 cap) has no meaning here and is ignored.
|
||||
uint16_t get_aqi(float pm2_5_value, float pm10_0_value, bool /*extended_range*/) override {
|
||||
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
|
||||
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
|
||||
float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f});
|
||||
aqi = std::min(aqi, static_cast<float>(std::numeric_limits<uint16_t>::max()));
|
||||
return static_cast<uint16_t>(std::lround(aqi));
|
||||
}
|
||||
|
||||
protected:
|
||||
static constexpr int NUM_LEVELS = 5;
|
||||
static constexpr int NUM_LEVELS = 4;
|
||||
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
|
||||
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}};
|
||||
|
||||
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
|
||||
// clang-format off
|
||||
{0.0f, 15.1f},
|
||||
{15.1f, 30.1f},
|
||||
{30.1f, 55.1f},
|
||||
{55.1f, 110.1f},
|
||||
{110.1f, std::numeric_limits<float>::max()}
|
||||
{55.1f, 110.1f}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
@@ -36,8 +39,7 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
{0.0f, 25.1f},
|
||||
{25.1f, 50.1f},
|
||||
{50.1f, 90.1f},
|
||||
{90.1f, 180.1f},
|
||||
{180.1f, std::numeric_limits<float>::max()}
|
||||
{90.1f, 180.1f}
|
||||
// clang-format on
|
||||
};
|
||||
|
||||
@@ -52,14 +54,15 @@ class CAQICalculator : public AbstractAQICalculator {
|
||||
float conc_lo = array[grid_index][0];
|
||||
float conc_hi = array[grid_index][1];
|
||||
|
||||
// The top band is open-ended (see get_grid_index), so for concentrations above the last
|
||||
// breakpoint this linear fit extrapolates past 100 unbounded, matching CAQI's open ">100" class.
|
||||
return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
|
||||
}
|
||||
|
||||
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
|
||||
for (int i = 0; i < NUM_LEVELS; i++) {
|
||||
const bool in_range =
|
||||
(value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive
|
||||
: (value < array[i][1])); // others exclusive on hi
|
||||
// The top band is open-ended: any value at or above its lower breakpoint falls into it.
|
||||
const bool in_range = (value >= array[i][0]) && (i == NUM_LEVELS - 1 || value < array[i][1]);
|
||||
if (in_range) {
|
||||
return i;
|
||||
}
|
||||
|
||||
@@ -8,14 +8,25 @@ from esphome.const import (
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, aqi_ns
|
||||
from . import AQI_CALCULATION_TYPE, CONF_CALCULATION_TYPE, CONF_EXTENDED_RANGE, aqi_ns
|
||||
|
||||
CODEOWNERS = ["@jasstrong"]
|
||||
DEPENDENCIES = ["sensor"]
|
||||
|
||||
AQISensor = aqi_ns.class_("AQISensor", sensor.Sensor, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = (
|
||||
|
||||
def _validate_extended_range(config):
|
||||
if CONF_EXTENDED_RANGE in config and config[CONF_CALCULATION_TYPE] == "CAQI":
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_EXTENDED_RANGE}' is not supported with 'calculation_type: CAQI'. "
|
||||
"CAQI has no maximum value by specification, so it is always reported unbounded.",
|
||||
[CONF_EXTENDED_RANGE],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
sensor.sensor_schema(
|
||||
AQISensor,
|
||||
accuracy_decimals=0,
|
||||
@@ -29,9 +40,11 @@ CONFIG_SCHEMA = (
|
||||
cv.Required(CONF_CALCULATION_TYPE): cv.enum(
|
||||
AQI_CALCULATION_TYPE, upper=True
|
||||
),
|
||||
cv.Optional(CONF_EXTENDED_RANGE): cv.boolean,
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
_validate_extended_range,
|
||||
)
|
||||
|
||||
|
||||
@@ -46,3 +59,4 @@ async def to_code(config):
|
||||
cg.add(var.set_pm_10_0_sensor(pm_10_0_sensor))
|
||||
|
||||
cg.add(var.set_aqi_calculation_type(config[CONF_CALCULATION_TYPE]))
|
||||
cg.add(var.set_extended_range(config.get(CONF_EXTENDED_RANGE, False)))
|
||||
|
||||
@@ -24,11 +24,7 @@ void I2CAS3935Component::write_register(uint8_t reg, uint8_t mask, uint8_t bits,
|
||||
|
||||
uint8_t I2CAS3935Component::read_register(uint8_t reg) {
|
||||
uint8_t value;
|
||||
if (write(®, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Writing register failed!");
|
||||
return 0;
|
||||
}
|
||||
if (read(&value, 1) != i2c::ERROR_OK) {
|
||||
if (!this->read_byte(reg, &value)) {
|
||||
ESP_LOGW(TAG, "Reading register failed!");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from esphome.const import (
|
||||
CONF_CLEAR,
|
||||
CONF_GAIN,
|
||||
CONF_ID,
|
||||
DEVICE_CLASS_ILLUMINANCE,
|
||||
DEVICE_CLASS_EMPTY,
|
||||
ICON_BRIGHTNESS_5,
|
||||
STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
@@ -54,7 +54,7 @@ SENSOR_SCHEMA = sensor.sensor_schema(
|
||||
unit_of_measurement=UNIT_COUNTS,
|
||||
icon=ICON_BRIGHTNESS_5,
|
||||
accuracy_decimals=0,
|
||||
device_class=DEVICE_CLASS_ILLUMINANCE,
|
||||
device_class=DEVICE_CLASS_EMPTY,
|
||||
state_class=STATE_CLASS_MEASUREMENT,
|
||||
)
|
||||
|
||||
|
||||
@@ -65,12 +65,11 @@ optional<ParseResult> ATCMiThermometer::parse_header_(const esp32_ble_tracker::S
|
||||
return {};
|
||||
}
|
||||
|
||||
static uint8_t last_frame_count = 0;
|
||||
if (last_frame_count == raw[12]) {
|
||||
ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", last_frame_count);
|
||||
if (this->last_frame_count_ == raw[12]) {
|
||||
ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", this->last_frame_count_);
|
||||
return {};
|
||||
}
|
||||
last_frame_count = raw[12];
|
||||
this->last_frame_count_ = raw[12];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ class ATCMiThermometer final : public Component, public esp32_ble_tracker::ESPBT
|
||||
sensor::Sensor *battery_voltage_{nullptr};
|
||||
sensor::Sensor *signal_strength_{nullptr};
|
||||
|
||||
uint8_t last_frame_count_{0};
|
||||
|
||||
optional<ParseResult> parse_header_(const esp32_ble_tracker::ServiceData &service_data);
|
||||
bool parse_message_(const std::vector<uint8_t> &message, ParseResult &result);
|
||||
bool report_results_(const optional<ParseResult> &result, const char *address);
|
||||
|
||||
@@ -113,7 +113,9 @@ def read_audio_file_and_type(file_config: ConfigType) -> tuple[bytes, MockObj]:
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["NONE"]
|
||||
if file_type == "wav":
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["WAV"]
|
||||
elif file_type in ("mp3", "mpeg", "mpga"):
|
||||
elif file_type in ("mp1", "mp2", "mp3", "mpeg", "mpga"):
|
||||
# With puremagic >=2.0 this can cause some MP3 (Layer III) files to be labeled as "mp1"/"mp2".
|
||||
# Treat those labels as MP3 so we still pick the MP3 decoder.
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["MP3"]
|
||||
elif file_type == "flac":
|
||||
media_file_type = audio.AUDIO_FILE_TYPE_ENUM["FLAC"]
|
||||
|
||||
@@ -37,15 +37,15 @@ namespace esphome::beken_spi_led_strip {
|
||||
|
||||
static const char *const TAG = "beken_spi_led_strip";
|
||||
|
||||
struct spi_data_t {
|
||||
struct SpiData {
|
||||
SemaphoreHandle_t dma_tx_semaphore;
|
||||
volatile bool tx_in_progress;
|
||||
bool first_run;
|
||||
};
|
||||
|
||||
static spi_data_t *spi_data = nullptr;
|
||||
static SpiData *spi_data = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
static void set_spi_ctrl_register(unsigned long bit, bool val) {
|
||||
static void set_spi_ctrl_register(uint32_t bit, bool val) {
|
||||
uint32_t value = REG_READ(SPI_CTRL);
|
||||
if (val == 0) {
|
||||
value &= ~bit;
|
||||
@@ -55,7 +55,7 @@ static void set_spi_ctrl_register(unsigned long bit, bool val) {
|
||||
REG_WRITE(SPI_CTRL, value);
|
||||
}
|
||||
|
||||
static void set_spi_config_register(unsigned long bit, bool val) {
|
||||
static void set_spi_config_register(uint32_t bit, bool val) {
|
||||
uint32_t value = REG_READ(SPI_CONFIG);
|
||||
if (val == 0) {
|
||||
value &= ~bit;
|
||||
@@ -67,7 +67,7 @@ static void set_spi_config_register(unsigned long bit, bool val) {
|
||||
|
||||
void spi_dma_tx_enable(bool enable) {
|
||||
GDMA_CFG_ST en_cfg;
|
||||
set_spi_config_register(SPI_TX_EN, enable ? 1 : 0);
|
||||
set_spi_config_register(SPI_TX_EN, enable);
|
||||
en_cfg.channel = SPI_TX_DMA_CHANNEL;
|
||||
en_cfg.param = enable ? 1 : 0;
|
||||
sddev_control(GDMA_DEV_NAME, CMD_GDMA_SET_DMA_ENABLE, &en_cfg);
|
||||
@@ -110,13 +110,13 @@ static void spi_set_clock(uint32_t max_hz) {
|
||||
param &= ~(SPI_CKR_MASK << SPI_CKR_POSI);
|
||||
param |= (div << SPI_CKR_POSI);
|
||||
REG_WRITE(SPI_CTRL, param);
|
||||
ESP_LOGD(TAG, "target frequency: %d, actual frequency: %d", max_hz, source_clk / 2 / div);
|
||||
ESP_LOGD(TAG, "target frequency: %" PRIu32 ", actual frequency: %d", max_hz, source_clk / 2 / div);
|
||||
}
|
||||
|
||||
void spi_dma_tx_finish_callback(unsigned int param) {
|
||||
spi_data->tx_in_progress = false;
|
||||
xSemaphoreGive(spi_data->dma_tx_semaphore);
|
||||
spi_dma_tx_enable(0);
|
||||
spi_dma_tx_enable(false);
|
||||
}
|
||||
|
||||
void BekenSPILEDStripLightOutput::setup() {
|
||||
@@ -161,7 +161,7 @@ void BekenSPILEDStripLightOutput::setup() {
|
||||
return;
|
||||
}
|
||||
|
||||
spi_data = (spi_data_t *) calloc(1, sizeof(spi_data_t));
|
||||
spi_data = (SpiData *) calloc(1, sizeof(SpiData)); // NOLINT(cppcoreguidelines-no-malloc)
|
||||
if (spi_data == nullptr) {
|
||||
ESP_LOGE(TAG, "Cannot allocate spi_data!");
|
||||
this->mark_failed();
|
||||
@@ -177,20 +177,20 @@ void BekenSPILEDStripLightOutput::setup() {
|
||||
|
||||
spi_data->first_run = true;
|
||||
|
||||
set_spi_ctrl_register(MSTEN, 0);
|
||||
set_spi_ctrl_register(BIT_WDTH, 0);
|
||||
set_spi_ctrl_register(MSTEN, false);
|
||||
set_spi_ctrl_register(BIT_WDTH, false);
|
||||
spi_set_clock(this->spi_frequency_);
|
||||
set_spi_ctrl_register(CKPOL, 0);
|
||||
set_spi_ctrl_register(CKPHA, 0);
|
||||
set_spi_ctrl_register(MSTEN, 1);
|
||||
set_spi_ctrl_register(SPIEN, 1);
|
||||
set_spi_ctrl_register(CKPOL, false);
|
||||
set_spi_ctrl_register(CKPHA, false);
|
||||
set_spi_ctrl_register(MSTEN, true);
|
||||
set_spi_ctrl_register(SPIEN, true);
|
||||
|
||||
set_spi_ctrl_register(TXINT_EN, 0);
|
||||
set_spi_ctrl_register(RXINT_EN, 0);
|
||||
set_spi_config_register(SPI_TX_FINISH_EN, 1);
|
||||
set_spi_config_register(SPI_RX_FINISH_EN, 1);
|
||||
set_spi_ctrl_register(RXOVR_EN, 0);
|
||||
set_spi_ctrl_register(TXOVR_EN, 0);
|
||||
set_spi_ctrl_register(TXINT_EN, false);
|
||||
set_spi_ctrl_register(RXINT_EN, false);
|
||||
set_spi_config_register(SPI_TX_FINISH_EN, true);
|
||||
set_spi_config_register(SPI_RX_FINISH_EN, true);
|
||||
set_spi_ctrl_register(RXOVR_EN, false);
|
||||
set_spi_ctrl_register(TXOVR_EN, false);
|
||||
|
||||
value = REG_READ(SPI_CTRL);
|
||||
value &= ~CTRL_NSSMD_3;
|
||||
@@ -199,7 +199,7 @@ void BekenSPILEDStripLightOutput::setup() {
|
||||
|
||||
value = GFUNC_MODE_SPI_DMA;
|
||||
sddev_control(GPIO_DEV_NAME, CMD_GPIO_ENABLE_SECOND, &value);
|
||||
set_spi_ctrl_register(SPI_S_CS_UP_INT_EN, 0);
|
||||
set_spi_ctrl_register(SPI_S_CS_UP_INT_EN, false);
|
||||
|
||||
GDMA_CFG_ST en_cfg;
|
||||
GDMACFG_TPYES_ST init_cfg;
|
||||
@@ -210,7 +210,7 @@ void BekenSPILEDStripLightOutput::setup() {
|
||||
init_cfg.dstptr_incr = 0;
|
||||
init_cfg.srcptr_incr = 1;
|
||||
init_cfg.src_start_addr = this->dma_buf_;
|
||||
init_cfg.dst_start_addr = (void *) SPI_DAT; // SPI_DMA_REG4_TXFIFO
|
||||
init_cfg.dst_start_addr = (void *) SPI_DAT; // NOLINT(performance-no-int-to-ptr) SPI_DMA_REG4_TXFIFO
|
||||
init_cfg.channel = SPI_TX_DMA_CHANNEL;
|
||||
init_cfg.prio = 0; // 10
|
||||
init_cfg.u.type4.src_loop_start_addr = this->dma_buf_;
|
||||
@@ -230,7 +230,7 @@ void BekenSPILEDStripLightOutput::setup() {
|
||||
en_cfg.param = 0;
|
||||
sddev_control(GDMA_DEV_NAME, CMD_GDMA_CFG_SRCADDR_LOOP, &en_cfg);
|
||||
|
||||
spi_dma_tx_enable(0);
|
||||
spi_dma_tx_enable(false);
|
||||
|
||||
value = REG_READ(SPI_CONFIG);
|
||||
value &= ~(0xFFF << 8);
|
||||
@@ -247,7 +247,8 @@ void BekenSPILEDStripLightOutput::set_led_params(uint8_t bit0, uint8_t bit1, uin
|
||||
void BekenSPILEDStripLightOutput::write_state(light::LightState *state) {
|
||||
// protect from refreshing too often
|
||||
uint32_t now = micros();
|
||||
if (*this->max_refresh_rate_ != 0 && (now - this->last_refresh_) < *this->max_refresh_rate_) {
|
||||
if (this->max_refresh_rate_.has_value() && *this->max_refresh_rate_ != 0 &&
|
||||
(now - this->last_refresh_) < *this->max_refresh_rate_) {
|
||||
// try again next loop iteration, so that this change won't get lost
|
||||
this->schedule_show();
|
||||
return;
|
||||
@@ -293,7 +294,7 @@ void BekenSPILEDStripLightOutput::write_state(light::LightState *state) {
|
||||
}
|
||||
|
||||
spi_data->first_run = false;
|
||||
spi_dma_tx_enable(1);
|
||||
spi_dma_tx_enable(true);
|
||||
|
||||
this->status_clear_warning();
|
||||
}
|
||||
@@ -376,7 +377,7 @@ void BekenSPILEDStripLightOutput::dump_config() {
|
||||
" RGB Order: %s\n"
|
||||
" Max refresh rate: %" PRIu32 "\n"
|
||||
" Number of LEDs: %u",
|
||||
rgb_order, *this->max_refresh_rate_, this->num_leds_);
|
||||
rgb_order, this->max_refresh_rate_.value_or(0), this->num_leds_);
|
||||
}
|
||||
|
||||
float BekenSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
|
||||
@@ -448,7 +448,9 @@ _BINARY_SENSOR_SCHEMA = (
|
||||
cv.Exclusive(
|
||||
CONF_TRIGGER_ON_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
cv.Optional(CONF_FILTERS): validate_filters,
|
||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
|
||||
cv.Optional(CONF_ON_RELEASE): automation.validate_automation({}),
|
||||
|
||||
@@ -245,7 +245,9 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
service_resp.characteristics.init(total_char_count);
|
||||
uint16_t char_offset = 0;
|
||||
esp_gattc_char_elem_t char_result;
|
||||
while (true) { // characteristics
|
||||
// Bound by total_char_count: the vector is sized for it, and a malicious peripheral
|
||||
// can make enumeration return more entries than the count query reported
|
||||
while (char_offset < total_char_count) { // characteristics
|
||||
uint16_t char_count = 1;
|
||||
esp_gatt_status_t char_status =
|
||||
esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, service_result.start_handle,
|
||||
@@ -287,7 +289,7 @@ void BluetoothConnection::send_service_for_discovery_() {
|
||||
characteristic_resp.descriptors.init(total_desc_count);
|
||||
uint16_t desc_offset = 0;
|
||||
esp_gattc_descr_elem_t desc_result;
|
||||
while (true) { // descriptors
|
||||
while (desc_offset < total_desc_count) { // descriptors
|
||||
uint16_t desc_count = 1;
|
||||
esp_gatt_status_t desc_status = esp_ble_gattc_get_all_descr(
|
||||
this->gattc_if_, this->conn_id_, char_result.char_handle, &desc_result, &desc_count, desc_offset);
|
||||
|
||||
@@ -50,7 +50,9 @@ _BUTTON_SCHEMA = (
|
||||
.extend(
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTButtonComponent),
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
cv.Optional(CONF_ON_PRESS): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -21,6 +21,7 @@ MULTI_CONF = True
|
||||
|
||||
ns = cg.esphome_ns.namespace("cc1101")
|
||||
CC1101Component = ns.class_("CC1101Component", cg.Component, spi.SPIDevice)
|
||||
CC1101Listener = ns.class_("CC1101Listener")
|
||||
|
||||
# Config keys
|
||||
CONF_RX_ATTENUATION = "rx_attenuation"
|
||||
@@ -48,6 +49,15 @@ CONF_FILTER_LENGTH_FSK_MSK = "filter_length_fsk_msk"
|
||||
CONF_FILTER_LENGTH_ASK_OOK = "filter_length_ask_ook"
|
||||
CONF_FREEZE = "freeze"
|
||||
CONF_HYST_LEVEL = "hyst_level"
|
||||
CONF_FOC_BS_CS_GATE = "foc_bs_cs_gate"
|
||||
CONF_FOC_LIMIT = "foc_limit"
|
||||
CONF_FOC_PRE_K = "foc_pre_k"
|
||||
CONF_FOC_POST_K = "foc_post_k"
|
||||
CONF_BS_LIMIT = "bs_limit"
|
||||
CONF_BS_PRE_KI = "bs_pre_ki"
|
||||
CONF_BS_PRE_KP = "bs_pre_kp"
|
||||
CONF_BS_POST_KI = "bs_post_ki"
|
||||
CONF_BS_POST_KP = "bs_post_kp"
|
||||
|
||||
# Packet mode config keys
|
||||
CONF_PACKET_MODE = "packet_mode"
|
||||
@@ -161,6 +171,64 @@ HYST_LEVEL = {
|
||||
"High": HystLevel.HYST_LEVEL_HIGH,
|
||||
}
|
||||
|
||||
FocLimit = ns.enum("FocLimit", True)
|
||||
FOC_LIMIT = {
|
||||
"Disabled": FocLimit.FOC_LIMIT_DISABLED,
|
||||
"BW/8": FocLimit.FOC_LIMIT_BW_8,
|
||||
"BW/4": FocLimit.FOC_LIMIT_BW_4,
|
||||
"BW/2": FocLimit.FOC_LIMIT_BW_2,
|
||||
}
|
||||
|
||||
FocPreK = ns.enum("FocPreK", True)
|
||||
FOC_PRE_K = {
|
||||
"K": FocPreK.FOC_PRE_K_K,
|
||||
"2K": FocPreK.FOC_PRE_K_2K,
|
||||
"3K": FocPreK.FOC_PRE_K_3K,
|
||||
"4K": FocPreK.FOC_PRE_K_4K,
|
||||
}
|
||||
|
||||
FocPostK = ns.enum("FocPostK", True)
|
||||
FOC_POST_K = {
|
||||
"Same": FocPostK.FOC_POST_K_SAME,
|
||||
"K/2": FocPostK.FOC_POST_K_K_2,
|
||||
}
|
||||
|
||||
BsLimit = ns.enum("BsLimit", True)
|
||||
BS_LIMIT = {
|
||||
"Disabled": BsLimit.BS_LIMIT_DISABLED,
|
||||
"3.125%": BsLimit.BS_LIMIT_3P125_PERCENT,
|
||||
"6.25%": BsLimit.BS_LIMIT_6P25_PERCENT,
|
||||
"12.5%": BsLimit.BS_LIMIT_12P5_PERCENT,
|
||||
}
|
||||
|
||||
BsPreKi = ns.enum("BsPreKi", True)
|
||||
BS_PRE_KI = {
|
||||
"KI": BsPreKi.BS_PRE_KI_KI,
|
||||
"2KI": BsPreKi.BS_PRE_KI_2KI,
|
||||
"3KI": BsPreKi.BS_PRE_KI_3KI,
|
||||
"4KI": BsPreKi.BS_PRE_KI_4KI,
|
||||
}
|
||||
|
||||
BsPreKp = ns.enum("BsPreKp", True)
|
||||
BS_PRE_KP = {
|
||||
"KP": BsPreKp.BS_PRE_KP_KP,
|
||||
"2KP": BsPreKp.BS_PRE_KP_2KP,
|
||||
"3KP": BsPreKp.BS_PRE_KP_3KP,
|
||||
"4KP": BsPreKp.BS_PRE_KP_4KP,
|
||||
}
|
||||
|
||||
BsPostKi = ns.enum("BsPostKi", True)
|
||||
BS_POST_KI = {
|
||||
"Same": BsPostKi.BS_POST_KI_SAME,
|
||||
"KI/2": BsPostKi.BS_POST_KI_KI_2,
|
||||
}
|
||||
|
||||
BsPostKp = ns.enum("BsPostKp", True)
|
||||
BS_POST_KP = {
|
||||
"Same": BsPostKp.BS_POST_KP_SAME,
|
||||
"KP": BsPostKp.BS_POST_KP_KP,
|
||||
}
|
||||
|
||||
# Optional settings to generate setter calls for
|
||||
CONFIG_MAP = {
|
||||
cv.Optional(CONF_OUTPUT_POWER, default=10): cv.float_range(min=-30.0, max=11.0),
|
||||
@@ -214,6 +282,15 @@ CONFIG_MAP = {
|
||||
cv.Optional(CONF_FREEZE): cv.enum(FREEZE, upper=False),
|
||||
cv.Optional(CONF_WAIT_TIME, default="32"): cv.enum(WAIT_TIME, upper=False),
|
||||
cv.Optional(CONF_HYST_LEVEL): cv.enum(HYST_LEVEL, upper=False),
|
||||
cv.Optional(CONF_FOC_BS_CS_GATE): cv.boolean,
|
||||
cv.Optional(CONF_FOC_LIMIT): cv.enum(FOC_LIMIT, upper=False),
|
||||
cv.Optional(CONF_FOC_PRE_K): cv.enum(FOC_PRE_K, upper=False),
|
||||
cv.Optional(CONF_FOC_POST_K): cv.enum(FOC_POST_K, upper=False),
|
||||
cv.Optional(CONF_BS_LIMIT): cv.enum(BS_LIMIT, upper=False),
|
||||
cv.Optional(CONF_BS_PRE_KI): cv.enum(BS_PRE_KI, upper=False),
|
||||
cv.Optional(CONF_BS_PRE_KP): cv.enum(BS_PRE_KP, upper=False),
|
||||
cv.Optional(CONF_BS_POST_KI): cv.enum(BS_POST_KI, upper=False),
|
||||
cv.Optional(CONF_BS_POST_KP): cv.enum(BS_POST_KP, upper=False),
|
||||
cv.Optional(CONF_PACKET_MODE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_PACKET_LENGTH): cv.uint8_t,
|
||||
cv.Optional(CONF_CRC_ENABLE, default=False): cv.boolean,
|
||||
|
||||
@@ -672,6 +672,69 @@ void CC1101Component::set_hyst_level(HystLevel value) {
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_foc_bs_cs_gate(bool value) {
|
||||
this->state_.FOC_BS_CS_GATE = value ? 1 : 0;
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::FOCCFG);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_foc_limit(FocLimit value) {
|
||||
this->state_.FOC_LIMIT = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::FOCCFG);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_foc_pre_k(FocPreK value) {
|
||||
this->state_.FOC_PRE_K = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::FOCCFG);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_foc_post_k(FocPostK value) {
|
||||
this->state_.FOC_POST_K = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::FOCCFG);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_bs_limit(BsLimit value) {
|
||||
this->state_.BS_LIMIT = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::BSCFG);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_bs_pre_ki(BsPreKi value) {
|
||||
this->state_.BS_PRE_KI = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::BSCFG);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_bs_pre_kp(BsPreKp value) {
|
||||
this->state_.BS_PRE_KP = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::BSCFG);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_bs_post_ki(BsPostKi value) {
|
||||
this->state_.BS_POST_KI = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::BSCFG);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_bs_post_kp(BsPostKp value) {
|
||||
this->state_.BS_POST_KP = static_cast<uint8_t>(value);
|
||||
if (this->initialized_) {
|
||||
this->write_(Register::BSCFG);
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::set_packet_mode(bool value) {
|
||||
this->state_.PKT_FORMAT =
|
||||
static_cast<uint8_t>(value ? PacketFormat::PACKET_FORMAT_FIFO : PacketFormat::PACKET_FORMAT_ASYNC_SERIAL);
|
||||
|
||||
@@ -71,6 +71,17 @@ class CC1101Component final : public Component,
|
||||
void set_wait_time(WaitTime value);
|
||||
void set_hyst_level(HystLevel value);
|
||||
|
||||
// Frequency offset compensation and bit synchronization settings
|
||||
void set_foc_bs_cs_gate(bool value);
|
||||
void set_foc_limit(FocLimit value);
|
||||
void set_foc_pre_k(FocPreK value);
|
||||
void set_foc_post_k(FocPostK value);
|
||||
void set_bs_limit(BsLimit value);
|
||||
void set_bs_pre_ki(BsPreKi value);
|
||||
void set_bs_pre_kp(BsPreKp value);
|
||||
void set_bs_post_ki(BsPostKi value);
|
||||
void set_bs_post_kp(BsPostKp value);
|
||||
|
||||
// Packet mode settings
|
||||
void set_packet_mode(bool value);
|
||||
void set_packet_length(uint8_t value);
|
||||
|
||||
@@ -231,6 +231,56 @@ enum class HystLevel : uint8_t {
|
||||
HYST_LEVEL_HIGH,
|
||||
};
|
||||
|
||||
enum class FocLimit : uint8_t {
|
||||
FOC_LIMIT_DISABLED,
|
||||
FOC_LIMIT_BW_8,
|
||||
FOC_LIMIT_BW_4,
|
||||
FOC_LIMIT_BW_2,
|
||||
};
|
||||
|
||||
enum class FocPreK : uint8_t {
|
||||
FOC_PRE_K_K,
|
||||
FOC_PRE_K_2K,
|
||||
FOC_PRE_K_3K,
|
||||
FOC_PRE_K_4K,
|
||||
};
|
||||
|
||||
enum class FocPostK : uint8_t {
|
||||
FOC_POST_K_SAME,
|
||||
FOC_POST_K_K_2,
|
||||
};
|
||||
|
||||
enum class BsLimit : uint8_t {
|
||||
BS_LIMIT_DISABLED,
|
||||
BS_LIMIT_3P125_PERCENT,
|
||||
BS_LIMIT_6P25_PERCENT,
|
||||
BS_LIMIT_12P5_PERCENT,
|
||||
};
|
||||
|
||||
enum class BsPreKi : uint8_t {
|
||||
BS_PRE_KI_KI,
|
||||
BS_PRE_KI_2KI,
|
||||
BS_PRE_KI_3KI,
|
||||
BS_PRE_KI_4KI,
|
||||
};
|
||||
|
||||
enum class BsPreKp : uint8_t {
|
||||
BS_PRE_KP_KP,
|
||||
BS_PRE_KP_2KP,
|
||||
BS_PRE_KP_3KP,
|
||||
BS_PRE_KP_4KP,
|
||||
};
|
||||
|
||||
enum class BsPostKi : uint8_t {
|
||||
BS_POST_KI_SAME,
|
||||
BS_POST_KI_KI_2,
|
||||
};
|
||||
|
||||
enum class BsPostKp : uint8_t {
|
||||
BS_POST_KP_SAME,
|
||||
BS_POST_KP_KP,
|
||||
};
|
||||
|
||||
enum class PacketFormat : uint8_t {
|
||||
PACKET_FORMAT_FIFO,
|
||||
PACKET_FORMAT_SYNC_SERIAL,
|
||||
|
||||
@@ -131,7 +131,9 @@ _COVER_SCHEMA = (
|
||||
cv.Optional(CONF_MQTT_JSON_STATE_PAYLOAD): cv.All(
|
||||
cv.requires_component("mqtt"), cv.boolean
|
||||
),
|
||||
cv.Optional(CONF_DEVICE_CLASS): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): cv.one_of(*DEVICE_CLASSES, lower=True),
|
||||
cv.Optional(CONF_POSITION_COMMAND_TOPIC): cv.All(
|
||||
cv.requires_component("mqtt"), cv.subscribe_topic
|
||||
),
|
||||
|
||||
@@ -28,7 +28,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
ESP_LOGD(TAG,
|
||||
"LibreTiny debug info:\n"
|
||||
" Version: %s\n"
|
||||
" Chip: %s (%04x) @ %u MHz\n"
|
||||
" Chip: %s (%04x) @ %" PRIu32 " MHz\n"
|
||||
" Chip ID: 0x%06" PRIX32 "\n"
|
||||
" Board: %s\n"
|
||||
" Flash: %" PRIu32 " KiB\n"
|
||||
@@ -38,7 +38,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
lt_get_board_code(), flash_kib, ram_kib, reset_reason);
|
||||
|
||||
pos = buf_append_str(buf, size, pos, "|Version: ");
|
||||
pos = buf_append_str(buf, size, pos, LT_BANNER_STR + 10);
|
||||
pos = buf_append_str(buf, size, pos, <_BANNER_STR[10]);
|
||||
pos = buf_append_str(buf, size, pos, "|Reset Reason: ");
|
||||
pos = buf_append_str(buf, size, pos, reset_reason);
|
||||
pos = buf_append_str(buf, size, pos, "|Chip Name: ");
|
||||
|
||||
@@ -74,7 +74,7 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||
char *buf = buffer.data();
|
||||
|
||||
uint32_t cpu_freq = ::rp2040.f_cpu();
|
||||
uint32_t cpu_freq = RP2040::f_cpu();
|
||||
ESP_LOGD(TAG, "CPU Frequency: %" PRIu32, cpu_freq);
|
||||
pos = buf_append_printf(buf, size, pos, "|CPU Frequency: %" PRIu32, cpu_freq);
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from esphome.const import (
|
||||
CONF_SECOND,
|
||||
CONF_SLEEP_DURATION,
|
||||
CONF_TIME_ID,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_WAKEUP_PIN,
|
||||
PLATFORM_BK72XX,
|
||||
PLATFORM_ESP32,
|
||||
@@ -234,6 +235,15 @@ EXT1_WAKEUP_MODES = {
|
||||
}
|
||||
WakeupCauseToRunDuration = deep_sleep_ns.struct("WakeupCauseToRunDuration")
|
||||
|
||||
WakeupCause = deep_sleep_ns.enum("WakeupCause")
|
||||
WakeTrigger = deep_sleep_ns.class_(
|
||||
"WakeTrigger", automation.Trigger.template(WakeupCause), cg.Component
|
||||
)
|
||||
Ext1WakeTrigger = deep_sleep_ns.class_(
|
||||
"Ext1WakeTrigger", automation.Trigger.template(), cg.Component
|
||||
)
|
||||
|
||||
CONF_ON_WAKE = "on_wake"
|
||||
CONF_WAKEUP_PIN_MODE = "wakeup_pin_mode"
|
||||
CONF_ESP32_EXT1_WAKEUP = "esp32_ext1_wakeup"
|
||||
CONF_TOUCH_WAKEUP = "touch_wakeup"
|
||||
@@ -256,6 +266,22 @@ WAKEUP_PIN_SCHEMA = cv.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
EXT1_WAKEUP_PIN_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PIN): cv.All(
|
||||
pins.internal_gpio_input_pin_schema, validate_pin_number_esp32
|
||||
),
|
||||
cv.Optional(CONF_ON_WAKE): automation.validate_automation(
|
||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(Ext1WakeTrigger)}
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
# Entries that are not in the {pin: ..., on_wake: ...} form are treated as a
|
||||
# bare pin config (the original syntax, e.g. a plain "GPIO5" or {number: 5}).
|
||||
validate_ext1_wakeup_pin = cv.maybe_simple_value(EXT1_WAKEUP_PIN_SCHEMA, key=CONF_PIN)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
@@ -282,8 +308,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_PINS): cv.ensure_list(
|
||||
pins.internal_gpio_input_pin_schema,
|
||||
validate_pin_number_esp32,
|
||||
validate_ext1_wakeup_pin,
|
||||
),
|
||||
cv.Required(CONF_MODE): cv.All(
|
||||
cv.enum(EXT1_WAKEUP_MODES, upper=True),
|
||||
@@ -292,6 +317,12 @@ CONFIG_SCHEMA = cv.All(
|
||||
}
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_ON_WAKE): cv.All(
|
||||
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
|
||||
automation.validate_automation(
|
||||
{cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(WakeTrigger)}
|
||||
),
|
||||
),
|
||||
cv.Optional(CONF_TOUCH_WAKEUP): cv.All(
|
||||
cv.only_on_esp32,
|
||||
esp32.only_on_variant(
|
||||
@@ -323,7 +354,7 @@ async def to_code(config):
|
||||
if CONF_WAKEUP_PIN in config:
|
||||
pins_as_list = config.get(CONF_WAKEUP_PIN, [])
|
||||
if CORE.is_bk72xx:
|
||||
cg.add(var.init_wakeup_pins_(len(pins_as_list)))
|
||||
cg.add(var.init_wakeup_pins(len(pins_as_list)))
|
||||
for item in pins_as_list:
|
||||
cg.add(
|
||||
var.add_wakeup_pin(
|
||||
@@ -362,16 +393,27 @@ async def to_code(config):
|
||||
)
|
||||
cg.add(var.set_run_duration(wakeup_cause_to_run_duration))
|
||||
|
||||
if CONF_ESP32_EXT1_WAKEUP in config:
|
||||
conf = config[CONF_ESP32_EXT1_WAKEUP]
|
||||
if (ext1_conf := config.get(CONF_ESP32_EXT1_WAKEUP)) is not None:
|
||||
mask = 0
|
||||
for pin in conf[CONF_PINS]:
|
||||
mask |= 1 << pin[CONF_NUMBER]
|
||||
for pin_conf in ext1_conf[CONF_PINS]:
|
||||
number = pin_conf[CONF_PIN][CONF_NUMBER]
|
||||
mask |= 1 << number
|
||||
for wake_conf in pin_conf.get(CONF_ON_WAKE, []):
|
||||
trigger = cg.new_Pvariable(wake_conf[CONF_TRIGGER_ID], number)
|
||||
await cg.register_component(trigger, wake_conf)
|
||||
await automation.build_automation(trigger, [], wake_conf)
|
||||
cg.add_define("USE_DEEP_SLEEP_ON_WAKE")
|
||||
struct = cg.StructInitializer(
|
||||
Ext1Wakeup, ("mask", mask), ("wakeup_mode", conf[CONF_MODE])
|
||||
Ext1Wakeup, ("mask", mask), ("wakeup_mode", ext1_conf[CONF_MODE])
|
||||
)
|
||||
cg.add(var.set_ext1_wakeup(struct))
|
||||
|
||||
for wake_conf in config.get(CONF_ON_WAKE, []):
|
||||
trigger = cg.new_Pvariable(wake_conf[CONF_TRIGGER_ID])
|
||||
await cg.register_component(trigger, wake_conf)
|
||||
await automation.build_automation(trigger, [(WakeupCause, "cause")], wake_conf)
|
||||
cg.add_define("USE_DEEP_SLEEP_ON_WAKE")
|
||||
|
||||
if CONF_TOUCH_WAKEUP in config:
|
||||
cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP]))
|
||||
if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations:
|
||||
|
||||
@@ -7,6 +7,21 @@ namespace esphome::deep_sleep {
|
||||
|
||||
static const char *const TAG = "deep_sleep.bk72xx";
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
WakeupCause get_wakeup_cause() {
|
||||
switch (lt_get_reboot_reason()) {
|
||||
case REBOOT_REASON_SLEEP_GPIO:
|
||||
return WAKEUP_CAUSE_GPIO;
|
||||
case REBOOT_REASON_SLEEP_RTC:
|
||||
return WAKEUP_CAUSE_TIMER;
|
||||
case REBOOT_REASON_SLEEP_USB:
|
||||
return WAKEUP_CAUSE_UNKNOWN;
|
||||
default:
|
||||
return WAKEUP_CAUSE_NONE;
|
||||
}
|
||||
}
|
||||
#endif // USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
void DeepSleepComponent::dump_config_platform_() {
|
||||
@@ -15,15 +30,15 @@ void DeepSleepComponent::dump_config_platform_() {
|
||||
}
|
||||
}
|
||||
|
||||
bool DeepSleepComponent::pin_prevents_sleep_(WakeUpPinItem &pinItem) const {
|
||||
return (pinItem.wakeup_pin_mode == WAKEUP_PIN_MODE_KEEP_AWAKE && pinItem.wakeup_pin != nullptr &&
|
||||
!this->sleep_duration_.has_value() && (pinItem.wakeup_level == get_real_pin_state_(*pinItem.wakeup_pin)));
|
||||
bool DeepSleepComponent::pin_prevents_sleep_(WakeUpPinItem &pin_item) const {
|
||||
return (pin_item.wakeup_pin_mode == WAKEUP_PIN_MODE_KEEP_AWAKE && pin_item.wakeup_pin != nullptr &&
|
||||
!this->sleep_duration_.has_value() && (pin_item.wakeup_level == get_real_pin_state_(*pin_item.wakeup_pin)));
|
||||
}
|
||||
|
||||
bool DeepSleepComponent::prepare_to_sleep_() {
|
||||
if (wakeup_pins_.size() > 0) {
|
||||
if (!this->wakeup_pins_.empty()) {
|
||||
for (WakeUpPinItem &item : this->wakeup_pins_) {
|
||||
if (pin_prevents_sleep_(item)) {
|
||||
if (this->pin_prevents_sleep_(item)) {
|
||||
// Defer deep sleep until inactive
|
||||
if (!this->next_enter_deep_sleep_) {
|
||||
this->status_set_warning();
|
||||
@@ -44,7 +59,7 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
item.wakeup_level = !item.wakeup_level;
|
||||
}
|
||||
}
|
||||
ESP_LOGI(TAG, "Wake-up on P%u %s (%d)", item.wakeup_pin->get_pin(), item.wakeup_level ? "HIGH" : "LOW",
|
||||
ESP_LOGI(TAG, "Wake-up on P%u %s (%" PRId32 ")", item.wakeup_pin->get_pin(), item.wakeup_level ? "HIGH" : "LOW",
|
||||
static_cast<int32_t>(item.wakeup_pin_mode));
|
||||
}
|
||||
|
||||
|
||||
@@ -60,6 +60,65 @@ struct WakeupCauseToRunDuration {
|
||||
|
||||
#endif // USE_ESP32
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
/// Why the device woke from deep sleep. Passed to on_wake automations.
|
||||
enum WakeupCause : uint8_t {
|
||||
/// The device did not wake from deep sleep (for example a cold boot, reset or OTA restart).
|
||||
WAKEUP_CAUSE_NONE = 0,
|
||||
/// The device woke from deep sleep, but the source could not be identified.
|
||||
WAKEUP_CAUSE_UNKNOWN,
|
||||
/// The device was woken by the sleep timer.
|
||||
WAKEUP_CAUSE_TIMER,
|
||||
/// The device was woken by a GPIO pin (wakeup_pin or esp32_ext1_wakeup).
|
||||
WAKEUP_CAUSE_GPIO,
|
||||
/// The device was woken by a touch pad.
|
||||
WAKEUP_CAUSE_TOUCH,
|
||||
};
|
||||
|
||||
/// Return why the device woke from deep sleep. Implemented per platform.
|
||||
WakeupCause get_wakeup_cause();
|
||||
|
||||
/** Setup priority of on_wake triggers.
|
||||
*
|
||||
* Between restoring global variables (setup_priority::HARDWARE, 800) and on_boot automations at
|
||||
* their default priority (600), so on_wake automations can update state (e.g. globals) that
|
||||
* on_boot automations then use.
|
||||
*/
|
||||
inline constexpr float ON_WAKE_TRIGGER_SETUP_PRIORITY = 700.0f;
|
||||
|
||||
/// Fires once on boot when the device woke from deep sleep, with the wakeup cause.
|
||||
class WakeTrigger : public Trigger<WakeupCause>, public Component {
|
||||
public:
|
||||
void setup() override {
|
||||
const WakeupCause cause = get_wakeup_cause();
|
||||
if (cause != WAKEUP_CAUSE_NONE) {
|
||||
this->trigger(cause);
|
||||
}
|
||||
}
|
||||
float get_setup_priority() const override { return ON_WAKE_TRIGGER_SETUP_PRIORITY; }
|
||||
};
|
||||
|
||||
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32C2) && !defined(USE_ESP32_VARIANT_ESP32C3)
|
||||
/// Fires once on boot when the device was woken from deep sleep by the given ext1 pin.
|
||||
class Ext1WakeTrigger : public Trigger<>, public Component {
|
||||
public:
|
||||
explicit Ext1WakeTrigger(uint8_t pin) : pin_(pin) {}
|
||||
void setup() override {
|
||||
if (esp_sleep_get_wakeup_cause() == ESP_SLEEP_WAKEUP_EXT1 &&
|
||||
(esp_sleep_get_ext1_wakeup_status() & (1ULL << this->pin_))) {
|
||||
this->trigger();
|
||||
}
|
||||
}
|
||||
float get_setup_priority() const override { return ON_WAKE_TRIGGER_SETUP_PRIORITY; }
|
||||
|
||||
protected:
|
||||
uint8_t pin_;
|
||||
};
|
||||
#endif
|
||||
|
||||
#endif // USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
template<typename... Ts> class EnterDeepSleepAction;
|
||||
|
||||
template<typename... Ts> class PreventDeepSleepAction;
|
||||
@@ -84,7 +143,7 @@ class DeepSleepComponent final : public Component {
|
||||
#endif // USE_ESP32
|
||||
|
||||
#if defined(USE_BK72XX)
|
||||
void init_wakeup_pins_(size_t capacity) { this->wakeup_pins_.init(capacity); }
|
||||
void init_wakeup_pins(size_t capacity) { this->wakeup_pins_.init(capacity); }
|
||||
void add_wakeup_pin(InternalGPIOPin *wakeup_pin, WakeupPinMode wakeup_pin_mode) {
|
||||
this->wakeup_pins_.emplace_back(WakeUpPinItem{wakeup_pin, wakeup_pin_mode, !wakeup_pin->is_inverted()});
|
||||
}
|
||||
@@ -132,7 +191,7 @@ class DeepSleepComponent final : public Component {
|
||||
bool should_teardown_();
|
||||
|
||||
#ifdef USE_BK72XX
|
||||
bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const;
|
||||
bool pin_prevents_sleep_(WakeUpPinItem &pin_item) const;
|
||||
bool get_real_pin_state_(InternalGPIOPin &pin) const { return (pin.digital_read() ^ pin.is_inverted()); }
|
||||
#endif // USE_BK72XX
|
||||
|
||||
|
||||
@@ -30,6 +30,25 @@ namespace esphome::deep_sleep {
|
||||
|
||||
static const char *const TAG = "deep_sleep";
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
WakeupCause get_wakeup_cause() {
|
||||
switch (esp_sleep_get_wakeup_cause()) {
|
||||
case ESP_SLEEP_WAKEUP_EXT0:
|
||||
case ESP_SLEEP_WAKEUP_EXT1:
|
||||
case ESP_SLEEP_WAKEUP_GPIO:
|
||||
return WAKEUP_CAUSE_GPIO;
|
||||
case ESP_SLEEP_WAKEUP_TIMER:
|
||||
return WAKEUP_CAUSE_TIMER;
|
||||
case ESP_SLEEP_WAKEUP_TOUCHPAD:
|
||||
return WAKEUP_CAUSE_TOUCH;
|
||||
case ESP_SLEEP_WAKEUP_UNDEFINED:
|
||||
return WAKEUP_CAUSE_NONE;
|
||||
default:
|
||||
return WAKEUP_CAUSE_UNKNOWN;
|
||||
}
|
||||
}
|
||||
#endif // USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const {
|
||||
if (this->wakeup_cause_to_run_duration_.has_value()) {
|
||||
esp_sleep_wakeup_cause_t wakeup_cause = esp_sleep_get_wakeup_cause();
|
||||
|
||||
@@ -3,10 +3,27 @@
|
||||
|
||||
#include <Esp.h>
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
}
|
||||
#endif
|
||||
|
||||
namespace esphome::deep_sleep {
|
||||
|
||||
static const char *const TAG = "deep_sleep";
|
||||
|
||||
#ifdef USE_DEEP_SLEEP_ON_WAKE
|
||||
WakeupCause get_wakeup_cause() {
|
||||
// The ESP8266 can only wake from deep sleep through the RTC timer (via GPIO16 -> RST).
|
||||
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
|
||||
if (ESP.getResetInfoPtr()->reason == REASON_DEEP_SLEEP_AWAKE) {
|
||||
return WAKEUP_CAUSE_TIMER;
|
||||
}
|
||||
return WAKEUP_CAUSE_NONE;
|
||||
}
|
||||
#endif // USE_DEEP_SLEEP_ON_WAKE
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
void DeepSleepComponent::dump_config_platform_() {}
|
||||
|
||||
@@ -1,13 +1,36 @@
|
||||
#include "deep_sleep_component.h"
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/wake.h"
|
||||
#include <zephyr/sys/poweroff.h>
|
||||
#include <algorithm>
|
||||
|
||||
namespace esphome::deep_sleep {
|
||||
|
||||
static const char *const TAG = "deep_sleep";
|
||||
|
||||
// The Zephyr watchdog has a short window (2s, or 10s with Zigbee) and
|
||||
// WDT_OPT_PAUSE_IN_SLEEP only pauses it during true hardware sleep — not while a
|
||||
// radio thread (e.g. the Zigbee stack) keeps the CPU busy in k_sem_take(). Feed
|
||||
// it at least this often while waiting so it does not reset the device.
|
||||
static const uint32_t WDT_FEED_INTERVAL_MS = 1000;
|
||||
|
||||
static bool wakeable_delay_feed_wdt(uint32_t ms) {
|
||||
while (ms > 0) {
|
||||
const uint32_t step = std::min(ms, WDT_FEED_INTERVAL_MS);
|
||||
esphome::internal::wakeable_delay(step);
|
||||
esphome::arch_feed_wdt();
|
||||
if (esphome::wake_request_take()) {
|
||||
return true;
|
||||
}
|
||||
if (ms != UINT32_MAX) {
|
||||
ms -= step;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
|
||||
|
||||
void DeepSleepComponent::dump_config_platform_() {}
|
||||
@@ -15,8 +38,9 @@ void DeepSleepComponent::dump_config_platform_() {}
|
||||
bool DeepSleepComponent::prepare_to_sleep_() { return true; }
|
||||
|
||||
void DeepSleepComponent::deep_sleep_() {
|
||||
bool woke = false;
|
||||
if (this->sleep_duration_.has_value()) {
|
||||
esphome::internal::wakeable_delay(static_cast<uint32_t>(*this->sleep_duration_ / 1000));
|
||||
woke = wakeable_delay_feed_wdt(static_cast<uint32_t>(*this->sleep_duration_ / 1000));
|
||||
} else {
|
||||
#ifndef USE_ZIGBEE
|
||||
// the device can be woken up through one of the following signals:
|
||||
@@ -29,10 +53,9 @@ void DeepSleepComponent::deep_sleep_() {
|
||||
// The system is reset when it wakes up from System OFF mode.
|
||||
sys_poweroff();
|
||||
#else
|
||||
esphome::internal::wakeable_delay(UINT32_MAX);
|
||||
woke = wakeable_delay_feed_wdt(UINT32_MAX);
|
||||
#endif
|
||||
}
|
||||
const bool woke = esphome::wake_request_take();
|
||||
if (woke) {
|
||||
ESP_LOGD(TAG, "Woken up by another thread");
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_ID, CONF_SLEEP_PIN, CONF_TYPE
|
||||
|
||||
CODEOWNERS = ["@tomwellnitz"]
|
||||
MULTI_CONF = True
|
||||
DEPENDENCIES = ["i2c"]
|
||||
|
||||
CONF_DS248X_ID = "ds248x_id"
|
||||
CONF_BUS_SLEEP = "bus_sleep"
|
||||
CONF_HUB_SLEEP = "hub_sleep"
|
||||
CONF_ACTIVE_PULLUP = "active_pullup"
|
||||
|
||||
CONF_RESET_LOW_TIME = "reset_low_time"
|
||||
CONF_MASTER_SAMPLE_TIME = "master_sample_time"
|
||||
CONF_WRITE_0_LOW_TIME = "write_0_low_time"
|
||||
CONF_RECOVERY_TIME = "recovery_time"
|
||||
CONF_ACTIVE_PULLUP_RESISTANCE = "active_pullup_resistance"
|
||||
|
||||
TYPE_DS2482_100 = "ds2482-100"
|
||||
TYPE_DS2482_101 = "ds2482-101"
|
||||
TYPE_DS2482_800 = "ds2482-800"
|
||||
TYPE_DS2484 = "ds2484"
|
||||
|
||||
CHANNEL_COUNTS = {
|
||||
TYPE_DS2482_100: 1,
|
||||
TYPE_DS2482_101: 1,
|
||||
TYPE_DS2482_800: 8,
|
||||
TYPE_DS2484: 1,
|
||||
}
|
||||
|
||||
ds248x_ns = cg.esphome_ns.namespace("ds248x")
|
||||
DS248xComponent = ds248x_ns.class_("DS248xComponent", cg.Component, i2c.I2CDevice)
|
||||
|
||||
|
||||
def _component_schema(*extras):
|
||||
schema = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DS248xComponent),
|
||||
cv.Optional(CONF_ACTIVE_PULLUP, default=False): cv.boolean,
|
||||
}
|
||||
)
|
||||
for extra in extras:
|
||||
schema = schema.extend(extra)
|
||||
return schema.extend(cv.COMPONENT_SCHEMA).extend(i2c.i2c_device_schema(0x18))
|
||||
|
||||
|
||||
SLEEP_SCHEMA = {
|
||||
cv.Optional(CONF_SLEEP_PIN): pins.internal_gpio_output_pin_schema,
|
||||
cv.Optional(CONF_BUS_SLEEP, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HUB_SLEEP, default=False): cv.boolean,
|
||||
}
|
||||
|
||||
DS2484_SCHEMA = {
|
||||
cv.Optional(CONF_RESET_LOW_TIME): cv.int_range(min=0, max=15),
|
||||
cv.Optional(CONF_MASTER_SAMPLE_TIME): cv.int_range(min=0, max=15),
|
||||
cv.Optional(CONF_WRITE_0_LOW_TIME): cv.int_range(min=0, max=15),
|
||||
cv.Optional(CONF_RECOVERY_TIME): cv.int_range(min=0, max=15),
|
||||
cv.Optional(CONF_ACTIVE_PULLUP_RESISTANCE): cv.enum(
|
||||
{
|
||||
# DS2484 Table 7: value codes 0-5 map to 500 ohm, 6-15 map to 1000 ohm.
|
||||
"500ohm": 0,
|
||||
"1000ohm": 6,
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
CONFIG_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
TYPE_DS2482_100: _component_schema(),
|
||||
TYPE_DS2482_101: _component_schema(SLEEP_SCHEMA),
|
||||
TYPE_DS2482_800: _component_schema(),
|
||||
TYPE_DS2484: _component_schema(SLEEP_SCHEMA, DS2484_SCHEMA),
|
||||
},
|
||||
key=CONF_TYPE,
|
||||
lower=True,
|
||||
)
|
||||
|
||||
|
||||
def get_channel_count(config):
|
||||
return CHANNEL_COUNTS[config[CONF_TYPE]]
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
cg.add(var.set_active_pullup(config[CONF_ACTIVE_PULLUP]))
|
||||
cg.add(var.set_channel_count(get_channel_count(config)))
|
||||
|
||||
if CONF_BUS_SLEEP in config:
|
||||
cg.add(var.set_bus_sleep(config[CONF_BUS_SLEEP]))
|
||||
if CONF_HUB_SLEEP in config:
|
||||
cg.add(var.set_hub_sleep(config[CONF_HUB_SLEEP]))
|
||||
|
||||
if CONF_RESET_LOW_TIME in config:
|
||||
cg.add(var.set_val_trstl(config[CONF_RESET_LOW_TIME]))
|
||||
if CONF_MASTER_SAMPLE_TIME in config:
|
||||
cg.add(var.set_val_tmsp(config[CONF_MASTER_SAMPLE_TIME]))
|
||||
if CONF_WRITE_0_LOW_TIME in config:
|
||||
cg.add(var.set_val_tw0l(config[CONF_WRITE_0_LOW_TIME]))
|
||||
if CONF_RECOVERY_TIME in config:
|
||||
cg.add(var.set_val_trec0(config[CONF_RECOVERY_TIME]))
|
||||
if CONF_ACTIVE_PULLUP_RESISTANCE in config:
|
||||
cg.add(var.set_val_rwpu(config[CONF_ACTIVE_PULLUP_RESISTANCE]))
|
||||
|
||||
if CONF_SLEEP_PIN in config:
|
||||
pin = await cg.gpio_pin_expression(config[CONF_SLEEP_PIN])
|
||||
cg.add(var.set_sleep_pin(pin))
|
||||
@@ -0,0 +1,320 @@
|
||||
#include "ds248x.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
namespace esphome::ds248x {
|
||||
|
||||
static const char *const TAG = "ds248x";
|
||||
|
||||
void DS248xComponent::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up DS248x...");
|
||||
|
||||
// Wake up device if sleep pin is configured
|
||||
if (this->sleep_pin_) {
|
||||
this->sleep_pin_->setup();
|
||||
this->sleep_pin_->pin_mode(esphome::gpio::FLAG_OUTPUT);
|
||||
this->sleep_pin_->digital_write(true); // Wake up
|
||||
delay(1); // DS2482-101 Datasheet: tOSCWUP = 100μs (using 10x margin)
|
||||
}
|
||||
|
||||
// Probe device
|
||||
ESP_LOGD(TAG, "Probing DS248x...");
|
||||
uint8_t status = 0;
|
||||
if (this->read(&status, 1) == i2c::ERROR_OK) {
|
||||
ESP_LOGD(TAG, "Device responded! Status: 0x%02x", status);
|
||||
} else {
|
||||
ESP_LOGW(TAG, "Device did not respond. Trying reset anyway...");
|
||||
}
|
||||
|
||||
if (!this->device_reset_()) {
|
||||
ESP_LOGW(TAG, "DS248x reset failed during setup!");
|
||||
}
|
||||
|
||||
// Configure device
|
||||
if (!this->device_configure_()) {
|
||||
ESP_LOGE(TAG, "DS248x configuration failed!");
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset to Channel 0
|
||||
this->select_channel(0);
|
||||
|
||||
ESP_LOGI(TAG, "DS248x initialized successfully.");
|
||||
}
|
||||
|
||||
void DS248xComponent::on_shutdown() {
|
||||
if (this->sleep_pin_ && (this->hub_sleep_ || this->bus_sleep_)) {
|
||||
this->sleep_pin_->digital_write(false); // Sleep
|
||||
}
|
||||
}
|
||||
|
||||
void DS248xComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DS248x:");
|
||||
LOG_I2C_DEVICE(this);
|
||||
ESP_LOGCONFIG(TAG, " Channel Count: %d", this->channel_count_);
|
||||
ESP_LOGCONFIG(TAG, " Active Pullup: %s", YESNO(this->active_pullup_));
|
||||
if (this->ds2484_mode_) {
|
||||
ESP_LOGCONFIG(TAG, " DS2484 Mode: enabled");
|
||||
}
|
||||
}
|
||||
|
||||
// --- Internal Helpers ---
|
||||
|
||||
// Datasheet command durations are sub-2ms; allow a little margin before forcing recovery.
|
||||
static constexpr uint32_t BUSY_TIMEOUT_MS = 5;
|
||||
|
||||
bool DS248xComponent::set_read_pointer_(uint8_t ptr) { return this->write_byte(DS248X_COMMAND_SETREADPTR, ptr); }
|
||||
|
||||
bool DS248xComponent::wait_busy_() {
|
||||
uint32_t start = millis();
|
||||
do {
|
||||
uint8_t status;
|
||||
if (this->read(&status, 1) == i2c::ERROR_OK && !(status & DS248X_STATUS_BUSY))
|
||||
return true;
|
||||
delayMicroseconds(100);
|
||||
} while (millis() - start < BUSY_TIMEOUT_MS);
|
||||
ESP_LOGW(TAG, "DS248x busy timeout");
|
||||
bool recovered = this->device_reset_() && this->device_configure_();
|
||||
this->current_channel_ = -1;
|
||||
if (!recovered) {
|
||||
ESP_LOGE(TAG, "DS248x recovery failed after busy timeout");
|
||||
this->mark_failed();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DS248xComponent::device_reset_() {
|
||||
ESP_LOGD(TAG, "Resetting device...");
|
||||
uint8_t cmd = DS248X_COMMAND_RESET;
|
||||
if (this->write(&cmd, 1) != i2c::ERROR_OK)
|
||||
return false;
|
||||
|
||||
uint8_t status;
|
||||
if (this->read(&status, 1) != i2c::ERROR_OK)
|
||||
return false;
|
||||
|
||||
if (!(status & DS248X_STATUS_RST)) {
|
||||
ESP_LOGW(TAG, "Device reset failed (RST bit not set)");
|
||||
return false;
|
||||
}
|
||||
|
||||
this->current_channel_ = -1;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS248xComponent::device_configure_() {
|
||||
ESP_LOGD(TAG, "Configuring device...");
|
||||
|
||||
if (!this->write_config_()) {
|
||||
ESP_LOGW(TAG, "Config write/verify failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Configured successfully");
|
||||
|
||||
// DS2484 Configuration
|
||||
if (this->ds2484_mode_) {
|
||||
if (this->ds2484_trstl_ != DS2484_PARAM_UNSET &&
|
||||
!this->configure_ds2484_port_(DS2484_PORT_PARAM_TRSTL, this->ds2484_trstl_))
|
||||
return false;
|
||||
if (this->ds2484_tmsp_ != DS2484_PARAM_UNSET &&
|
||||
!this->configure_ds2484_port_(DS2484_PORT_PARAM_TMSP, this->ds2484_tmsp_))
|
||||
return false;
|
||||
if (this->ds2484_tw0l_ != DS2484_PARAM_UNSET &&
|
||||
!this->configure_ds2484_port_(DS2484_PORT_PARAM_TW0L, this->ds2484_tw0l_))
|
||||
return false;
|
||||
if (this->ds2484_trec0_ != DS2484_PARAM_UNSET &&
|
||||
!this->configure_ds2484_port_(DS2484_PORT_PARAM_TREC0, this->ds2484_trec0_))
|
||||
return false;
|
||||
if (this->ds2484_rwpu_ != DS2484_PARAM_UNSET &&
|
||||
!this->configure_ds2484_port_(DS2484_PORT_PARAM_RWPU, this->ds2484_rwpu_))
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS248xComponent::configure_ds2484_port_(uint8_t param, uint8_t val) {
|
||||
uint8_t cmd = DS2484_COMMAND_ADJUSTPORT;
|
||||
// Control Byte format (DS2484 Table 6): P[2:0] in bits 7:5, OD in bit 4, VAL[3:0] in bits 3:0
|
||||
uint8_t data = ((param & 0x07) << 5) | (val & 0x0F);
|
||||
|
||||
// The DS2484 always acknowledges the Adjust 1-Wire Port control byte (datasheet "Adjust
|
||||
// 1-Wire Port"), so a successful write confirms the update. We deliberately do not read
|
||||
// back to verify: a single read of the Port Configuration register always returns the
|
||||
// fixed 8-byte report starting at Byte 1 (tRSTL standard speed), not the parameter that
|
||||
// was just written, so a per-parameter readback comparison would spuriously fail for
|
||||
// tMSP/tW0L/tREC0/RWPU.
|
||||
if (!this->write_byte(cmd, data)) {
|
||||
ESP_LOGW(TAG, "DS2484 port config failed (param %d)", param);
|
||||
return false;
|
||||
}
|
||||
|
||||
return this->set_read_pointer_(DS248X_POINTER_STATUS);
|
||||
}
|
||||
|
||||
bool DS248xComponent::write_config_() {
|
||||
uint8_t config = 0;
|
||||
if (this->active_pullup_)
|
||||
config |= DS248X_CONFIG_ACTIVE_PULLUP;
|
||||
|
||||
// The DS248x only accepts the config byte if the upper nibble is the one's-complement of the lower nibble.
|
||||
uint8_t config_byte = (config & 0x0F) | ((~config & 0x0F) << 4);
|
||||
|
||||
if (!this->write_byte(DS248X_COMMAND_WRITECONFIG, config_byte)) {
|
||||
ESP_LOGW(TAG, "Failed to write config byte");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->set_read_pointer_(DS248X_POINTER_CONFIG)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t read_config;
|
||||
if (this->read(&read_config, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "Failed to read back config byte");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((read_config & 0x0F) != (config_byte & 0x0F)) {
|
||||
ESP_LOGW(TAG, "Config mismatch! Wrote 0x%02x, Read 0x%02x", config_byte, read_config);
|
||||
return false;
|
||||
}
|
||||
|
||||
return this->set_read_pointer_(DS248X_POINTER_STATUS);
|
||||
}
|
||||
|
||||
// --- Channel Selection ---
|
||||
|
||||
// Channel select codes: write code -> expected read code
|
||||
static constexpr uint8_t CHANNEL_WRITE_CODES[8] = {0xF0, 0xE1, 0xD2, 0xC3, 0xB4, 0xA5, 0x96, 0x87};
|
||||
static constexpr uint8_t CHANNEL_READ_CODES[8] = {0xB8, 0xB1, 0xAA, 0xA3, 0x9C, 0x95, 0x8E, 0x87};
|
||||
|
||||
bool DS248xComponent::select_channel(uint8_t channel) {
|
||||
if (this->channel_count_ <= 1)
|
||||
return true;
|
||||
if (channel >= this->channel_count_)
|
||||
return false;
|
||||
|
||||
if (this->current_channel_ == channel)
|
||||
return true;
|
||||
|
||||
if (!this->write_byte(DS248X_COMMAND_CHANNELSELECT, CHANNEL_WRITE_CODES[channel])) {
|
||||
this->current_channel_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t read_code;
|
||||
if (this->read(&read_code, 1) != i2c::ERROR_OK) {
|
||||
this->current_channel_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (read_code != CHANNEL_READ_CODES[channel]) {
|
||||
ESP_LOGW(TAG, "Channel select failed! Expected 0x%02x, got 0x%02x", CHANNEL_READ_CODES[channel], read_code);
|
||||
this->current_channel_ = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->set_read_pointer_(DS248X_POINTER_STATUS))
|
||||
return false;
|
||||
|
||||
this->current_channel_ = channel;
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- 1-Wire Bus Operations ---
|
||||
|
||||
bool DS248xComponent::ow_reset(bool &presence) {
|
||||
if (!this->set_read_pointer_(DS248X_POINTER_STATUS))
|
||||
return false;
|
||||
|
||||
uint8_t cmd = DS248X_COMMAND_RESETWIRE;
|
||||
if (this->write(&cmd, 1) != i2c::ERROR_OK)
|
||||
return false;
|
||||
|
||||
if (!this->wait_busy_()) {
|
||||
ESP_LOGW(TAG, "ow_reset: wait busy failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t status;
|
||||
if (this->read(&status, 1) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "ow_reset: read status failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (status & DS248X_STATUS_SD) {
|
||||
ESP_LOGW(TAG, "Short detected on 1-Wire bus!");
|
||||
return false;
|
||||
}
|
||||
|
||||
presence = (status & DS248X_STATUS_PPD);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS248xComponent::ow_write_byte(uint8_t byte) {
|
||||
if (!this->set_read_pointer_(DS248X_POINTER_STATUS))
|
||||
return false;
|
||||
|
||||
if (!this->wait_busy_()) {
|
||||
ESP_LOGW(TAG, "Device busy before writing byte 0x%02x", byte);
|
||||
return false;
|
||||
}
|
||||
|
||||
uint8_t cmd[2] = {DS248X_COMMAND_WRITEBYTE, byte};
|
||||
if (this->write(cmd, 2) != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "I2C write failed for byte 0x%02x", byte);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->wait_busy_()) {
|
||||
ESP_LOGW(TAG, "Timeout waiting for write byte to complete!");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS248xComponent::ow_read_byte(uint8_t &byte) {
|
||||
if (!this->set_read_pointer_(DS248X_POINTER_STATUS))
|
||||
return false;
|
||||
|
||||
uint8_t cmd = DS248X_COMMAND_READBYTE;
|
||||
if (this->write(&cmd, 1) != i2c::ERROR_OK)
|
||||
return false;
|
||||
|
||||
if (!this->wait_busy_())
|
||||
return false;
|
||||
|
||||
if (!this->set_read_pointer_(DS248X_POINTER_DATA))
|
||||
return false;
|
||||
|
||||
if (this->read(&byte, 1) != i2c::ERROR_OK)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DS248xComponent::search_triplet(bool search_direction, uint8_t &status) {
|
||||
if (!this->set_read_pointer_(DS248X_POINTER_STATUS))
|
||||
return false;
|
||||
|
||||
// DS248x Datasheet: 1-Wire Triplet command requires 2 bytes:
|
||||
// Byte 1: Command code 0x78
|
||||
// Byte 2: Direction byte (bit 7 = V, search direction if discrepancy)
|
||||
uint8_t buffer[2] = {DS248X_COMMAND_TRIPLET, static_cast<uint8_t>(search_direction ? 0x80 : 0x00)};
|
||||
if (this->write(buffer, 2) != i2c::ERROR_OK)
|
||||
return false;
|
||||
|
||||
if (!this->wait_busy_())
|
||||
return false;
|
||||
|
||||
if (this->read(&status, 1) != i2c::ERROR_OK)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace esphome::ds248x
|
||||
@@ -0,0 +1,133 @@
|
||||
#pragma once
|
||||
|
||||
// DS248x I2C-to-1-Wire Bridge Family
|
||||
// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ds2482-100.pdf
|
||||
// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ds2482-800.pdf
|
||||
// Datasheet: https://www.analog.com/media/en/technical-documentation/data-sheets/ds2484.pdf
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
|
||||
namespace esphome::ds248x {
|
||||
|
||||
// DS248x I2C Commands
|
||||
static constexpr uint8_t DS248X_COMMAND_RESET = 0xF0;
|
||||
static constexpr uint8_t DS248X_COMMAND_SETREADPTR = 0xE1;
|
||||
static constexpr uint8_t DS248X_COMMAND_WRITECONFIG = 0xD2;
|
||||
static constexpr uint8_t DS248X_COMMAND_CHANNELSELECT = 0xC3;
|
||||
static constexpr uint8_t DS248X_COMMAND_RESETWIRE = 0xB4;
|
||||
static constexpr uint8_t DS248X_COMMAND_WRITEBYTE = 0xA5;
|
||||
static constexpr uint8_t DS248X_COMMAND_READBYTE = 0x96;
|
||||
static constexpr uint8_t DS248X_COMMAND_TRIPLET = 0x78;
|
||||
static constexpr uint8_t DS2484_COMMAND_ADJUSTPORT = 0xC3;
|
||||
|
||||
// DS2484 "Adjust 1-Wire Port" parameter codes (datasheet Table 6, control byte P[2:0])
|
||||
static constexpr uint8_t DS2484_PORT_PARAM_TRSTL = 0x0;
|
||||
static constexpr uint8_t DS2484_PORT_PARAM_TMSP = 0x1;
|
||||
static constexpr uint8_t DS2484_PORT_PARAM_TW0L = 0x2;
|
||||
static constexpr uint8_t DS2484_PORT_PARAM_TREC0 = 0x3;
|
||||
static constexpr uint8_t DS2484_PORT_PARAM_RWPU = 0x4;
|
||||
|
||||
// DS248x Status Register Bits
|
||||
static constexpr uint8_t DS248X_STATUS_BUSY = 0x01;
|
||||
static constexpr uint8_t DS248X_STATUS_PPD = 0x02;
|
||||
static constexpr uint8_t DS248X_STATUS_SD = 0x04;
|
||||
static constexpr uint8_t DS248X_STATUS_RST = 0x10;
|
||||
static constexpr uint8_t DS248X_STATUS_SBR = 0x20;
|
||||
static constexpr uint8_t DS248X_STATUS_TSB = 0x40;
|
||||
static constexpr uint8_t DS248X_STATUS_DIR = 0x80;
|
||||
|
||||
// DS248x Register Pointers
|
||||
static constexpr uint8_t DS248X_POINTER_STATUS = 0xF0;
|
||||
static constexpr uint8_t DS248X_POINTER_DATA = 0xE1;
|
||||
static constexpr uint8_t DS248X_POINTER_CONFIG = 0xC3;
|
||||
|
||||
// DS248x Configuration Bits
|
||||
static constexpr uint8_t DS248X_CONFIG_ACTIVE_PULLUP = 0x01;
|
||||
|
||||
/**
|
||||
* @brief DS248x I2C-to-1-Wire Bridge Component.
|
||||
*
|
||||
* This component manages the DS248x chip (DS2482-100, DS2482-800, DS2484).
|
||||
* It provides low-level 1-Wire bus operations via I2C.
|
||||
*
|
||||
* Usage: Configure DS248xOneWireBus instances for each channel.
|
||||
* These buses implement the one_wire::OneWireBus interface for compatibility
|
||||
* with all existing 1-Wire device components (dallas_temp, etc.).
|
||||
*/
|
||||
class DS248xComponent : public Component, public i2c::I2CDevice {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void on_shutdown() override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS; }
|
||||
|
||||
void set_sleep_pin(InternalGPIOPin *pin) { this->sleep_pin_ = pin; }
|
||||
void set_bus_sleep(bool enabled) { this->bus_sleep_ = enabled; }
|
||||
void set_hub_sleep(bool enabled) { this->hub_sleep_ = enabled; }
|
||||
void set_channel_count(uint8_t count) { this->channel_count_ = count; }
|
||||
void set_active_pullup(bool enabled) { this->active_pullup_ = enabled; }
|
||||
|
||||
// DS2484 Timing Parameters
|
||||
void set_val_trstl(uint8_t val) {
|
||||
this->ds2484_trstl_ = val;
|
||||
this->ds2484_mode_ = true;
|
||||
}
|
||||
void set_val_tmsp(uint8_t val) {
|
||||
this->ds2484_tmsp_ = val;
|
||||
this->ds2484_mode_ = true;
|
||||
}
|
||||
void set_val_tw0l(uint8_t val) {
|
||||
this->ds2484_tw0l_ = val;
|
||||
this->ds2484_mode_ = true;
|
||||
}
|
||||
void set_val_trec0(uint8_t val) {
|
||||
this->ds2484_trec0_ = val;
|
||||
this->ds2484_mode_ = true;
|
||||
}
|
||||
void set_val_rwpu(uint8_t val) {
|
||||
this->ds2484_rwpu_ = val;
|
||||
this->ds2484_mode_ = true;
|
||||
}
|
||||
|
||||
/// Get the channel count (1 for DS2482-100/DS2484, 8 for DS2482-800)
|
||||
uint8_t get_channel_count() const { return this->channel_count_; }
|
||||
|
||||
// --- Core 1-Wire API (used by DS248xOneWireBus) ---
|
||||
bool select_channel(uint8_t channel);
|
||||
bool ow_reset(bool &presence);
|
||||
bool ow_write_byte(uint8_t byte);
|
||||
bool ow_read_byte(uint8_t &byte);
|
||||
|
||||
// --- Search support (used by DS248xOneWireBus) ---
|
||||
bool search_triplet(bool search_direction, uint8_t &status);
|
||||
|
||||
protected:
|
||||
InternalGPIOPin *sleep_pin_{nullptr};
|
||||
uint8_t channel_count_ = 1;
|
||||
bool bus_sleep_{false};
|
||||
bool hub_sleep_{false};
|
||||
bool active_pullup_ = false;
|
||||
|
||||
// DS2484 Config
|
||||
bool ds2484_mode_ = false;
|
||||
static constexpr uint8_t DS2484_PARAM_UNSET = 0xFF;
|
||||
uint8_t ds2484_trstl_{DS2484_PARAM_UNSET};
|
||||
uint8_t ds2484_tmsp_{DS2484_PARAM_UNSET};
|
||||
uint8_t ds2484_tw0l_{DS2484_PARAM_UNSET};
|
||||
uint8_t ds2484_trec0_{DS2484_PARAM_UNSET};
|
||||
uint8_t ds2484_rwpu_{DS2484_PARAM_UNSET};
|
||||
|
||||
int8_t current_channel_{-1};
|
||||
|
||||
// Internal helpers
|
||||
bool set_read_pointer_(uint8_t ptr);
|
||||
bool wait_busy_();
|
||||
bool device_reset_();
|
||||
bool device_configure_();
|
||||
bool configure_ds2484_port_(uint8_t param, uint8_t val);
|
||||
bool write_config_();
|
||||
};
|
||||
|
||||
} // namespace esphome::ds248x
|
||||
@@ -0,0 +1,171 @@
|
||||
#include "ds248x_one_wire_bus.h"
|
||||
#include "ds248x.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::ds248x {
|
||||
|
||||
static const char *const TAG = "ds248x.one_wire";
|
||||
|
||||
void DS248xOneWireBus::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up DS248x 1-Wire Bus (Channel %d)...", this->channel_);
|
||||
|
||||
// Parent setup happens in DS248xComponent::setup()
|
||||
// We just need to scan for devices on this channel
|
||||
if (!this->ensure_channel_()) {
|
||||
ESP_LOGE(TAG, "Failed to select channel %d during setup", this->channel_);
|
||||
this->mark_failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform device search on this channel
|
||||
this->search();
|
||||
|
||||
ESP_LOGCONFIG(TAG, "Found %zu devices on channel %d", this->devices_.size(), this->channel_);
|
||||
}
|
||||
|
||||
void DS248xOneWireBus::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "DS248x 1-Wire Bus (Channel %d):", this->channel_);
|
||||
this->dump_devices_(TAG);
|
||||
}
|
||||
|
||||
bool DS248xOneWireBus::ensure_channel_() {
|
||||
if (this->parent_ == nullptr) {
|
||||
ESP_LOGE(TAG, "Parent not set!");
|
||||
return false;
|
||||
}
|
||||
return this->parent_->select_channel(this->channel_);
|
||||
}
|
||||
|
||||
int DS248xOneWireBus::reset_int() {
|
||||
if (!this->ensure_channel_()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool presence = false;
|
||||
if (!this->parent_->ow_reset(presence)) {
|
||||
return -1;
|
||||
}
|
||||
return presence ? 1 : 0;
|
||||
}
|
||||
|
||||
void DS248xOneWireBus::write8(uint8_t val) {
|
||||
if (!this->ensure_channel_()) {
|
||||
return;
|
||||
}
|
||||
if (!this->parent_->ow_write_byte(val)) {
|
||||
ESP_LOGE(TAG, "Failed to write byte 0x%02X on channel %d", val, this->channel_);
|
||||
}
|
||||
}
|
||||
|
||||
void DS248xOneWireBus::write64(uint64_t val) {
|
||||
if (!this->ensure_channel_()) {
|
||||
return;
|
||||
}
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
uint8_t byte = static_cast<uint8_t>(val >> (i * 8));
|
||||
if (!this->parent_->ow_write_byte(byte)) {
|
||||
ESP_LOGE(TAG, "Failed to write byte %d/8 (0x%02X) on channel %d - aborting write64", i + 1, byte, this->channel_);
|
||||
return; // Stop writing to prevent sending corrupted data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t DS248xOneWireBus::read8() {
|
||||
if (!this->ensure_channel_()) {
|
||||
return 0;
|
||||
}
|
||||
uint8_t value = 0;
|
||||
if (!this->parent_->ow_read_byte(value)) {
|
||||
ESP_LOGE(TAG, "Failed to read byte on channel %d", this->channel_);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
uint64_t DS248xOneWireBus::read64() {
|
||||
if (!this->ensure_channel_()) {
|
||||
return 0;
|
||||
}
|
||||
uint64_t value = 0;
|
||||
for (uint8_t i = 0; i < 8; i++) {
|
||||
uint8_t byte = 0;
|
||||
if (!this->parent_->ow_read_byte(byte)) {
|
||||
ESP_LOGE(TAG, "Failed to read byte %d/8 on channel %d - returning partial data", i + 1, this->channel_);
|
||||
return value; // Return partial data to avoid blocking, caller should validate
|
||||
}
|
||||
value |= (static_cast<uint64_t>(byte) << (i * 8));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void DS248xOneWireBus::reset_search() {
|
||||
this->search_last_discrepancy_ = 0;
|
||||
this->search_last_device_flag_ = false;
|
||||
this->search_address_ = 0;
|
||||
}
|
||||
|
||||
uint64_t DS248xOneWireBus::search_int() {
|
||||
if (!this->ensure_channel_()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (this->search_last_device_flag_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint8_t last_zero = 0;
|
||||
uint64_t address = this->search_address_;
|
||||
|
||||
// Iterate through all 64 bits
|
||||
for (uint8_t bit_number = 1; bit_number <= 64; bit_number++) {
|
||||
uint64_t bit_mask = 1ULL << (bit_number - 1);
|
||||
|
||||
// Determine search direction
|
||||
bool search_direction;
|
||||
if (bit_number < this->search_last_discrepancy_) {
|
||||
search_direction = (address & bit_mask) != 0;
|
||||
} else {
|
||||
search_direction = (bit_number == this->search_last_discrepancy_);
|
||||
}
|
||||
|
||||
// Perform triplet operation
|
||||
uint8_t status = 0;
|
||||
if (!this->parent_->search_triplet(search_direction, status)) {
|
||||
ESP_LOGW(TAG, "1-Wire triplet failed at bit %d on channel %d - aborting search", bit_number, this->channel_);
|
||||
this->reset_search();
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool id_bit = (status & DS248X_STATUS_SBR) != 0;
|
||||
bool cmp_id_bit = (status & DS248X_STATUS_TSB) != 0;
|
||||
bool dir_taken = (status & DS248X_STATUS_DIR) != 0;
|
||||
|
||||
if (id_bit && cmp_id_bit) {
|
||||
// No devices participating
|
||||
this->reset_search();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!id_bit && !cmp_id_bit && !dir_taken) {
|
||||
// Discrepancy, went 0 - record position
|
||||
last_zero = bit_number;
|
||||
}
|
||||
|
||||
// Update address based on direction taken
|
||||
if (dir_taken) {
|
||||
address |= bit_mask;
|
||||
} else {
|
||||
address &= ~bit_mask;
|
||||
}
|
||||
}
|
||||
|
||||
// Search successful
|
||||
this->search_last_discrepancy_ = last_zero;
|
||||
if (last_zero == 0) {
|
||||
this->search_last_device_flag_ = true;
|
||||
}
|
||||
this->search_address_ = address;
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
} // namespace esphome::ds248x
|
||||
@@ -0,0 +1,57 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/components/one_wire/one_wire_bus.h"
|
||||
|
||||
namespace esphome::ds248x {
|
||||
|
||||
class DS248xComponent;
|
||||
|
||||
/**
|
||||
* @brief OneWireBus implementation for DS248x I2C-to-1-Wire bridges.
|
||||
*
|
||||
* This class wraps the DS248xComponent to provide the one_wire::OneWireBus interface,
|
||||
* enabling compatibility with all existing 1-Wire device components (dallas_temp, etc.).
|
||||
*
|
||||
* For DS2482-800, multiple instances of this class can be created (one per channel).
|
||||
* For DS2482-100/DS2484, a single instance is used.
|
||||
*/
|
||||
class DS248xOneWireBus : public one_wire::OneWireBus, public Component {
|
||||
public:
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
float get_setup_priority() const override { return setup_priority::BUS - 1.0f; }
|
||||
|
||||
/// Set the parent DS248x component
|
||||
void set_parent(DS248xComponent *parent) { this->parent_ = parent; }
|
||||
|
||||
/// Set the 1-Wire channel (0-7, only relevant for DS2482-800)
|
||||
void set_channel(uint8_t channel) { this->channel_ = channel; }
|
||||
|
||||
/// Get the channel number
|
||||
uint8_t get_channel() const { return this->channel_; }
|
||||
|
||||
// OneWireBus interface implementation
|
||||
int reset_int() override;
|
||||
void write8(uint8_t val) override;
|
||||
void write64(uint64_t val) override;
|
||||
uint8_t read8() override;
|
||||
uint64_t read64() override;
|
||||
|
||||
protected:
|
||||
void reset_search() override;
|
||||
uint64_t search_int() override;
|
||||
|
||||
/// Select the channel on the DS248x before any 1-Wire operation
|
||||
bool ensure_channel_();
|
||||
|
||||
DS248xComponent *parent_{nullptr};
|
||||
uint8_t channel_{0};
|
||||
|
||||
// Search state
|
||||
uint64_t search_address_{0};
|
||||
uint8_t search_last_discrepancy_{0};
|
||||
bool search_last_device_flag_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::ds248x
|
||||
@@ -0,0 +1,56 @@
|
||||
"""DS248x 1-Wire Bus Platform.
|
||||
|
||||
This platform creates one_wire bus instances backed by a DS248x I2C-to-1-Wire bridge.
|
||||
It supports DS2482-100/101 (single channel), DS2482-800 (8 channels), and DS2484 (single channel).
|
||||
|
||||
For multi-channel devices (DS2482-800), create one platform entry per channel.
|
||||
Each entry becomes a separate one_wire bus that can be used by dallas_temp and other 1-Wire devices.
|
||||
"""
|
||||
|
||||
from esphome import final_validate as fv
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.one_wire import OneWireBus
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_CHANNEL, CONF_ID
|
||||
|
||||
from . import CONF_DS248X_ID, DS248xComponent, ds248x_ns, get_channel_count
|
||||
|
||||
CODEOWNERS = ["@tomwellnitz"]
|
||||
DEPENDENCIES = ["ds248x"]
|
||||
|
||||
DS248xOneWireBus = ds248x_ns.class_("DS248xOneWireBus", OneWireBus, cg.Component)
|
||||
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DS248xOneWireBus),
|
||||
cv.GenerateID(CONF_DS248X_ID): cv.use_id(DS248xComponent),
|
||||
cv.Optional(CONF_CHANNEL, default=0): cv.int_range(min=0, max=7),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
def _final_validate(config):
|
||||
"""Validate that the channel is within the parent's channel count."""
|
||||
fconf = fv.full_config.get()
|
||||
path = fconf.get_path_for_id(config[CONF_DS248X_ID])[:-1]
|
||||
parent_config = fconf.get_config_for_path(path)
|
||||
channel_count = get_channel_count(parent_config)
|
||||
channel = config[CONF_CHANNEL]
|
||||
|
||||
if channel >= channel_count:
|
||||
raise cv.Invalid(
|
||||
f"Channel {channel} is invalid for DS248x with {channel_count} channel(s). "
|
||||
f"Valid range: 0-{channel_count - 1}"
|
||||
)
|
||||
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await cg.register_component(var, config)
|
||||
|
||||
parent = await cg.get_variable(config[CONF_DS248X_ID])
|
||||
cg.add(var.set_parent(parent))
|
||||
cg.add(var.set_channel(config[CONF_CHANNEL]))
|
||||
@@ -145,9 +145,9 @@ float Emc2101Component::get_external_temperature() {
|
||||
return NAN;
|
||||
}
|
||||
|
||||
// join msb and lsb (5 least significant bits are not used)
|
||||
uint16_t raw = (msb << 8 | lsb) >> 5;
|
||||
return raw * 0.125;
|
||||
// join msb and lsb (5 least significant bits are not used); msb is signed, so read as int16_t
|
||||
int16_t raw = static_cast<int16_t>((msb << 8) | lsb) >> 5;
|
||||
return raw * 0.125f;
|
||||
}
|
||||
|
||||
float Emc2101Component::get_speed() {
|
||||
|
||||
@@ -14,10 +14,9 @@ void EPaperMono::refresh_screen(bool partial) {
|
||||
}
|
||||
|
||||
void EPaperMono::deep_sleep() {
|
||||
ESP_LOGV(TAG, "Deep sleep");
|
||||
if (this->is_using_partial_update_()) {
|
||||
this->cmd_data(0x10, {0x00}); // sleep in power on mode
|
||||
} else {
|
||||
// Deep sleep loses RAM so cannot be used with partial update
|
||||
if (!this->is_using_partial_update_()) {
|
||||
ESP_LOGV(TAG, "Deep sleep");
|
||||
this->cmd_data(0x10, {0x03}); // deep sleep
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,9 @@ CONF_ENGINEERING_SAMPLE = "engineering_sample"
|
||||
CONF_INCLUDE_BUILTIN_IDF_COMPONENTS = "include_builtin_idf_components"
|
||||
CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
|
||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
|
||||
CONF_KEY_ID = "key_id"
|
||||
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
|
||||
CONF_NVS_ENCRYPTION = "nvs_encryption"
|
||||
CONF_RELEASE = "release"
|
||||
CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification"
|
||||
CONF_SIGNING_KEY = "signing_key"
|
||||
@@ -167,6 +169,20 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = {
|
||||
VARIANT_ESP32,
|
||||
}
|
||||
|
||||
# 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
|
||||
# should be added here.
|
||||
NVS_ENCRYPTION_HMAC_VARIANTS = {
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
VARIANT_ESP32C6,
|
||||
VARIANT_ESP32H2,
|
||||
VARIANT_ESP32P4,
|
||||
}
|
||||
|
||||
COMPILER_OPTIMIZATIONS = {
|
||||
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
|
||||
"NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE",
|
||||
@@ -1144,6 +1160,74 @@ def _ota_downgrade_protection_errors(
|
||||
return errs
|
||||
|
||||
|
||||
_SIGNED_OTA_VERIFICATION_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SIGNING_KEY): cv.file_,
|
||||
cv.Optional(CONF_VERIFICATION_KEY): cv.file_,
|
||||
cv.Optional(CONF_SIGNING_SCHEME, default="rsa3072"): cv.one_of(
|
||||
*SIGNING_SCHEMES, lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@schema_extractor("schema")
|
||||
def _validate_signed_ota_verification(value):
|
||||
if value is SCHEMA_EXTRACT:
|
||||
# Expose the inner schema so the language-schema dumper can walk the
|
||||
# signing_key / verification_key / signing_scheme options.
|
||||
return _SIGNED_OTA_VERIFICATION_SCHEMA
|
||||
if value is None:
|
||||
# A bare `signed_ota_verification:` block is valid: the default V2
|
||||
# scheme needs no keys (verify externally-signed binaries).
|
||||
value = {}
|
||||
return _validate_signed_ota_keys(_SIGNED_OTA_VERIFICATION_SCHEMA(value))
|
||||
|
||||
|
||||
def _validate_signed_ota_keys(config: ConfigType) -> ConfigType:
|
||||
"""Validate the signing/verification key combination for the selected scheme.
|
||||
|
||||
A verification key is only used by the Secure Boot V1 scheme (ecdsa_v1):
|
||||
the public key is compiled into the app so it can verify externally-signed
|
||||
images. ESP-IDF's CONFIG_SECURE_BOOT_VERIFICATION_KEY only takes effect
|
||||
when the V1 ECDSA scheme is selected and binaries are not signed during
|
||||
the build (see SECURE_BOOT_VERIFICATION_KEY in the bootloader Kconfig).
|
||||
|
||||
The V2 schemes (rsa3072, ecdsa256) embed the public key in the signature
|
||||
block appended to each image, so verifying externally-signed binaries
|
||||
needs no key in the config at all -- omitting both keys selects that
|
||||
external-signing mode.
|
||||
"""
|
||||
has_signing_key = CONF_SIGNING_KEY in config
|
||||
has_verification_key = CONF_VERIFICATION_KEY in config
|
||||
scheme = config[CONF_SIGNING_SCHEME]
|
||||
if has_signing_key and has_verification_key:
|
||||
raise cv.Invalid(
|
||||
f"Provide at most one of '{CONF_SIGNING_KEY}' and "
|
||||
f"'{CONF_VERIFICATION_KEY}', not both.",
|
||||
path=[CONF_VERIFICATION_KEY],
|
||||
)
|
||||
if scheme == "ecdsa_v1":
|
||||
if not has_signing_key and not has_verification_key:
|
||||
raise cv.Invalid(
|
||||
f"Signing scheme 'ecdsa_v1' requires either '{CONF_SIGNING_KEY}' "
|
||||
f"(to sign binaries during the build) or '{CONF_VERIFICATION_KEY}' "
|
||||
f"(to verify binaries signed externally).",
|
||||
path=[CONF_SIGNING_KEY],
|
||||
)
|
||||
elif has_verification_key:
|
||||
raise cv.Invalid(
|
||||
f"'{CONF_VERIFICATION_KEY}' is only used with signing scheme "
|
||||
f"'ecdsa_v1'. With '{scheme}' the public key is embedded in each "
|
||||
f"image's signature block, so no key file is needed to verify "
|
||||
f"externally-signed binaries: remove '{CONF_VERIFICATION_KEY}', and "
|
||||
f"set '{CONF_SIGNING_KEY}' only if binaries should be signed during "
|
||||
f"the build.",
|
||||
path=[CONF_VERIFICATION_KEY],
|
||||
)
|
||||
return config
|
||||
|
||||
|
||||
def final_validate(config):
|
||||
# Imported locally to avoid circular import issues
|
||||
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
|
||||
@@ -1345,10 +1429,33 @@ def final_validate(config):
|
||||
)
|
||||
else:
|
||||
_LOGGER.info(
|
||||
"Signed OTA verification is configured with a public verification key. "
|
||||
"Signed OTA verification is enabled without a signing key. "
|
||||
"Binaries will NOT be signed automatically during build. "
|
||||
"You must sign them externally before flashing."
|
||||
)
|
||||
if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None:
|
||||
variant = config[CONF_VARIANT]
|
||||
if variant in NVS_ENCRYPTION_HMAC_VARIANTS:
|
||||
_LOGGER.warning(
|
||||
"NVS encryption will burn an HMAC key into eFuse key block %d on the "
|
||||
"first boot of each device. This is PERMANENT and IRREVERSIBLE: "
|
||||
"the block cannot be erased or reused afterwards. Enabling (or "
|
||||
"later disabling) encryption also wipes any previously saved "
|
||||
"preferences once, because the older data can no longer be read.",
|
||||
nvs_enc[CONF_KEY_ID],
|
||||
)
|
||||
else:
|
||||
supported = ", ".join(
|
||||
sorted(VARIANT_FRIENDLY[v] for v in NVS_ENCRYPTION_HMAC_VARIANTS)
|
||||
)
|
||||
errs.append(
|
||||
cv.Invalid(
|
||||
f"NVS encryption (HMAC scheme) is not supported on "
|
||||
f"{VARIANT_FRIENDLY[variant]} (it has no HMAC peripheral). "
|
||||
f"Supported variants: {supported}.",
|
||||
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_NVS_ENCRYPTION],
|
||||
)
|
||||
)
|
||||
if advanced[CONF_ENABLE_OTA_DOWNGRADE_PROTECTION]:
|
||||
project = full_config[CONF_ESPHOME].get(CONF_PROJECT)
|
||||
errs.extend(
|
||||
@@ -1539,16 +1646,20 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO),
|
||||
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
|
||||
cv.Optional(CONF_RELEASE): cv.string_strict,
|
||||
cv.Optional(CONF_SOURCE): cv.string_strict,
|
||||
cv.Optional(CONF_PLATFORM_VERSION): _parse_pio_platform_version,
|
||||
cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
|
||||
cv.string_strict: cv.string_strict
|
||||
},
|
||||
cv.Optional(CONF_RELEASE, visibility=cv.Visibility.YAML_ONLY): cv.string_strict,
|
||||
cv.Optional(CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY): cv.string_strict,
|
||||
cv.Optional(
|
||||
CONF_PLATFORM_VERSION, visibility=cv.Visibility.YAML_ONLY
|
||||
): _parse_pio_platform_version,
|
||||
cv.Optional(
|
||||
CONF_SDKCONFIG_OPTIONS, default={}, visibility=cv.Visibility.YAML_ONLY
|
||||
): {cv.string_strict: cv.string_strict},
|
||||
cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of(
|
||||
*LOG_LEVELS_IDF, upper=True
|
||||
),
|
||||
cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
|
||||
cv.Optional(
|
||||
CONF_ADVANCED, default={}, visibility=cv.Visibility.YAML_ONLY
|
||||
): cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of(
|
||||
*ASSERTION_LEVELS, upper=True
|
||||
@@ -1597,17 +1708,17 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(
|
||||
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION, default=False
|
||||
): cv.boolean,
|
||||
cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_SIGNING_KEY): cv.file_,
|
||||
cv.Optional(CONF_VERIFICATION_KEY): cv.file_,
|
||||
cv.Optional(
|
||||
CONF_SIGNING_SCHEME, default="rsa3072"
|
||||
): cv.one_of(*SIGNING_SCHEMES, lower=True),
|
||||
}
|
||||
),
|
||||
cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
|
||||
cv.Optional(
|
||||
CONF_SIGNED_OTA_VERIFICATION
|
||||
): _validate_signed_ota_verification,
|
||||
cv.Optional(CONF_NVS_ENCRYPTION): cv.Schema(
|
||||
{
|
||||
# eFuse key block (0-5) that stores the HMAC key from
|
||||
# which the NVS encryption keys are derived. The block is
|
||||
# written on first boot if empty -- an irreversible
|
||||
# operation -- so it must be chosen explicitly.
|
||||
cv.Required(CONF_KEY_ID): cv.int_range(min=0, max=5),
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
|
||||
@@ -1629,7 +1740,9 @@ FRAMEWORK_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DISABLE_FATFS, default=True): cv.boolean,
|
||||
}
|
||||
),
|
||||
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
|
||||
cv.Optional(
|
||||
CONF_COMPONENTS, default=[], visibility=cv.Visibility.YAML_ONLY
|
||||
): cv.ensure_list(
|
||||
cv.All(
|
||||
cv.Any(
|
||||
cv.All(cv.string_strict, _parse_idf_component),
|
||||
@@ -1729,7 +1842,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_FLASH_FREQUENCY): cv.one_of(
|
||||
*FLASH_FREQUENCIES, upper=True
|
||||
),
|
||||
cv.Optional(CONF_PARTITIONS): cv.Any(
|
||||
cv.Optional(CONF_PARTITIONS, visibility=cv.Visibility.YAML_ONLY): cv.Any(
|
||||
cv.file_,
|
||||
cv.ensure_list(
|
||||
cv.All(
|
||||
@@ -1753,7 +1866,9 @@ CONFIG_SCHEMA = cv.All(
|
||||
),
|
||||
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
|
||||
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
|
||||
cv.Optional(CONF_TOOLCHAIN): _validate_toolchain,
|
||||
cv.Optional(
|
||||
CONF_TOOLCHAIN, visibility=cv.Visibility.ADVANCED
|
||||
): _validate_toolchain,
|
||||
cv.Optional(CONF_WATCHDOG_TIMEOUT, default="5s"): cv.All(
|
||||
cv.positive_time_period_seconds,
|
||||
cv.Range(min=cv.TimePeriod(seconds=5), max=cv.TimePeriod(seconds=60)),
|
||||
@@ -2442,15 +2557,33 @@ async def to_code(config):
|
||||
signed_ota[CONF_SIGNING_KEY].resolve().as_posix(),
|
||||
)
|
||||
else:
|
||||
# Public key mode — verification only, external signing required
|
||||
# External signing mode — binaries must be signed after the build
|
||||
add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", False)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SECURE_BOOT_VERIFICATION_KEY",
|
||||
signed_ota[CONF_VERIFICATION_KEY].resolve().as_posix(),
|
||||
)
|
||||
if CONF_VERIFICATION_KEY in signed_ota:
|
||||
# V1 ECDSA only: the public key is compiled into the app to
|
||||
# verify externally-signed images. V2 schemes carry the public
|
||||
# key in each image's signature block and need no key here.
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_SECURE_BOOT_VERIFICATION_KEY",
|
||||
signed_ota[CONF_VERIFICATION_KEY].resolve().as_posix(),
|
||||
)
|
||||
|
||||
cg.add_define("USE_OTA_SIGNED_VERIFICATION")
|
||||
|
||||
# Encrypt NVS using the HMAC peripheral scheme. The NVS encryption keys are
|
||||
# derived at runtime from an HMAC key stored in the configured eFuse block
|
||||
# (no flash encryption required). The HMAC key is generated and burned into
|
||||
# the eFuse block on first boot if it is empty. With the scheme selected,
|
||||
# nvs_sec_provider registers it at startup and the default nvs_flash_init()
|
||||
# (used in esp32/preferences.cpp) transparently performs the secure init, so
|
||||
# no C++ changes are needed.
|
||||
if (nvs_enc := advanced.get(CONF_NVS_ENCRYPTION)) is not None:
|
||||
add_idf_sdkconfig_option("CONFIG_NVS_ENCRYPTION", True)
|
||||
add_idf_sdkconfig_option("CONFIG_NVS_SEC_KEY_PROTECT_USING_HMAC", True)
|
||||
add_idf_sdkconfig_option(
|
||||
"CONFIG_NVS_SEC_HMAC_EFUSE_KEY_ID", nvs_enc[CONF_KEY_ID]
|
||||
)
|
||||
|
||||
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
|
||||
|
||||
cg.add_define(
|
||||
@@ -2925,19 +3058,23 @@ def copy_files():
|
||||
|
||||
|
||||
def _decode_pc(config, addr):
|
||||
# _decode_pc runs from the api log processor's asyncio callback, which
|
||||
# only catches EsphomeError. Any other exception escaping here tears down
|
||||
# the protocol and triggers an infinite reconnect/replay loop. Convert
|
||||
# toolchain-resolution errors (e.g. missing build dir / cmake cache) into
|
||||
# EsphomeError so the caller can disable decoding cleanly.
|
||||
# Convert toolchain-resolution errors (e.g. missing build dir / cmake
|
||||
# cache) into EsphomeError. The api log processor stops decoding on any
|
||||
# exception, so this is about the message it reports rather than about
|
||||
# catching it at all: EsphomeError carries an explanation worth showing
|
||||
# the user, where a raw OSError repr does not.
|
||||
if CORE.using_toolchain_esp_idf:
|
||||
from esphome.espidf import toolchain as idf_toolchain
|
||||
|
||||
try:
|
||||
addr2line_path = idf_toolchain.get_addr2line_path()
|
||||
firmware_elf_path = idf_toolchain.get_elf_path()
|
||||
except RuntimeError as err:
|
||||
except (RuntimeError, OSError) as err:
|
||||
# OSError covers a missing build directory or a cmake that isn't
|
||||
# on PATH; both surface from the subprocess call, not as RuntimeError.
|
||||
raise EsphomeError(f"ESP-IDF toolchain not available: {err}") from err
|
||||
if not firmware_elf_path.is_file():
|
||||
raise EsphomeError(f"Firmware ELF not found: {firmware_elf_path}")
|
||||
else:
|
||||
from esphome.platformio import toolchain
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
#include <esp_bt.h>
|
||||
#else
|
||||
#include "esphome/components/watchdog/watchdog.h"
|
||||
#include <cinttypes>
|
||||
extern "C" {
|
||||
#include <esp_hosted.h>
|
||||
#include <esp_hosted_misc.h>
|
||||
@@ -33,6 +35,19 @@ namespace esphome::esp32_ble {
|
||||
|
||||
static const char *const TAG = "esp32_ble";
|
||||
|
||||
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
// Bringing up the remote BT controller issues synchronous RPCs to the
|
||||
// co-processor with 5 second response timeouts, and the default task watchdog
|
||||
// is also 5 seconds. If the co-processor firmware does not answer (for example
|
||||
// factory firmware without Bluetooth support), the watchdog would reboot the
|
||||
// device before the RPC could return an error, causing a boot loop. Raise the
|
||||
// watchdog for the duration of the bring-up so failures surface as error
|
||||
// returns instead. 60 seconds covers the worst case: transport reconnect
|
||||
// (up to ~20s), version preflight (1s), controller init/enable (5s each) and
|
||||
// the bluedroid host bring-up over the hosted HCI transport.
|
||||
static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
|
||||
#endif
|
||||
|
||||
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
|
||||
#define GAP_SCAN_COMPLETE_EVENTS \
|
||||
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
|
||||
@@ -164,6 +179,9 @@ void ESP32BLE::advertising_init_() {
|
||||
|
||||
bool ESP32BLE::ble_setup_() {
|
||||
esp_err_t err;
|
||||
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS);
|
||||
#endif
|
||||
#ifndef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
if (esp_bt_controller_get_status() != ESP_BT_CONTROLLER_STATUS_ENABLED) {
|
||||
// start bt controller
|
||||
@@ -192,15 +210,35 @@ bool ESP32BLE::ble_setup_() {
|
||||
|
||||
esp_bt_controller_mem_release(ESP_BT_MODE_CLASSIC_BT);
|
||||
#else
|
||||
esp_hosted_connect_to_slave(); // NOLINT
|
||||
if (esp_hosted_connect_to_slave() != ESP_OK) { // NOLINT
|
||||
ESP_LOGE(TAG, "Co-processor transport failed; BLE disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fast preflight (1 second RPC timeout): verifies the co-processor answers
|
||||
// RPCs at all before the 5 second timeout BT controller RPCs below, and
|
||||
// before hosted_hci_bluedroid_open(), which aborts if the transport is down.
|
||||
esp_hosted_coprocessor_fwver_t fw_ver{};
|
||||
if (esp_hosted_get_coprocessor_fwversion(&fw_ver) != ESP_OK) {
|
||||
ESP_LOGE(TAG, "Co-processor not responding; BLE disabled. Update its firmware with the esp32_hosted "
|
||||
"update component");
|
||||
return false;
|
||||
}
|
||||
ESP_LOGD(TAG, "Co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32, fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
|
||||
|
||||
if (esp_hosted_bt_controller_init() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_init failed");
|
||||
ESP_LOGE(TAG,
|
||||
"BT controller init failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32
|
||||
" may lack BT support. Update it with the esp32_hosted update component; BLE disabled",
|
||||
fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_hosted_bt_controller_enable() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_enable failed");
|
||||
ESP_LOGE(TAG,
|
||||
"BT controller enable failed; co-processor firmware %" PRIu32 ".%" PRIu32 ".%" PRIu32
|
||||
" may lack BT support. Update it with the esp32_hosted update component; BLE disabled",
|
||||
fw_ver.major1, fw_ver.minor1, fw_ver.patch1);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -332,6 +370,10 @@ bool ESP32BLE::ble_setup_() {
|
||||
}
|
||||
|
||||
bool ESP32BLE::ble_dismantle_() {
|
||||
#ifdef CONFIG_ESP_HOSTED_ENABLE_BT_BLUEDROID
|
||||
// Same 5 second RPCs as the bring-up path; see HOSTED_BT_WDT_TIMEOUT_MS
|
||||
watchdog::WatchdogManager wdt(HOSTED_BT_WDT_TIMEOUT_MS);
|
||||
#endif
|
||||
esp_err_t err = esp_bluedroid_disable();
|
||||
if (err != ESP_OK) {
|
||||
// ESP_ERR_INVALID_STATE means Bluedroid is already disabled, which is fine
|
||||
@@ -377,12 +419,12 @@ bool ESP32BLE::ble_dismantle_() {
|
||||
}
|
||||
#else
|
||||
if (esp_hosted_bt_controller_disable() != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_disable failed");
|
||||
ESP_LOGE(TAG, "esp_hosted_bt_controller_disable failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (esp_hosted_bt_controller_deinit(false) != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_hosted_bt_controller_deinit failed");
|
||||
ESP_LOGE(TAG, "esp_hosted_bt_controller_deinit failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -181,12 +181,6 @@ const char *ESPBTUUID::to_str(std::span<char, UUID_STR_LEN> output) const {
|
||||
return output.data();
|
||||
}
|
||||
}
|
||||
std::string ESPBTUUID::to_string() const {
|
||||
char buf[UUID_STR_LEN];
|
||||
this->to_str(buf);
|
||||
return std::string(buf);
|
||||
}
|
||||
|
||||
} // namespace esphome::esp32_ble
|
||||
|
||||
#endif // USE_ESP32_BLE_UUID
|
||||
|
||||
@@ -46,9 +46,6 @@ class ESPBTUUID {
|
||||
|
||||
esp_bt_uuid_t get_uuid() const;
|
||||
|
||||
// Remove before 2026.8.0
|
||||
ESPDEPRECATED("Use to_str() instead. Removed in 2026.8.0", "2026.2.0")
|
||||
std::string to_string() const; // NOLINT
|
||||
const char *to_str(std::span<char, UUID_STR_LEN> output) const;
|
||||
|
||||
protected:
|
||||
|
||||
@@ -77,6 +77,10 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
|
||||
case ESP_GATTS_WRITE_EVT: {
|
||||
if (this->handle_ != param->write.handle)
|
||||
break;
|
||||
if (param->write.len > this->value_.attr_max_len) {
|
||||
ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", param->write.len, this->value_.attr_max_len);
|
||||
break;
|
||||
}
|
||||
this->value_.attr_len = param->write.len;
|
||||
memcpy(this->value_.attr_value, param->write.value, param->write.len);
|
||||
if (this->on_write_callback_) {
|
||||
|
||||
@@ -18,6 +18,8 @@ from esphome.const import (
|
||||
from esphome.cpp_generator import add_define
|
||||
|
||||
CODEOWNERS = ["@swoboda1337"]
|
||||
# esp32_ble raises the task watchdog around the remote BT controller bring-up
|
||||
AUTO_LOAD = ["watchdog"]
|
||||
|
||||
CONF_ACTIVE_HIGH = "active_high"
|
||||
CONF_BUS_WIDTH = "bus_width"
|
||||
|
||||
@@ -7,6 +7,10 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#ifdef USE_PROVISIONING
|
||||
#include "esphome/components/provisioning/provisioning.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
namespace esphome::esp32_improv {
|
||||
@@ -41,6 +45,15 @@ void ESP32ImprovComponent::setup() {
|
||||
#endif
|
||||
global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
|
||||
|
||||
#ifdef USE_PROVISIONING
|
||||
if (provisioning::global_provisioning_manager != nullptr) {
|
||||
provisioning::global_provisioning_manager->add_on_closed_callback([this]() {
|
||||
ESP_LOGD(TAG, "Provisioning window closed; stopping Improv");
|
||||
this->stop();
|
||||
});
|
||||
}
|
||||
#endif
|
||||
|
||||
// Start with loop disabled - will be enabled by start() when needed
|
||||
this->disable_loop();
|
||||
}
|
||||
@@ -282,6 +295,15 @@ void ESP32ImprovComponent::start() {
|
||||
if (this->should_start_ || this->state_ != improv::STATE_STOPPED)
|
||||
return;
|
||||
|
||||
#ifdef USE_PROVISIONING
|
||||
// Don't (re)start advertising once the provisioning window has closed - e.g. when
|
||||
// wifi tries to restart Improv after the window expired at runtime.
|
||||
if (provisioning::global_provisioning_manager != nullptr && provisioning::global_provisioning_manager->closed()) {
|
||||
ESP_LOGD(TAG, "Provisioning window closed; not starting Improv");
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
ESP_LOGD(TAG, "Setting Improv to start");
|
||||
this->should_start_ = true;
|
||||
this->enable_loop();
|
||||
@@ -338,6 +360,15 @@ void ESP32ImprovComponent::process_incoming_data_() {
|
||||
this->incoming_data_.clear();
|
||||
return;
|
||||
}
|
||||
#ifdef USE_PROVISIONING
|
||||
if (provisioning::global_provisioning_manager != nullptr &&
|
||||
provisioning::global_provisioning_manager->closed()) {
|
||||
ESP_LOGW(TAG, "Provisioning window closed; refusing settings");
|
||||
this->set_error_(improv::ERROR_NOT_AUTHORIZED);
|
||||
this->incoming_data_.clear();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
if (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.
|
||||
|
||||
@@ -202,8 +202,12 @@ ARDUINO_FRAMEWORK_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
|
||||
cv.Optional(CONF_SOURCE): cv.string_strict,
|
||||
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
|
||||
cv.Optional(
|
||||
CONF_SOURCE, visibility=cv.Visibility.YAML_ONLY
|
||||
): cv.string_strict,
|
||||
cv.Optional(
|
||||
CONF_PLATFORM_VERSION, visibility=cv.Visibility.YAML_ONLY
|
||||
): _parse_platform_version,
|
||||
}
|
||||
),
|
||||
_arduino_check_versions,
|
||||
|
||||
@@ -112,8 +112,10 @@ enum class EthernetComponentState : uint8_t {
|
||||
|
||||
// Platform-neutral duplex/speed types
|
||||
#ifndef USE_ESP32
|
||||
// NOLINTBEGIN(readability-identifier-naming)
|
||||
enum eth_duplex_t { ETH_DUPLEX_HALF, ETH_DUPLEX_FULL };
|
||||
enum eth_speed_t { ETH_SPEED_10M, ETH_SPEED_100M };
|
||||
// NOLINTEND(readability-identifier-naming)
|
||||
#endif
|
||||
|
||||
class EthernetComponent final : public Component {
|
||||
|
||||
@@ -187,17 +187,18 @@ void EthernetComponent::loop() {
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_config() {
|
||||
const char *type_str = "Unknown";
|
||||
#if defined(USE_ETHERNET_W5500)
|
||||
type_str = "W5500";
|
||||
const char *type_str = "W5500";
|
||||
#elif defined(USE_ETHERNET_W5100)
|
||||
type_str = "W5100";
|
||||
const char *type_str = "W5100";
|
||||
#elif defined(USE_ETHERNET_W6100)
|
||||
type_str = "W6100";
|
||||
const char *type_str = "W6100";
|
||||
#elif defined(USE_ETHERNET_W6300)
|
||||
type_str = "W6300";
|
||||
const char *type_str = "W6300";
|
||||
#elif defined(USE_ETHERNET_ENC28J60)
|
||||
type_str = "ENC28J60";
|
||||
const char *type_str = "ENC28J60";
|
||||
#else
|
||||
const char *type_str = "Unknown";
|
||||
#endif
|
||||
#if defined(USE_ETHERNET_W6300)
|
||||
// W6300 uses PIO QSPI with hardcoded pins — SPI pin fields are not used
|
||||
|
||||
@@ -50,7 +50,9 @@ _EVENT_SCHEMA = (
|
||||
{
|
||||
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(mqtt.MQTTEventComponent),
|
||||
cv.GenerateID(): cv.declare_id(Event),
|
||||
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
|
||||
cv.Optional(
|
||||
CONF_DEVICE_CLASS, visibility=cv.Visibility.ADVANCED
|
||||
): validate_device_class,
|
||||
cv.Optional(CONF_ON_EVENT): automation.validate_automation({}),
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY)
|
||||
|
||||
#include "fastled_light.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#ifdef USE_ARDUINO
|
||||
#if defined(USE_ARDUINO) && !defined(USE_RP2) && !defined(USE_LIBRETINY)
|
||||
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -0,0 +1,315 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import hashlib
|
||||
import io
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
|
||||
from esphome import core, external_files
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.const import CONF_BYTE_ORDER
|
||||
from esphome.components.image import (
|
||||
CONF_INVERT_ALPHA,
|
||||
CONF_OPAQUE,
|
||||
CONF_TRANSPARENCY,
|
||||
DOMAIN,
|
||||
IMAGE_TYPE,
|
||||
Image_,
|
||||
ImageEncoder,
|
||||
add_metadata,
|
||||
get_image_type_enum,
|
||||
get_transparency_enum,
|
||||
is_svg_file,
|
||||
validate_settings,
|
||||
validate_transparency,
|
||||
validate_type,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_DITHER,
|
||||
CONF_FILE,
|
||||
CONF_ICON,
|
||||
CONF_ID,
|
||||
CONF_PATH,
|
||||
CONF_RAW_DATA_ID,
|
||||
CONF_RESIZE,
|
||||
CONF_SOURCE,
|
||||
CONF_TYPE,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import CORE, HexInt
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
# If the MDI file cannot be downloaded within this time, abort.
|
||||
IMAGE_DOWNLOAD_TIMEOUT = 30 # seconds
|
||||
|
||||
SOURCE_LOCAL = "local"
|
||||
SOURCE_WEB = "web"
|
||||
|
||||
SOURCE_MDI = "mdi"
|
||||
SOURCE_MDIL = "mdil"
|
||||
SOURCE_MEMORY = "memory"
|
||||
|
||||
MDI_SOURCES = {
|
||||
SOURCE_MDI: "https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/",
|
||||
SOURCE_MDIL: "https://raw.githubusercontent.com/Pictogrammers/MaterialDesignLight/refs/heads/master/svg/",
|
||||
SOURCE_MEMORY: "https://raw.githubusercontent.com/Pictogrammers/Memory/refs/heads/main/src/svg/",
|
||||
}
|
||||
|
||||
|
||||
def compute_local_image_path(value) -> Path:
|
||||
url = value[CONF_URL] if isinstance(value, dict) else value
|
||||
h = hashlib.new("sha256")
|
||||
h.update(url.encode())
|
||||
key = h.hexdigest()[:8]
|
||||
# Downloaded files are cached under the shared `image` domain directory so
|
||||
# the cache location is unaffected by which platform requested the file.
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN)
|
||||
return base_dir / key
|
||||
|
||||
|
||||
def local_path(value):
|
||||
value = value[CONF_PATH] if isinstance(value, dict) else value
|
||||
return str(CORE.relative_config_path(value))
|
||||
|
||||
|
||||
def download_file(url, path):
|
||||
external_files.download_content(url, path, IMAGE_DOWNLOAD_TIMEOUT)
|
||||
return str(path)
|
||||
|
||||
|
||||
def download_gh_svg(value, source):
|
||||
mdi_id = value[CONF_ICON] if isinstance(value, dict) else value
|
||||
base_dir = external_files.compute_local_file_dir(DOMAIN) / source
|
||||
path = base_dir / f"{mdi_id}.svg"
|
||||
|
||||
url = MDI_SOURCES[source] + mdi_id + ".svg"
|
||||
return download_file(url, path)
|
||||
|
||||
|
||||
def download_image(value):
|
||||
value = value[CONF_URL] if isinstance(value, dict) else value
|
||||
return download_file(value, compute_local_image_path(value))
|
||||
|
||||
|
||||
def validate_file_shorthand(value):
|
||||
value = cv.string_strict(value)
|
||||
parts = value.strip().split(":")
|
||||
if len(parts) == 2 and parts[0] in MDI_SOURCES:
|
||||
match = re.match(r"^[a-zA-Z0-9\-]+$", parts[1])
|
||||
if match is None:
|
||||
raise cv.Invalid(f"Could not parse mdi icon name from '{value}'.")
|
||||
return download_gh_svg(parts[1], parts[0])
|
||||
|
||||
if value.startswith(("http://", "https://")):
|
||||
return download_image(value)
|
||||
|
||||
value = cv.file_(value)
|
||||
return local_path(value)
|
||||
|
||||
|
||||
LOCAL_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_PATH): cv.file_,
|
||||
},
|
||||
local_path,
|
||||
)
|
||||
|
||||
|
||||
def mdi_schema(source):
|
||||
def validate_mdi(value):
|
||||
return download_gh_svg(value, source)
|
||||
|
||||
return cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ICON): cv.string,
|
||||
}
|
||||
),
|
||||
validate_mdi,
|
||||
)
|
||||
|
||||
|
||||
WEB_SCHEMA = cv.All(
|
||||
{
|
||||
cv.Required(CONF_URL): cv.string,
|
||||
},
|
||||
download_image,
|
||||
)
|
||||
|
||||
|
||||
TYPED_FILE_SCHEMA = cv.typed_schema(
|
||||
{
|
||||
SOURCE_LOCAL: LOCAL_SCHEMA,
|
||||
SOURCE_WEB: WEB_SCHEMA,
|
||||
}
|
||||
| {source: mdi_schema(source) for source in MDI_SOURCES},
|
||||
key=CONF_SOURCE,
|
||||
)
|
||||
|
||||
|
||||
OPTIONS_SCHEMA = {
|
||||
cv.Optional(CONF_RESIZE): cv.dimensions,
|
||||
cv.Optional(CONF_DITHER, default="NONE"): cv.one_of(
|
||||
"NONE", "FLOYDSTEINBERG", upper=True
|
||||
),
|
||||
cv.Optional(CONF_INVERT_ALPHA, default=False): cv.boolean,
|
||||
cv.Optional(CONF_BYTE_ORDER): cv.one_of("BIG_ENDIAN", "LITTLE_ENDIAN", upper=True),
|
||||
cv.Optional(CONF_TRANSPARENCY, default=CONF_OPAQUE): validate_transparency(),
|
||||
}
|
||||
|
||||
|
||||
def image_schema(class_: MockObjClass = Image_) -> cv.Schema:
|
||||
"""Build the validation schema for a single file-backed image entry.
|
||||
|
||||
Shared by the built-in ``file`` image platform and the ``animation``
|
||||
platform (which extends it). Platforms that source their pixels elsewhere
|
||||
(e.g. ``online_image``) provide their own schema instead.
|
||||
|
||||
:param class_: The declared C++ class for the generated image instance.
|
||||
"""
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(class_),
|
||||
cv.Required(CONF_FILE): cv.Any(validate_file_shorthand, TYPED_FILE_SCHEMA),
|
||||
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
|
||||
**OPTIONS_SCHEMA,
|
||||
cv.Required(CONF_TYPE): validate_type(IMAGE_TYPE),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def validate_image_final(config: ConfigType) -> ConfigType:
|
||||
"""Per-entry final validation, shared by file-backed image platforms.
|
||||
|
||||
For LVGL 9 the default byte order for RGB565 images is little-endian, so
|
||||
fill in that default when the user did not specify a byte order and warn
|
||||
when big-endian was explicitly requested.
|
||||
"""
|
||||
if byte_order := config.get(CONF_BYTE_ORDER):
|
||||
if byte_order == "BIG_ENDIAN":
|
||||
_LOGGER.warning(
|
||||
"The image '%s' is configured with big-endian byte order, little-endian is expected",
|
||||
config.get(CONF_FILE),
|
||||
)
|
||||
else:
|
||||
config[CONF_BYTE_ORDER] = "LITTLE_ENDIAN"
|
||||
return config
|
||||
|
||||
|
||||
async def new_image(config: ConfigType) -> MockObj:
|
||||
"""Generate a single file-backed ``image::Image`` instance.
|
||||
|
||||
Used by the built-in ``file`` platform; encodes the image data, registers
|
||||
the C++ variable and records its metadata for other components to consume.
|
||||
"""
|
||||
prog_arr, width, height, image_type, trans_value, _ = await write_image(config)
|
||||
var = cg.new_Pvariable(
|
||||
config[CONF_ID], prog_arr, width, height, image_type, trans_value
|
||||
)
|
||||
add_metadata(
|
||||
config[CONF_ID], width, height, config[CONF_TYPE], config[CONF_TRANSPARENCY]
|
||||
)
|
||||
return var
|
||||
|
||||
|
||||
async def write_image(config, all_frames=False):
|
||||
path = Path(config[CONF_FILE])
|
||||
if not path.is_file():
|
||||
raise core.EsphomeError(f"Could not load image file {path}")
|
||||
|
||||
resize = config.get(CONF_RESIZE)
|
||||
try:
|
||||
if is_svg_file(path):
|
||||
import resvg_py
|
||||
|
||||
resize = resize or (None, None)
|
||||
image_data = resvg_py.svg_to_bytes(
|
||||
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
|
||||
)
|
||||
|
||||
# Convert bytes to Pillow Image
|
||||
image = Image.open(io.BytesIO(image_data))
|
||||
width, height = image.size
|
||||
|
||||
else:
|
||||
image = Image.open(path)
|
||||
width, height = image.size
|
||||
if resize:
|
||||
# Preserve aspect ratio
|
||||
new_width_max = min(width, resize[0])
|
||||
new_height_max = min(height, resize[1])
|
||||
ratio = min(new_width_max / width, new_height_max / height)
|
||||
width, height = int(width * ratio), int(height * ratio)
|
||||
except (OSError, UnidentifiedImageError, ValueError) as exc:
|
||||
raise core.EsphomeError(f"Could not read image file {path}: {exc}") from exc
|
||||
|
||||
if not resize and (width > 500 or height > 500):
|
||||
_LOGGER.warning(
|
||||
'The image "%s" you requested is very big. Please consider'
|
||||
" using the resize parameter.",
|
||||
path,
|
||||
)
|
||||
|
||||
dither = (
|
||||
Image.Dither.NONE
|
||||
if config[CONF_DITHER] == "NONE"
|
||||
else Image.Dither.FLOYDSTEINBERG
|
||||
)
|
||||
type = config[CONF_TYPE]
|
||||
transparency = config.get(CONF_TRANSPARENCY, CONF_OPAQUE)
|
||||
invert_alpha = config[CONF_INVERT_ALPHA]
|
||||
frame_count = 1
|
||||
if all_frames:
|
||||
with contextlib.suppress(AttributeError):
|
||||
frame_count = image.n_frames
|
||||
if frame_count <= 1:
|
||||
_LOGGER.warning("Image file %s has no animation frames", path)
|
||||
|
||||
# Encode each frame with its own encoder and concatenate. This keeps every
|
||||
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
|
||||
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
|
||||
# correct without needing to know the total frame count.
|
||||
byte_order = config.get(CONF_BYTE_ORDER)
|
||||
combined_data: list[int] = []
|
||||
encoder: ImageEncoder | None = None
|
||||
for frame_index in range(frame_count):
|
||||
image.seek(frame_index)
|
||||
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
|
||||
if byte_order is not None:
|
||||
# Check for valid type has already been done in validate_settings
|
||||
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
|
||||
pixels = encoder.convert(image.resize((width, height)), path).getdata()
|
||||
for row in range(height):
|
||||
for col in range(width):
|
||||
encoder.encode(pixels[row * width + col])
|
||||
encoder.end_row()
|
||||
encoder.end_image()
|
||||
combined_data.extend(encoder.data)
|
||||
|
||||
rhs = [HexInt(x) for x in combined_data]
|
||||
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
|
||||
image_type = get_image_type_enum(type)
|
||||
trans_value = get_transparency_enum(encoder.transparency)
|
||||
|
||||
return prog_arr, width, height, image_type, trans_value, frame_count
|
||||
|
||||
|
||||
# The built-in static-image platform: pixels embedded at compile time from a
|
||||
# local file, a downloaded web image, or a Material Design Icon.
|
||||
CONFIG_SCHEMA = cv.All(image_schema(Image_), validate_settings)
|
||||
|
||||
FINAL_VALIDATE_SCHEMA = validate_image_final
|
||||
|
||||
|
||||
async def to_code(config: ConfigType) -> None:
|
||||
await new_image(config)
|
||||
@@ -0,0 +1 @@
|
||||
CODEOWNERS = ["@clydebarrow"]
|
||||
@@ -0,0 +1,167 @@
|
||||
#include "gsl3670_touchscreen.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/hal.h"
|
||||
|
||||
namespace esphome::gsl3670 {
|
||||
|
||||
static const char *const TAG = "gsl3670.touchscreen";
|
||||
static const size_t MAX_TOUCHES = 3;
|
||||
// ---------------------------------------------------------------------------
|
||||
// setup() – mirrors esp_lcd_touch_gsl3670_init() in the Seeed BSP:
|
||||
// clear_reg → reset → load_fw → startup_chip → reset → startup_chip
|
||||
// ---------------------------------------------------------------------------
|
||||
void GSL3670Touchscreen::setup() {
|
||||
ESP_LOGCONFIG(TAG, "Setting up GSL3670 touchscreen...");
|
||||
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->setup();
|
||||
this->reset_pin_->digital_write(true);
|
||||
}
|
||||
|
||||
if (this->interrupt_pin_ != nullptr) {
|
||||
this->interrupt_pin_->setup();
|
||||
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
|
||||
}
|
||||
|
||||
if (this->x_raw_max_ == this->x_raw_min_) {
|
||||
this->x_raw_max_ = this->display_->get_native_width();
|
||||
}
|
||||
if (this->y_raw_max_ == this->y_raw_min_) {
|
||||
this->y_raw_max_ = this->display_->get_native_height();
|
||||
}
|
||||
|
||||
this->clear_reg_();
|
||||
this->reset_();
|
||||
this->load_firmware_();
|
||||
this->startup_chip_();
|
||||
this->reset_();
|
||||
this->startup_chip_();
|
||||
|
||||
ESP_LOGCONFIG(TAG, "GSL3670 initialised OK");
|
||||
}
|
||||
|
||||
void GSL3670Touchscreen::dump_config() {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
"GSL3670 Touchscreen:\n"
|
||||
" X-raw-max: %d\n"
|
||||
" Y-raw-max: %d\n",
|
||||
this->x_raw_max_, this->y_raw_max_);
|
||||
LOG_I2C_DEVICE(this);
|
||||
LOG_PIN(" Reset Pin: ", this->reset_pin_);
|
||||
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
|
||||
ESP_LOGCONFIG(TAG, " Firmware records: %zu", this->firmware_len_);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// update_touches() – mirrors esp_lcd_touch_gsl3670_read_data() in Seeed BSP
|
||||
// ---------------------------------------------------------------------------
|
||||
void GSL3670Touchscreen::update_touches() {
|
||||
uint8_t buf[44] = {};
|
||||
auto err = this->read_register(0x80, buf, sizeof(buf));
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "I2C read failed (%d)", err);
|
||||
return;
|
||||
}
|
||||
uint8_t finger_num = clamp_at_most(buf[0], MAX_TOUCHES);
|
||||
|
||||
// Build gsl_touch_info exactly as the Seeed driver does
|
||||
for (uint8_t j = 0; j != finger_num; j++) {
|
||||
// buf[(j+1)*4 + 0..3]: byte0=y_lo, byte1=y_hi, byte2=x_lo, byte3=id|x_hi
|
||||
auto x = (uint16_t) (((buf[(j + 1) * 4 + 3] & 0x0f) << 8) | buf[(j + 1) * 4 + 2]);
|
||||
auto y = (uint16_t) ((buf[(j + 1) * 4 + 1] << 8) | buf[(j + 1) * 4 + 0]);
|
||||
auto id = (buf[(j + 1) * 4 + 3] >> 4) & 0x0f;
|
||||
ESP_LOGV(TAG, "Touch id=%u, x=%u y=%u", id, x, y);
|
||||
if (x <= 8192 && y <= 8192)
|
||||
this->add_raw_touch_position_(id, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// clear_reg_() – mirrors esp_lcd_touch_gsl3670_clear_reg()
|
||||
// GPIO reset → write 0x01 to 0x88 → write 0x04 to 0xe4 → write 0x00 to 0xe0
|
||||
// ---------------------------------------------------------------------------
|
||||
void GSL3670Touchscreen::clear_reg_() {
|
||||
ESP_LOGD(TAG, "clear_reg");
|
||||
|
||||
// GPIO reset pulse
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(1);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
}
|
||||
|
||||
this->write_reg8_(0x88, 0x01);
|
||||
// delay(5);
|
||||
this->write_reg8_(0xe4, 0x04);
|
||||
// delay(5);
|
||||
this->write_reg8_(0xe0, 0x00);
|
||||
// delay(5);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// reset_() – mirrors touch_gsl3670_reset()
|
||||
// GPIO reset → write 0x04 to 0xe4 → write 4×0x00 to 0xbc
|
||||
// ---------------------------------------------------------------------------
|
||||
void GSL3670Touchscreen::reset_() {
|
||||
ESP_LOGD(TAG, "reset");
|
||||
|
||||
if (this->reset_pin_ != nullptr) {
|
||||
this->reset_pin_->digital_write(false);
|
||||
delay(1);
|
||||
this->reset_pin_->digital_write(true);
|
||||
delay(5);
|
||||
}
|
||||
|
||||
this->write_reg8_(0xe4, 0x04);
|
||||
|
||||
uint8_t zeros[4] = {0, 0, 0, 0};
|
||||
this->write_reg_(0xbc, zeros, 4);
|
||||
}
|
||||
|
||||
void GSL3670Touchscreen::load_firmware_() {
|
||||
if (firmware_ == nullptr || firmware_len_ == 0) {
|
||||
ESP_LOGW(TAG, "No firmware supplied – skipping");
|
||||
return;
|
||||
}
|
||||
|
||||
ESP_LOGD(TAG, "Loading firmware (%zu blocks)...", firmware_len_);
|
||||
|
||||
static constexpr size_t FW_BLK_SIZE = 128 + 4;
|
||||
|
||||
for (size_t i = 0; i != this->firmware_len_; i++) {
|
||||
auto offset = i * FW_BLK_SIZE;
|
||||
uint8_t val = this->firmware_[offset + 0];
|
||||
ESP_LOGV(TAG, "Firmware address 0x%02X", val);
|
||||
this->write_reg_(0xf0, &val, 1);
|
||||
this->write_reg_(0, this->firmware_ + offset + 4, 128);
|
||||
}
|
||||
ESP_LOGD(TAG, "Firmware load complete");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// startup_chip_() – mirrors esp_lcd_touch_gsl3670_startup_chip()
|
||||
// write 0x00 to 0xe0
|
||||
// ---------------------------------------------------------------------------
|
||||
void GSL3670Touchscreen::startup_chip_() {
|
||||
ESP_LOGD(TAG, "startup_chip");
|
||||
this->write_reg8_(0xe0, 0x00);
|
||||
delay(5);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// I2C helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
bool GSL3670Touchscreen::write_reg_(uint8_t reg, const uint8_t *data, size_t len) {
|
||||
auto err = this->write_register(reg, data, len);
|
||||
if (err != i2c::ERROR_OK) {
|
||||
ESP_LOGW(TAG, "I2C write reg 0x%02X len %zu failed (%d)", reg, len, err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool GSL3670Touchscreen::write_reg8_(uint8_t reg, uint8_t val) { return write_reg_(reg, &val, 1); }
|
||||
|
||||
} // namespace esphome::gsl3670
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/components/i2c/i2c.h"
|
||||
#include "esphome/components/touchscreen/touchscreen.h"
|
||||
#include "esphome/core/component.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::gsl3670 {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// GSL3670 touchscreen ESPHome component
|
||||
// ---------------------------------------------------------------------------
|
||||
class GSL3670Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
|
||||
public:
|
||||
/// Supply the firmware table (generated by codegen from the YAML)
|
||||
void set_firmware(const uint8_t *fw, size_t len) {
|
||||
this->firmware_ = fw;
|
||||
this->firmware_len_ = len;
|
||||
}
|
||||
|
||||
void set_interrupt_pin(InternalGPIOPin *pin) { interrupt_pin_ = pin; }
|
||||
void set_reset_pin(GPIOPin *pin) { reset_pin_ = pin; }
|
||||
|
||||
// touchscreen::Touchscreen / Component interface
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
|
||||
protected:
|
||||
void update_touches() override;
|
||||
|
||||
private:
|
||||
// ---------- init steps (mirrors esp_lcd_touch_gsl3670_init) ----------
|
||||
void clear_reg_(); // GPIO reset + 0x88/0xe4/0xe0 sequence
|
||||
void reset_(); // GPIO reset + 0xe4/0xbc sequence
|
||||
void load_firmware_(); // write GSLX670_FW table
|
||||
void startup_chip_(); // 0x00→0xe0 + gsl_DataInit
|
||||
|
||||
// ---------- I2C helpers ----------
|
||||
bool write_reg_(uint8_t reg, const uint8_t *data, size_t len);
|
||||
bool write_reg8_(uint8_t reg, uint8_t val);
|
||||
|
||||
InternalGPIOPin *interrupt_pin_{nullptr};
|
||||
GPIOPin *reset_pin_{nullptr};
|
||||
|
||||
const uint8_t *firmware_{nullptr};
|
||||
size_t firmware_len_{0};
|
||||
};
|
||||
|
||||
} // namespace esphome::gsl3670
|
||||
@@ -0,0 +1,209 @@
|
||||
"""ESPHome codegen for the gsl3670 touchscreen sub-platform."""
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from esphome import external_files, pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components import i2c, touchscreen
|
||||
from esphome.components.const import CONF_SHA256
|
||||
from esphome.components.touchscreen import (
|
||||
CONF_X_MAX,
|
||||
CONF_X_MIN,
|
||||
CONF_Y_MAX,
|
||||
CONF_Y_MIN,
|
||||
option_with_default,
|
||||
touchscreen_schema,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_FILE,
|
||||
CONF_ID,
|
||||
CONF_INTERRUPT_PIN,
|
||||
CONF_MIRROR_X,
|
||||
CONF_MIRROR_Y,
|
||||
CONF_MODEL,
|
||||
CONF_RESET_PIN,
|
||||
CONF_SWAP_XY,
|
||||
CONF_URL,
|
||||
)
|
||||
from esphome.core import ID
|
||||
|
||||
DEPENDENCIES = ["i2c"]
|
||||
AUTO_LOAD = ["touchscreen"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "gsl3670"
|
||||
|
||||
gsl3670_ns = cg.esphome_ns.namespace("gsl3670")
|
||||
GSL3670Touchscreen = gsl3670_ns.class_(
|
||||
"GSL3670Touchscreen",
|
||||
touchscreen.Touchscreen,
|
||||
i2c.I2CDevice,
|
||||
)
|
||||
|
||||
CONF_FIRMWARE = "firmware"
|
||||
|
||||
# Firmware blobs are published as release assets of the companion repository
|
||||
# rather than vendored into the ESPHome source tree. The default URL/SHA-256
|
||||
# for each model point at a pinned release artifact; users may override them
|
||||
# (or supply a local file via `firmware: { file: ... }`).
|
||||
FIRMWARE_RELEASE = "v1.0.0"
|
||||
FIRMWARE_BASE_URL = f"https://github.com/esphome-libs/gsl3670-firmware/releases/download/{FIRMWARE_RELEASE}"
|
||||
|
||||
MODELS = {
|
||||
"SEEED-RETERMINAL-D1001": {
|
||||
CONF_SWAP_XY: True,
|
||||
CONF_MIRROR_X: True,
|
||||
CONF_MIRROR_Y: True,
|
||||
CONF_X_MIN: 20,
|
||||
CONF_Y_MIN: 20,
|
||||
CONF_X_MAX: 872,
|
||||
CONF_Y_MAX: 1644,
|
||||
CONF_RESET_PIN: {"xl9535": None, "number": 14},
|
||||
CONF_INTERRUPT_PIN: 16,
|
||||
CONF_FIRMWARE: {
|
||||
CONF_URL: f"{FIRMWARE_BASE_URL}/seeed-d1001-fw.bin",
|
||||
CONF_SHA256: "2e50501ad83656fb6fa3d92591f9f31add4d442c8e8a79f29f5c4d335bd127a4",
|
||||
},
|
||||
},
|
||||
"CUSTOM": {},
|
||||
}
|
||||
|
||||
_FW_BLK_SIZE = 128 + 4
|
||||
|
||||
|
||||
def _validate_firmware_data(data: bytes, source: str) -> None:
|
||||
"""Validate the structure of a decoded GSL3670 firmware blob."""
|
||||
blk_cnt = len(data) // _FW_BLK_SIZE
|
||||
if blk_cnt == 0 or blk_cnt * _FW_BLK_SIZE != len(data):
|
||||
raise cv.Invalid(f"Firmware file length is incorrect: {source}")
|
||||
for i in range(0, len(data), _FW_BLK_SIZE):
|
||||
if data[i] > 0xEF or data[i + 1] != 1 or data[i + 2] != 2 or data[i + 3] != 3:
|
||||
raise cv.Invalid(
|
||||
f"Corrupted firmware at block {i // _FW_BLK_SIZE} in: {source}"
|
||||
)
|
||||
|
||||
|
||||
def _cache_path(url: str) -> Path:
|
||||
"""Cache path for a downloaded firmware blob, keyed by URL."""
|
||||
key = hashlib.sha256(url.encode()).hexdigest()[:8]
|
||||
return external_files.compute_local_file_dir(DOMAIN) / key
|
||||
|
||||
|
||||
def firmware_path(firmware: dict) -> Path:
|
||||
"""Return the path the firmware bytes will be read from at codegen time."""
|
||||
if path := firmware.get(CONF_FILE):
|
||||
return path
|
||||
return _cache_path(firmware[CONF_URL])
|
||||
|
||||
|
||||
def _validate_firmware(firmware: dict) -> dict:
|
||||
"""Require a single source, download (with caching), verify and validate."""
|
||||
if (CONF_FILE in firmware) == (CONF_URL in firmware):
|
||||
raise cv.Invalid(
|
||||
f"Exactly one of '{CONF_URL}' or '{CONF_FILE}' must be provided"
|
||||
)
|
||||
|
||||
if path := firmware.get(CONF_FILE):
|
||||
_validate_firmware_data(path.read_bytes(), str(path.absolute()))
|
||||
return firmware
|
||||
|
||||
url = firmware[CONF_URL]
|
||||
data = external_files.download_content(url, _cache_path(url))
|
||||
|
||||
if expected := firmware.get(CONF_SHA256):
|
||||
actual = hashlib.sha256(data).hexdigest()
|
||||
if actual.lower() != expected.lower():
|
||||
raise cv.Invalid(
|
||||
f"Firmware SHA-256 mismatch for {url}: "
|
||||
f"expected {expected.lower()}, got {actual}",
|
||||
[CONF_SHA256],
|
||||
)
|
||||
else:
|
||||
LOGGER.warning(
|
||||
"No SHA256 provided for gsl3670 firmware - firmware integrity can not be checked"
|
||||
)
|
||||
_validate_firmware_data(data, url)
|
||||
return firmware
|
||||
|
||||
|
||||
FIRMWARE_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Optional(CONF_URL): cv.url,
|
||||
cv.Optional(CONF_SHA256): cv.string_strict,
|
||||
cv.Optional(CONF_FILE): cv.file_,
|
||||
}
|
||||
),
|
||||
_validate_firmware,
|
||||
)
|
||||
|
||||
|
||||
def _config_schema(config):
|
||||
model_option = {
|
||||
cv.Optional(CONF_MODEL, default="CUSTOM"): cv.one_of(*MODELS, upper=True)
|
||||
}
|
||||
config = cv.Schema(model_option, extra=True)(config)
|
||||
defaults = MODELS[config[CONF_MODEL]]
|
||||
schema = (
|
||||
touchscreen_schema(cv.UNDEFINED, False, defaults)
|
||||
.extend(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(GSL3670Touchscreen),
|
||||
option_with_default(
|
||||
CONF_INTERRUPT_PIN, defaults
|
||||
): pins.internal_gpio_input_pin_schema,
|
||||
option_with_default(
|
||||
CONF_RESET_PIN, defaults
|
||||
): pins.gpio_output_pin_schema,
|
||||
**model_option,
|
||||
option_with_default(
|
||||
CONF_FIRMWARE, defaults, required=True
|
||||
): FIRMWARE_SCHEMA,
|
||||
}
|
||||
)
|
||||
.extend(i2c.i2c_device_schema(0x40))
|
||||
.extend(cv.COMPONENT_SCHEMA)
|
||||
)
|
||||
return schema(config)
|
||||
|
||||
|
||||
CONFIG_SCHEMA = _config_schema
|
||||
|
||||
|
||||
def _read_firmware(config) -> bytes:
|
||||
path = firmware_path(config[CONF_FIRMWARE])
|
||||
data = path.read_bytes()
|
||||
LOGGER.info(
|
||||
"Read gsl3670 touchscreen firmware file %s: %d bytes, %d blocks",
|
||||
path.absolute(),
|
||||
len(data),
|
||||
len(data) // _FW_BLK_SIZE,
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Code generation
|
||||
# ---------------------------------------------------------------------------
|
||||
async def to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
await touchscreen.register_touchscreen(var, config)
|
||||
await i2c.register_i2c_device(var, config)
|
||||
|
||||
if CONF_INTERRUPT_PIN in config:
|
||||
pin = await cg.gpio_pin_expression(config[CONF_INTERRUPT_PIN])
|
||||
cg.add(var.set_interrupt_pin(pin))
|
||||
|
||||
if CONF_RESET_PIN in config:
|
||||
pin = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
|
||||
cg.add(var.set_reset_pin(pin))
|
||||
|
||||
# Firmware table
|
||||
data = _read_firmware(config)
|
||||
fw_array = cg.progmem_array(
|
||||
ID(config[CONF_ID].id + "_fw", type=cg.uint8), list(data)
|
||||
)
|
||||
cg.add(var.set_firmware(fw_array, len(data) // _FW_BLK_SIZE))
|
||||
@@ -132,7 +132,7 @@ void HaierClimateBase::save_settings() {
|
||||
}
|
||||
|
||||
bool HaierClimateBase::get_display_state() const {
|
||||
return (this->display_status_ == SwitchState::ON) || (this->display_status_ == SwitchState::PENDING_ON);
|
||||
return (this->display_status_ == SwitchState::SWITCH_ON) || (this->display_status_ == SwitchState::PENDING_ON);
|
||||
}
|
||||
|
||||
void HaierClimateBase::set_display_state(bool state) {
|
||||
@@ -144,7 +144,7 @@ void HaierClimateBase::set_display_state(bool state) {
|
||||
}
|
||||
|
||||
bool HaierClimateBase::get_health_mode() const {
|
||||
return (this->health_mode_ == SwitchState::ON) || (this->health_mode_ == SwitchState::PENDING_ON);
|
||||
return (this->health_mode_ == SwitchState::SWITCH_ON) || (this->health_mode_ == SwitchState::PENDING_ON);
|
||||
}
|
||||
|
||||
void HaierClimateBase::set_health_mode(bool state) {
|
||||
|
||||
@@ -147,8 +147,8 @@ class HaierClimateBase : public esphome::Component,
|
||||
esphome::optional<haier_protocol::HaierMessage> message;
|
||||
};
|
||||
enum class SwitchState {
|
||||
OFF = 0b00,
|
||||
ON = 0b01,
|
||||
SWITCH_OFF = 0b00,
|
||||
SWITCH_ON = 0b01,
|
||||
PENDING_OFF = 0b10,
|
||||
PENDING_ON = 0b11,
|
||||
};
|
||||
@@ -157,8 +157,8 @@ class HaierClimateBase : public esphome::Component,
|
||||
esphome::optional<PendingAction> action_request_;
|
||||
uint8_t fan_mode_speed_;
|
||||
uint8_t other_modes_fan_speed_;
|
||||
SwitchState display_status_{SwitchState::ON};
|
||||
SwitchState health_mode_{SwitchState::OFF};
|
||||
SwitchState display_status_{SwitchState::SWITCH_ON};
|
||||
SwitchState health_mode_{SwitchState::SWITCH_OFF};
|
||||
bool force_send_control_;
|
||||
bool forced_request_status_;
|
||||
bool reset_protocol_request_;
|
||||
|
||||
@@ -50,7 +50,7 @@ void HonClimate::set_quiet_mode_state(bool state) {
|
||||
this->quiet_mode_state_ = state ? SwitchState::PENDING_ON : SwitchState::PENDING_OFF;
|
||||
this->force_send_control_ = true;
|
||||
} else {
|
||||
this->quiet_mode_state_ = state ? SwitchState::ON : SwitchState::OFF;
|
||||
this->quiet_mode_state_ = state ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
|
||||
}
|
||||
this->settings_.quiet_mode_state = state;
|
||||
#ifdef USE_SWITCH
|
||||
@@ -63,7 +63,7 @@ void HonClimate::set_quiet_mode_state(bool state) {
|
||||
}
|
||||
|
||||
bool HonClimate::get_quiet_mode_state() const {
|
||||
return (this->quiet_mode_state_ == SwitchState::ON) || (this->quiet_mode_state_ == SwitchState::PENDING_ON);
|
||||
return (this->quiet_mode_state_ == SwitchState::SWITCH_ON) || (this->quiet_mode_state_ == SwitchState::PENDING_ON);
|
||||
}
|
||||
|
||||
esphome::optional<hon_protocol::VerticalSwingMode> HonClimate::get_vertical_airflow() const {
|
||||
@@ -513,7 +513,7 @@ void HonClimate::initialization() {
|
||||
}
|
||||
this->current_vertical_swing_ = this->settings_.last_vertiacal_swing;
|
||||
this->current_horizontal_swing_ = this->settings_.last_horizontal_swing;
|
||||
this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::ON : SwitchState::OFF;
|
||||
this->quiet_mode_state_ = this->settings_.quiet_mode_state ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
|
||||
}
|
||||
|
||||
haier_protocol::HaierMessage HonClimate::get_control_message() {
|
||||
@@ -825,7 +825,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
#ifdef USE_SENSOR
|
||||
this->update_sub_sensor_(SubSensorType::INDOOR_COIL_TEMPERATURE, bd_packet->indoor_coil_temperature / 2.0 - 20);
|
||||
this->update_sub_sensor_(SubSensorType::OUTDOOR_COIL_TEMPERATURE, bd_packet->outdoor_coil_temperature - 64);
|
||||
this->update_sub_sensor_(SubSensorType::OUTDOOR_DEFROST_TEMPERATURE, bd_packet->outdoor_coil_temperature - 64);
|
||||
this->update_sub_sensor_(SubSensorType::OUTDOOR_DEFROST_TEMPERATURE, bd_packet->outdoor_defrost_temperature - 64);
|
||||
this->update_sub_sensor_(SubSensorType::OUTDOOR_IN_AIR_TEMPERATURE, bd_packet->outdoor_in_air_temperature - 64);
|
||||
this->update_sub_sensor_(SubSensorType::OUTDOOR_OUT_AIR_TEMPERATURE, bd_packet->outdoor_out_air_temperature - 64);
|
||||
this->update_sub_sensor_(SubSensorType::POWER, encode_uint16(bd_packet->power[0], bd_packet->power[1]));
|
||||
@@ -939,14 +939,14 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
// AC just turned on from remote need to turn off display
|
||||
this->force_send_control_ = true;
|
||||
} else if ((((uint8_t) this->display_status_) & 0b10) == 0) {
|
||||
this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF;
|
||||
this->display_status_ = disp_status ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Health mode
|
||||
if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
|
||||
bool old_health_mode = this->get_health_mode();
|
||||
this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF;
|
||||
this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
|
||||
should_publish = should_publish || (old_health_mode != this->get_health_mode());
|
||||
}
|
||||
{
|
||||
@@ -1008,7 +1008,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
|
||||
// In proper mode and not in pending state
|
||||
bool new_quiet_mode = packet.control.quiet_mode != 0;
|
||||
if (new_quiet_mode != this->get_quiet_mode_state()) {
|
||||
this->quiet_mode_state_ = new_quiet_mode ? SwitchState::ON : SwitchState::OFF;
|
||||
this->quiet_mode_state_ = new_quiet_mode ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
|
||||
this->settings_.quiet_mode_state = new_quiet_mode;
|
||||
#ifdef USE_SWITCH
|
||||
if (this->quiet_mode_switch_ != nullptr) {
|
||||
|
||||
@@ -197,7 +197,7 @@ class HonClimate final : public HaierClimateBase {
|
||||
esphome::optional<hon_protocol::HorizontalSwingMode> current_horizontal_swing_{};
|
||||
HonSettings settings_{};
|
||||
ESPPreferenceObject hon_rtc_;
|
||||
SwitchState quiet_mode_state_{SwitchState::OFF};
|
||||
SwitchState quiet_mode_state_{SwitchState::SWITCH_OFF};
|
||||
};
|
||||
|
||||
} // namespace esphome::haier
|
||||
|
||||
@@ -464,14 +464,14 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
|
||||
// AC just turned on from remote need to turn off display
|
||||
this->force_send_control_ = true;
|
||||
} else if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
|
||||
this->display_status_ = disp_status ? SwitchState::ON : SwitchState::OFF;
|
||||
this->display_status_ = disp_status ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Health mode
|
||||
if ((((uint8_t) this->health_mode_) & 0b10) == 0) {
|
||||
bool old_health_mode = this->get_health_mode();
|
||||
this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::ON : SwitchState::OFF;
|
||||
this->health_mode_ = packet.control.health_mode == 1 ? SwitchState::SWITCH_ON : SwitchState::SWITCH_OFF;
|
||||
should_publish = should_publish || (old_health_mode != this->get_health_mode());
|
||||
}
|
||||
{
|
||||
|
||||
@@ -242,7 +242,7 @@ void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (data[1] != HlkFm22xResult::SUCCESS) {
|
||||
if (data[1] != HlkFm22xResult::SUCCEEDED) {
|
||||
ESP_LOGE(TAG, "Command <0x%.2X> failed. Error: 0x%.2X", data[0], data[1]);
|
||||
switch (expected) {
|
||||
case HlkFm22xCommand::ENROLL:
|
||||
|
||||
@@ -41,7 +41,7 @@ enum HlkFm22xNoteType {
|
||||
};
|
||||
|
||||
enum HlkFm22xResult {
|
||||
SUCCESS = 0x00,
|
||||
SUCCEEDED = 0x00,
|
||||
REJECTED = 0x01,
|
||||
ABORTED = 0x02,
|
||||
FAILED4_CAMERA = 0x04,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user