Compare commits

..
367 changed files with 1810 additions and 9239 deletions
+3 -59
View File
@@ -92,7 +92,6 @@ jobs:
outputs:
core-ci: ${{ steps.determine.outputs.core-ci }}
integration-tests: ${{ steps.determine.outputs.integration-tests }}
integration-run-all: ${{ steps.determine.outputs.integration-run-all }}
integration-test-buckets: ${{ steps.determine.outputs.integration-test-buckets }}
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
@@ -153,9 +152,6 @@ jobs:
# Extract individual fields
echo "core-ci=$(echo "$output" | jq -r '.core_ci')" >> $GITHUB_OUTPUT
echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
# A missing key must fail here, not silently disable the junit upload
run_all=$(echo "$output" | jq -r 'if has("integration_run_all") then .integration_run_all else error("integration_run_all missing") end')
echo "integration-run-all=${run_all}" >> $GITHUB_OUTPUT
echo "integration-test-buckets=$(echo "$output" | jq -c '.integration_test_buckets')" >> $GITHUB_OUTPUT
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
@@ -359,24 +355,14 @@ jobs:
fail-fast: false
matrix:
bucket: ${{ fromJson(needs.determine-jobs.outputs.integration-test-buckets) }}
env:
# What the cache steps persist; libdeps is excluded (keyed per xdist
# worker and env, it never crosses runs).
INTEGRATION_PIO_CACHE_PATH: |
~/.esphome-integration-tests/platformio/platforms
~/.esphome-integration-tests/platformio/packages
~/.esphome-integration-tests/platformio/appstate.json
~/.esphome-integration-tests/platformio/.cache
~/.esphome-integration-tests/platformio/.esphome.pio.stamp.json
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- 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. Packages and version must match seed-apt-cache exactly.
# libsdl2-dev is needed by the headless display tests, which capture
# screenshots.
# path. Packages and version must match seed-apt-cache exactly;
# libsdl2-dev is unused here and carried only for cache-key parity.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
@@ -387,14 +373,6 @@ jobs:
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Restore integration PlatformIO cache
# Native platform + toolchain installed by shared_platformio_cache in
# tests/integration/conftest.py; a miss self-heals, so no restore-keys.
id: pio-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: integration-pio-v1-${{ runner.os }}-py${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements.txt', 'tests/integration/fixtures/cache_init.yaml', 'esphome/components/host/__init__.py') }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
@@ -432,46 +410,12 @@ jobs:
run: |
. venv/bin/activate
mapfile -t test_files < <(echo "$BUCKET_TESTS" | jq -r '.[]')
if [ "${#test_files[@]}" -eq 0 ]; then
echo "::error::Empty integration test bucket; pytest would collect the whole tree"
exit 1
fi
echo "Bucket ${{ matrix.bucket.name }}: running ${#test_files[@]} integration tests"
pytest -vv --no-cov --tb=native --durations=30 -n auto --dist worksteal \
--junitxml=junit-integration.xml "${test_files[@]}"
- name: Upload test artifacts
# 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/
if-no-files-found: ignore
retention-days: 7
- name: Upload junit timings
# Consumed by sync-integration-durations.yml through
# script/update_integration_test_durations.py; only full matrix dev
# runs produce usable data.
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: junit-integration-${{ strategy.job-index }}
path: junit-integration.xml
if-no-files-found: error
# A full cron period of margin for the weekly refresh
retention-days: 14
pytest -vv --no-cov --tb=native --durations=30 -n auto "${test_files[@]}"
- name: Print ccache statistics
# esphome stores the PlatformIO ccache under the machine-global cache
# dir (see _ccache_env() in esphome/platformio/toolchain.py).
run: CCACHE_DIR="$HOME/.cache/esphome/platformio-ccache" ccache -s
- name: Save integration PlatformIO cache
# Bucket 0 only; the others would race the same immutable key.
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.pio-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ env.INTEGRATION_PIO_CACHE_PATH }}
key: ${{ steps.pio-cache.outputs.cache-primary-key }}
import-time:
name: Check import esphome.__main__ time
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
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@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
with:
category: "/language:${{matrix.language}}"
@@ -1,98 +0,0 @@
---
name: Refresh integration test durations
on:
workflow_dispatch:
schedule:
- cron: "45 5 * * 1"
# Repo writes (branch push, PR open) happen via the App token minted below,
# so the workflow's GITHUB_TOKEN does not need any write scopes.
permissions:
contents: read
actions: read # gh api / gh run download for the CI junit artifacts
jobs:
sync:
name: Refresh integration test durations
runs-on: ubuntu-latest
if: github.repository == 'esphome/esphome'
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
permission-contents: write # push the sync branch
permission-pull-requests: write # open or refresh the sync PR
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Setup Python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
- name: Refresh from the newest usable dev run
env:
GH_TOKEN: ${{ github.token }}
run: |
# Only full matrix dev runs upload junit-integration-* artifacts
# (see the integration-tests job); the merge script re-checks
# coverage regardless.
# Newest-first candidates via their bucket-0 artifact. Fork PRs run
# their own ci.yml, so name and branch are spoofable; require
# same-repo. Assignment failures trip set -e and fail loudly.
candidates=$(
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts?name=junit-integration-0&per_page=100" \
--jq '.artifacts[] | select(.expired | not) | .workflow_run
| select(.head_branch == "dev" and .head_repository_id != null
and .head_repository_id == .repository_id)
| .id'
)
# Green runs first, then the rest newest first; a run missing a
# bucket fails the coverage check and the next one is tried
green=""
rest=""
for id in ${candidates}; do
conclusion=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${id}" --jq '.conclusion // ""')
if [ "${conclusion}" = "success" ]; then
green="${green} ${id}"
elif [ -n "${conclusion}" ]; then
rest="${rest} ${id}"
fi
done
# helpers.py imports colorama; the script needs nothing else
pip install colorama
for id in ${green} ${rest}; do
rm -rf /tmp/junit
if ! gh run download "${id}" --repo "${GITHUB_REPOSITORY}" -p "junit-integration-*" -D /tmp/junit; then
echo "::warning::Could not download artifacts for run ${id}; trying the next"
continue
fi
status=0
python script/update_integration_test_durations.py /tmp/junit || status=$?
if [ "${status}" -eq 0 ]; then
echo "Refreshed from run ${id}"
exit 0
fi
# Only EXIT_LOW_COVERAGE (3) from the script advances to the next run
[ "${status}" -eq 3 ] || exit 1
echo "::warning::Run ${id} covers too few test files; trying the next"
done
echo "::error::No dev CI run with usable junit artifacts in range; the feed is starved"
exit 1
- name: Commit changes
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
with:
commit-message: "[ci] Refresh integration test durations"
committer: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
author: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
branch: sync/integration-durations
delete-branch: true
title: "[ci] Refresh integration test durations"
body-path: .github/PULL_REQUEST_TEMPLATE.md
token: ${{ steps.generate-token.outputs.token }}
-2
View File
@@ -137,8 +137,6 @@ config/
!tests/component_tests/**/config/
tests/build/
tests/.esphome/
# Output kept by failing tests for inspection; uploaded by CI
test_artifacts/
/.temp-clang-tidy.cpp
/.temp/
.pio/
+2 -96
View File
@@ -44,16 +44,6 @@ This document provides essential context for AI models interacting with this pro
## 4. Coding Conventions & Style Guide
**Read the developer documentation before writing a component.** https://developers.esphome.io covers the
component lifecycle, the main loop, and the reasoning behind the rules below in far more depth than this
file does, and it is the authority when they disagree. The most useful starting points:
* https://developers.esphome.io/architecture/components/ - component lifecycle, `setup()`, `loop()`,
setup priorities, and how a component is registered.
* https://developers.esphome.io/architecture/components/advanced/ - choosing between `loop()`,
`set_interval`, `set_timeout` and `defer`; waking the loop from another thread; the RAM cost of each.
* https://developers.esphome.io/contributing/code/ - contribution rules, public API and breaking changes.
* **Formatting:**
* **Python:** Uses `ruff` and `flake8` for linting and formatting. Configuration is in `pyproject.toml`.
* **C++:** Uses `clang-format` for formatting. Configuration is in `.clang-format`.
@@ -152,47 +142,6 @@ file does, and it is the authority when they disagree. The most useful starting
* **Indentation:** Use spaces (two per indentation level), not tabs
* **Type aliases:** Prefer `using type_t = int;` over `typedef int type_t;`
* **Line length:** Wrap lines at no more than 120 characters
* **Timing in `loop()`:** Never call `millis()` in a `loop()` body. The current tick's timestamp is
already cached - use `App.get_loop_component_start_time()` (from `esphome/core/application.h`).
Only reach for `millis()` when you genuinely need sub-tick resolution inside a long operation.
* **The main loop runs every 16 ms.** A rate-limit gate shorter than that does nothing: the check
passes on essentially every pass of the loop, so it costs a comparison and buys nothing. Pick an
interval comfortably coarser than 16 ms, or drop the gate entirely and accept running every loop.
```cpp
// Bad - a 10ms gate against a 16ms loop never holds anything back
static constexpr uint32_t POLL_INTERVAL_MS = 10;
const uint32_t now = millis();
if (now - this->last_poll_ < POLL_INTERVAL_MS)
return;
this->last_poll_ = now;
```
```cpp
// Good - an interval that actually rate limits, off the cached timestamp
static constexpr uint32_t POLL_INTERVAL_MS = 100;
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_poll_ < POLL_INTERVAL_MS)
return;
this->last_poll_ = now;
```
Pick the primitive by cadence: under 250 ms use a gated `loop()`; 500 ms and above use
`set_interval`. Full reasoning, including why `set_interval` costs more below 500 ms:
https://developers.esphome.io/architecture/components/advanced/#quick-rule-of-thumb
* **Don't override a default with the same value:** if a base class method already returns what you
want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a
component that wants `DATA` should simply leave it alone.
```cpp
// Bad - this is exactly what the base class already does
float get_setup_priority() const override { return setup_priority::DATA; }
```
* **Logging string literals:** wrap literals passed as `%s` arguments in `LOG_STR_LITERAL()` so they
can be stored in flash rather than RAM.
```cpp
// Bad
ESP_LOGV(TAG, "Key %u %s", key, pressed ? "pressed" : "released");
// Good
ESP_LOGV(TAG, "Key %u %s", key, pressed ? LOG_STR_LITERAL("pressed") : LOG_STR_LITERAL("released"));
```
* **Constructor parameters vs setters:** Component properties that are both **required** and **invariant**
(never change after construction) should be constructor parameters rather than set via setter methods.
This makes the dependency explicit and prevents use of the object in an incompletely-initialized state.
@@ -613,33 +562,6 @@ file does, and it is the authority when they disagree. The most useful starting
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code.
**Listener and child-entity registration lists are the most common case, and the most commonly
missed.** A `register_*()` method called once per child at code generation time has a count that
is known at compile time, so it should never be a `std::vector`. Use `cg.slot_counter()`: it
returns a function that each consumer calls once per slot it will occupy, and after every
`to_code` has run it emits the define with the final count. When nothing registers, no define is
emitted and the storage plus its registration method compile out entirely.
```python
# hub component's __init__.py
_request_listener_slot = cg.slot_counter("MY_COMPONENT_LISTENER_COUNT")
async def register_listener(hub: MockObj, var: MockObj) -> None:
_request_listener_slot()
cg.add(hub.register_listener(var))
```
```cpp
#ifdef MY_COMPONENT_LISTENER_COUNT
void register_listener(MyComponentListener *listener);
#endif
protected:
#ifdef MY_COMPONENT_LISTENER_COUNT
StaticVector<MyComponentListener *, MY_COMPONENT_LISTENER_COUNT> listeners_;
#endif
```
Request slots from `to_code`, not from a job that runs after `CoroPriority.FINAL` - a late
request raises rather than silently undercounting.
3. **Runtime-known sizes:** Use `FixedVector` from `esphome/core/helpers.h` when the size is only known at runtime initialization.
```cpp
// Bad - generates STL realloc code (_M_realloc_insert)
@@ -677,25 +599,9 @@ file does, and it is the authority when they disagree. The most useful starting
```
Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise.
5. **Strings set once from configuration:** Use `StringRef` (`esphome/core/string_ref.h`) rather than
`std::string`. Code generation passes a string literal that lives in flash for the life of the
program, so storing a `std::string` copies it onto the heap for nothing. `StringRef` is a
non-owning pointer plus length; it does not copy, and it must only ever refer to storage that
outlives it (a string literal, or a buffer owned elsewhere).
```cpp
// Bad - heap copy of a literal that is already in flash
void set_keys(std::string keys) { this->keys_ = std::move(keys); }
std::string keys_;
```
```cpp
// Good - no allocation
void set_keys(const char *keys) { this->keys_ = StringRef(keys); }
StringRef keys_;
```
5. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
6. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices.
7. **Detection:** Look for these patterns in compiler output:
6. **Detection:** Look for these patterns in compiler output:
- Large code sections with STL symbols (vector, map, set)
- `alloc`, `realloc`, `dealloc` in symbol names
- `_M_realloc_insert`, `_M_default_append` (vector reallocation)
-3
View File
@@ -131,7 +131,6 @@ esphome/components/cst816/* @clydebarrow
esphome/components/cst9220/* @clydebarrow
esphome/components/ct_clamp/* @jesserockz
esphome/components/current_based/* @djwmarcx
esphome/components/d01/* @ch604
esphome/components/dac7678/* @NickB1
esphome/components/daikin_arc/* @MagicBear
esphome/components/daikin_brc/* @hagak
@@ -149,7 +148,6 @@ esphome/components/display_menu_base/* @numo68
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds1603l/* @JakeLC15
esphome/components/ds2484/* @mrk-its
esphome/components/ds248x/* @tomwellnitz
esphome/components/dsmr/* @glmnet @PolarGoose
@@ -496,7 +494,6 @@ esphome/components/sm2335/* @Cossid
esphome/components/sml/* @alengwenus
esphome/components/smt100/* @piechade
esphome/components/sn74hc165/* @jesserockz
esphome/components/snapshot/* @clydebarrow
esphome/components/socket/* @esphome/core
esphome/components/sonoff_d1/* @anatoly-savchenkov
esphome/components/sound_level/* @kahrendt
+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.10.0-dev
PROJECT_NUMBER = 2026.9.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
+3 -35
View File
@@ -23,8 +23,7 @@ For this repository there are two trusted inputs by design:
1. **The configuration.** Anyone who can supply or edit a YAML config is trusted
(see below).
2. **Authenticated peers of a running device** — clients holding the device's
API/OTA encryption key, API password, OTA password, or web server
credentials.
API encryption key / password, OTA password, or web server credentials.
The security boundary is therefore **unauthenticated network traffic vs. those
trusted inputs.** A bug that lets an unauthenticated attacker cross it is a
@@ -77,8 +76,8 @@ These *are* security bugs in this repo, and we want to hear about them privately
captive portal, etc.) **without** valid credentials.
- Authentication or encryption bypass on the device — reaching API calls, OTA
updates, or the web server without the configured key/password.
- Flaws that weaken the device's API or OTA encryption (Noise), OTA auth, or
web server auth below their documented guarantees.
- Flaws that weaken the device's API encryption (Noise), OTA, or web server auth
below their documented guarantees.
## The web server is an open HTTP API by design
@@ -122,37 +121,6 @@ and any memory-safety or protocol bug in the server reachable without credential
This section documents the current design and scope; it is not a judgment that the
design is optimal or that it will not change.
## OTA update encryption
The `esphome` OTA platform optionally encrypts updates with the same Noise
`NNpsk0` pattern the native API uses; one key protects the device. With an
`encryption:` block configured the guarantees are: the firmware image is
confidential in transit, the uploader is authenticated by the pre-shared key,
and the plaintext negotiation preceding the handshake is bound into the
handshake prologue, so stripping or tampering with it fails the first MAC.
Both ends fail closed with no override: a device built with a key refuses
plaintext uploads, and the CLI refuses to send plaintext when a key is
configured.
Defeating any of that without the key is in scope: a keyed device accepting a
plaintext or downgraded upload, getting past the MAC, or recovering image
contents from captured traffic.
The following are **not** vulnerabilities, by design:
- Plaintext OTA on a device with no `encryption:` block. That is the
documented default, authenticated (if at all) by the OTA password.
- The enablement window: turning encryption on takes one last upload of the
encryption-enabled firmware over the existing plaintext channel, with the
pre-existing plaintext exposure.
- The web OTA `/update` endpoint alongside encryption. The `web_server`
component keeps it always reachable, and `captive_portal:` auto-loads it
for the fallback AP window; validation warns about both combinations, and
the operator keeps the recovery path.
- CLI retry behavior on transport or MAC failures; every attempt renegotiates
a fresh handshake with fresh ephemerals, so retrying does not weaken
authentication.
## Explicitly out of scope
- Local attackers who already have shell access on the host that runs `esphome`.
+1 -1
View File
@@ -22,7 +22,7 @@ RUN \
-r /requirements.txt
# Install the ESPHome Device Builder dashboard.
RUN uv pip install --no-cache-dir esphome-device-builder==1.14.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.13.1
RUN \
platformio settings set enable_telemetry No \
+1 -5
View File
@@ -1,6 +1,5 @@
#!/usr/bin/env python3
import argparse
import os
import re
CHANNEL_DEV = "dev"
@@ -65,10 +64,7 @@ def main():
suffix = f"-{args.suffix}" if args.suffix else ""
repository = (
(os.environ.get("GITHUB_REPOSITORY") or "esphome/esphome").strip().lower()
)
image_name = f"{repository}{suffix}"
image_name = f"esphome/esphome{suffix}"
print(f"channel={channel}")
+18 -44
View File
@@ -26,9 +26,7 @@ from esphome.const import (
CONF_DEASSERT_RTS_DTR,
CONF_DISABLED,
CONF_DISCOVER_IP,
CONF_ENCRYPTION,
CONF_ESPHOME,
CONF_KEY,
CONF_LEVEL,
CONF_LOG,
CONF_LOG_TOPIC,
@@ -1338,19 +1336,6 @@ def _upload_via_native_api(
remote_port = int(ota_conf[CONF_PORT])
password = ota_conf.get(CONF_PASSWORD)
# Fail closed: an encryption block whose key did not resolve must never
# fall back to a plaintext upload
noise_psk = None
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
noise_psk = encryption_conf.get(CONF_KEY)
if not noise_psk:
raise EsphomeError(
"OTA encryption is configured but no key was resolved; "
"set the key under 'ota: encryption:' or 'api: encryption:'"
)
# Ensure the key is a string, as required by the underlying OTA implementation.
# It arrives here as a SensitiveStr which aioesphomeapi rejects.
noise_psk = str(noise_psk)
def check_partition_access(option_string: str) -> None:
if not ota_conf.get("allow_partition_access"):
@@ -1381,9 +1366,7 @@ def _upload_via_native_api(
if ota_type == espota2.OTA_TYPE_UPDATE_BOOTLOADER:
_validate_bootloader_binary(binary)
return espota2.run_ota(
network_devices, remote_port, password, binary, ota_type, noise_psk
)
return espota2.run_ota(network_devices, remote_port, password, binary, ota_type)
def _upload_via_web_server(
@@ -1392,16 +1375,6 @@ def _upload_via_web_server(
from esphome import web_server_ota
from esphome.web_server_helpers import get_web_server_connection
if any(
ota_item.get(CONF_PLATFORM) == CONF_ESPHOME
and ota_item.get(CONF_ENCRYPTION) is not None
for ota_item in config.get(CONF_OTA, [])
):
_LOGGER.warning(
"This config has OTA encryption, but the web_server OTA path sends "
"the image over plaintext HTTP; use the esphome OTA platform to "
"keep it confidential"
)
remote_port, username, password = get_web_server_connection(config)
return web_server_ota.run_ota(
network_devices, remote_port, username, password, binary
@@ -1697,26 +1670,20 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
if exit_code != 0:
return exit_code
if CORE.is_host:
_LOGGER.info(
"Successfully compiled program to path '%s'", _host_program_path(config)
)
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
program_path = str(toolchain.get_elf_path())
else:
from esphome.platformio.toolchain import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Successfully compiled program to path '%s'", program_path)
else:
_LOGGER.info("Successfully compiled program.")
return 0
def _host_program_path(config: ConfigType) -> str:
"""Return the compiled host ELF path."""
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
return str(toolchain.get_elf_path())
from esphome.platformio.toolchain import get_idedata
# Memoized by compile_program's own call; this is a dict lookup
return str(get_idedata(config).firmware_elf_path)
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
# Get devices, resolving special identifiers like OTA
devices = choose_upload_log_host(
@@ -1761,7 +1728,14 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
return exit_code
_LOGGER.info("Successfully compiled program.")
if CORE.is_host:
program_path = _host_program_path(config)
if CORE.using_toolchain_esp_idf:
from esphome.espidf import toolchain
program_path = str(toolchain.get_elf_path())
else:
from esphome.platformio.toolchain import get_idedata
program_path = str(get_idedata(config).firmware_elf_path)
_LOGGER.info("Running program from path '%s'", program_path)
return run_external_process(program_path)
+9 -4
View File
@@ -44,7 +44,8 @@ def get_arduino8266_tools_path() -> Path:
return tools_cache_path(*ARDUINO8266_TOOLS_CACHE)
# 3.1.1 rather than 3.1.0: the registry has no packages for 3.0.0, 3.0.1 or 3.1.0
# 3.1.1 rather than 3.1.0: the registry has no package for 3.1.0, and the
# encoder below cannot name 3.0.0/3.0.1 either (see its docstring)
MIN_FRAMEWORK_VERSION = Version(3, 1, 1)
@@ -52,16 +53,20 @@ def framework_package_version(ver: Version) -> str:
"""Map an Arduino core version to its registry package version (3.1.2 ->
3.30102.0; the leading 3 is the package major).
Exact registry names for 3.x cores; callers floor at MIN_FRAMEWORK_VERSION.
Exact registry names only for cores > 2.6.2 and >= 3.0.2; callers floor
at MIN_FRAMEWORK_VERSION.
"""
if ver.major > 3:
raise EsphomeError(
f"Arduino core {ver} is not supported yet; "
"the newest known core series is 3.x"
)
if ver.major < 3:
if ver <= Version(2, 6, 2):
# Cores <= 2.6.2 use the older 1.x/2.x package-major encodings (same
# boundary as _format_framework_arduino_version's era guard)
raise EsphomeError(
f"Arduino core {ver} is not supported; ESPHome requires core 3.x"
f"Arduino core {ver} uses an older package encoding than this "
"helper implements (newer than 2.6.2)"
)
return f"3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
-1
View File
@@ -78,7 +78,6 @@ from esphome.cpp_types import ( # noqa: F401
StringRef,
arduino_json_ns,
bool_,
char,
const_char_ptr,
double,
esphome_ns,
-23
View File
@@ -13,7 +13,6 @@ from esphome.components.esp32 import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32S31,
get_esp32_variant,
)
import esphome.config_validation as cv
@@ -157,17 +156,6 @@ ESP32_VARIANT_ADC1_PIN_TO_CHANNEL = {
9: adc_channel_t.ADC_CHANNEL_8,
10: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h
VARIANT_ESP32S31: {
42: adc_channel_t.ADC_CHANNEL_0,
43: adc_channel_t.ADC_CHANNEL_1,
44: adc_channel_t.ADC_CHANNEL_2,
45: adc_channel_t.ADC_CHANNEL_3,
46: adc_channel_t.ADC_CHANNEL_4,
47: adc_channel_t.ADC_CHANNEL_5,
48: adc_channel_t.ADC_CHANNEL_6,
49: adc_channel_t.ADC_CHANNEL_7,
},
}
# pin to adc2 channel mapping
@@ -237,17 +225,6 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
19: adc_channel_t.ADC_CHANNEL_8,
20: adc_channel_t.ADC_CHANNEL_9,
},
# https://github.com/espressif/esp-idf/blob/master/components/soc/esp32s31/include/soc/adc_channel.h
VARIANT_ESP32S31: {
50: adc_channel_t.ADC_CHANNEL_0,
51: adc_channel_t.ADC_CHANNEL_1,
52: adc_channel_t.ADC_CHANNEL_2,
53: adc_channel_t.ADC_CHANNEL_3,
54: adc_channel_t.ADC_CHANNEL_4,
55: adc_channel_t.ADC_CHANNEL_5,
56: adc_channel_t.ADC_CHANNEL_6,
57: adc_channel_t.ADC_CHANNEL_7,
},
}
+38 -40
View File
@@ -74,7 +74,9 @@ void ADCSensor::setup() {
if (this->calibration_handle_ == nullptr) {
adc_cali_handle_t handle = nullptr;
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
// RISC-V variants (except C2) and S3 use curve fitting calibration
adc_cali_curve_fitting_config_t cali_config = {}; // Zero initialize first
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
@@ -92,7 +94,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Curve fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else // ESP32, ESP32-S2, and ESP32-C2 use line fitting calibration
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = this->attenuation_,
@@ -110,11 +112,7 @@ void ADCSensor::setup() {
ESP_LOGW(TAG, "Line fitting calibration failed with error %d, will use uncalibrated readings", err);
this->setup_flags_.calibration_complete = false;
}
#else // No calibration scheme available
(void) handle;
ESP_LOGD(TAG, "No calibration scheme for this variant, readings are uncalibrated");
this->setup_flags_.calibration_complete = false;
#endif
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
}
this->setup_flags_.init_complete = true;
@@ -123,28 +121,23 @@ void ADCSensor::setup() {
void ADCSensor::dump_config() {
LOG_SENSOR("", "ADC Sensor", this);
LOG_PIN(" Pin: ", this->pin_);
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED) || defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
const char *calibration_status = this->setup_flags_.calibration_complete ? "OK" : "FAILED";
#else
const char *calibration_status = "N/A"; // This variant has no calibration scheme
#endif
ESP_LOGCONFIG(TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s\n"
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
this->setup_flags_.handle_init_complete ? "OK" : "FAILED",
this->setup_flags_.config_complete ? "OK" : "FAILED", calibration_status,
this->setup_flags_.init_complete ? "OK" : "FAILED");
ESP_LOGCONFIG(
TAG,
" Channel: %d\n"
" Unit: %s\n"
" Attenuation: %s\n"
" Samples: %i\n"
" Sampling mode: %s\n"
" Setup Status:\n"
" Handle Init: %s\n"
" Config: %s\n"
" Calibration: %s\n"
" Overall Init: %s",
this->channel_, LOG_STR_ARG(adc_unit_to_str(this->adc_unit_)),
this->autorange_ ? "Auto" : LOG_STR_ARG(attenuation_to_str(this->attenuation_)), this->sample_count_,
LOG_STR_ARG(sampling_mode_to_str(this->sampling_mode_)),
this->setup_flags_.handle_init_complete ? "OK" : "FAILED", this->setup_flags_.config_complete ? "OK" : "FAILED",
this->setup_flags_.calibration_complete ? "OK" : "FAILED", this->setup_flags_.init_complete ? "OK" : "FAILED");
LOG_UPDATE_INTERVAL(this);
}
@@ -191,11 +184,12 @@ float ADCSensor::sample_fixed_attenuation_() {
} else {
ESP_LOGW(TAG, "ADC calibration conversion failed with error %d, disabling calibration", err);
if (this->calibration_handle_ != nullptr) {
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else // Other ESP32 variants use line fitting calibration
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif
#endif // ESP32C3 || ESP32C5 || ESP32C6 || ESP32C61 || ESP32H2 || ESP32P4 || ESP32S3
this->calibration_handle_ = nullptr;
}
}
@@ -223,9 +217,10 @@ float ADCSensor::sample_autorange_() {
// Need to recalibrate for the new attenuation
if (this->calibration_handle_ != nullptr) {
// Delete old calibration handle
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(this->calibration_handle_);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_delete_scheme_line_fitting(this->calibration_handle_);
#endif
this->calibration_handle_ = nullptr;
@@ -234,7 +229,8 @@ float ADCSensor::sample_autorange_() {
// Create new calibration handle for this attenuation
adc_cali_handle_t handle = nullptr;
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_curve_fitting_config_t cali_config = {};
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
cali_config.chan = this->channel_;
@@ -246,7 +242,7 @@ float ADCSensor::sample_autorange_() {
err = adc_cali_create_scheme_curve_fitting(&cali_config, &handle);
ESP_LOGVV(TAG, "Autorange atten=%d: Calibration handle creation %s (err=%d)", atten,
(err == ESP_OK) ? "SUCCESS" : "FAILED", err);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_line_fitting_config_t cali_config = {
.unit_id = this->adc_unit_,
.atten = atten,
@@ -268,9 +264,10 @@ float ADCSensor::sample_autorange_() {
if (err != ESP_OK) {
ESP_LOGW(TAG, "ADC read failed in autorange with error %d", err);
if (handle != nullptr) {
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(handle);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
}
@@ -289,9 +286,10 @@ float ADCSensor::sample_autorange_() {
ESP_LOGVV(TAG, "Autorange atten=%d: UNCALIBRATED FALLBACK - raw=%d -> %.6fV (3.3V ref)", atten, raw, voltage);
}
// Clean up calibration handle
#if defined(ADC_CALI_SCHEME_CURVE_FITTING_SUPPORTED)
#if USE_ESP32_VARIANT_ESP32C3 || USE_ESP32_VARIANT_ESP32C5 || USE_ESP32_VARIANT_ESP32C6 || \
USE_ESP32_VARIANT_ESP32C61 || USE_ESP32_VARIANT_ESP32H2 || USE_ESP32_VARIANT_ESP32P4 || USE_ESP32_VARIANT_ESP32S3
adc_cali_delete_scheme_curve_fitting(handle);
#elif defined(ADC_CALI_SCHEME_LINE_FITTING_SUPPORTED)
#else
adc_cali_delete_scheme_line_fitting(handle);
#endif
} else {
-9
View File
@@ -3,7 +3,6 @@ import logging
import esphome.codegen as cg
from esphome.components import sensor, voltage_sampler
from esphome.components.esp32 import (
VARIANT_ESP32S31,
get_esp32_variant,
include_builtin_idf_component,
require_adc_oneshot_iram,
@@ -57,14 +56,6 @@ def validate_config(config: ConfigType) -> ConfigType:
if config[CONF_RAW] and config.get(CONF_ATTENUATION, None) == "auto":
raise cv.Invalid("Automatic attenuation cannot be used when raw output is set")
# The S31 ADC supports a single attenuation level (SOC_ADC_ATTEN_NUM is 1)
if (
CORE.is_esp32
and get_esp32_variant() == VARIANT_ESP32S31
and config.get(CONF_ATTENUATION, "0db") != "0db"
):
raise cv.Invalid("ESP32-S31 only supports 'attenuation: 0db'")
if config.get(CONF_ATTENUATION, None) == "auto" and config.get(CONF_SAMPLES, 1) > 1:
raise cv.Invalid(
"Automatic attenuation cannot be used when multisampling is set"
+1 -2
View File
@@ -162,9 +162,8 @@ void Alpha3::send_request_(uint8_t *request, size_t len) {
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->geni_handle_, len,
request, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
if (status) {
if (status)
ESP_LOGW(TAG, "[%s] esp_ble_gattc_write_char failed, status=%d", this->parent_->address_str(), status);
}
}
void Alpha3::update() {
+13 -125
View File
@@ -5,7 +5,6 @@ from typing import Any
from esphome import automation
from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components.const import CONF_DESCRIPTION
from esphome.components.logger import request_log_listener
# ENCRYPTION_SCHEMA and validate_encryption_key are re-exported for external
@@ -42,12 +41,10 @@ from esphome.const import (
CONF_TAG,
CONF_THEN,
CONF_TRIGGER_ID,
CONF_TYPE,
CONF_VARIABLES,
)
from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_priority
from esphome.cpp_generator import MockObj, TemplateArgsType
from esphome.helpers import fnv1_hash
from esphome.types import ConfigFragmentType, ConfigType
# Compat alias: downstream consumers (e.g. device-builder) referenced the
@@ -128,7 +125,6 @@ SERVICE_ARG_FALLBACK_TYPES: dict[str, MockObj] = {
}
CONF_BATCH_DELAY = "batch_delay"
CONF_CUSTOM_SERVICES = "custom_services"
CONF_EXAMPLE = "example"
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
CONF_LISTEN_BACKLOG = "listen_backlog"
@@ -232,30 +228,14 @@ def _validate_supports_response(value: Any) -> str:
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small
ESP8266_ACTION_STRINGS_MAX_TOTAL = 384
VARIABLE_SCHEMA = cv.Schema(
{
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
cv.Optional(CONF_EXAMPLE): cv.string_strict,
}
)
# Accepts the plain `name: type` shorthand or the full mapping form
validate_variable = cv.maybe_simple_value(VARIABLE_SCHEMA, key=CONF_TYPE)
ACTIONS_SCHEMA = automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(UserServiceTrigger),
cv.Exclusive(CONF_SERVICE, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Exclusive(CONF_ACTION, group_of_exclusion=CONF_ACTION): cv.valid_name,
cv.Optional(CONF_DESCRIPTION): cv.string_strict,
cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
{
cv.validate_id_name: validate_variable,
cv.validate_id_name: cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
}
),
# No default - auto-detected by _auto_detect_supports_response
@@ -372,85 +352,6 @@ CONFIG_SCHEMA = cv.All(
)
def _has_action_metadata(actions: list[ConfigType]) -> bool:
# Empty strings count as unset, matching _action_strings
return any(
conf.get(CONF_DESCRIPTION)
or any(
var_.get(CONF_DESCRIPTION) or var_.get(CONF_EXAMPLE)
for var_ in conf[CONF_VARIABLES].values()
)
for conf in actions
)
def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
"""Strings of one action in the table order UserServiceStatic (user_services.h) expects."""
# An empty description or example is treated as unset
strings: list[str | None] = [conf[CONF_ACTION]]
if has_metadata:
strings.append(conf.get(CONF_DESCRIPTION) or None)
for name, var_ in conf[CONF_VARIABLES].items():
strings.append(name)
if has_metadata:
strings += [
var_.get(CONF_DESCRIPTION) or None,
var_.get(CONF_EXAMPLE) or None,
]
return strings
def _action_strings_size(strings: list[str | None]) -> int:
"""Bytes needed to copy every string out of flash, each with its terminator."""
return sum(
len(string.encode("utf-8")) + 1 for string in strings if string is not None
)
def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
if not CORE.is_esp8266:
return config
actions = config.get(CONF_ACTIONS, [])
has_metadata = _has_action_metadata(actions)
for conf in actions:
size = _action_strings_size(_action_strings(conf, has_metadata))
if size > ESP8266_ACTION_STRINGS_MAX_TOTAL:
raise cv.Invalid(
f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, "
f"description and example text; ESP8266 allows at most "
f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action"
)
return config
FINAL_VALIDATE_SCHEMA = _validate_esp8266_action_strings
def _add_action_strings(
index: int, strings: list[str | None], interned: dict[str, MockObj]
) -> MockObj:
"""Emit the PROGMEM string table for one action.
Each string is its own PROGMEM array because on ESP8266 .rodata is RAM, and identical
strings are shared between actions through `interned`.
"""
entries: list[MockObj] = []
for string in strings:
if string is None:
entries.append(cg.nullptr)
continue
if (var := interned.get(string)) is None:
var = interned[string] = cg.progmem_array(
ID(f"api_action_str{len(interned)}", is_declaration=True, type=cg.char),
string,
)
entries.append(var)
return cg.progmem_array(
ID(f"api_action{index}_strings", is_declaration=True, type=cg.const_char_ptr),
entries,
)
@coroutine_with_priority(CoroPriority.WEB)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -470,10 +371,8 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
actions = config.get(CONF_ACTIONS, [])
has_user_actions = bool(actions) or config[CONF_CUSTOM_SERVICES]
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
if has_user_actions:
if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
cg.add_define("USE_API_USER_DEFINED_ACTIONS")
# Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
@@ -486,17 +385,10 @@ async def to_code(config: ConfigType) -> None:
if config[CONF_HOMEASSISTANT_STATES]:
cg.add_define("USE_API_HOMEASSISTANT_STATES")
scratch_size = 0
if actions:
# Metadata is compiled in for every action once any action declares it, because the
# string table layout is fixed by the define rather than per action
has_metadata = _has_action_metadata(actions)
if has_metadata:
cg.add_define("USE_API_USER_DEFINED_ACTION_METADATA")
interned_strings: dict[str, MockObj] = {}
if actions := config.get(CONF_ACTIONS, []):
# Collect all triggers first, then register all at once with initializer_list
triggers: list[cg.MockObj] = []
for index, conf in enumerate(actions):
for conf in actions:
func_args: list[tuple[MockObj, str]] = []
service_template_args: list[MockObj] = [] # User service argument types
@@ -529,23 +421,22 @@ async def to_code(config: ConfigType) -> None:
conf.get(CONF_THEN, [])
)
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items():
var_type = var_[CONF_TYPE]
if has_non_synchronous and var_type in SERVICE_ARG_FALLBACK_TYPES:
native = SERVICE_ARG_FALLBACK_TYPES[var_type]
if has_non_synchronous and var_ in SERVICE_ARG_FALLBACK_TYPES:
native = SERVICE_ARG_FALLBACK_TYPES[var_]
else:
native = SERVICE_ARG_NATIVE_TYPES[var_type]
native = SERVICE_ARG_NATIVE_TYPES[var_]
service_template_args.append(native)
func_args.append((native, name))
strings = _action_strings(conf, has_metadata)
table = _add_action_strings(index, strings, interned_strings)
if CORE.is_esp8266:
scratch_size = max(scratch_size, _action_strings_size(strings))
service_arg_names.append(name)
# Template args: supports_response mode, then user service arg types
templ = cg.TemplateArguments(supports_response, *service_template_args)
# Key is hashed here because the name is not readable at runtime on ESP8266
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], templ, table, fnv1_hash(conf[CONF_ACTION])
conf[CONF_TRIGGER_ID],
templ,
conf[CONF_ACTION],
service_arg_names,
)
triggers.append(trigger)
auto = await automation.build_automation(trigger, func_args, conf)
@@ -567,9 +458,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(auto.add_actions([unregister_action]))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CORE.is_esp8266 and has_user_actions:
# Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action
cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1))
if CONF_ON_CLIENT_CONNECTED in config:
cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
-3
View File
@@ -1034,8 +1034,6 @@ message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
ServiceArgType type = 2;
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ListEntitiesServicesResponse {
option (id) = 41;
@@ -1046,7 +1044,6 @@ message ListEntitiesServicesResponse {
fixed32 key = 2 [(force) = true];
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
+1 -2
View File
@@ -2391,9 +2391,8 @@ void APIConnection::process_batch_() {
} else if (payload_size == 0) {
// payload_size == 0 with remove set means encoding hit OOM and the
// connection is being dropped; warn only for a genuinely oversized message
if (!this->flags_.remove) {
if (!this->flags_.remove)
ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
}
this->clear_batch_();
}
return;
-18
View File
@@ -1275,24 +1275,12 @@ uint8_t *ListEntitiesServicesArgument::encode(ProtoWriteBuffer &buffer PROTO_ENC
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, this->name);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->example);
#endif
return pos;
}
uint32_t ListEntitiesServicesArgument::calculate_size() const {
uint32_t size = 0;
size += ProtoSize::calc_length(1, this->name.size());
size += this->type ? 2 : 0;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->description.size());
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->example.size());
#endif
return size;
}
uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1303,9 +1291,6 @@ uint8_t *ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer PROTO_ENC
ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, buffer, 3, it);
}
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, this->description);
#endif
return pos;
}
uint32_t ListEntitiesServicesResponse::calculate_size() const {
@@ -1318,9 +1303,6 @@ uint32_t ListEntitiesServicesResponse::calculate_size() const {
}
}
size += this->supports_response ? 2 : 0;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
size += ProtoSize::calc_length(1, this->description.size());
#endif
return size;
}
bool ExecuteServiceArgument::decode_varint(uint32_t field_id, proto_varint_value_t value) {
+1 -10
View File
@@ -1317,12 +1317,6 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
public:
StringRef name{};
enums::ServiceArgType type{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef example{};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1334,7 +1328,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
class ListEntitiesServicesResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 41;
static constexpr uint8_t ESTIMATED_SIZE = 59;
static constexpr uint8_t ESTIMATED_SIZE = 50;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
#endif
@@ -1342,9 +1336,6 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{};
#endif
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
-9
View File
@@ -1500,12 +1500,6 @@ const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument"));
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("example"), this->example);
#endif
return out.c_str();
}
const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
@@ -1518,9 +1512,6 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
out.append("\n");
}
dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("description"), this->description);
#endif
return out.c_str();
}
const char *ExecuteServiceArgument::dump_to(DumpBuffer &out) const {
+2 -4
View File
@@ -433,10 +433,8 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
// Home Assistant subscribes to actions shortly *after* authenticating, so actions
// fired right at connection time (on_client_connected, on_time_sync, ...) can
// arrive before the subscription and are lost - warn instead of failing silently.
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s",
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"), call.service.c_str(),
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
: LOG_STR_LITERAL("no client connected"));
ESP_LOGW(TAG, "Home Assistant %s '%s' dropped; %s", call.is_event ? "event" : "action", call.service.c_str(),
this->is_connected() ? "client has not subscribed to actions (yet)" : "no client connected");
}
}
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
+1 -2
View File
@@ -99,8 +99,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
UserActionScratch scratch;
auto resp = service->encode_list_service_response(scratch);
auto resp = service->encode_list_service_response();
if (!this->client_->send_message(resp))
return false;
// at_ is this service's index
-43
View File
@@ -1,52 +1,9 @@
#include "user_services.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
StringRef UserServiceStatic::str_(size_t idx, std::span<char> &scratch) const {
const char *s = progmem_read_ptr(&this->strings_[idx]);
if (s == nullptr)
return {};
#ifdef USE_ESP8266
// Codegen sizes the scratch buffer for the largest service; the bound only guards other callers
if (scratch.empty())
return {};
size_t len = strnlen_P(s, scratch.size() - 1);
progmem_memcpy(scratch.data(), s, len);
scratch[len] = '\0';
StringRef ref(scratch.data(), len);
scratch = scratch.subspan(len + 1);
return ref;
#else
return StringRef(s);
#endif
}
ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
std::span<const enums::ServiceArgType> arg_types, std::span<char> scratch) const {
ListEntitiesServicesResponse msg;
msg.name = this->str_(0, scratch);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
msg.description = this->str_(1, scratch);
#endif
msg.args.init(arg_types.size());
for (size_t i = 0; i < arg_types.size(); i++) {
size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS;
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = this->str_(base, scratch);
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
arg.description = this->str_(base + 1, scratch);
arg.example = this->str_(base + 2, scratch);
#endif
}
return msg;
}
template<> bool get_execute_arg_value<bool>(const ExecuteServiceArgument &arg) { return arg.bool_; }
template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &arg) {
if (arg.legacy_int != 0)
+36 -55
View File
@@ -1,6 +1,5 @@
#pragma once
#include <span>
#include <tuple>
#include <utility>
#include <vector>
@@ -20,9 +19,7 @@ class APIServer;
class UserServiceDescriptor {
public:
/// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into
/// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it.
virtual ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) = 0;
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -37,51 +34,29 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
template<typename T> enums::ServiceArgType to_service_arg_type();
// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
#ifdef USE_ESP8266
using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
#else
using UserActionScratch = std::array<char, 0>;
#endif
// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
// each argument name; with USE_API_USER_DEFINED_ACTION_METADATA the action description follows
// the name and every argument is (name, description, example). Unset metadata is nullptr.
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
static constexpr size_t USER_ACTION_HEADER_STRINGS = 2;
static constexpr size_t USER_ACTION_ARG_STRINGS = 3;
#else
static constexpr size_t USER_ACTION_HEADER_STRINGS = 1;
static constexpr size_t USER_ACTION_ARG_STRINGS = 1;
#endif
class UserServiceStatic : public UserServiceDescriptor {
// Base class for YAML-defined services (most common case)
// Stores only pointers to string literals in flash - no heap allocation
template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
public:
UserServiceStatic(const char *const *strings, uint32_t key,
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
: strings_(strings), key_(key), supports_response_(supports_response) {}
UserServiceBase(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names,
enums::SupportsResponseType supports_response = enums::SUPPORTS_RESPONSE_NONE)
: name_(name), arg_names_(arg_names), supports_response_(supports_response) {
this->key_ = fnv1_hash(name);
}
protected:
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
std::span<char> scratch) const;
/// Reference table entry `idx`; nullptr gives an empty StringRef.
/// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span
/// is advanced past the copy.
StringRef str_(size_t idx, std::span<char> &scratch) const;
const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
uint32_t key_;
enums::SupportsResponseType supports_response_;
};
template<typename... Ts> class UserServiceBase : public UserServiceStatic {
public:
using UserServiceStatic::UserServiceStatic;
ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) override {
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
return this->encode_list_service_response_(arg_types, scratch);
msg.args.init(sizeof...(Ts));
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = StringRef(this->arg_names_[i]);
}
return msg;
}
bool execute_service(const ExecuteServiceRequest &req) override {
@@ -114,6 +89,12 @@ template<typename... Ts> class UserServiceBase : public UserServiceStatic {
void execute_(const ArgsContainer &args, uint32_t call_id, bool return_response, std::index_sequence<S...> /*type*/) {
this->execute(call_id, return_response, (get_execute_arg_value<Ts>(args[S]))...);
}
// Pointers to string literals in flash - no heap allocation
const char *name_;
std::array<const char *, sizeof...(Ts)> arg_names_;
uint32_t key_{0};
enums::SupportsResponseType supports_response_{enums::SUPPORTS_RESPONSE_NONE};
};
// Separate class for custom_api_device services (rare case)
@@ -125,7 +106,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
this->key_ = fnv1_hash(this->name_.c_str());
}
ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.key = this->key_;
@@ -186,8 +167,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_NONE, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_NONE) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_NONE) {}
protected:
void execute(uint32_t /*call_id*/, bool /*return_response*/, Ts... x) override { this->trigger(x...); }
@@ -198,8 +179,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_OPTIONAL, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, bool, Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_OPTIONAL) {}
protected:
void execute(uint32_t call_id, bool return_response, Ts... x) override {
@@ -212,8 +193,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_ONLY, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_ONLY) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_ONLY) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
@@ -224,8 +205,8 @@ template<typename... Ts>
class UserServiceTrigger<enums::SUPPORTS_RESPONSE_STATUS, Ts...> final : public UserServiceBase<Ts...>,
public Trigger<uint32_t, Ts...> {
public:
UserServiceTrigger(const char *const *strings, uint32_t key)
: UserServiceBase<Ts...>(strings, key, enums::SUPPORTS_RESPONSE_STATUS) {}
UserServiceTrigger(const char *name, const std::array<const char *, sizeof...(Ts)> &arg_names)
: UserServiceBase<Ts...>(name, arg_names, enums::SUPPORTS_RESPONSE_STATUS) {}
protected:
void execute(uint32_t call_id, bool /*return_response*/, Ts... x) override { this->trigger(call_id, x...); }
+2 -4
View File
@@ -23,10 +23,8 @@ void AQISensor::setup() {
void AQISensor::dump_config() {
ESP_LOGCONFIG(TAG, "AQI Sensor:");
ESP_LOGCONFIG(TAG, " Calculation Type: %s",
this->aqi_calc_type_ == AQI_TYPE ? LOG_STR_LITERAL("AQI") : LOG_STR_LITERAL("CAQI"));
ESP_LOGCONFIG(TAG, " Extended Range: %s",
this->extended_range_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"));
ESP_LOGCONFIG(TAG, " Calculation Type: %s", this->aqi_calc_type_ == AQI_TYPE ? "AQI" : "CAQI");
ESP_LOGCONFIG(TAG, " Extended Range: %s", this->extended_range_ ? "enabled" : "disabled");
if (this->pm_2_5_sensor_ != nullptr) {
ESP_LOGCONFIG(TAG, " PM2.5 Sensor: '%s'", this->pm_2_5_sensor_->get_name().c_str());
}
+2 -3
View File
@@ -6,7 +6,7 @@ import esphome.codegen as cg
from esphome.components.esp32 import (
add_idf_component,
add_idf_sdkconfig_option,
include_builtin_idf_component,
request_http_client,
require_certificate_bundle,
)
import esphome.config_validation as cv
@@ -334,8 +334,7 @@ def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> N
async def to_code(config: ConfigType) -> None:
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
include_builtin_idf_component("esp_http_client")
request_http_client()
# HTTPS streams verify the server against the root certificate bundle
require_certificate_bundle()
@@ -100,15 +100,13 @@ void MultiClickTriggerBase::schedule_is_valid_(uint32_t min_length) {
}
this->is_valid_ = false;
this->set_timeout(MULTICLICK_IS_VALID_ID, min_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You can now %s the button.",
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
ESP_LOGV(TAG, "Multi Click: You can now %s the button.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = true;
});
}
void MultiClickTriggerBase::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout(MULTICLICK_IS_NOT_VALID_ID, max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.",
this->parent_->state ? LOG_STR_LITERAL("RELEASE") : LOG_STR_LITERAL("PRESS"));
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = false;
this->schedule_cooldown_();
});
+1 -2
View File
@@ -62,9 +62,8 @@ BdkActivityState bdk_scan_state(uint8_t activity_idx) {
uint8_t bdk_scan_acquire_activity() {
uint8_t idx = app_ble_get_idle_actv_idx_handle(SCAN_ACTV);
if (idx == INVALID_ACTIVITY_IDX) {
if (idx == INVALID_ACTIVITY_IDX)
ESP_LOGE(TAG, "Scan start failed: no idle activity handle");
}
return idx;
}
+5 -10
View File
@@ -181,9 +181,8 @@ void BK72xxBLE::enable() {
break;
}
}
if (!bdaddr_live) {
if (!bdaddr_live)
ESP_LOGW(TAG, "Controller address still unset after init; BLE stack may not have started");
}
#endif
this->state_ = BLEComponentState::ACTIVE;
@@ -211,9 +210,8 @@ void BK72xxBLE::loop() {
// Re-check a settled scan; scan_start() refills the bring-up budget.
// WARN: the only report of a drop that recovers inside its budget.
if (this->scan_start(this->requested_.interval, this->requested_.window, this->requested_.active) !=
ScanOpResult::SETTLED) {
ScanOpResult::SETTLED)
ESP_LOGW(TAG, "Controller dropped the scan; restarting");
}
}
// Drain the lock-free ring filled by the BLE task; all per-report work runs
@@ -232,9 +230,8 @@ void BK72xxBLE::loop() {
// Log dropped reports — only reachable when reports were processed; drops can
// only occur while the queue is full, and only this loop drains it.
uint16_t dropped = this->report_queue_.get_and_reset_dropped_count();
if (dropped > 0) {
if (dropped > 0)
ESP_LOGW(TAG, "Dropped %u scan reports due to queue overflow", dropped);
}
}
void BK72xxBLE::get_mac_lsb_first(uint8_t out[MAC_ADDRESS_SIZE]) const {
@@ -452,9 +449,8 @@ ScanOpResult BK72xxBLE::advance_stop_(BdkActivityState state, bool ready) {
if (!ready) {
// Acting mid-operation could delete an activity whose start lands
// afterwards, leaking the slot with the radio on; wait.
if (this->last_result_ == ScanOpResult::SETTLED) {
if (this->last_result_ == ScanOpResult::SETTLED)
ESP_LOGD(TAG, "Scan stop deferred (controller busy)");
}
return ScanOpResult::PENDING;
}
// Settled, so CREATED unambiguously means "never started".
@@ -478,9 +474,8 @@ ScanOpResult BK72xxBLE::advance_start_(BdkActivityState state, bool ready) {
return ScanOpResult::PENDING;
}
if (!ready) {
if (this->last_result_ == ScanOpResult::SETTLED) {
if (this->last_result_ == ScanOpResult::SETTLED)
ESP_LOGD(TAG, "Scan start deferred (controller busy)");
}
return ScanOpResult::PENDING;
}
if (state == BdkActivityState::CREATED) {
@@ -69,9 +69,8 @@ void BK72xxBLETracker::on_ota_global_state(ota::OTAState state, float progress,
this->stop_scan();
// The transfer starves the loop; a deferred stop would leave the radio
// scanning for the whole update, so drain it here, bounded.
if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS)) {
if (!this->parent_->flush_pending_stop(OTA_STOP_FLUSH_MS))
ESP_LOGE(TAG, "Scan still stopping at OTA start; the radio may contend with the update");
}
} else if (state == ota::OTA_ERROR || state == ota::OTA_ABORT) {
// On success the device reboots, so restore only on a failed/aborted update;
// loop() restarts the scan on its next iteration (continuous idle branch).
@@ -80,9 +80,8 @@ void BLEBinaryOutput::write_state(bool state) {
esp_err_t err =
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_,
sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE);
if (err != ESP_GATT_OK) {
if (err != ESP_GATT_OK)
ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_str(char_buf), err);
}
}
} // namespace esphome::ble_client
+2 -4
View File
@@ -327,12 +327,10 @@ void BME680Component::read_data_() {
ESP_LOGD(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%% gas_resistance=%.1fΩ", temperature, pressure,
humidity, gas_resistance);
if (!gas_valid) {
if (!gas_valid)
ESP_LOGW(TAG, "Gas measurement unsuccessful, reading invalid!");
}
if (!heat_stable) {
if (!heat_stable)
ESP_LOGW(TAG, "Heater unstable, reading invalid! (Normal for a few readings after a power cycle)");
}
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
@@ -162,9 +162,8 @@ void BME680BSECComponent::dump_config() {
" Supply Voltage: %sV\n"
" Sample Rate: %s\n"
" State Save Interval: %" PRIu32 "ms",
this->temperature_offset_,
this->iaq_mode_ == IAQ_MODE_STATIC ? LOG_STR_LITERAL("Static") : LOG_STR_LITERAL("Mobile"),
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? LOG_STR_LITERAL("3.3") : LOG_STR_LITERAL("1.8"),
this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile",
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8",
BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_);
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
+8 -14
View File
@@ -368,8 +368,8 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
}
void Climate::save_state_(const ClimateTraits &traits) {
#if (defined(USE_ESP32) || defined(USE_ESP8266)) && !defined(CLANG_TIDY)
#pragma GCC diagnostic push
#if (defined(USE_ESP32) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
!defined(CLANG_TIDY)
#pragma GCC diagnostic ignored "-Wclass-memaccess"
#define TEMP_IGNORE_MEMACCESS
#endif
@@ -749,39 +749,33 @@ void Climate::dump_traits_(const char *tag) {
}
if (!traits.get_supported_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported modes:");
for (ClimateMode m : traits.get_supported_modes()) {
for (ClimateMode m : traits.get_supported_modes())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_mode_to_string(m)));
}
}
if (!traits.get_supported_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported fan modes:");
for (ClimateFanMode m : traits.get_supported_fan_modes()) {
for (ClimateFanMode m : traits.get_supported_fan_modes())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
}
}
if (!traits.get_supported_custom_fan_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported custom fan modes:");
for (const char *s : traits.get_supported_custom_fan_modes()) {
for (const char *s : traits.get_supported_custom_fan_modes())
ESP_LOGCONFIG(tag, " - %s", s);
}
}
if (!traits.get_supported_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported presets:");
for (ClimatePreset p : traits.get_supported_presets()) {
for (ClimatePreset p : traits.get_supported_presets())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_preset_to_string(p)));
}
}
if (!traits.get_supported_custom_presets().empty()) {
ESP_LOGCONFIG(tag, " Supported custom presets:");
for (const char *s : traits.get_supported_custom_presets()) {
for (const char *s : traits.get_supported_custom_presets())
ESP_LOGCONFIG(tag, " - %s", s);
}
}
if (!traits.get_supported_swing_modes().empty()) {
ESP_LOGCONFIG(tag, " Supported swing modes:");
for (ClimateSwingMode m : traits.get_supported_swing_modes()) {
for (ClimateSwingMode m : traits.get_supported_swing_modes())
ESP_LOGCONFIG(tag, " - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
}
}
}
+1
View File
@@ -100,6 +100,7 @@ bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t comma
void CM1106Component::dump_config() {
ESP_LOGCONFIG(TAG, "CM1106:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(9600);
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
-8
View File
@@ -46,14 +46,6 @@ CONFIG_SCHEMA = (
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cm1106",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
"""Code generation entry point."""
-3
View File
@@ -14,10 +14,8 @@ CONF_CHANNEL_COLORS = "channel_colors"
CONF_CLIMATE_ID = "climate_id"
CONF_CO2_EQUIVALENT = "co2_equivalent"
CONF_COLOR_DEPTH = "color_depth"
CONF_COLUMNS = "columns"
CONF_CRC_ENABLE = "crc_enable"
CONF_DATA_BITS = "data_bits"
CONF_DESCRIPTION = "description"
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ENABLE_OTA_DOWNGRADE_PROTECTION = "enable_ota_downgrade_protection"
CONF_ENABLED = "enabled"
@@ -26,7 +24,6 @@ CONF_GYROSCOPE_RANGE = "gyroscope_range"
CONF_IAQ = "iaq"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_IS_WRGB = "is_wrgb"
CONF_KEYS = "keys"
CONF_LABEL = "label"
CONF_LIBRETINY = "libretiny"
CONF_LOOP = "loop"
+5 -8
View File
@@ -249,7 +249,7 @@ bool CS5460AComponent::check_status_() {
bool dir = status & (1 << 21);
if (current_gain_ < 0)
dir = !dir;
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? LOG_STR_LITERAL("negative") : LOG_STR_LITERAL("positive"));
ESP_LOGI(TAG, "Energy counter %s pulse", dir ? "negative" : "positive");
clear |= 1 << 22;
}
@@ -319,9 +319,7 @@ void CS5460AComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"CS5460A:\n"
" Init status: %s",
state == COMPONENT_STATE_LOOP
? LOG_STR_LITERAL("OK")
: (state == COMPONENT_STATE_FAILED ? LOG_STR_LITERAL("failed") : LOG_STR_LITERAL("other")));
state == COMPONENT_STATE_LOOP ? "OK" : (state == COMPONENT_STATE_FAILED ? "failed" : "other"));
LOG_PIN(" CS Pin: ", cs_);
ESP_LOGCONFIG(TAG,
" Samples / cycle: %" PRIu32 "\n"
@@ -332,10 +330,9 @@ void CS5460AComponent::dump_config() {
" Current HPF: %s\n"
" Voltage HPF: %s\n"
" Pulse energy: %.2f Wh",
samples_, phase_offset_,
pga_gain_ == CS5460A_PGA_GAIN_50X ? LOG_STR_LITERAL("50x") : LOG_STR_LITERAL("10x"), current_gain_,
voltage_gain_, current_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"),
voltage_hpf_ ? LOG_STR_LITERAL("enabled") : LOG_STR_LITERAL("disabled"), pulse_energy_wh_);
samples_, phase_offset_, pga_gain_ == CS5460A_PGA_GAIN_50X ? "50x" : "10x", current_gain_,
voltage_gain_, current_hpf_ ? "enabled" : "disabled", voltage_hpf_ ? "enabled" : "disabled",
pulse_energy_wh_);
LOG_SENSOR(" ", "Voltage", voltage_sensor_);
LOG_SENSOR(" ", "Current", current_sensor_);
LOG_SENSOR(" ", "Power", power_sensor_);
+1
View File
@@ -58,6 +58,7 @@ void CSE7761Component::dump_config() {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
LOG_UPDATE_INTERVAL(this);
this->check_uart_settings(38400, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
}
void CSE7761Component::update() {
+1 -7
View File
@@ -68,13 +68,7 @@ CONFIG_SCHEMA = (
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cse7761",
baud_rate=38400,
require_rx=True,
require_tx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
"cse7761", baud_rate=38400, require_rx=True, require_tx=True
)
+1
View File
@@ -255,6 +255,7 @@ void CSE7766Component::dump_config() {
LOG_SENSOR(" ", "Apparent Power", this->apparent_power_sensor_);
LOG_SENSOR(" ", "Reactive Power", this->reactive_power_sensor_);
LOG_SENSOR(" ", "Power Factor", this->power_factor_sensor_);
this->check_uart_settings(4800, 1, uart::UART_CONFIG_PARITY_EVEN);
}
} // namespace esphome::cse7766
+1 -6
View File
@@ -84,12 +84,7 @@ CONFIG_SCHEMA = (
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cse7766",
baud_rate=4800,
require_rx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
"cse7766", baud_rate=4800, parity="EVEN", require_rx=True
)
View File
-45
View File
@@ -1,45 +0,0 @@
#include "d01.h"
#include "esphome/core/log.h"
// uart specification for d01 sensor from https://manuals.plus/ae/1005006417362019:
//
// A frame of serial output data includes 4 bytes, formatted as follows:
// __Characteristic byte: Fixed value 0xA5.
// __Data byte: DATAH is the high 7 bits of the concentration value, and DATAL is the low 7 bits of the concentration
// value.
// __Check byte: The low 7 bits of the sum of all bytes before the check byte.
//
// If the serial output is 4 bytes of data: 0*A5 0*01 0*2C 0*52, then DATAH = 0*01 = 1, DATAL = 0*2C = 44.
// Concentration value = 1*128 + 44 = 172 µg/m³.
//
// The PM2.5 dust concentration value obtained from the dust sensor needs to be calibrated with a K value coefficient
// based on the TSI instrument's photometric method. It is generally recommended to use 0.4.
namespace esphome::d01 {
static const char *const TAG = "d01";
static const uint8_t D01_FRAME_HEADER = 0xA5;
void D01SensorComponent::dump_config() { LOG_SENSOR(" ", "D01 PM2.5", this); }
void D01SensorComponent::loop() {
uint8_t buf[4];
while (this->available() >= 4) {
if (this->peek() != D01_FRAME_HEADER) {
this->read();
continue;
}
this->read_array(buf, 4);
uint8_t sum = (buf[0] + buf[1] + buf[2]) & 0x7F;
if (sum != buf[3]) {
ESP_LOGW(TAG, "checksum mismatch");
continue;
}
uint16_t latest_concentration = (buf[1] & 0x7F) * 128 + (buf[2] & 0x7F);
ESP_LOGV(TAG, "Unadjusted PM2.5 Concentration: %d µg/m³", latest_concentration);
this->publish_state(latest_concentration);
}
}
} // namespace esphome::d01
-14
View File
@@ -1,14 +0,0 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome::d01 {
class D01SensorComponent final : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
void dump_config() override;
void loop() override;
};
} // namespace esphome::d01
-45
View File
@@ -1,45 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_PM25,
ICON_BLUR,
STATE_CLASS_MEASUREMENT,
UNIT_MICROGRAMS_PER_CUBIC_METER,
)
from esphome.types import ConfigType
CODEOWNERS = ["@ch604"]
DEPENDENCIES = ["uart"]
d01_ns = cg.esphome_ns.namespace("d01")
D01SensorComponent = d01_ns.class_(
"D01SensorComponent", sensor.Sensor, uart.UARTDevice, cg.Component
)
CONFIG_SCHEMA = (
sensor.sensor_schema(
D01SensorComponent,
unit_of_measurement=UNIT_MICROGRAMS_PER_CUBIC_METER,
icon=ICON_BLUR,
accuracy_decimals=0,
device_class=DEVICE_CLASS_PM25,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"d01",
baud_rate=9600,
require_rx=True,
require_tx=False,
)
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
-8
View File
@@ -26,14 +26,6 @@ CONFIG_SCHEMA = (
.extend(cv.polling_component_schema("30s"))
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"daly_bms",
baud_rate=9600,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
+4 -1
View File
@@ -22,7 +22,10 @@ static const uint8_t DALY_REQUEST_TEMPERATURE = 0x96;
void DalyBmsComponent::setup() { this->next_request_ = 1; }
void DalyBmsComponent::dump_config() { ESP_LOGCONFIG(TAG, "Daly BMS:"); }
void DalyBmsComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Daly BMS:");
this->check_uart_settings(9600);
}
void DalyBmsComponent::update() {
this->trigger_next_ = true;
+2 -2
View File
@@ -22,9 +22,9 @@ void DebugComponent::dump_config() {
LOG_SENSOR(" ", "Free space on heap", this->free_sensor_);
LOG_SENSOR(" ", "Largest free heap block", this->block_sensor_);
LOG_SENSOR(" ", "CPU frequency", this->cpu_frequency_sensor_);
#ifdef USE_ESP8266
#if defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
LOG_SENSOR(" ", "Heap fragmentation", this->fragmentation_sensor_);
#endif // USE_ESP8266
#endif // defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
#endif // USE_SENSOR
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
+2 -2
View File
@@ -35,7 +35,7 @@ class DebugComponent final : public PollingComponent {
#ifdef USE_SENSOR
void set_free_sensor(sensor::Sensor *free_sensor) { free_sensor_ = free_sensor; }
void set_block_sensor(sensor::Sensor *block_sensor) { block_sensor_ = block_sensor; }
#if defined(USE_ESP8266) || defined(USE_ESP32)
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
void set_fragmentation_sensor(sensor::Sensor *fragmentation_sensor) { fragmentation_sensor_ = fragmentation_sensor; }
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
@@ -61,7 +61,7 @@ class DebugComponent final : public PollingComponent {
sensor::Sensor *free_sensor_{nullptr};
sensor::Sensor *block_sensor_{nullptr};
#if defined(USE_ESP8266) || defined(USE_ESP32)
#if (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)) || defined(USE_ESP32)
sensor::Sensor *fragmentation_sensor_{nullptr};
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
@@ -159,10 +159,12 @@ void DebugComponent::update_platform_() {
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
this->block_sensor_->publish_state(ESP.getMaxFreeBlockSize());
}
#if USE_ARDUINO_VERSION_CODE >= VERSION_CODE(2, 5, 2)
if (this->fragmentation_sensor_ != nullptr) {
// NOLINTNEXTLINE(readability-static-accessed-through-instance)
this->fragmentation_sensor_->publish_state(ESP.getHeapFragmentation());
}
#endif
#endif
}
+5 -2
View File
@@ -52,9 +52,12 @@ CONFIG_SCHEMA = {
),
cv.Optional(CONF_FRAGMENTATION): cv.All(
cv.Any(
cv.only_on_esp8266,
cv.All(
cv.only_on_esp8266,
cv.require_framework_version(esp8266_arduino=cv.Version(2, 5, 2)),
),
cv.only_on_esp32,
msg="This feature is only available on ESP8266 and ESP32",
msg="This feature is only available on ESP8266 (Arduino 2.5.2+) and ESP32",
),
sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
+1 -6
View File
@@ -60,12 +60,7 @@ CONFIG_SCHEMA = cv.All(
).extend(uart.UART_DEVICE_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"dfplayer",
baud_rate=9600,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
"dfplayer", baud_rate=9600, require_tx=True
)
+4 -1
View File
@@ -277,6 +277,9 @@ void DFPlayer::loop() {
}
}
}
void DFPlayer::dump_config() { ESP_LOGCONFIG(TAG, "DFPlayer:"); }
void DFPlayer::dump_config() {
ESP_LOGCONFIG(TAG, "DFPlayer:");
this->check_uart_settings(9600);
}
} // namespace esphome::dfplayer
+3 -4
View File
@@ -20,8 +20,8 @@ void DHT::dump_config() {
"DHT:\n"
" %sModel: %s\n"
" Internal pull-up: %s",
this->is_auto_detect_ ? LOG_STR_LITERAL("Auto-detected ") : "",
this->model_ == DHT_MODEL_DHT11 ? LOG_STR_LITERAL("DHT11") : LOG_STR_LITERAL("DHT22 or equivalent"),
this->is_auto_detect_ ? "Auto-detected " : "",
this->model_ == DHT_MODEL_DHT11 ? "DHT11" : "DHT22 or equivalent",
ONOFF(this->t_pin_->get_flags() & gpio::FLAG_PULLUP));
LOG_PIN(" Pin: ", this->t_pin_);
LOG_UPDATE_INTERVAL(this);
@@ -154,9 +154,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
}
}
if (error_code != 0) {
if (report_errors) {
if (report_errors)
ESP_LOGW(TAG, ESP_LOG_MSG_COMM_FAIL);
}
return false;
}
-68
View File
@@ -1,68 +0,0 @@
#include "ds1603l.h"
#include <cstring>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ds1603l {
static const char *const TAG = "ds1603l.sensor";
void DS1603L::loop() {
// Assemble frames one byte at a time so a stream that starts mid-frame can realign
uint8_t byte;
while (this->available() > 0 && this->read_byte(&byte)) {
if (this->rx_count_ == 0 && byte != HEADER_BYTE) {
ESP_LOGV(TAG, "Skipping byte 0x%02X while looking for header", byte);
continue;
}
this->rx_buffer_[this->rx_count_++] = byte;
if (this->rx_count_ < FRAME_SIZE) {
continue;
}
if (this->parse_data_()) {
this->rx_count_ = 0;
} else {
// The header byte was part of the payload of a misaligned frame, so realign instead of dropping everything
this->resync_();
}
}
}
void DS1603L::dump_config() { LOG_SENSOR("", "DS1603L", this); }
bool DS1603L::parse_data_() {
uint8_t header = this->rx_buffer_[0];
uint8_t data_h = this->rx_buffer_[1];
uint8_t data_l = this->rx_buffer_[2];
uint8_t checksum = this->rx_buffer_[3];
uint8_t computed_checksum = (header + data_h + data_l) & 0xFF;
ESP_LOGV(TAG, "Data: Header=0x%02X, Data_H=0x%02X, Data_L=0x%02X, Checksum=0x%02X", header, data_h, data_l, checksum);
if (checksum != computed_checksum) {
ESP_LOGW(TAG, "Checksum mismatch: received 0x%02X, expected 0x%02X", checksum, computed_checksum);
return false;
}
this->publish_state(encode_uint16(data_h, data_l));
return true;
}
void DS1603L::resync_() {
// Drop the byte that was treated as the header, then look for the next candidate header in what is left
size_t start = 1;
while (start < this->rx_count_ && this->rx_buffer_[start] != HEADER_BYTE) {
start++;
}
this->rx_count_ -= start;
if (this->rx_count_ > 0) {
memmove(this->rx_buffer_, this->rx_buffer_ + start, this->rx_count_);
}
}
} // namespace esphome::ds1603l
-30
View File
@@ -1,30 +0,0 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/component.h"
namespace esphome::ds1603l {
class DS1603L final : public sensor::Sensor, public Component, public uart::UARTDevice {
public:
void loop() override;
void dump_config() override;
protected:
static constexpr uint8_t HEADER_BYTE = 0xFF;
static constexpr size_t FRAME_SIZE = 4;
// Validates the checksum of the frame in rx_buffer_ and publishes it. Returns false if the frame is invalid.
bool parse_data_();
// Drops the first buffered byte and realigns the buffer on the next possible header byte.
void resync_();
uint8_t rx_buffer_[FRAME_SIZE]; // Buffer for the frame being assembled
size_t rx_count_{0}; // Number of bytes currently in rx_buffer_
};
} // namespace esphome::ds1603l
-43
View File
@@ -1,43 +0,0 @@
import esphome.codegen as cg
from esphome.components import sensor, uart
import esphome.config_validation as cv
from esphome.const import (
DEVICE_CLASS_DISTANCE,
STATE_CLASS_MEASUREMENT,
UNIT_MILLIMETER,
)
from esphome.types import ConfigType
CODEOWNERS = ["@JakeLC15"]
DEPENDENCIES = ["uart"]
ds1603l_ns = cg.esphome_ns.namespace("ds1603l")
DS1603L = ds1603l_ns.class_("DS1603L", sensor.Sensor, cg.Component, uart.UARTDevice)
CONFIG_SCHEMA = (
sensor.sensor_schema(
DS1603L,
unit_of_measurement=UNIT_MILLIMETER,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"ds1603l",
baud_rate=9600,
require_tx=False,
require_rx=True,
data_bits=8,
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = await sensor.new_sensor(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
+1 -1
View File
@@ -93,7 +93,7 @@ void Emc2101Component::dump_config() {
if (this->is_failed()) {
ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
}
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? LOG_STR_LITERAL("DAC") : LOG_STR_LITERAL("PWM"));
ESP_LOGCONFIG(TAG, " Mode: %s", this->dac_mode_ ? "DAC" : "PWM");
if (this->dac_mode_) {
ESP_LOGCONFIG(TAG, " DAC Conversion Rate: %X", this->dac_conversion_rate_);
} else {
+1 -1
View File
@@ -216,7 +216,7 @@ void ENS210Component::extract_measurement_(uint32_t val, int *data, int *status)
// Sets ENS210 to low (true) or high (false) power. Returns false on I2C problems.
bool ENS210Component::set_low_power_(bool enable) {
uint8_t low_power_cmd = enable ? 0x01 : 0x00;
ESP_LOGD(TAG, "Enable low power: %s", enable ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
ESP_LOGD(TAG, "Enable low power: %s", enable ? "true" : "false");
bool result = this->write_byte(ENS210_REGISTER_SYS_CTRL, low_power_cmd);
delay(ENS210_BOOTING_MS);
return result;
@@ -1,139 +0,0 @@
#include "epaper_spi_uc8179.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.uc8179";
bool EPaperUC8179::initialise(bool partial) {
EPaperBase::initialise(partial); // send the model init sequence
this->partial_ = partial;
ESP_LOGV(TAG, "Power on");
// POWER ON must precede the waveform/mode registers and the data transfer
// (the original driver powers on and busy-waits before writing them).
// The state machine busy-waits before entering TRANSFER_DATA.
this->command(0x04);
// Give the busy line time to assert before the state machine polls it
this->next_delay_ = 100;
return true;
}
// Set up the refresh mode. Must be called after power-on has completed.
void EPaperUC8179::set_refresh_mode_() {
if (!this->is_using_partial_update_()) {
return; // plain full refresh uses the mode set by the init sequence
}
// Fast and partial refresh use flipped data polarity and a floating border
this->cmd_data(0x50, {0xA9, 0x07});
// Force the waveform via the temperature registers: 0x5A selects the fast
// full-refresh waveform, 0x6E the partial-refresh waveform
this->cmd_data(0xE0, {0x02});
if (this->partial_) {
this->cmd_data(0xE5, {0x6E});
this->command(0x91); // enter partial mode
// Set the partial window to the full screen
const uint16_t x_end = this->width_ - 1;
const uint16_t y_end = this->height_ - 1;
this->cmd_data(0x90, {0x00, 0x00, static_cast<uint8_t>(x_end >> 8), static_cast<uint8_t>(x_end & 0xFF), 0x00, 0x00,
static_cast<uint8_t>(y_end >> 8), static_cast<uint8_t>(y_end & 0xFF), 0x01});
} else {
this->cmd_data(0xE5, {0x5A});
this->command(0x92); // exit partial mode
}
}
bool HOT EPaperUC8179::transfer_data() {
const uint32_t start_time = millis();
const size_t buffer_length = this->buffer_length_;
if (this->current_data_index_ == 0) {
this->set_refresh_mode_();
}
// Fast full refresh sends the previous-image plane as well, so that every pixel transitions
const bool two_pass = this->is_using_partial_update_() && !this->partial_;
// Plain full refresh sends inverted data (buffer is 1=white, the wire wants 0=white);
// in fast/partial mode the data polarity is flipped via the VCOM/data-interval
// register instead, so the new-image plane is sent unmodified
const bool invert_new_data = !this->is_using_partial_update_();
uint8_t bytes_to_send[MAX_TRANSFER_SIZE];
// Phase 1 (fast full refresh only): previous image via 0x10 (DTM1), inverse of the new image
if (two_pass && this->current_data_index_ < buffer_length) {
if (this->current_data_index_ == 0) {
this->command(0x10); // DATA START TRANSMISSION 1 (previous image)
}
this->start_data_();
while (this->current_data_index_ < buffer_length) {
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, buffer_length - this->current_data_index_);
for (size_t i = 0; i < bytes_to_copy; i++) {
bytes_to_send[i] = ~this->buffer_[this->current_data_index_ + i];
}
this->write_array(bytes_to_send, bytes_to_copy);
this->current_data_index_ += bytes_to_copy;
if (millis() - start_time > MAX_TRANSFER_TIME) {
this->disable();
return false;
}
}
this->disable();
}
// Phase 2: new image via 0x13 (DTM2)
const size_t offset = two_pass ? buffer_length : 0;
const size_t total = offset + buffer_length;
if (this->current_data_index_ < total) {
if (this->current_data_index_ == offset) {
this->command(0x13); // DATA START TRANSMISSION 2 (new image)
}
this->start_data_();
while (this->current_data_index_ < total) {
const size_t bytes_to_copy = std::min(MAX_TRANSFER_SIZE, total - this->current_data_index_);
const size_t data_idx = this->current_data_index_ - offset;
for (size_t i = 0; i < bytes_to_copy; i++) {
const uint8_t byte = this->buffer_[data_idx + i];
bytes_to_send[i] = invert_new_data ? ~byte : byte;
}
this->write_array(bytes_to_send, bytes_to_copy);
this->current_data_index_ += bytes_to_copy;
if (millis() - start_time > MAX_TRANSFER_TIME) {
this->disable();
return false;
}
}
this->disable();
}
this->current_data_index_ = 0;
return true;
}
void EPaperUC8179::power_on() {
// Power-on is sent at the end of initialise() instead, because the
// waveform/mode registers and the data transfer must follow it
}
void EPaperUC8179::refresh_screen(bool /*partial*/) {
ESP_LOGV(TAG, "Refresh");
this->command(0x12); // DISPLAY REFRESH
// Delay the next busy poll: the busy line takes a short time to assert after
// the refresh command, and polling too early would read it as already idle
this->next_delay_ = 100;
}
void EPaperUC8179::power_off() {
ESP_LOGV(TAG, "Power off");
this->command(0x02); // POWER OFF
}
void EPaperUC8179::deep_sleep() {
// Deep sleep loses the previous-image RAM that partial refresh compares against
if (!this->is_using_partial_update_()) {
ESP_LOGV(TAG, "Deep sleep");
this->cmd_data(0x07, {0xA5}); // DEEP SLEEP with check code
}
}
} // namespace esphome::epaper_spi
@@ -1,52 +0,0 @@
#pragma once
#include "epaper_spi.h"
namespace esphome::epaper_spi {
/**
* Monochrome e-paper displays using the UC8179 controller.
* Supports: 7.5" V2 (EPD_7in5_V2), 800x480 pixels, as used by the
* Waveshare 7.5" V2 HAT and the Seeed reTerminal E1001.
*
* Buffer layout: 1 bit per pixel, 1=white, 0=black (the base class default).
*
* The INITIALISE state sends the panel configuration followed by power-on
* (0x04); the state machine busy-waits for power-on to complete before
* TRANSFER_DATA, which first writes the waveform/mode registers (these are
* only accepted while powered) and then the image data. The state machine
* busy-waits again before triggering REFRESH_SCREEN (0x12).
*
* Three refresh modes are used, following the Waveshare EPD_7in5_V2 examples:
* - full_update_every == 1: plain full refresh. The new image is sent
* inverted to DTM2 (0x13) and the controller uses its normal waveform.
* - full_update_every > 1, full update: fast full refresh. The data polarity
* is flipped via the VCOM/data-interval register, a fast waveform is forced
* via the temperature registers, and the image is sent to both DTM1 (0x10,
* inverted) and DTM2 (0x13) so that every pixel transitions.
* - full_update_every > 1, partial update: partial refresh. A partial-update
* waveform is forced, partial mode is entered with a full-screen window and
* only DTM2 is sent; the controller compares against its previous-image RAM.
*/
class EPaperUC8179 final : public EPaperBase {
public:
EPaperUC8179(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
this->buffer_length_ = this->row_width_ * height;
}
protected:
bool initialise(bool partial) override;
bool transfer_data() override;
void refresh_screen(bool partial) override;
void power_on() override;
void power_off() override;
void deep_sleep() override;
void set_refresh_mode_();
// Set by initialise() so transfer_data() knows which planes to send
bool partial_{};
};
} // namespace esphome::epaper_spi
@@ -1,93 +0,0 @@
"""Monochrome e-paper displays using the UC8179 controller.
Supported models:
- waveshare-7.5in-v2: 7.5" mono display, 800x480 pixels (EPD_7in5_V2)
- seeed-reterminal-e1001: Seeed reTerminal E1001, which uses the same
7.5" 800x480 panel on an integrated ESP32-S3 board
Panel configuration and power-on (0x04) are both sent during the INITIALISE
state; the state machine's built-in busy wait then covers the power-on delay
before the waveform/mode registers and image data are transferred.
These displays support fast full and partial refresh: set ``full_update_every``
greater than 1 to enable it. Every ``full_update_every``-th update is a fast
full refresh, with partial refreshes in between.
"""
from typing import Any
from esphome.const import CONF_DATA_RATE
from . import EpaperModel
class UC8179(EpaperModel):
"""EpaperModel class for monochrome displays using the UC8179 controller."""
def __init__(
self,
name: str,
class_name: str = "EPaperUC8179",
data_rate: str = "10MHz",
**defaults: Any,
) -> None:
defaults.setdefault(CONF_DATA_RATE, data_rate)
super().__init__(name, class_name, **defaults)
def get_init_sequence(self, config: dict) -> tuple:
"""Generate the initialization sequence for UC8179 mono displays.
Panel configuration only the driver appends power-on (0x04) at the
end of the INITIALISE state, and the state machine busy-waits for it
to complete before the data transfer starts.
"""
width, height = self.get_dimensions(config)
return (
# POWER SETTING
(0x01, 0x07, 0x07, 0x3F, 0x3F),
# BOOSTER SOFT START
(0x06, 0x17, 0x17, 0x28, 0x17),
# PANEL SETTING (black/white mode, LUT from OTP)
(0x00, 0x1F),
# RESOLUTION SETTING (width x height)
(
0x61,
(width >> 8) & 0xFF,
width & 0xFF,
(height >> 8) & 0xFF,
height & 0xFF,
),
# DUAL SPI MODE (disabled)
(0x15, 0x00),
# VCOM AND DATA INTERVAL SETTING
(0x50, 0x10, 0x07),
# TCON SETTING
(0x60, 0x22),
)
uc8179 = UC8179("uc8179")
# Waveshare 7.5" V2 mono (EPD_7in5_V2) — 800x480, UC8179 controller
waveshare_7_5_v2 = uc8179.extend(
"waveshare-7.5in-v2",
width=800,
height=480,
)
# Seeed reTerminal E1001 — 7.5" mono e-paper (800x480), same panel as the
# Waveshare 7.5" V2, driven by an integrated ESP32-S3 board
waveshare_7_5_v2.extend(
"seeed-reterminal-e1001",
cs_pin=10,
dc_pin=11,
reset_pin=12,
busy_pin={
"number": 13,
"inverted": True,
"mode": {
"input": True,
"pullup": True,
},
},
)
+156 -65
View File
@@ -73,6 +73,7 @@ from .const import (
KEY_FLASH_SIZE,
KEY_FULL_CERT_BUNDLE,
KEY_IDF_VERSION,
KEY_MBEDTLS_SDKCONFIG,
KEY_NETWORK_SDKCONFIG,
KEY_PATH,
KEY_REF,
@@ -182,13 +183,6 @@ SIGNED_OTA_V1_ECDSA_VARIANTS = {
VARIANT_ESP32,
}
# Variants that support execution from PSRAM
PSRAM_XIP_VARIANTS = {
VARIANT_ESP32S3,
VARIANT_ESP32P4,
VARIANT_ESP32S31,
}
# NVS encryption (HMAC peripheral scheme) is only available on variants that
# expose the HMAC peripheral (SOC_HMAC_SUPPORTED in soc_caps.h). The original
# ESP32 and ESP32-C2 do not have it. New variants with an HMAC peripheral
@@ -225,7 +219,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"cmock", # Unit testing mock framework - ESPHome doesn't use IDF's testing
"console", # Console REPL - unused by ESPHome; espressif/mdns pulls it back when configured
"driver", # Legacy driver shim - only needed by esp32_touch, esp32_can for legacy headers
"esp-tls", # TLS wrapper - re-included by http_request, mqtt, web_server_idf
"esp-tls", # TLS wrapper - re-included by request_tls()
"esp_adc", # ADC driver - only needed by adc component
"esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back
"esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back
@@ -253,7 +247,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_https_server", # HTTPS server - ESPHome has its own web server
"esp_lcd", # LCD controller drivers - only needed by display component
"esp_local_ctrl", # Local control over HTTPS/BLE - ESPHome has native API
"esp_phy", # RF PHY - re-included by internal_temperature on the original ESP32; esp_wifi/bt/ieee802154 pull it back
"esp_phy", # RF PHY - esp_wifi/bt/ieee802154 pull it back when they are in the build
"esp_wifi", # WiFi stack - re-included by request_wifi(), espnow; bt pulls it back for BLE builds
"espcoredump", # Core dump support - ESPHome has its own debug component
"fatfs", # FAT filesystem - ESPHome doesn't use filesystem storage
@@ -747,6 +741,48 @@ def request_software_coexistence() -> None:
include_builtin_idf_component("esp_coex")
@dataclass
class MbedtlsSdkconfigData:
"""Inputs for the mbedTLS sdkconfig flags, reconciled at FINAL.
Components call request_tls() / require_mbedtls_*() instead of writing the
CONFIG_MBEDTLS_* flags directly; _reconcile_mbedtls_sdkconfig() decides the
final values once every to_code has run.
"""
tls_required: bool = False # TLS/DTLS handshake user
ecp_required: bool = False # ECDH/ECDSA without TLS (openthread SRP host key)
peer_cert_required: bool = False # keep the peer certificate after the handshake
pkcs7_required: bool = False # PKCS#7 parsing
sha512_required: bool = False # SHA-384/SHA-512
# esp32 advanced disable_mbedtls_peer_cert / disable_mbedtls_pkcs7 options
disable_peer_cert: bool = True
disable_pkcs7: bool = True
def _mbedtls_sdkconfig() -> MbedtlsSdkconfigData:
data = CORE.data[KEY_ESP32]
if KEY_MBEDTLS_SDKCONFIG not in data:
data[KEY_MBEDTLS_SDKCONFIG] = MbedtlsSdkconfigData()
return data[KEY_MBEDTLS_SDKCONFIG]
def request_tls() -> None:
"""Request the mbedTLS TLS stack and the esp-tls wrapper.
Without a request TLS and its ECP/PEM-write/CRL/CSR crypto compile out;
hashes, AES and RSA stay available.
"""
_mbedtls_sdkconfig().tls_required = True
include_builtin_idf_component("esp-tls")
def request_http_client() -> None:
"""Request ESP-IDF's HTTP client; it links esp_tls even for plain http."""
include_builtin_idf_component("esp_http_client")
request_tls()
def add_idf_component(
*,
name: str,
@@ -1530,7 +1566,7 @@ def final_validate(config) -> None:
)
)
if advanced[CONF_EXECUTE_FROM_PSRAM]:
if config[CONF_VARIANT] not in PSRAM_XIP_VARIANTS:
if config[CONF_VARIANT] not in {VARIANT_ESP32S3, VARIANT_ESP32P4}:
errs.append(
cv.Invalid(
f"'{CONF_EXECUTE_FROM_PSRAM}' is not available on this esp32 variant",
@@ -1744,10 +1780,7 @@ KEY_VFS_TERMIOS_REQUIRED = "vfs_termios_required"
# Feature requirement tracking - components can call require_* functions to re-enable
# These are stored in CORE.data[KEY_ESP32] dict
KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED = "usb_serial_jtag_secondary_required"
KEY_MBEDTLS_PEER_CERT_REQUIRED = "mbedtls_peer_cert_required"
KEY_MBEDTLS_PKCS7_REQUIRED = "mbedtls_pkcs7_required"
KEY_FATFS_REQUIRED = "fatfs_required"
KEY_MBEDTLS_SHA512_REQUIRED = "mbedtls_sha512_required"
KEY_ADC_ONESHOT_IRAM_REQUIRED = "adc_oneshot_iram_required"
KEY_LIBC_PICOLIBC_NEWLIB_COMPAT_REQUIRED = "libc_picolibc_newlib_compat_required"
@@ -1786,6 +1819,8 @@ def require_certificate_bundle() -> None:
certificates (http_request, audio streaming) call this so the bundle is
compiled and gen_crt_bundle runs only when something uses it.
"""
# esp_crt_bundle.c calls mbedtls_ssl_conf_*, so a bundle always needs TLS.
request_tls()
CORE.data[KEY_ESP32][KEY_CERT_BUNDLE] = True
@@ -1811,33 +1846,32 @@ def require_usb_serial_jtag_secondary() -> None:
CORE.data[KEY_ESP32][KEY_USB_SERIAL_JTAG_SECONDARY_REQUIRED] = True
def require_mbedtls_peer_cert() -> None:
"""Mark that mbedTLS peer certificate retention is required by a component.
def require_mbedtls_ecp() -> None:
"""Keep mbedTLS elliptic curve support (ECDH/ECDSA) without requesting TLS.
Call this from components that need access to the peer certificate after
the TLS handshake is complete. This prevents CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE
from being disabled.
Call this from components that sign or verify with ECDSA outside a TLS
handshake (openthread's SRP host key). WiFi, Bluetooth and secure boot
select it through Kconfig on their own.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_PEER_CERT_REQUIRED] = True
_mbedtls_sdkconfig().ecp_required = True
def require_mbedtls_peer_cert() -> None:
"""Keep the peer certificate after the TLS handshake (CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE).
A user sdkconfig_options value takes precedence.
"""
_mbedtls_sdkconfig().peer_cert_required = True
def require_mbedtls_pkcs7() -> None:
"""Mark that mbedTLS PKCS#7 support is required by a component.
Call this from components that need PKCS#7 certificate validation.
This prevents CONFIG_MBEDTLS_PKCS7_C from being disabled.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_PKCS7_REQUIRED] = True
"""Keep mbedTLS PKCS#7 support (CONFIG_MBEDTLS_PKCS7_C). A user sdkconfig_options value takes precedence."""
_mbedtls_sdkconfig().pkcs7_required = True
def require_mbedtls_sha512() -> None:
"""Mark that mbedTLS SHA-384/SHA-512 support is required by a component.
Call this from components that need to verify TLS certificates or signatures
using SHA-384 or SHA-512 algorithms. This prevents CONFIG_MBEDTLS_SHA384_C
and CONFIG_MBEDTLS_SHA512_C from being disabled.
"""
CORE.data[KEY_ESP32][KEY_MBEDTLS_SHA512_REQUIRED] = True
"""Keep mbedTLS SHA-384/SHA-512 (CONFIG_MBEDTLS_SHA384_C / CONFIG_MBEDTLS_SHA512_C)."""
_mbedtls_sdkconfig().sha512_required = True
def idf_version() -> cv.Version:
@@ -2302,6 +2336,81 @@ async def _reconcile_certificate_bundle_sdkconfig() -> None:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE_DEFAULT_CMN", True)
# User sdkconfig_options that mean "keep TLS on" when set to y.
_MBEDTLS_TLS_ON_OPTIONS = (
"CONFIG_MBEDTLS_TLS_ENABLED",
"CONFIG_MBEDTLS_TLS_SERVER_AND_CLIENT",
"CONFIG_MBEDTLS_TLS_SERVER_ONLY",
"CONFIG_MBEDTLS_TLS_CLIENT_ONLY",
)
# Any user option under these prefixes only makes sense with TLS compiled in.
_TLS_OPTION_PREFIXES = ("CONFIG_ESP_TLS_", "CONFIG_MBEDTLS_SSL_", "CONFIG_ESP_HTTPS_")
def _user_sdkconfig_wants_tls(options: dict[str, Any]) -> bool:
"""True when sdkconfig_options turn TLS on or tune something under it; an `n` is never a request."""
return any(
(name in _MBEDTLS_TLS_ON_OPTIONS and value == "y")
or (name == "CONFIG_MBEDTLS_TLS_DISABLED" and value == "n")
or (name.startswith(_TLS_OPTION_PREFIXES) and value != "n")
for name, value in options.items()
)
@coroutine_with_priority(CoroPriority.FINAL)
async def _reconcile_mbedtls_sdkconfig() -> None:
"""Reconcile the mbedTLS sdkconfig flags after every request_tls() / require_mbedtls_*() call.
mbedtls cannot be excluded from an IDF build (bootloader_support needs its
SHA-256), but with no TLS user the ssl_*.c sources and the TLS-only crypto
compile to empty objects. User sdkconfig_options win.
"""
data = _mbedtls_sdkconfig()
idf6 = idf_version() >= cv.Version(6, 0, 0)
# A component that re-includes esp-tls on its own (external components
# predating request_tls()) wants TLS just as much as a request_tls() call.
tls_required = (
data.tls_required
or "esp-tls" not in CORE.data[KEY_ESP32][KEY_EXCLUDE_COMPONENTS]
)
if not CORE.using_arduino and not tls_required:
# IDF 6 made CONFIG_MBEDTLS_TLS_ENABLED a normal bool; on IDF 5 it has
# no prompt and is only reachable through the "None" TLS role choice.
if idf6:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_TLS_ENABLED", False)
else:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_TLS_DISABLED", True)
# Enterprise WiFi selects TLS back on; wifi writes this itself, but
# esp_wifi can also be in the build without a wifi: block (openthread).
set_idf_sdkconfig_default("CONFIG_ESP_WIFI_ENTERPRISE_SUPPORT", False)
# WiFi (ESP_WIFI_MBEDTLS_CRYPTO), Bluetooth and signed apps
# (SECURE_SIGNED_APPS) select ECP back on through Kconfig.
if not data.ecp_required:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_ECP_C", False)
set_idf_sdkconfig_default("CONFIG_MBEDTLS_PEM_WRITE_C", False)
set_idf_sdkconfig_default("CONFIG_MBEDTLS_X509_CRL_PARSE_C", False)
set_idf_sdkconfig_default("CONFIG_MBEDTLS_X509_CSR_PARSE_C", False)
# Keeping the peer certificate costs ~4KB heap per connection.
if data.peer_cert_required:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True)
elif data.disable_peer_cert:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False)
if data.pkcs7_required:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_PKCS7_C", True)
elif data.disable_pkcs7:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_PKCS7_C", False)
# SHA-384 shares the SHA-512 compression function, so both go together.
# Only IDF 6.0's PSA engine links a ~3KB software fallback for them; on
# IDF 5 they are a single hardware-only option with no code size cost.
if idf6 and not data.sha512_required:
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SHA384_C", False)
set_idf_sdkconfig_default("CONFIG_MBEDTLS_SHA512_C", False)
@coroutine_with_priority(CoroPriority.FINAL)
async def _reconcile_network_sdkconfig() -> None:
"""Reconcile WiFi/Ethernet/Bluetooth/coexistence sdkconfig flags.
@@ -2734,7 +2843,13 @@ async def to_code(config):
_configure_lwip_max_sockets(conf)
if advanced[CONF_EXECUTE_FROM_PSRAM]:
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
if variant == VARIANT_ESP32S3:
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
elif variant == VARIANT_ESP32P4:
add_idf_sdkconfig_option("CONFIG_SPIRAM_XIP_FROM_PSRAM", True)
else:
raise ValueError("Unhandled ESP32 variant")
# Apply LWIP core locking for better socket performance
# This is already enabled by default in Arduino framework, where it provides
@@ -2950,38 +3065,6 @@ async def to_code(config):
if advanced[CONF_DISABLE_DEV_NULL_VFS]:
add_idf_sdkconfig_option("CONFIG_VFS_INITIALIZE_DEV_NULL", False)
# Disable keeping peer certificate after TLS handshake
# Saves ~4KB heap per connection, but prevents certificate inspection after handshake
# Components that need it can call require_mbedtls_peer_cert()
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PEER_CERT_REQUIRED, False):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", True)
elif advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SSL_KEEP_PEER_CERTIFICATE", False)
# Disable PKCS#7 support in mbedTLS
# Only needed for specific certificate validation scenarios
# Components that need it can call require_mbedtls_pkcs7()
if CORE.data[KEY_ESP32].get(KEY_MBEDTLS_PKCS7_REQUIRED, False):
# Component called require_mbedtls_pkcs7() - enable regardless of user setting
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", True)
elif advanced[CONF_DISABLE_MBEDTLS_PKCS7]:
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PKCS7_C", False)
# Disable SHA-384 and SHA-512 in mbedTLS
# ESPHome doesn't use either algorithm. SHA-384 shares the same
# compression function as SHA-512 (mbedtls_internal_sha512_process),
# so both must be disabled to eliminate the ~3KB software fallback
# that IDF 6.0's PSA parallel engine always links in.
# On IDF < 6.0 these are a single config and hardware-only (no
# software fallback), so there was no code size cost to leaving
# them enabled.
# Components that need SHA-384/SHA-512 can call require_mbedtls_sha512()
if idf_version() >= cv.Version(6, 0, 0) and not CORE.data[KEY_ESP32].get(
KEY_MBEDTLS_SHA512_REQUIRED, False
):
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA384_C", False)
add_idf_sdkconfig_option("CONFIG_MBEDTLS_SHA512_C", False)
# FINAL priority: runs after every require_libc_picolibc_newlib_compat() call
CORE.add_job(_set_libc_picolibc_newlib_compat)
@@ -2991,6 +3074,12 @@ async def to_code(config):
# FINAL priority: runs after every require_certificate_bundle() call
CORE.add_job(_reconcile_certificate_bundle_sdkconfig)
# FINAL priority: runs after every request_tls() / require_mbedtls_*() call
mbedtls = _mbedtls_sdkconfig()
mbedtls.disable_peer_cert = advanced[CONF_DISABLE_MBEDTLS_PEER_CERT]
mbedtls.disable_pkcs7 = advanced[CONF_DISABLE_MBEDTLS_PKCS7]
CORE.add_job(_reconcile_mbedtls_sdkconfig)
# FINAL: require_*() calls can come from to_code at or below this priority, so an
# inline read would be iteration-order-dependent; reconcile once after every job ran.
CORE.add_job(
@@ -3022,6 +3111,8 @@ async def to_code(config):
# so it still gets the CMN variant pinned.
if conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE") == "y":
require_certificate_bundle()
if _user_sdkconfig_wants_tls(conf[CONF_SDKCONFIG_OPTIONS]):
request_tls()
# Components from YAML are added in a separate coroutine with FINAL priority
# Schedule it to run after all other components
+1
View File
@@ -30,6 +30,7 @@ KEY_EXTRA_BUILD_FILES = "extra_build_files"
KEY_CERT_BUNDLE = "cert_bundle"
KEY_FULL_CERT_BUNDLE = "full_cert_bundle"
KEY_NETWORK_SDKCONFIG = "network_sdkconfig"
KEY_MBEDTLS_SDKCONFIG = "mbedtls_sdkconfig"
VARIANT_ESP32 = "ESP32"
VARIANT_ESP32C2 = "ESP32C2"
+4 -13
View File
@@ -5,14 +5,11 @@ import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER, CONF_SCL, CONF_SDA
from esphome.pins import check_strapping_pin
# Per the ESP32-S31 IDF DOCS and datasheet:
# https://docs.espressif.com/projects/esp-idf/en/v6.1/esp32s31/api-reference/peripherals/gpio.html
# Per the ESP32-S31 datasheet (page 96):
# https://documentation.espressif.com/esp32-s31_datasheet_en.pdf
_ESP32S31_SPI_FLASH_PINS: set[int] = {26, 27, 28, 30, 31, 32}
_ESP32S31_INVALID_PINS: set[int] = {29, 41}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source;
# GPIO36 sets the VDD_SPI voltage.
_ESP32S31_STRAPPING_PINS: set[int] = {36, 37, 60, 61}
_ESP32S31_SPI_FLASH_PINS: set[int] = {27, 28, 29, 31, 32, 33}
# GPIO60/GPIO61 set the boot mode; GPIO37 selects the JTAG signal source.
_ESP32S31_STRAPPING_PINS: set[int] = {37, 60, 61}
# LP I2C is fixed to GPIO6 (SCL) / GPIO7 (SDA) per the datasheet IO MUX table.
_ESP32S31_I2C_LP_PINS = {"SDA": 7, "SCL": 6}
@@ -22,8 +19,6 @@ _LOGGER = logging.getLogger(__name__)
def esp32_s31_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 61:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-61)")
if value in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{value} does not exist on ESP32-S31.")
if value in _ESP32S31_SPI_FLASH_PINS:
raise cv.Invalid(
f"GPIO{value} is reserved for the SPI flash interface on ESP32-S31 and cannot be used."
@@ -38,10 +33,6 @@ def esp32_s31_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
if num < 0 or num > 61:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-61)")
# Checked here as well so ignore_pin_validation_error cannot bypass it;
# these pins are not bonded and can never work
if num in _ESP32S31_INVALID_PINS:
raise cv.Invalid(f"GPIO{num} does not exist on ESP32-S31.")
if is_input:
# All ESP32 pins support input mode
pass
+7 -28
View File
@@ -100,38 +100,21 @@ void ESP32BLE::disable() {
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_start() {
this->advertising_init_();
this->advertising_ref_count_++;
this->advertising_refresh();
}
void ESP32BLE::advertising_stop() {
if (this->advertising_ref_count_ == 0)
if (!this->is_active())
return;
this->advertising_ref_count_--;
this->advertising_refresh();
}
void ESP32BLE::advertising_refresh() {
if (this->advertising_ == nullptr || !this->is_active())
return;
// Advertise while any component still needs it, otherwise stop
if (this->advertising_ref_count_ == 0) {
this->advertising_->stop();
} else {
this->advertising_->start();
}
this->advertising_->start();
}
void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
this->advertising_init_();
this->advertising_->set_service_data(data);
this->advertising_refresh();
this->advertising_start();
}
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_init_();
this->advertising_->set_manufacturer_data(data);
this->advertising_refresh();
this->advertising_start();
}
void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name) {
@@ -153,7 +136,7 @@ void ESP32BLE::advertising_set_service_data_and_name(std::span<const uint8_t> da
this->advertising_->set_service_data(data);
}
this->advertising_refresh();
this->advertising_start();
}
void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback) {
@@ -164,13 +147,13 @@ void ESP32BLE::advertising_register_raw_advertisement_callback(std::function<voi
void ESP32BLE::advertising_add_service_uuid(ESPBTUUID uuid) {
this->advertising_init_();
this->advertising_->add_service_uuid(uuid);
this->advertising_refresh();
this->advertising_start();
}
void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
this->advertising_init_();
this->advertising_->remove_service_uuid(uuid);
this->advertising_refresh();
this->advertising_start();
}
#endif
@@ -592,10 +575,6 @@ void ESP32BLE::loop_handle_state_transition_not_active_() {
}
this->state_ = BLE_COMPONENT_STATE_ACTIVE;
#ifdef USE_ESP32_BLE_ADVERTISING
// Requests made before the stack was up (or before it was re-enabled) take effect now
this->advertising_refresh();
#endif
}
}
-13
View File
@@ -114,17 +114,7 @@ class ESP32BLE final : public Component {
void set_name(const char *name) { this->name_ = name; }
#ifdef USE_ESP32_BLE_ADVERTISING
/** Request advertising on behalf of a component.
*
* Requests are reference counted: advertising runs until every component that called
* advertising_start() has released it again with advertising_stop(). Each component must
* pair its calls, so nothing advertises until something actually asks for it.
*/
void advertising_start();
/// Release a request made with advertising_start(); advertising stops at the last release.
void advertising_stop();
/// Apply the current payload and request count: advertise while requested, otherwise stop.
void advertising_refresh();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
@@ -236,9 +226,6 @@ class ESP32BLE final : public Component {
// 1-byte aligned members (grouped together to minimize padding)
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; // 1 byte (uint8_t enum)
bool enable_on_boot_{}; // 1 byte
#ifdef USE_ESP32_BLE_ADVERTISING
uint8_t advertising_ref_count_{0}; // 1 byte, number of components requesting advertising
#endif
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
optional<esp_ble_auth_req_t> auth_req_mode_;
@@ -67,8 +67,6 @@ void ESP32BLEBeacon::setup() {
this->on_advertise_();
}
});
// A beacon always needs the device to advertise, and never releases the request
global_ble->advertising_start();
}
void ESP32BLEBeacon::on_advertise_() {
@@ -596,18 +596,6 @@ async def to_code(config):
cg.add(var.set_parent(parent))
cg.add(parent.advertising_set_appearance(config[CONF_APPEARANCE]))
cg.add(var.set_max_clients(config[CONF_MAX_CLIENTS]))
# Only advertise for the server itself when the configuration gives clients something to
# find. A server that is auto-loaded purely to host a runtime service (esp32_improv) stays
# silent until that service asks for advertising.
cg.add(
var.set_advertising_required(
CONF_MANUFACTURER_DATA in config
or any(
not uuid_is(service_config[CONF_UUID], DEVICE_INFORMATION_SERVICE_UUID)
for service_config in config[CONF_SERVICES]
)
)
)
if CONF_MANUFACTURER_DATA in config:
cg.add(var.set_manufacturer_data(config[CONF_MANUFACTURER_DATA]))
for service_config in config[CONF_SERVICES]:
@@ -81,7 +81,6 @@ void BLEServer::loop() {
if (this->device_information_service_->is_running()) {
this->state_ = RUNNING;
this->restart_advertising_();
this->request_advertising_();
ESP_LOGD(TAG, "BLE server setup successfully");
} else if (this->device_information_service_->is_created()) {
this->device_information_service_->start();
@@ -99,20 +98,6 @@ void BLEServer::restart_advertising_() {
}
}
void BLEServer::request_advertising_() {
if (!this->advertising_required_ || this->advertising_requested_)
return;
this->advertising_requested_ = true;
this->parent_->advertising_start();
}
void BLEServer::release_advertising_() {
if (!this->advertising_requested_)
return;
this->advertising_requested_ = false;
this->parent_->advertising_stop();
}
BLEService *BLEServer::create_service(ESPBTUUID uuid, bool advertise, uint16_t num_handles) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char uuid_buf[esp32_ble::UUID_STR_LEN];
@@ -185,7 +170,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
this->add_client_(param->connect.conn_id);
// Resume advertising so additional clients can discover and connect
if (this->client_count_ < this->max_clients_) {
this->parent_->advertising_refresh();
this->parent_->advertising_start();
}
this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
break;
@@ -193,7 +178,7 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
case ESP_GATTS_DISCONNECT_EVT: {
ESP_LOGD(TAG, "BLE Client disconnected");
this->remove_client_(param->disconnect.conn_id);
this->parent_->advertising_refresh();
this->parent_->advertising_start();
this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id);
break;
}
@@ -241,8 +226,6 @@ void BLEServer::remove_client_(uint16_t conn_id) {
}
void BLEServer::ble_before_disabled_event_handler() {
// Advertising is re-requested once the server is running again after BLE is re-enabled
this->release_advertising_();
// Delete all clients
this->client_count_ = 0;
// Delete all services
@@ -38,13 +38,6 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
this->restart_advertising_();
}
/** Whether this server needs the device to advertise so clients can find and connect to it.
*
* False for a server that only hosts services created at runtime (e.g. esp32_improv), which
* request advertising themselves for as long as they need it.
*/
void set_advertising_required(bool required) { this->advertising_required_ = required; }
void set_max_clients(uint8_t max_clients) { this->max_clients_ = max_clients; }
uint8_t get_max_clients() const { return this->max_clients_; }
@@ -89,8 +82,6 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
};
void restart_advertising_();
void request_advertising_();
void release_advertising_();
int8_t find_client_index_(uint16_t conn_id) const;
void add_client_(uint16_t conn_id);
@@ -102,8 +93,6 @@ class BLEServer final : public Component, public Parented<ESP32BLE> {
std::vector<uint8_t> manufacturer_data_{};
esp_gatt_if_t gatts_if_{0};
bool registered_{false};
bool advertising_required_{true};
bool advertising_requested_{false};
uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
uint8_t client_count_{0};
@@ -210,9 +210,8 @@ esp_err_t CameraWebServer::streaming_handler_(struct httpd_req *req) {
if (!image) {
// A shutdown is not a lost frame: wait_for_image_() returns empty as soon
// as running_ clears, and the loop condition below ends the stream anyway.
if (this->running_) {
if (this->running_)
ESP_LOGW(TAG, "STREAM: failed to acquire frame");
}
res = ESP_FAIL;
}
if (res == ESP_OK) {
@@ -112,7 +112,6 @@ void ESP32ImprovComponent::loop() {
this->state_callback_.call(this->state_, this->error_state_);
#endif
}
this->release_advertising_();
this->incoming_data_.clear();
return;
}
@@ -144,9 +143,8 @@ void ESP32ImprovComponent::loop() {
ESP_LOGV(TAG, "Starting with device name advertising");
this->advertising_device_name_ = true;
this->last_name_adv_time_ = App.get_loop_component_start_time();
// Set the payload before requesting, so advertising starts exactly once
esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>{}, true);
this->request_advertising_();
esp32_ble::global_ble->advertising_start();
// Set initial state based on whether we have an authorizer
this->set_state_(this->get_initial_state_(), false);
@@ -328,8 +326,6 @@ void ESP32ImprovComponent::stop() {
this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] {
if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
return;
// Release first so removing the service UUID does not restart advertising on the way out
this->release_advertising_();
this->service_->stop();
this->set_state_(improv::STATE_STOPPED);
});
@@ -524,20 +520,6 @@ void ESP32ImprovComponent::update_advertising_type_() {
}
}
void ESP32ImprovComponent::request_advertising_() {
if (this->advertising_requested_)
return;
this->advertising_requested_ = true;
esp32_ble::global_ble->advertising_start();
}
void ESP32ImprovComponent::release_advertising_() {
if (!this->advertising_requested_)
return;
this->advertising_requested_ = false;
esp32_ble::global_ble->advertising_stop();
}
improv::State ESP32ImprovComponent::get_initial_state_() const {
#ifdef USE_BINARY_SENSOR
// If we have an authorizer, start in awaiting authorization state
@@ -104,11 +104,8 @@ class ESP32ImprovComponent final : public Component, public improv_base::ImprovB
bool status_indicator_state_{false};
uint32_t last_name_adv_time_{0};
bool advertising_device_name_{false};
bool advertising_requested_{false};
void set_status_indicator_state_(bool state);
void update_advertising_type_();
void request_advertising_();
void release_advertising_();
void set_state_(improv::State state, bool update_advertising = true);
void set_error_(improv::Error error);
+40 -17
View File
@@ -35,7 +35,7 @@ from esphome.platformio.toolchain import copy_ccache_script
from esphome.storage_json import StorageJSON
from esphome.types import ConfigType
from .boards import BOARDS, board_ld_script
from .boards import BOARDS, ESP8266_LD_SCRIPTS, board_ld_script
from .const import (
CONF_EARLY_PIN_INIT,
CONF_ENABLE_SERIAL,
@@ -43,6 +43,8 @@ from .const import (
CONF_RESTORE_FROM_FLASH,
KEY_BOARD,
KEY_ESP8266,
KEY_FLASH_SIZE,
KEY_LDSCRIPT,
KEY_PIN_INITIAL_STATES,
KEY_SERIAL1_REQUIRED,
KEY_SERIAL_REQUIRED,
@@ -131,6 +133,10 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# format the given arduino (https://github.com/esp8266/Arduino/releases) version to
# a PIO platformio/framework-arduinoespressif8266 value
# List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
if ver <= cv.Version(2, 4, 1):
return f"~1.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
if ver <= cv.Version(2, 6, 2):
return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
# Same encoding the native toolchain uses for its package download, so a
# version bump cannot drift between the two paths.
from esphome.arduino8266.framework import framework_package_version
@@ -153,9 +159,11 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
# - https://github.com/esp8266/Arduino/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif8266
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 2)
# The platformio/espressif8266 version to use for arduino 3 framework versions
# The platformio/espressif8266 version to use for arduino 2 framework versions
# - https://github.com/platformio/platform-espressif8266/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif8266
ARDUINO_2_PLATFORM_VERSION = cv.Version(2, 6, 3)
# for arduino 3 framework versions
ARDUINO_3_PLATFORM_VERSION = cv.Version(3, 2, 0)
# for arduino 4 framework versions
ARDUINO_4_PLATFORM_VERSION = cv.Version(4, 2, 1)
@@ -180,14 +188,6 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
source = value.get(CONF_SOURCE, None)
if version < cv.Version(3, 0, 0):
raise cv.Invalid(
f"Arduino framework {version} is no longer supported; ESPHome requires "
f"C++20, which needs Arduino core 3.x. Use the recommended version "
f"({RECOMMENDED_ARDUINO_FRAMEWORK_VERSION}).",
path=[CONF_VERSION],
)
value[CONF_VERSION] = str(version)
value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
@@ -195,8 +195,12 @@ def _arduino_check_versions(value: ConfigType) -> ConfigType:
if platform_version is None:
if version >= cv.Version(3, 1, 0):
platform_version = _parse_platform_version(str(ARDUINO_4_PLATFORM_VERSION))
else:
elif version >= cv.Version(3, 0, 0):
platform_version = _parse_platform_version(str(ARDUINO_3_PLATFORM_VERSION))
elif version >= cv.Version(2, 5, 0):
platform_version = _parse_platform_version(str(ARDUINO_2_PLATFORM_VERSION))
else:
platform_version = _parse_platform_version(str(cv.Version(1, 8, 0)))
value[CONF_PLATFORM_VERSION] = platform_version
if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
@@ -285,11 +289,29 @@ def check_rosetta() -> None:
)
def _choose_ld_script(board: str) -> str:
"""The flash ld to pin for this board."""
def _choose_ld_script(board: str, ver: cv.Version) -> str | None:
"""The flash ld to pin for this board and core, or None for cores
without ld-script support."""
board_data = BOARDS[board]
ld_scripts = ESP8266_LD_SCRIPTS[board_data[KEY_FLASH_SIZE]]
if ver <= cv.Version(2, 3, 0):
# No ld script support
return None
if ver <= cv.Version(2, 4, 2):
# Old ld script path; the modern per-board override names do not
# exist in this core's SDK, so the override cannot be honored.
# Substituting the size default would move _FS_end and the
# preferences sector, wiping flash-backed state on flash.
if KEY_LDSCRIPT in board_data:
raise EsphomeError(
f"Board {board} requires its {board_data[KEY_LDSCRIPT]} "
f"flash layout, which Arduino core {ver} cannot honor; "
"use a core newer than 2.4.2"
)
return ld_scripts[0]
# A per-board override preserves a layout the board shipped with
# (see d1_wroom_02 in boards.py)
return board_ld_script(BOARDS[board])
return board_ld_script(board_data)
@coroutine_with_priority(CoroPriority.PLATFORM)
@@ -413,9 +435,10 @@ async def to_code(config: ConfigType) -> None:
)
if config[CONF_BOARD] in BOARDS:
cg.add_platformio_option(
"board_build.ldscript", _choose_ld_script(config[CONF_BOARD])
)
ld_script = _choose_ld_script(config[CONF_BOARD], ver)
if ld_script is not None:
cg.add_platformio_option("board_build.ldscript", ld_script)
CORE.add_job(add_pin_initial_states_array)
CORE.add_job(finalize_waveform_config)
+2 -133
View File
@@ -1,20 +1,12 @@
import logging
import esphome.codegen as cg
from esphome.components.noise import (
decode_encryption_key,
encryption_schema,
is_reserved_key,
)
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
from esphome.config_helpers import merge_config
import esphome.config_validation as cv
from esphome.const import (
CONF_API,
CONF_ENCRYPTION,
CONF_ESPHOME,
CONF_ID,
CONF_KEY,
CONF_NUM_ATTEMPTS,
CONF_OTA,
CONF_PASSWORD,
@@ -23,7 +15,6 @@ from esphome.const import (
CONF_REBOOT_TIMEOUT,
CONF_SAFE_MODE,
CONF_VERSION,
CONF_WEB_SERVER,
)
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
@@ -31,7 +22,6 @@ import esphome.final_validate as fv
from esphome.types import ConfigType
CONF_ALLOW_PARTITION_ACCESS = "allow_partition_access"
CONF_CAPTIVE_PORTAL = "captive_portal"
_LOGGER = logging.getLogger(__name__)
@@ -40,15 +30,7 @@ CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network"]
def AUTO_LOAD(config: ConfigType) -> list[str]:
"""Auto-load noise only when encryption is configured."""
base = ["sha256", "socket"]
# A falsy config is a tooling probe for the maximal set (None from
# dependency resolution, {} from the components-graph platform probe);
# a validated config always carries defaults, never empty
if not config or CONF_ENCRYPTION in config:
return base + ["noise"]
return base
AUTO_LOAD = ["sha256", "socket"]
esphome = cg.esphome_ns.namespace("esphome")
@@ -85,24 +67,11 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
CONF_PASSWORD in merged_ota_esphome_configs_by_port[conf_port]
and CONF_PASSWORD in ota_conf
and merged_ota_esphome_configs_by_port[conf_port][CONF_PASSWORD]
!= ota_conf[CONF_PASSWORD]
!= ota_conf.get(CONF_PASSWORD)
):
raise cv.Invalid(
f"Found multiple configurations but {CONF_PASSWORD} is inconsistent"
)
# Encryption blocks conflict only when both pin a key; a bare
# `encryption:` (a package/device split) is compatible with a
# keyed one, and merge_config yields the keyed result
merged_key = (
merged_ota_esphome_configs_by_port[conf_port]
.get(CONF_ENCRYPTION, {})
.get(CONF_KEY)
)
other_key = ota_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY)
if merged_key and other_key and merged_key != other_key:
raise cv.Invalid(
f"Found multiple configurations but {CONF_ENCRYPTION} is inconsistent"
)
ports_with_merged_configs.append(conf_port)
merged_ota_esphome_configs_by_port[conf_port] = merge_config(
@@ -125,20 +94,6 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
new_ota_conf.extend(merged_ota_esphome_configs_by_port.values())
api_conf = full_conf.get(CONF_API) or {}
for ota_conf in merged_ota_esphome_configs_by_port.values():
# Merging same-port blocks can combine a password from one block with
# encryption from another; re-check the exclusion on the merged result.
_validate_no_password_with_encryption(ota_conf)
if (encryption_conf := ota_conf.get(CONF_ENCRYPTION)) is not None:
_resolve_encryption_key(encryption_conf, api_conf)
if any(
conf.get(CONF_PLATFORM) == CONF_WEB_SERVER for conf in full_ota_conf
) and any(
CONF_ENCRYPTION in conf for conf in merged_ota_esphome_configs_by_port.values()
):
_warn_web_server_ota(full_conf)
full_conf[CONF_OTA] = new_ota_conf
fv.full_config.set(full_conf)
@@ -152,73 +107,6 @@ def ota_esphome_final_validate(config: ConfigType) -> None:
)
def _warn_web_server_ota(full_conf: ConfigType) -> None:
"""The web_server ota platform accepts the same image over plaintext HTTP
with basic auth, bypassing the encryption; warn rather than fail so the
operator keeps the recovery path."""
if CONF_CAPTIVE_PORTAL in full_conf and CONF_WEB_SERVER not in full_conf:
# The captive_portal auto-load: the endpoint only exists while the
# fallback AP is active
_LOGGER.warning(
"OTA encryption does not cover the %s OTA platform (auto-loaded "
"by captive_portal); the plaintext /update endpoint stays "
"reachable while the fallback AP is active",
CONF_WEB_SERVER,
)
else:
_LOGGER.warning(
"OTA encryption does not cover the %s OTA platform; its "
"plaintext /update endpoint accepts the same image",
CONF_WEB_SERVER,
)
def _resolve_encryption_key(encryption_conf: ConfigType, api_conf: ConfigType) -> None:
"""Resolve the one encryption key per device into the ota block.
An explicit ota key must match the api key, a bare block inherits it,
a runtime provisioned api key cannot be inherited, and the all-zeros
provisioning sentinel is rejected (the device treats it as no key).
"""
api_key = api_conf.get(CONF_ENCRYPTION, {}).get(CONF_KEY)
if ota_key := encryption_conf.get(CONF_KEY):
if api_key and ota_key != api_key:
raise cv.Invalid(
f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY} must match the "
f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY}; omit the "
f"'{CONF_OTA}' {CONF_KEY} to use the '{CONF_API}' one"
)
elif not api_key:
if CONF_ENCRYPTION in api_conf:
raise cv.Invalid(
f"the '{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} is provisioned at "
f"runtime and cannot be inherited at build time; set an explicit "
f"'{CONF_OTA}' {CONF_ENCRYPTION} {CONF_KEY}"
)
raise cv.Invalid(
f"'{CONF_OTA}' {CONF_ENCRYPTION} has no {CONF_KEY} and there is no "
f"'{CONF_API}' {CONF_ENCRYPTION} {CONF_KEY} to inherit; set one of them"
)
else:
encryption_conf[CONF_KEY] = api_key
if is_reserved_key(encryption_conf[CONF_KEY]):
raise cv.Invalid(
f"The all-zeros {CONF_KEY} is reserved and provides no protection; "
f"generate a real key with: openssl rand -base64 32"
)
# Also called on merged same-port configs in final validate, where schemas
# do not run
def _validate_no_password_with_encryption(config: ConfigType) -> ConfigType:
if CONF_PASSWORD in config and CONF_ENCRYPTION in config:
raise cv.Invalid(
f"'{CONF_PASSWORD}' cannot be combined with '{CONF_ENCRYPTION}'; the "
f"encryption key already authenticates the uploader, remove '{CONF_PASSWORD}'"
)
return config
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for OTA component."""
from esphome.components import socket
@@ -246,7 +134,6 @@ CONFIG_SCHEMA = cv.All(
): cv.port,
cv.Optional(CONF_ALLOW_PARTITION_ACCESS, default=False): cv.boolean,
cv.Optional(CONF_PASSWORD): cv.sensitive(),
cv.Optional(CONF_ENCRYPTION): encryption_schema,
cv.Optional(CONF_NUM_ATTEMPTS): cv.invalid(
f"'{CONF_SAFE_MODE}' (and its related configuration variables) has moved from 'ota' to its own component. See https://esphome.io/components/safe_mode"
),
@@ -260,24 +147,12 @@ CONFIG_SCHEMA = cv.All(
)
.extend(BASE_OTA_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
_validate_no_password_with_encryption,
_consume_ota_sockets,
)
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
def FILTER_SOURCE_FILES() -> list[str]:
"""Filter out the noise transport when no ota entry configures encryption."""
for ota_conf in CORE.config.get(CONF_OTA, []):
if (
ota_conf.get(CONF_PLATFORM) == CONF_ESPHOME
and ota_conf.get(CONF_ENCRYPTION) is not None
):
return []
return ["ota_esphome_noise.cpp"]
@coroutine_with_priority(CoroPriority.OTA_UPDATES)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
@@ -296,12 +171,6 @@ async def to_code(config: ConfigType) -> None:
if config.get(CONF_ALLOW_PARTITION_ACCESS):
cg.add_define("USE_OTA_PARTITIONS")
if (encryption_conf := config.get(CONF_ENCRYPTION)) is not None:
# A missing key was resolved from the api component in final validate.
key = encryption_conf[CONF_KEY]
cg.add_define("USE_OTA_ENCRYPTION")
cg.add(var.set_noise_psk(list(decode_encryption_key(key))))
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
+27 -97
View File
@@ -27,6 +27,7 @@ namespace esphome {
static const char *const TAG = "esphome.ota";
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
static constexpr size_t OTA_BUFFER_SIZE = 1024; // buffer size for OTA data transfer
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000; // milliseconds for initial handshake
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000; // milliseconds for data transfer
@@ -104,11 +105,6 @@ void ESPHomeOTAComponent::dump_config() {
ESP_LOGCONFIG(TAG, " Password configured");
}
#endif
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ctx_.has_psk()) {
ESP_LOGCONFIG(TAG, " Encryption configured");
}
#endif
#ifdef USE_OTA_PARTITIONS
ESP_LOGCONFIG(TAG,
" Partition access allowed\n"
@@ -132,8 +128,7 @@ void ESPHomeOTAComponent::dump_config() {
esp_partition_iterator_release(it);
esp_bootloader_desc_t bootloader_desc;
esp_err_t err = esp_ota_get_bootloader_description(nullptr, &bootloader_desc);
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s",
(err == ESP_OK) ? bootloader_desc.idf_ver : LOG_STR_LITERAL("version unknown"));
ESP_LOGCONFIG(TAG, " Bootloader: ESP-IDF %s", (err == ESP_OK) ? bootloader_desc.idf_ver : "version unknown");
#endif // USE_ESP32
#endif // USE_OTA_PARTITIONS
}
@@ -153,10 +148,8 @@ void ESPHomeOTAComponent::loop() {
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_COMPRESSION = 0x01;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_SHA256_AUTH = 0x02;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL = 0x04;
static constexpr uint8_t CLIENT_FEATURE_SUPPORTS_NOISE = 0x08;
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_COMPRESSION = 0x01;
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS = 0x02;
static constexpr uint8_t SERVER_FEATURE_SUPPORTS_NOISE = 0x04;
void ESPHomeOTAComponent::handle_handshake_() {
/// Handle the OTA handshake and authentication.
@@ -208,7 +201,8 @@ void ESPHomeOTAComponent::handle_handshake_() {
}
// Validate magic bytes
if (memcmp(this->handshake_buf_, MAGIC_BYTES, sizeof(MAGIC_BYTES)) != 0) {
static const uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
if (memcmp(this->handshake_buf_, MAGIC_BYTES, 5) != 0) {
ESP_LOGW(TAG, "Magic bytes mismatch! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", this->handshake_buf_[0],
this->handshake_buf_[1], this->handshake_buf_[2], this->handshake_buf_[3], this->handshake_buf_[4]);
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_MAGIC);
@@ -240,19 +234,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
}
this->ota_features_ = this->handshake_buf_[0];
ESP_LOGV(TAG, "Features: 0x%02X", this->ota_features_);
#ifdef USE_OTA_ENCRYPTION
// Fail closed: with a PSK configured the client must negotiate encryption
// (which requires the extended protocol); refuse plaintext uploads.
static constexpr uint8_t NOISE_REQUIRED_FEATURES =
CLIENT_FEATURE_SUPPORTS_NOISE | CLIENT_FEATURE_SUPPORTS_EXTENDED_PROTOCOL;
if (this->noise_ctx_.has_psk() && (this->ota_features_ & NOISE_REQUIRED_FEATURES) != NOISE_REQUIRED_FEATURES) {
ESP_LOGW(TAG, "Client does not support encryption");
this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_ENCRYPTION_REQUIRED);
return;
}
#endif
this->transition_ota_state_(OTAState::FEATURE_ACK);
const bool supports_compression =
@@ -268,11 +249,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
this->handshake_buf_[1] = (supports_compression ? SERVER_FEATURE_SUPPORTS_COMPRESSION : 0);
#ifdef USE_OTA_PARTITIONS
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_PARTITION_ACCESS;
#endif
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ctx_.has_psk()) {
this->handshake_buf_[1] |= SERVER_FEATURE_SUPPORTS_NOISE;
}
#endif
} else {
this->handshake_buf_[0] =
@@ -288,20 +264,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
if (!this->try_write_(ack_size, LOG_STR("ack feature"))) {
return;
}
#ifdef USE_OTA_ENCRYPTION
// With a PSK configured the rest of the session runs inside the noise
// transport; the client sends the first handshake frame next, so there
// is nothing to do until data arrives.
if (this->noise_ctx_.has_psk()) {
// handshake_buf_ still holds the feature ack composed above; a
// would-block re-entry lands here without rebuilding it
if (!this->noise_start_session_(this->handshake_buf_[1])) {
return;
}
this->transition_ota_state_(OTAState::NOISE_HANDSHAKE);
return;
}
#endif
#ifdef USE_OTA_PASSWORD
// If password is set, move to auth phase
if (!this->password_.empty()) {
@@ -339,16 +301,6 @@ void ESPHomeOTAComponent::handle_handshake_() {
this->handle_data_();
return;
#ifdef USE_OTA_ENCRYPTION
case OTAState::NOISE_HANDSHAKE:
if (!this->handle_noise_handshake_()) {
return;
}
this->transition_ota_state_(OTAState::DATA);
this->handle_data_();
return;
#endif
default:
break;
}
@@ -387,8 +339,6 @@ void ESPHomeOTAComponent::handle_data_() {
/// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses
/// wakeable_delay() in read();
/// write() always returns immediately
// Backend calls overwrite this with OK; reset to UNKNOWN before any
// goto error that follows a successful begin()/write()
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
size_t total = 0;
uint32_t last_progress = 0;
@@ -410,11 +360,11 @@ void ESPHomeOTAComponent::handle_data_() {
this->client_->setblocking(true);
// Acknowledge auth OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_AUTH_OK);
this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
if (this->extended_proto_) {
// Read ota type, 1 byte
if (!this->data_readall_(buf, 1)) {
if (!this->readall_(buf, 1)) {
this->log_read_error_(LOG_STR("OTA type"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
@@ -423,7 +373,7 @@ void ESPHomeOTAComponent::handle_data_() {
ESP_LOGV(TAG, "OTA type is 0x%02x", ota_type);
// Read size, 4 bytes MSB first
if (!this->data_readall_(buf, 4)) {
if (!this->readall_(buf, 4)) {
this->log_read_error_(LOG_STR("size"));
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
@@ -454,12 +404,11 @@ void ESPHomeOTAComponent::handle_data_() {
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
// Acknowledge prepare OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
// Read binary MD5, 32 bytes
if (!this->data_readall_(buf, 32)) {
if (!this->readall_(buf, 32)) {
this->log_read_error_(LOG_STR("MD5 checksum"));
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
sbuf[32] = '\0';
@@ -467,7 +416,7 @@ void ESPHomeOTAComponent::handle_data_() {
this->backend_->set_update_md5(sbuf);
// Acknowledge MD5 OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
// Track when we last received data so a silently-vanished peer (no FIN/RST
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
@@ -483,35 +432,19 @@ void ESPHomeOTAComponent::handle_data_() {
}
size_t remaining = ota_size - total;
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
ssize_t read;
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ != nullptr) {
// One frame per call; noise_read_data_ waits internally (readall_), so
// there is no would-block retry here and failures are already logged.
read = this->noise_read_data_(buf, requested);
if (read <= 0) {
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
} else
#endif
{
read = this->client_->read(buf, requested);
if (read == -1) {
const int err = errno;
if (this->would_block_(err)) {
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
App.feed_wdt();
continue;
}
ESP_LOGW(TAG, "Read err %d", err);
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} else if (read == 0) {
ESP_LOGW(TAG, "Remote closed");
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
ssize_t read = this->client_->read(buf, requested);
if (read == -1) {
const int err = errno;
if (this->would_block_(err)) {
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
App.feed_wdt();
continue;
}
ESP_LOGW(TAG, "Read err %d", err);
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} else if (read == 0) {
ESP_LOGW(TAG, "Remote closed");
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
last_data_ms = millis();
@@ -523,7 +456,7 @@ void ESPHomeOTAComponent::handle_data_() {
total += read;
#if USE_OTA_VERSION == 2
while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
this->data_write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
size_acknowledged += OTA_BLOCK_SIZE;
}
#endif
@@ -542,7 +475,7 @@ void ESPHomeOTAComponent::handle_data_() {
}
// Acknowledge receive OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
error_code = this->backend_->end();
if (error_code != ota::OTA_RESPONSE_OK) {
@@ -551,10 +484,10 @@ void ESPHomeOTAComponent::handle_data_() {
}
// Acknowledge Update end OK - 1 byte
this->data_write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
// Read ACK
if (!this->data_readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
this->log_read_error_(LOG_STR("ack"));
// do not go to error, this is not fatal
}
@@ -577,7 +510,7 @@ void ESPHomeOTAComponent::handle_data_() {
App.safe_reboot();
error:
this->data_write_byte_(static_cast<uint8_t>(error_code));
this->write_byte_(static_cast<uint8_t>(error_code));
// Abort backend before cleanup - cleanup_connection_() destroys the backend.
// Always call abort() unconditionally: backends register external partitions before
@@ -744,9 +677,6 @@ void ESPHomeOTAComponent::cleanup_connection_() {
this->backend_ = nullptr;
#ifdef USE_OTA_PASSWORD
this->cleanup_auth_();
#endif
#ifdef USE_OTA_ENCRYPTION
this->noise_ = nullptr;
#endif
// Intentionally no disable_loop() — letting loop() run one more iteration catches
// any connection that queued on the listener mid-session (otherwise the wake flag,
+1 -69
View File
@@ -4,9 +4,6 @@
#ifdef USE_OTA
#include "esphome/components/ota/ota_backend_factory.h"
#include "esphome/components/socket/socket.h"
#ifdef USE_OTA_ENCRYPTION
#include "esphome/components/noise/noise_handshake.h"
#endif
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
@@ -27,10 +24,7 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
AUTH_SEND, // Sending authentication request
AUTH_READ, // Reading authentication data
#endif // USE_OTA_PASSWORD
#ifdef USE_OTA_ENCRYPTION
NOISE_HANDSHAKE, // Exchanging Noise handshake frames
#endif
DATA, // BLOCKING! Processing OTA data (update, etc.)
DATA, // BLOCKING! Processing OTA data (update, etc.)
};
#ifdef USE_OTA_PASSWORD
void set_auth_password(const std::string &password) { password_ = password; }
@@ -44,10 +38,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
}
#endif // USE_OTA_PASSWORD
#ifdef USE_OTA_ENCRYPTION
void set_noise_psk(noise::psk_t psk) { this->noise_ctx_.set_psk(psk); }
#endif
/// Manually set the port OTA should listen on
void set_port(uint16_t port) { this->port_ = port; }
@@ -73,48 +63,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
bool writeall_(const uint8_t *buf, size_t len);
inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
#ifdef USE_OTA_ENCRYPTION
// Heap-allocated only while an encrypted OTA session is active.
struct NoiseSession {
~NoiseSession();
noise::NoiseResponderHandshake handshake;
NoiseCipherState *send_cipher{nullptr};
NoiseCipherState *recv_cipher{nullptr};
uint16_t frame_len{0}; // total frame size once the header is parsed, 0 until then
uint16_t frame_pos{0}; // bytes read or written so far
bool writing{false}; // a produced handshake frame is still being flushed
uint8_t frame_buf[noise::FRAME_HEADER_SIZE + 1 + noise::MAX_HANDSHAKE_SIZE];
};
bool noise_start_session_(uint8_t server_feature_flags);
bool handle_noise_handshake_();
bool noise_try_read_frame_();
bool noise_try_write_frame_();
void noise_send_reject_(const LogString *reason);
ssize_t noise_decrypt_(uint8_t *buf, size_t len);
ssize_t noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext);
bool noise_readall_(uint8_t *buf, size_t len);
ssize_t noise_read_data_(uint8_t *buf, size_t capacity);
bool noise_write_byte_(uint8_t byte);
#endif // USE_OTA_ENCRYPTION
// Data-phase I/O dispatch: through the noise transport when a session is
// active, straight to the socket otherwise.
inline bool data_write_byte_(uint8_t byte) {
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ != nullptr)
return this->noise_write_byte_(byte);
#endif
return this->write_byte_(byte);
}
// When encrypted, buf must have room for len + noise::MAC_SIZE bytes.
inline bool data_readall_(uint8_t *buf, size_t len) {
#ifdef USE_OTA_ENCRYPTION
if (this->noise_ != nullptr)
return this->noise_readall_(buf, len);
#endif
return this->readall_(buf, len);
}
bool try_read_(size_t to_read, const LogString *desc);
bool try_write_(size_t to_write, const LogString *desc);
@@ -143,10 +91,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
std::string password_;
std::unique_ptr<uint8_t[]> auth_buf_;
#endif // USE_OTA_PASSWORD
#ifdef USE_OTA_ENCRYPTION
noise::NoiseContext noise_ctx_;
std::unique_ptr<NoiseSession> noise_;
#endif // USE_OTA_ENCRYPTION
socket::ListenSocket *server_{nullptr};
std::unique_ptr<socket::Socket> client_;
@@ -154,18 +98,6 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
uint32_t client_connect_time_{0};
static constexpr size_t HANDSHAKE_BUF_SIZE = 5;
// Buffer size for OTA data transfer. The upload client derives its maximum
// encrypted frame plaintext from this (espota2.NOISE_MAX_PLAINTEXT is this
// minus the 16-byte MAC); both must change together.
static constexpr size_t OTA_BUFFER_SIZE = 1040;
#ifdef USE_OTA_ENCRYPTION
// espota2.NOISE_MAX_PLAINTEXT; shrinking the buffer would reject every
// frame a current CLI sends
static constexpr size_t NOISE_CLIENT_MAX_PLAINTEXT = 1024;
static_assert(OTA_BUFFER_SIZE >= NOISE_CLIENT_MAX_PLAINTEXT + noise::MAC_SIZE,
"OTA_BUFFER_SIZE must fit a full encrypted data frame");
#endif
static constexpr uint8_t MAGIC_BYTES[5] = {0x6C, 0x26, 0xF7, 0x5C, 0x45};
#ifdef USE_OTA_PARTITIONS
uint32_t running_app_offset_{0};
size_t running_app_size_{0};
@@ -1,279 +0,0 @@
#include "ota_esphome.h"
#ifdef USE_OTA
#ifdef USE_OTA_ENCRYPTION
#include "esphome/components/noise/noise.h"
#include "esphome/components/ota/ota_backend.h"
#include "esphome/core/log.h"
#include <cstring>
#include <new>
#ifdef USE_ESP8266
#include <pgmspace.h>
#endif
namespace esphome {
static const char *const TAG = "esphome.ota";
#ifdef USE_ESP8266
static constexpr char OTA_NOISE_PROLOGUE_INIT[] PROGMEM = "NoiseOTAInit";
#else
static constexpr char OTA_NOISE_PROLOGUE_INIT[] = "NoiseOTAInit";
#endif
static constexpr size_t OTA_NOISE_PROLOGUE_INIT_LEN = sizeof(OTA_NOISE_PROLOGUE_INIT) - 1;
ESPHomeOTAComponent::NoiseSession::~NoiseSession() {
if (this->send_cipher != nullptr) {
noise_cipherstate_free(this->send_cipher);
}
if (this->recv_cipher != nullptr) {
noise_cipherstate_free(this->recv_cipher);
}
}
/** Allocate the session and start the responder handshake.
*
* The prologue binds the whole plaintext preamble, so any tampering with the
* negotiation (a stripped feature flag, a changed version) breaks the first
* handshake MAC on either side:
* "NoiseOTAInit" | magic(5) | OK,version | client_features | FEATURE_FLAGS,server_flags
*/
bool ESPHomeOTAComponent::noise_start_session_(uint8_t server_feature_flags) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
this->noise_ = std::unique_ptr<NoiseSession>(new (std::nothrow) NoiseSession());
if (this->noise_ == nullptr) {
ESP_LOGW(TAG, "Session allocation failed");
this->cleanup_connection_();
return false;
}
static constexpr size_t PROLOGUE_ACK_LEN = 2; // OTA_RESPONSE_OK + version
static constexpr size_t PROLOGUE_CLIENT_FEATURES_LEN = 1;
static constexpr size_t PROLOGUE_FEATURE_ACK_LEN = 2; // OTA_RESPONSE_FEATURE_FLAGS + server flags
uint8_t prologue[OTA_NOISE_PROLOGUE_INIT_LEN + sizeof(MAGIC_BYTES) + PROLOGUE_ACK_LEN + PROLOGUE_CLIENT_FEATURES_LEN +
PROLOGUE_FEATURE_ACK_LEN];
#ifdef USE_ESP8266
memcpy_P(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
#else
std::memcpy(prologue, OTA_NOISE_PROLOGUE_INIT, OTA_NOISE_PROLOGUE_INIT_LEN);
#endif
uint8_t *p = prologue + OTA_NOISE_PROLOGUE_INIT_LEN;
// Magic bytes, already validated in MAGIC_READ
std::memcpy(p, MAGIC_BYTES, sizeof(MAGIC_BYTES));
p += sizeof(MAGIC_BYTES);
// Our magic ack
*p++ = ota::OTA_RESPONSE_OK;
*p++ = USE_OTA_VERSION;
// The feature byte the client sent
*p++ = this->ota_features_;
// The feature ack we sent (noise requires the extended protocol)
*p++ = ota::OTA_RESPONSE_FEATURE_FLAGS;
*p++ = server_feature_flags;
int err = this->noise_->handshake.init(this->noise_ctx_.get_psk(), prologue, sizeof(prologue));
if (err != 0) {
ESP_LOGW(TAG, "Handshake init: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
this->cleanup_connection_();
return false;
}
return true;
}
/** Drive the non-blocking handshake from loop(); returns true once the
* transport ciphers are ready. A would-block returns false and the next
* loop() resumes from the NoiseSession cursors; on failure the connection
* is cleaned up.
*/
bool ESPHomeOTAComponent::handle_noise_handshake_() {
NoiseSession &s = *this->noise_;
while (true) {
if (s.writing) {
if (!this->noise_try_write_frame_()) {
return false; // would block, or errored and cleaned up
}
s.writing = false;
s.frame_pos = 0;
s.frame_len = 0;
}
switch (s.handshake.action()) {
case noise::NoiseResponderHandshake::Action::ACTION_READ: {
if (!this->noise_try_read_frame_()) {
return false;
}
const uint16_t payload_len = s.frame_len - noise::FRAME_HEADER_SIZE;
s.frame_pos = 0;
s.frame_len = 0;
if (s.frame_buf[noise::FRAME_HEADER_SIZE] != noise::HANDSHAKE_STATUS_OK) {
ESP_LOGW(TAG, "Bad handshake error byte: %u", s.frame_buf[noise::FRAME_HEADER_SIZE]);
this->cleanup_connection_();
return false;
}
int err = s.handshake.read_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, payload_len - 1);
if (err != 0) {
ESP_LOGW(TAG, "Handshake read: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
this->noise_send_reject_(noise::reject_reason_for(err));
this->cleanup_connection_();
return false;
}
break;
}
case noise::NoiseResponderHandshake::Action::ACTION_WRITE: {
size_t msg_len = 0;
int err =
s.handshake.write_message(s.frame_buf + noise::FRAME_HEADER_SIZE + 1, noise::MAX_HANDSHAKE_SIZE, msg_len);
if (err != 0) {
ESP_LOGW(TAG, "Handshake write: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
this->cleanup_connection_();
return false;
}
const uint16_t payload_len = msg_len + 1;
noise::write_frame_header(s.frame_buf, payload_len);
s.frame_buf[noise::FRAME_HEADER_SIZE] = noise::HANDSHAKE_STATUS_OK;
s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
s.frame_pos = 0;
s.writing = true;
break;
}
case noise::NoiseResponderHandshake::Action::ACTION_SPLIT: {
int err = s.handshake.split(s.send_cipher, s.recv_cipher);
if (err != 0) {
ESP_LOGW(TAG, "Handshake split: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
this->cleanup_connection_();
return false;
}
ESP_LOGD(TAG, "Noise handshake complete");
return true;
}
default: {
ESP_LOGW(TAG, "Bad handshake state");
this->cleanup_connection_();
return false;
}
}
}
}
/// Non-blocking read of one handshake frame into the session buffer.
bool ESPHomeOTAComponent::noise_try_read_frame_() {
NoiseSession &s = *this->noise_;
while (s.frame_pos < noise::FRAME_HEADER_SIZE) {
ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, noise::FRAME_HEADER_SIZE - s.frame_pos);
if (!this->handle_read_error_(read, LOG_STR("read noise header"))) {
return false;
}
s.frame_pos += read;
}
if (s.frame_len == 0) {
const uint16_t payload_len = encode_uint16(s.frame_buf[1], s.frame_buf[2]);
if (s.frame_buf[0] != noise::FRAME_INDICATOR || payload_len < 1 || payload_len > 1 + noise::MAX_HANDSHAKE_SIZE) {
ESP_LOGW(TAG, "Bad handshake frame: 0x%02X, %u bytes", s.frame_buf[0], payload_len);
this->cleanup_connection_();
return false;
}
s.frame_len = noise::FRAME_HEADER_SIZE + payload_len;
}
while (s.frame_pos < s.frame_len) {
ssize_t read = this->client_->read(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
if (!this->handle_read_error_(read, LOG_STR("read noise frame"))) {
return false;
}
s.frame_pos += read;
}
return true;
}
/// Non-blocking write of the pending session-buffer frame.
bool ESPHomeOTAComponent::noise_try_write_frame_() {
NoiseSession &s = *this->noise_;
while (s.frame_pos < s.frame_len) {
ssize_t written = this->client_->write(s.frame_buf + s.frame_pos, s.frame_len - s.frame_pos);
if (!this->handle_write_error_(written, LOG_STR("write noise frame"))) {
return false;
}
s.frame_pos += written;
}
return true;
}
/// Best-effort explicit reject frame so the client can log a readable reason.
void ESPHomeOTAComponent::noise_send_reject_(const LogString *reason) {
// Every reason here comes from noise::reject_reason_for(), so the exported
// floor is the exact capacity needed
uint8_t data[noise::FRAME_HEADER_SIZE + noise::MAC_FAILURE_PAYLOAD_SIZE];
const size_t payload_len =
noise::format_reject_payload(data + noise::FRAME_HEADER_SIZE, sizeof(data) - noise::FRAME_HEADER_SIZE, reason);
noise::write_frame_header(data, payload_len);
this->client_->write(data, noise::FRAME_HEADER_SIZE + payload_len); // Best effort, non-blocking
}
/// Decrypt a ciphertext in place; returns the plaintext size or -1.
ssize_t ESPHomeOTAComponent::noise_decrypt_(uint8_t *buf, size_t len) {
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, buf, len, len);
int err = noise_cipherstate_decrypt(this->noise_->recv_cipher, &mbuf);
if (err != 0) {
ESP_LOGW(TAG, "Decrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
return -1;
}
return mbuf.size;
}
/** Blocking read of one frame whose ciphertext size must be within the given
* bounds, decrypted in place; returns the plaintext size, or -1 on error.
* buf needs max_ciphertext capacity.
*/
ssize_t ESPHomeOTAComponent::noise_read_frame_blocking_(uint8_t *buf, size_t min_ciphertext, size_t max_ciphertext) {
uint8_t header[noise::FRAME_HEADER_SIZE];
if (!this->readall_(header, sizeof(header))) {
return -1;
}
const size_t ciphertext_len = encode_uint16(header[1], header[2]);
if (header[0] != noise::FRAME_INDICATOR || ciphertext_len < min_ciphertext || ciphertext_len > max_ciphertext) {
ESP_LOGW(TAG, "Bad frame: 0x%02X, %zu bytes", header[0], ciphertext_len);
return -1;
}
if (!this->readall_(buf, ciphertext_len)) {
return -1;
}
return this->noise_decrypt_(buf, ciphertext_len);
}
/** Blocking read of one frame whose plaintext must be exactly len bytes
* (control units are one unit per frame). buf needs len + noise::MAC_SIZE
* capacity; the plaintext lands at buf[0..len).
*/
bool ESPHomeOTAComponent::noise_readall_(uint8_t *buf, size_t len) {
return this->noise_read_frame_blocking_(buf, len + noise::MAC_SIZE, len + noise::MAC_SIZE) == (ssize_t) len;
}
/** Blocking read of one data-phase frame, decrypted in place; returns the
* plaintext size, or -1 on error. buf is the OTA_BUFFER_SIZE data buffer.
* The ciphertext must fit that buffer and its plaintext must fit what the
* caller accepts (the remaining image bytes).
*/
ssize_t ESPHomeOTAComponent::noise_read_data_(uint8_t *buf, size_t capacity) {
const size_t max_ciphertext = std::min(capacity + noise::MAC_SIZE, OTA_BUFFER_SIZE);
return this->noise_read_frame_blocking_(buf, noise::MAC_SIZE + 1, max_ciphertext);
}
/// Blocking write of one response byte as an encrypted frame.
bool ESPHomeOTAComponent::noise_write_byte_(uint8_t byte) {
uint8_t frame[noise::FRAME_HEADER_SIZE + 1 + noise::MAC_SIZE];
frame[noise::FRAME_HEADER_SIZE] = byte;
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame + noise::FRAME_HEADER_SIZE, 1, 1 + noise::MAC_SIZE);
int err = noise_cipherstate_encrypt(this->noise_->send_cipher, &mbuf);
if (err != 0) {
ESP_LOGW(TAG, "Encrypt: %s", LOG_STR_ARG(noise::noise_err_to_logstr(err)));
return false;
}
noise::write_frame_header(frame, mbuf.size);
return this->writeall_(frame, noise::FRAME_HEADER_SIZE + mbuf.size);
}
} // namespace esphome
#endif // USE_OTA_ENCRYPTION
#endif // USE_OTA
+1 -2
View File
@@ -334,9 +334,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) {
}
if (traits.supports_preset_modes()) {
ESP_LOGCONFIG(tag, "%s Supported presets:", prefix);
for (const char *s : traits.supported_preset_modes()) {
for (const char *s : traits.supported_preset_modes())
ESP_LOGCONFIG(tag, "%s - %s", prefix, s);
}
}
}
+7 -10
View File
@@ -93,8 +93,7 @@ void FeedbackCover::set_open_sensor(binary_sensor::BinarySensor *open_feedback)
// setup callbacks to react to sensor changes
open_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
ESP_LOGD(TAG, "'%s' - Open feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_OPENING) {
this->endstop_reached_(true);
@@ -107,8 +106,7 @@ void FeedbackCover::set_close_sensor(binary_sensor::BinarySensor *close_feedback
this->close_feedback_ = close_feedback;
close_feedback->add_on_state_callback([this](bool state) {
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(),
state ? LOG_STR_LITERAL("STARTED") : LOG_STR_LITERAL("ENDED"));
ESP_LOGD(TAG, "'%s' - Close feedback '%s'.", this->name_.c_str(), state ? "STARTED" : "ENDED");
this->recompute_position_();
if (!state && this->infer_endstop_ && this->current_trigger_operation_ == COVER_OPERATION_CLOSING) {
this->endstop_reached_(false);
@@ -146,8 +144,7 @@ void FeedbackCover::endstop_reached_(bool open_endstop) {
// from a position slightly past the endpoint
if (this->current_trigger_operation_ == (open_endstop ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING)) {
float dur = (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
open_endstop ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(), open_endstop ? "Open" : "Close", dur);
// if there is no external mechanism, stop the cover
if (!this->has_built_in_endstop_) {
@@ -369,7 +366,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
// the case when an obstacle appears while moving is handled in the callback
if (obstacle != nullptr && obstacle->state) {
ESP_LOGD(TAG, "'%s' - %s obstacle detected. Action not started.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"));
dir == COVER_OPERATION_OPENING ? "Open" : "Close");
return;
}
#endif
@@ -386,9 +383,9 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
this->set_current_operation_(dir, true);
this->prev_command_trigger_ = trig;
ESP_LOGD(TAG, "'%s' - Firing '%s' trigger.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
: dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
: LOG_STR_LITERAL("STOP"));
dir == COVER_OPERATION_OPENING ? "OPEN"
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
: "STOP");
trig->trigger();
}
}
@@ -547,8 +547,8 @@ void FingerprintGrowComponent::dump_config() {
" System Identifier Code: 0x%.4X\n"
" Touch Sensing Pin: %s\n"
" Sensor Power Pin: %s",
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : LOG_STR_LITERAL("None"),
this->has_power_pin_ ? power_pin_buf : LOG_STR_LITERAL("None"));
this->system_identifier_code_, this->has_sensing_pin_ ? sensing_pin_buf : "None",
this->has_power_pin_ ? power_pin_buf : "None");
if (this->idle_period_to_sleep_ms_ < UINT32_MAX) {
ESP_LOGCONFIG(TAG, " Idle Period to Sleep: %" PRIu32 " ms", this->idle_period_to_sleep_ms_);
} else {
@@ -35,20 +35,18 @@ void GraphicalDisplayMenu::setup() {
}
void GraphicalDisplayMenu::dump_config() {
ESP_LOGCONFIG(
TAG,
"Graphical Display Menu\n"
" Has Display: %s\n"
" Popup Mode: %s\n"
" Advanced Drawing Mode: %s\n"
" Has Font: %s\n"
" Mode: %s\n"
" Active: %s\n"
" Menu items:",
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
YESNO(this->font_ != nullptr),
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? LOG_STR_LITERAL("Rotary") : LOG_STR_LITERAL("Joystick"),
YESNO(this->active_));
ESP_LOGCONFIG(TAG,
"Graphical Display Menu\n"
" Has Display: %s\n"
" Popup Mode: %s\n"
" Advanced Drawing Mode: %s\n"
" Has Font: %s\n"
" Mode: %s\n"
" Active: %s\n"
" Menu items:",
YESNO(this->display_ != nullptr), YESNO(this->display_ != nullptr), YESNO(this->display_ == nullptr),
YESNO(this->font_ != nullptr),
this->mode_ == display_menu_base::MENU_MODE_ROTARY ? "Rotary" : "Joystick", YESNO(this->active_));
for (size_t i = 0; i < this->displayed_item_->items_size(); i++) {
auto *item = this->displayed_item_->get_item(i);
ESP_LOGCONFIG(TAG, " %i: %s (Type: %s, Immediate Edit: %s)", i, item->get_text().c_str(),
@@ -69,12 +69,10 @@ void GT911Touchscreen::setup_internal_() {
// Direct MCU pin: attach a hardware interrupt, no polling needed.
this->attach_interrupt_(static_cast<InternalGPIOPin *>(this->interrupt_pin_),
active_high ? gpio::INTERRUPT_RISING_EDGE : gpio::INTERRUPT_FALLING_EDGE);
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s",
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
ESP_LOGD(TAG, "Interrupt pin: hardware interrupt, active %s", active_high ? "HIGH" : "LOW");
} else {
// IO expander pin: leave as output for configuration only.
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s",
active_high ? LOG_STR_LITERAL("HIGH") : LOG_STR_LITERAL("LOW"));
ESP_LOGD(TAG, "Interrupt pin: IO expander polling mode, active %s", active_high ? "HIGH" : "LOW");
}
}
}
+1 -2
View File
@@ -248,8 +248,7 @@ void HaierClimateBase::setup() {
void HaierClimateBase::dump_config() {
LOG_CLIMATE("", "Haier Climate", this);
ESP_LOGCONFIG(TAG, " Device communication status: %s",
this->valid_connection() ? LOG_STR_LITERAL("established") : LOG_STR_LITERAL("none"));
ESP_LOGCONFIG(TAG, " Device communication status: %s", this->valid_connection() ? "established" : "none");
}
void HaierClimateBase::loop() {
+5 -5
View File
@@ -343,11 +343,11 @@ void HonClimate::dump_config() {
this->hvac_hardware_info_.value().software_version_,
this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_);
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
(this->hvac_hardware_info_.value().functions_[0] ? LOG_STR_LITERAL(" interactive") : ""),
(this->hvac_hardware_info_.value().functions_[1] ? LOG_STR_LITERAL(" controller-device") : ""),
(this->hvac_hardware_info_.value().functions_[2] ? LOG_STR_LITERAL(" crc") : ""),
(this->hvac_hardware_info_.value().functions_[3] ? LOG_STR_LITERAL(" multinode") : ""),
(this->hvac_hardware_info_.value().functions_[4] ? LOG_STR_LITERAL(" role") : ""));
(this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""),
(this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""),
(this->hvac_hardware_info_.value().functions_[2] ? " crc" : ""),
(this->hvac_hardware_info_.value().functions_[3] ? " multinode" : ""),
(this->hvac_hardware_info_.value().functions_[4] ? " role" : ""));
ESP_LOGCONFIG(TAG, " Active alarms: %s", buf_to_hex(this->active_alarms_, sizeof(this->active_alarms_)).c_str());
}
}
@@ -29,9 +29,8 @@ void HBridgeSwitch::dump_config() {
LOG_PIN(" On Pin: ", this->on_pin_);
LOG_PIN(" Off Pin: ", this->off_pin_);
ESP_LOGCONFIG(TAG, " Pulse length: %" PRId32 " ms", this->pulse_length_);
if (this->wait_time_) {
if (this->wait_time_)
ESP_LOGCONFIG(TAG, " Wait time %" PRId32 " ms", this->wait_time_);
}
}
void HBridgeSwitch::write_state(bool state) {
+1
View File
@@ -96,6 +96,7 @@ void HC8Component::dump_config() {
" Warmup time: %" PRIu32 " s",
this->warmup_seconds_);
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(9600);
}
} // namespace esphome::hc8
-3
View File
@@ -47,9 +47,6 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
baud_rate=9600,
require_rx=True,
require_tx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
+1 -1
View File
@@ -38,7 +38,7 @@ void HDC302XComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"HDC302x:\n"
" Heater: %s",
this->heater_active_ ? LOG_STR_LITERAL("active") : LOG_STR_LITERAL("inactive"));
this->heater_active_ ? "active" : "inactive");
LOG_I2C_DEVICE(this);
LOG_UPDATE_INTERVAL(this);
LOG_SENSOR(" ", "Temperature", this->temp_sensor_);
+7 -8
View File
@@ -38,14 +38,14 @@ CoverTraits HE60rCover::get_traits() {
void HE60rCover::dump_config() {
LOG_COVER("", "HE60R Cover", this);
this->check_uart_settings(1200, 1, uart::UART_CONFIG_PARITY_EVEN, 8);
ESP_LOGCONFIG(TAG,
" Open Duration: %.1fs\n"
" Close Duration: %.1fs",
this->open_duration_ / 1e3f, this->close_duration_ / 1e3f);
auto restore = this->restore_state_();
if (restore.has_value()) {
if (restore.has_value())
ESP_LOGCONFIG(TAG, " Saved position %d%%", (int) (restore->position * 100.f));
}
}
void HE60rCover::endstop_reached_(CoverOperation operation) {
@@ -59,7 +59,7 @@ void HE60rCover::endstop_reached_(CoverOperation operation) {
if (this->last_command_ == operation) {
float dur = (float) (now - this->start_dir_time_) / 1e3f;
ESP_LOGD(TAG, "'%s' - %s endstop reached. Took %.1fs.", this->name_.c_str(),
operation == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("Open") : LOG_STR_LITERAL("Close"), dur);
operation == COVER_OPERATION_OPENING ? "Open" : "Close", dur);
}
this->publish_state();
}
@@ -77,9 +77,8 @@ void HE60rCover::process_rx_(uint8_t data) {
ESP_LOGV(TAG, "Process RX data %X", data);
if (!this->query_seen_) {
this->query_seen_ = data == QUERY_BYTE;
if (!this->query_seen_) {
if (!this->query_seen_)
ESP_LOGD(TAG, "RX Byte %02X", data);
}
return;
}
switch (data) {
@@ -214,9 +213,9 @@ void HE60rCover::start_direction_(CoverOperation dir) {
if (this->current_operation == dir)
return;
ESP_LOGD(TAG, "'%s' - Direction '%s' requested.", this->name_.c_str(),
dir == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("OPEN")
: dir == COVER_OPERATION_CLOSING ? LOG_STR_LITERAL("CLOSE")
: LOG_STR_LITERAL("STOP"));
dir == COVER_OPERATION_OPENING ? "OPEN"
: dir == COVER_OPERATION_CLOSING ? "CLOSE"
: "STOP");
if (dir == this->next_direction_) {
// either moving and needs to stop, or stopped and will move correctly on one trigger
+1 -2
View File
@@ -336,8 +336,7 @@ void HlkFm22xComponent::dump_config() {
}
if (this->enrolling_binary_sensor_) {
LOG_BINARY_SENSOR(" ", "Enrolling", this->enrolling_binary_sensor_);
ESP_LOGCONFIG(TAG, " Current Value: %s",
this->enrolling_binary_sensor_->state ? LOG_STR_LITERAL("ON") : LOG_STR_LITERAL("OFF"));
ESP_LOGCONFIG(TAG, " Current Value: %s", this->enrolling_binary_sensor_->state ? "ON" : "OFF");
}
if (this->face_count_sensor_) {
LOG_SENSOR(" ", "Face Count", this->face_count_sensor_);
+1 -2
View File
@@ -97,8 +97,7 @@ void HLW8012Component::update() {
if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) {
this->current_mode_ = !this->current_mode_;
ESP_LOGV(TAG, "Changing mode to %s mode",
this->current_mode_ ? LOG_STR_LITERAL("CURRENT") : LOG_STR_LITERAL("VOLTAGE"));
ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE");
this->change_mode_at_ = 0;
this->sel_pin_->digital_write(this->current_mode_);
}
@@ -257,9 +257,8 @@ void HoermannHcp::on_state_reg_(uint16_t value) {
}
}
// The low byte can change on its own, so only report a state we cannot decode once.
if (state != (previous >> 8)) {
if (state != (previous >> 8))
ESP_LOGW(TAG, "Unknown door state 0x%02X", state);
}
}
// Low byte of register 6: bit 0x10 is the lamp, bit 0x04 the relay. The reference implementation records
@@ -68,6 +68,8 @@ void HrxlMaxsonarWrComponent::check_buffer_() {
void HrxlMaxsonarWrComponent::dump_config() {
ESP_LOGCONFIG(TAG, "HRXL MaxSonar WR Sensor:");
LOG_SENSOR(" ", "Distance", this);
// As specified in the sensor's data sheet
this->check_uart_settings(9600, 1, esphome::uart::UART_CONFIG_PARITY_NONE, 8);
}
} // namespace esphome::hrxl_maxsonar_wr

Some files were not shown because too many files have changed in this diff Show More