Merge remote-tracking branch 'origin/dev' into neutral-ble-client

This commit is contained in:
J. Nick Koston committed 2026-11-04 18:36:55 -06:00
commit f0532afc38
524 files changed
+14430 -3055

No files matched your search

+2
View File
@@ -116,6 +116,8 @@ void add_on_state_callback(std::function<void(bool)> &&callback) {
|------|----------------------|-------------|
| `CallbackManager<void(Ts...)>` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered |
| `LazyCallbackManager<void(Ts...)>` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) |
| `AutomationList<N, Ts...>` + `build_automation_lists` | 4 bytes per automation + count, none when unused | A hub fires its own `on_*` automations with no filter (4 B per automation vs 8 B per `Callback`) |
| `automation.new_automation(args, conf)` + the parent's own fixed table | the table entry (pointer plus its own data) | Like `AutomationList`, but each automation needs a stored value of its own, e.g. a threshold the parent compares against |
Both live in `esphome/core/helpers.h` and accept `std::function`, lambdas and forwarder structs through
their templatized `add()` method. `LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first
+27
View File
@@ -41,6 +41,12 @@ performance nit, because it fragments a small shared heap. Flag:
literal already lives in flash).
- `std::string` / `std::to_string` / string-returning helpers on hot paths where
a buffer or view API exists.
- A `set_timeout` / `set_interval` / `defer` lambda whose captures exceed 8 bytes
on 32-bit or are not trivially copyable (`this` plus two 4-byte values, `this`
plus a member function pointer, a `std::string`, a `std::function`):
`std::function` then heap-allocates each time it is scheduled. Flag it on
retries, re-armed timeouts and hot-path `defer`; a `set_interval` set up once
in `setup()` is a single allocation. `[this]` plus one 4-byte value fits.
## C++ conventions
@@ -69,6 +75,27 @@ performance nit, because it fragments a small shared heap. Flag:
typed as `std::function`, so lightweight forwarders avoid a heap allocation.
- Two-space indent, `using` over `typedef`, wrap at 120 columns.
## Use the existing helper
New components often hand-write code a helper already covers. Ask for the
helper; `AGENTS.md` section 4 and the `component-codegen` skill have the detail.
| Seen in the PR | Ask for |
| --- | --- |
| `if (this->x_sensor_ != nullptr) this->x_sensor_->publish_state(v);` | `PUBLISH_STATE(this->x_sensor_, v);` (`esphome/core/entity_base.h`) |
| set warning / `return` / clear warning around a bus result | `if (!this->status_comm_result(ok)) return;` |
| hand-written `set_x_sensor()` plus `x_sensor_` member | `SUB_SENSOR(x)`, `SUB_BINARY_SENSOR`, `SUB_TEXT_SENSOR`, `SUB_SWITCH`, ... |
| `if CONF_X in config: s = await sensor.new_sensor(config[CONF_X])` plus setter | `sensors = sensor.sub_sensors(config)` then `await sensors(CONF_X, hub.set_x_sensor)` |
| hand-written `{"a": MyEnum.MY_ENUM_A, ...}` dict | `MyEnum.options("a", "b", prefix="MY_ENUM_")` |
| loops on `available()` / `read_byte()` | UART `read_available(on_byte)`, `discard_available()`, `LineReader<N>`, `FrameReader<N, HEADER...>` |
| hand-rolled CRC, checksum or bit loop | `crc8`, `crc16`, `xor_bytes`, `sum16_bytes`, `encode_uint16`, ... (`esphome/core/helpers.h`) |
| integer split into buffer bytes by hand (`buf[0] = v >> 8; buf[1] = v;`) | `put_be16`/`24`/`32`, `put_le16`, `put_le32` (`esphome/core/helpers.h`) |
| bare `mark_failed()`, or `ESP_LOGE` then `mark_failed()` | `this->mark_failed(LOG_STR("reason"))` or `LOG_STR(ESP_LOG_MSG_COMM_FAIL)` |
| button/switch/number/select C++ class that only calls the hub | `ApplyButton`, `ApplySwitch`, `ApplyNumber`, `ApplySelect` with `apply=` |
| action or condition class that only forwards to the parent | `register_apply_action` / `register_apply_condition` |
| enum-to-string `switch` returning literals | `PROGMEM_STRING_TABLE` with `get_log_str` / `get_progmem_str` |
| `if (x := config.get(CONF_X)) is not None: cg.add(var.set_x(x))` | `cg.add_optional(var.set_x, config.get(CONF_X))`, when the setter takes the value unchanged |
## Python conventions
- Type-annotate every new function signature (params and return), new dataclass
+31 -3
View File
@@ -188,9 +188,37 @@ Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `res
drive a state when the restore mode is disabled. A hub that applies its switches' restored states itself
reads `sw->get_initial_write_state()`, which already applies `inverted`.
**Entities that each watch a register field:** one class taking register, shift, mask and match from a
Python tuple table, in typed `StaticVector`s sized by `cg.slot_counter`; not a template per bit, not a
virtual listener base (a second vptr per entity).
**A hub that forwards data to its children:** when what each child does with the data is known at build time
(decode a register field, match a key or tag, publish a value), declare a `cg.HubFunction` once in the hub's
`__init__.py` and have each platform add one item per entity, built from codegen expressions. Code generation writes
one function per hub. Pass `define=` and put the hub's `HUB_PUBLISHER(<name>, args...)` (in a public section; it
ends on `public:`) and its call under that `#ifdef`; call the pointer only when it is not null. The children stay
plain entities, with no listener class or list. A child that lost a `Component` base keeps its dump lines through
a second hub function (`"dump"`, a `const char *` tag parameter, no define of its own) that the hub's
`dump_config()` calls inside the value function's `#ifdef`; every child adds to both.
**Entities that each watch a register field:** when every field (register, shift, mask, match) is known at
build time, use the rule above (see `bq25186`). A binary sensor tests its field value against an on-values
bitmap (`match`); a one-bit field reads the bit (simplified from bq25186's `_state_expression`):
```python
# hub __init__.py
STATUS_PUBLISHER = cg.HubFunction(
"status", # esphome__<hub>__status_fn, passed to HUB_PUBLISHER(status, ...)'s setter
cg.void,
[(BQ25186StatusRegisters.const_ref(), "status")],
define="USE_BQ25186_STATUS_PUBLISHER",
)
STATUS = STATUS_PUBLISHER.param("status")
# platform to_code, per configured entity
value = (STATUS[reg] >> shift) & mask
if mask != 0x01 or match != 0b10: # 0b10: a one-bit field that is on when set
value = (cg.MockObj(f"{match:#06b}") >> value) & 1
STATUS_PUBLISHER.add(hub, parent_ref(var).publish_state(value))
```
Store a field description per entity (a struct in a `StaticVector` sized by `cg.slot_counter`) only when the
field is chosen at run time, as in `sen6x`, where the status bit depends on the variant read in `setup()`.
Never a template per bit or a virtual listener base (a second vptr per entity).
## Other helpers
+5 -2
View File
@@ -58,8 +58,11 @@ APIs.
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
**Listener and child-entity registration lists are the most common case, and the most commonly
missed.** A `register_*()` method called once per child at code generation time has a count that is
known at compile time, so it should never be a `std::vector`. Use `cg.slot_counter()`: it returns a
missed.** When the hub only forwards data to its children and each child's handling is known at build
time, keep no list at all: use `cg.HubFunction` (see the component-codegen skill). When the hub must
store the children (it iterates them for other reasons, or picks fields at run time), a `register_*()`
method called once per child at code generation time has a count that is known at compile time, so it
should never be a `std::vector`. Use `cg.slot_counter()`: it returns a
function that each consumer calls once per slot it will occupy, and after every `to_code` has run it
emits the define with the final count. When nothing registers, no define is emitted and the storage plus
its registration method compile out entirely.
+1 -1
View File
@@ -1,4 +1,4 @@
ARG BUILD_BASE_VERSION=2026.06.1
ARG BUILD_BASE_VERSION=2026.11.0
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
+1 -1
View File
@@ -45,7 +45,7 @@ runs:
id: apt
# Pinned on purpose and not watched by Dependabot: the step above relies on how this
# release calls apt-fast and where it writes install.log.
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
uses: awalsh128/cache-apt-pkgs-action@v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
# 1.3 drops the empty caches saved before the step above existed
+2 -2
View File
@@ -42,7 +42,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
uses: docker/build-push-action@v7.4.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -67,7 +67,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
uses: docker/build-push-action@v7.4.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
+2 -2
View File
@@ -27,13 +27,13 @@ runs:
echo "key=$key" >> "$GITHUB_OUTPUT"
- name: Cache the native toolchain (write on dev)
if: github.ref == 'refs/heads/dev'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
- name: Restore the native toolchain (off dev)
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
@@ -38,13 +38,13 @@ runs:
# Extend the extension list if a component ships extensionless headers.
- name: Cache clang-tidy idedata (write on dev)
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && steps.key.outputs.skip != 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: ${{ steps.key.outputs.path }}
key: ${{ steps.key.outputs.key }}
- name: Cache clang-tidy idedata (restore-only off dev)
if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') && steps.key.outputs.skip != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ steps.key.outputs.path }}
key: ${{ steps.key.outputs.key }}
+3 -3
View File
@@ -49,13 +49,13 @@ runs:
# -slim: bump when prune-esp-idf changes what it removes; a key is never overwritten.
- name: Cache ESP-IDF install (write on dev)
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && inputs.restore-only != 'true'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim
- name: Cache ESP-IDF install (restore-only off dev)
if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') || inputs.restore-only == 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim
@@ -67,4 +67,4 @@ runs:
. venv/bin/activate
python -c 'from esphome.espidf.framework import check_esp_idf_install; check_esp_idf_install("${{ steps.version.outputs.version }}")'
- name: Prune ESP-IDF install
uses: ./.github/actions/prune-esp-idf
uses: $/.github/actions/prune-esp-idf
+2 -2
View File
@@ -35,14 +35,14 @@ runs:
# restore-only and never push multi-GB artifacts into their own scope.
- name: Cache nRF Connect SDK install (write on dev)
if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: ~/.esphome-sdk-nrf
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-esphome-sdk-nrf-${{ steps.version.outputs.version }}-${{ hashFiles('esphome/components/nrf52/requirements.txt') }}
- name: Cache nRF Connect SDK install (restore-only off dev)
if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ~/.esphome-sdk-nrf
# yamllint disable-line rule:line-length
+3 -3
View File
@@ -17,12 +17,12 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: venv
# yamllint disable-line rule:line-length
@@ -32,7 +32,7 @@ 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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+232
View File
@@ -0,0 +1,232 @@
# This file is machine-generated by `gh actions-lock`.
# Do not edit by hand; run `gh actions-lock` to update.
# Docs: https://gh.io/actions-lockfile
version: 'v0.0.3'
workflows:
'.github/workflows/auto-label-pr.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/github-script@v9.0.0'
'.github/workflows/ci-api-proto.yml':
- 'actions/checkout@v7.0.1'
- 'actions/github-script@v9.0.0'
- 'actions/setup-python@v7.1.0'
- 'actions/upload-artifact@v7.0.2'
- 'astral-sh/setup-uv@v10.3.3'
'.github/workflows/ci-docker.yml':
- 'actions/checkout@v7.0.1'
- 'actions/download-artifact@v8.0.2'
- 'actions/setup-python@v7.1.0'
- 'actions/upload-artifact@v7.0.2'
- 'docker/login-action@v4.7.0'
- 'docker/setup-buildx-action@v4.4.1'
'.github/workflows/ci-github-scripts.yml':
- 'actions/checkout@v7.0.1'
'.github/workflows/ci-memory-impact-comment.yml':
- 'actions/cache@v6.1.0'
- 'actions/checkout@v7.0.1'
- 'actions/setup-python@v7.1.0'
- 'astral-sh/setup-uv@v10.3.3'
'.github/workflows/ci.yml':
- 'actions/cache@v5.0.5'
- 'actions/cache@v6.1.0'
- 'actions/checkout@v7.0.1'
- 'actions/download-artifact@v8.0.2'
- 'actions/setup-python@v7.1.0'
- 'actions/upload-artifact@v7.0.2'
- 'astral-sh/setup-uv@v10.3.3'
- 'awalsh128/cache-apt-pkgs-action@v1.6.3'
- 'codecov/codecov-action@v7.1.1'
- 'codspeedhq/action@v5.6.1'
- 'j178/prek-action@v3.1.0'
- 'pre-commit-ci/lite-action@v1.2.0'
'.github/workflows/close-pr-from-fork-default-branch.yml':
- 'actions/github-script@v9.0.0'
'.github/workflows/codeowner-approved-label-update.yml':
- 'actions/checkout@v7.0.1'
- 'actions/github-script@v9.0.0'
'.github/workflows/codeowner-review-request.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/github-script@v9.0.0'
'.github/workflows/codeql.yml':
- 'actions/checkout@v7.0.1'
- 'github/codeql-action@v4.39.1'
'.github/workflows/external-component-bot.yml':
- 'actions/create-github-app-token@v3.3.0'
- 'actions/github-script@v9.0.0'
'.github/workflows/issue-codeowner-notify.yml':
- 'actions/github-script@v9.0.0'
'.github/workflows/lock.yml': []
'.github/workflows/pr-title-check.yml':
- 'actions/checkout@v7.0.1'
- 'actions/github-script@v9.0.0'
'.github/workflows/release-nightly.yml':
- 'actions/checkout@v7.0.1'
'.github/workflows/release.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/download-artifact@v8.0.2'
- 'actions/github-script@v9.0.0'
- 'actions/setup-python@v7.1.0'
- 'actions/upload-artifact@v7.0.2'
- 'docker/build-push-action@v7.4.0'
- 'docker/login-action@v4.7.0'
- 'docker/setup-buildx-action@v4.4.1'
- 'pypa/gh-action-pypi-publish@v1.14.3'
'.github/workflows/stale.yml': []
'.github/workflows/status-check-labels.yml':
- 'actions/github-script@v9.0.0'
'.github/workflows/sync-dependency-versions.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/setup-python@v7.1.0'
'.github/workflows/sync-device-classes.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/setup-python@v7.1.0'
- 'astral-sh/setup-uv@v10.3.3'
- 'peter-evans/create-pull-request@v8.1.1'
'.github/workflows/sync-integration-durations.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/setup-python@v7.1.0'
- 'peter-evans/create-pull-request@v8.1.1'
'.github/workflows/verify-lockfile.yml':
- 'actions/checkout@v7.0.1'
dependencies:
'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9':
ref: 'v6.1.0'
commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
owner_id: 44036562
repo_id: 215566462
'actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7':
ref: 'v5.0.4'
commit: 'sha1-668228422ae6a00e4ad889ee87cd7109ec5666a7'
owner_id: 44036562
repo_id: 215566462
'actions/cache@v5.0.5':
ref: 'v5.0.5'
commit: 'sha1-27d5ce7f107fe9357f9df03efb73ab90386fccae'
owner_id: 44036562
repo_id: 215566462
'actions/cache@v6.1.0':
ref: 'v6.1.0'
commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
owner_id: 44036562
repo_id: 215566462
'actions/checkout@v7.0.1':
ref: 'v7.0.1'
commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
owner_id: 44036562
repo_id: 197814629
'actions/create-github-app-token@v3.3.0':
ref: 'v3.3.0'
commit: 'sha1-64ab729a6294000f5aa4a4bc0b73d1f32bc45510'
owner_id: 44036562
repo_id: 642580244
'actions/download-artifact@v8.0.2':
ref: 'v8.0.2'
commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333'
owner_id: 44036562
repo_id: 192626254
'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd':
ref: 'v8.0.0'
commit: 'sha1-ed597411d8f924073f98dfc5c65a23a2325f34cd'
owner_id: 44036562
repo_id: 205262760
'actions/github-script@v9.0.0':
ref: 'v9.0.0'
commit: 'sha1-3a2844b7e9c422d3c10d287c895573f7108da1b3'
owner_id: 44036562
repo_id: 205262760
'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405':
ref: 'v6.2.0'
commit: 'sha1-a309ff8b426b58ec0e2a45f0f869d46889d02405'
owner_id: 44036562
repo_id: 192625525
'actions/setup-python@v7.1.0':
ref: 'v7.1.0'
commit: 'sha1-6d4fd22b20465ccc0f7354476588e8df7c9ca0e0'
owner_id: 44036562
repo_id: 192625525
'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f':
ref: 'v7.0.0'
commit: 'sha1-bbbca2ddaa5d8feaa63e36b76fdaad77386f024f'
owner_id: 44036562
repo_id: 192625955
'actions/upload-artifact@v7.0.2':
ref: 'v7.0.2'
commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9'
owner_id: 44036562
repo_id: 192625955
'astral-sh/setup-uv@v10.3.3':
ref: 'v10.3.3'
commit: 'sha1-68559e99cc807c1eb7e9238fd43a15bba460eec2'
owner_id: 115962839
repo_id: 846733598
'awalsh128/cache-apt-pkgs-action@v1.6.3':
ref: 'v1.6.3'
commit: 'sha1-553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9'
owner_id: 2087466
repo_id: 408212054
uses:
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f'
'codecov/codecov-action@v7.1.1':
ref: 'v7.1.1'
commit: 'sha1-303a32d7a59b442fa8d48b6a1cc6825c09c847a5'
owner_id: 8226205
repo_id: 200299178
uses:
- 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd'
'codspeedhq/action@v5.6.1':
ref: 'v5.6.1'
commit: 'sha1-61efe07e57e55bedeab750c434504de272a56e8c'
owner_id: 116658140
repo_id: 561699778
uses:
- 'actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7'
- 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd'
'docker/build-push-action@v7.4.0':
ref: 'v7.4.0'
commit: 'sha1-c3c9e263c25d99ce0380d002d59b67737d91b0dc'
owner_id: 5429470
repo_id: 241092383
'docker/login-action@v4.7.0':
ref: 'v4.7.0'
commit: 'sha1-81c6f93d4bc991e510b342a00b419b3bb4efc5c6'
owner_id: 5429470
repo_id: 287743349
'docker/setup-buildx-action@v4.4.1':
ref: 'v4.4.1'
commit: 'sha1-f87e5991a6d7451dcb8d9637bfbc97413f497069'
owner_id: 5429470
repo_id: 288485773
'github/codeql-action@v4.39.1':
ref: 'v4.39.1'
commit: 'sha1-9b3254085494b55a0f7c4c2b3d2d52cc712ce370'
owner_id: 9919
repo_id: 259445878
'j178/prek-action@v3.1.0':
ref: 'v3.1.0'
commit: 'sha1-44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d'
owner_id: 10510431
repo_id: 1052387486
'peter-evans/create-pull-request@v8.1.1':
ref: 'v8.1.1'
commit: 'sha1-5f6978faf089d4d20b00c7766989d076bb2fc7f1'
owner_id: 18365890
repo_id: 197176365
'pre-commit-ci/lite-action@v1.2.0':
ref: 'v1.2.0'
commit: 'sha1-062bca0919bc9d6e66755cc05074b70c77e111fc'
owner_id: 64617429
repo_id: 560608915
'pypa/gh-action-pypi-publish@v1.14.3':
ref: 'v1.14.3'
commit: 'sha1-7245906fb61fa33a13554f9d5fb856fe61758086'
owner_id: 647025
repo_id: 178055147
uses:
- 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405'
+3 -3
View File
@@ -24,11 +24,11 @@ jobs:
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -38,7 +38,7 @@ jobs:
permission-pull-requests: write # pulls.get, pulls.listFiles, list/create/update/dismissReview
- name: Auto Label PR
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
+6 -6
View File
@@ -21,15 +21,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.12"
- 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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
@@ -82,7 +82,7 @@ jobs:
fi
- if: failure()
name: Review PR
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
script: |
await github.rest.pulls.createReview({
@@ -97,7 +97,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: generated-proto-files
path: |
@@ -105,7 +105,7 @@ jobs:
esphome/components/api/api_pb2_service.*
- if: success()
name: Dismiss review
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
script: |
let reviews = await github.rest.pulls.listReviews({
+11 -11
View File
@@ -60,13 +60,13 @@ jobs:
tag: ${{ steps.tag.outputs.tag }}
push: ${{ steps.tag.outputs.push }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@v4.4.1
- name: Determine tag and whether to push
id: tag
@@ -97,7 +97,7 @@ jobs:
- name: Log in to the GitHub container registry
if: steps.tag.outputs.push == 'true'
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -128,7 +128,7 @@ jobs:
- name: Upload compile-test image artifact
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
# The tar is already compressed, so upload it as-is. archive: false
# skips the redundant zip and makes the file name the artifact name
@@ -152,16 +152,16 @@ jobs:
- "ha-addon"
- "docker"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@v4.4.1
- name: Log in to the GitHub container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -208,9 +208,9 @@ jobs:
- nrf52
- host
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Download image artifact
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
uses: actions/download-artifact@v8.0.2
with:
name: compile-test-image.tar.zst
- name: Load image
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Run tests
working-directory: .github/scripts/auto-label-pr
@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo
@@ -58,7 +58,7 @@ jobs:
- name: Restore Python
if: steps.pr.outputs.skip != 'true'
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: "3.12"
cache-key: ${{ hashFiles('.cache-key') }}
+104 -104
View File
@@ -28,18 +28,18 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: venv
# yamllint disable-line rule:line-length
@@ -49,7 +49,7 @@ 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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -79,11 +79,11 @@ jobs:
timeout-minutes: 10
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
sparse-checkout: .github
- name: Install apt packages (cached)
uses: ./.github/actions/apt-packages
uses: $/.github/actions/apt-packages
determine-jobs:
name: Determine which jobs to run
@@ -125,17 +125,17 @@ jobs:
release-pr: ${{ github.base_ref == 'beta' || github.base_ref == 'release' }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Fetch enough history to find the merge base
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -185,7 +185,7 @@ jobs:
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@v6.1.0
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -204,14 +204,14 @@ jobs:
timeout-minutes: 15
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native toolchain
uses: ./.github/actions/cache-arduino8266
uses: $/.github/actions/cache-arduino8266
- name: Install the native toolchain
run: |
. venv/bin/activate
@@ -226,9 +226,9 @@ jobs:
if: needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -255,9 +255,9 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -277,7 +277,7 @@ jobs:
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.release-pr == 'false' && needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Read prek version from requirements_test.txt
id: prek
# requirements_test.txt is the only place the version is pinned, so a
@@ -289,7 +289,7 @@ jobs:
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Run prek
uses: j178/prek-action@44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d # v3.1.0
uses: j178/prek-action@v3.1.0
with:
prek-version: ${{ steps.prek.outputs.version }}
# This job only runs on pull requests, so nothing ever populates
@@ -302,7 +302,7 @@ jobs:
# Pushes any fixes the hooks made back to the pull request. This step
# must keep its default name: the GitHub App that performs the push
# locates the workflow run by that name.
- uses: pre-commit-ci/lite-action@062bca0919bc9d6e66755cc05074b70c77e111fc # v1.2.0
- uses: pre-commit-ci/lite-action@v1.2.0
if: always()
with:
msg: apply automatic formatting fixes
@@ -336,10 +336,10 @@ jobs:
if: needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ matrix.python-version }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -356,10 +356,10 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
uses: codecov/codecov-action@v7.1.1
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@v6.1.0
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -383,9 +383,9 @@ jobs:
if: needs.determine-jobs.outputs.core-ci == 'false'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Report empty upload to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
uses: codecov/codecov-action@v7.1.1
with:
run_command: empty-upload
force: true
@@ -416,23 +416,23 @@ jobs:
INTEGRATION_CCACHE_SAVE: ${{ needs.determine-jobs.outputs.integration-run-all == 'true' && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Install apt packages (cached)
# ccache speeds up the host compiles. A cache hit never touches apt
# (mirror outages cannot hang the job); the timeout bounds the cold
# path. libsdl2-dev is needed by the headless display tests, which
# capture screenshots.
timeout-minutes: 10
uses: ./.github/actions/apt-packages
uses: $/.github/actions/apt-packages
- name: Set up Python 3.13
id: python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.13"
- name: Restore integration library cache
# A miss or a changed pin self-heals with a download, so no restore-keys
id: library-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_LIBRARY_CACHE_PATH }}
key: integration-libraries-v1-${{ runner.os }}-${{ hashFiles('esphome/components/json/__init__.py', 'esphome/components/noise/__init__.py', 'esphome/components/lvgl/__init__.py', 'esphome/components/improv_base/__init__.py') }}
@@ -441,35 +441,35 @@ jobs:
# per slice of INTEGRATION_TESTS_SPLIT_BUCKETS in script/determine-jobs.py.
- name: Restore integration ccache (slice 1/5)
if: needs.determine-jobs.outputs.integration-run-all != 'true' || strategy.job-index == 0
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-0-${{ github.run_id }}
restore-keys: integration-ccache-v1-0-
- name: Restore integration ccache (slice 2/5)
if: needs.determine-jobs.outputs.integration-run-all != 'true' || strategy.job-index == 1
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-1-${{ github.run_id }}
restore-keys: integration-ccache-v1-1-
- name: Restore integration ccache (slice 3/5)
if: needs.determine-jobs.outputs.integration-run-all != 'true' || strategy.job-index == 2
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-2-${{ github.run_id }}
restore-keys: integration-ccache-v1-2-
- name: Restore integration ccache (slice 4/5)
if: needs.determine-jobs.outputs.integration-run-all != 'true' || strategy.job-index == 3
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-3-${{ github.run_id }}
restore-keys: integration-ccache-v1-3-
- name: Restore integration ccache (slice 5/5)
if: needs.determine-jobs.outputs.integration-run-all != 'true' || strategy.job-index == 4
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-4-${{ github.run_id }}
@@ -486,14 +486,14 @@ jobs:
CCACHE_DIR="$dir" ccache -z
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
- 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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -560,7 +560,7 @@ jobs:
# Tests that compare rendered output write the image they actually got here, so a
# failure can be looked at without reproducing the whole build locally.
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/
@@ -571,7 +571,7 @@ jobs:
# script/update_integration_test_durations.py; only full matrix dev
# runs produce usable data.
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: junit-integration-${{ strategy.job-index }}
path: junit-integration.xml
@@ -587,14 +587,14 @@ jobs:
run: ccache -s
- name: Save integration ccache slice
if: success() && env.INTEGRATION_CCACHE_SAVE == 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-${{ strategy.job-index }}-${{ github.run_id }}
- name: Save integration library cache
# Bucket 0 only; the others would race the same immutable key.
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.library-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@v6.1.0
with:
path: ${{ env.INTEGRATION_LIBRARY_CACHE_PATH }}
key: ${{ steps.library-cache.outputs.cache-primary-key }}
@@ -643,9 +643,9 @@ jobs:
if: needs.determine-jobs.outputs.import-time == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -655,7 +655,7 @@ jobs:
script/check_import_time.py --check --har importtime.har
- name: Upload waterfall HAR
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: import-time-waterfall
path: importtime.har
@@ -689,10 +689,10 @@ jobs:
# have already been benchmarked on their original dev pull requests.
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -753,7 +753,7 @@ jobs:
apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@78102a468f96d8e0ccb92d0ec49425753fc42062 # v5.6.0
uses: CodSpeedHQ/action@v5.6.1
with:
run: |
. venv/bin/activate
@@ -770,10 +770,10 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -834,7 +834,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -843,11 +843,11 @@ jobs:
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
# by dsmr and dlms_meter.
timeout-minutes: 10
uses: ./.github/actions/apt-packages
uses: $/.github/actions/apt-packages
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -858,33 +858,33 @@ jobs:
# a new image ships a newer patch release and drops the old interpreter.
- name: Cache platformio
if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ steps.restore-python.outputs.python-version }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ steps.restore-python.outputs.python-version }}-${{ hashFiles('platformio.ini') }}
- name: Cache ESP-IDF install
if: matrix.cache_idf
uses: ./.github/actions/cache-esp-idf
uses: $/.github/actions/cache-esp-idf
with:
framework: arduino
- name: Cache clang-tidy idedata
if: matrix.cache_idf
uses: ./.github/actions/cache-clang-tidy-idedata
uses: $/.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-arduino-tidy
- name: Cache nRF Connect SDK install
if: matrix.cache_sdk_nrf
uses: ./.github/actions/cache-sdk-nrf
uses: $/.github/actions/cache-sdk-nrf
- name: Register problem matchers
run: |
@@ -933,7 +933,7 @@ jobs:
- name: Prune ESP-IDF install before cache save
if: matrix.cache_idf && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
uses: $/.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }}
@@ -953,22 +953,22 @@ jobs:
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
uses: $/.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
uses: $/.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-idf-tidy
@@ -1008,7 +1008,7 @@ jobs:
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
uses: $/.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
@@ -1047,22 +1047,22 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
uses: $/.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
uses: $/.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-idf-tidy
@@ -1102,7 +1102,7 @@ jobs:
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
uses: $/.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
@@ -1143,22 +1143,22 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
uses: $/.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
uses: $/.github/actions/cache-clang-tidy-idedata
with:
environment: ${{ matrix.tidy_environment }}
@@ -1197,7 +1197,7 @@ jobs:
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
uses: $/.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
@@ -1231,7 +1231,7 @@ jobs:
run: echo ${{ matrix.batch.components }}
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Install apt packages (cached)
# A cache hit (seeded on dev by seed-apt-cache) never touches apt,
@@ -1239,9 +1239,9 @@ jobs:
# the cold path. The action has no --no-install-recommends; same
# package set this job used before #17463.
timeout-minutes: 10
uses: ./.github/actions/apt-packages
uses: $/.github/actions/apt-packages
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -1250,7 +1250,7 @@ jobs:
# ESP-IDF install; never save -- just reuse the shared install the
# dev tidy jobs already cached when present.
if: matrix.batch.needs_idf
uses: ./.github/actions/cache-esp-idf
uses: $/.github/actions/cache-esp-idf
with:
restore-only: true
- name: Cache nRF Connect SDK install (restore-only)
@@ -1258,14 +1258,14 @@ jobs:
# sdk-nrf install; never save -- just reuse the shared install the
# dev nrf52 tidy job cached when present.
if: matrix.batch.needs_nrf
uses: ./.github/actions/cache-sdk-nrf
uses: $/.github/actions/cache-sdk-nrf
with:
restore-only: true
- name: Cache the native ESP8266 toolchain
# Only batches whose test platforms include esp8266; never saves
# here, it reuses the install the dev seed job cached.
if: matrix.batch.needs_arduino8266
uses: ./.github/actions/cache-arduino8266
uses: $/.github/actions/cache-arduino8266
- name: Validate and compile components with intelligent grouping
run: |
. venv/bin/activate
@@ -1408,10 +1408,10 @@ jobs:
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp32-platformio-components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -1446,16 +1446,16 @@ jobs:
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp8266-native-components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native toolchain
uses: ./.github/actions/cache-arduino8266
uses: $/.github/actions/cache-arduino8266
- name: Run native toolchain compile test
run: |
@@ -1495,10 +1495,10 @@ jobs:
CCACHE_SHA256: 2568347a697e103ca1b073981c704ad76fb2507d066c38dba038dd73399d968f
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -1622,17 +1622,17 @@ jobs:
if: needs.determine-jobs.outputs.device-builder == 'true'
steps:
- name: Check out esphome (this PR)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
path: esphome
- name: Check out esphome/device-builder
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
repository: esphome/device-builder
ref: main
path: device-builder
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.13"
- name: Set up uv
@@ -1641,7 +1641,7 @@ 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@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -1684,7 +1684,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.base_ref }}
@@ -1756,7 +1756,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -1773,14 +1773,14 @@ jobs:
- name: Restore Python
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native ESP8266 toolchain
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266
uses: ./.github/actions/cache-arduino8266
uses: $/.github/actions/cache-arduino8266
- name: Build, compile, and analyze memory
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
@@ -1819,7 +1819,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@v6.1.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -1840,7 +1840,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -1859,15 +1859,15 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native ESP8266 toolchain
if: fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266
uses: ./.github/actions/cache-arduino8266
uses: $/.github/actions/cache-arduino8266
- name: Build, compile, and analyze memory
id: extract
run: |
@@ -1903,7 +1903,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -1926,20 +1926,20 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
uses: actions/download-artifact@v8.0.2
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
uses: actions/download-artifact@v8.0.2
with:
name: memory-analysis-pr
path: ./memory-analysis
@@ -17,7 +17,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
&& github.event.pull_request.head.ref == github.event.repository.default_branch
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- uses: actions/github-script@v9.0.0
with:
script: |
const { owner, repo } = context.repo;
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
@@ -34,7 +34,7 @@ jobs:
CODEOWNERS
- name: Check codeowner approval and update label
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
with:
@@ -29,13 +29,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -44,7 +44,7 @@ jobs:
permission-issues: write # issues.listComments and issues.createComment (PR comments use the issues API)
- name: Request reviews from component codeowners
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
+3 -3
View File
@@ -52,11 +52,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/init@v4.39.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@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/analyze@v4.39.1
with:
category: "/language:${{matrix.language}}"
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -24,7 +24,7 @@ jobs:
permission-pull-requests: write
- name: Add external component comment
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify codeowners for component issues
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
script: |
const owner = context.repo.owner;
+1 -1
View File
@@ -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@31d160e5f0ffa0ee65d5b27cb55c153d415f2abe # 2026.10.2
uses: esphome/workflows/.github/workflows/lock.yml@e1432c7b31fec7d24a13dc192953ebac836ed98a # 2026.10.3
+2 -2
View File
@@ -16,9 +16,9 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- uses: actions/github-script@v9.0.0
with:
script: |
const {
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
actions: write # gh workflow run starts release.yml
steps:
- name: Check out the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Start the release workflow
env:
+19 -19
View File
@@ -31,7 +31,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Get tag
id: tag
env:
@@ -84,9 +84,9 @@ jobs:
contents: read # actions/checkout to build the sdist/wheel
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.x"
- name: Build
@@ -94,7 +94,7 @@ jobs:
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
uses: pypa/gh-action-pypi-publish@v1.14.3
with:
skip-existing: true
@@ -116,29 +116,29 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@v4.4.1
- name: Log in to docker hub
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build docker
uses: ./.github/actions/build-image
uses: $/.github/actions/build-image
with:
target: final
build_type: docker
@@ -146,7 +146,7 @@ jobs:
version: ${{ needs.init.outputs.tag }}
- name: Build ha-addon
uses: ./.github/actions/build-image
uses: $/.github/actions/build-image
with:
target: final
build_type: ha-addon
@@ -162,7 +162,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -192,27 +192,27 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Download digests
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
uses: actions/download-artifact@v8.0.2
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@v4.4.1
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -245,7 +245,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -254,7 +254,7 @@ jobs:
permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token)
- name: Trigger Workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@31d160e5f0ffa0ee65d5b27cb55c153d415f2abe # main
uses: esphome/workflows/.github/workflows/stale.yml@e1432c7b31fec7d24a13dc192953ebac836ed98a # main
secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with:
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check for blocking labels
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
script: |
const blockingLabels = ['needs-docs', 'needs-developer-docs', 'merge-after-release', 'chained-pr'];
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -54,7 +54,7 @@ jobs:
- name: Check out base branch
# Provides the script that runs below. Deliberately the base branch
# so the pull request cannot change what executes here.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
@@ -64,14 +64,14 @@ jobs:
# refuses heads that live in a different repository, and the job
# condition above already limits runs to same-repository branches.
# Leaving it off keeps that refusal as a backstop for fork heads.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.event.pull_request.head.ref }}
path: pull-request
token: ${{ steps.generate-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.12"
+6 -6
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -28,16 +28,16 @@ jobs:
permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Checkout Home Assistant
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
repository: home-assistant/core
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.14"
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
@@ -93,7 +93,7 @@ jobs:
run: python script/run-in-env.py prek run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
uses: peter-evans/create-pull-request@v8.1.1
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -28,10 +28,10 @@ jobs:
permission-pull-requests: write # open or refresh the sync PR
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Setup Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.13"
@@ -86,7 +86,7 @@ jobs:
exit 1
- name: Commit changes
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
uses: peter-evans/create-pull-request@v8.1.1
with:
commit-message: "[ci] Refresh integration test durations"
committer: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
+26
View File
@@ -0,0 +1,26 @@
name: verify-lockfile
on:
pull_request:
push:
branches:
- dev
permissions:
contents: read
jobs:
verify:
name: Verify actions lockfile
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Install gh-actions-lock
run: gh extension install github/gh-actions-lock --pin v0.4.0
- name: Verify lockfile
run: gh actions-lock --verify
+19 -3
View File
@@ -205,6 +205,18 @@ file does, and it is the authority when they disagree. The most useful starting
static constexpr uint32_t READ_TIMEOUT_ID = 0;
this->set_timeout(READ_TIMEOUT_ID, 50, [this]() { this->read_(); });
```
* **Scheduler lambdas capture at most 8 bytes:** `set_timeout`, `set_interval` and `defer` store the lambda
in a `std::function`, which keeps it inline only when it is at most 8 bytes on 32-bit targets and trivially
copyable; anything else is a heap allocation each time it is scheduled, which matters for retries, re-armed
timeouts and `defer` on a hot path. `[this]` plus one 4-byte value fits. `[this]` plus two 4-byte values,
`[this]` plus a member function pointer (8 bytes), a `std::string` or a `std::function` do not: keep that
state in members and capture `[this]`, or capture a small index instead.
```cpp
// Bad - 12 bytes on 32-bit, allocates on every retry
this->set_timeout(RETRY_ID, 10, [this, configure = config.configure]() { (this->*configure)(); });
// Good - 8 bytes, the pointer is looked up when the timeout runs
this->set_timeout(RETRY_ID, 10, [this, feature]() { this->run_configure_(feature); });
```
* **Don't override a default with the same value:** if a base class method already returns what you
want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a
component that wants `DATA` should simply leave it alone.
@@ -227,16 +239,20 @@ file does, and it is the authority when they disagree. The most useful starting
A `dump_config` helper that picks a name returns `const LogString *` from `LOG_STR(...)`.
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `xor_bytes`, `sum16_bytes`,
`reverse_bits`, `encode_uint16`, `encode_value_le`, `sign_extend<BITS>`, `bcd_to_uint8`, `assign_bits`); pick
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
the helper's arguments instead of writing a loop. `put_be16`/`24`/`32`, `put_le16` and `put_le32` write an
integer into a byte buffer and return the pointer past it. 7-byte BCD RTC calendars use
`time::decode_bcd_calendar<LAYOUT>` and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`,
`DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
`if (!this->status_comm_result(ok)) return;` sets or clears the warning.
`if (!this->status_comm_result(ok)) return;` sets or clears the warning; in `setup()`,
`if (!this->setup_check(ok)) return;` marks the component failed on error.
* **A hub's pointer to a child entity** comes from `SUB_SENSOR(name)`, `SUB_BINARY_SENSOR`, `SUB_TEXT_SENSOR`,
`SUB_SWITCH`, `SUB_NUMBER`, `SUB_SELECT` or `SUB_BUTTON`, which declare the `set_<name>_<type>()` setter and
the `<name>_<type>_` member (`SUB_SWITCH(turbo)` gives `set_turbo_switch()`); do not write the pair by hand.
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
Publish to an optional child with `PUBLISH_STATE(this->x_sensor_, value);`, not a hand-written null check.
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
end of `setup()` when the entities need a state at boot.
+2
View File
@@ -154,6 +154,7 @@ esphome/components/d01/* @ch604
esphome/components/dac7678/* @NickB1
esphome/components/daikin_arc/* @MagicBear
esphome/components/daikin_brc/* @hagak
esphome/components/daikin_madoka/* @Petapton
esphome/components/dallas_temp/* @ssieb
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
@@ -632,6 +633,7 @@ esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004
esphome/components/stcc4/* @j9brown
esphome/components/store_yaml/* @bdraco
esphome/components/stts22h/* @B48D81EFCC
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.11.0-dev
PROJECT_NUMBER = 2026.12.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -1,5 +1,5 @@
ARG BUILD_VERSION=dev
ARG BUILD_BASE_VERSION=2026.06.1
ARG BUILD_BASE_VERSION=2026.11.0
ARG BUILD_TYPE=docker
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base-source-docker
+135 -74
View File
@@ -3,6 +3,7 @@ from contextvars import ContextVar
from dataclasses import dataclass, field
import logging
import math
import re
import string
from typing import Any
@@ -44,6 +45,9 @@ CONF_CHOOSE = "choose"
CONF_CHOICE_ID = "choice_id"
_BYTE_VECTOR = cg.std_vector.template(cg.uint8)
def progmem_bytes(name: str, data: bytes | list[int]) -> MockObj:
"""Shared PROGMEM table for constant bytes; equal payloads share one, empty is nullptr."""
if not data:
@@ -59,10 +63,11 @@ async def templatable_bytes(
set_template: MockObj,
set_static: MockObj,
table_name: str,
return_type: MockObj = _BYTE_VECTOR,
) -> None:
"""Set a TemplatableBytes: a lambda via set_template, constant bytes via set_static."""
if cg.is_template(value):
fn = await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
fn = await cg.templatable(value, args, return_type)
cg.add(set_template(fn))
else:
cg.add(set_static(*_static_bytes(table_name, value)))
@@ -623,34 +628,34 @@ async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
return str(parent_ref(await cg.get_variable(config[id_key])))
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
# The generated function's parameters: a reference keeps its spelling, so a mutable
# one stays mutable; a value is taken by const reference, so a std::string is never copied.
return [
(
(
t
if str(cg.safe_exp(t)).rstrip().endswith("&")
else cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &")
),
arg,
)
for t, arg in args
]
def _is_reference(type_: SafeExpType) -> bool:
return str(cg.safe_exp(type_)).rstrip().endswith("&")
def _apply_param_type(type_: SafeExpType) -> SafeExpType:
# A reference keeps its spelling; a value is taken by const reference, so it is never copied.
if _is_reference(type_):
return type_
return cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(type_)}> &")
def _apply_params(args: TemplateArgsType) -> TemplateArgsType:
"""The generated function's parameters."""
return [(_apply_param_type(t), arg) for t, arg in args]
def _apply_function(
id_: ID,
return_type: SafeExpType,
template_arg: cg.TemplateArguments,
lambda_args: TemplateArgsType,
args: TemplateArgsType,
statements: list[str],
) -> MockObj:
"""Emit the generated function and declare ``id_`` as the ``ApplyAction`` or
``ApplyCondition`` templated on it, so ``play()`` calls it directly."""
def emit() -> MockObj:
fn = apply_static_function(id_, return_type, lambda_args, statements)
fn = apply_static_function(id_, return_type, _apply_params(args), statements)
return cg.new_Pvariable(id_, cg.TemplateArguments(fn, *template_arg))
return _COLLECTED_ACTION if _collect_action(id_, statements, emit) else emit()
@@ -667,9 +672,7 @@ def apply_statements_action(
Use it when the statements do not fit ``register_apply_action``. Generated names should
start with ``esphome__`` so a trigger argument cannot shadow them.
"""
return _apply_function(
action_id, cg.void, template_arg, _apply_lambda_args(args), statements
)
return _apply_function(action_id, cg.void, template_arg, args, statements)
def apply_static_function(
@@ -682,19 +685,78 @@ def apply_static_function(
return cg.static_function(f"esphome__{id_.id}__fn", return_type, params, statements)
async def _render_values(
# Comments, raw strings, strings and char literals; a ' after a digit is a digit separator.
_CPP_NOISE = re.compile(
r"//[^\n]*|/\*.*?\*/"
r'|\b(?:u8|u|U|L)?R"([^()\\\s]{0,16})\(.*?\)\1"'
r'|"(?:\\.|[^"\\\n])*"'
r"|(?<!\w)'(?:\\.[^'\\\n]{0,8}|[^'\\\n])'",
re.DOTALL,
)
def _cpp_code_only(text: str) -> str:
return _CPP_NOISE.sub(" ", text)
def _names_arg(code: str, name: str) -> bool:
"""Whether ``code`` (with noise removed) names ``name`` other than as a member."""
return re.search(rf"(?<!\.)(?<!->)(?<!::)\b{re.escape(name)}\b", code) is not None
def _has_top_level_comma(code: str) -> bool:
depth = 0
for ch in code:
if ch in "([{":
depth += 1
elif ch in ")]}":
depth -= 1
elif ch == "," and depth == 0:
return True
return False
async def _call_user_lambda(
value: Lambda, args: TemplateArgsType, return_type: SafeExpType
) -> Expression:
"""Call a config lambda in place; a value arg its body names is a copy it may modify.
The other args are the generated function's const references, so a body that names
none of them is reduced to its expression, as ``call_lambda`` does. When unsure the
name check counts a use: a needless copy is only a cost, a missed one fails to compile.
"""
code = _cpp_code_only(value.value)
by_value = {
name for t, name in args if not _is_reference(t) and _names_arg(code, name)
}
params = [
(t if name in by_value else _apply_param_type(t), name) for t, name in args
]
inner = await cg.process_lambda(value, params, return_type=return_type)
if by_value:
return immediate_lambda_call(inner)
expr = call_lambda(inner)
# An uncast `a, b` would split into two arguments or end an initializer early
if isinstance(expr, cg.RawExpression) and _has_top_level_comma(
_cpp_code_only(str(expr))
):
return cg.RawExpression(f"({expr})")
return expr
async def _render_call(
name: str,
target: str,
members: list[tuple[Any, Any, Any]],
values: list[Any],
config: ConfigType,
parent: str | None,
lambda_args: TemplateArgsType,
args: TemplateArgsType,
compare: bool = False,
) -> list[str]:
"""Render the argument text of one statement; every key must be present.
) -> str:
"""Render ``target`` with its config values filled in; every key must be present.
``compare``: an inlined lambda expression is parenthesized so it binds as a whole
``compare``: a reduced lambda expression is parenthesized so it binds as a whole
beside an operator.
"""
if any(value is None for value in values):
@@ -705,8 +767,7 @@ async def _render_values(
if isinstance(value, Lambda):
if isinstance(type_, str):
type_ = cg.RawExpression(type_.format(parent=parent))
inner = await cg.process_lambda(value, lambda_args, return_type=type_)
expr = call_lambda(inner)
expr = await _call_user_lambda(value, args, type_)
bare = compare and isinstance(expr, cg.RawExpression)
exprs.append(f"({expr})" if bare else str(expr))
elif isinstance(value, ID):
@@ -716,7 +777,7 @@ async def _render_values(
exprs.append(const_fn(config, value))
else:
exprs.append(str(cg.safe_exp(value)))
return exprs
return target.format(*exprs, parent=parent)
def _apply_values(config: ConfigType, members: list[tuple[Any, Any, Any]]) -> list[Any]:
@@ -780,7 +841,6 @@ def apply_action_builder(
args: TemplateArgsType,
) -> MockObj:
parent = await _apply_parent(config, id_key)
lambda_args = _apply_lambda_args(args)
receiver = "apply_call." if call else f"{parent}->"
statements: list[str] = []
for spec in statements_spec:
@@ -795,19 +855,17 @@ def apply_action_builder(
values = _apply_values(config, members)
if not _apply_call_active(members, values):
continue
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args
rendered = await _render_call(
name, target, members, values, config, parent, args
)
statements.append(f"{receiver}{target.format(*exprs, parent=parent)};")
statements.append(f"{receiver}{rendered};")
if call:
statements = [
f"auto apply_call = {parent}->{call}();",
*statements,
"apply_call.perform();",
]
return _apply_function(
action_id, cg.void, template_arg, lambda_args, statements
)
return _apply_function(action_id, cg.void, template_arg, args, statements)
return builder
@@ -820,11 +878,7 @@ async def _render_bytes(
args: TemplateArgsType,
table_name: str,
) -> list[str]:
"""The statements for one ``ApplyBytes``: what ``TemplatableBytes::visit`` does, inlined.
The payload lambda keeps the automation's own argument types, as ``TemplatableBytes``
gave it, and is always called, so a body that modifies an argument still compiles.
"""
"""The statements for one ``ApplyBytes``: what ``TemplatableBytes::visit`` does, inlined."""
value = _config_lookup(config, spec.conf_key)
if value is None:
return []
@@ -834,11 +888,7 @@ async def _render_bytes(
return f"{receiver}{target.format(data, parent=parent)};"
if isinstance(value, Lambda):
inner = await cg.process_lambda(
value, args, return_type=cg.std_vector.template(cg.uint8)
)
# Never reduced into the outer function, whose own parameters are const
call = CallExpression(inner, *[cg.MockObj(name) for _, name in args])
call = await _call_user_lambda(value, args, _BYTE_VECTOR)
return [
"{",
f" const std::vector<uint8_t> esphome__bytes = {call};",
@@ -873,22 +923,6 @@ def _apply_call_active(members: list[tuple[Any, Any, Any]], values: list[Any]) -
return not members or any(value is not None for value in values)
async def _render_check(
name: str,
target: str,
members: list[tuple[Any, Any, Any]],
values: list[Any],
config: ConfigType,
parent: str | None,
lambda_args: TemplateArgsType,
) -> str:
"""Render a boolean check with ``values`` compared against config."""
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args, compare=True
)
return target.format(*exprs, parent=parent)
def register_apply_condition(
name: str,
schema: cv.Schema,
@@ -921,16 +955,15 @@ def register_apply_condition(
if defaults:
config = {**defaults, **config}
parent = await _apply_parent(config, id_key)
lambda_args = _apply_lambda_args(args)
values = _apply_values(config, members)
check = await _render_check(
name, call.target, members, values, config, parent, lambda_args
check = await _render_call(
name, call.target, members, values, config, parent, args, compare=True
)
return _apply_function(
condition_id,
cg.bool_,
template_arg,
lambda_args,
args,
[f"return {parent}->{check};"],
)
@@ -1718,7 +1751,7 @@ def _emit_inline_run(
cg.void,
[
(cg.RawExpression("const int &"), _RUNNING_PARAM),
*_apply_lambda_args(args),
*_apply_params(args),
],
statements,
)
@@ -1873,10 +1906,13 @@ def validate_synchronous_automation(
)
async def _new_automation(
async def new_automation(
args: TemplateArgsType, config: ConfigType, *ctor_args: MockObj
) -> MockObj:
"""Create the Automation for ``config`` with its actions."""
"""Create the ``Automation<args>`` for an ``on_*`` config with its actions.
For a parent that stores and fires the automation itself; ``build_automation`` attaches it to a trigger.
"""
plan = await _build_automation_plan(args, config)
return _automation_from_plan(plan, args, config, *ctor_args)
@@ -1923,7 +1959,7 @@ def _direct_function(
return None
statements = [line for line in _run_statements(plan[0]) if line != _STOP_CHECK]
return apply_static_function(
plan[0][0].id_, cg.void, _apply_lambda_args(args), statements
plan[0][0].id_, cg.void, _apply_params(args), statements
)
@@ -1941,7 +1977,7 @@ def _when_call(
async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj:
return await _new_automation(args, config, trigger)
return await new_automation(args, config, trigger)
async def build_trigger_callback(
@@ -1979,27 +2015,27 @@ async def _trigger_callback(
) -> LambdaExpression:
if (target := _direct_function(plan, args, config)) is None:
target = f"{parent_ref(_automation_from_plan(plan, args, config))}->trigger"
lambda_args = _apply_lambda_args(params)
statements: list[str] = []
if call is not None:
members = call.members
values = _apply_values(config, members)
if _apply_call_active(members, values):
check = await _render_check(
check = await _render_call(
"trigger filter",
call.target,
members,
values,
config,
None if parent is None else str(parent_ref(parent)),
lambda_args,
params,
compare=True,
)
statements.append(f"if (!({check}))\n return;")
if forward is None:
forward = [name for _, name in params]
statements.append(f"{target}({', '.join(map(str, forward))});")
return LambdaExpression(
["\n".join(statements)], lambda_args, capture="", return_type=cg.void
["\n".join(statements)], _apply_params(params), capture="", return_type=cg.void
)
@@ -2102,6 +2138,31 @@ async def build_trigger_automations(
await build_automation(trigger, args, conf)
@dataclass(frozen=True, slots=True)
class AutomationList:
"""Automations a parent keeps in an ``AutomationList<count_define, args...>`` and triggers itself.
The parent adds each one with ``add_<conf_key>_automation()``. ``count_define`` is the most
automations any one parent gets; it is absent when none do.
"""
conf_key: str
count_define: str
args: TemplateArgsType
async def build_automation_lists(
parent: MockObj, config: ConfigType, entries: tuple[AutomationList, ...]
) -> None:
"""Build each entry's automations and add them to the parent's slot-counted list."""
for entry in entries:
request_slot = cg.slot_counter(entry.count_define)
for conf in config.get(entry.conf_key, []):
request_slot(str(parent))
obj = await new_automation(entry.args, conf)
cg.add(getattr(parent, f"add_{entry.conf_key}_automation")(obj))
@dataclass(frozen=True, slots=True)
class CallbackAutomation:
"""A single callback automation entry for build_callback_automations."""
+1
View File
@@ -54,6 +54,7 @@ from esphome.cpp_generator import ( # noqa: F401
with_local_variable,
)
from esphome.cpp_helpers import ( # noqa: F401
HubFunction,
add_optional,
add_pin,
add_variable,
+8 -18
View File
@@ -13,33 +13,25 @@ void AdE7953I2c::dump_config() {
}
bool AdE7953I2c::ade_write_8(uint16_t reg, uint8_t value) {
uint8_t data[3];
data[0] = reg >> 8;
data[1] = reg >> 0;
put_be16(data, reg);
data[2] = value;
return this->write(data, 3) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_write_16(uint16_t reg, uint16_t value) {
uint8_t data[4];
data[0] = reg >> 8;
data[1] = reg >> 0;
data[2] = value >> 8;
data[3] = value >> 0;
put_be16(data, reg);
put_be16(data + 2, value);
return this->write(data, 4) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_write_32(uint16_t reg, uint32_t value) {
uint8_t data[6];
data[0] = reg >> 8;
data[1] = reg >> 0;
data[2] = value >> 24;
data[3] = value >> 16;
data[4] = value >> 8;
data[5] = value >> 0;
put_be16(data, reg);
put_be32(data + 2, value);
return this->write(data, 6) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_read_8(uint16_t reg, uint8_t *value) {
uint8_t reg_data[2];
reg_data[0] = reg >> 8;
reg_data[1] = reg >> 0;
put_be16(reg_data, reg);
i2c::ErrorCode err = this->write(reg_data, 2);
if (err != i2c::ERROR_OK)
return true;
@@ -48,8 +40,7 @@ bool AdE7953I2c::ade_read_8(uint16_t reg, uint8_t *value) {
}
bool AdE7953I2c::ade_read_16(uint16_t reg, uint16_t *value) {
uint8_t reg_data[2];
reg_data[0] = reg >> 8;
reg_data[1] = reg >> 0;
put_be16(reg_data, reg);
i2c::ErrorCode err = this->write(reg_data, 2);
if (err != i2c::ERROR_OK)
return true;
@@ -62,8 +53,7 @@ bool AdE7953I2c::ade_read_16(uint16_t reg, uint16_t *value) {
}
bool AdE7953I2c::ade_read_32(uint16_t reg, uint32_t *value) {
uint8_t reg_data[2];
reg_data[0] = reg >> 8;
reg_data[1] = reg >> 0;
put_be16(reg_data, reg);
i2c::ErrorCode err = this->write(reg_data, 2);
if (err != i2c::ERROR_OK)
return true;
+2 -6
View File
@@ -10,10 +10,8 @@ static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
void ADS1115Component::setup() {
uint16_t value;
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)))
return;
}
uint16_t config = 0;
// Clear single-shot bit
@@ -57,10 +55,8 @@ void ADS1115Component::setup() {
// 0bxxxxxxxxxxxxxx11
config |= 0b0000000000000011;
if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte_16(ADS1115_REGISTER_CONFIG, config)))
return;
}
this->prev_config_ = config;
}
void ADS1115Component::dump_config() {
+4 -9
View File
@@ -51,28 +51,23 @@ void AHT10Component::setup() {
error_code = this->write(AHT10_INITIALIZE_CMD, sizeof(AHT10_INITIALIZE_CMD));
break;
}
if (error_code != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(error_code == i2c::ERROR_OK))
return;
}
uint8_t cal_attempts = 0;
uint8_t data = AHT10_STATUS_BUSY;
while (data & AHT10_STATUS_BUSY) {
delay(AHT10_DEFAULT_DELAY);
if (this->read(&data, 1) != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read(&data, 1) == i2c::ERROR_OK))
return;
}
++cal_attempts;
if (cal_attempts > AHT10_INIT_ATTEMPTS) {
this->mark_failed(LOG_STR("Initialization timed out"));
return;
}
}
if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
this->mark_failed(LOG_STR("Initialization failed"));
// Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
if (!this->setup_check((data & 0x68) == 0x08, LOG_STR("Initialization failed")))
return;
}
}
void AHT10Component::restart_read_() {
@@ -373,9 +373,7 @@ void AirtonSerialClimate::send_ab_() {
}
void AirtonSerialClimate::write_frame_(uint8_t *frame, size_t len) {
const uint16_t crc = crc16(frame, len - 2);
frame[len - 2] = static_cast<uint8_t>(crc >> 8);
frame[len - 1] = static_cast<uint8_t>(crc);
put_be16(frame + len - 2, crc16(frame, len - 2));
this->write_array(frame, len);
}
+1 -3
View File
@@ -72,10 +72,8 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
void AM2315C::setup() {
// get status
uint8_t status = 0;
if (this->read(&status, 1) != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read(&status, 1) == i2c::ERROR_OK))
return;
}
// reset registers if required, according to the datasheet
// this can be required after power on, although this was
+1 -3
View File
@@ -34,10 +34,8 @@ void AM2320Component::setup() {
uint8_t data[8];
data[0] = 0;
data[1] = 4;
if (!this->read_data_(data)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_data_(data)))
return;
}
}
void AM2320Component::dump_config() {
ESP_LOGD(TAG, "AM2320:");
+1 -2
View File
@@ -26,8 +26,7 @@ Am43Packet Am43Encoder::get_position_request() {
Am43Packet Am43Encoder::get_send_pin_request(uint16_t pin) {
uint8_t data[2];
data[0] = (pin & 0xFF00) >> 8;
data[1] = pin & 0xFF;
put_be16(data, pin);
return encode(CMD_SEND_PIN, data, 2);
}
+1 -3
View File
@@ -304,9 +304,7 @@ void APC1Component::send_command_(APC1Command cmd, uint16_t data) {
0,
0,
};
uint16_t checksum = sum16_bytes(buf.data(), 5);
buf[5] = static_cast<uint8_t>((checksum >> 8) & 0xFF);
buf[6] = static_cast<uint8_t>(checksum & 0xFF);
put_be16(buf.data() + 5, sum16_bytes(buf.data(), 5));
this->write_array(buf);
}
+1 -3
View File
@@ -60,10 +60,8 @@ static constexpr i2c::I2CRegValue APDS9960_GESTURE_OFFSETS[] PROGMEM = {
void APDS9960::setup() {
uint8_t id;
if (!this->read_byte(0x92, &id)) { // ID register
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_byte(0x92, &id))) // ID register
return;
}
if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
this->mark_failed(LOG_STR("APDS9960 has invalid id"));
+39 -1
View File
@@ -83,6 +83,7 @@ service APIConnection {
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
rpc sendspin_pairing_token(SendspinPairingTokenRequest) returns (SendspinPairingTokenResponse) {}
rpc get_yaml(GetYamlRequest) returns (void) {}
}
@@ -401,6 +402,11 @@ message ZWaveProxyCapabilities {
uint32 home_id = 2;
}
message StoreYamlCapabilities {
// Firmware embeds its YAML, so clients can request it with get_yaml
bool supported = 1;
}
message WizardCapabilities {
// True when the device has a wizard configured, so DeviceWizardRequest will be answered
bool configured = 1;
@@ -425,6 +431,7 @@ message DeviceCapabilitiesResponse {
[(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
WizardCapabilities wizard = 5 [(field_ifdef) = "USE_API_WIZARD"];
SendspinCapabilities sendspin = 6 [(field_ifdef) = "USE_SENDSPIN"];
StoreYamlCapabilities store_yaml = 7 [(field_ifdef) = "USE_STORE_YAML"];
}
// ==================== DEVICE WIZARD ====================
@@ -2192,6 +2199,8 @@ message BluetoothScannerSetModeRequest {
enum VoiceAssistantSubscribeFlag {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0;
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1;
// Not 2 or 4: aioesphomeapi sends API audio as 1 << 2
VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO = 8;
}
message SubscribeVoiceAssistantRequest {
@@ -2225,6 +2234,11 @@ message VoiceAssistantRequest {
uint32 flags = 3;
VoiceAssistantAudioSettings audio_settings = 4;
string wake_word_phrase = 5;
string wake_word_id = 6;
uint32 wake_word_audio_bytes = 7;
float wake_word_probability = 8;
float wake_word_probability_cutoff = 9;
uint32 wake_word_audio_bytes2 = 10;
}
message VoiceAssistantResponse {
@@ -2250,6 +2264,7 @@ enum VoiceAssistantEvent {
VOICE_ASSISTANT_WAKE_WORD_END = 10;
VOICE_ASSISTANT_STT_VAD_START = 11;
VOICE_ASSISTANT_STT_VAD_END = 12;
VOICE_ASSISTANT_WAKE_WORD_REJECTED = 13;
VOICE_ASSISTANT_TTS_STREAM_START = 98;
VOICE_ASSISTANT_TTS_STREAM_END = 99;
VOICE_ASSISTANT_INTENT_PROGRESS = 100;
@@ -2843,7 +2858,7 @@ message InfraredRFReceiveEvent {
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::span<const int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}
// Sent only to the client that issued an InfraredRFTransmitRawTimingsRequest, once the
@@ -3071,6 +3086,7 @@ message UsbDeviceDescriptor {
uint32 interface_number = 4; // bInterfaceNumber the port is bound through
uint32 interface_class = 5; // bInterfaceClass of that interface
uint32 port_number = 6;
string interface_description = 7; // iInterface string of that interface, empty when it has none
}
// The identity of the device behind a port. Identifies one physical device among others of
@@ -3143,3 +3159,25 @@ message SendspinPairingTokenResponse {
// The pairing token (Sendspin pairing.md "Pairing Token"). Set only when status is OK.
string token = 2 [(max_data_length) = 127];
}
// ==================== STORE YAML ====================
// Streams the compressed user YAML embedded in firmware; the client decompresses.
// A GetYamlRequest during an active transfer is ignored; reconnect to restart.
message GetYamlRequest {
option (id) = 161;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_STORE_YAML";
option (no_delay) = true;
}
message GetYamlResponse {
option (id) = 162;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_STORE_YAML";
// A zstd frame of the EHY1 file envelope, sent from flash
bytes data = 1 [(force) = true, (pointer_to_buffer) = true, (progmem) = true];
bool done = 2 [(force) = true];
// Set on the first chunk only
uint32 total_size = 3;
}
+46 -2
View File
@@ -366,6 +366,13 @@ void APIConnection::loop() {
// (missing a frame is fine, missing a state update is not)
this->try_send_camera_image_();
#endif
#ifdef USE_STORE_YAML
// Guard inline so the idle hot path pays a compare, not a function call.
if (this->store_yaml_pos_ != STORE_YAML_IDLE) {
this->try_send_store_yaml_();
}
#endif
}
void APIConnection::check_keepalive_(uint32_t now) {
@@ -1222,6 +1229,36 @@ void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
}
#endif
#ifdef USE_STORE_YAML
void APIConnection::on_get_yaml_request() {
// A re-request while a transfer is in flight is ignored (see api.proto)
if (this->store_yaml_pos_ != STORE_YAML_IDLE)
return;
this->store_yaml_pos_ = 0;
this->try_send_store_yaml_();
}
void APIConnection::try_send_store_yaml_() {
// Advance only after a successful send, so a full TX buffer retries the same chunk from loop()
while (this->helper_->can_write_without_blocking()) {
const size_t to_send = std::min(STORE_YAML_DATA_SIZE - this->store_yaml_pos_, MAX_BATCH_PACKET_SIZE);
GetYamlResponse resp;
resp.data = STORE_YAML_DATA + this->store_yaml_pos_;
resp.data_len = to_send;
if (this->store_yaml_pos_ == 0)
resp.total_size = STORE_YAML_DATA_SIZE;
resp.done = this->store_yaml_pos_ + to_send == STORE_YAML_DATA_SIZE;
if (!this->send_message(resp))
return;
if (resp.done) {
this->store_yaml_pos_ = STORE_YAML_IDLE;
return;
}
this->store_yaml_pos_ += to_send;
}
}
#endif
#ifdef USE_HOMEASSISTANT_TIME
void APIConnection::on_get_time_response(const GetTimeResponse &value) {
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1316,7 +1353,7 @@ bool APIConnection::check_voice_assistant_api_connection_() const {
void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe, msg.flags);
}
}
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
@@ -2123,6 +2160,9 @@ bool APIConnection::send_device_capabilities_response_() {
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
resp.sendspin.client_id = sendspin_::global_sendspin_hub->get_client_id();
#endif
#ifdef USE_STORE_YAML
resp.store_yaml.supported = true;
#endif
return this->send_message(resp);
}
@@ -2365,10 +2405,14 @@ bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) {
bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn,
const void *msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise)
// Skip dump for log messages (recursive logging risk), camera frames (high-frequency noise)
// and YAML chunks (may contain secrets)
if (message_type != SubscribeLogsResponse::MESSAGE_TYPE
#ifdef USE_CAMERA
&& message_type != CameraImageResponse::MESSAGE_TYPE
#endif
#ifdef USE_STORE_YAML
&& message_type != GetYamlResponse::MESSAGE_TYPE
#endif
) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
+15
View File
@@ -32,6 +32,11 @@ namespace esphome::api {
// Forward-declared to break the api_server.h cycle; full-type inlines are in api_connection_buffer.h.
class APIServer;
#ifdef USE_STORE_YAML
/// The zstd YAML envelope generated by store_yaml, STORE_YAML_DATA_SIZE bytes in flash.
extern const uint8_t STORE_YAML_DATA[] PROGMEM;
#endif
// One shared flash string for every refused-frame warning: send_message()
// fails as soon as the TCP buffer is full, and each caller only pays for its
// short name. The guard drops the helper and its arguments below WARN.
@@ -123,6 +128,9 @@ class APIConnection final : public APIServerConnectionBase {
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
void on_camera_image_request(const CameraImageRequest &msg);
#endif
#ifdef USE_STORE_YAML
void on_get_yaml_request();
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
void on_climate_command_request(const ClimateCommandRequest &msg);
@@ -421,6 +429,13 @@ class APIConnection final : public APIServerConnectionBase {
void try_send_camera_image_();
#endif
#ifdef USE_STORE_YAML
void try_send_store_yaml_();
static constexpr size_t STORE_YAML_IDLE = std::numeric_limits<size_t>::max();
// Streaming offset into STORE_YAML_DATA, or STORE_YAML_IDLE
size_t store_yaml_pos_{STORE_YAML_IDLE};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_();
#endif
+53
View File
@@ -299,6 +299,19 @@ uint32_t ZWaveProxyCapabilities::calc_size_msg(const void *self) {
return size;
}
#endif
#ifdef USE_STORE_YAML
uint8_t *StoreYamlCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const StoreYamlCapabilities *>(self);
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.supported);
return pos;
}
uint32_t StoreYamlCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const StoreYamlCapabilities *>(self);
uint32_t size = 0;
size += ProtoSize::calc_bool(1, msg.supported);
return size;
}
#endif
#ifdef USE_API_WIZARD
uint8_t *WizardCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const WizardCapabilities *>(self);
@@ -348,6 +361,9 @@ uint8_t *DeviceCapabilitiesResponse::encode_msg(const void *self, uint8_t *__res
#endif
#ifdef USE_SENDSPIN
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.sendspin);
#endif
#ifdef USE_STORE_YAML
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.store_yaml);
#endif
return pos;
}
@@ -373,6 +389,9 @@ uint32_t DeviceCapabilitiesResponse::calc_size_msg(const void *self) {
#endif
#ifdef USE_SENDSPIN
size += ProtoSize::calc_message(1, msg.sendspin.calculate_size());
#endif
#ifdef USE_STORE_YAML
size += ProtoSize::calc_message(1, msg.store_yaml.calculate_size());
#endif
return size;
}
@@ -3028,6 +3047,15 @@ uint8_t *VoiceAssistantRequest::encode_msg(const void *self, uint8_t *__restrict
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.flags);
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.audio_settings);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.wake_word_phrase);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.wake_word_id);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.wake_word_audio_bytes);
if (uint32_t raw = float_to_raw(msg.wake_word_probability); raw != 0) [[likely]] {
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 69, raw);
}
if (uint32_t raw = float_to_raw(msg.wake_word_probability_cutoff); raw != 0) [[likely]] {
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 77, raw);
}
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, msg.wake_word_audio_bytes2);
return pos;
}
uint32_t VoiceAssistantRequest::calc_size_msg(const void *self) {
@@ -3038,6 +3066,11 @@ uint32_t VoiceAssistantRequest::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.flags);
size += ProtoSize::calc_message(1, msg.audio_settings.calculate_size());
size += ProtoSize::calc_length(1, msg.wake_word_phrase.size());
size += ProtoSize::calc_length(1, msg.wake_word_id.size());
size += ProtoSize::calc_uint32(1, msg.wake_word_audio_bytes);
size += ProtoSize::calc_float(1, msg.wake_word_probability);
size += ProtoSize::calc_float(1, msg.wake_word_probability_cutoff);
size += ProtoSize::calc_uint32(1, msg.wake_word_audio_bytes2);
return size;
}
void VoiceAssistantResponse::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -4314,6 +4347,7 @@ uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.interface_class);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.port_number);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.interface_description);
return pos;
}
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
@@ -4325,6 +4359,7 @@ uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.interface_number);
size += ProtoSize::calc_uint32(1, msg.interface_class);
size += ProtoSize::calc_uint32(1, msg.port_number);
size += ProtoSize::calc_length(1, msg.interface_description.size());
return size;
}
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -4407,6 +4442,24 @@ uint32_t SendspinPairingTokenResponse::calc_size_msg(const void *self) {
return size;
}
#endif
#ifdef USE_STORE_YAML
uint8_t *GetYamlResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const GetYamlResponse *>(self);
pos = ProtoEncode::encode_progmem_bytes_force(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.data, msg.data_len);
pos = ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
pos = ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, msg.done ? 0x01 : 0x00);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.total_size);
return pos;
}
uint32_t GetYamlResponse::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const GetYamlResponse *>(self);
uint32_t size = 0;
size += ProtoSize::calc_length_force(1, msg.data_len);
size += ProtoSize::calc_bool_force(1);
size += ProtoSize::calc_uint32(1, msg.total_size);
return size;
}
#endif
#ifndef HAS_PROTO_MESSAGE_DUMP
static_assert(!std::is_polymorphic_v<HelloRequest>, "decodable messages carry no vtable");
static_assert(!std::is_polymorphic_v<DisconnectRequest>, "decodable messages carry no vtable");
+56 -3
View File
@@ -262,6 +262,7 @@ enum BluetoothScannerMode : uint32_t {
enum VoiceAssistantSubscribeFlag : uint32_t {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0,
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1,
VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO = 8,
};
enum VoiceAssistantRequestFlag : uint32_t {
VOICE_ASSISTANT_REQUEST_NONE = 0,
@@ -283,6 +284,7 @@ enum VoiceAssistantEvent : uint32_t {
VOICE_ASSISTANT_WAKE_WORD_END = 10,
VOICE_ASSISTANT_STT_VAD_START = 11,
VOICE_ASSISTANT_STT_VAD_END = 12,
VOICE_ASSISTANT_WAKE_WORD_REJECTED = 13,
VOICE_ASSISTANT_TTS_STREAM_START = 98,
VOICE_ASSISTANT_TTS_STREAM_END = 99,
VOICE_ASSISTANT_INTENT_PROGRESS = 100,
@@ -734,6 +736,23 @@ class ZWaveProxyCapabilities final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_STORE_YAML
class StoreYamlCapabilities final : public ProtoMessage {
public:
bool supported{false};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_API_WIZARD
class WizardCapabilities final : public ProtoMessage {
public:
@@ -772,7 +791,7 @@ class SendspinCapabilities final : public ProtoMessage {
class DeviceCapabilitiesResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 150;
static constexpr uint8_t ESTIMATED_SIZE = 136;
static constexpr uint8_t ESTIMATED_SIZE = 153;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("device_capabilities_response"); }
#endif
@@ -793,6 +812,9 @@ class DeviceCapabilitiesResponse final : public ProtoMessage {
#endif
#ifdef USE_SENDSPIN
SendspinCapabilities sendspin{};
#endif
#ifdef USE_STORE_YAML
StoreYamlCapabilities store_yaml{};
#endif
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -2953,7 +2975,7 @@ class VoiceAssistantAudioSettings final : public ProtoMessage {
class VoiceAssistantRequest final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 90;
static constexpr uint8_t ESTIMATED_SIZE = 41;
static constexpr uint8_t ESTIMATED_SIZE = 68;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("voice_assistant_request"); }
#endif
@@ -2962,6 +2984,11 @@ class VoiceAssistantRequest final : public ProtoMessage {
uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{};
StringRef wake_word_phrase{nullptr, 0}; // null until set, encode only
StringRef wake_word_id{nullptr, 0}; // null until set, encode only
uint32_t wake_word_audio_bytes{0};
float wake_word_probability{0.0f};
float wake_word_probability_cutoff{0.0f};
uint32_t wake_word_audio_bytes2{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -3841,7 +3868,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
uint32_t device_id{0};
#endif
uint32_t key{0};
const std::vector<int32_t> *timings{};
const std::span<const int32_t> *timings{};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -4102,6 +4129,7 @@ class UsbDeviceDescriptor final : public ProtoMessage {
uint32_t interface_number{0};
uint32_t interface_class{0};
uint32_t port_number{0};
StringRef interface_description{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -4209,5 +4237,30 @@ class SendspinPairingTokenResponse final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_STORE_YAML
class GetYamlResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 162;
static constexpr uint8_t ESTIMATED_SIZE = 25;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("get_yaml_response"); }
#endif
const uint8_t *data{nullptr};
uint16_t data_len{0};
bool done{false};
uint32_t total_size{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
} // namespace esphome::api
+31
View File
@@ -675,6 +675,8 @@ const char *proto_enum_to_string<enums::VoiceAssistantSubscribeFlag>(enums::Voic
return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_NONE");
case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO:
return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO");
case enums::VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO:
return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -720,6 +722,8 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V
return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_START");
case enums::VOICE_ASSISTANT_STT_VAD_END:
return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_END");
case enums::VOICE_ASSISTANT_WAKE_WORD_REJECTED:
return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_REJECTED");
case enums::VOICE_ASSISTANT_TTS_STREAM_START:
return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_START");
case enums::VOICE_ASSISTANT_TTS_STREAM_END:
@@ -1127,6 +1131,13 @@ const char *ZWaveProxyCapabilities::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_STORE_YAML
const char *StoreYamlCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("StoreYamlCapabilities"));
dump_field(out, ESPHOME_PSTR("supported"), this->supported);
return out.c_str();
}
#endif
#ifdef USE_API_WIZARD
const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("WizardCapabilities"));
@@ -1175,6 +1186,11 @@ const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const {
out.append(2, ' ').append_p(ESPHOME_PSTR("sendspin")).append(": ");
this->sendspin.dump_to(out);
out.append("\n");
#endif
#ifdef USE_STORE_YAML
out.append(2, ' ').append_p(ESPHOME_PSTR("store_yaml")).append(": ");
this->store_yaml.dump_to(out);
out.append("\n");
#endif
return out.c_str();
}
@@ -2391,6 +2407,11 @@ const char *VoiceAssistantRequest::dump_to(DumpBuffer &out) const {
this->audio_settings.dump_to(out);
out.append("\n");
dump_field(out, ESPHOME_PSTR("wake_word_phrase"), this->wake_word_phrase);
dump_field(out, ESPHOME_PSTR("wake_word_id"), this->wake_word_id);
dump_field(out, ESPHOME_PSTR("wake_word_audio_bytes"), this->wake_word_audio_bytes);
dump_field(out, ESPHOME_PSTR("wake_word_probability"), this->wake_word_probability);
dump_field(out, ESPHOME_PSTR("wake_word_probability_cutoff"), this->wake_word_probability_cutoff);
dump_field(out, ESPHOME_PSTR("wake_word_audio_bytes2"), this->wake_word_audio_bytes2);
return out.c_str();
}
const char *VoiceAssistantResponse::dump_to(DumpBuffer &out) const {
@@ -2975,6 +2996,7 @@ const char *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
dump_field(out, ESPHOME_PSTR("interface_class"), this->interface_class);
dump_field(out, ESPHOME_PSTR("port_number"), this->port_number);
dump_field(out, ESPHOME_PSTR("interface_description"), this->interface_description);
return out.c_str();
}
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
@@ -3016,6 +3038,15 @@ const char *SendspinPairingTokenResponse::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_STORE_YAML
const char *GetYamlResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("GetYamlResponse"));
dump_progmem_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
dump_field(out, ESPHOME_PSTR("done"), this->done);
dump_field(out, ESPHOME_PSTR("total_size"), this->total_size);
return out.c_str();
}
#endif
} // namespace esphome::api
@@ -760,6 +760,15 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
this->on_sendspin_pairing_token_request();
break;
}
#endif
#ifdef USE_STORE_YAML
case 161 /* GetYamlRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_get_yaml_request"));
#endif
this->on_get_yaml_request();
break;
}
#endif
default:
break;
+4
View File
@@ -256,6 +256,10 @@ class APIServerConnectionBase {
#ifdef USE_SENDSPIN
void on_sendspin_pairing_token_request() {};
#endif
#ifdef USE_STORE_YAML
void on_get_yaml_request() {};
#endif
};
} // namespace esphome::api
+2 -2
View File
@@ -520,13 +520,13 @@ void APIServer::send_serial_proxy_identity(const SerialProxyIdentity &msg) {
#ifdef USE_IR_RF
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
const std::vector<int32_t> *timings) {
std::span<const int32_t> timings) {
InfraredRFReceiveEvent resp{};
#ifdef USE_DEVICES
resp.device_id = device_id;
#endif
resp.key = key;
resp.timings = timings;
resp.timings = &timings;
for (auto &c : this->active_clients())
c->send_infrared_rf_receive_event(resp);
+2 -1
View File
@@ -30,6 +30,7 @@
#include <array>
#include <memory>
#include <span>
#include <vector>
namespace esphome::api {
@@ -208,7 +209,7 @@ class APIServer final : public Component
void send_serial_proxy_identity(const SerialProxyIdentity &msg);
#endif
#ifdef USE_IR_RF
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, std::span<const int32_t> timings);
#endif
bool is_connected() const { return this->api_connection_count_ != 0; }
+2 -6
View File
@@ -22,10 +22,8 @@ static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R
static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
void AS5600Component::setup() {
if (!this->read_byte(REGISTER_STATUS).has_value()) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_byte(REGISTER_STATUS).has_value()))
return;
}
// configuration direction pin, if given
// the dir pin on the chip should be low for clockwise
@@ -53,10 +51,8 @@ void AS5600Component::setup() {
// clang-format on
// write config to config register
if (!this->write_byte_16(REGISTER_CONF, config)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte_16(REGISTER_CONF, config)))
return;
}
// configure the start position
this->write_byte_16(REGISTER_ZPOS, this->start_position_);
+1 -3
View File
@@ -315,10 +315,8 @@ void AS734XComponent::setup() {
this->write_byte(AS7343_CHAIN_CMD, 0x46 + (i / len) * 0x10);
}
}
if (!ok) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(ok))
return;
}
this->disable_loop();
}
@@ -160,17 +160,14 @@ void spi_dma_tx_finish_callback(unsigned int param) {
void BekenSPILEDStripLightOutput::setup() {
RAMAllocator<uint8_t> allocator;
if (!this->internal_accessor_.allocate(allocator, this->num_leds_)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(this->internal_accessor_.allocate(allocator, this->num_leds_),
LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
const size_t dma_buffer_size = (this->get_buffer_size_() * 8) + (2 * 64);
this->dma_buf_ = allocator.allocate(dma_buffer_size);
if (this->dma_buf_ == nullptr) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(this->dma_buf_ != nullptr, LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
memset(this->dma_buf_, 0, dma_buffer_size);
@@ -180,22 +177,16 @@ void BekenSPILEDStripLightOutput::setup() {
value = PWD_SPI_CLK_BIT;
sddev_control(ICU_DEV_NAME, CMD_CLK_PWR_UP, &value);
if (spi_data != nullptr) {
this->mark_failed(LOG_STR("SPI already initialized"));
if (!this->setup_check(spi_data == nullptr, LOG_STR("SPI already initialized")))
return;
}
spi_data = (SpiData *) calloc(1, sizeof(SpiData)); // NOLINT(cppcoreguidelines-no-malloc)
if (spi_data == nullptr) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(spi_data != nullptr, LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
spi_data->dma_tx_semaphore = xSemaphoreCreateBinary();
if (spi_data->dma_tx_semaphore == nullptr) {
this->mark_failed(LOG_STR("TX semaphore init failed"));
if (!this->setup_check(spi_data->dma_tx_semaphore != nullptr, LOG_STR("TX semaphore init failed")))
return;
}
spi_data->first_run = true;
+2 -6
View File
@@ -71,10 +71,8 @@ void BH1745Component::setup() {
this->mark_failed(LOG_STR("Wrong part ID"));
return;
}
if (!this->write_byte(REG_SYSTEM_CONTROL, SYSTEM_CONTROL_SW_RESET)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(REG_SYSTEM_CONTROL, SYSTEM_CONTROL_SW_RESET)))
return;
}
// Light lost behind glass is scaled back up, as the other light sensors do
this->lux_scale_ = 160.0f * this->glass_attenuation_factor_ /
@@ -85,10 +83,8 @@ void BH1745Component::setup() {
// High threshold 0 and low threshold 0xFFFF (low byte first) make the interrupt condition always
// true, so the INT pin simply follows the enable bit, which drives the breakout's LEDs.
static constexpr uint8_t THRESHOLDS[4] = {0x00, 0x00, 0xFF, 0xFF};
if (!this->write_bytes(REG_TH_LSB, THRESHOLDS, sizeof(THRESHOLDS))) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_bytes(REG_TH_LSB, THRESHOLDS, sizeof(THRESHOLDS))))
return;
}
}
// RGBC_EN makes the chip measure continuously; update() reads the latest result.
if (!this->write_byte(REG_MODE_CONTROL1, static_cast<uint8_t>(this->measurement_time_)) ||
+1 -3
View File
@@ -64,10 +64,8 @@ MTreg:
void BH1750Sensor::setup() {
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write(&turn_on, 1) == i2c::ERROR_OK))
return;
}
this->state_ = IDLE;
}
+1 -3
View File
@@ -22,10 +22,8 @@ void BH1900NUXSensor::setup() {
// Initialize I2C device
i2c::ErrorCode result_code =
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
if (result_code != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(result_code == i2c::ERROR_OK))
return;
}
}
void BH1900NUXSensor::update() {
+5 -11
View File
@@ -1,4 +1,5 @@
#include "bl0942.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -89,26 +90,19 @@ void BL0942::write_reg_(uint8_t reg, uint32_t val) {
pkt[2] = (val & 0xff);
pkt[3] = (val >> 8) & 0xff;
pkt[4] = (val >> 16) & 0xff;
pkt[5] = (pkt[0] + pkt[1] + pkt[2] + pkt[3] + pkt[4]) ^ 0xff;
pkt[5] = sum_bytes(pkt, 5) ^ 0xFF;
this->write_array(pkt, 6);
delay(1);
}
int BL0942::read_reg_(uint8_t reg) {
union {
uint8_t b[4];
uint32_le_t le32;
} resp;
uint8_t resp[4];
this->write_byte(BL0942_READ_COMMAND | this->address_);
this->write_byte(reg);
this->flush();
if (this->read_array(resp.b, 4) &&
resp.b[3] ==
(uint8_t) ((BL0942_READ_COMMAND + this->address_ + reg + resp.b[0] + resp.b[1] + resp.b[2]) ^ 0xff)) {
resp.b[3] = 0;
return resp.le32;
}
if (this->read_array(resp, 4) && resp[3] == (sum_bytes(resp, 3, BL0942_READ_COMMAND + this->address_ + reg) ^ 0xFF))
return encode_uint24(resp[2], resp[1], resp[0]);
return -1;
}
+1 -3
View File
@@ -21,10 +21,8 @@ static constexpr uint8_t CLOCK_1_60_HZ = 0x83;
static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60;
void BM8563::setup() {
if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte_16(CONTROL_STATUS_1_REG, 0)))
return;
}
}
void BM8563::update() { this->read_time(); }
+23 -36
View File
@@ -75,29 +75,21 @@ void BME280Component::setup() {
this->reset_to_construction_state();
}
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_byte(BME280_REGISTER_CHIPID, &chip_id)))
return;
}
if (chip_id != 0x60) {
this->mark_failed(LOG_STR("Wrong chip ID or no response"));
if (!this->setup_check(chip_id == 0x60, LOG_STR("Wrong chip ID or no response")))
return;
}
// Send a soft reset.
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
this->mark_failed(LOG_STR("Reset failed"));
if (!this->setup_check(this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET), LOG_STR("Reset failed")))
return;
}
// Wait until the NVM data has finished loading.
uint8_t status;
uint8_t retry = 5;
do { // NOLINT
delay(2);
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
this->mark_failed(LOG_STR("Error reading status register"));
if (!this->setup_check(this->read_byte(BME280_REGISTER_STATUS, &status), LOG_STR("Error reading status register")))
return;
}
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
if (status & BME280_STATUS_IM_UPDATE) {
this->mark_failed(LOG_STR("Timeout loading NVM"));
@@ -129,29 +121,23 @@ void BME280Component::setup() {
this->calibration_.h6 = read_u8_(BME280_REGISTER_DIG_H6);
uint8_t humid_control_val = 0;
if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
this->mark_failed(LOG_STR("Read humidity control"));
if (!this->setup_check(this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val),
LOG_STR("Read humidity control")))
return;
}
humid_control_val &= ~0b00000111;
humid_control_val |= this->humidity_oversampling_ & 0b111;
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
this->mark_failed(LOG_STR("Write humidity control"));
if (!this->setup_check(this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val),
LOG_STR("Write humidity control")))
return;
}
uint8_t config_register = 0;
if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
this->mark_failed(LOG_STR("Read config"));
if (!this->setup_check(this->read_byte(BME280_REGISTER_CONFIG, &config_register), LOG_STR("Read config")))
return;
}
config_register &= ~0b11111100;
config_register |= 0b101 << 5; // 1000 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
this->mark_failed(LOG_STR("Write config"));
if (!this->setup_check(this->write_byte(BME280_REGISTER_CONFIG, config_register), LOG_STR("Write config")))
return;
}
}
void BME280Component::dump_config() {
ESP_LOGCONFIG(TAG, "BME280:");
@@ -171,6 +157,10 @@ inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (
void BME280Component::update() {
// Enable sensor
ESP_LOGV(TAG, "Sending conversion request");
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, this->humidity_oversampling_ & 0b111)) {
this->status_set_warning();
return;
}
uint8_t meas_value = 0;
meas_value |= (this->temperature_oversampling_ & 0b111) << 5;
meas_value |= (this->pressure_oversampling_ & 0b111) << 2;
@@ -210,12 +200,11 @@ void BME280Component::update() {
});
}
float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) {
int32_t adc = encode_uint24(data[3], data[4], data[5]);
adc >>= 4;
if (adc == 0x80000) {
// temperature was disabled
if (this->temperature_oversampling_ == BME280_OVERSAMPLING_NONE) {
return NAN;
}
int32_t adc = encode_uint24(data[3], data[4], data[5]);
adc >>= 4;
const int32_t t1 = this->calibration_.t1;
const int32_t t2 = this->calibration_.t2;
@@ -230,12 +219,11 @@ float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) {
}
float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
int32_t adc = encode_uint24(data[0], data[1], data[2]);
adc >>= 4;
if (adc == 0x80000) {
// pressure was disabled
if (this->pressure_oversampling_ == BME280_OVERSAMPLING_NONE) {
return NAN;
}
int32_t adc = encode_uint24(data[0], data[1], data[2]);
adc >>= 4;
const int64_t p1 = this->calibration_.p1;
const int64_t p2 = this->calibration_.p2;
const int64_t p3 = this->calibration_.p3;
@@ -267,11 +255,10 @@ float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
}
float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
uint16_t const raw_adc = encode_uint16(data[6], data[7]);
if (raw_adc == 0x8000)
if (this->humidity_oversampling_ == BME280_OVERSAMPLING_NONE) {
return NAN;
int32_t const adc = raw_adc;
}
int32_t const adc = encode_uint16(data[6], data[7]);
const int32_t h1 = this->calibration_.h1;
const int32_t h2 = this->calibration_.h2;
@@ -130,14 +130,62 @@ void BME68xBSEC2Component::check_status_() {
}
void BME68xBSEC2Component::loop() {
// Process a single action from the queue. These are primarily sensor state publishes
// that in totality take too long to send in a single call.
if (this->queue_.size()) {
auto action = std::move(this->queue_.front());
this->queue_.pop();
action();
} else {
// Publish a single waiting state. All of them in one call take too long.
if (this->pending_.empty()) {
this->disable_loop();
return;
}
const PendingPublish pending = this->pending_.front();
this->pending_.pop();
const float value = pending.value;
switch (pending.slot) {
#ifdef USE_SENSOR
case PUBLISH_SLOT_IAQ:
this->publish_sensor_(this->iaq_sensor_, value);
break;
case PUBLISH_SLOT_IAQ_STATIC:
this->publish_sensor_(this->iaq_static_sensor_, value);
break;
case PUBLISH_SLOT_CO2_EQUIVALENT:
this->publish_sensor_(this->co2_equivalent_sensor_, value);
break;
case PUBLISH_SLOT_BREATH_VOC_EQUIVALENT:
this->publish_sensor_(this->breath_voc_equivalent_sensor_, value);
break;
case PUBLISH_SLOT_PRESSURE:
this->publish_sensor_(this->pressure_sensor_, value);
break;
case PUBLISH_SLOT_GAS_RESISTANCE:
this->publish_sensor_(this->gas_resistance_sensor_, value);
break;
case PUBLISH_SLOT_TEMPERATURE:
this->publish_sensor_(this->temperature_sensor_, value);
break;
case PUBLISH_SLOT_HUMIDITY:
this->publish_sensor_(this->humidity_sensor_, value);
break;
case PUBLISH_SLOT_IAQ_ACCURACY:
this->publish_sensor_(this->iaq_accuracy_sensor_, value, true);
break;
#endif
#ifdef USE_TEXT_SENSOR
case PUBLISH_SLOT_IAQ_ACCURACY_TEXT:
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[static_cast<uint8_t>(value)]);
break;
#endif
case PUBLISH_SLOT_SAVE_STATE:
this->save_state_(static_cast<uint8_t>(value));
break;
default:
break;
}
}
void BME68xBSEC2Component::queue_publish_(PublishSlot slot, float value) {
// Only full when the loop stalled for seconds; drop the oldest so the newest reading still goes out
if (!this->pending_.push({value, slot})) {
this->pending_.pop();
this->pending_.push({value, slot});
}
}
@@ -414,44 +462,44 @@ void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_ou
max_accuracy = std::max(outputs[i].accuracy, max_accuracy);
update_accuracy = true;
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_IAQ, signal);
#endif
break;
case BSEC_OUTPUT_STATIC_IAQ:
max_accuracy = std::max(outputs[i].accuracy, max_accuracy);
update_accuracy = true;
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_static_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_IAQ_STATIC, signal);
#endif
break;
case BSEC_OUTPUT_CO2_EQUIVALENT:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_CO2_EQUIVALENT, signal);
#endif
break;
case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_BREATH_VOC_EQUIVALENT, signal);
#endif
break;
case BSEC_OUTPUT_RAW_PRESSURE:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); });
this->queue_publish_(PUBLISH_SLOT_PRESSURE, signal / 100.0f);
#endif
break;
case BSEC_OUTPUT_RAW_GAS:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_GAS_RESISTANCE, signal);
#endif
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_TEMPERATURE, signal);
#endif
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_HUMIDITY, signal);
#endif
break;
}
@@ -459,16 +507,13 @@ void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_ou
if (update_accuracy) {
max_accuracy = std::min<uint8_t>(max_accuracy, std::size(IAQ_ACCURACY_STATES) - 1);
#ifdef USE_SENSOR
this->queue_push_(
[this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); });
this->queue_publish_(PUBLISH_SLOT_IAQ_ACCURACY, max_accuracy);
#endif
#ifdef USE_TEXT_SENSOR
this->queue_push_([this, max_accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[max_accuracy]);
});
this->queue_publish_(PUBLISH_SLOT_IAQ_ACCURACY_TEXT, max_accuracy);
#endif
// Queue up an opportunity to save state
this->queue_push_([this, max_accuracy]() { this->save_state_(max_accuracy); });
this->queue_publish_(PUBLISH_SLOT_SAVE_STATE, max_accuracy);
}
this->enable_loop();
}
+25 -3
View File
@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#ifdef USE_BSEC2
@@ -15,7 +16,6 @@
#endif
#include <cinttypes>
#include <queue>
#include <bsec2.h>
@@ -83,6 +83,28 @@ class BME68xBSEC2Component : public Component {
virtual uint32_t get_hash() = 0;
protected:
// States waiting for loop() to publish them, one per pass
enum PublishSlot : uint8_t {
PUBLISH_SLOT_IAQ,
PUBLISH_SLOT_IAQ_STATIC,
PUBLISH_SLOT_CO2_EQUIVALENT,
PUBLISH_SLOT_BREATH_VOC_EQUIVALENT,
PUBLISH_SLOT_PRESSURE,
PUBLISH_SLOT_GAS_RESISTANCE,
PUBLISH_SLOT_TEMPERATURE,
PUBLISH_SLOT_HUMIDITY,
PUBLISH_SLOT_IAQ_ACCURACY,
PUBLISH_SLOT_IAQ_ACCURACY_TEXT,
PUBLISH_SLOT_SAVE_STATE,
PUBLISH_SLOT_COUNT,
};
struct __attribute__((packed)) PendingPublish {
float value; // the accuracy for the accuracy and save slots
PublishSlot slot;
};
// A read returns up to 3 fields in parallel mode, each published in full
static constexpr uint8_t PUBLISH_QUEUE_SIZE = 3 * PUBLISH_SLOT_COUNT;
void set_config_(const uint8_t *config, u_int32_t len);
float calc_sensor_sample_rate_(SampleRate sample_rate);
void update_subscription_();
@@ -103,14 +125,14 @@ class BME68xBSEC2Component : public Component {
void load_state_();
void save_state_(uint8_t accuracy);
void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
void queue_publish_(PublishSlot slot, float value);
struct bme68x_dev bme68x_;
bsec_bme_settings_t bsec_settings_;
bsec_version_t version_;
struct bme68x_heatr_conf bme68x_heatr_conf_;
std::queue<std::function<void()>> queue_;
StaticRingBuffer<PendingPublish, PUBLISH_QUEUE_SIZE> pending_;
ESPPreferenceObject bsec_state_;
uint8_t const *bsec2_configuration_{nullptr};
+9 -24
View File
@@ -82,59 +82,44 @@ void BMI270Component::setup() {
return;
// 2. Soft-reset via CMD register (0x7E = 0xB6)
if (!this->write_byte(BMI270_REG_CMD, BMI270_CMD_SOFT_RESET)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_CMD, BMI270_CMD_SOFT_RESET)))
return;
}
delay(20);
// 4. Upload the configuration blob
if (!load_config_file_()) {
this->mark_failed(LOG_STR("Config file upload failed"));
if (!this->setup_check(load_config_file_(), LOG_STR("Config file upload failed")))
return;
}
ESP_LOGD(TAG, "Config blob uploaded ✓");
// 5. Configure accelerometer
// ACC_CONF: ODR | BWP(0x2 = normal avg4) | perf_mode(1)
uint8_t acc_conf =
(uint8_t) (accel_odr_) | (BMI270_CONF_BWP_NORMAL << BMI270_CONF_BWP_SHIFT) | BMI270_ACC_CONF_PERF_MODE;
if (!this->write_byte(BMI270_REG_ACC_CONF, acc_conf)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_ACC_CONF, acc_conf)))
return;
}
if (!this->write_byte(BMI270_REG_ACC_RANGE, (uint8_t) accel_range_)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_ACC_RANGE, (uint8_t) accel_range_)))
return;
}
// 6. Configure gyroscope
// GYR_CONF: ODR | BWP(0x2 = normal) | noise_perf(1) | filter_perf(1)
uint8_t gyr_conf = (uint8_t) (gyro_odr_) | (BMI270_CONF_BWP_NORMAL << BMI270_CONF_BWP_SHIFT) |
BMI270_GYR_CONF_NOISE_PERF | BMI270_GYR_CONF_FILTER_PERF;
if (!this->write_byte(BMI270_REG_GYR_CONF, gyr_conf)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_GYR_CONF, gyr_conf)))
return;
}
if (!this->write_byte(BMI270_REG_GYR_RANGE, (uint8_t) gyro_range_)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_GYR_RANGE, (uint8_t) gyro_range_)))
return;
}
// 7. Enable accelerometer, gyroscope, and temperature sensor
// PWR_CTRL bits: temp_en[3] | gyr_en[2] | acc_en[1]
if (!this->write_byte(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_IMU)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_IMU)))
return;
}
delay(5);
// 8. Re-enable advanced power save (optional; keeps current low between reads)
// Disabled here for simplicity – leave in performance mode
if (!this->write_byte(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_FIFO_SELF_WAKEUP)) { // bit1 = fifo_self_wakeup
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
// bit1 = fifo_self_wakeup
if (!this->setup_check(this->write_byte(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_FIFO_SELF_WAKEUP)))
return;
}
// 9. Optionally bring up whatever is wired to the aux interface.
// This is entirely additive: it is off by default, and failure only disables
+1 -3
View File
@@ -24,10 +24,8 @@ void BMP085Component::update() {
}
void BMP085Component::setup() {
uint8_t data[22];
if (!this->read_bytes(BMP085_REGISTER_AC1_H, data, 22)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_bytes(BMP085_REGISTER_AC1_H, data, 22)))
return;
}
// Load calibration
this->calibration_.ac1 = encode_uint16(data[0], data[1]);
+14 -29
View File
@@ -40,33 +40,24 @@ void BMP280Component::setup() {
// Read the chip id twice, to work around a bug where the first read is 0.
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
if (!this->bmp_read_byte(0xD0, &chip_id)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->bmp_read_byte(0xD0, &chip_id)))
return;
}
if (!this->bmp_read_byte(0xD0, &chip_id)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->bmp_read_byte(0xD0, &chip_id)))
return;
}
if (chip_id != 0x58) {
this->mark_failed(LOG_STR("Wrong chip ID or no response"));
if (!this->setup_check(chip_id == 0x58, LOG_STR("Wrong chip ID or no response")))
return;
}
// Send a soft reset.
if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
this->mark_failed(LOG_STR("Reset failed"));
if (!this->setup_check(this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET), LOG_STR("Reset failed")))
return;
}
// Wait until the NVM data has finished loading.
uint8_t status;
uint8_t retry = 5;
do {
delay(2);
if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) {
this->mark_failed(LOG_STR("Error reading status register"));
if (!this->setup_check(this->bmp_read_byte(BMP280_REGISTER_STATUS, &status),
LOG_STR("Error reading status register")))
return;
}
} while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
if (status & BMP280_STATUS_IM_UPDATE) {
this->mark_failed(LOG_STR("Timeout loading NVM"));
@@ -89,17 +80,13 @@ void BMP280Component::setup() {
this->calibration_.p9 = this->read_s16_le_(0x9E);
uint8_t config_register = 0;
if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
this->mark_failed(LOG_STR("Read config"));
if (!this->setup_check(this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register), LOG_STR("Read config")))
return;
}
config_register &= ~0b11111100;
config_register |= 0b000 << 5; // 0.5 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) {
this->mark_failed(LOG_STR("Write config"));
if (!this->setup_check(this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register), LOG_STR("Write config")))
return;
}
}
void BMP280Component::dump_config() {
ESP_LOGCONFIG(TAG, "BMP280:");
@@ -148,16 +135,15 @@ void BMP280Component::update() {
}
float BMP280Component::read_temperature_(int32_t *t_fine) {
if (this->temperature_oversampling_ == BMP280_OVERSAMPLING_NONE) {
return NAN;
}
uint8_t data[3]{};
if (!this->bmp_read_bytes(BMP280_REGISTER_TEMPDATA, data, 3))
return NAN;
ESP_LOGV(TAG, "Read temperature data, raw: %02X %02X %02X", data[0], data[1], data[2]);
int32_t adc = encode_uint24(data[0], data[1], data[2]);
adc >>= 4;
if (adc == 0x80000) {
// temperature was disabled
return NAN;
}
const int32_t t1 = this->calibration_.t1;
const int32_t t2 = this->calibration_.t2;
@@ -172,15 +158,14 @@ float BMP280Component::read_temperature_(int32_t *t_fine) {
}
float BMP280Component::read_pressure_(int32_t t_fine) {
if (this->pressure_oversampling_ == BMP280_OVERSAMPLING_NONE) {
return NAN;
}
uint8_t data[3];
if (!this->bmp_read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3))
return NAN;
int32_t adc = encode_uint24(data[0], data[1], data[2]);
adc >>= 4;
if (adc == 0x80000) {
// pressure was disabled
return NAN;
}
const int64_t p1 = this->calibration_.p1;
const int64_t p2 = this->calibration_.p2;
const int64_t p3 = this->calibration_.p3;
+8 -16
View File
@@ -45,15 +45,11 @@ static const LogString *iir_filter_to_str(IIRFilter filter) {
void BMP3XXComponent::setup() {
// Call the Device base class "initialise" function
if (!reset()) {
this->mark_failed(LOG_STR("Failed to reset"));
if (!this->setup_check(this->reset(), LOG_STR("Failed to reset")))
return;
}
if (!read_byte(BMP388_CHIP_ID, &this->chip_id_.reg)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(read_byte(BMP388_CHIP_ID, &this->chip_id_.reg)))
return;
}
ESP_LOGCONFIG(TAG, "Chip %s Id 0x%X", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg);
if (chip_id_.reg != BMP388_ID && chip_id_.reg != BMP390_ID) {
@@ -63,10 +59,10 @@ void BMP3XXComponent::setup() {
// set sensor in sleep mode
stop_conversion();
// Read the calibration parameters into the params structure
if (!read_bytes(BMP388_TRIM_PARAMS, (uint8_t *) &compensation_params_, sizeof(compensation_params_))) {
this->mark_failed(LOG_STR("Can't read calibration data"));
if (!this->setup_check(
read_bytes(BMP388_TRIM_PARAMS, (uint8_t *) &compensation_params_, sizeof(compensation_params_)),
LOG_STR("Can't read calibration data")))
return;
}
compensation_float_params_.param_T1 =
(float) compensation_params_.param_T1 / powf(2.0f, -8.0f); // Calculate the floating point trim parameters
compensation_float_params_.param_T2 = (float) compensation_params_.param_T2 / powf(2.0f, 30.0f);
@@ -84,10 +80,8 @@ void BMP3XXComponent::setup() {
compensation_float_params_.param_P11 = (float) compensation_params_.param_P11 / powf(2.0f, 65.0f);
// Initialise the BMP388 IIR filter register
if (!set_iir_filter(this->iir_filter_)) {
this->mark_failed(LOG_STR("Failed to set IIR filter"));
if (!this->setup_check(this->set_iir_filter(this->iir_filter_), LOG_STR("Failed to set IIR filter")))
return;
}
// Set power control registers
pwr_ctrl_.bit.press_en = 1;
@@ -103,10 +97,8 @@ void BMP3XXComponent::setup() {
this->temperature_oversampling_ = OVERSAMPLING_NONE;
}
// Initialise the BMP388 oversampling register
if (!set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_)) {
this->mark_failed(LOG_STR("Failed to set oversampling register"));
return;
}
this->setup_check(this->set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_),
LOG_STR("Failed to set oversampling register"));
}
void BMP3XXComponent::dump_config() {
+8 -13
View File
@@ -65,19 +65,14 @@ BQ25186Component = bq25186_ns.class_(
BQ25186_CHILD_SCHEMA = cv.Schema(
{cv.GenerateID(CONF_BQ25186_ID): cv.use_id(BQ25186Component)}
)
_request_binary_sensor_slot = cg.slot_counter("BQ25186_BINARY_SENSOR_COUNT")
_request_text_sensor_slot = cg.slot_counter("BQ25186_TEXT_SENSOR_COUNT")
def register_binary_sensor(hub: cg.MockObj, var: cg.MockObj) -> cg.Expression:
_request_binary_sensor_slot(str(hub))
return hub.register_binary_sensor(var)
def register_text_sensor(hub: cg.MockObj, var: cg.MockObj) -> cg.Expression:
_request_text_sensor_slot(str(hub))
return hub.register_text_sensor(var)
BQ25186StatusRegisters = bq25186_ns.class_("BQ25186StatusRegisters")
STATUS_PUBLISHER = cg.HubFunction(
"status",
cg.void,
[(BQ25186StatusRegisters.const_ref(), "status")],
define="USE_BQ25186_STATUS_PUBLISHER",
)
STATUS = STATUS_PUBLISHER.param("status")
REG_VBAT_CTRL = 0x03
REG_ICHG_CTRL = 0x04
+15 -11
View File
@@ -1,5 +1,4 @@
from functools import partial
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
@@ -9,16 +8,13 @@ from esphome.const import (
DEVICE_CLASS_PLUG,
DEVICE_CLASS_RUNNING,
)
from esphome.cpp_generator import HexIntLiteral
from esphome.types import ConfigType
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns, register_binary_sensor
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, STATUS, STATUS_PUBLISHER
DEPENDENCIES = ["bq25186"]
BQ25186BinarySensor = bq25186_ns.class_(
"BQ25186BinarySensor", binary_sensor.BinarySensor
)
REG_STAT0 = 0
REG_STAT1 = 1
REG_FLAG0 = 2
@@ -51,11 +47,19 @@ _FIELDS = (
)
def _state_expression(reg: int, shift: int, mask: int, match: int) -> cg.MockObj:
"""Whether the field (status[reg] >> shift) & mask reads as on, as a bit of match."""
value = (STATUS[reg] >> shift) & HexIntLiteral(mask)
if mask == 0x01 and match == _BIT_SET:
return value
return (cg.MockObj(f"{match:#06b}") >> value) & 1
CONFIG_SCHEMA = cv.All(
BQ25186_CHILD_SCHEMA.extend(
{
cv.Optional(key): binary_sensor.binary_sensor_schema(
BQ25186BinarySensor, device_class=device_class
device_class=device_class
)
for key, *_, device_class in _FIELDS
}
@@ -68,6 +72,6 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_BQ25186_ID])
sensors = binary_sensor.sub_binary_sensors(config)
for key, reg, shift, mask, match, _ in _FIELDS:
await sensors(
key, partial(register_binary_sensor, hub), reg, shift, mask, match
)
if (var := await sensors(key, None)) is not None:
state = _state_expression(reg, shift, mask, match)
STATUS_PUBLISHER.add(hub, parent_ref(var).publish_state(state))
+3 -13
View File
@@ -1,10 +1,4 @@
#include "bq25186.h"
#ifdef USE_BINARY_SENSOR
#include "bq25186_binary_sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "bq25186_text_sensor.h"
#endif
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -144,13 +138,9 @@ void BQ25186Component::update() {
#endif
if (!pending)
this->status_clear_warning();
#ifdef BQ25186_BINARY_SENSOR_COUNT
for (auto *sensor : this->binary_sensors_)
sensor->on_status(status);
#endif
#ifdef BQ25186_TEXT_SENSOR_COUNT
for (auto *sensor : this->text_sensors_)
sensor->on_status(status);
#ifdef USE_BQ25186_STATUS_PUBLISHER
if (this->status_publisher_ != nullptr)
this->status_publisher_(status);
#endif
}
+21 -13
View File
@@ -7,6 +7,10 @@
#ifdef USE_BQ25186_PG_GPO
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/core/progmem.h"
#endif
#include <array>
@@ -38,8 +42,21 @@ static constexpr uint8_t BQ25186_SYS_REG_PG_GPO_LEVEL = 0x10;
using BQ25186ConfigRegisters = std::array<uint8_t, BQ25186_CONFIG_REGISTER_COUNT>;
using BQ25186StatusRegisters = std::array<uint8_t, BQ25186_STATUS_REGISTER_COUNT>;
class BQ25186BinarySensor;
class BQ25186TextSensor;
#ifdef USE_TEXT_SENSOR
// STAT0[6:5] charge status, then STAT1[4:3] TS status
PROGMEM_STRING_TABLE(BQ25186StatusStrings, "Not Charging", "Constant Current", "Constant Voltage",
"Charge Done/Disabled", "Normal", "Hot/Cold (Charging Suspended)", "Cool (Current Reduced)",
"Warm (Voltage Reduced)");
/// Publish the two-bit field in the low bits of `value` as string `first + field`, only when it differs from `last`
inline void publish_status_text(text_sensor::TextSensor *sensor, uint8_t &last, uint8_t value, uint8_t first) {
value &= 0x03;
if (value == last)
return;
last = value;
sensor->publish_state(BQ25186StatusStrings::get_progmem_str(first + value, BQ25186StatusStrings::LAST_INDEX));
}
#endif
class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
public:
@@ -52,11 +69,8 @@ class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
this->configure_on_boot_ = true;
}
bool apply_config(const BQ25186ConfigRegisters &regs);
#ifdef BQ25186_BINARY_SENSOR_COUNT
void register_binary_sensor(BQ25186BinarySensor *sensor) { this->binary_sensors_.push_back(sensor); }
#endif
#ifdef BQ25186_TEXT_SENSOR_COUNT
void register_text_sensor(BQ25186TextSensor *sensor) { this->text_sensors_.push_back(sensor); }
#ifdef USE_BQ25186_STATUS_PUBLISHER
HUB_PUBLISHER(status, const BQ25186StatusRegisters &)
#endif
#ifdef USE_BQ25186_PG_GPO
/// The /PG/GPO pin is put in general purpose output mode when a switch drives it
@@ -78,12 +92,6 @@ class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
void check_pg_gpo_(const BQ25186StatusRegisters &status, bool &pending);
#endif
#ifdef BQ25186_BINARY_SENSOR_COUNT
StaticVector<BQ25186BinarySensor *, BQ25186_BINARY_SENSOR_COUNT> binary_sensors_;
#endif
#ifdef BQ25186_TEXT_SENSOR_COUNT
StaticVector<BQ25186TextSensor *, BQ25186_TEXT_SENSOR_COUNT> text_sensors_;
#endif
BQ25186ConfigRegisters regs_{}; // the configuration last applied, written again after a chip reset
bool configure_on_boot_{false};
bool configured_{false}; // regs_ went to the chip at least once
@@ -1,31 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR
#include "bq25186.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::bq25186 {
/// One field of a status register; `match` has a bit set for each field value that reads as on
class BQ25186BinarySensor final : public binary_sensor::BinarySensor {
public:
BQ25186BinarySensor(uint8_t reg, uint8_t shift, uint8_t mask, uint8_t match)
: reg_(reg), shift_(shift), mask_(mask), match_(match) {}
void on_status(const BQ25186StatusRegisters &status) {
const uint8_t value = (status[this->reg_] >> this->shift_) & this->mask_;
this->publish_state((this->match_ >> value) & 1);
}
protected:
const uint8_t reg_;
const uint8_t shift_;
const uint8_t mask_;
const uint8_t match_;
};
} // namespace esphome::bq25186
#endif // USE_BINARY_SENSOR
@@ -1,22 +0,0 @@
#include "bq25186_text_sensor.h"
#ifdef USE_TEXT_SENSOR
#include "esphome/core/progmem.h"
namespace esphome::bq25186 {
// STAT0[6:5] charge status, then STAT1[4:3] TS status
PROGMEM_STRING_TABLE(StatusStrings, "Not Charging", "Constant Current", "Constant Voltage", "Charge Done/Disabled",
"Normal", "Hot/Cold (Charging Suspended)", "Cool (Current Reduced)", "Warm (Voltage Reduced)");
void BQ25186TextSensor::on_status(const BQ25186StatusRegisters &status) {
const uint8_t value = (status[this->reg_] >> this->shift_) & 0x03;
if (value == this->last_)
return;
this->last_ = value;
this->publish_state(StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX));
}
} // namespace esphome::bq25186
#endif // USE_TEXT_SENSOR
@@ -1,26 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_TEXT_SENSOR
#include "bq25186.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::bq25186 {
/// A two-bit status field; `first` is the string table index of the field's value 0
class BQ25186TextSensor final : public text_sensor::TextSensor {
public:
BQ25186TextSensor(uint8_t reg, uint8_t shift, uint8_t first) : reg_(reg), shift_(shift), first_(first) {}
void on_status(const BQ25186StatusRegisters &status);
protected:
const uint8_t reg_;
const uint8_t shift_;
const uint8_t first_;
uint8_t last_{0xFF}; // the field value last published
};
} // namespace esphome::bq25186
#endif // USE_TEXT_SENSOR
+35 -10
View File
@@ -1,16 +1,24 @@
from functools import partial
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.cpp_generator import (
AssignmentExpression,
HexIntLiteral,
VariableDeclarationExpression,
)
from esphome.types import ConfigType
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns, register_text_sensor
from . import (
BQ25186_CHILD_SCHEMA,
CONF_BQ25186_ID,
STATUS,
STATUS_PUBLISHER,
bq25186_ns,
)
DEPENDENCIES = ["bq25186"]
BQ25186TextSensor = bq25186_ns.class_("BQ25186TextSensor", text_sensor.TextSensor)
REG_STAT0 = 0
REG_STAT1 = 1
@@ -20,12 +28,28 @@ _FIELDS = (
("ts_status", REG_STAT1, 3, 4),
)
def _add_text_status(
hub: cg.MockObj, var: cg.MockObj, reg: int, shift: int, first: int
) -> None:
"""Publish status string first + the two-bit field at status[reg] >> shift, on change."""
last = cg.MockObj(f"esphome__{var}__last")
# 0xFF so the first poll publishes
declaration = VariableDeclarationExpression(cg.uint8, "", last, static=True)
STATUS_PUBLISHER.add(
hub, AssignmentExpression(None, None, declaration, HexIntLiteral(0xFF))
)
STATUS_PUBLISHER.add(
hub,
bq25186_ns.publish_status_text(
parent_ref(var), last, STATUS[reg] >> shift, first
),
)
CONFIG_SCHEMA = cv.All(
BQ25186_CHILD_SCHEMA.extend(
{
cv.Optional(key): text_sensor.text_sensor_schema(BQ25186TextSensor)
for key, *_ in _FIELDS
}
{cv.Optional(key): text_sensor.text_sensor_schema() for key, *_ in _FIELDS}
),
cv.has_at_least_one_key(*(key for key, *_ in _FIELDS)),
)
@@ -35,4 +59,5 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_BQ25186_ID])
sensors = text_sensor.sub_text_sensors(config)
for key, reg, shift, first in _FIELDS:
await sensors(key, partial(register_text_sensor, hub), reg, shift, first)
if (var := await sensors(key, None)) is not None:
_add_text_status(hub, var, reg, shift, first)
+4 -6
View File
@@ -27,16 +27,14 @@ static constexpr uint16_t TIME_TO_EMPTY_NOT_APPLICABLE = 0xFFFF;
void BQ27220Component::setup() {
const uint8_t device_number_cmd[2] = {0x01, 0x00}; // DEVICE_NUMBER subcommand, little-endian
if (this->write_register(REG_CONTROL, device_number_cmd, 2) != i2c::ERROR_OK) {
this->mark_failed(LOG_STR("Failed to communicate with BQ27220"));
if (!this->setup_check(this->write_register(REG_CONTROL, device_number_cmd, 2) == i2c::ERROR_OK,
LOG_STR("Failed to communicate with BQ27220")))
return;
}
delay(2); // SLUUBD4A §5.3: the subcommand result needs a moment before it can be read
uint16_t device_number = 0;
if (!this->read_byte_16_le(REG_MAC_DATA, &device_number)) {
this->mark_failed(LOG_STR("Failed to read device number from BQ27220"));
if (!this->setup_check(this->read_byte_16_le(REG_MAC_DATA, &device_number),
LOG_STR("Failed to read device number from BQ27220")))
return;
}
if (device_number != DEVICE_NUMBER) {
ESP_LOGE(TAG, "Unexpected device number 0x%04X (expected 0x%04X)", device_number, DEVICE_NUMBER);
this->mark_failed(LOG_STR("Unexpected device number"));
@@ -10,6 +10,8 @@ CODEOWNERS = ["@nagyrobi"]
AUTO_LOAD = ["ble_device_base"]
DOMAIN = "bthome_mithermometer"
CONF_REPLAY_PROTECTION = "replay_protection"
bthome_mithermometer_ns = cg.esphome_ns.namespace("bthome_mithermometer")
BTHomeMiThermometer = bthome_mithermometer_ns.class_(
@@ -29,14 +31,25 @@ def bthome_mithermometer_base_schema(
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_BINDKEY): cv.bind_key,
cv.Optional(CONF_REPLAY_PROTECTION, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(extra_schema)
.extend(ble_device_base.BLE_DEVICE_SCHEMA),
_validate_replay_protection,
)
def _validate_replay_protection(config: ConfigType) -> ConfigType:
if config[CONF_REPLAY_PROTECTION] and CONF_BINDKEY not in config:
raise cv.Invalid(
f"'{CONF_REPLAY_PROTECTION}' requires '{CONF_BINDKEY}'",
path=[CONF_REPLAY_PROTECTION],
)
return config
async def setup_bthome_mithermometer(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
await ble_device_base.register_ble_device(var, config)
@@ -47,3 +60,6 @@ async def setup_bthome_mithermometer(var: MockObj, config: ConfigType) -> None:
for index in range(0, len(bindkey), 2)
]
cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes)))
if config[CONF_REPLAY_PROTECTION]:
cg.add_define("USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION")
cg.add(var.set_replay_protection(True))
@@ -1,10 +1,12 @@
#include "bthome_ble.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <array>
#include <cinttypes>
#include <cstring>
#include <span>
@@ -31,6 +33,9 @@ static constexpr size_t BTHOME_BINDKEY_SIZE = 16;
static constexpr size_t BTHOME_NONCE_SIZE = 13;
static constexpr size_t BTHOME_MIC_SIZE = 4;
static constexpr size_t BTHOME_COUNTER_SIZE = 4;
// If no frame with a higher counter has been accepted for this long, a lower
// counter is accepted again (the sensor rebooted, e.g. after a battery swap).
static constexpr uint32_t BTHOME_REPLAY_RESYNC_MS = 10 * 60 * 1000;
// Both callers are log macros (LOGCONFIG / LOGVV); below CONFIG level they
// compile away and an ungated helper trips -Wunused-function.
@@ -156,6 +161,9 @@ void BTHomeMiThermometer::dump_config() {
if (this->has_bindkey_) {
char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)];
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.'));
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
ESP_LOGCONFIG(TAG, " Replay Protection: %s", YESNO(this->replay_protection_));
#endif
}
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
@@ -253,6 +261,40 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da
return true;
}
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
// PVVX sends new data under an unchanged counter, so only an exact repeat (counter and MIC) is a duplicate and
// earlier frames under the current counter can be replayed; the lower-counter rule is the defence. It holds only
// while the sensor keeps being heard: after the resync window one old frame becomes the baseline and recorded
// frames above it can follow in order, the price of recovering from a counter restart. RAM only.
bool BTHomeMiThermometer::check_replay_counter_(uint32_t counter, uint32_t mic) {
const uint32_t now = App.get_loop_component_start_time();
if (counter == this->last_counter_ && mic == this->last_mic_) {
ESP_LOGVV(TAG, "Duplicate BTHome frame (counter %" PRIu32 ")", counter);
return false;
}
if (counter < this->last_counter_) {
if (now - this->last_accepted_ms_ < BTHOME_REPLAY_RESYNC_MS) {
// One warning per run of rejections
if (this->replay_rejected_) {
ESP_LOGVV(TAG, "BTHome frame rejected: counter %" PRIu32 " lower than last %" PRIu32, counter,
this->last_counter_);
} else {
ESP_LOGW(TAG, "BTHome frame rejected: counter %" PRIu32 " lower than last %" PRIu32 " (replay?)", counter,
this->last_counter_);
this->replay_rejected_ = true;
}
return false;
}
ESP_LOGW(TAG, "BTHome counter restarted (%" PRIu32 " -> %" PRIu32 "), resync", this->last_counter_, counter);
}
this->last_counter_ = counter;
this->last_mic_ = mic;
this->last_accepted_ms_ = now;
this->replay_rejected_ = false;
return true;
}
#endif
bool BTHomeMiThermometer::handle_service_data_(const ble_device_base::ServiceData &service_data,
const ble_device_base::ESPBTDevice &device) {
if (!service_data.uuid.contains(0xD2, 0xFC)) {
@@ -339,6 +381,17 @@ bool BTHomeMiThermometer::handle_service_data_(const ble_device_base::ServiceDat
return false;
}
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
// After the MIC and address checks, so other devices never touch the state
if (is_encrypted && this->replay_protection_) {
const uint8_t *tail = data.data() + data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE;
if (!this->check_replay_counter_(encode_value_le<uint32_t>(tail),
encode_value_le<uint32_t>(tail + BTHOME_COUNTER_SIZE))) {
return false;
}
}
#endif
if (payload_size == 0) {
ESP_LOGVV(TAG, "BTHome payload empty after header");
return false;
@@ -13,6 +13,10 @@
// decryption availability is selected per platform in the .cpp.
namespace esphome::bthome_mithermometer {
namespace testing {
class ReplayClockAccess;
} // namespace testing
class BTHomeMiThermometer final : public ble_device_base::ESPBTDeviceListener, public Component {
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
@@ -23,18 +27,36 @@ class BTHomeMiThermometer final : public ble_device_base::ESPBTDeviceListener, p
public:
void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(std::initializer_list<uint8_t> bindkey);
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
void set_replay_protection(bool replay_protection) { this->replay_protection_ = replay_protection; }
#endif
void dump_config() override;
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
protected:
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
friend class testing::ReplayClockAccess;
#endif
bool handle_service_data_(const ble_device_base::ServiceData &service_data,
const ble_device_base::ESPBTDevice &device);
bool decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
std::vector<uint8_t> &payload) const;
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
bool check_replay_counter_(uint32_t counter, uint32_t mic);
#endif
uint64_t address_{0};
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
uint32_t last_counter_{0};
uint32_t last_mic_{0};
uint32_t last_accepted_ms_{0};
#endif
optional<uint8_t> last_packet_id_{};
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
bool replay_protection_{false};
bool replay_rejected_{false};
#endif
bool has_bindkey_{false};
uint8_t bindkey_[16];
};
+147 -30
View File
@@ -4,10 +4,12 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.config import iter_ids
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_PLATFORM, CONF_TRIGGER_ID
from esphome.const import CONF_DATA, CONF_ID, CONF_PLATFORM
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -24,8 +26,25 @@ CONF_CANBUS_ID = "canbus_id"
CONF_BIT_RATE = "bit_rate"
CONF_ON_FRAME = "on_frame"
CONF_MAX_FRAMES_PER_LOOP = "max_frames_per_loop"
CONF_FD = "fd"
CONF_BIT_RATE_SWITCH = "bit_rate_switch"
CONF_PAD_DATA = "pad_data"
CONF_DEFAULT_FD = "default_fd"
CONF_DEFAULT_BIT_RATE_SWITCH = "default_bit_rate_switch"
CONF_DEFAULT_PAD_DATA = "default_pad_data"
DEFAULT_MAX_FRAMES_PER_LOOP = 50 # Keep in sync with max_frames_per_loop_ in canbus.h
DEFAULT_BIT_RATE = "125KBPS" # Keep in sync with bit_rate_ in canbus.h
# All 29 id bits; received ids never use more, so on_frame skips the & for this mask
CAN_ID_MASK_ALL = 0x1FFFFFFF
CAN_MAX_DATA_LENGTH = 8
# Payload length of each CAN FD DLC, keep in sync with CAN_FD_DLC_LENGTHS in canbus.h
CAN_FD_DATA_LENGTHS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64)
# canbus.send options that need a platform built on CANBUS_FD_SCHEMA
_FD_SEND_KEYS = (CONF_FD, CONF_BIT_RATE_SWITCH, CONF_PAD_DATA)
def _flag(name: str, value: bool) -> str:
return name if value else f"!{name}"
def validate_id(config: ConfigType) -> ConfigType:
@@ -49,11 +68,8 @@ def validate_raw_data(value: Any) -> bytes | list:
canbus_ns = cg.esphome_ns.namespace("canbus")
CanbusComponent = canbus_ns.class_("CanbusComponent", cg.Component)
CanbusTrigger = canbus_ns.class_(
"CanbusTrigger",
automation.Trigger.template(cg.std_vector.template(cg.uint8), cg.uint32),
cg.Component,
)
CanFrame = canbus_ns.struct("CanFrame")
CanPayload = canbus_ns.class_("CanPayload")
CanSpeed = canbus_ns.enum("CAN_SPEED")
CAN_SPEEDS = CanSpeed.options(
@@ -81,6 +97,21 @@ CAN_SPEEDS = CanSpeed.options(
prefix="CAN_",
)
# Data phase rates for a CAN FD platform's own schema
CAN_DATA_SPEEDS = {
**CAN_SPEEDS,
**CanSpeed.options(
"2000KBPS",
"3000KBPS",
"4000KBPS",
"5000KBPS",
"6000KBPS",
"7000KBPS",
"8000KBPS",
prefix="CAN_",
),
}
def get_rate(value: str) -> int:
match = re.match(r"(\d+)(?:K(\d+)?)?BPS", value, re.IGNORECASE)
@@ -93,7 +124,7 @@ def get_rate(value: str) -> int:
CANBUS_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CanbusComponent),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=CAN_ID_MASK_ALL),
cv.Optional(CONF_BIT_RATE, default=DEFAULT_BIT_RATE): cv.enum(
CAN_SPEEDS, upper=True
),
@@ -103,10 +134,9 @@ CANBUS_SCHEMA = cv.Schema(
): cv.positive_not_null_int,
cv.Optional(CONF_ON_FRAME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_CAN_ID_MASK, default=0x1FFFFFFF): cv.int_range(
min=0, max=0x1FFFFFFF
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=CAN_ID_MASK_ALL),
cv.Optional(CONF_CAN_ID_MASK, default=CAN_ID_MASK_ALL): cv.int_range(
min=0, max=CAN_ID_MASK_ALL
),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean,
@@ -118,39 +148,67 @@ CANBUS_SCHEMA = cv.Schema(
CANBUS_SCHEMA.add_extra(validate_id)
# A platform whose controller supports CAN FD extends this instead of CANBUS_SCHEMA. Its buses then accept the
# CAN FD options of canbus.send, and register_canbus() turns on USE_CANBUS_FD, which makes every CanFrame
# 64 bytes long.
CANBUS_FD_SCHEMA = CANBUS_SCHEMA.extend(
{
cv.Optional(CONF_DEFAULT_FD, default=False): cv.boolean,
cv.Optional(CONF_DEFAULT_BIT_RATE_SWITCH, default=False): cv.boolean,
cv.Optional(CONF_DEFAULT_PAD_DATA, default=False): cv.boolean,
}
)
def _is_fd_bus(config: ConfigType) -> bool:
return CONF_DEFAULT_FD in config
async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
cg.add(var.set_can_id(config[CONF_CAN_ID]))
if config[CONF_USE_EXTENDED_ID]:
cg.add(var.set_use_extended_id(True))
if _is_fd_bus(config):
cg.add_define("USE_CANBUS_FD")
cg.add(var.set_supports_fd(True))
for key, setter in (
(CONF_DEFAULT_FD, var.set_default_fd),
(CONF_DEFAULT_BIT_RATE_SWITCH, var.set_default_bit_rate_switch),
(CONF_DEFAULT_PAD_DATA, var.set_default_pad_data),
):
if config.get(key):
cg.add(setter(True))
if (bit_rate := config[CONF_BIT_RATE]) != DEFAULT_BIT_RATE:
cg.add(var.set_bitrate(CAN_SPEEDS[bit_rate]))
if (max_frames := config[CONF_MAX_FRAMES_PER_LOOP]) != DEFAULT_MAX_FRAMES_PER_LOOP:
cg.add(var.set_max_frames_per_loop(max_frames))
for conf in config.get(CONF_ON_FRAME, []):
can_id = conf[CONF_CAN_ID]
can_id_mask = conf[CONF_CAN_ID_MASK]
ext_id = conf[CONF_USE_EXTENDED_ID]
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id
)
if CONF_REMOTE_TRANSMISSION_REQUEST in conf:
cg.add(
trigger.set_remote_transmission_request(
conf[CONF_REMOTE_TRANSMISSION_REQUEST]
)
)
await cg.register_component(trigger, conf)
await automation.build_automation(
trigger,
can_id = f"0x{conf[CONF_CAN_ID]:X}"
if (mask := conf[CONF_CAN_ID_MASK]) != CAN_ID_MASK_ALL:
checks = [f"(frame.can_id & 0x{mask:X}) == {can_id}"]
else:
checks = [f"frame.can_id == {can_id}"]
checks.append(_flag("frame.use_extended_id", conf[CONF_USE_EXTENDED_ID]))
if (rtr := conf.get(CONF_REMOTE_TRANSMISSION_REQUEST)) is not None:
checks.append(_flag("frame.remote_transmission_request", rtr))
await automation.build_callback_automation(
var,
"add_frame_callback",
[
(cg.std_vector.template(cg.uint8), "x"),
(CanPayload, "x"),
(cg.uint32, "can_id"),
(cg.bool_, "remote_transmission_request"),
],
conf,
params=[(CanFrame.operator("const").operator("ref"), "frame")],
forward=[
"frame.payload()",
"frame.can_id",
"frame.remote_transmission_request",
],
when=" && ".join(checks),
)
@@ -170,16 +228,66 @@ async def register_canbus(
await setup_canbus_core_(var, config)
def validate_send(action: ConfigType, bus: ConfigType) -> None:
"""Reject a canbus.send that its bus cannot send, as far as the values are known.
Lambda values are checked when the action runs.
"""
data = action[CONF_DATA]
length = None if cg.is_template(data) else len(data)
if not _is_fd_bus(bus):
for key in _FD_SEND_KEYS:
if key in action:
raise cv.Invalid(
f"{key} needs a canbus platform that supports CAN FD", path=[key]
)
fd = False
else:
fd = action.get(CONF_FD, bus[CONF_DEFAULT_FD])
if fd is False and action.get(CONF_BIT_RATE_SWITCH) is True:
raise cv.Invalid("bit_rate_switch requires fd: true")
if fd is True and action[CONF_REMOTE_TRANSMISSION_REQUEST]:
raise cv.Invalid("remote_transmission_request cannot be used with fd: true")
if length is None:
return
if fd is False and length > CAN_MAX_DATA_LENGTH:
raise cv.Invalid("Classic CAN data must be at most 8 bytes", path=[CONF_DATA])
if (
fd is True
and length not in CAN_FD_DATA_LENGTHS
and action.get(CONF_PAD_DATA, bus[CONF_DEFAULT_PAD_DATA]) is False
):
raise cv.Invalid("CAN FD data length requires pad_data: true", path=[CONF_DATA])
def final_validate(config: list[ConfigType]) -> None:
full_config = fv.full_config.get()
for bus_id, path in iter_ids(full_config):
if len(path) < 2 or path[-2:] != ["canbus.send", CONF_CANBUS_ID]:
continue
action_path = path[:-1]
action = full_config.get_config_for_path(action_path)
bus = fv.get_declaring_config(bus_id, full_config)
with cv.prepend_path([cv.ROOT_CONFIG_PATH, *action_path]):
validate_send(action, bus)
FINAL_VALIDATE_SCHEMA = final_validate
# Actions
CANBUS_SEND_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent),
cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=CAN_ID_MASK_ALL),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST, default=False): cv.boolean,
# One classic CAN frame
cv.Optional(CONF_FD): cv.templatable(cv.boolean),
cv.Optional(CONF_BIT_RATE_SWITCH): cv.templatable(cv.boolean),
cv.Optional(CONF_PAD_DATA): cv.templatable(cv.boolean),
# One CAN FD frame; final_validate holds a classic bus to 8 bytes
cv.Required(CONF_DATA): cv.templatable(
cv.All(validate_raw_data, cv.Length(max=8))
cv.All(validate_raw_data, cv.Length(max=CAN_FD_DATA_LENGTHS[-1]))
),
},
validate_id,
@@ -209,6 +317,14 @@ async def canbus_action_to_code(
cg.add(
var.set_remote_transmission_request(config[CONF_REMOTE_TRANSMISSION_REQUEST])
)
# Only set on a CAN FD bus, which final_validate checks
for key, setter in (
(CONF_FD, var.set_fd),
(CONF_BIT_RATE_SWITCH, var.set_bit_rate_switch),
(CONF_PAD_DATA, var.set_pad_data),
):
if (value := config.get(key)) is not None:
cg.add(setter(await cg.templatable(value, args, cg.bool_)))
await automation.templatable_bytes(
config[CONF_DATA],
@@ -216,5 +332,6 @@ async def canbus_action_to_code(
var.set_data_template,
var.set_data_static,
"canbus_data",
return_type=CanPayload,
)
return var
+104 -52
View File
@@ -22,49 +22,112 @@ void Canbus::dump_config() {
}
}
canbus::Error Canbus::send_data(const CanFrame &frame) {
if (frame.can_id > (frame.use_extended_id ? CAN_EXTENDED_ID_MAX : CAN_STANDARD_ID_MAX)) {
ESP_LOGE(TAG, "CAN ID 0x%08" PRIx32 " exceeds frame identifier width", frame.can_id);
return ERROR_FAILTX;
}
#ifdef USE_CANBUS_FD
if (frame.fd && !this->supports_fd_) {
ESP_LOGE(TAG, "CAN FD frame on a bus without CAN FD");
return ERROR_FAILTX;
}
if (frame.fd ? (frame.remote_transmission_request || can_dlc_to_len(frame.can_data_length_code) == CAN_INVALID_DLC)
: frame.bit_rate_switch) {
ESP_LOGE(TAG, "Invalid CAN frame: fd=%d rtr=%d brs=%d dlc=%" PRIu8, frame.fd, frame.remote_transmission_request,
frame.bit_rate_switch, frame.can_data_length_code);
return ERROR_FAILTX;
}
#endif
return this->send_frame_(frame);
}
#ifdef USE_CANBUS_FD
canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const uint8_t *data, size_t len, bool fd, bool bit_rate_switch, bool pad_data) {
#else
canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const uint8_t *data, size_t len) {
struct CanFrame can_message;
#endif
CanFrame frame;
frame.use_extended_id = use_extended_id;
frame.remote_transmission_request = remote_transmission_request;
frame.can_id = can_id;
#ifdef USE_CANBUS_FD
frame.fd = fd;
frame.bit_rate_switch = bit_rate_switch;
frame.error_state_indicator = false;
if (fd) {
frame.can_data_length_code = can_len_to_dlc(len, pad_data);
if (frame.can_data_length_code == CAN_INVALID_DLC) {
ESP_LOGE(TAG, "CAN FD cannot carry %zu bytes without pad_data", len);
return ERROR_FAILTX;
}
if (!remote_transmission_request && len != 0) {
std::memcpy(frame.data, data, len);
std::memset(frame.data + len, 0, can_dlc_to_len(frame.can_data_length_code) - len);
}
return this->send_data(frame);
}
#endif
if (len > CAN_MAX_DATA_LENGTH) {
ESP_LOGW(TAG, "Classic CAN carries 8 bytes, %zu given, sent 8", len);
}
// Clamp before narrowing, so a lambda payload of 256 bytes or more cannot wrap to a short length
const uint8_t size = static_cast<uint8_t>(std::min<size_t>(len, CAN_MAX_DATA_LENGTH));
if (use_extended_id) {
ESP_LOGD(TAG, "send extended id=0x%08" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request),
size);
frame.can_data_length_code = static_cast<uint8_t>(std::min<size_t>(len, CAN_MAX_DATA_LENGTH));
// A remote request carries no data, so its bytes are zeroed rather than left from the stack. An empty vector may
// hand over a null pointer, which memcpy must not get even for 0 bytes.
if (remote_transmission_request) {
std::memset(frame.data, 0, frame.can_data_length_code);
} else if (frame.can_data_length_code != 0) {
std::memcpy(frame.data, data, frame.can_data_length_code);
}
#ifdef USE_CANBUS_FD
// The caller's flags can still contradict each other
return this->send_data(frame);
#else
return this->send_frame_(frame);
#endif
}
bool Canbus::lambda_payload_fits_(const CanPayload &payload, [[maybe_unused]] bool fd) {
if (!payload.truncated())
return true;
#ifdef USE_CANBUS_FD
if (fd) {
ESP_LOGE(TAG, "CAN FD carries at most %u bytes, frame not sent", CAN_FRAME_DATA_LENGTH);
return false;
}
// The payload still holds 64 bytes, so send_data() warns as it cuts a classic frame to 8
#else
ESP_LOGW(TAG, "Classic CAN carries 8 bytes, the rest is cut");
#endif
return true;
}
canbus::Error Canbus::send_frame_(const CanFrame &frame) {
const uint8_t size = frame.payload_length();
if (frame.use_extended_id) {
ESP_LOGD(TAG, "send extended id=0x%08" PRIx32 " rtr=%s size=%d", frame.can_id,
TRUEFALSE(frame.remote_transmission_request), size);
} else {
ESP_LOGD(TAG, "send standard id=0x%03" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request),
size);
ESP_LOGD(TAG, "send standard id=0x%03" PRIx32 " rtr=%s size=%d", frame.can_id,
TRUEFALSE(frame.remote_transmission_request), size);
}
can_message.can_data_length_code = size;
can_message.can_id = can_id;
can_message.use_extended_id = use_extended_id;
can_message.remote_transmission_request = remote_transmission_request;
ESP_LOGV_HEX(TAG, frame.data, size, CAN_FRAME_DATA_LENGTH, " data=%s");
for (int i = 0; i < size; i++) {
can_message.data[i] = data[i];
ESP_LOGVV(TAG, " data[%d]=%02x", i, can_message.data[i]);
}
canbus::Error error = this->send_message(&can_message);
// Drivers take a mutable pointer but do not change the frame
canbus::Error error = this->send_message(const_cast<CanFrame *>(&frame));
if (error != canbus::ERROR_OK) {
if (use_extended_id) {
ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed with error %d!", can_id, error);
if (frame.use_extended_id) {
ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed with error %d!", frame.can_id, error);
} else {
ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed with error %d!", can_id, error);
ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed with error %d!", frame.can_id, error);
}
}
return error;
}
void Canbus::add_trigger(CanbusTrigger *trigger) {
if (trigger->use_extended_id_) {
ESP_LOGVV(TAG, "add trigger for extended canid=0x%08" PRIx32, trigger->can_id_);
} else {
ESP_LOGVV(TAG, "add trigger for std canid=0x%03" PRIx32, trigger->can_id_);
}
this->triggers_.push_back(trigger);
};
void Canbus::loop() {
uint32_t now = App.get_loop_component_start_time();
if ((now - this->last_event_check_time_) >= EVENT_CHECK_INTERVAL_MS) {
@@ -75,38 +138,27 @@ void Canbus::loop() {
struct CanFrame can_message;
uint32_t message_counter = 0;
for (; message_counter < this->max_frames_per_loop_; message_counter++) {
#ifdef USE_CANBUS_FD
// Classic drivers leave the CAN FD flags alone
can_message.fd = false;
can_message.bit_rate_switch = false;
can_message.error_state_indicator = false;
#endif
if (this->read_message(&can_message) != canbus::ERROR_OK) {
break;
}
const uint8_t size = can_message.payload_length();
if (can_message.use_extended_id) {
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") extended can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
can_message.can_id, size);
} else {
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") std can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
can_message.can_id, size);
}
std::vector<uint8_t> data;
// show data received
for (int i = 0; i < std::min(can_message.can_data_length_code, CAN_MAX_DATA_LENGTH); i++) {
ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]);
data.push_back(can_message.data[i]);
}
this->callback_manager_(can_message.can_id, can_message.use_extended_id, can_message.remote_transmission_request,
data);
// fire all triggers
for (auto *trigger : this->triggers_) {
if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
(trigger->use_extended_id_ == can_message.use_extended_id) &&
(!trigger->remote_transmission_request_.has_value() ||
trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) {
trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request);
}
}
ESP_LOGV_HEX(TAG, can_message.data, size, CAN_FRAME_DATA_LENGTH, " data=%s");
this->frame_callbacks_.call(can_message);
}
if (message_counter == this->max_frames_per_loop_) {
+198 -45
View File
@@ -2,9 +2,16 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/optional.h"
#include <algorithm>
#include <cinttypes>
#include <cstddef>
#include <cstring>
#include <initializer_list>
#include <type_traits>
#include <vector>
namespace esphome::canbus {
@@ -39,28 +46,128 @@ enum CanSpeed : uint8_t {
CAN_250KBPS,
CAN_500KBPS,
CAN_800KBPS,
CAN_1000KBPS
CAN_1000KBPS,
#ifdef USE_CANBUS_FD
// Data phase rates of a CAN FD controller
CAN_2000KBPS,
CAN_3000KBPS,
CAN_4000KBPS,
CAN_5000KBPS,
CAN_6000KBPS,
CAN_7000KBPS,
CAN_8000KBPS,
#endif
};
class CanbusTrigger;
template<typename... Ts> class CanbusSendAction;
/* CAN payload length definitions according to ISO 11898-1 */
static const uint8_t CAN_MAX_DATA_LENGTH = 8;
static constexpr uint8_t CAN_MAX_DATA_LENGTH = 8;
#ifdef USE_CANBUS_FD
static constexpr uint8_t CAN_FD_MAX_DATA_LENGTH = 64;
/// Payload bytes a CanFrame holds: one CAN FD frame when a platform that supports it is configured
static constexpr uint8_t CAN_FRAME_DATA_LENGTH = CAN_FD_MAX_DATA_LENGTH;
#else
/// Payload bytes a CanFrame holds: one classic frame
static constexpr uint8_t CAN_FRAME_DATA_LENGTH = CAN_MAX_DATA_LENGTH;
#endif
static const uint8_t CAN_ERROR_PASSIVE_THRESHOLD = 128;
static constexpr uint32_t CAN_STANDARD_ID_MAX = 0x7FF;
static constexpr uint32_t CAN_EXTENDED_ID_MAX = 0x1FFFFFFF;
/*
Can Frame describes a normative CAN Frame
The RTR = Remote Transmission Request is implemented in every CAN controller but rarely used
So currently the flag is passed to and from the hardware but currently ignored to the user application.
*/
static constexpr uint8_t CAN_ERROR_PASSIVE_THRESHOLD = 128;
#ifdef USE_CANBUS_FD
/// Returned by the length conversions for a value that cannot be represented
static constexpr uint8_t CAN_INVALID_DLC = UINT8_MAX;
/// Payload length of each CAN FD DLC; a classic frame carries min(DLC, 8) bytes
static constexpr uint8_t CAN_FD_DLC_COUNT = 16;
static constexpr uint8_t CAN_FD_DLC_LENGTHS[CAN_FD_DLC_COUNT] PROGMEM = {0, 1, 2, 3, 4, 5, 6, 7,
8, 12, 16, 20, 24, 32, 48, 64};
/// CAN_FD_DLC_LENGTHS[dlc] for dlc below CAN_FD_DLC_COUNT; read from flash at run time
constexpr uint8_t can_fd_dlc_length(uint8_t dlc) {
return std::is_constant_evaluated() ? CAN_FD_DLC_LENGTHS[dlc] : progmem_read_byte(&CAN_FD_DLC_LENGTHS[dlc]);
}
/// Payload length of a CAN FD DLC, or CAN_INVALID_DLC above 15
constexpr uint8_t can_dlc_to_len(uint8_t dlc) {
return dlc < CAN_FD_DLC_COUNT ? can_fd_dlc_length(dlc) : CAN_INVALID_DLC;
}
/// CAN FD DLC for a payload length. With pad, the next longer length is used and the caller zero fills
/// the extra bytes; otherwise a length CAN FD cannot carry returns CAN_INVALID_DLC.
constexpr uint8_t can_len_to_dlc(size_t len, bool pad) {
for (uint8_t dlc = 0; dlc < CAN_FD_DLC_COUNT; dlc++) {
const uint8_t length = can_fd_dlc_length(dlc);
if (length >= len)
return (length == len || pad) ? dlc : CAN_INVALID_DLC;
}
return CAN_INVALID_DLC;
}
#else
/// Payload length of a classic DLC; 9 to 15 still carry 8 bytes
constexpr uint8_t can_dlc_to_len(uint8_t dlc) { return std::min(dlc, CAN_MAX_DATA_LENGTH); }
#endif
/// Frame payload without heap; converts to and from std::vector<uint8_t> for older lambdas.
class CanPayload : public StaticVector<uint8_t, CAN_FRAME_DATA_LENGTH> {
public:
using StaticVector<uint8_t, CAN_FRAME_DATA_LENGTH>::StaticVector;
// Not = default, so value-init does not zero the buffer
CanPayload() {}
// Every constructor that can be given more than one frame records that it cut the rest
template<typename InputIt> CanPayload(InputIt first, InputIt last) {
for (; first != last; ++first) {
if (this->size() == CAN_FRAME_DATA_LENGTH) {
this->truncated_ = true;
break;
}
this->push_back(*first);
}
}
CanPayload(std::initializer_list<uint8_t> data) : CanPayload(data.begin(), data.end()) {}
CanPayload(const std::vector<uint8_t> &data) // NOLINT(google-explicit-constructor)
: CanPayload(data.begin(), data.end()) {}
operator std::vector<uint8_t>() const { return {this->begin(), this->end()}; } // NOLINT(google-explicit-constructor)
/// Whether it was built from more bytes than one frame can carry
bool truncated() const { return this->truncated_; }
protected:
bool truncated_{false};
};
/// One CAN frame as passed between the bus and its driver. The CAN FD flags come after data, so code that
/// brace-initializes a classic frame keeps working.
struct CanFrame {
bool use_extended_id = false;
bool remote_transmission_request = false;
uint32_t can_id; /* 29 or 11 bit CAN_ID */
uint8_t can_data_length_code; /* frame payload length in byte (0 .. CAN_MAX_DATA_LENGTH) */
uint8_t data[CAN_MAX_DATA_LENGTH] __attribute__((aligned(8)));
uint32_t can_id; /* 29 or 11 bit CAN_ID; drivers clear bits 29-31 (on_frame relies on it) */
uint8_t can_data_length_code; /* DLC as on the wire, not a byte count (see payload_length()) */
uint8_t data[CAN_FRAME_DATA_LENGTH] __attribute__((aligned(8)));
#ifdef USE_CANBUS_FD
bool fd = false; /* CAN FD frame; it cannot be a remote transmission request */
bool bit_rate_switch = false; /* CAN FD only: data phase sent at the data bit rate */
bool error_state_indicator = false; /* CAN FD only: received transmitter was error passive */
#endif
/// Payload bytes for the DLC: at most 8 for a classic frame, 0 for an invalid CAN FD DLC
uint8_t payload_length() const {
#ifdef USE_CANBUS_FD
if (this->fd) {
const uint8_t len = can_dlc_to_len(this->can_data_length_code);
return len == CAN_INVALID_DLC ? 0 : len;
}
#endif
return std::min(this->can_data_length_code, CAN_MAX_DATA_LENGTH);
}
CanPayload payload() const {
CanPayload out;
std::memcpy(out.data(), this->data, CAN_FRAME_DATA_LENGTH);
out.resize(this->payload_length());
return out;
}
};
enum CanEventFlags : uint8_t {
@@ -91,8 +198,18 @@ class Canbus : public Component {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override;
/// Validates the frame and passes it to the driver.
canbus::Error send_data(const CanFrame &frame);
#ifdef USE_CANBUS_FD
/// Sends up to 8 bytes as a classic frame, longer data is cut. An fd frame carries up to 64 bytes; pad_data
/// zero fills a length CAN FD cannot carry up to the next one.
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, const uint8_t *data,
size_t len, bool fd = false, bool bit_rate_switch = false, bool pad_data = false);
#else
/// Sends up to 8 bytes as a classic frame, longer data is cut.
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, const uint8_t *data,
size_t len);
#endif
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const std::vector<uint8_t> &data) {
return this->send_data(can_id, use_extended_id, remote_transmission_request, data.data(), data.size());
@@ -105,9 +222,28 @@ class Canbus : public Component {
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
void set_max_frames_per_loop(uint32_t max_frames_per_loop) { this->max_frames_per_loop_ = max_frames_per_loop; }
#ifdef USE_CANBUS_FD
// Defaults for canbus.send actions only, not for send_data()
void set_default_fd(bool default_fd) { this->default_fd_ = default_fd; }
void set_default_bit_rate_switch(bool default_bit_rate_switch) {
this->default_bit_rate_switch_ = default_bit_rate_switch;
}
void set_default_pad_data(bool default_pad_data) { this->default_pad_data_ = default_pad_data; }
void set_supports_fd(bool supports_fd) { this->supports_fd_ = supports_fd; }
#endif
/// Whether this bus sends and receives CAN FD frames; a classic bus can share a build with a CAN FD one
bool supports_fd() const {
#ifdef USE_CANBUS_FD
return this->supports_fd_;
#else
return false;
#endif
}
virtual CanStatus get_status() { return {}; }
void add_trigger(CanbusTrigger *trigger);
/// The frame is only valid during the call.
template<typename F> void add_frame_callback(F &&callback) { this->frame_callbacks_.add(std::forward<F>(callback)); }
/**
* Add a callback to be called when a CAN message is received. All received messages
* are passed to the callback without filtering.
@@ -118,19 +254,30 @@ class Canbus : public Component {
* - rtr If this is a remote transmission request
* - data The message data
*/
template<typename F> void add_callback(F &&callback) { this->callback_manager_.add(std::forward<F>(callback)); }
template<typename F>
ESPDEPRECATED("Use add_frame_callback(), which does not copy the payload. Removed in 2027.5.0.", "2026.11.0")
void add_callback(F &&callback) {
this->frame_callbacks_.add([cb = std::forward<F>(callback)](const CanFrame &frame) mutable {
cb(frame.can_id, frame.use_extended_id, frame.remote_transmission_request,
std::vector<uint8_t>(frame.data, frame.data + frame.payload_length()));
});
}
protected:
template<typename... Ts> friend class CanbusSendAction;
std::vector<CanbusTrigger *> triggers_{};
uint32_t can_id_;
uint32_t max_frames_per_loop_{50}; // Keep in sync with DEFAULT_MAX_FRAMES_PER_LOOP in __init__.py
CanSpeed bit_rate_{CAN_125KBPS}; // Keep in sync with DEFAULT_BIT_RATE in __init__.py
bool use_extended_id_{false};
#ifdef USE_CANBUS_FD
bool default_fd_{false};
bool default_bit_rate_switch_{false};
bool default_pad_data_{false};
bool supports_fd_{false};
#endif
uint8_t events_to_log_{0};
bool bus_off_{false};
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{};
CallbackManager<void(const CanFrame &frame)> frame_callbacks_{};
// interval in which to check for new can bus events (bus-off, passive, active)
static constexpr uint32_t EVENT_CHECK_INTERVAL_MS = 100;
@@ -150,6 +297,10 @@ class Canbus : public Component {
#endif
void log_events_(CanEventFlags events, uint32_t now);
/// Logs and sends a frame that is already valid.
canbus::Error send_frame_(const CanFrame &frame);
/// For a canbus.send lambda: a CAN FD frame that did not fit is refused, a classic one is cut to 8 bytes.
bool lambda_payload_fits_(const CanPayload &payload, [[maybe_unused]] bool fd);
virtual bool setup_internal() = 0;
virtual Error send_message(struct CanFrame *frame) = 0;
virtual Error read_message(struct CanFrame *frame) = 0;
@@ -157,7 +308,12 @@ class Canbus : public Component {
};
template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, public Parented<Canbus> {
TEMPLATABLE_BYTES(data)
TEMPLATABLE_BUFFER(data, CanPayload)
#ifdef USE_CANBUS_FD
TEMPLATABLE_VALUE(bool, fd)
TEMPLATABLE_VALUE(bool, bit_rate_switch)
TEMPLATABLE_VALUE(bool, pad_data)
#endif
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
@@ -171,11 +327,30 @@ template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, pu
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
auto use_extended_id =
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
this->data_.template visit<CAN_MAX_DATA_LENGTH>(
[&](const uint8_t *data, size_t len) {
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data, len);
},
x...);
#ifdef USE_CANBUS_FD
const bool fd = this->fd_.value_or(x..., this->parent_->default_fd_);
const bool bit_rate_switch = this->bit_rate_switch_.value_or(x..., fd && this->parent_->default_bit_rate_switch_);
const bool pad_data = this->pad_data_.value_or(x..., this->parent_->default_pad_data_);
#endif
#ifndef USE_CANBUS_FD
const bool fd = false;
#endif
auto send = [&](const uint8_t *data, size_t len) {
#ifdef USE_CANBUS_FD
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data, len, fd,
bit_rate_switch, pad_data);
#else
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data, len);
#endif
};
if (this->data_.is_static()) {
this->data_.template visit<CAN_FRAME_DATA_LENGTH>(send, x...);
return;
}
// The conversion from a lambda's vector cuts it to one frame, so the length it had is checked here
const CanPayload payload = this->data_.value(x...);
if (this->parent_->lambda_payload_fits_(payload, fd))
send(payload.data(), payload.size());
}
protected:
@@ -184,26 +359,4 @@ template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, pu
bool remote_transmission_request_{false};
};
class CanbusTrigger final : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
friend class Canbus;
public:
explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
const bool use_extended_id)
: parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id) {};
void set_remote_transmission_request(bool remote_transmission_request) {
this->remote_transmission_request_ = remote_transmission_request;
}
void setup() override { this->parent_->add_trigger(this); }
protected:
Canbus *parent_;
uint32_t can_id_;
uint32_t can_id_mask_;
bool use_extended_id_;
optional<bool> remote_transmission_request_{};
};
} // namespace esphome::canbus
+5 -8
View File
@@ -72,15 +72,12 @@ void CAP1188Component::dump_config() {
void CAP1188Component::loop() {
uint8_t touched = 0;
this->read_register(CAP1188_SENSOR_INPUT_STATUS, &touched, 1);
// On a failed read the channels keep their state instead of reporting a release
if (!this->status_comm_result(this->read_byte(CAP1188_SENSOR_INPUT_STATUS, &touched)))
return;
if (touched) {
uint8_t data = 0;
this->read_register(CAP1188_MAIN, &data, 1);
data = data & ~CAP1188_MAIN_INT;
this->write_register(CAP1188_MAIN, &data, 1);
}
if (touched)
this->update_register(CAP1188_MAIN, CAP1188_MAIN_INT, 0);
for (auto *channel : this->channels_) {
channel->process(touched);
+1 -3
View File
@@ -24,10 +24,8 @@ void CH423Component::setup() {
if (success && !(this->sys_params_ & CH423_SYS_IO_OE)) {
success = this->read_inputs_();
}
if (!success) {
this->mark_failed(LOG_STR("CH423 not detected"));
if (!this->setup_check(success, LOG_STR("CH423 not detected")))
return;
}
ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
+4 -7
View File
@@ -23,10 +23,9 @@ uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
void CM1106Component::setup() {
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(
this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))))
return;
}
if (this->abc_boot_logic_ != CM1106_ABC_NONE) {
this->abc_set_(this->abc_boot_logic_ == CM1106_ABC_ENABLED);
@@ -76,8 +75,7 @@ void CM1106Component::update() {
void CM1106Component::calibrate_zero(uint16_t ppm) {
uint8_t cmd[6];
memcpy(cmd, C_M1106_CMD_SET_CO2_CALIB, sizeof(cmd));
cmd[3] = ppm >> 8;
cmd[4] = ppm & 0xFF;
put_be16(cmd + 3, ppm);
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_CO2_CALIB_RESPONSE)) {
ESP_LOGD(TAG, "Successfully calibrated sensor to %uppm", ppm);
}
@@ -89,8 +87,7 @@ void CM1106Component::abc_set_(bool enabled) {
memcpy(cmd, C_M1106_CMD_SET_ABC_STATUS, sizeof(cmd));
cmd[4] = enabled ? CM1106_ABC_FLAG_ENABLE : CM1106_ABC_FLAG_DISABLE;
cmd[5] = this->abc_cycle_;
cmd[6] = this->abc_baseline_ >> 8;
cmd[7] = this->abc_baseline_ & 0xFF;
put_be16(cmd + 6, this->abc_baseline_);
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_ABC_STATUS_RESPONSE)) {
ESP_LOGD(TAG, "Successfully set ABC status");
}
+2
View File
@@ -108,7 +108,9 @@ CONF_VOC_INDEX = "voc_index"
CONF_VOLUME_INCREMENT = "volume_increment"
CONF_VOLUME_INITIAL = "volume_initial"
CONF_VOLUME_MAX = "volume_max"
CONF_VOLUME_MAX_DB = "volume_max_db"
CONF_VOLUME_MIN = "volume_min"
CONF_VOLUME_MIN_DB = "volume_min_db"
CONF_WINDOW = "window"
ICON_CURRENT_DC = "mdi:current-dc"
+5 -3
View File
@@ -189,7 +189,8 @@ void CSE7766Component::parse_data_() {
if (have_power && this->reactive_power_sensor_ != nullptr) {
const float q_squared = apparent_power * apparent_power - power * power;
if (q_squared < 0.0f) {
ESP_LOGD(TAG, "Impossible reactive power: S^2-P^2 is negative (%.4f)", q_squared);
// From the same noise as a power factor above 1 (below)
ESP_LOGV(TAG, "Impossible reactive power: S^2-P^2 is negative (%.4f)", q_squared);
this->reactive_power_sensor_->publish_state(0.0f);
} else {
this->reactive_power_sensor_->publish_state(std::sqrt(q_squared));
@@ -200,8 +201,9 @@ void CSE7766Component::parse_data_() {
if (apparent_power > 0) {
pf = power / apparent_power;
if (pf > 1) {
// Noise near the chip's resolution limit, seen during inrush, pushes the ratio past unity
ESP_LOGD(TAG, "Power factor %.4f above 1, clamped", pf);
// Noise near the chip's resolution limit pushes the ratio past unity: during inrush, and on
// nearly every frame of a steady resistive load, so it is logged only at verbose level
ESP_LOGV(TAG, "Power factor %.4f above 1, clamped", pf);
pf = 1.0f;
}
} else if (apparent_power == 0 && power == 0) {
@@ -63,11 +63,9 @@ void CST226Touchscreen::continue_setup_() {
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
buffer[0] = 0xD1;
if (this->write_register16(0xD1, buffer, 1) != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Write byte to 0xD1 failed");
this->mark_failed();
if (!this->setup_check(this->write_register16(0xD1, buffer, 1) == i2c::ERROR_OK,
LOG_STR("Write byte to 0xD1 failed")))
return;
}
delay(10);
if (this->read16_(0xD204, buffer, 4)) {
uint16_t chip_id = buffer[2] + (buffer[3] << 8);
+2 -1
View File
@@ -1,4 +1,5 @@
#include "d01.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
// uart specification for d01 sensor from https://manuals.plus/ae/1005006417362019:
@@ -31,7 +32,7 @@ void D01SensorComponent::loop() {
continue;
}
this->read_array(buf, 4);
uint8_t sum = (buf[0] + buf[1] + buf[2]) & 0x7F;
uint8_t sum = sum_bytes(buf, 3) & 0x7F;
if (sum != buf[3]) {
ESP_LOGW(TAG, "checksum mismatch");
continue;
+5 -10
View File
@@ -22,23 +22,18 @@ void DAC7678Output::setup() {
ESP_LOGV(TAG, "Resetting device");
// Reset device
if (!this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000)) {
this->mark_failed(LOG_STR("Reset failed"));
if (!this->setup_check(this->write_byte_16(DAC7678_REG_SOFTWARE_RESET, 0x0000), LOG_STR("Reset failed")))
return;
} else {
ESP_LOGV(TAG, "Reset succeeded");
}
ESP_LOGV(TAG, "Reset succeeded");
delayMicroseconds(1000);
// Set internal reference
if (this->internal_reference_) {
if (!this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4)) {
this->mark_failed(LOG_STR("Set internal reference failed"));
if (!this->setup_check(this->write_byte_16(DAC7678_REG_INTERNAL_REF_0, 1 << 4),
LOG_STR("Set internal reference failed")))
return;
} else {
ESP_LOGV(TAG, "Internal reference enabled");
}
ESP_LOGV(TAG, "Internal reference enabled");
}
}
+1 -3
View File
@@ -37,9 +37,7 @@ void DaikinClimate::transmit_state() {
this->set_presets_(remote_state);
remote_state[21] = this->operation_mode_();
remote_state[22] = this->temperature_();
uint16_t fan_speed = this->fan_speed_();
remote_state[24] = fan_speed >> 8;
remote_state[25] = fan_speed & 0xff;
put_be16(remote_state + 24, this->fan_speed_());
remote_state[7] = sum_bytes(remote_state, 7);
remote_state[34] = sum_bytes(remote_state + 16, 18);
+2 -3
View File
@@ -4,6 +4,7 @@
#include "esphome/components/remote_base/remote_base.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
namespace esphome::daikin_arc {
@@ -89,9 +90,7 @@ void DaikinArcClimate::transmit_state() {
remote_header[9] |= 0x10;
this->last_humidity_ = remote_state[7];
}
uint16_t fan_speed = this->fan_speed_();
remote_state[8] = fan_speed >> 8;
remote_state[9] = fan_speed & 0xff;
put_be16(remote_state + 8, this->fan_speed_());
// Calculate checksum
for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
Loaded 100 of 524 files, more files were not shown because too many files have changed in this diff. Show more