Merge remote-tracking branch 'origin/dev' into sx1509-max6956-apply-outputs

# Conflicts:
#	esphome/components/sx1509/sx1509.h
This commit is contained in:
J. Nick Koston committed 2026-11-03 21:55:39 -06:00
commit 39966b6182
842 files changed
+30368 -5413

No files matched your search

+26
View File
@@ -70,6 +70,21 @@ await automation.build_callback_automation(
`build_parent_callback_automation`, `build_trigger_callback`, `build_callback_automations` and
`build_trigger_automations` cover the other shapes; see their docstrings in `esphome/automation.py`.
When every action in a callback automation is inline (see Actions below), no `Automation` or action objects
are generated: the callback calls one generated function, at the same RAM cost as a hand-written lambda. A
custom forwarder blocks this unless its filter is also given as `params`, `forward` and `when`, so always pass
them together:
```python
automation.CallbackAutomation(
CONF_ON_IDLE,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(State.IDLE),
params=[(State, "state")],
forward=[],
when=f"state == {State.IDLE}",
)
```
**C++: no trigger class needed.** The callback registration method must be templatized to accept both
`std::function` and lightweight forwarder structs (which avoid heap allocation):
```cpp
@@ -101,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
@@ -187,6 +204,15 @@ payload passes `automation.ApplyBytes(CONF_DATA, "send({})")` in the same call;
flash and a lambda's vector is forwarded as data and size. `TEMPLATABLE_VALUE` with `cg.templatable` stays
for actions whose `play()` has real logic beyond forwarding values.
Adjacent `register_apply_action` and `lambda:` actions in a list merge into one generated function and one
object, which saves 12 to 16 bytes of RAM per merged action. An action with its own C++ class ends the merged
run, so prefer `register_apply_action` wherever the action allows it. Because of this, `build_action_list` can
return fewer objects than the config has entries. A builder that wraps `apply_action_builder` may do other work
first, but must return its result unchanged and not use it. A builder that must generate its own statements, and
is registered with `automation.ApplyAction`, can call `apply_statements_action` and still merge; `script.execute`
is an example. `if`, `choose`, `while` and `repeat` whose bodies are all inline become C++ control flow in the
same function, and `register_apply_condition` and `lambda:` conditions become plain expressions there.
## Conditions
```cpp
+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
+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.
+4 -9
View File
@@ -33,8 +33,6 @@ esphome/components/airthings_wave_base/* @jeromelaban @ncareau
esphome/components/airthings_wave_mini/* @ncareau
esphome/components/airthings_wave_plus/* @jeromelaban @precurse
esphome/components/airton/* @Procsiab
esphome/components/airton/select/* @Procsiab
esphome/components/airton/switch/* @Procsiab
esphome/components/airton_serial/* @guiguid
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/alpha3/* @jan-hofmeier
@@ -156,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
@@ -262,14 +261,7 @@ esphome/components/growatt_solar/* @leeuwte
esphome/components/gsl3670/* @clydebarrow
esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
esphome/components/haier/button/* @paveldn
esphome/components/haier/select/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
esphome/components/haier_tundra/* @fauxpark
esphome/components/haier_tundra/button/* @fauxpark
esphome/components/haier_tundra/switch/* @fauxpark
esphome/components/haier_water_heater/* @TheLexus
esphome/components/havells_solar/* @sourabhjaiswal
@@ -303,6 +295,7 @@ esphome/components/honeywellabp2_i2c/* @jpfaff
esphome/components/host/* @clydebarrow @esphome/core
esphome/components/host/time/* @clydebarrow
esphome/components/hrxl_maxsonar_wr/* @netmikey
esphome/components/ht16k33_char/* @ilikecake
esphome/components/hte501/* @Stock-M
esphome/components/http_request/ota/* @oarcher
esphome/components/http_request/update/* @jesserockz
@@ -384,6 +377,7 @@ esphome/components/ltr_als_ps/* @latonita
esphome/components/lvgl/* @clydebarrow
esphome/components/m5stack_4relay/* @KoenBreeman
esphome/components/m5stack_8angle/* @rnauber
esphome/components/m5stack_unit_bldc/* @lboue
esphome/components/mapping/* @clydebarrow
esphome/components/matrix_keypad/* @ssieb
esphome/components/max17043/* @blacknell
@@ -481,6 +475,7 @@ esphome/components/output/* @esphome/core
esphome/components/packet_interface/* @clydebarrow
esphome/components/packet_interface/packet_transport/* @clydebarrow
esphome/components/packet_transport/* @clydebarrow
esphome/components/panasonic_ir/* @duckle29
esphome/components/pca6416a/* @Mat931
esphome/components/pca9505/* @ajfriesen @ssieb
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
+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
+706 -118
View File
File diff suppressed because it is too large. Load diff
@@ -14,9 +14,10 @@ enum SaturationVaporPressureEquation {
/// This class implements calculation of absolute humidity from temperature and relative humidity.
class AbsoluteHumidityComponent final : public sensor::Sensor, public Component {
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
public:
void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_equation(SaturationVaporPressureEquation equation) { this->equation_ = equation; }
void setup() override;
@@ -48,9 +49,6 @@ class AbsoluteHumidityComponent final : public sensor::Sensor, public Component
*/
static float vapor_density(float es, float hr, float ta);
sensor::Sensor *temperature_sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
float temperature_{NAN};
float humidity_{NAN};
SaturationVaporPressureEquation equation_;
@@ -22,7 +22,6 @@ void AddressableLightDisplay::update() {
void AddressableLightDisplay::display() {
bool dirty = false;
uint8_t old_r, old_g, old_b, old_w;
Color *c;
for (uint32_t offset = 0; offset < this->addressable_light_buffer_.size(); offset++) {
@@ -32,16 +31,11 @@ void AddressableLightDisplay::display() {
// Track the original values for the pixel view. If it has changed updating, then
// we trigger a redraw. Avoiding redraws == avoiding flicker!
old_r = pixel.get_red();
old_g = pixel.get_green();
old_b = pixel.get_blue();
old_w = pixel.get_white();
pixel.set_rgbw(c->r, c->g, c->b, c->w);
const Color old_raw_color = pixel.get_raw();
pixel.set(Color{c->r, c->g, c->b, c->w});
// If the actual value of the pixel changed, then schedule a redraw.
if (pixel.get_red() != old_r || pixel.get_green() != old_g || pixel.get_blue() != old_b ||
pixel.get_white() != old_w) {
if (pixel.get_raw() != old_raw_color) {
dirty = true;
}
}
+33 -57
View File
@@ -53,9 +53,6 @@ CONF_POWER_GAIN = "power_gain"
CONF_NEUTRAL = "neutral"
# Tuple of power channel phases
POWER_PHASES = (CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C)
# Tuple of sensor types that can be configured for power channels
POWER_SENSOR_TYPES = (
CONF_CURRENT,
@@ -66,6 +63,20 @@ POWER_SENSOR_TYPES = (
CONF_FORWARD_ACTIVE_ENERGY,
CONF_REVERSE_ACTIVE_ENERGY,
)
POWER_CALIBRATION_TYPES = (
CONF_CURRENT_GAIN,
CONF_VOLTAGE_GAIN,
CONF_POWER_GAIN,
CONF_PHASE_ANGLE,
)
# Channel config key, ADE7880 setter, sensor types, calibration types
CHANNELS = (
(CONF_PHASE_A, "set_channel_a", POWER_SENSOR_TYPES, POWER_CALIBRATION_TYPES),
(CONF_PHASE_B, "set_channel_b", POWER_SENSOR_TYPES, POWER_CALIBRATION_TYPES),
(CONF_PHASE_C, "set_channel_c", POWER_SENSOR_TYPES, POWER_CALIBRATION_TYPES),
(CONF_NEUTRAL, "set_channel_n", (CONF_CURRENT,), (CONF_CURRENT_GAIN,)),
)
NEUTRAL_CHANNEL_SCHEMA = cv.Schema(
{
@@ -191,7 +202,7 @@ def prefix_sensor_name(
def process_channel_sensors(
config: ConfigType, channel_key: str, sensor_types: tuple
config: ConfigType, channel_key: str, sensor_types: tuple[str, ...]
) -> None:
"""Process sensors for a channel and prefix their names.
@@ -212,12 +223,8 @@ def process_channel_sensors(
def preprocess_channels(config: ConfigType) -> ConfigType:
"""Preprocess channel configurations to add channel name prefix to sensor names."""
# Process power channels
for channel in POWER_PHASES:
process_channel_sensors(config, channel, POWER_SENSOR_TYPES)
# Process neutral channel
process_channel_sensors(config, CONF_NEUTRAL, (CONF_CURRENT,))
for channel_key, _, sensor_types, _ in CHANNELS:
process_channel_sensors(config, channel_key, sensor_types)
return config
@@ -244,39 +251,20 @@ CONFIG_SCHEMA = cv.All(
)
async def neutral_channel(config: ConfigType) -> MockObj:
async def new_channel(
config: ConfigType,
sensor_types: tuple[str, ...],
calibration_types: tuple[str, ...],
) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
current = config[CONF_CURRENT]
sens = await sensor.new_sensor(current)
cg.add(var.set_current(sens))
sensors = sensor.sub_sensors(config)
for sensor_type in sensor_types:
await sensors(sensor_type, getattr(var, f"set_{sensor_type}"))
cg.add(
var.set_current_gain_calibration(config[CONF_CALIBRATION][CONF_CURRENT_GAIN])
)
return var
async def power_channel(config: ConfigType) -> MockObj:
var = cg.new_Pvariable(config[CONF_ID])
for sensor_type in POWER_SENSOR_TYPES:
if conf := config.get(sensor_type):
sens = await sensor.new_sensor(conf)
cg.add(getattr(var, f"set_{sensor_type}")(sens))
for calib_type in [
CONF_CURRENT_GAIN,
CONF_VOLTAGE_GAIN,
CONF_POWER_GAIN,
CONF_PHASE_ANGLE,
]:
cg.add(
getattr(var, f"set_{calib_type}_calibration")(
config[CONF_CALIBRATION][calib_type]
)
)
calibration = config[CONF_CALIBRATION]
for calib_type in calibration_types:
cg.add(getattr(var, f"set_{calib_type}_calibration")(calibration[calib_type]))
return var
@@ -288,21 +276,9 @@ async def to_code(config: ConfigType) -> None:
await cg.add_pin(var.set_irq1_pin, config[CONF_IRQ1_PIN])
await cg.add_pin(var.set_reset_pin, config.get(CONF_RESET_PIN))
if frequency := config.get(CONF_FREQUENCY):
cg.add(var.set_frequency(frequency))
cg.add(var.set_frequency(config[CONF_FREQUENCY]))
if channel := config.get(CONF_PHASE_A):
chan = await power_channel(channel)
cg.add(var.set_channel_a(chan))
if channel := config.get(CONF_PHASE_B):
chan = await power_channel(channel)
cg.add(var.set_channel_b(chan))
if channel := config.get(CONF_PHASE_C):
chan = await power_channel(channel)
cg.add(var.set_channel_c(chan))
if channel := config.get(CONF_NEUTRAL):
chan = await neutral_channel(channel)
cg.add(var.set_channel_n(chan))
for channel_key, setter, sensor_types, calibration_types in CHANNELS:
if channel := config.get(channel_key):
chan = await new_channel(channel, sensor_types, calibration_types)
cg.add(getattr(var, setter)(chan))
+1 -4
View File
@@ -120,10 +120,7 @@ void AHT10Component::read_data_() {
uint32_t raw_temperature = encode_uint24(data[3] & 0xF, data[4], data[5]);
uint32_t raw_humidity = encode_uint24(data[1], data[2], data[3]) >> 4;
if (this->temperature_sensor_ != nullptr) {
float temperature = ((200.0f * static_cast<float>(raw_temperature)) / AHT10_DIVISOR) - 50.0f;
this->temperature_sensor_->publish_state(temperature);
}
PUBLISH_STATE(this->temperature_sensor_, ((200.0f * static_cast<float>(raw_temperature)) / AHT10_DIVISOR) - 50.0f);
if (this->humidity_sensor_ != nullptr) {
float humidity = raw_humidity == 0 ? NAN : static_cast<float>(raw_humidity) * 100.0f / AHT10_DIVISOR;
if (std::isnan(humidity)) {
@@ -175,11 +175,7 @@ void AirthingsWaveBase::read_battery_(uint8_t *raw_value, uint16_t value_len) {
if ((value_len >= (sizeof(AccessControlPointResponse) + 2)) && (raw_value[0] == ACCESS_CONTROL_POINT_COMMAND)) {
ESP_LOGD(TAG, "Battery received: %u mV", (unsigned int) value->battery);
if (this->battery_voltage_ != nullptr) {
float voltage = value->battery / 1000.0f;
this->battery_voltage_->publish_state(voltage);
}
PUBLISH_STATE(this->battery_voltage_, value->battery / 1000.0f);
// read the battery again at the configured update interval
if (this->battery_update_interval_ != this->update_interval_) {
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from ..climate import (
from .climate import (
CONF_AIRTON_ID,
CONF_VERTICAL_DIRECTION,
VERTICAL_DIRECTIONS,
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_DISPLAY, ENTITY_CATEGORY_CONFIG
from esphome.types import ConfigType
from ..climate import CONF_AIRTON_ID, CONF_SLEEP_MODE, AirtonClimate, airton_ns
from .climate import CONF_AIRTON_ID, CONF_SLEEP_MODE, AirtonClimate, airton_ns
CODEOWNERS = ["@Procsiab"]
@@ -256,16 +256,11 @@ void AirtonSerialClimate::handle_ac_(const uint8_t *data) {
void AirtonSerialClimate::publish_settings_() {
#ifdef USE_SWITCH
if (this->display_switch_ != nullptr)
this->display_switch_->publish_state((this->flags_ & FLAG_DISPLAY) != 0);
if (this->ionizer_switch_ != nullptr)
this->ionizer_switch_->publish_state((this->flags_ & FLAG_IONIZER) != 0);
if (this->sleep_switch_ != nullptr)
this->sleep_switch_->publish_state((this->flags_ & FLAG_SLEEP) != 0);
if (this->eco_switch_ != nullptr)
this->eco_switch_->publish_state((this->flags_ & FLAG_ECO) != 0);
if (this->health_switch_ != nullptr)
this->health_switch_->publish_state((this->flags_ & FLAG_HEALTH) != 0);
PUBLISH_STATE(this->display_switch_, (this->flags_ & FLAG_DISPLAY) != 0);
PUBLISH_STATE(this->ionizer_switch_, (this->flags_ & FLAG_IONIZER) != 0);
PUBLISH_STATE(this->sleep_switch_, (this->flags_ & FLAG_SLEEP) != 0);
PUBLISH_STATE(this->eco_switch_, (this->flags_ & FLAG_ECO) != 0);
PUBLISH_STATE(this->health_switch_, (this->flags_ & FLAG_HEALTH) != 0);
#endif
}
@@ -105,57 +105,32 @@ ALARM_CONTROL_PANEL_CODE_ACTION_SCHEMA = maybe_simple_id(
ALARM_CONTROL_PANEL_CONDITION_SCHEMA = ALARM_CONTROL_PANEL_ACTION_SCHEMA
def _on_state_enter(conf_key: str, state: cg.MockObj) -> automation.CallbackAutomation:
return automation.CallbackAutomation(
conf_key,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(state),
params=[(AlarmControlPanelState, "state")],
forward=[],
when=f"state == {state}",
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_STATE, "add_on_state_callback", forwarder=StateAnyForwarder
),
automation.CallbackAutomation(
CONF_ON_TRIGGERED,
CONF_ON_STATE,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_TRIGGERED
),
),
automation.CallbackAutomation(
CONF_ON_ARMING,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(AlarmControlPanelState.ACP_STATE_ARMING),
),
automation.CallbackAutomation(
CONF_ON_PENDING,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_PENDING
),
),
automation.CallbackAutomation(
CONF_ON_ARMED_HOME,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_ARMED_HOME
),
),
automation.CallbackAutomation(
CONF_ON_ARMED_NIGHT,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_ARMED_NIGHT
),
),
automation.CallbackAutomation(
CONF_ON_ARMED_AWAY,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_ARMED_AWAY
),
),
automation.CallbackAutomation(
CONF_ON_DISARMED,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_DISARMED
),
forwarder=StateAnyForwarder,
params=[(AlarmControlPanelState, "state")],
forward=[],
),
_on_state_enter(CONF_ON_TRIGGERED, AlarmControlPanelState.ACP_STATE_TRIGGERED),
_on_state_enter(CONF_ON_ARMING, AlarmControlPanelState.ACP_STATE_ARMING),
_on_state_enter(CONF_ON_PENDING, AlarmControlPanelState.ACP_STATE_PENDING),
_on_state_enter(CONF_ON_ARMED_HOME, AlarmControlPanelState.ACP_STATE_ARMED_HOME),
_on_state_enter(CONF_ON_ARMED_NIGHT, AlarmControlPanelState.ACP_STATE_ARMED_NIGHT),
_on_state_enter(CONF_ON_ARMED_AWAY, AlarmControlPanelState.ACP_STATE_ARMED_AWAY),
_on_state_enter(CONF_ON_DISARMED, AlarmControlPanelState.ACP_STATE_DISARMED),
automation.CallbackAutomation(CONF_ON_CLEARED, "add_on_cleared_callback"),
automation.CallbackAutomation(CONF_ON_CHIME, "add_on_chime_callback"),
automation.CallbackAutomation(CONF_ON_READY, "add_on_ready_callback"),
+3 -4
View File
@@ -7,20 +7,19 @@
namespace esphome::aqi {
class AQISensor final : public sensor::Sensor, public Component {
SUB_SENSOR(pm_2_5)
SUB_SENSOR(pm_10_0)
public:
void setup() override;
void dump_config() override;
void set_pm_2_5_sensor(sensor::Sensor *sensor) { this->pm_2_5_sensor_ = sensor; }
void set_pm_10_0_sensor(sensor::Sensor *sensor) { this->pm_10_0_sensor_ = sensor; }
void set_aqi_calculation_type(AQICalculatorType type) { this->aqi_calc_type_ = type; }
void set_extended_range(bool extended_range) { this->extended_range_ = extended_range; }
protected:
void calculate_aqi_();
sensor::Sensor *pm_2_5_sensor_{nullptr};
sensor::Sensor *pm_10_0_sensor_{nullptr};
AQICalculatorType aqi_calc_type_{AQI_TYPE};
bool extended_range_{false};
AQICalculatorFactory aqi_calculator_factory_;
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
from esphome.const import DEVICE_CLASS_SWITCH, ICON_WIFI
from esphome.types import ConfigType
from .. import CONF_AT581X_ID, AT581XComponent
from . import CONF_AT581X_ID, AT581XComponent
DEPENDENCIES = ["at581x"]
+262 -211
View File
@@ -1,8 +1,10 @@
#include "atm90e32.h"
#include <algorithm>
#include <cinttypes>
#include <cmath>
#include <cstring>
#include <numbers>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::atm90e32 {
@@ -107,7 +109,6 @@ void ATM90E32Component::update() {
#ifdef USE_TEXT_SENSOR
this->check_phase_status();
this->check_over_current();
this->check_freq_status();
#endif
}
@@ -120,23 +121,19 @@ const char *ATM90E32Component::get_calibration_id_() { return this->instance_id_
void ATM90E32Component::setup() {
this->spi_setup();
this->configure_chip_();
}
void ATM90E32Component::configure_chip_() {
const char *cs = this->get_calibration_id_();
char legacy_cs[GPIO_SUMMARY_MAX_LEN];
this->get_cs_summary_(legacy_cs);
const bool has_distinct_legacy_namespace = strcmp(cs, legacy_cs) != 0;
uint16_t mmode0 = 0x87; // 3P4W 50Hz
uint16_t high_thresh = 0;
uint16_t low_thresh = 0;
if (line_freq_ == 60) {
mmode0 |= 1 << 12; // sets 12th bit to 1, 60Hz
// for freq threshold registers
high_thresh = 6300; // 63.00 Hz
low_thresh = 5700; // 57.00 Hz
} else {
high_thresh = 5300; // 53.00 Hz
low_thresh = 4700; // 47.00 Hz
}
if (current_phases_ == 2) {
@@ -160,8 +157,6 @@ void ATM90E32Component::setup() {
this->write16_(ATM90E32_REGISTER_ZXCONFIG, 0xD654); // Zero crossing (ZX2, ZX1, ZX0) pin config
this->write16_(ATM90E32_REGISTER_MMODE0, mmode0); // Mode Config (frequency set in main program)
this->write16_(ATM90E32_REGISTER_MMODE1, pga_gain_); // PGA Gain Configuration for Current Channels
this->write16_(ATM90E32_REGISTER_FREQHITH, high_thresh); // Frequency high threshold
this->write16_(ATM90E32_REGISTER_FREQLOTH, low_thresh); // Frequency low threshold
this->write16_(ATM90E32_REGISTER_PSTARTTH, 0x1D4C); // All Active Startup Power Threshold - 0.02A/0.00032 = 7500
this->write16_(ATM90E32_REGISTER_QSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
this->write16_(ATM90E32_REGISTER_SSTARTTH, 0x1D4C); // All Reactive Startup Power Threshold - 50%
@@ -249,30 +244,19 @@ void ATM90E32Component::setup() {
}
this->restore_gain_calibrations_();
if (!this->using_saved_calibrations_) {
for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
}
}
} else {
ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs);
for (uint8_t phase = 0; phase < 3; ++phase) {
this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
this->gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_;
this->gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_;
}
this->write_gains_to_registers_();
}
// Sag threshold (78%)
uint16_t sagth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 0.78f);
// Overvoltage threshold (122%)
uint16_t ovth = calculate_voltage_threshold(line_freq_, this->phase_[0].voltage_gain_, 1.22f);
// Write to registers
this->write16_(ATM90E32_REGISTER_SAGTH, sagth);
this->write16_(ATM90E32_REGISTER_OVTH, ovth);
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);
this->write16_(ATM90E32_REGISTER_FREQHITH, this->frequency_high_threshold_);
this->write16_(ATM90E32_REGISTER_FREQLOTH, this->frequency_low_threshold_);
// SAGTH, OVTH and OITH were written with the gains, by write_gains_to_registers_().
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000); // end configuration
}
@@ -658,6 +642,8 @@ void ATM90E32Component::run_gain_calibrations() {
cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
GainCalibration previous_gains[3] = {this->gain_phase_[0], this->gain_phase_[1], this->gain_phase_[2]};
for (uint8_t phase = 0; phase < 3; phase++) {
float measured_voltage = this->get_phase_voltage_avg_(phase);
float measured_current = this->get_phase_current_avg_(phase);
@@ -728,81 +714,66 @@ void ATM90E32Component::run_gain_calibrations() {
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
this->save_gain_calibration_to_memory_();
this->write_gains_to_registers_();
this->verify_gain_writes_();
this->finish_gain_calibration_(previous_gains);
}
void ATM90E32Component::save_gain_calibration_to_memory_() {
template<typename T, typename F>
void ATM90E32Component::finish_calibration_(const LogString *name, const LogString *noun,
ESPPreferenceObject &preference, const T (&values)[3], bool writes_verified,
bool &has_stored, bool &restored, bool (&mismatches)[3], F &&rollback) {
static_assert(sizeof(values) == CALIBRATION_PAYLOAD_SIZE, "Calibration payload size mismatch");
const char *cs = this->get_calibration_id_();
bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
global_preferences->sync();
if (success) {
this->using_saved_calibrations_ = true;
ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration saved to memory.", cs);
} else {
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save gain calibration to memory!", cs);
}
}
void ATM90E32Component::finish_offset_calibration_(const OffsetCalibration (&previous)[3], bool previous_restored,
bool previous_using_saved, OffsetCalibrationType type) {
const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER;
const char *cs = this->get_calibration_id_();
const LogString *name = offset_calibration_name(power_offsets);
OffsetCalibration(*offsets)[3] = power_offsets ? &this->power_offset_phase_ : &this->offset_phase_;
ESPPreferenceObject *preference = power_offsets ? &this->power_offset_pref_ : &this->offset_pref_;
bool *has_stored =
power_offsets ? &this->has_stored_power_offset_calibration_ : &this->has_stored_offset_calibration_;
bool *restored = power_offsets ? &this->restored_power_offset_calibration_ : &this->restored_offset_calibration_;
bool *mismatches = power_offsets ? this->power_offset_calibration_mismatch_ : this->offset_calibration_mismatch_;
const bool writes_verified = this->verify_offset_writes_(type);
bool saved = false;
bool synced = false;
if (writes_verified) {
saved = preference->save(offsets);
synced = global_preferences->sync();
const bool previous_has_stored = has_stored;
const bool previous_restored = restored;
// Raw stored bytes, re-saved on rollback; all zero (the cleared sentinel) when nothing was stored.
uint8_t previous_stored[CALIBRATION_PAYLOAD_SIZE]{};
bool attempt = writes_verified;
if (writes_verified && previous_has_stored && !preference.load(&previous_stored)) {
attempt = false;
ESP_LOGE(TAG,
"[CALIBRATION][%s] Failed to load stored %s calibration "
"values; refusing to overwrite them. "
"Clear stored %s calibrations and retry.",
cs, LOG_STR_ARG(noun), LOG_STR_ARG(noun));
}
if (writes_verified && saved && synced) {
this->using_saved_calibrations_ = true;
*has_stored = true;
*restored = true;
for (uint8_t phase = 0; phase < 3; phase++)
mismatches[phase] = false;
bool persisted = false;
if (attempt) {
const bool saved = preference.save(&values);
const bool synced = global_preferences->sync();
persisted = saved && synced;
}
if (persisted) {
has_stored = true;
restored = true;
for (bool &mismatch : mismatches)
mismatch = false;
ESP_LOGI(TAG, "[CALIBRATION][%s] %s calibration saved to memory. %s calibration completed and verified.", cs,
LOG_STR_ARG(name), LOG_STR_ARG(name));
return;
}
if (writes_verified) {
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save %s calibration to memory!", cs, LOG_STR_ARG(name));
if (attempt) {
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save %s calibration to memory!", cs, LOG_STR_ARG(noun));
}
for (uint8_t phase = 0; phase < 3; phase++) {
this->write_offsets_to_registers_(phase, previous[phase].first_offset, previous[phase].second_offset, type);
}
const bool rollback_verified = this->verify_offset_writes_(type);
const bool rollback_verified = rollback();
bool rollback_persisted = false;
if (writes_verified) {
OffsetCalibration rollback[3]{};
prepare_offset_rollback(previous, previous_restored, rollback);
const bool rollback_saved = preference->save(&rollback);
if (attempt) {
const bool rollback_saved = preference.save(&previous_stored);
const bool rollback_synced = global_preferences->sync();
rollback_persisted = rollback_saved && rollback_synced;
if (!rollback_saved || !rollback_synced) {
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to persist restored %s calibration values!", cs, LOG_STR_ARG(name));
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to persist restored %s calibration values!", cs, LOG_STR_ARG(noun));
}
}
*restored = previous_restored;
if (rollback_persisted)
*has_stored = previous_restored;
this->using_saved_calibrations_ = previous_using_saved;
restored = previous_restored && rollback_verified;
has_stored = previous_has_stored;
if (!rollback_verified) {
for (bool &mismatch : mismatches)
mismatch = false;
ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration failed; rollback readback verification failed.", cs,
LOG_STR_ARG(name));
return;
@@ -810,6 +781,33 @@ void ATM90E32Component::finish_offset_calibration_(const OffsetCalibration (&pre
ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration failed; previous values restored.", cs, LOG_STR_ARG(name));
}
void ATM90E32Component::finish_gain_calibration_(const GainCalibration (&previous)[3]) {
this->finish_calibration_(LOG_STR("Gain"), LOG_STR("gain"), this->gain_calibration_pref_, this->gain_phase_,
this->verify_gain_writes_(), this->has_stored_gain_calibration_,
this->restored_gain_calibration_, this->gain_calibration_mismatch_, [this, &previous]() {
for (uint8_t phase = 0; phase < 3; phase++)
this->gain_phase_[phase] = previous[phase];
this->write_gains_to_registers_();
return this->verify_gain_writes_();
});
}
void ATM90E32Component::finish_offset_calibration_(const OffsetCalibration (&previous)[3], OffsetCalibrationType type) {
const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER;
this->finish_calibration_(
offset_calibration_name(power_offsets), offset_calibration_name(power_offsets),
power_offsets ? this->power_offset_pref_ : this->offset_pref_,
power_offsets ? this->power_offset_phase_ : this->offset_phase_, this->verify_offset_writes_(type),
power_offsets ? this->has_stored_power_offset_calibration_ : this->has_stored_offset_calibration_,
power_offsets ? this->restored_power_offset_calibration_ : this->restored_offset_calibration_,
power_offsets ? this->power_offset_calibration_mismatch_ : this->offset_calibration_mismatch_,
[this, &previous, type]() {
for (uint8_t phase = 0; phase < 3; phase++)
this->write_offsets_to_registers_(phase, previous[phase].first_offset, previous[phase].second_offset, type);
return this->verify_offset_writes_(type);
});
}
void ATM90E32Component::run_offset_calibrations() {
const char *cs = this->get_calibration_id_();
if (!this->enable_offset_calibration_) {
@@ -826,8 +824,6 @@ void ATM90E32Component::run_offset_calibrations() {
ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs);
OffsetCalibration previous_offsets[3] = {this->offset_phase_[0], this->offset_phase_[1], this->offset_phase_[2]};
const bool previous_restored = this->restored_offset_calibration_;
const bool previous_using_saved = this->using_saved_calibrations_;
for (uint8_t phase = 0; phase < 3; phase++) {
int16_t voltage_offset = calibrate_offset(phase, true);
@@ -842,8 +838,7 @@ void ATM90E32Component::run_offset_calibrations() {
ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs);
this->finish_offset_calibration_(previous_offsets, previous_restored, previous_using_saved,
OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT);
this->finish_offset_calibration_(previous_offsets, OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT);
}
void ATM90E32Component::run_power_offset_calibrations() {
@@ -864,8 +859,6 @@ void ATM90E32Component::run_power_offset_calibrations() {
OffsetCalibration previous_offsets[3] = {this->power_offset_phase_[0], this->power_offset_phase_[1],
this->power_offset_phase_[2]};
const bool previous_restored = this->restored_power_offset_calibration_;
const bool previous_using_saved = this->using_saved_calibrations_;
for (uint8_t phase = 0; phase < 3; ++phase) {
int16_t active_offset = calibrate_power_offset(phase, false);
@@ -879,8 +872,7 @@ void ATM90E32Component::run_power_offset_calibrations() {
}
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
this->finish_offset_calibration_(previous_offsets, previous_restored, previous_using_saved,
OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER);
this->finish_offset_calibration_(previous_offsets, OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER);
}
void ATM90E32Component::write_gains_to_registers_() {
@@ -891,9 +883,56 @@ void ATM90E32Component::write_gains_to_registers_() {
this->write16_(current_gain_registers[phase], this->gain_phase_[phase].current_gain);
}
this->write_thresholds_();
this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);
}
void ATM90E32Component::write_thresholds_() {
static constexpr uint16_t LEGACY_OITH = 0xC000;
struct GainRange {
uint16_t min{UINT16_MAX};
uint16_t max{0};
};
// Reads back the gain registers of the configured phases; {0, 0} when none are configured.
auto read_gain_range = [this](const bool (&configured)[3], const uint16_t (&registers)[3]) {
GainRange range;
for (uint8_t phase = 0; phase < 3; phase++) {
if (configured[phase]) {
const uint16_t gain = this->read16_(registers[phase]);
range.min = std::min(range.min, gain);
range.max = std::max(range.max, gain);
}
}
if (range.max == 0)
range.min = 0;
return range;
};
const GainRange voltage = read_gain_range(this->has_config_voltage_gain_, this->voltage_gain_registers);
const GainRange current = read_gain_range(this->has_config_current_gain_, this->current_gain_registers);
const bool current_configured = !std::isnan(this->threshold_current_peak_a_);
#ifdef USE_TEXT_SENSOR
this->use_voltage_threshold_flags_ = voltage.min == voltage.max;
if (!this->use_voltage_threshold_flags_) {
ESP_LOGW(TAG, "Phase voltage gains differ; using per-phase RMS voltage alarms");
}
// The shared OITH is encoded at the highest gain, so a lower-gain phase would flag below current_peak.
this->use_current_threshold_flags_ = current.min == current.max;
if (!this->use_current_threshold_flags_ && current_configured) {
ESP_LOGW(TAG, "Phase current gains differ; using per-phase RMS over-current alarms");
}
#endif
const uint16_t sagth = calculate_voltage_threshold_from_volts(this->threshold_voltage_sag_v_, voltage.min);
const uint16_t ovth = calculate_voltage_threshold_from_volts(this->threshold_voltage_peak_v_, voltage.max);
const uint16_t oith = current_configured
? calculate_current_threshold_from_amps(this->threshold_current_peak_a_, current.max)
: LEGACY_OITH;
ESP_LOGD(TAG, "Thresholds: SAGTH=0x%04" PRIX16 ", OVTH=0x%04" PRIX16 ", OITH=0x%04" PRIX16, sagth, ovth, oith);
this->write16_(ATM90E32_REGISTER_SAGTH, sagth);
this->write16_(ATM90E32_REGISTER_OVTH, ovth);
this->write16_(ATM90E32_REGISTER_OITH, oith);
}
void ATM90E32Component::write_offsets_to_registers_(uint8_t phase, int16_t first_offset, int16_t second_offset,
OffsetCalibrationType type) {
const bool power_offsets = type == OffsetCalibrationType::OFFSET_CALIBRATION_TYPE_POWER;
@@ -925,52 +964,58 @@ void ATM90E32Component::restore_gain_calibrations_() {
this->gain_phase_[i] = this->config_gain_phase_[i];
}
bool have_data = this->gain_calibration_pref_.load(&this->gain_phase_);
const bool have_data = this->gain_calibration_pref_.load(&this->gain_phase_);
bool all_zero = true;
bool same_as_config = true;
if (have_data) {
bool all_zero = true;
bool same_as_config = true;
for (uint8_t phase = 0; phase < 3; ++phase) {
const auto &cfg = this->config_gain_phase_[phase];
const auto &config = this->config_gain_phase_[phase];
const auto &saved = this->gain_phase_[phase];
if (saved.voltage_gain != 0 || saved.current_gain != 0)
all_zero = false;
if (saved.voltage_gain != cfg.voltage_gain || saved.current_gain != cfg.current_gain)
if (saved.voltage_gain != config.voltage_gain || saved.current_gain != config.current_gain)
same_as_config = false;
}
if (!all_zero && !same_as_config) {
for (uint8_t phase = 0; phase < 3; ++phase) {
bool mismatch = false;
if (this->has_config_voltage_gain_[phase] &&
this->gain_phase_[phase].voltage_gain != this->config_gain_phase_[phase].voltage_gain)
mismatch = true;
if (this->has_config_current_gain_[phase] &&
this->gain_phase_[phase].current_gain != this->config_gain_phase_[phase].current_gain)
mismatch = true;
if (mismatch)
this->gain_calibration_mismatch_[phase] = true;
}
this->write_gains_to_registers_();
if (this->verify_gain_writes_()) {
this->using_saved_calibrations_ = true;
this->restored_gain_calibration_ = true;
return;
}
this->using_saved_calibrations_ = false;
ESP_LOGE(TAG, "[CALIBRATION][%s] Gain verification failed! Calibration may not be applied correctly.", cs);
}
}
this->using_saved_calibrations_ = false;
for (uint8_t i = 0; i < 3; ++i)
this->gain_phase_[i] = this->config_gain_phase_[i];
this->write_gains_to_registers_();
// A stored table equal to the config still counts as stored, so it is backed up and can be cleared;
// it is only not reported as restored from flash.
this->has_stored_gain_calibration_ = have_data && !all_zero;
if (!this->has_stored_gain_calibration_) {
for (uint8_t phase = 0; phase < 3; ++phase)
this->gain_phase_[phase] = this->config_gain_phase_[phase];
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs);
}
ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs);
this->write_gains_to_registers_();
const bool initial_values_verified = this->verify_gain_writes_();
if (initial_values_verified) {
for (uint8_t phase = 0; phase < 3; ++phase) {
this->gain_calibration_mismatch_[phase] =
this->has_stored_gain_calibration_ &&
((this->has_config_voltage_gain_[phase] &&
this->gain_phase_[phase].voltage_gain != this->config_gain_phase_[phase].voltage_gain) ||
(this->has_config_current_gain_[phase] &&
this->gain_phase_[phase].current_gain != this->config_gain_phase_[phase].current_gain));
}
this->restored_gain_calibration_ = this->has_stored_gain_calibration_ && !same_as_config;
return;
}
for (uint8_t phase = 0; phase < 3; ++phase) {
this->gain_calibration_mismatch_[phase] = false;
this->gain_phase_[phase] = this->config_gain_phase_[phase];
}
this->write_gains_to_registers_();
const bool fallback_verified = this->verify_gain_writes_();
this->restored_gain_calibration_ = false;
if (fallback_verified) {
ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration restore failed verification; config values verified.", cs,
LOG_STR_LITERAL("gain"));
} else {
ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration restore and config fallback both failed verification.", cs,
LOG_STR_LITERAL("gain"));
}
}
void ATM90E32Component::restore_offset_calibrations_(OffsetCalibrationType type) {
@@ -1003,15 +1048,6 @@ void ATM90E32Component::restore_offset_calibrations_(OffsetCalibrationType type)
}
*has_stored = have_data && !all_zero;
*restored = false;
for (uint8_t phase = 0; phase < 3; phase++) {
mismatches[phase] = false;
if (*has_stored) {
mismatches[phase] =
(has_first[phase] && (*offsets)[phase].first_offset != (*config_offsets)[phase].first_offset) ||
(has_second[phase] && (*offsets)[phase].second_offset != (*config_offsets)[phase].second_offset);
}
}
if (!*has_stored) {
for (uint8_t phase = 0; phase < 3; phase++)
@@ -1024,22 +1060,26 @@ void ATM90E32Component::restore_offset_calibrations_(OffsetCalibrationType type)
}
const bool initial_values_verified = this->verify_offset_writes_(type);
if (initial_values_verified) {
const auto state = resolve_offset_restore_state(*has_stored, true, false);
*restored = state.restored;
for (uint8_t phase = 0; phase < 3; phase++) {
mismatches[phase] =
*has_stored &&
((has_first[phase] && (*offsets)[phase].first_offset != (*config_offsets)[phase].first_offset) ||
(has_second[phase] && (*offsets)[phase].second_offset != (*config_offsets)[phase].second_offset));
}
*restored = *has_stored;
ESP_LOGI(TAG, "[CALIBRATION][%s] %s calibration values verified.", cs, LOG_STR_ARG(name));
return;
}
this->using_saved_calibrations_ = false;
for (uint8_t phase = 0; phase < 3; phase++)
mismatches[phase] = false;
for (uint8_t phase = 0; phase < 3; phase++) {
(*offsets)[phase] = (*config_offsets)[phase];
this->write_offsets_to_registers_(phase, (*offsets)[phase].first_offset, (*offsets)[phase].second_offset, type);
}
const auto state = resolve_offset_restore_state(*has_stored, false, this->verify_offset_writes_(type));
*restored = state.restored;
if (state.values_verified) {
const bool fallback_verified = this->verify_offset_writes_(type);
*restored = false;
if (fallback_verified) {
ESP_LOGE(TAG, "[CALIBRATION][%s] %s calibration restore failed verification; config values verified.", cs,
LOG_STR_ARG(name));
} else {
@@ -1048,9 +1088,22 @@ void ATM90E32Component::restore_offset_calibrations_(OffsetCalibrationType type)
}
}
bool ATM90E32Component::clear_calibration_pref_(ESPPreferenceObject &preference, bool &has_stored, bool &restored,
bool (&mismatches)[3]) {
const uint8_t cleared[CALIBRATION_PAYLOAD_SIZE]{};
const bool saved = preference.save(&cleared);
const bool synced = global_preferences->sync();
if (saved && synced)
has_stored = false;
restored = false;
for (bool &mismatch : mismatches)
mismatch = false;
return saved && synced;
}
void ATM90E32Component::clear_gain_calibrations() {
const char *cs = this->get_calibration_id_();
if (!this->using_saved_calibrations_) {
if (!this->has_stored_gain_calibration_) {
ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
@@ -1081,16 +1134,8 @@ void ATM90E32Component::clear_gain_calibrations() {
}
ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs);
GainCalibration zero_gains[3]{{0, 0}, {0, 0}, {0, 0}};
bool success = this->gain_calibration_pref_.save(&zero_gains);
global_preferences->sync();
this->using_saved_calibrations_ = false;
this->restored_gain_calibration_ = false;
for (bool &phase : this->gain_calibration_mismatch_)
phase = false;
if (!success) {
if (!this->clear_calibration_pref_(this->gain_calibration_pref_, this->has_stored_gain_calibration_,
this->restored_gain_calibration_, this->gain_calibration_mismatch_)) {
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to clear gain calibrations!", cs);
}
@@ -1129,16 +1174,12 @@ void ATM90E32Component::clear_offset_calibrations() {
}
ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs);
OffsetCalibration zero_offsets[3]{{0, 0}, {0, 0}, {0, 0}};
this->offset_pref_.save(&zero_offsets); // Clear stored values in flash
global_preferences->sync();
this->has_stored_offset_calibration_ = false;
this->restored_offset_calibration_ = false;
for (bool &phase : this->offset_calibration_mismatch_)
phase = false;
ESP_LOGI(TAG, "[CALIBRATION][%s] Offsets cleared.", cs);
if (!this->clear_calibration_pref_(this->offset_pref_, this->has_stored_offset_calibration_,
this->restored_offset_calibration_, this->offset_calibration_mismatch_)) {
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to clear offset calibrations!", cs);
} else {
ESP_LOGI(TAG, "[CALIBRATION][%s] Offsets cleared.", cs);
}
}
void ATM90E32Component::clear_power_offset_calibrations() {
@@ -1173,16 +1214,13 @@ void ATM90E32Component::clear_power_offset_calibrations() {
}
ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
OffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}};
this->power_offset_pref_.save(&zero_power_offsets);
global_preferences->sync();
this->has_stored_power_offset_calibration_ = false;
this->restored_power_offset_calibration_ = false;
for (bool &phase : this->power_offset_calibration_mismatch_)
phase = false;
ESP_LOGI(TAG, "[CALIBRATION][%s] Power offsets cleared.", cs);
if (!this->clear_calibration_pref_(this->power_offset_pref_, this->has_stored_power_offset_calibration_,
this->restored_power_offset_calibration_,
this->power_offset_calibration_mismatch_)) {
ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to clear power offsets!", cs);
} else {
ESP_LOGI(TAG, "[CALIBRATION][%s] Power offsets cleared.", cs);
}
}
int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
@@ -1259,28 +1297,45 @@ bool ATM90E32Component::verify_offset_writes_(OffsetCalibrationType type) {
#ifdef USE_TEXT_SENSOR
void ATM90E32Component::check_phase_status() {
static constexpr uint16_t OVER_CURRENT_FLAGS[3] = {ATM90E32_STATUS_S0_OIPHASEAST, ATM90E32_STATUS_S0_OIPHASEBST,
ATM90E32_STATUS_S0_OIPHASECST};
static constexpr float LEGACY_OVER_CURRENT_A = 65.53f;
uint16_t state0 = this->read16_(ATM90E32_REGISTER_EMMSTATE0);
uint16_t state1 = this->read16_(ATM90E32_REGISTER_EMMSTATE1);
const bool current_configured = !std::isnan(this->threshold_current_peak_a_);
const float current_limit = current_configured ? this->threshold_current_peak_a_ : LEGACY_OVER_CURRENT_A;
for (int phase = 0; phase < 3; phase++) {
std::string status;
if (state0 & over_voltage_flags[phase])
status += "Over Voltage; ";
if (state1 & voltage_sag_flags[phase])
status += "Voltage Sag; ";
if (state1 & phase_loss_flags[phase])
status += "Phase Loss; ";
auto *sensor = this->phase_status_text_sensor_[phase];
if (sensor == nullptr)
continue;
if (!status.empty()) {
status.pop_back(); // remove space
status.pop_back(); // remove semicolon
ESP_LOGW(TAG, "%s: %s", LOG_STR_ARG(sensor->get_log_name()), status.c_str());
sensor->publish_state(status);
char status[sizeof("Over Current; Over Voltage; Voltage Sag; Phase Loss; ")];
size_t length = 0;
bool over_voltage = state0 & this->over_voltage_flags[phase];
bool voltage_sag = state1 & this->voltage_sag_flags[phase];
if (!this->use_voltage_threshold_flags_) {
const float voltage = this->get_phase_voltage_(phase);
over_voltage = voltage > this->threshold_voltage_peak_v_;
voltage_sag = voltage < this->threshold_voltage_sag_v_;
}
const float current = this->has_config_current_gain_[phase] ? this->get_phase_current_(phase) : 0.0f;
if ((current_configured && this->use_current_threshold_flags_ && (state0 & OVER_CURRENT_FLAGS[phase])) ||
current > current_limit)
length = buf_append_str(status, sizeof(status), length, "Over Current; ");
if (over_voltage)
length = buf_append_str(status, sizeof(status), length, "Over Voltage; ");
if (voltage_sag)
length = buf_append_str(status, sizeof(status), length, "Voltage Sag; ");
if (state1 & this->phase_loss_flags[phase])
length = buf_append_str(status, sizeof(status), length, "Phase Loss; ");
if (length != 0) {
length -= 2; // Remove the trailing separator.
status[length] = '\0';
ESP_LOGW(TAG, "%s: %s", LOG_STR_ARG(sensor->get_log_name()), status);
sensor->publish_state(status, length);
} else {
sensor->publish_state("Okay");
}
@@ -1309,34 +1364,30 @@ void ATM90E32Component::check_freq_status() {
}
}
void ATM90E32Component::check_over_current() {
constexpr float max_current_threshold = 65.53f;
for (uint8_t phase = 0; phase < 3; phase++) {
float current_val =
this->phase_[phase].current_sensor_ != nullptr ? this->phase_[phase].current_sensor_->state : 0.0f;
if (current_val > max_current_threshold) {
ESP_LOGW(TAG, "Over current detected on Phase %c: %.2f A", 'A' + phase, current_val);
ESP_LOGW(TAG, "You may need to half your gain_ct: value & multiply the current and power values by 2");
PUBLISH_STATE(this->phase_status_text_sensor_[phase], "Over Current; ");
}
}
}
#endif
uint16_t ATM90E32Component::calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier) {
// this assumes that 60Hz electrical systems use 120V mains,
// which is usually, but not always the case
float nominal_voltage = (line_freq == 60) ? 120.0f : 220.0f;
float target_voltage = nominal_voltage * multiplier;
static uint16_t encode_threshold(float value) {
if (!std::isfinite(value) || value > UINT16_MAX) {
ESP_LOGW(TAG, "Threshold exceeds the 16-bit register range; clamping to 65535");
return UINT16_MAX;
}
return static_cast<uint16_t>(std::round(std::max(value, 0.0f)));
}
float peak_01v = target_voltage * 100.0f * std::numbers::sqrt2_v<float>; // convert RMS → peak, scale to 0.01V
float divider = (2.0f * ugain) / 32768.0f;
// Datasheet equation (section "Power Quality Monitoring"): xxThRegValue =
// RmsRegValue * sqrt(2) / (VIgain / 2^14). RmsRegValue is in 0.01V for voltage and 0.001A for current.
static uint16_t calculate_threshold(float rms, float scale, uint16_t gain) {
if (gain == 0)
return 0;
return encode_threshold(rms * scale * std::numbers::sqrt2_v<float> / (gain / 16384.0f));
}
float threshold = peak_01v / divider;
uint16_t ATM90E32Component::calculate_voltage_threshold_from_volts(float voltage_rms, uint16_t ugain) {
return calculate_threshold(voltage_rms, 100.0f, ugain);
}
return static_cast<uint16_t>(threshold);
uint16_t ATM90E32Component::calculate_current_threshold_from_amps(float current_rms, uint16_t igain) {
return calculate_threshold(current_rms, 1000.0f, igain);
}
bool ATM90E32Component::validate_spi_read_(uint16_t expected, const char *context) {
+53 -32
View File
@@ -1,10 +1,14 @@
#pragma once
#include <span>
#include <unordered_map>
#include "atm90e32_reg.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/spi/spi.h"
#include <cmath>
#include <span>
#include <unordered_map>
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
@@ -22,31 +26,24 @@ struct OffsetCalibration {
int16_t second_offset{0};
};
static_assert(sizeof(OffsetCalibration[3]) == 12, "Offset calibration preference layout must remain compatible");
struct GainCalibration {
uint16_t voltage_gain{1};
uint16_t current_gain{1};
};
// Stored size of each calibration table; an all-zero payload marks a table as cleared.
static constexpr size_t CALIBRATION_PAYLOAD_SIZE = 12;
static_assert(sizeof(OffsetCalibration[3]) == CALIBRATION_PAYLOAD_SIZE,
"Offset calibration preference layout must remain compatible");
static_assert(sizeof(GainCalibration[3]) == CALIBRATION_PAYLOAD_SIZE,
"Gain calibration preference layout must remain compatible");
enum class OffsetCalibrationType : uint8_t {
OFFSET_CALIBRATION_TYPE_VOLTAGE_CURRENT,
OFFSET_CALIBRATION_TYPE_POWER,
};
struct OffsetRestoreState {
bool restored;
bool values_verified;
};
inline OffsetRestoreState resolve_offset_restore_state(bool has_stored_values, bool initial_values_verified,
bool fallback_values_verified) {
if (initial_values_verified)
return {has_stored_values, true};
return {false, fallback_values_verified};
}
inline void prepare_offset_rollback(const OffsetCalibration (&previous)[3], bool had_stored_values,
OffsetCalibration (&rollback)[3]) {
for (uint8_t phase = 0; phase < 3; phase++)
rollback[phase] = had_stored_values ? previous[phase] : OffsetCalibration{};
}
class ATM90E32Component final : public PollingComponent,
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_HIGH,
spi::CLOCK_PHASE_TRAILING, spi::DATA_RATE_1MHZ> {
@@ -121,6 +118,13 @@ class ATM90E32Component final : public PollingComponent,
this->has_config_reactive_power_offset_[phase] = true;
}
void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; }
void set_threshold_voltage_sag_v(float voltage) { this->threshold_voltage_sag_v_ = voltage; }
void set_threshold_voltage_peak_v(float voltage) { this->threshold_voltage_peak_v_ = voltage; }
void set_threshold_current_peak_a(float current) { this->threshold_current_peak_a_ = current; }
void set_frequency_thresholds(uint16_t low, uint16_t high) {
this->frequency_low_threshold_ = low;
this->frequency_high_threshold_ = high;
}
void set_line_freq(int freq) { line_freq_ = freq; }
void set_current_phases(int phases) { current_phases_ = phases; }
void set_pga_gain(uint16_t gain) { pga_gain_ = gain; }
@@ -156,18 +160,18 @@ class ATM90E32Component final : public PollingComponent,
return 5.0f; // Default current
#endif
}
bool using_saved_calibrations_ = false; // Track if stored calibrations are being used
#ifdef USE_TEXT_SENSOR
void check_phase_status();
void check_freq_status();
void check_over_current();
void set_phase_status_text_sensor(uint8_t phase, text_sensor::TextSensor *sensor) {
this->phase_status_text_sensor_[phase] = sensor;
}
#endif
uint16_t calculate_voltage_threshold(int line_freq, uint16_t ugain, float multiplier);
static uint16_t calculate_voltage_threshold_from_volts(float voltage_rms, uint16_t ugain);
static uint16_t calculate_current_threshold_from_amps(float current_rms, uint16_t igain);
protected:
friend class ATM90E32ThresholdRegisters;
#ifdef USE_NUMBER
number::Number *ref_voltages_[3]{nullptr, nullptr, nullptr};
number::Number *ref_currents_[3]{nullptr, nullptr, nullptr};
@@ -205,12 +209,21 @@ class ATM90E32Component final : public PollingComponent,
void set_publish_interval_flag_(bool flag) { publish_interval_flag_ = flag; };
void restore_offset_calibrations_(OffsetCalibrationType type);
void restore_gain_calibrations_();
void save_gain_calibration_to_memory_();
void finish_offset_calibration_(const OffsetCalibration (&previous)[3], bool previous_restored,
bool previous_using_saved, OffsetCalibrationType type);
void finish_gain_calibration_(const GainCalibration (&previous)[3]);
void finish_offset_calibration_(const OffsetCalibration (&previous)[3], OffsetCalibrationType type);
// Persists verified register values; otherwise rollback() rewrites the previous values and returns their readback.
// name starts a sentence; noun is used mid-sentence, keeping the log text the calibration helper matches.
template<typename T, typename F>
void finish_calibration_(const LogString *name, const LogString *noun, ESPPreferenceObject &preference,
const T (&values)[3], bool writes_verified, bool &has_stored, bool &restored,
bool (&mismatches)[3], F &&rollback);
bool clear_calibration_pref_(ESPPreferenceObject &preference, bool &has_stored, bool &restored,
bool (&mismatches)[3]);
void configure_chip_();
void write_offsets_to_registers_(uint8_t phase, int16_t first_offset, int16_t second_offset,
OffsetCalibrationType type);
void write_gains_to_registers_();
void write_thresholds_();
bool verify_gain_writes_();
bool verify_offset_writes_(OffsetCalibrationType type);
bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
@@ -256,11 +269,7 @@ class ATM90E32Component final : public PollingComponent,
OffsetCalibration power_offset_phase_[3];
OffsetCalibration config_power_offset_phase_[3];
struct GainCalibration {
uint16_t voltage_gain{1};
uint16_t current_gain{1};
} gain_phase_[3];
GainCalibration gain_phase_[3];
GainCalibration config_gain_phase_[3];
bool has_config_voltage_offset_[3]{false, false, false};
@@ -274,6 +283,8 @@ class ATM90E32Component final : public PollingComponent,
ESPPreferenceObject power_offset_pref_;
ESPPreferenceObject gain_calibration_pref_;
friend class ATM90E32CalibrationTestAccess;
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *phase_status_text_sensor_[3]{nullptr};
#endif
@@ -284,13 +295,23 @@ class ATM90E32Component final : public PollingComponent,
bool peak_current_signed_{false};
bool enable_offset_calibration_{false};
bool enable_gain_calibration_{false};
float threshold_voltage_sag_v_{0.0f};
float threshold_voltage_peak_v_{0.0f};
float threshold_current_peak_a_{NAN}; // NAN keeps the legacy OITH register and 65.53 A software limit
uint16_t frequency_low_threshold_{0};
uint16_t frequency_high_threshold_{0};
const char *instance_id_{nullptr};
bool has_stored_offset_calibration_{false};
bool has_stored_power_offset_calibration_{false};
bool has_stored_gain_calibration_{false};
bool restored_offset_calibration_{false};
bool restored_power_offset_calibration_{false};
bool restored_gain_calibration_{false};
bool calibration_message_printed_{false};
#ifdef USE_TEXT_SENSOR
bool use_voltage_threshold_flags_{true};
bool use_current_threshold_flags_{true};
#endif
bool offset_calibration_mismatch_[3]{false, false, false};
bool power_offset_calibration_mismatch_[3]{false, false, false};
bool gain_calibration_mismatch_[3]{false, false, false};
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID, ENTITY_CATEGORY_CONFIG, ICON_SCALE
from esphome.types import ConfigType
from ..sensor import ATM90E32Component
from .sensor import ATM90E32Component
CONF_RUN_GAIN_CALIBRATION = "run_gain_calibration"
CONF_CLEAR_GAIN_CALIBRATION = "clear_gain_calibration"
+189 -2
View File
@@ -1,3 +1,6 @@
import logging
import math
import esphome.codegen as cg
from esphome.components import sensor, spi
import esphome.config_validation as cv
@@ -45,6 +48,8 @@ from esphome.types import ConfigType
from . import atm90e32_ns
_LOGGER = logging.getLogger(__name__)
CONF_CHIP_TEMPERATURE = "chip_temperature"
CONF_GAIN_PGA = "gain_pga"
CONF_CURRENT_PHASES = "current_phases"
@@ -61,6 +66,16 @@ CONF_ENABLE_OFFSET_CALIBRATION = "enable_offset_calibration"
CONF_ENABLE_GAIN_CALIBRATION = "enable_gain_calibration"
CONF_PHASE_STATUS = "phase_status"
CONF_FREQUENCY_STATUS = "frequency_status"
CONF_THRESHOLDS = "thresholds"
CONF_ALLOW_UNSAFE_THRESHOLDS = "allow_unsafe_thresholds"
CONF_VOLTAGE_NOMINAL = "voltage_nominal"
CONF_VOLTAGE_SAG_PCT = "voltage_sag_pct"
CONF_VOLTAGE_PEAK_PCT = "voltage_peak_pct"
CONF_VOLTAGE_SAG_V = "voltage_sag_v"
CONF_VOLTAGE_PEAK_V = "voltage_peak_v"
CONF_CURRENT_PEAK = "current_peak"
CONF_FREQUENCY_LOW_HZ = "frequency_low_hz"
CONF_FREQUENCY_HIGH_HZ = "frequency_high_hz"
UNIT_DEG = "degrees"
LINE_FREQS = {
"50HZ": 50,
@@ -76,6 +91,13 @@ PGA_GAINS = {
"4X": 0x2A,
}
DEFAULT_VOLTAGE_SAG_PCT = 0.78
DEFAULT_VOLTAGE_PEAK_PCT = 1.22
# IRMS saturates at 65.535 A, so a higher limit could never be exceeded.
MAX_CURRENT_PEAK_A = 65.53
MAX_FREQUENCY_HZ = 655.35
DEFAULT_FREQUENCY_THRESHOLD_BAND_HZ = 3.0
ATM90E32Component = atm90e32_ns.class_(
"ATM90E32Component", cg.PollingComponent, spi.SPIDevice
)
@@ -156,7 +178,158 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
}
)
CONFIG_SCHEMA = (
POSITIVE_VOLTAGE = cv.All(cv.voltage, cv.positive_not_null_float)
THRESHOLD_PERCENT = cv.All(
cv.unbounded_percentage, cv.float_range(min=0, min_included=False, max=2.0)
)
REGISTER_FREQUENCY = cv.All(
cv.frequency, cv.float_range(min=0, min_included=False, max=MAX_FREQUENCY_HZ)
)
THRESHOLDS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ALLOW_UNSAFE_THRESHOLDS, default=False): cv.boolean,
cv.Optional(CONF_VOLTAGE_NOMINAL): POSITIVE_VOLTAGE,
cv.Exclusive(CONF_VOLTAGE_SAG_PCT, "voltage_sag_threshold"): THRESHOLD_PERCENT,
cv.Exclusive(CONF_VOLTAGE_SAG_V, "voltage_sag_threshold"): POSITIVE_VOLTAGE,
cv.Exclusive(
CONF_VOLTAGE_PEAK_PCT, "voltage_peak_threshold"
): THRESHOLD_PERCENT,
cv.Exclusive(CONF_VOLTAGE_PEAK_V, "voltage_peak_threshold"): POSITIVE_VOLTAGE,
cv.Optional(CONF_CURRENT_PEAK): cv.All(
cv.current,
cv.float_range(min=0, min_included=False, max=MAX_CURRENT_PEAK_A),
),
cv.Optional(CONF_FREQUENCY_LOW_HZ): REGISTER_FREQUENCY,
cv.Optional(CONF_FREQUENCY_HIGH_HZ): REGISTER_FREQUENCY,
}
)
def _default_voltage_nominal(line_frequency: int) -> float:
return 120.0 if line_frequency == 60 else 220.0
def _resolve_thresholds(config: ConfigType) -> ConfigType:
line_frequency = config[CONF_LINE_FREQUENCY].enum_value
resolved = dict(config[CONF_THRESHOLDS])
nominal_voltage = resolved.setdefault(
CONF_VOLTAGE_NOMINAL, _default_voltage_nominal(line_frequency)
)
for volts_key, pct_key, default_pct in (
(CONF_VOLTAGE_SAG_V, CONF_VOLTAGE_SAG_PCT, DEFAULT_VOLTAGE_SAG_PCT),
(CONF_VOLTAGE_PEAK_V, CONF_VOLTAGE_PEAK_PCT, DEFAULT_VOLTAGE_PEAK_PCT),
):
resolved.setdefault(
volts_key, nominal_voltage * resolved.get(pct_key, default_pct)
)
resolved.setdefault(
CONF_FREQUENCY_LOW_HZ, line_frequency - DEFAULT_FREQUENCY_THRESHOLD_BAND_HZ
)
resolved.setdefault(
CONF_FREQUENCY_HIGH_HZ, line_frequency + DEFAULT_FREQUENCY_THRESHOLD_BAND_HZ
)
return resolved
def _validate_thresholds(config: ConfigType) -> ConfigType:
configured = config[CONF_THRESHOLDS]
thresholds = _resolve_thresholds(config)
nominal_voltage = thresholds[CONF_VOLTAGE_NOMINAL]
sag_voltage = thresholds[CONF_VOLTAGE_SAG_V]
peak_voltage = thresholds[CONF_VOLTAGE_PEAK_V]
if not thresholds[CONF_ALLOW_UNSAFE_THRESHOLDS]:
for key, ratio, low, high in (
(CONF_VOLTAGE_SAG_V, sag_voltage / nominal_voltage, 0.50, 0.99),
(CONF_VOLTAGE_PEAK_V, peak_voltage / nominal_voltage, 1.01, 1.50),
):
if not low <= ratio <= high:
raise cv.Invalid(
f"thresholds.{key} must be between {low:.0%} and {high:.0%} of voltage_nominal; "
"set thresholds.allow_unsafe_thresholds: true to override."
)
if not sag_voltage < nominal_voltage < peak_voltage:
raise cv.Invalid(
"Invalid voltage thresholds: expected sag < nominal < peak "
f"(got {sag_voltage:.3f}V < {nominal_voltage:.3f}V < {peak_voltage:.3f}V)."
)
frequency_low = thresholds[CONF_FREQUENCY_LOW_HZ]
frequency_high = thresholds[CONF_FREQUENCY_HIGH_HZ]
if round(frequency_low * 100) >= round(frequency_high * 100):
raise cv.Invalid(
"thresholds.frequency_low_hz must be lower than thresholds.frequency_high_hz "
"at the register resolution of 0.01Hz."
)
# Each limit counts as set by the user when any key it is derived from was given.
for gain_key, scale, registers in (
(
CONF_GAIN_VOLTAGE,
100,
(
(
CONF_VOLTAGE_SAG_V,
min,
(CONF_VOLTAGE_NOMINAL, CONF_VOLTAGE_SAG_PCT, CONF_VOLTAGE_SAG_V),
),
(
CONF_VOLTAGE_PEAK_V,
max,
(CONF_VOLTAGE_NOMINAL, CONF_VOLTAGE_PEAK_PCT, CONF_VOLTAGE_PEAK_V),
),
),
),
(CONF_GAIN_CT, 1000, ((CONF_CURRENT_PEAK, max, (CONF_CURRENT_PEAK,)),)),
):
explicit = {
key: any(source in configured for source in sources)
for key, _, sources in registers
}
# A legacy config without current_peak keeps the fixed OITH register value.
if gain_key == CONF_GAIN_CT and not explicit[CONF_CURRENT_PEAK]:
continue
gains = {
config[phase][gain_key]
for phase in (CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C)
if phase in config
}
if not gains or 0 in gains:
if not any(explicit.values()):
continue
raise cv.Invalid(
f"Thresholds require at least one phase and a nonzero {gain_key} on every configured phase."
)
for key, select_gain, _ in registers:
gain = select_gain(gains)
limit = 65535 * gain / (scale * math.sqrt(2) * 16384)
if thresholds[key] <= limit:
continue
message = (
f"thresholds.{key} ({thresholds[key]:.1f}) cannot fit the 16-bit threshold register with "
f"{gain_key}: {gain}; the highest value that fits is {limit:.1f}"
)
if explicit[key]:
raise cv.Invalid(
f"{message}. Increase {gain_key} or lower the threshold."
)
# Defaults are only warned about so existing configs keep building; the register is clamped.
_LOGGER.warning(
"%s. The default is clamped to the register maximum, so this alarm cannot trigger; "
"set thresholds.%s to at most %.1f to use it.",
message,
key,
limit,
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ATM90E32Component),
@@ -185,10 +358,12 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_PEAK_CURRENT_SIGNED, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_OFFSET_CALIBRATION, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_GAIN_CALIBRATION, default=False): cv.boolean,
cv.Optional(CONF_THRESHOLDS, default={}): THRESHOLDS_SCHEMA,
}
)
.extend(cv.polling_component_schema("60s"))
.extend(spi.spi_device_schema())
.extend(spi.spi_device_schema()),
_validate_thresholds,
)
@@ -250,3 +425,15 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_peak_current_signed(config[CONF_PEAK_CURRENT_SIGNED]))
cg.add(var.set_enable_offset_calibration(config[CONF_ENABLE_OFFSET_CALIBRATION]))
cg.add(var.set_enable_gain_calibration(config[CONF_ENABLE_GAIN_CALIBRATION]))
# The user's keys stay as written so `esphome config` output can be loaded again.
thresholds = _resolve_thresholds(config)
cg.add(var.set_threshold_voltage_sag_v(thresholds[CONF_VOLTAGE_SAG_V]))
cg.add(var.set_threshold_voltage_peak_v(thresholds[CONF_VOLTAGE_PEAK_V]))
cg.add_optional(var.set_threshold_current_peak_a, thresholds.get(CONF_CURRENT_PEAK))
cg.add(
var.set_frequency_thresholds(
round(thresholds[CONF_FREQUENCY_LOW_HZ] * 100),
round(thresholds[CONF_FREQUENCY_HIGH_HZ] * 100),
)
)
@@ -4,7 +4,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C
from esphome.types import ConfigType
from ..sensor import ATM90E32Component
from .sensor import ATM90E32Component
CONF_PHASE_STATUS = "phase_status"
CONF_FREQUENCY_STATUS = "frequency_status"
+19 -2
View File
@@ -2,6 +2,8 @@
#ifdef USE_ESP32
#include <algorithm>
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
@@ -19,6 +21,9 @@ static const uint32_t CONNECTION_TIMEOUT_MS = 5000;
static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6;
static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;
// esp_http_client_read() blocks until it fills the request; small requests deliver a phrase tail before a pause ends
static constexpr size_t HTTP_READ_MAX_BYTES = 1024;
static constexpr int HTTP_BODY_TIMEOUT_MS = 100;
static const uint8_t MAX_REDIRECTIONS = 5;
@@ -196,10 +201,18 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
file_type = this->audio_file_type_;
}
// Keep the longer timeout for connection, headers, and redirects.
err = esp_http_client_set_timeout_ms(this->client_, HTTP_BODY_TIMEOUT_MS);
if (err != ESP_OK) {
this->cleanup_connection_();
return err;
}
this->last_data_read_ms_ = millis();
this->output_transfer_buffer_ = AudioSinkTransferBuffer::create(this->buffer_size_);
if (this->output_transfer_buffer_ == nullptr) {
this->cleanup_connection_();
return ESP_ERR_NO_MEM;
}
@@ -254,8 +267,9 @@ AudioReaderState AudioReader::http_read_() {
return AudioReaderState::FINISHED;
}
} else if (this->output_transfer_buffer_->free() > 0) {
int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(),
this->output_transfer_buffer_->free());
size_t read_size = std::min(this->output_transfer_buffer_->free(), HTTP_READ_MAX_BYTES);
int received_len =
esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), read_size);
if (received_len > 0) {
this->output_transfer_buffer_->increase_buffer_length(received_len);
@@ -278,6 +292,9 @@ AudioReaderState AudioReader::http_read_() {
delay(READ_WRITE_TIMEOUT_MS);
}
} else {
// A full buffer means the consumer is behind, not that the server went quiet
this->last_data_read_ms_ = millis();
}
return AudioReaderState::READING;
+3 -6
View File
@@ -1,4 +1,5 @@
#include "ballu.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ballu {
@@ -90,9 +91,7 @@ void BalluClimate::transmit_state() {
for (uint8_t i = 0; i < BALLU_STATE_LENGTH - 1; i++)
remote_state[12] += remote_state[i];
ESP_LOGV(TAG, "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0],
remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6],
remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12]);
ESP_LOGV_HEX(TAG, remote_state, BALLU_STATE_LENGTH, BALLU_STATE_LENGTH, "Sending: %s");
// Send code
auto transmit = this->transmitter_->transmit();
@@ -136,9 +135,7 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
}
ESP_LOGV(TAG, "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0],
remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6],
remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12]);
ESP_LOGV_HEX(TAG, remote_state, BALLU_STATE_LENGTH, BALLU_STATE_LENGTH, "Received: %s");
// verify header remote code
if (remote_state[0] != 0xc3)
@@ -8,14 +8,14 @@ ESPHOME_LOG_TAG(TAG, "bang_bang.climate");
BangBangClimate::BangBangClimate() = default;
void BangBangClimate::setup() {
this->sensor_->add_on_state_callback([this](float state) {
this->temperature_sensor_->add_on_state_callback([this](float state) {
this->current_temperature = state;
// control may have changed, recompute
this->compute_state_();
// current temperature changed, publish state
this->publish_state();
});
this->current_temperature = this->sensor_->state;
this->current_temperature = this->temperature_sensor_->state;
// register for humidity values and get initial state
if (this->humidity_sensor_ != nullptr) {
@@ -17,13 +17,14 @@ struct BangBangClimateTargetTempConfig {
};
class BangBangClimate final : public climate::Climate, public Component {
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
public:
BangBangClimate();
void setup() override;
void dump_config() override;
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
void set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
void set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
@@ -47,11 +48,6 @@ class BangBangClimate final : public climate::Climate, public Component {
/// Switch the climate device to the given climate mode.
void switch_to_action_(climate::ClimateAction action);
/// The sensor used for getting the current temperature
sensor::Sensor *sensor_{nullptr};
/// The sensor used for getting the current humidity
sensor::Sensor *humidity_sensor_{nullptr};
/** The trigger to call when the controller should switch to idle mode.
*
* In idle mode, the controller is assumed to have both heating and cooling disabled.
+1 -1
View File
@@ -46,7 +46,7 @@ async def to_code(config: ConfigType) -> None:
var = await climate.new_climate(config)
await cg.register_component(var, config)
await cg.add_variable(var.set_sensor, config[CONF_SENSOR])
await cg.add_variable(var.set_temperature_sensor, config[CONF_SENSOR])
await cg.add_variable(var.set_humidity_sensor, config.get(CONF_HUMIDITY_SENSOR))
normal_config = BangBangClimateTargetTempConfig(
@@ -159,33 +159,19 @@ void spi_dma_tx_finish_callback(unsigned int param) {
}
void BekenSPILEDStripLightOutput::setup() {
size_t buffer_size = this->get_buffer_size_();
size_t dma_buffer_size = (buffer_size * 8) + (2 * 64);
RAMAllocator<uint8_t> allocator;
this->buf_ = allocator.allocate(buffer_size);
if (this->buf_ == nullptr) {
ESP_LOGE(TAG, "Cannot allocate LED buffer!");
this->mark_failed();
return;
}
this->effect_data_ = allocator.allocate(this->num_leds_);
if (this->effect_data_ == nullptr) {
ESP_LOGE(TAG, "Cannot allocate effect data!");
this->mark_failed();
if (!this->internal_accessor_.allocate(allocator, this->num_leds_)) {
this->mark_failed(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) {
ESP_LOGE(TAG, "Cannot allocate DMA buffer!");
this->mark_failed();
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
return;
}
memset(this->buf_, 0, buffer_size);
memset(this->effect_data_, 0, this->num_leds_);
memset(this->dma_buf_, 0, dma_buffer_size);
uint32_t value = PCLK_POSI_SPI;
@@ -195,22 +181,19 @@ void BekenSPILEDStripLightOutput::setup() {
sddev_control(ICU_DEV_NAME, CMD_CLK_PWR_UP, &value);
if (spi_data != nullptr) {
ESP_LOGE(TAG, "SPI device already initialized!");
this->mark_failed();
this->mark_failed(LOG_STR("SPI already initialized"));
return;
}
spi_data = (SpiData *) calloc(1, sizeof(SpiData)); // NOLINT(cppcoreguidelines-no-malloc)
if (spi_data == nullptr) {
ESP_LOGE(TAG, "Cannot allocate spi_data!");
this->mark_failed();
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
return;
}
spi_data->dma_tx_semaphore = xSemaphoreCreateBinary();
if (spi_data->dma_tx_semaphore == nullptr) {
ESP_LOGE(TAG, "TX Semaphore init faild!");
this->mark_failed();
this->mark_failed(LOG_STR("TX semaphore init failed"));
return;
}
@@ -291,6 +274,8 @@ void BekenSPILEDStripLightOutput::setup() {
// T-like variants: No transfer-length register. The SPI controller shifts out
// everything the DMA writes into SPI_DAT.
#endif
this->accessor_ = &this->internal_accessor_;
}
void BekenSPILEDStripLightOutput::set_led_params(uint8_t bit0, uint8_t bit1, uint32_t spi_frequency) {
@@ -351,7 +336,7 @@ void BekenSPILEDStripLightOutput::write_state(light::LightState *state) {
size_t buffer_size = this->get_buffer_size_();
size_t size = 0;
uint8_t *psrc = this->buf_;
uint8_t *psrc = this->internal_accessor_.color_data;
uint8_t *pdest = this->dma_buf_ + 64;
// The 64 byte padding is a workaround for a SPI DMA bug where the
// output doesn't exactly start at the beginning of dma_buf_
@@ -381,7 +366,8 @@ void BekenSPILEDStripLightOutput::dump_config() {
" Channel colors: %s\n"
" Max refresh rate: %" PRIu32 "\n"
" Number of LEDs: %u",
this->channel_colors_.to_string(channel_colors), this->max_refresh_rate_, this->num_leds_);
this->internal_accessor_.channel_colors.to_string(channel_colors), this->max_refresh_rate_,
this->num_leds_);
}
float BekenSPILEDStripLightOutput::get_setup_priority() const { return setup_priority::HARDWARE; }
@@ -18,33 +18,27 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
float get_setup_priority() const override;
int32_t size() const override { return this->num_leds_; }
light::LightTraits get_traits() override { return this->channel_colors_.traits(); }
light::LightTraits get_traits() override { return this->internal_accessor_.channel_colors.traits(); }
void set_pin(uint8_t pin) { this->pin_ = pin; }
void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
void set_channel_colors(light::ChannelColors channel_colors) { this->channel_colors_ = channel_colors; }
void set_channel_colors(light::ChannelColors channel_colors) {
this->internal_accessor_.channel_colors = channel_colors;
}
/// Set a maximum refresh rate in µs as some lights do not like being updated too often.
void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
void set_led_params(uint8_t bit0, uint8_t bit1, uint32_t spi_frequency);
void clear_effect_data() override {
if (this->effect_data_ != nullptr)
memset(this->effect_data_, 0, this->num_leds_);
}
void clear_effect_data() override { this->internal_accessor_.clear_effect_data(this->num_leds_); }
void dump_config() override;
protected:
light::ESPColorView get_view_internal(int32_t index) const override {
return this->channel_colors_.view(this->buf_, this->effect_data_, index, &this->correction_);
}
size_t get_buffer_size_() const { return this->num_leds_ * this->internal_accessor_.channel_colors.bytes_per_led(); }
size_t get_buffer_size_() const { return this->num_leds_ * this->channel_colors_.bytes_per_led(); }
uint8_t *buf_{nullptr};
uint8_t *effect_data_{nullptr};
light::CorrectedBufferColorAccessor internal_accessor_{nullptr, nullptr, {0, 1, 2}, &this->correction_};
uint8_t *dma_buf_{nullptr};
uint8_t pin_;
@@ -53,7 +47,6 @@ class BekenSPILEDStripLightOutput final : public light::AddressableLight {
uint32_t spi_frequency_{6666666};
uint8_t bit0_{0xE0};
uint8_t bit1_{0xFC};
light::ChannelColors channel_colors_{0, 1, 2, light::ChannelColors::NO_WHITE};
uint32_t last_refresh_{0};
uint32_t max_refresh_rate_{0};
@@ -3,7 +3,7 @@ from esphome.components import output
import esphome.config_validation as cv
from esphome.types import ConfigType
from .. import CONF_BH1745_ID, BH1745Component
from . import CONF_BH1745_ID, BH1745Component
DEPENDENCIES = ["bh1745"]
+1 -1
View File
@@ -162,8 +162,8 @@ class SettleFilter : public Filter {
protected:
void reset() override;
bool steady_{true}; // Before delay_ so it fills the Filter base's tail padding
TemplatableFn<uint32_t> delay_{};
bool steady_{true};
};
} // namespace esphome::binary_sensor
@@ -3,8 +3,8 @@ import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_RESTART
from esphome.types import ConfigType
from .. import CONF_BL0940_ID
from ..sensor import BL0940
from . import CONF_BL0940_ID
from .sensor import BL0940
CONFIG_SCHEMA = button.button_schema(
button.ApplyButton,
+9 -5
View File
@@ -5,7 +5,14 @@ from esphome import automation
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
from esphome.components.const import CONF_ACCEPT, CONF_ON_NUMERIC_COMPARISON_REQUEST
from esphome.components.ble_device_base import validate_passkey
from esphome.components.const import (
CONF_ACCEPT,
CONF_ON_NUMERIC_COMPARISON_REQUEST,
CONF_ON_PASSKEY_NOTIFICATION,
CONF_ON_PASSKEY_REQUEST,
CONF_PASSKEY,
)
from esphome.components.esp32_ble import BTLoggers
import esphome.config_validation as cv
from esphome.const import (
@@ -101,9 +108,6 @@ BLERemoveBondAction = ble_client_ns.class_(
"BLEClientRemoveBondAction", automation.Action
)
CONF_PASSKEY = "passkey"
CONF_ON_PASSKEY_REQUEST = "on_passkey_request"
CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification"
CONF_AUTO_CONNECT = "auto_connect"
MULTI_CONF = True
@@ -205,7 +209,7 @@ BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema(
BLE_PASSKEY_REPLY_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
cv.Required(CONF_PASSKEY): cv.templatable(cv.int_range(min=0, max=999999)),
cv.Required(CONF_PASSKEY): cv.templatable(validate_passkey),
}
)
@@ -46,6 +46,9 @@ DOMAIN = "ble_device_base"
CONF_BLE_HUB_ID = "ble_hub_id"
# A six digit pairing passkey
validate_passkey = cv.int_range(min=0, max=999999)
# Number of parsed-advertisement listeners registered in this build; read via
# cg.get_slot_count() by esp32_ble_tracker's feature coupling.
LISTENER_COUNT_DEFINE = "ESPHOME_BLE_DEVICE_BASE_LISTENER_COUNT"
@@ -0,0 +1,116 @@
"""Actions shared by the BLE server platforms (esp32_ble_server, zephyr_ble_server).
An action name lives in one registry, so a platform cannot register it on its own without
replacing another platform's entry. Each server instead adds its schema and builder here,
and the action validates and builds with the one for the target platform.
"""
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
from typing import Any
from esphome import automation
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.core import ID
from esphome.cpp_generator import TemplateArgsType
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
from esphome.types import ConfigType
ActionBuilder = Callable[
[ConfigType, ID, cg.TemplateArguments, TemplateArgsType], Awaitable[cg.MockObj]
]
@dataclass(frozen=True)
class _PlatformAction:
is_target: Callable[[], bool]
schema: cv.Schema | cv.All
builder: ActionBuilder
_ACTIONS: dict[str, list[_PlatformAction]] = {}
def _for_target(name: str) -> _PlatformAction:
for platform in _ACTIONS[name]:
if platform.is_target():
return platform
raise cv.Invalid(f"{name} is not supported on this platform")
def _keys_schema(schema: cv.Schema | cv.All) -> cv.Schema:
"""The dict schema of ``schema``, or of a ``cv.All`` that adds validators after it."""
if isinstance(schema, cv.All):
return next(v for v in schema.validators if isinstance(v, cv.Schema))
return schema
def _merged_schema(platforms: list[_PlatformAction]) -> cv.Schema:
"""All platforms' keys, with a key that not every platform takes shown as optional."""
schemas = [_keys_schema(p.schema).schema for p in platforms]
shared = set.intersection(*({str(key) for key in s} for s in schemas))
merged: dict[Any, Any] = {}
seen: set[str] = set()
for schema in schemas:
for key, validator in schema.items():
if (text := str(key)) in seen:
continue
seen.add(text)
merged[key if text in shared else cv.Optional(text)] = validator
return cv.Schema(merged)
def register_server_action(
name: str,
is_target: Callable[[], bool],
schema: cv.Schema | cv.All,
builder: ActionBuilder,
) -> None:
"""Add a server platform's version of the action ``name``.
``is_target`` tells whether the build is for this platform, e.g. ``lambda: CORE.is_esp32``.
"""
if (platforms := _ACTIONS.get(name)) is None:
platforms = _ACTIONS[name] = []
# The schema depends on the target, so the language schema shows every platform's keys
@schema_extractor("schema")
def validate(value: Any) -> ConfigType:
if value is SCHEMA_EXTRACT:
return _merged_schema(platforms)
return _for_target(name).schema(value)
async def dispatch(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> cg.MockObj:
return await _for_target(name).builder(
config, action_id, template_arg, args
)
automation.register_action(
name, automation.ApplyAction, validate, synchronous=True
)(dispatch)
platforms.append(_PlatformAction(is_target, schema, builder))
def register_server_apply_action(
name: str,
is_target: Callable[[], bool],
schema: cv.Schema | cv.All,
*fields: automation.ApplyField | automation.ApplyCall,
) -> None:
"""Add a server platform's version of the apply action ``name``.
The other arguments are those of ``register_server_action`` and
``automation.register_apply_action``.
"""
register_server_action(
name,
is_target,
schema,
automation.apply_action_builder(name, schema, *fields),
)
@@ -635,16 +635,8 @@ void BluetoothProxy::loop() {
#endif
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
// The API subscriber is gone: tear down any connections it left behind
// (disconnect() on an already-disconnecting slot is a no-op).
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0) {
connection->disconnect();
}
}
#endif
// The API subscriber is gone: tear down any connections it left behind.
this->disconnect_all_connections_();
return;
}
@@ -737,6 +729,17 @@ void BluetoothProxy::reset_owed_replies_() {
#endif
}
void BluetoothProxy::disconnect_all_connections_() {
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0) {
connection->disconnect();
}
}
#endif
}
void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) {
if (api_connection != this->api_connection_) {
if (this->api_connection_ != nullptr) {
@@ -754,8 +757,11 @@ void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection
// Stale retry latches belong to the previous subscriber's session; a
// re-subscribe by the current one keeps what it is still owed.
this->reset_owed_replies_();
this->api_connection_ = api_connection;
// Held links belong to a client that no longer gets their events; free them so idle-only
// advertisers are seen again. After the swap, so a sync slot free reports to the new client.
this->disconnect_all_connections_();
}
this->api_connection_ = api_connection;
#ifdef USE_BLE_SCANNER_STATE_CALLBACK
// get_scanner_state() is part of the push-hub surface (see BLEHubContract).
this->send_scanner_state_(this->hub_->get_scanner_state());
@@ -328,6 +328,8 @@ class BluetoothProxy final : public Component {
/// Drops state only, never sends: api_connection_ is the departing
/// subscriber on subscribe and nullptr on unsubscribe.
void reset_owed_replies_();
/// Tear down every held link; repeat calls are no-ops.
void disconnect_all_connections_();
#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
/// Report a reply we deliberately do not latch, so no drop is silent.
void log_reply_dropped_(const char *what, uint64_t address);
+13 -12
View File
@@ -171,6 +171,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 +214,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 +233,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 +269,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;
@@ -148,16 +148,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 +171,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;
@@ -249,13 +249,8 @@ void BMP581Component::update() {
// 4) Publish measurements to sensor(s), if applicable //
/////////////////////////////////////////////////////////
if (this->temperature_sensor_) {
this->temperature_sensor_->publish_state(temperature);
}
if (this->pressure_sensor_) {
this->pressure_sensor_->publish_state(pressure);
}
PUBLISH_STATE(this->temperature_sensor_, temperature);
PUBLISH_STATE(this->pressure_sensor_, pressure);
this->status_clear_warning();
});
+1 -2
View File
@@ -4,6 +4,7 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.const import CONF_CHARGE_ENABLED, CONF_INPUT_CURRENT_LIMIT
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL, CONF_WATCHDOG
from esphome.core import CORE, ID, EsphomeError, Lambda, TimePeriod
@@ -19,7 +20,6 @@ CONF_BQ25186_ID = "bq25186_id"
CONF_CONFIGURE_ON_BOOT = "configure_on_boot"
CONF_BATTERY_REGULATION_VOLTAGE = "battery_regulation_voltage"
CONF_FAST_CHARGE_CURRENT = "fast_charge_current"
CONF_CHARGE_ENABLED = "charge_enabled"
CONF_FLASH_CHARGING_MODE = "flash_charging_mode"
CONF_PRECHARGE_IS_ITERM = "precharge_is_iterm"
CONF_TERMINATION_PERCENT = "termination_percent"
@@ -38,7 +38,6 @@ CONF_SAFETY_TIMER = "safety_timer"
CONF_LONG_PRESS_DURATION = "long_press_duration"
CONF_HW_RESET_REQUIRES_VIN = "hw_reset_requires_vin"
CONF_AUTOWAKE = "autowake"
CONF_INPUT_CURRENT_LIMIT = "input_current_limit"
CONF_PUSH_BUTTON_LONG_PRESS_ACTION = "push_button_long_press_action"
CONF_WAKE1_TIMER = "wake1_timer"
CONF_WAKE2_TIMER = "wake2_timer"
+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);
+2 -2
View File
@@ -21,6 +21,8 @@ class ClimateIR : public Component,
public climate::Climate,
public remote_base::RemoteReceiverListener,
public remote_base::RemoteTransmittable {
SUB_SENSOR(humidity)
public:
ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
bool supports_dry = false, bool supports_fan_only = false,
@@ -47,7 +49,6 @@ class ClimateIR : public Component,
this->set_mode_supported_(climate::CLIMATE_MODE_HEAT_COOL, supports_heat_cool);
}
void set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
void set_humidity_sensor(sensor::Sensor *sensor) { this->humidity_sensor_ = sensor; }
protected:
float minimum_temperature_, maximum_temperature_, temperature_step_;
@@ -84,7 +85,6 @@ class ClimateIR : public Component,
climate::ClimatePresetMask presets_{};
sensor::Sensor *sensor_{nullptr};
sensor::Sensor *humidity_sensor_{nullptr};
};
} // namespace esphome::climate_ir
+8 -2
View File
@@ -42,7 +42,10 @@ void CM1106Component::update() {
}
if (response[0] != 0x16 || response[1] != 0x05 || response[2] != 0x01) {
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
char hex_buf[format_hex_pretty_size(4)];
ESP_LOGW(TAG, "Unexpected response: %s", format_hex_pretty_to(hex_buf, response, 4, ' '));
#endif
this->status_set_warning();
return;
}
@@ -101,7 +104,10 @@ bool CM1106Component::send_command_expect_ack_(const uint8_t *command, size_t co
return false;
}
if (!this->status_comm_result(memcmp(response, ack, sizeof(response)) == 0)) {
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
char hex_buf[format_hex_pretty_size(4)];
ESP_LOGW(TAG, "Unexpected response: %s", format_hex_pretty_to(hex_buf, response, 4, ' '));
#endif
return false;
}
return true;
+3 -4
View File
@@ -59,6 +59,9 @@ class CombinationOneParameterComponent : public CombinationComponent {
};
class KalmanCombinationComponent final : public CombinationOneParameterComponent {
// Optional sensor for publishing the current error
SUB_SENSOR(std_dev)
public:
void dump_config() override;
void setup() override;
@@ -66,15 +69,11 @@ class KalmanCombinationComponent final : public CombinationOneParameterComponent
void set_process_std_dev(float process_std_dev) {
this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f;
}
void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; }
protected:
void update_variance_();
void correct_(float value, float stddev);
// Optional sensor for publishing the current error
sensor::Sensor *std_dev_sensor_{nullptr};
// Tick of the last update
uint32_t last_update_{0};
// Change of the variance, per ms
+11
View File
@@ -9,6 +9,7 @@ BYTE_ORDER_BIG = "big_endian"
CONF_ACCELEROMETER_ODR = "accelerometer_odr"
CONF_ACCELEROMETER_RANGE = "accelerometer_range"
CONF_ACCEPT = "accept"
CONF_ACTIVE_HIGH = "active_high"
CONF_ADC_AVERAGING = "adc_averaging"
CONF_ADC_TIME = "adc_time"
CONF_ALLOWED_IPS = "allowed_ips"
@@ -23,6 +24,8 @@ CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order"
CONF_CHANNEL_COLORS = "channel_colors"
CONF_CHARGE_CURRENT = "charge_current"
CONF_CHARGE_ENABLED = "charge_enabled"
CONF_CLEAR_CHANNEL = "clear_channel"
CONF_CLIMATE_ID = "climate_id"
CONF_CO2_EQUIVALENT = "co2_equivalent"
@@ -49,6 +52,7 @@ CONF_HOST = "host"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_IAQ = "iaq"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_INPUT_CURRENT_LIMIT = "input_current_limit"
CONF_IS_WRGB = "is_wrgb"
CONF_KEYS = "keys"
CONF_LABEL = "label"
@@ -59,15 +63,21 @@ CONF_MAX_PACKET_SIZE = "max_packet_size"
CONF_MODEM = "modem"
CONF_MODULATION = "modulation"
CONF_NOX_INDEX = "nox_index"
CONF_ON_DOUBLE_TAP = "on_double_tap"
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
CONF_ON_PACKET = "on_packet"
CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification"
CONF_ON_PASSKEY_REQUEST = "on_passkey_request"
CONF_ON_RECEIVE = "on_receive"
CONF_ON_SCAN_END = "on_scan_end"
CONF_ON_STATE_CHANGE = "on_state_change"
CONF_ON_STOP = "on_stop"
CONF_ON_TAP = "on_tap"
CONF_OUT_PIN = "out_pin"
CONF_OUT_PIN_LEVEL = "out_pin_level"
CONF_PARITY = "parity"
CONF_PASSKEY = "passkey"
CONF_PRECHARGE_CURRENT = "precharge_current"
CONF_PROVIDER = "provider"
CONF_PWM_FREQUENCY = "pwm_frequency"
CONF_QUERY_PARAMS = "query_params"
@@ -87,6 +97,7 @@ CONF_SLOT = "slot"
CONF_SOURCES = "sources"
CONF_STATE_SAVE_INTERVAL = "state_save_interval"
CONF_STOP_BITS = "stop_bits"
CONF_TAP = "tap"
CONF_TARGET_COUNT = "target_count"
CONF_TEMPERATURE_SENSOR = "temperature_sensor"
CONF_USE_PSRAM = "use_psram"
@@ -38,4 +38,4 @@ async def to_code(config: ConfigType) -> None:
var = await binary_sensor.new_binary_sensor(config)
await cg.register_component(var, config)
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
await cg.add_variable(var.set_source_binary_sensor, config[CONF_SOURCE_ID])
@@ -6,9 +6,9 @@ namespace esphome::copy {
ESPHOME_LOG_TAG(TAG, "copy.binary_sensor");
void CopyBinarySensor::setup() {
source_->add_on_state_callback([this](bool value) { this->publish_state(value); });
if (source_->has_state())
this->publish_state(source_->state);
this->source_binary_sensor_->add_on_state_callback([this](bool value) { this->publish_state(value); });
if (this->source_binary_sensor_->has_state())
this->publish_state(this->source_binary_sensor_->state);
}
void CopyBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Copy Binary Sensor", this); }
@@ -6,13 +6,11 @@
namespace esphome::copy {
class CopyBinarySensor final : public binary_sensor::BinarySensor, public Component {
SUB_BINARY_SENSOR(source)
public:
void set_source(binary_sensor::BinarySensor *source) { source_ = source; }
void setup() override;
void dump_config() override;
protected:
binary_sensor::BinarySensor *source_;
};
} // namespace esphome::copy
File renamed without changes.
+1 -1
View File
@@ -38,4 +38,4 @@ async def to_code(config: ConfigType) -> None:
var = await number.new_number(config, min_value=0, max_value=0, step=0)
await cg.register_component(var, config)
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
await cg.add_variable(var.set_source_number, config[CONF_SOURCE_ID])
@@ -6,20 +6,20 @@ namespace esphome::copy {
ESPHOME_LOG_TAG(TAG, "copy.number");
void CopyNumber::setup() {
source_->add_on_state_callback([this](float value) { this->publish_state(value); });
this->source_number_->add_on_state_callback([this](float value) { this->publish_state(value); });
traits.set_min_value(source_->traits.get_min_value());
traits.set_max_value(source_->traits.get_max_value());
traits.set_step(source_->traits.get_step());
traits.set_min_value(this->source_number_->traits.get_min_value());
traits.set_max_value(this->source_number_->traits.get_max_value());
traits.set_step(this->source_number_->traits.get_step());
if (source_->has_state())
this->publish_state(source_->state);
if (this->source_number_->has_state())
this->publish_state(this->source_number_->state);
}
void CopyNumber::dump_config() { LOG_NUMBER("", "Copy Number", this); }
void CopyNumber::control(float value) {
auto call2 = source_->make_call();
auto call2 = this->source_number_->make_call();
call2.set_value(value);
call2.perform();
}
+2 -3
View File
@@ -6,15 +6,14 @@
namespace esphome::copy {
class CopyNumber final : public number::Number, public Component {
SUB_NUMBER(source)
public:
void set_source(number::Number *source) { source_ = source; }
void setup() override;
void dump_config() override;
protected:
void control(float value) override;
number::Number *source_;
};
} // namespace esphome::copy
+1 -1
View File
@@ -31,4 +31,4 @@ async def to_code(config: ConfigType) -> None:
await select.register_select(var, config, options=[])
await cg.register_component(var, config)
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
await cg.add_variable(var.set_source_select, config[CONF_SOURCE_ID])
@@ -6,11 +6,11 @@ namespace esphome::copy {
ESPHOME_LOG_TAG(TAG, "copy.select");
void CopySelect::setup() {
source_->add_on_state_callback([this](size_t index) { this->publish_state(index); });
this->source_select_->add_on_state_callback([this](size_t index) { this->publish_state(index); });
traits.set_options(source_->traits.get_options());
traits.set_options(this->source_select_->traits.get_options());
auto idx = this->source_->active_index();
auto idx = this->source_select_->active_index();
if (idx.has_value())
this->publish_state(*idx);
}
@@ -18,7 +18,7 @@ void CopySelect::setup() {
void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); }
void CopySelect::control(size_t index) {
auto call = source_->make_call();
auto call = this->source_select_->make_call();
call.set_index(index);
call.perform();
}
+2 -3
View File
@@ -6,15 +6,14 @@
namespace esphome::copy {
class CopySelect final : public select::Select, public Component {
SUB_SELECT(source)
public:
void set_source(select::Select *source) { source_ = source; }
void setup() override;
void dump_config() override;
protected:
void control(size_t index) override;
select::Select *source_;
};
} // namespace esphome::copy
File renamed without changes.
+1 -1
View File
@@ -36,4 +36,4 @@ async def to_code(config: ConfigType) -> None:
var = await switch.new_switch(config)
await cg.register_component(var, config)
await cg.add_variable(var.set_source, config[CONF_SOURCE_ID])
await cg.add_variable(var.set_source_switch, config[CONF_SOURCE_ID])
@@ -6,13 +6,13 @@ namespace esphome::copy {
ESPHOME_LOG_TAG(TAG, "copy.switch");
void CopySwitch::setup() {
source_->add_on_state_callback([this](float value) { this->publish_state(value); });
this->source_switch_->add_on_state_callback([this](float value) { this->publish_state(value); });
this->publish_state(source_->state);
this->publish_state(this->source_switch_->state);
}
void CopySwitch::dump_config() { LOG_SWITCH("", "Copy Switch", this); }
void CopySwitch::write_state(bool state) { this->source_->control(state); }
void CopySwitch::write_state(bool state) { this->source_switch_->control(state); }
} // namespace esphome::copy
+2 -3
View File
@@ -6,15 +6,14 @@
namespace esphome::copy {
class CopySwitch final : public switch_::Switch, public Component {
SUB_SWITCH(source)
public:
void set_source(switch_::Switch *source) { source_ = source; }
void setup() override;
void dump_config() override;
protected:
void write_state(bool state) override;
switch_::Switch *source_;
};
} // namespace esphome::copy
+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) {
@@ -13,8 +13,6 @@ CST816Touchscreen = cst816_ns.class_(
i2c.I2CDevice,
)
CST816ButtonListener = cst816_ns.class_("CST816ButtonListener")
CONF_SKIP_PROBE = "skip_probe"
CONFIG_SCHEMA = touchscreen.TOUCHSCREEN_SCHEMA.extend(
{
@@ -32,11 +32,6 @@ static const uint8_t CST816D_CHIP_ID = 0xB6;
static const uint8_t CST816T_CHIP_ID = 0xB5;
static const uint8_t CST716_CHIP_ID = 0x20;
class CST816ButtonListener {
public:
virtual void update_button(bool state) = 0;
};
class CST816Touchscreen final : public touchscreen::Touchscreen, public i2c::I2CDevice {
public:
void setup() override;
@@ -9,6 +9,9 @@
namespace esphome::current_based {
class CurrentBasedCover final : public cover::Cover, public Component {
SUB_SENSOR(open)
SUB_SENSOR(close)
public:
void setup() override;
void loop() override;
@@ -17,7 +20,6 @@ class CurrentBasedCover final : public cover::Cover, public Component {
Trigger<> *get_stop_trigger() { return &this->stop_trigger_; }
Trigger<> *get_open_trigger() { return &this->open_trigger_; }
void set_open_sensor(sensor::Sensor *open_sensor) { this->open_sensor_ = open_sensor; }
void set_open_moving_current_threshold(float open_moving_current_threshold) {
this->open_moving_current_threshold_ = open_moving_current_threshold;
}
@@ -27,7 +29,6 @@ class CurrentBasedCover final : public cover::Cover, public Component {
void set_open_duration(uint32_t open_duration) { this->open_duration_ = open_duration; }
Trigger<> *get_close_trigger() { return &this->close_trigger_; }
void set_close_sensor(sensor::Sensor *close_sensor) { this->close_sensor_ = close_sensor; }
void set_close_moving_current_threshold(float close_moving_current_threshold) {
this->close_moving_current_threshold_ = close_moving_current_threshold;
}
@@ -64,13 +65,11 @@ class CurrentBasedCover final : public cover::Cover, public Component {
Trigger<> stop_trigger_;
sensor::Sensor *open_sensor_{nullptr};
Trigger<> open_trigger_;
float open_moving_current_threshold_{0.0f};
float open_obstacle_current_threshold_{FLT_MAX};
uint32_t open_duration_{0};
sensor::Sensor *close_sensor_{nullptr};
Trigger<> close_trigger_;
float close_moving_current_threshold_{0.0f};
float close_obstacle_current_threshold_{FLT_MAX};
Loaded 100 of 842 files, more files were not shown because too many files have changed in this diff. Show more