Compare commits

...
Author SHA1 Message Date
J. Nick Koston 1e8f084741 [ci] Stop saving an empty apt cache when the install fails (#21713) 2026-10-27 19:34:58 +13:00
Jesse Hills 6595df0cab Bump version to 2026.10.1 2026-10-27 16:17:54 +13:00
J. Nick Koston 73894b2490 [core] Turn off the precompiled header with ccache older than 4.7 (#21627) 2026-10-27 16:17:53 +13:00
Jonathan Swoboda 9bef20d8dc [core] Avoid division by zero in Color::random_color() (#21623) 2026-10-27 16:17:53 +13:00
Jonathan Swoboda 0e440500a2 [core] Generate valid C++ for infinite float values (#21625) 2026-10-27 16:17:53 +13:00
minimicro34 badfa84fe0 [esp32_ble] Ignore unhandled BLE key GAP events (#21457) 2026-10-27 16:17:53 +13:00
J. Nick Koston e686ecf76e [core] Resolve version-less library dependencies like PlatformIO (#21317) 2026-10-27 16:17:53 +13:00
Jonathan Swoboda 64fb4e06b5 [ledc] Reset the peripheral through the HAL on targets without the legacy periph API (#21397) 2026-10-27 16:17:53 +13:00
Jonathan Swoboda cb82c9031a [core] Keep the color codes of a filtered build output line (#21396) 2026-10-27 16:17:53 +13:00
J. Nick KostonandJonathan Swoboda ca74ab9873 [climate] Add StringRef overloads for ClimateCall::set_fan_mode and set_preset (#21359)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-27 16:17:53 +13:00
J. Nick Koston 86fc740b65 [http_request] Copy a manifest URL set at run time and keep the config literal by pointer (#21373) 2026-10-27 16:17:53 +13:00
J. Nick Koston 450bb52f65 [mqtt] Resend the fan state when its first publish fails (#21328) 2026-10-27 16:17:35 +13:00
J. Nick Koston ed3615ec69 [light] Add a StringRef overload for LightCall::set_effect (#21358) 2026-10-27 16:15:01 +13:00
J. Nick Koston a5f25ebe2d [core] Emit generated lambda code after the user includes (#21307) 2026-10-27 16:12:58 +13:00
Jonathan Swoboda 7a4775de3d [esp32_hosted] Keep the OTA watchdog timeout through the coprocessor end call (#21300) 2026-10-27 15:48:16 +13:00
Jesse Hills 704551a6c9 Merge pull request #21268 from esphome/bump-2026.10.0
2026.10.0
2026-10-22 00:42:40 +13:00
Jesse Hills f30e70acfa Bump version to 2026.10.0 2026-10-21 23:53:12 +13:00
Jeroen 594a691d9f [web_server_idf] Increase HTTP server task stack to 4512 bytes (#21038) 2026-10-21 23:53:09 +13:00
Jesse Hills 6c90b5c549 Merge pull request #21254 from esphome/bump-2026.10.0b6
2026.10.0b6
2026-10-21 16:39:54 +13:00
Jesse Hills c8df23a9cb Bump version to 2026.10.0b6 2026-10-21 16:17:51 +13:00
4fdce9d96b [web_server_idf] Reduce loopTask stack usage for state SSE (#21109)
Co-authored-by: Jeroen Jansen <jeroen@Jeroens-MacBook-Air.local>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-21 16:17:49 +13:00
J. Nick Koston 9d296a5748 [ota] Accept an unknown image size in the RP2040 backend so web_server OTA works (#21178) 2026-10-21 16:17:49 +13:00
esphome[bot] f2e013d71a Bump bundled esphome-device-builder to 1.25.0 (#21175) 2026-10-21 16:17:49 +13:00
Jesse Hills 2001dd2be8 Merge pull request #21097 from esphome/bump-2026.10.0b5
2026.10.0b5
2026-10-19 14:03:02 +13:00
Jesse Hills a6a4b49e75 Bump version to 2026.10.0b5 2026-10-19 11:29:05 +13:00
Bonne EgglestonandJ. Nick Koston 649f2c91f5 [api] Poll the outgoing dial every loop (#21085)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-19 11:29:02 +13:00
Bonne Eggleston ffa20abe96 [api] Poll the outgoing dial every loop on deep sleep builds (#21077) 2026-10-19 11:29:02 +13:00
J. Nick Koston f979ddcf0d [bl0906] Read stale receive bytes instead of spinning on flush() (#21035) 2026-10-19 11:29:02 +13:00
J. Nick Koston 26064f1415 [uart_tcp] Expose the connection entities as platforms (#21008) 2026-10-19 11:29:02 +13:00
J. Nick Koston c50ee215f2 [tcp_uart] Expose the connection entities as platforms (#21009) 2026-10-19 10:36:23 +13:00
Jonathan Swoboda 0fbba03522 [debug] Log uptime in dump_config (#20989) 2026-10-19 10:33:13 +13:00
Jonathan Swoboda 7ccaedfb9e [esp32] Bump recommended ESP-IDF to 5.5.6 (#20987) 2026-10-19 10:33:13 +13:00
esphome[bot] 0173127239 Bump bundled esphome-device-builder to 1.24.0 (#20851)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-19 10:33:13 +13:00
Clyde Stubbs 235c614e68 [epaper_spi] Ensure test card gets drawn (#20804) 2026-10-19 10:33:13 +13:00
Jonathan Swoboda 79823c89ab [mipi_rgb] Rotate pure RGB panels in software (#20757) 2026-10-19 10:33:13 +13:00
J. Nick Koston e09cb64756 [bluetooth_connection] Value-initialize the connection parameters for ESP-IDF 6.1 (#20774) 2026-10-19 10:33:13 +13:00
J. Nick Koston bd5b3fa641 [esp32_ble_client] Value-initialize the connection parameters for ESP-IDF 6.1 (#20776) 2026-10-19 10:33:13 +13:00
Kevin AhrendtandJ. Nick Koston f464994f85 [api][sendspin] Add the Sendspin client id to the capabilities message (#20775)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-19 10:33:13 +13:00
esphome[bot] 4552dbad7a Bump aioesphomeapi from 46.11.0 to 46.12.0 (#20781)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-19 10:33:13 +13:00
J. Nick Koston ad8cd0dd3e [noise] Initialize the protocol id's reserved field for ESP-IDF 6.1 (#20779) 2026-10-19 10:33:13 +13:00
puddlyandJ. Nick Koston dd2582bdfd [usb_uart] Expose interface_class and port_number (#20659)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-19 10:33:13 +13:00
J. Nick Koston ba575600dc [mqtt] Match subscription topics in a loop instead of recursing per character (#20767) 2026-10-19 10:33:13 +13:00
esphome[bot] cd6c0dd402 Bump aioesphomeapi from 46.10.0 to 46.11.0 (#20766)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-19 10:33:13 +13:00
4645d34e1a [web_server_idf] Keep active SSE sessions out of the LRU purge (#17383)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-19 10:33:13 +13:00
dff1422e36 [mdns] Advertise the connected interface in the network TXT record (#20673)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-19 10:33:13 +13:00
Jesse Hills 9d530f5c56 Merge pull request #20680 from esphome/bump-2026.10.0b4
2026.10.0b4
2026-10-14 11:14:49 +13:00
Jesse Hills 5a78f636b9 Bump version to 2026.10.0b4 2026-10-14 10:43:50 +13:00
tomaszduda23andClaude 53d504137b [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-14 10:43:49 +13:00
bb0546fc0b [script] Keep the running script.wait item out of the queue while its chain runs (#20570)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 10:43:49 +13:00
emes30andJ. Nick Koston b43630cf8e [rp2_pio] Use pioasm 4.1.0 so PIO programs run on RP2350 (#19260)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 10:43:49 +13:00
Firat HajiyevandFirat17aze c9664d4e42 [wifi] Reject unsupported WPA3 on ESP8266 (#20592)
Co-authored-by: Firat17aze <187820330+Firat17aze@users.noreply.github.com>
2026-10-14 10:43:49 +13:00
Kevin Ahrendt 39ea6a3220 [audio] Bump microFLAC to v0.3.0 (#20591) 2026-10-14 10:43:49 +13:00
J. Nick Koston f4a6d6f18b [core] Create the PlatformIO HTTP cache dir before the prefetch resolve threads run (#20579) 2026-10-14 10:43:49 +13:00
esphome[bot] 6c24d4aa43 Bump aioesphomeapi from 46.9.0 to 46.10.0 (#20563) 2026-10-14 10:43:49 +13:00
Jesse Hills 2c4b1f3ed5 Merge pull request #20542 from esphome/bump-2026.10.0b3
2026.10.0b3
2026-10-12 15:42:25 +13:00
J. Nick Koston 0df7b0b366 [core] Move zstd_module from the api wizard to esphome.helpers (#20342) 2026-10-12 12:46:59 +13:00
Jesse Hills bd4b32873a Bump version to 2026.10.0b3 2026-10-12 11:52:33 +13:00
Bascht74 9e580db434 [uart] Reuse the ISR pin argument on an ESP8266 software serial reload (#20541) 2026-10-12 11:52:31 +13:00
Jesse Hills 87187c5391 [api] Let a wizard input pick multiple entities (#20537) 2026-10-12 11:52:31 +13:00
tomaszduda23andClaude 60402614cd [nrf52] fix zigbee time component (#20527)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-12 11:52:25 +13:00
puddly 2727d35655 [usb_uart] Fix invalid stop bits and parity enum mappings (#20408) 2026-10-12 11:51:44 +13:00
J. Nick Koston fb1ef29d54 [uart] Stop ESP8266 software serial RX ISR from blocking for a whole byte (#18519) 2026-10-12 11:51:39 +13:00
Frédéric MetrichandClaude Opus 5.5 7c0f63ea20 [core] Copy trigger arguments in wait_until and script.wait (#20536)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:46:44 +13:00
heikkikiiandJ. Nick Koston 5d75828ceb [seeed_mr24hpc1] Wait for the module to boot before sending UART traffic (#18860)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-12 11:46:44 +13:00
bd2e8d184d [gree] Keep display light on by default for YAN/YAA/YAC/YAC1FB9 (#18516)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-12 11:46:44 +13:00
J. Nick Koston 3c7ce71d6f [uart_tcp] Prove the accept has run before checking what a new client receives (#20489) 2026-10-12 11:46:44 +13:00
Frédéric MetrichandClaude Opus 5.5 58e03a875f [teleinfo] Fix out-of-bounds read when splitting a frame into groups (#20513)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:46:44 +13:00
Clyde StubbsandClaude Opus 5.5 41a9fca881 [ci] Reset sdkconfig after ESP-IDF component discovery (#20511)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:46:44 +13:00
ec58e864f5 [canbus] Fix high frame rates causing crashes due to unbounded iteration (#14481)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-12 11:46:44 +13:00
esphome[bot] 375fbd829e Bump bundled esphome-device-builder to 1.23.1 (#20497) 2026-10-12 11:46:44 +13:00
4bae89bd4a [nrf52] Fix BLE OTA/log device discovery & transport selection on macOS (#17101)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-12 11:46:43 +13:00
Bascht74andJ. Nick Koston 897b2f16a9 [tests] Close leftover server connections in the tcp_uart bridge and TCP client link tests (#20215)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-12 11:46:43 +13:00
tomaszduda23 93c8796ae8 [nrf52] fix build error on 3.4.0 with pch ON (#20474) 2026-10-12 11:46:43 +13:00
J. Nick Koston 654afb536e [core] Fix the native ESP8266 and host builds on Windows without ccache (#20468) 2026-10-12 11:46:43 +13:00
jabla 97154f9e5f [espidf] Clean the build when the IDF target changes (#20473) 2026-10-12 11:46:43 +13:00
c9e49b8e8f [teleinfo] Add predefined properties for main tags of TIC standard and historical (#9167)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-12 11:42:45 +13:00
J. Nick Koston 3ae52e21cf [usb_host] Warn instead of failing when a device duplicates a usb_uart entry (#20373) 2026-10-12 11:38:38 +13:00
Zhaoqi Xu f2395f9001 [heatpumpir] Fix fan speeds for mitsubishi_msc protocol (#20460) 2026-10-12 11:38:38 +13:00
Ali Alaeiandtomaszduda23 3d248496ab [zephyr_mcumgr] Enable MCUMGR_GRP_OS for UART transport, not just BLE (#18319)
Co-authored-by: tomaszduda23 <tomaszduda23@gmail.com>
2026-10-12 11:38:38 +13:00
esphome[bot] 34eca948c4 Bump bundled esphome-device-builder to 1.23.0 (#20446) 2026-10-12 11:38:37 +13:00
J. Nick Koston 58815575ef [core] Pass a single condition to actions directly instead of wrapping it in a group (#19223) 2026-10-12 11:38:37 +13:00
J. Nick Koston 28b012389d [ci] Keep bus group partners on the same runner (#20431) 2026-10-12 11:38:37 +13:00
J. Nick Koston 54f86537ad [esp32][libretiny] Skip registering static destructors that never run (#20440) 2026-10-12 11:38:37 +13:00
J. Nick Koston 3284d93a33 [nrf52] Fake a larger flash in testing mode (#20439) 2026-10-12 11:38:37 +13:00
J. Nick Koston 01686a5f67 [improv_serial] Use a numeric scheduler id for the Wi-Fi connect timeout (#20360) 2026-10-12 11:38:37 +13:00
J. Nick Koston 3b0c485701 [mqtt] Use a numeric scheduler id for the deferred state publish (#20359) 2026-10-12 11:38:37 +13:00
J. Nick Koston e61d348f0c [wifi] Use numeric scheduler ids instead of string names (#20357) 2026-10-12 11:38:37 +13:00
J. Nick Koston 22e594bc62 [esp32_ble_client] Track conn_id per attempt and stop logging an error on a failed open (#20417) 2026-10-12 11:38:37 +13:00
J. Nick Koston 061733672f [ci] Refit component batch estimates to a full run (#20401) 2026-10-12 11:38:37 +13:00
J. Nick Koston b1f5da9561 [esp8266] Keep ArduinoJson's float tables in flash (Arduino core 3.1.2-esphome.3) (#20416) 2026-10-12 11:38:37 +13:00
J. Nick Koston db517059a3 [ota] Collapse duplicate CI test variants (#20407) 2026-10-12 11:38:37 +13:00
J. Nick Koston 0b40892775 [mcp23xxx_base] Keep the interrupt handler in IRAM on ESP8266 (#20409) 2026-10-12 11:38:37 +13:00
J. Nick Koston 8e6ed71e2a [ethernet] Collapse duplicate CI test variants (#20406) 2026-10-12 11:38:37 +13:00
J. Nick Koston cf569702c4 [ci] Stop the toolchain tests from installing ESP-IDF (#20413) 2026-10-12 11:38:37 +13:00
241 changed files with 6117 additions and 1173 deletions

No files matched your search

+76
View File
@@ -0,0 +1,76 @@
name: Install apt packages (cached)
description: >-
Installs libsdl2-dev, libmbedtls-dev and ccache from one shared cache. The cache key comes
only from the package list and version, so both live here and nowhere else.
runs:
using: "composite"
steps:
- name: Keep a failed install out of the cache
# cache-apt-pkgs ignores install failures and saved an empty cache when a stale package
# list got a 404 (upstream awalsh128/cache-apt-pkgs-action#221). It uses an apt-fast
# already on PATH: this one refreshes the lists and retries when the install fails and,
# if that fails too, removes the folder the action would save. Only a cache miss installs.
shell: bash
run: |
sudo tee /etc/apt/apt.conf.d/99ci-acquire-timeouts >/dev/null <<'EOF'
Acquire::Retries "1";
Acquire::http::Timeout "15";
Acquire::https::Timeout "15";
EOF
sudo tee /usr/local/bin/apt-fast > /dev/null <<'EOF'
#!/bin/bash
if [[ " $* " != *" install "* ]]; then
exec apt-get "$@"
fi
touch /tmp/apt-fast-standin-ran
apt-get "$@" && exit 0
apt-get update -qq >&2
# The action pins each package to the version the stale lists named
args=()
for arg in "$@"; do
if [[ "${arg}" == -* ]]; then args+=("${arg}"); else args+=("${arg%%=*}"); fi
done
apt-get "${args[@]}" && exit 0
# The action writes our output to install.log inside the folder it saves
log="$(readlink -f "/proc/$$/fd/1")"
if [ "$(basename "${log}")" = install.log ]; then
rm -rf "$(dirname "${log}")"
else
touch /tmp/apt-fast-cleanup-missed
fi
exit 1
EOF
sudo chmod +x /usr/local/bin/apt-fast
- name: Install apt packages (cached)
id: apt
# Pinned on purpose and not watched by Dependabot: the step above relies on how this
# release calls apt-fast and where it writes install.log.
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
# 1.3 drops the empty caches saved before the step above existed
version: 1.3
- name: Check the packages are installed
shell: bash
env:
CACHE_HIT: ${{ steps.apt.outputs.cache-hit }}
run: |
if [ "${CACHE_HIT}" != "true" ] && [ ! -e /tmp/apt-fast-standin-ran ]; then
echo "::error::cache-apt-pkgs did not use the apt-fast stand-in; check the pinned release"
exit 1
fi
if [ -e /tmp/apt-fast-cleanup-missed ]; then
echo "::error::the failed install may have been cached; check the pinned release and bump the version"
exit 1
fi
# The action restores files without registering them with dpkg
for cmd in ccache sdl2-config; do
if ! command -v "${cmd}" > /dev/null; then
echo "::error::${cmd} is missing after the cached install"
exit 1
fi
done
if [ ! -f /usr/include/mbedtls/version.h ]; then
echo "::error::libmbedtls-dev is missing after the cached install"
exit 1
fi
+22 -23
View File
@@ -73,16 +73,17 @@ jobs:
runs-on: ubuntu-24.04
# PR-branch cache saves are invisible to other PRs, so dev/beta/release
# pushes seed the one shared entry PR jobs restore. The key is derived
# only from the package list and version; keep both identical in every
# step that restores it. In ci-status needs so a broken seed fails dev.
# only from the package list and version, kept in the apt-packages action.
# In ci-status needs so a broken seed fails dev.
if: github.event_name == 'push'
timeout-minutes: 10
steps:
- name: Install apt packages (cached)
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
packages: libsdl2-dev ccache
version: 1.1
sparse-checkout: .github
- name: Install apt packages (cached)
uses: ./.github/actions/apt-packages
determine-jobs:
name: Determine which jobs to run
@@ -409,14 +410,10 @@ jobs:
- 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. libsdl2-dev is needed by the headless display tests, which
# capture screenshots.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
uses: ./.github/actions/apt-packages
- name: Set up Python 3.13
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@@ -707,6 +704,12 @@ jobs:
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Install apt packages (cached)
if: matrix.pio_cache_key == 'tidyhost'
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
# by dsmr and dlms_meter.
timeout-minutes: 10
uses: ./.github/actions/apt-packages
- name: Restore Python
id: restore-python
@@ -1093,20 +1096,16 @@ jobs:
- name: List components
run: echo ${{ matrix.batch.components }}
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install apt packages (cached)
# A cache hit (seeded on dev by seed-apt-cache) never touches apt,
# so mirror outages cannot hang this PR-only job; the timeout bounds
# the cold path. Packages and version must match seed-apt-cache
# exactly. The action has no --no-install-recommends; same package
# set this job used before #17463.
# the cold path. The action has no --no-install-recommends; same
# package set this job used before #17463.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: ./.github/actions/apt-packages
- name: Restore Python
uses: ./.github/actions/restore-python
with:
+1 -1
View File
@@ -568,7 +568,7 @@ esphome/components/tca9555/* @mobrembski
esphome/components/tcl112/* @glmnet
esphome/components/tcp_uart/* @Bascht74
esphome/components/tee501/* @Stock-M
esphome/components/teleinfo/* @0hax
esphome/components/teleinfo/* @0hax @FredM67
esphome/components/tem3200/* @bakerkj
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
esphome/components/template/datetime/* @rfdarter
+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.0b2
PROJECT_NUMBER = 2026.10.1
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+1 -1
View File
@@ -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.22.0
RUN uv pip install --no-cache-dir esphome-device-builder==1.25.0
RUN \
platformio settings set enable_telemetry No \
-2
View File
@@ -44,7 +44,6 @@ from esphome.platformio.library import (
normalize_dependencies,
parse_library_json,
parse_library_properties,
warn_properties_depends,
)
_LOGGER = logging.getLogger(__name__)
@@ -283,7 +282,6 @@ def _bundled_library(framework_path: Path, name: str) -> ArduinoLibrary:
"resolved automatically; add them with add_library() if needed",
name,
)
warn_properties_depends(name, data)
build = data.get("build")
if isinstance(build, dict) and build.get("extraScript"):
# Scripts only run on the converted path; building without
+3 -3
View File
@@ -49,9 +49,9 @@ class FrameworkRelease(NamedTuple):
# Arduino core version -> its build in esphome-libs/arduino-esp8266
FRAMEWORK_RELEASES: dict[Version, FrameworkRelease] = {
Version(3, 1, 2): FrameworkRelease(
"3.1.2-esphome.2",
"9377d63f569c524823199b1897ed7fde2063e50e2676dd519a0d758ab73678f7",
37192128,
"3.1.2-esphome.3",
"3af533a85163804e164500a5b254dc23b922080e25ecb7dc37b3dd8ac84651db",
37185650,
),
}
+6 -51
View File
@@ -672,52 +672,20 @@ XorCondition = cg.esphome_ns.class_("XorCondition", Condition)
@register_condition("and", AndCondition, validate_condition_list)
async def and_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("or", OrCondition, validate_condition_list)
async def or_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("all", AndCondition, validate_condition_list)
async def all_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("or", OrCondition, validate_condition_list)
@register_condition("any", OrCondition, validate_condition_list)
async def any_condition_to_code(
@register_condition("xor", XorCondition, validate_condition_list)
async def condition_group_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
# A group of one is that condition for and, or and xor; skip the wrapper.
if len(conditions) == 1:
return conditions[0]
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@@ -734,19 +702,6 @@ async def not_condition_to_code(
return cg.new_Pvariable(condition_id, template_arg, condition)
@register_condition("xor", XorCondition, validate_condition_list)
async def xor_condition_to_code(
config: ConfigType,
condition_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
conditions = await build_condition_list(config, template_arg, args)
return cg.new_Pvariable(
condition_id, cg.TemplateArguments(len(conditions), *template_arg), conditions
)
@register_condition("lambda", LambdaCondition, cv.returning_lambda)
async def lambda_condition_to_code(
config: ConfigType,
+1 -1
View File
@@ -1057,7 +1057,7 @@ def write_project(paths: InstalledPaths, ccache: str | None) -> bool:
cxx = (toolchain_tool(paths.toolchain, "g++"),)
lines = [
*tool_lines((toolchain_tool(paths.toolchain, "gcc"),), cxx, ccache),
*compile_rule_lines(),
*compile_rule_lines(ccache),
*ar_rule_lines(toolchain_tool(paths.toolchain, "ar")),
*pch_rule_lines(),
"rule link",
+30 -13
View File
@@ -7,6 +7,7 @@ import re
import textwrap
from esphome.build_helpers import pch
from esphome.build_helpers.ccache import CCACHE_PCH_MIN_VERSION
from esphome.components.esp32 import (
get_esp32_variant,
get_excluded_builtin_components,
@@ -474,7 +475,7 @@ target_link_options(${{COMPONENT_LIB}} PUBLIC
# Where CMake puts the .gch of the src component; ccache reads the checksum
# next to it in place of the .gch
_PCH_SUM_PATH = "build/esp-idf/src/CMakeFiles/__idf_src.dir/cmake_pch.hxx.gch.sum"
# Where the Windows gate records its choice
# Where the CMake gate records its choice
_PCH_CHOICE_VAR = "ESPHOME_PCH"
@@ -490,16 +491,32 @@ def _pch_cmake_block() -> str:
block = f"""target_precompile_headers(${{COMPONENT_LIB}} PRIVATE
{headers}
)"""
if not pch.pch_needs_gcc_check():
if pch.pch_forced():
return f"\n# ESPHome precompiled header\n{block}\n"
# Before the first configure only CMake knows the compiler version
return f"""
# ESPHome precompiled header, unless GCC bug 14940 keeps it from loading
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND ({pch.PCH_WINDOWS_CMAKE_OLD_GCC}))
# Only CMake knows the compiler version before the first configure, and
# the ccache IDF found is the one the build runs
gcc_gate = ""
if pch.pch_needs_gcc_check():
gcc_gate = f"""if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND ({pch.PCH_WINDOWS_CMAKE_OLD_GCC}))
message(STATUS "ESPHome: GCC ${{CMAKE_CXX_COMPILER_VERSION}} cannot load a precompiled header on Windows; compiling without it")
set({_PCH_CHOICE_VAR} OFF CACHE BOOL "ESPHome precompiled header in use" FORCE)
else()
set({_PCH_CHOICE_VAR} ON CACHE BOOL "ESPHome precompiled header in use" FORCE)
set(_esphome_pch OFF)
endif()
"""
min_ccache = ".".join(map(str, CCACHE_PCH_MIN_VERSION))
return f"""
# ESPHome precompiled header, unless a check below rules it out
set(_esphome_pch ON)
{gcc_gate}if(CCACHE_ENABLE AND CCACHE_FOUND)
execute_process(COMMAND "${{CCACHE_FOUND}}" --version OUTPUT_VARIABLE _esphome_ccache ERROR_QUIET)
if(_esphome_ccache MATCHES "ccache version ([0-9.]+)")
if(CMAKE_MATCH_1 VERSION_LESS {min_ccache})
message(STATUS "ESPHome: ccache ${{CMAKE_MATCH_1}} can reuse stale objects with a precompiled header; compiling without it (upgrade ccache to {min_ccache} or newer, or set ESPHOME_PCH_ENABLE=1 to force)")
set(_esphome_pch OFF)
endif()
endif()
endif()
set({_PCH_CHOICE_VAR} ${{_esphome_pch}} CACHE BOOL "ESPHome precompiled header in use" FORCE)
if(_esphome_pch)
{textwrap.indent(block, " ")}
endif()
"""
@@ -516,10 +533,10 @@ def write_pch_checksum() -> None:
if not pch.pch_enabled():
return
# The gate's choice, cached by configure
if pch.pch_needs_gcc_check() and get_cmake_cache_value(_PCH_CHOICE_VAR) != "ON":
return
pch.log_pch_in_use()
# Written whatever the gate chose last time: a build tree without that
# choice, or a reconfigure that turns the pch on, must not see a stale sum
if pch.pch_forced() or get_cmake_cache_value(_PCH_CHOICE_VAR) == "ON":
pch.log_pch_in_use()
checksum = pch.pch_checksum(
CORE.relative_src_path(),
pch.PCH_DEFAULT_HEADERS,
+1 -1
View File
@@ -240,7 +240,7 @@ def write_project(compilers: HostCompilers, ccache: str | None) -> bool:
lines = [
*tool_lines(compilers.cc, compilers.cxx, ccache),
*compile_rule_lines(),
*compile_rule_lines(ccache),
*pch_rule_lines(),
"rule link",
" command = $cxx -o $out $linkflags @$out.rsp $archives $libdirflags $libflags",
+48 -1
View File
@@ -3,21 +3,68 @@ resolution, and default ``CCACHE_*`` values."""
from __future__ import annotations
import functools
import logging
import os
from pathlib import Path
import re
from esphome.framework_helpers import strip_win_long_path_prefix, tool_version_runs
from esphome.framework_helpers import (
strip_win_long_path_prefix,
tool_version_probe,
tool_version_runs,
)
from esphome.helpers import FALSY_ENV_STRINGS, TRUTHY_ENV_STRINGS
_LOGGER = logging.getLogger(__name__)
# Before 4.7 ccache stores a pch consumer whose header is newer than one
# second without that header, so a later build reuses it after the header
# changed (#21621)
CCACHE_PCH_MIN_VERSION = (4, 7)
_VERSION_RE = re.compile(r"ccache version (\d+(?:\.\d+)*)")
@functools.cache
def _ccache_probe(ccache: str) -> str | Exception:
"""``ccache --version``, run once per binary per process."""
return tool_version_probe(ccache)
def _ccache_runs(ccache: str) -> bool:
"""Return True when the ``ccache`` found on PATH actually runs."""
return tool_version_runs(
ccache,
"Ignoring ccache at %s because it failed to run; compiling without ccache",
probe=_ccache_probe,
)
@functools.cache
def _ccache_too_old_for_pch(ccache: str) -> bool:
output = _ccache_probe(ccache)
if not isinstance(output, str) or (found := _VERSION_RE.search(output)) is None:
return False
if tuple(map(int, found[1].split("."))) >= CCACHE_PCH_MIN_VERSION:
return False
_LOGGER.info(
"ccache %s at %s can reuse stale objects with a precompiled header; "
"compiling without it (upgrade ccache to %s or newer, or set "
"ESPHOME_PCH_ENABLE=1 to force)",
found[1],
ccache,
".".join(map(str, CCACHE_PCH_MIN_VERSION)),
)
return True
def ccache_breaks_pch() -> bool:
"""Whether the ccache this build uses is too old for a precompiled header.
ESP-IDF builds check the ccache IDF finds in CMake instead (build_gen.espidf)."""
return (ccache := resolve_ccache_path()) is not None and _ccache_too_old_for_pch(
ccache
)
+14 -7
View File
@@ -74,33 +74,40 @@ def tool_lines(
# path gets
f"python = {_q(strip_win_long_path_prefix(sys.executable))}",
f"buildtool = {_q(_BUILD_TOOL)}",
f"ccache = {_q(ccache) if ccache else ''}",
*([f"ccache = {_q(ccache)}"] if ccache else []),
"",
]
def compile_rule_lines() -> list[str]:
"""The compile rules; ``$own_includes`` is empty unless an edge sets it."""
def compile_rule_lines(ccache: str | None) -> list[str]:
"""The compile rules; ``$own_includes`` is empty unless an edge sets it.
``ccache`` is the launcher ``tool_lines`` was given; the same test here
keeps the variable and the rules in step. It is spliced in rather than
left as an empty variable: a leading space on the command line makes
CreateProcess fail on Windows.
"""
launcher = "$ccache " if ccache else ""
return [
"rule c",
" command = $ccache $cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
f" command = {launcher}$cc -MMD -MF $out.d $own_includes $cflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CC $out",
"rule cxx",
" command = $ccache $cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
f" command = {launcher}$cxx -MMD -MF $out.d $own_includes $cxxflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = CXX $out",
"rule aspp",
" command = $ccache $cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
f" command = {launcher}$cc -MMD -MF $out.d -x assembler-with-cpp $own_includes $asflags $flags -c $in -o $out",
" depfile = $out.d",
" deps = gcc",
" description = AS $out",
# Plain assembler, as SCons's ASCOM: no preprocessor, so no
# depfile and no $flags (defines/includes) either
"rule asm",
" command = $ccache $cc -x assembler $asflags -c $in -o $out",
f" command = {launcher}$cc -x assembler $asflags -c $in -o $out",
" description = AS $out",
]
+19 -4
View File
@@ -12,7 +12,11 @@ import re
import subprocess
import sys
from esphome.build_helpers.ccache import effective_ccache_basedir, parse_enable_env
from esphome.build_helpers.ccache import (
ccache_breaks_pch,
effective_ccache_basedir,
parse_enable_env,
)
from esphome.const import PLATFORM_NRF52
from esphome.helpers import write_file_if_changed
@@ -117,9 +121,16 @@ def pch_needs_gcc_check() -> bool:
return sys.platform == "win32" and parse_enable_env("ESPHOME_PCH_ENABLE") is None
def _ccache_rules_out_pch() -> bool:
"""An old ccache, unless ``ESPHOME_PCH_ENABLE=1`` forces the pch on.
ESP-IDF runs the same check in CMake (build_gen.espidf)."""
return not pch_forced() and ccache_breaks_pch()
def pch_usable(cxx: Sequence[Path | str]) -> bool:
"""The knob plus the host rule; ``ESPHOME_PCH_ENABLE=1`` skips the rule."""
if not pch_enabled():
"""The knob plus the ccache and host rules; ``ESPHOME_PCH_ENABLE=1``
skips the rules."""
if not pch_enabled() or _ccache_rules_out_pch():
return False
if not pch_needs_gcc_check():
return True
@@ -162,7 +173,11 @@ def pch_script_enabled() -> bool:
"""Whether this PlatformIO build takes the pch script."""
from esphome.core import CORE
return pch_enabled() and CORE.target_platform not in PCH_SCRIPT_EXCLUDED_PLATFORMS
return (
pch_enabled()
and CORE.target_platform not in PCH_SCRIPT_EXCLUDED_PLATFORMS
and not _ccache_rules_out_pch()
)
def pch_header_text(include_headers: Iterable[str]) -> str:
+3 -1
View File
@@ -111,7 +111,9 @@ class ToolOutput(RedirectText):
def _emit_line(self, line: str) -> None:
if self._progress and _PROGRESS.match(line):
if not self._is_filtered(line):
if self._is_filtered(line):
self._drop_line(line)
else:
text = _fit_terminal(line.strip("\r\n"))
self._write_color_replace(f"\r{text}\x1b[K")
self._on_progress_line = True
+25 -6
View File
@@ -403,6 +403,9 @@ message WizardCapabilities {
message SendspinCapabilities {
// Bitmask of the Sendspin features this device supports
uint32 feature_flags = 1;
// The Sendspin client_id: base64url of the client's static public key, 43 characters.
// Empty until the Sendspin client has started once since boot.
string client_id = 2 [(max_data_length) = 43];
}
message DeviceCapabilitiesResponse {
@@ -444,7 +447,7 @@ message DeviceWizardRequest {
// {"version": 1,
// "pages": [{"title": "...", "description": "...",
// "entities": [{"key": 123, "device_id": 456, "description": "..."}],
// "inputs": [{"key": 789, "description": "...",
// "inputs": [{"key": 789, "description": "...", "multiple": true, "max_length": 1023,
// "entity_filters": [{"integration": "...", "domain": ["..."],
// "device_class": ["..."], "supported_features": ["..."]}]}]}]}
//
@@ -468,6 +471,14 @@ message DeviceWizardRequest {
// an entity matches a filter when it satisfies every field that is set, and an entity
// that matches any one of the filters is accepted.
//
// An input with "multiple": true lets the user pick several entities. The client sends
// them in one WizardInputSetRequest as a comma separated list with no spaces
// ("calendar.a,calendar.b"); the device also accepts spaces around each id and stores the
// list without them. "max_length" (only on a multiple input, 255 when left out) is the
// longest list in bytes the device accepts. The version stays 1: a client that does not
// know "multiple" ignores it, shows a single picker and sends one entity id, which a
// multiple input accepts.
//
// The device keeps the entity id of an input in RAM only and does not store it across
// restarts. The client therefore stores the choice itself and, after every connection
// and before it sends SubscribeHomeAssistantStatesRequest, sends a WizardInputSetRequest
@@ -483,8 +494,13 @@ message DeviceWizardResponse {
// Sets the entity id of a wizard input (see WizardInputField.key). Clients send
// this only when DeviceCapabilitiesResponse.wizard.configured is set. There is no
// reply. The device ignores a request whose key matches no input or whose entity_id
// is empty, longer than 255 bytes or has no '.'. Otherwise it keeps the id in RAM,
// reply. The device ignores a request whose key matches no input. A single input is
// ignored when the entity_id is empty, longer than 255 bytes or has no '.'. A multiple
// input takes a comma separated list ("calendar.a,calendar.b"); the client sends no
// spaces, the device trims spaces around each id and stores the list without them. The
// whole request is ignored when any id is empty or has no '.', or when the list without
// spaces does not fit the input's max_length (255 by default). Otherwise it keeps the id
// or list in RAM,
// not across restarts, and when the input belongs to a homeassistant platform entity
// it subscribes to that entity's state. Clients send this for every input right
// after connecting, before SubscribeHomeAssistantStatesRequest. A request that
@@ -496,7 +512,7 @@ message WizardInputSetRequest {
option (ifdef) = "USE_API_WIZARD_INPUTS";
fixed32 key = 1;
string entity_id = 2 [(max_data_length) = 255];
string entity_id = 2 [(max_data_length) = 4095];
}
message ListEntitiesRequest {
@@ -3030,12 +3046,15 @@ enum SerialProxyIdentityFlag {
// IDs below are empty
}
// The descriptor fields of a USB device as seen by the host stack
// The descriptor fields of a USB device as seen by the host stack, and the port's index
// among those a vendor driver exposes on the interface
message UsbDeviceDescriptor {
uint32 vendor_id = 1;
uint32 product_id = 2;
uint32 bcd_device = 3;
uint32 interface_number = 4; // bInterfaceNumber the host driver binds to
uint32 interface_number = 4; // bInterfaceNumber the port is bound through
uint32 interface_class = 5; // bInterfaceClass of that interface
uint32 port_number = 6;
}
// The identity of the device behind a port. Identifies one physical device among others of
@@ -2128,6 +2128,7 @@ bool APIConnection::send_device_capabilities_response_() {
#endif
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
resp.sendspin.client_id = sendspin_::global_sendspin_hub->get_client_id();
#endif
return this->send_message(resp);
}
@@ -152,10 +152,7 @@ void OutgoingConnectionManager::loop(APIServer *server) {
void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
if (!network::is_connected()) {
// Flips within seconds of boot; recheck fast so a deep sleep wake
// window is not spent waiting
this->schedule_wait_(now, NETWORK_RETRY_MS);
return;
return; // still WAITING with the wait elapsed, so the next loop rechecks
}
struct sockaddr_storage addr;
socklen_t addr_len = this->target_sockaddr_(&addr);
@@ -197,7 +194,6 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) {
}
this->state_ = DialState::DIAL_STATE_CONNECTING;
this->state_ts_ = now;
this->last_poll_ = now;
}
void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
@@ -206,10 +202,6 @@ void OutgoingConnectionManager::poll_connect_(APIServer *server, uint32_t now) {
this->schedule_retry_(now);
return;
}
if (now - this->last_poll_ < CONNECT_POLL_INTERVAL_MS) {
return;
}
this->last_poll_ = now;
int err = 0;
switch (socket::poll_connect(*this->dial_socket_, err)) {
case socket::ConnectPollResult::CONNECT_POLL_RESULT_PENDING:
@@ -59,8 +59,6 @@ class OutgoingConnectionManager {
static constexpr uint32_t BACKOFF_MIN_MS = 5000;
static constexpr uint32_t BACKOFF_MAX_MS = 300000;
static constexpr uint32_t CONNECT_TIMEOUT_MS = 10000;
static constexpr uint32_t CONNECT_POLL_INTERVAL_MS = 250;
static constexpr uint32_t NETWORK_RETRY_MS = 500;
static constexpr uint32_t PRECONDITION_RETRY_MS = 5000;
// A deep sleep wake window is too short to spend on the delay, so those
// builds dial out as soon as the target is gone
@@ -104,7 +102,6 @@ class OutgoingConnectionManager {
uint32_t backoff_{BACKOFF_MIN_MS};
uint32_t wait_{BOOT_WAIT_MS};
uint32_t state_ts_{0};
uint32_t last_poll_{0};
// Byte-aligned types last
#ifndef API_OUTGOING_CONNECTION_HOST
+6
View File
@@ -310,12 +310,14 @@ uint32_t WizardCapabilities::calc_size_msg(const void *self) {
uint8_t *SendspinCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.feature_flags);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.client_id);
return pos;
}
uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.feature_flags);
size += !msg.client_id.empty() ? 2 + msg.client_id.size() : 0;
return size;
}
#endif
@@ -4175,6 +4177,8 @@ uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.product_id);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.bcd_device);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.interface_class);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.port_number);
return pos;
}
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
@@ -4184,6 +4188,8 @@ uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.product_id);
size += ProtoSize::calc_uint32(1, msg.bcd_device);
size += ProtoSize::calc_uint32(1, msg.interface_number);
size += ProtoSize::calc_uint32(1, msg.interface_class);
size += ProtoSize::calc_uint32(1, msg.port_number);
return size;
}
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
+3
View File
@@ -750,6 +750,7 @@ class WizardCapabilities final : public ProtoMessage {
class SendspinCapabilities final : public ProtoMessage {
public:
uint32_t feature_flags{0};
StringRef client_id{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -4083,6 +4084,8 @@ class UsbDeviceDescriptor final : public ProtoMessage {
uint32_t product_id{0};
uint32_t bcd_device{0};
uint32_t interface_number{0};
uint32_t interface_class{0};
uint32_t port_number{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
+3
View File
@@ -1119,6 +1119,7 @@ const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities"));
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
dump_field(out, ESPHOME_PSTR("client_id"), this->client_id);
return out.c_str();
}
#endif
@@ -2942,6 +2943,8 @@ const char *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
dump_field(out, ESPHOME_PSTR("interface_class"), this->interface_class);
dump_field(out, ESPHOME_PSTR("port_number"), this->port_number);
return out.c_str();
}
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
+49 -7
View File
@@ -14,26 +14,68 @@ namespace esphome::api {
static const char *const TAG = "api.wizard";
#ifdef USE_API_WIZARD_INPUTS
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
// Walks the comma separated entity ids in value, without the spaces around each. Returns the length they take joined
// by ',', or 0 when one is empty or has no '.'. Writes the joined list to out unless it is nullptr.
static size_t wizard_join_entity_ids(const char *value, size_t length, char *out) {
size_t joined = 0;
const char *end = value + length;
while (true) {
const char *comma = static_cast<const char *>(memchr(value, ',', end - value));
const char *id_end = comma != nullptr ? comma : end;
while (value < id_end && *value == ' ')
value++;
while (id_end > value && id_end[-1] == ' ')
id_end--;
size_t id_length = id_end - value;
if (id_length == 0 || memchr(value, '.', id_length) == nullptr)
return 0;
if (joined != 0) {
if (out != nullptr)
out[joined] = ',';
joined++;
}
if (out != nullptr)
memcpy(out + joined, value, id_length);
joined += id_length;
if (comma == nullptr)
return joined;
value = comma + 1;
}
}
#endif // USE_API_WIZARD_MULTIPLE_INPUTS
static bool wizard_entity_id_valid(const char *entity_id, size_t length) {
return length > 0 && length < WIZARD_ENTITY_ID_BUFFER_SIZE && memchr(entity_id, '.', length) != nullptr;
}
const char *wizard_set_input(const WizardInputSetRequest &msg) {
if (!wizard_entity_id_valid(msg.entity_id.c_str(), msg.entity_id.size())) {
ESP_LOGW(TAG, "Ignoring an invalid entity id for wizard input");
return nullptr;
}
const char *value = msg.entity_id.c_str();
const size_t length = msg.entity_id.size();
for (size_t i = 0; i < API_WIZARD_INPUT_COUNT; i++) {
// The table is in flash, which ESP8266 can only read through progmem_memcpy
WizardInputEntry entry;
progmem_memcpy(&entry, &API_WIZARD_INPUTS[i], sizeof(entry));
if (entry.key != msg.key)
continue;
memcpy(entry.entity_id, msg.entity_id.c_str(), msg.entity_id.size());
entry.entity_id[msg.entity_id.size()] = '\0';
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
if (entry.size != 0) {
// Two passes, validate and measure then write, so a rejected request leaves the buffer unchanged
const size_t joined = wizard_join_entity_ids(value, length, nullptr);
if (joined == 0 || joined >= entry.size)
break;
wizard_join_entity_ids(value, length, entry.entity_id);
entry.entity_id[joined] = '\0';
return entry.entity_id;
}
#endif
if (!wizard_entity_id_valid(value, length))
break;
memcpy(entry.entity_id, value, length);
entry.entity_id[length] = '\0';
return entry.entity_id;
}
ESP_LOGW(TAG, "Ignoring an entity id for an unknown wizard input");
ESP_LOGW(TAG, "Ignoring an entity id for an unknown wizard input, or one that is not valid");
return nullptr;
}
#endif // USE_API_WIZARD_INPUTS
+33 -2
View File
@@ -6,6 +6,9 @@
#include <cstddef>
#include <cstdint>
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
#include <cstring>
#endif
#include "esphome/core/hal.h"
#include "proto.h"
@@ -26,7 +29,10 @@ extern const uint8_t API_WIZARD_DATA[] PROGMEM;
/// Where the entity id of an input is kept, found by the key the client uses for it.
struct WizardInputEntry {
uint32_t key; // FNV-1 hash of the ESPHome id of the input
char *entity_id; // RAM buffer of WIZARD_ENTITY_ID_BUFFER_SIZE bytes, shared with the homeassistant entity
char *entity_id; // RAM buffer, WIZARD_ENTITY_ID_BUFFER_SIZE bytes or size, shared with the homeassistant entity
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
uint16_t size; // Buffer size of a multiple input, 0 for a single input
#endif
};
/// The inputs of the wizard, in flash. API_WIZARD_INPUT_COUNT entries long.
@@ -44,9 +50,34 @@ const char *wizard_set_input(const WizardInputSetRequest &msg);
class WizardInput {
public:
explicit WizardInput(const char *entity_id) : entity_id_(entity_id) {}
/// The Home Assistant entity ID, empty until the wizard sets one.
/// The Home Assistant entity ID, empty until the wizard sets one. For a multiple input this is the whole
/// comma separated list, which homeassistant.action data can use as it is.
StringRef entity_id() const { return StringRef(this->entity_id_); }
bool has_entity_id() const { return this->entity_id_[0] != '\0'; }
#ifdef USE_API_WIZARD_MULTIPLE_INPUTS
/// The number of entity IDs in the list, 0 until the wizard sets one.
size_t entity_id_count() const {
if (!this->has_entity_id())
return 0;
size_t count = 1;
for (const char *p = this->entity_id_; (p = strchr(p, ',')) != nullptr; p++)
count++;
return count;
}
/// Call callback(StringRef) for each entity ID in the list, in order. The StringRef points into the list and is
/// not null terminated, so print it with "%.*s".
template<typename F> void for_each_entity_id(F &&callback) const {
// The device stores the list without spaces or empty entries
const char *start = this->entity_id_;
while (*start != '\0') {
const char *end = strchr(start, ',');
if (end == nullptr)
end = start + strlen(start);
callback(StringRef(start, end - start));
start = *end == ',' ? end + 1 : end;
}
}
#endif
protected:
const char *entity_id_;
+54 -17
View File
@@ -6,7 +6,6 @@ Home Assistant shows the wizard when the device is added. See api_wizard.h for t
from collections.abc import Callable
import importlib
import json
from types import ModuleType
from typing import Any
from esphome import automation
@@ -20,6 +19,8 @@ from esphome.const import (
CONF_ENTITY_ID,
CONF_ID,
CONF_INTERNAL,
CONF_MAX_LENGTH,
CONF_MULTIPLE,
CONF_NAME,
CONF_PAGES,
CONF_PLATFORM,
@@ -27,7 +28,12 @@ from esphome.const import (
)
from esphome.core import CORE, ID
import esphome.final_validate as fv
from esphome.helpers import fnv1_hash, fnv1_hash_object_id, fnv1a_32bit_hash
from esphome.helpers import (
fnv1_hash,
fnv1_hash_object_id,
fnv1a_32bit_hash,
zstd_module,
)
from esphome.types import ConfigType
API_DOMAIN = "api"
@@ -46,6 +52,10 @@ WizardInput = _API.class_("WizardInput")
WIZARD_ENTITY_ID_BUFFER_SIZE = (
256 # api_wizard.h; Home Assistant entity IDs are at most 255 bytes
)
WIZARD_ENTITY_ID_MAX_LENGTH = WIZARD_ENTITY_ID_BUFFER_SIZE - 1
# The value of a multiple input fits one WizardInputSetRequest, which the device accepts up to 8 KiB on ESP8266;
# api.proto max_data_length documents the same value
WIZARD_MULTIPLE_MAX_LENGTH = 4095
# One API message must fit APIBuffer::MAX_SIZE (65535) together with the largest frame header (7 bytes,
# Noise) and footer (16 bytes, Noise MAC)
WIZARD_RESPONSE_MAX_SIZE = 65535 - 7 - 16
@@ -101,6 +111,21 @@ WIZARD_ENTITY_SCHEMA = cv.Schema(
)
def _validate_multiple(conf: ConfigType) -> ConfigType:
if conf[CONF_MULTIPLE] and CONF_ENTITY in conf:
raise cv.Invalid(
"A linked input follows the state of exactly one Home Assistant entity, so multiple "
"is only for standalone inputs (id)",
path=[CONF_MULTIPLE],
)
if CONF_MAX_LENGTH in conf and not conf[CONF_MULTIPLE]:
raise cv.Invalid(
"max_length only applies to an input with multiple: true",
path=[CONF_MAX_LENGTH],
)
return conf
# An input is either standalone (id declares a new WizardInput) or linked to a homeassistant entity that has no
# entity_id of its own: Home Assistant sets it
WIZARD_INPUT_SCHEMA = cv.All(
@@ -109,6 +134,10 @@ WIZARD_INPUT_SCHEMA = cv.All(
cv.Optional(CONF_ID): cv.declare_id(WizardInput),
cv.Optional(CONF_ENTITY): cv.use_id(cg.EntityBase),
cv.Optional(CONF_DESCRIPTION): _wizard_text(WIZARD_DESCRIPTION_MAX_LENGTH),
cv.Optional(CONF_MULTIPLE, default=False): cv.boolean,
cv.Optional(CONF_MAX_LENGTH): cv.int_range(
min=WIZARD_ENTITY_ID_MAX_LENGTH, max=WIZARD_MULTIPLE_MAX_LENGTH
),
cv.Optional(CONF_TARGET): cv.Schema(
{
cv.Required(CONF_ENTITY): cv.All(
@@ -119,6 +148,7 @@ WIZARD_INPUT_SCHEMA = cv.All(
}
),
cv.has_exactly_one_key(CONF_ID, CONF_ENTITY),
_validate_multiple,
)
WIZARD_PAGE_SCHEMA = cv.All(
@@ -243,6 +273,8 @@ def _wizard_defines(wizard: ConfigType) -> set[str]:
defines.add("USE_API_WIZARD_LINKED_INPUTS")
if any(CONF_ID in conf for conf in inputs):
defines.add("USE_API_WIZARD_STANDALONE_INPUTS")
if any(conf.get(CONF_MULTIPLE) for conf in inputs):
defines.add("USE_API_WIZARD_MULTIPLE_INPUTS")
return defines
@@ -351,14 +383,6 @@ automation.register_apply_condition(
)
def zstd_module() -> ModuleType:
"""The zstd module: the standard library one from Python 3.14, otherwise the backport."""
try:
return importlib.import_module("compression.zstd")
except ImportError:
return importlib.import_module("backports.zstd")
def _entity_document(conf: ConfigType, config: fv.FinalValidateConfig) -> ConfigType:
"""An entity of the device that the page shows, keyed as ListEntitiesResponse keys it."""
_, declaration = _wizard_input_declaration(config, conf[CONF_ID])
@@ -374,6 +398,10 @@ def _input_document(conf: ConfigType, config: fv.FinalValidateConfig) -> ConfigT
document: ConfigType = {"key": fnv1_hash(_wizard_input_id(conf).id)}
if description := conf.get(CONF_DESCRIPTION):
document[CONF_DESCRIPTION] = description
if conf.get(CONF_MULTIPLE):
document["multiple"] = True
if (max_length := conf.get(CONF_MAX_LENGTH)) is not None:
document["max_length"] = max_length
if filters := _wizard_input_filters(conf, config):
document["entity_filters"] = [dict(entity_filter) for entity_filter in filters]
return document
@@ -387,7 +415,7 @@ def wizard_document(wizard: ConfigType, config: fv.FinalValidateConfig) -> Confi
{"version": 1,
"pages": [{"title": "...", "description": "...",
"entities": [{"key": 123, "device_id": 456, "description": "..."}],
"inputs": [{"key": 789, "description": "...",
"inputs": [{"key": 789, "description": "...", "multiple": true, "max_length": 1023,
"entity_filters": [{"integration": "...", "domain": ["..."],
"device_class": ["..."], "supported_features": ["..."]}]}]}]}
@@ -399,6 +427,14 @@ def wizard_document(wizard: ConfigType, config: fv.FinalValidateConfig) -> Confi
the device it belongs to (esphome/core/config.py), and is left out for the main device.
- An input key is the FNV-1 hash of the ESPHome id of the input, or of the linked entity. A linked
entity must not set entity_id, as that is a static entry kept apart from the ones the wizard sets.
- multiple means the user may pick several entities. The client sends them in one
WizardInputSetRequest as a comma separated list ("calendar.a,calendar.b") with no spaces; the
device trims spaces around each id and stores the list without them. The whole request is
ignored when any id is empty or has no '.', or when the list without spaces does not fit
max_length (only on a multiple input, 255 when left out). A single input is ignored when the id
is empty, longer than 255 bytes or has no '.'. The version stays 1: a client that does not know
multiple ignores it, shows a single picker and sends one entity id, which a multiple input
accepts.
- entity_filters are the filters of the input, or the default filters of a linked switch,
number, text, select or button.
"""
@@ -444,14 +480,15 @@ async def to_code(wizard: ConfigType) -> None:
input_id = _wizard_input_id(conf)
# Every buffer starts empty, until the wizard sets it. A linked homeassistant entity uses it as its entity id.
buffer = _wizard_buffer_name(input_id)
cg.add_global(
cg.RawStatement(
f'static char {buffer}[{WIZARD_ENTITY_ID_BUFFER_SIZE}] = "";'
)
)
size = conf.get(CONF_MAX_LENGTH, WIZARD_ENTITY_ID_MAX_LENGTH) + 1
cg.add_global(cg.RawStatement(f'static char {buffer}[{size}] = "";'))
if CONF_ID in conf:
cg.new_Pvariable(input_id, cg.RawExpression(buffer))
entries.append(cg.RawExpression(f"{{{fnv1_hash(input_id.id)}u, {buffer}}}"))
key = fnv1_hash(input_id.id)
if conf[CONF_MULTIPLE]:
entries.append(cg.RawExpression(f"{{{key}u, {buffer}, {size}}}"))
else:
entries.append(cg.RawExpression(f"{{{key}u, {buffer}}}"))
if entries:
cg.extern_progmem_array(
"esphome::api::API_WIZARD_INPUTS",
+1 -1
View File
@@ -391,7 +391,7 @@ async def to_code(config: ConfigType) -> None:
# Adds a define and IDF component for legacy `audio_decoder.cpp`.
if data.flac_support:
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
add_idf_component(name="esphome/micro-flac", ref="0.2.0")
add_idf_component(name="esphome/micro-flac", ref="0.3.0")
_emit_memory_pair(
data.flac.buffer_memory,
"CONFIG_MICRO_FLAC_PREFER_PSRAM",
+2 -2
View File
@@ -20,7 +20,7 @@ constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data)
void BL0906::loop() {
while (this->available())
this->flush();
this->read();
if (this->current_stage_ == STAGE_IDLE) {
// Woken up between cycles to drain the action queue. Go back to sleep.
@@ -92,7 +92,7 @@ void BL0906::advance_stage_() {
void BL0906::setup() {
while (this->available())
this->flush();
this->read();
this->write_array(USR_WRPROT_WITABLE, sizeof(USR_WRPROT_WITABLE));
// Calibration (1: register address; 2: value before calibration; 3: value after calibration)
this->bias_correction_(BL0906_RMSOS_1, 0.01600, 0); // Calibration current_1
@@ -382,7 +382,7 @@ void BluedroidGattClient::set_disconnecting_() {
esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {{0}};
esp_ble_conn_update_params_t conn_params = {};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
+16 -4
View File
@@ -19,6 +19,9 @@ CONF_REMOTE_TRANSMISSION_REQUEST = "remote_transmission_request"
CONF_CANBUS_ID = "canbus_id"
CONF_BIT_RATE = "bit_rate"
CONF_ON_FRAME = "on_frame"
CONF_MAX_FRAMES_PER_LOOP = "max_frames_per_loop"
DEFAULT_MAX_FRAMES_PER_LOOP = 50 # Keep in sync with max_frames_per_loop_ in canbus.h
DEFAULT_BIT_RATE = "125KBPS" # Keep in sync with bit_rate_ in canbus.h
def validate_id(config: ConfigType) -> ConfigType:
@@ -86,8 +89,13 @@ CANBUS_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CanbusComponent),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_BIT_RATE, default="125KBPS"): cv.enum(CAN_SPEEDS, upper=True),
cv.Optional(CONF_BIT_RATE, default=DEFAULT_BIT_RATE): cv.enum(
CAN_SPEEDS, upper=True
),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(
CONF_MAX_FRAMES_PER_LOOP, default=DEFAULT_MAX_FRAMES_PER_LOOP
): cv.positive_not_null_int,
cv.Optional(CONF_ON_FRAME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger),
@@ -108,9 +116,13 @@ CANBUS_SCHEMA.add_extra(validate_id)
async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
cg.add(var.set_can_id([config[CONF_CAN_ID]]))
cg.add(var.set_use_extended_id([config[CONF_USE_EXTENDED_ID]]))
cg.add(var.set_bitrate(CAN_SPEEDS[config[CONF_BIT_RATE]]))
cg.add(var.set_can_id(config[CONF_CAN_ID]))
if config[CONF_USE_EXTENDED_ID]:
cg.add(var.set_use_extended_id(True))
if (bit_rate := config[CONF_BIT_RATE]) != DEFAULT_BIT_RATE:
cg.add(var.set_bitrate(CAN_SPEEDS[bit_rate]))
if (max_frames := config[CONF_MAX_FRAMES_PER_LOOP]) != DEFAULT_MAX_FRAMES_PER_LOOP:
cg.add(var.set_max_frames_per_loop(max_frames))
for conf in config.get(CONF_ON_FRAME, []):
can_id = conf[CONF_CAN_ID]
+15 -7
View File
@@ -67,16 +67,18 @@ void Canbus::add_trigger(CanbusTrigger *trigger) {
void Canbus::loop() {
struct CanFrame can_message;
// read all messages until queue is empty
int message_counter = 0;
while (this->read_message(&can_message) == canbus::ERROR_OK) {
message_counter++;
uint32_t message_counter = 0;
for (; message_counter < this->max_frames_per_loop_; message_counter++) {
if (this->read_message(&can_message) != canbus::ERROR_OK) {
break;
}
if (can_message.use_extended_id) {
ESP_LOGD(TAG, "received can message (#%d) extended can_id=0x%" PRIx32 " size=%d", message_counter,
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") extended can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
} else {
ESP_LOGD(TAG, "received can message (#%d) std can_id=0x%" PRIx32 " size=%d", message_counter, can_message.can_id,
can_message.can_data_length_code);
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") std can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
}
std::vector<uint8_t> data;
@@ -100,6 +102,12 @@ void Canbus::loop() {
}
}
}
if (message_counter == this->max_frames_per_loop_) {
ESP_LOGW(TAG,
"Reached max_frames_per_loop=%" PRIu32 ", deferring remaining CAN frames. Some frames might be dropped",
this->max_frames_per_loop_);
}
}
} // namespace esphome::canbus
+4 -2
View File
@@ -78,6 +78,7 @@ class Canbus : public Component {
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
void set_max_frames_per_loop(uint32_t max_frames_per_loop) { this->max_frames_per_loop_ = max_frames_per_loop; }
void add_trigger(CanbusTrigger *trigger);
/**
@@ -96,8 +97,9 @@ class Canbus : public Component {
template<typename... Ts> friend class CanbusSendAction;
std::vector<CanbusTrigger *> triggers_{};
uint32_t can_id_;
bool use_extended_id_;
CanSpeed bit_rate_;
uint32_t max_frames_per_loop_{50}; // Keep in sync with DEFAULT_MAX_FRAMES_PER_LOOP in __init__.py
CanSpeed bit_rate_{CAN_125KBPS}; // Keep in sync with DEFAULT_BIT_RATE in __init__.py
bool use_extended_id_{false};
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{};
+8
View File
@@ -82,6 +82,10 @@ class ClimateCall {
ClimateCall &set_fan_mode(optional<std::string> fan_mode);
/// Set the custom fan mode of the climate device.
ClimateCall &set_fan_mode(const char *custom_fan_mode);
/// Set the custom fan mode of the climate device (StringRef overload to resolve ambiguity, zero-copy).
ClimateCall &set_fan_mode(StringRef custom_fan_mode) {
return this->set_fan_mode(custom_fan_mode.c_str(), custom_fan_mode.size());
}
/// Set the custom fan mode of the climate device (zero-copy API path).
ClimateCall &set_fan_mode(const char *custom_fan_mode, size_t len);
/// Set the swing mode of the climate device.
@@ -102,6 +106,10 @@ class ClimateCall {
ClimateCall &set_preset(optional<std::string> preset);
/// Set the custom preset of the climate device.
ClimateCall &set_preset(const char *custom_preset);
/// Set the custom preset of the climate device (StringRef overload to resolve ambiguity, zero-copy).
ClimateCall &set_preset(StringRef custom_preset) {
return this->set_preset(custom_preset.c_str(), custom_preset.size());
}
/// Set the custom preset of the climate device (zero-copy API path).
ClimateCall &set_preset(const char *custom_preset, size_t len);
@@ -34,6 +34,15 @@ void DebugComponent::dump_config() {
this->free_heap_ = get_free_heap_();
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
uint32_t uptime = static_cast<uint32_t>(millis_64() / 1000);
uint32_t days = uptime / 86400;
uptime -= days * 86400;
uint32_t hours = uptime / 3600;
uptime -= hours * 3600;
uint32_t minutes = uptime / 60;
uptime -= minutes * 60;
ESP_LOGD(TAG, "Uptime: %" PRIu32 "d %02" PRIu32 "h %02" PRIu32 "m %02" PRIu32 "s", days, hours, minutes, uptime);
pos = get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>(device_info_buffer), pos);
#ifdef USE_TEXT_SENSOR
+1
View File
@@ -208,6 +208,7 @@ def _final_validate(config) -> None:
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
+29 -5
View File
@@ -1099,7 +1099,7 @@ ARDUINO_PLATFORM_VERSION_LOOKUP = {
# See: https://github.com/pioarduino/esp-idf/releases
ARDUINO_IDF_VERSION_LOOKUP = {
cv.Version(4, 0, 0, "alpha1"): cv.Version(6, 0, 1),
cv.Version(3, 3, 12): cv.Version(5, 5, 5),
cv.Version(3, 3, 12): cv.Version(5, 5, 6),
cv.Version(3, 3, 11): cv.Version(5, 5, 5),
cv.Version(3, 3, 10): cv.Version(5, 5, 5),
cv.Version(3, 3, 9): cv.Version(5, 5, 4),
@@ -1123,9 +1123,9 @@ ARDUINO_IDF_VERSION_LOOKUP = {
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"recommended": cv.Version(5, 5, 5),
"latest": cv.Version(5, 5, 5),
"dev": cv.Version(5, 5, 5),
"recommended": cv.Version(5, 5, 6),
"latest": cv.Version(5, 5, 6),
"dev": cv.Version(5, 5, 6),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
@@ -1135,6 +1135,7 @@ ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(
6, 0, 0
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(5, 5, 6): cv.Version(55, 3, 312),
cv.Version(5, 5, 5): cv.Version(55, 3, 312),
cv.Version(5, 5, 4): cv.Version(55, 3, 39),
cv.Version(5, 5, 3, "1"): cv.Version(55, 3, 37),
@@ -2925,6 +2926,11 @@ async def to_code(config):
# produce reproducible outputs and downstream tooling can reuse artifacts.
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
# Static destructors never run, so skip registering them. See atexit_stubs.cpp.
# --undefined: libsrc.a is scanned before the IDF libraries that also register them.
cg.add_build_flag("-Wl,--wrap=__cxa_atexit")
cg.add_build_flag("-Wl,--undefined=__wrap___cxa_atexit")
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
@@ -3677,6 +3683,20 @@ def _write_sdkconfig():
clean_build(clear_pio_cache=False)
def _arduino_provides(name: str) -> bool:
"""Whether arduino-esp32 bundles the library a manifest depends on, so it is
never fetched by name; it still has to be enabled to build."""
if name not in ARDUINO_DISABLED_LIBRARIES:
return False
if name not in CORE.data[KEY_ESP32][KEY_ARDUINO_LIBRARIES]:
_LOGGER.warning(
"A library depends on the Arduino library %s, which is not enabled; "
"add it under esphome: libraries:",
name,
)
return True
def _write_idf_component_yml():
yml_path = CORE.relative_build_path("src/idf_component.yml")
dependencies: dict[str, dict] = {}
@@ -3698,7 +3718,11 @@ def _write_idf_component_yml():
# IDF sees the same requirement twice; converted components reach it through
# ${ESPHOME_PROJECT_MANAGED_COMPONENTS}.
managed = set(CORE.data[KEY_ESP32].get(KEY_COMPONENTS, {}))
converted = generate_idf_components(libraries, managed=managed)
converted = generate_idf_components(
libraries,
managed=managed,
provides=_arduino_provides if CORE.using_arduino else None,
)
# IDF names a component after its directory and a later registration of the
# same name replaces the earlier one, so a stub beside a converted library of
# the same name (espressif/libsodium vs esphome/libsodium) would win or lose
+20
View File
@@ -0,0 +1,20 @@
/*
* Linker wrap stub for __cxa_atexit().
*
* GCC registers the destructor of every global and function local static
* object with __cxa_atexit(). The firmware never returns from main or calls
* exit(), so these destructors never run, but the first registration links
* newlib's exit handler tables (about 400 bytes of RAM on ESP32).
*/
#include "esphome/core/defines.h"
#ifdef USE_ESP32
namespace esphome::esp32 {}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" int __wrap___cxa_atexit(void (*)(void *), void *, void *) { return 0; }
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_ESP32
+1
View File
@@ -719,6 +719,7 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
}
// Ignore these GAP events as they are not relevant for our use case
case ESP_GAP_BLE_KEY_EVT:
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
case ESP_GAP_BLE_PHY_UPDATE_COMPLETE_EVT: // BLE 5.0 PHY update complete
case ESP_GAP_BLE_CHANNEL_SELECT_ALGORITHM_EVT: // BLE 5.0 channel selection algorithm
@@ -150,6 +150,8 @@ void BLEClientBase::connect() {
// A registration whose event never arrived must not block this connection's release.
this->services_released_ = false;
this->pending_notify_regs_ = 0;
// A CONNECT_EVT seen while idle can leave the id of a link this attempt does not own.
this->conn_id_ = UNSET_CONN_ID;
// Enable loop for state processing
this->enable_loop();
// Immediately transition to CONNECTING to prevent duplicate connection attempts
@@ -298,7 +300,7 @@ void BLEClientBase::log_warning_(const char *message) {
esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {{0}};
esp_ble_conn_update_params_t conn_params = {};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
@@ -350,7 +352,6 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
if (!this->check_addr(param->open.remote_bda))
return false;
this->log_gattc_lifecycle_event_("OPEN");
// conn_id was already set in ESP_GATTC_CONNECT_EVT
this->service_count_ = 0;
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
@@ -362,19 +363,24 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
break;
}
if (this->state() != espbt::ClientState::CONNECTING) {
// This should not happen but lets log it in case it does
// because it means we have a bad assumption about how the
// ESP BT stack works.
const bool open_failed = param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN;
// Bluedroid reports a failed open as DISCONNECT_EVT then a failing OPEN_EVT, with no CONNECT_EVT,
// so DISCONNECTING with no id is expected for a failure. Anything else means a bad assumption
// about how the ESP BT stack works, so log it.
const bool expected_failure =
open_failed && this->state() == espbt::ClientState::DISCONNECTING && this->conn_id_ == UNSET_CONN_ID;
if (this->state() != espbt::ClientState::CONNECTING && !expected_failure) {
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
this->address_str_, espbt::client_state_to_string(this->state()), param->open.status);
}
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
if (open_failed) {
this->log_gattc_warning_("Connection open", param->open.status);
// Connection was never established so CLOSE_EVT may not follow
this->set_idle_();
break;
}
// An open on a link that is already up sends no CONNECT_EVT, so take the id here.
this->conn_id_ = param->open.conn_id;
if (this->want_disconnect_) {
// Disconnect was requested after connecting started,
// but before the connection was established. Now that we have
@@ -26,9 +26,14 @@ static const char *const TAG = "esp32_hosted.update";
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
constexpr size_t CHUNK_SIZE = 1500;
// OTA begin blocks while the coprocessor erases its partition
// OTA begin blocks while the coprocessor erases its partition, end while it validates the image
constexpr uint32_t OTA_WDT_TIMEOUT_MS = 60000;
static esp_err_t ota_end_guarded() {
watchdog::WatchdogManager wdt(OTA_WDT_TIMEOUT_MS);
return esp_hosted_slave_ota_end(); // NOLINT
}
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// Interval/timeout IDs (uint32_t to avoid string comparison)
constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
@@ -380,7 +385,7 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
} else {
ESP_LOGE(TAG, "Error reading firmware data: %d", read_or_error);
}
esp_hosted_slave_ota_end(); // NOLINT
ota_end_guarded();
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
@@ -390,7 +395,7 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
err = esp_hosted_slave_ota_write(buffer, read_or_error); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
ota_end_guarded();
container->end();
this->status_set_error(LOG_STR("Failed to write OTA data"));
return false;
@@ -402,7 +407,7 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
hasher.calculate();
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
ESP_LOGE(TAG, "SHA256 mismatch");
esp_hosted_slave_ota_end(); // NOLINT
ota_end_guarded();
this->status_set_error(LOG_STR("SHA256 verification failed"));
return false;
}
@@ -451,7 +456,7 @@ bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
err = esp_hosted_slave_ota_write(chunk, chunk_size); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
ota_end_guarded();
this->status_set_error(LOG_STR("Failed to write OTA data"));
return false;
}
@@ -494,7 +499,7 @@ void Esp32HostedUpdate::perform(bool force) {
}
// End OTA and activate new firmware
esp_err_t end_err = esp_hosted_slave_ota_end(); // NOLINT
esp_err_t end_err = ota_end_guarded();
if (end_err != ESP_OK) {
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(end_err));
this->state_ = prev_state;
-2
View File
@@ -37,7 +37,6 @@ void GreeClimate::transmit_state() {
remote_state[1] = this->temperature_();
if (model == GREE_YAN) {
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[4] = this->vertical_swing_();
}
@@ -61,7 +60,6 @@ void GreeClimate::transmit_state() {
}
if (model == GREE_YAA || model == GREE_YAC || model == GREE_YAC1FB9) {
remote_state[2] = 0x20; // bits 0..3 always 0000, bits 4..7 TURBO, LIGHT, HEALTH, X-FAN
remote_state[3] = 0x50; // bits 4..7 always 0101
remote_state[6] = 0x20; // YAA1FB, FAA1FB1, YB1F2 bits 4..7 always 0010
+7 -1
View File
@@ -76,6 +76,12 @@ static constexpr uint8_t GREE_PRESET_NONE = 0x00;
static constexpr uint8_t GREE_PRESET_SLEEP = 0x01;
static constexpr uint8_t GREE_PRESET_SLEEP_BIT = 0x80;
// Mode bits stored in remote_state[2] bits 4..7 for YAN/YAA/YAC/YAC1FB9.
static constexpr uint8_t GREE_MODE_BIT_TURBO = 0x10;
static constexpr uint8_t GREE_MODE_BIT_LIGHT = 0x20;
static constexpr uint8_t GREE_MODE_BIT_HEALTH = 0x40;
static constexpr uint8_t GREE_MODE_BIT_XFAN = 0x80;
// Model codes
enum Model : uint8_t { GREE_GENERIC, GREE_YAN, GREE_YAA, GREE_YAC, GREE_YAC1FB9, GREE_YX1FF, GREE_YAG };
@@ -103,7 +109,7 @@ class GreeClimate final : public climate_ir::ClimateIR {
uint8_t preset_();
Model model_{};
uint8_t mode_bits_{0}; // Combined mode bits for remote_state[2]
uint8_t mode_bits_{GREE_MODE_BIT_LIGHT}; // The display light is on until a switch says otherwise
};
} // namespace esphome::gree
+19 -6
View File
@@ -17,12 +17,22 @@ CONF_HEALTH = "health"
CONF_XFAN = "xfan"
CONF_GREE_ID = "gree_id"
# Switch configurations: (config_key, display_name, bit_mask, icon)
# Switch configurations: (config_key, display_name, mode_bit, icon)
SWITCH_CONFIGS = (
(CONF_TURBO, "Gree Turbo Switch", 0x10, "mdi:car-turbocharger"),
(CONF_LIGHT, "Gree Light Switch", 0x20, "mdi:led-outline"),
(CONF_HEALTH, "Gree Health Switch", 0x40, "mdi:pine-tree"),
(CONF_XFAN, "Gree X-FAN Switch", 0x80, "mdi:wall-sconce-flat"),
(
CONF_TURBO,
"Gree Turbo Switch",
gree_ns.GREE_MODE_BIT_TURBO,
"mdi:car-turbocharger",
),
(CONF_LIGHT, "Gree Light Switch", gree_ns.GREE_MODE_BIT_LIGHT, "mdi:led-outline"),
(CONF_HEALTH, "Gree Health Switch", gree_ns.GREE_MODE_BIT_HEALTH, "mdi:pine-tree"),
(
CONF_XFAN,
"Gree X-FAN Switch",
gree_ns.GREE_MODE_BIT_XFAN,
"mdi:wall-sconce-flat",
),
)
SUPPORTED_MODELS = {
@@ -39,7 +49,10 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(key): switch.switch_schema(
GreeModeBitSwitch,
icon=icon,
default_restore_mode="RESTORE_DEFAULT_OFF",
# The display light was on before the switches existed, so it stays on by default
default_restore_mode="RESTORE_DEFAULT_ON"
if key == CONF_LIGHT
else "RESTORE_DEFAULT_OFF",
device_class=DEVICE_CLASS_SWITCH,
entity_category=ENTITY_CATEGORY_CONFIG,
)
+6 -3
View File
@@ -187,15 +187,18 @@ void HeatpumpIRClimate::transmit_state() {
swing_h_cmd = HDIR_SWING;
}
// MitsubishiMSCHeatpumpIR only knows FAN_1..FAN_3 and sends anything else as auto,
// so low/medium/high have to start at FAN_1 there instead of FAN_2.
const bool three_speed_fan = this->protocol_ == PROTOCOL_MITSUBISHI_MSC;
switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) {
case climate::CLIMATE_FAN_LOW:
fan_speed_cmd = FAN_2;
fan_speed_cmd = three_speed_fan ? FAN_1 : FAN_2;
break;
case climate::CLIMATE_FAN_MEDIUM:
fan_speed_cmd = FAN_3;
fan_speed_cmd = three_speed_fan ? FAN_2 : FAN_3;
break;
case climate::CLIMATE_FAN_HIGH:
fan_speed_cmd = FAN_4;
fan_speed_cmd = three_speed_fan ? FAN_3 : FAN_4;
break;
case climate::CLIMATE_FAN_AUTO:
default:
@@ -37,7 +37,7 @@ async def to_code(config: ConfigType) -> None:
request_parent = await cg.get_variable(config[CONF_HTTP_REQUEST_ID])
cg.add(var.set_request_parent(request_parent))
cg.add(var.set_source_url(config[CONF_SOURCE]))
cg.add(var.set_source_url_static(config[CONF_SOURCE]))
ota.request_ota_state_listeners()
@@ -24,12 +24,6 @@ namespace esphome::http_request {
static const char *const TAG = "http_request.update";
// Wraps UpdateInfo + error for the task→main-loop handoff.
struct TaskResult {
update::UpdateInfo info;
const LogString *error_str{nullptr};
};
static const size_t MAX_READ_SIZE = 256;
static constexpr uint32_t INITIAL_CHECK_INTERVAL_ID = 0;
static constexpr uint32_t INITIAL_CHECK_INTERVAL_MS = 10000;
@@ -68,6 +62,37 @@ void HttpRequestUpdate::on_ota_state(ota::OTAState state, float progress, uint8_
}
}
void HttpRequestUpdate::set_source_url(const char *source_url, size_t len) {
// Copy before releasing, so a caller passing the current URL back in stays valid.
char *copy = new char[len + 1];
memcpy(copy, source_url, len);
copy[len] = '\0';
this->release_source_url_();
this->source_url_ = copy;
this->source_url_owned_ = true;
}
void HttpRequestUpdate::release_source_url_() {
if (this->source_url_owned_)
delete[] this->source_url_;
this->source_url_owned_ = false;
}
void HttpRequestUpdate::begin_check_(TaskResult *result) {
result->update = this;
result->source_url = this->source_url_;
result->owns_source_url = this->source_url_owned_;
this->source_url_owned_ = false;
}
void HttpRequestUpdate::end_check_(TaskResult *result) {
if (this->source_url_ == result->source_url) {
this->source_url_owned_ = result->owns_source_url;
} else if (result->owns_source_url) {
delete[] result->source_url; // replaced while the check ran
}
}
void HttpRequestUpdate::update() {
if (!network::is_connected()) {
ESP_LOGD(TAG, "Network not connected, skipping update check");
@@ -75,28 +100,37 @@ void HttpRequestUpdate::update() {
}
this->cancel_interval(INITIAL_CHECK_INTERVAL_ID);
#ifdef USE_ESP32
if (this->update_task_handle_ != nullptr) {
if (this->task_result_ != nullptr) {
ESP_LOGW(TAG, "Update check already in progress");
return;
}
xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, (void *) this, 1, &this->update_task_handle_);
#endif
// Allocated once per check; every path in the task returns via its single defer, which frees it.
auto *result = new TaskResult();
this->begin_check_(result);
#ifdef USE_ESP32
this->task_result_ = result;
if (xTaskCreate(HttpRequestUpdate::update_task, "update_task", 8192, result, 1, nullptr) != pdPASS) {
this->task_result_ = nullptr;
this->end_check_(result);
delete result;
}
#else
this->update_task(this);
this->update_task(result);
this->end_check_(result);
#endif
}
void HttpRequestUpdate::update_task(void *params) {
HttpRequestUpdate *this_update = (HttpRequestUpdate *) params;
// Allocate once — every path below returns via the single defer at the end.
// On failure, error_str is set; on success it is nullptr.
auto *result = new TaskResult();
auto *result = static_cast<TaskResult *>(params);
HttpRequestUpdate *this_update = result->update;
auto *info = &result->info;
const char *source_url = result->source_url;
auto container = this_update->request_parent_->get(this_update->source_url_);
auto container = this_update->request_parent_->get(source_url);
if (container == nullptr || container->status_code != HTTP_STATUS_OK) {
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this_update->source_url_);
ESP_LOGE(TAG, "Failed to fetch manifest from %s", source_url);
if (container != nullptr)
container->end();
result->error_str = LOG_STR("Failed to fetch manifest");
@@ -176,14 +210,14 @@ void HttpRequestUpdate::update_task(void *params) {
allocator.deallocate(data, content_length);
if (!valid) {
ESP_LOGE(TAG, "Failed to parse JSON from %s", this_update->source_url_);
ESP_LOGE(TAG, "Failed to parse JSON from %s", source_url);
result->error_str = LOG_STR("Failed to parse manifest JSON");
goto defer; // NOLINT(cppcoreguidelines-avoid-goto)
}
// Merge source_url_ and firmware_url
if (!info->firmware_url.empty() && info->firmware_url.find("http") == std::string::npos) {
const char *source = this_update->source_url_;
const char *source = source_url;
const size_t source_len = strlen(source);
size_t prefix_len;
if (info->firmware_url[0] == '/') {
@@ -215,7 +249,8 @@ defer:
// Lambda captures only 2 pointers (8 bytes) — fits in std::function SBO on supported toolchains.
this_update->defer([this_update, result]() {
#ifdef USE_ESP32
this_update->update_task_handle_ = nullptr;
this_update->task_result_ = nullptr;
this_update->end_check_(result);
#endif
if (result->error_str != nullptr) {
this_update->status_set_error(result->error_str);
@@ -13,6 +13,21 @@
namespace esphome::http_request {
namespace testing {
class HttpRequestUpdateTest;
} // namespace testing
class HttpRequestUpdate;
// One manifest check: handed from update() to the task and back to the main loop.
struct TaskResult {
HttpRequestUpdate *update{nullptr};
const char *source_url{nullptr}; // the URL this check reads, fixed before it starts
update::UpdateInfo info;
const LogString *error_str{nullptr}; // set on failure, nullptr on success
bool owns_source_url{false}; // the check holds the buffer until it ends, see begin_check_()
};
class HttpRequestUpdate final : public update::UpdateEntity, public PollingComponent, public ota::OTAStateListener {
public:
void setup() override;
@@ -21,7 +36,18 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo
void perform(bool force) override;
void check() override { this->update(); }
void set_source_url(const char *source_url) { this->source_url_ = source_url; }
~HttpRequestUpdate() override { this->release_source_url_(); }
/// Codegen only: the config literal lives in flash for the life of the program, so it is kept by pointer.
void set_source_url_static(const char *source_url) {
this->release_source_url_();
this->source_url_ = source_url;
}
/// Set the manifest URL at run time; the string is copied, so any storage may be passed.
void set_source_url(const char *source_url) { this->set_source_url(source_url, strlen(source_url)); }
void set_source_url(const std::string &source_url) { this->set_source_url(source_url.data(), source_url.size()); }
void set_source_url(const char *source_url, size_t len);
const char *get_source_url() const { return this->source_url_; }
void set_request_parent(HttpRequestComponent *request_parent) { this->request_parent_ = request_parent; }
void set_ota_parent(OtaHttpRequestComponent *ota_parent) { this->ota_parent_ = ota_parent; }
@@ -36,12 +62,21 @@ class HttpRequestUpdate final : public update::UpdateEntity, public PollingCompo
static void update_task(void *params);
#ifdef USE_ESP32
TaskHandle_t update_task_handle_{nullptr};
TaskResult *task_result_{nullptr}; // set while a check runs on the update task
#endif
uint8_t initial_check_remaining_{0};
private:
const char *source_url_{nullptr}; // literal from codegen
friend class testing::HttpRequestUpdateTest;
void release_source_url_();
// A check reads the URL it started with until it ends, on the update task or inline through an
// automation it triggers. Ownership of that buffer moves to the check, so a set_source_url in the
// meantime cannot free it; end_check_() hands it back or frees it on the main loop.
void begin_check_(TaskResult *result);
void end_check_(TaskResult *result);
bool source_url_owned_{false}; // source_url_ was copied by a run-time setter; sits in the padding above
const char *source_url_{nullptr};
};
} // namespace esphome::http_request
@@ -18,6 +18,8 @@ namespace esphome::improv_serial {
static const char *const TAG = "improv_serial";
static constexpr uint32_t WIFI_CONNECT_TIMEOUT_ID = 0;
void ImprovSerialComponent::setup() {
global_improv_serial_component = this;
#ifdef USE_IMPROV_SERIAL_UART
@@ -73,7 +75,7 @@ void ImprovSerialComponent::loop() {
wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
this->connecting_sta_.get_password());
this->connecting_sta_ = {};
this->cancel_timeout("wifi-connect-timeout");
this->cancel_timeout(WIFI_CONNECT_TIMEOUT_ID);
this->set_state_(improv::STATE_PROVISIONED);
this->send_settings_response_(improv::WIFI_SETTINGS);
@@ -302,7 +304,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
command.password.c_str());
this->set_timeout("wifi-connect-timeout", switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS,
this->set_timeout(WIFI_CONNECT_TIMEOUT_ID, switching ? WIFI_SWITCH_TIMEOUT_MS : WIFI_CONNECT_TIMEOUT_MS,
[this]() { this->on_wifi_connect_timeout_(); });
#else
// No Wi-Fi support compiled in; there is nothing to provision.
+3 -1
View File
@@ -169,7 +169,9 @@ void LEDCOutput::setup() {
#if !defined(CLANG_TIDY)
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 1, 0)
PERIPH_RCC_ATOMIC() { ledc_ll_reset_register(0); }
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#elif ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \
(ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0) && !defined(__PERIPH_CTRL_ALLOW_LEGACY_API))
// Also on the targets whose periph_module_reset() is an empty stub in ESP-IDF 5.x (P4, C5, C61, H4)
PERIPH_RCC_ATOMIC() {
ledc_ll_enable_reset_reg(true);
ledc_ll_enable_reset_reg(false);
+3
View File
@@ -491,6 +491,9 @@ async def component_to_code(config):
# Not enabled on RTL87xx/LN882x — costs more heap than it saves there.
cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1")
# Static destructors never run, so skip registering them. See atexit_stubs.cpp.
cg.add_build_flag("-Wl,--wrap=__cxa_atexit")
# LN882x: a zero-size allocation must not trip the SDK's assert (see ln882x_zero_malloc.c).
if config[CONF_COMPONENT_ID] == COMPONENT_LN882X:
cg.add_build_flag("-Wl,--wrap=pvPortMalloc")
@@ -0,0 +1,20 @@
/*
* Linker wrap stub for __cxa_atexit().
*
* GCC registers the destructor of every global and function local static
* object with __cxa_atexit(). The firmware never returns from main or calls
* exit(), so these destructors never run, but the first registration links
* newlib's exit handler tables.
*/
#include "esphome/core/defines.h"
#ifdef USE_LIBRETINY
namespace esphome::libretiny {}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" int __wrap___cxa_atexit(void (*)(void *), void *, void *) { return 0; }
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
#endif // USE_LIBRETINY
+3
View File
@@ -1,6 +1,7 @@
#pragma once
#include "light_color_values.h"
#include "esphome/core/string_ref.h"
namespace esphome {
@@ -133,6 +134,8 @@ class LightCall {
LightCall &set_effect(const std::string &effect) { return this->set_effect(effect.data(), effect.size()); }
/// Set the effect of the light by its name (const char * overload to resolve ambiguity).
LightCall &set_effect(const char *effect) { return this->set_effect(effect, strlen(effect)); }
/// Set the effect of the light by its name (StringRef overload to resolve ambiguity, zero-copy).
LightCall &set_effect(StringRef effect) { return this->set_effect(effect.c_str(), effect.size()); }
/// Set the effect of the light by its name and length (zero-copy from API).
LightCall &set_effect(const char *effect, size_t len);
/// Set the effect of the light by its internal index number (only for internal use).
@@ -4,6 +4,8 @@
namespace esphome::mcp23xxx_base {
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg) { arg->enable_loop_soon_any_context(); }
template<uint8_t N> void MCP23XXXGPIOPin<N>::setup() {
this->pin_mode(flags_);
// When interrupt_pin is configured, pin_mode() already auto-enables CHANGE
@@ -8,6 +8,10 @@ namespace esphome::mcp23xxx_base {
enum MCP23XXXInterruptMode : uint8_t { MCP23XXX_NO_INTERRUPT = 0, MCP23XXX_CHANGE, MCP23XXX_RISING, MCP23XXX_FALLING };
// Not a template member: GCC 10 (ESP8266) ignores section attributes on template instantiations, leaving the
// ISR in flash.
void IRAM_ATTR mcp23xxx_gpio_intr(Component *arg);
template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander::CachedGpioExpander<uint8_t, N> {
public:
virtual void pin_mode(uint8_t pin, gpio::Flags flags);
@@ -35,7 +39,8 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
void setup_interrupt_pin_() {
if (this->interrupt_pin_ != nullptr) {
this->interrupt_pin_->setup();
this->interrupt_pin_->attach_interrupt(&MCP23XXXBase::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
this->interrupt_pin_->attach_interrupt(&mcp23xxx_gpio_intr, static_cast<Component *>(this),
gpio::INTERRUPT_FALLING_EDGE);
this->set_invalidate_on_read_(false);
}
// Disable loop until an input pin is configured via pin_mode()
@@ -43,7 +48,6 @@ template<uint8_t N> class MCP23XXXBase : public Component, public gpio_expander:
// For polling mode, loop is re-enabled when pin_mode() registers an input pin
this->disable_loop();
}
static void IRAM_ATTR gpio_intr(MCP23XXXBase *arg) { arg->enable_loop_soon_any_context(); }
// read a given register
virtual bool read_reg(uint8_t reg, uint8_t *value) = 0;
+52 -1
View File
@@ -20,6 +20,10 @@
#ifdef USE_DASHBOARD_IMPORT
#include "esphome/components/dashboard_import/dashboard_import.h"
#endif
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
#include "esphome/components/ethernet/ethernet_component.h"
#include "esphome/components/wifi/wifi_component.h"
#endif
namespace esphome::mdns {
@@ -39,6 +43,45 @@ MDNS_STATIC_CONST_CHAR(SERVICE_TCP, "_tcp");
// Wrap build-time defines into flash storage
MDNS_STATIC_CONST_CHAR(VALUE_VERSION, ESPHOME_VERSION);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
MDNS_STATIC_CONST_CHAR(NETWORK_ETHERNET, "ethernet");
// Same order as the default route in NetworkComponent::loop()
const char *MDNSComponent::connected_network_() const {
const bool ethernet = ethernet::global_eth_component->is_connected();
const bool wifi = wifi::global_wifi_component->is_connected();
#ifdef USE_NETWORK_PRIMARY_INTERFACE_WIFI
if (wifi)
return NETWORK_WIFI;
#endif
if (ethernet)
return NETWORK_ETHERNET;
if (wifi)
return NETWORK_WIFI;
return nullptr;
}
void MDNSComponent::update_network_txt_() {
if (this->network_txt_ == nullptr)
return;
// With no interface connected, keep the last value instead of flapping to wifi
const char *network = this->connected_network_();
if (network == nullptr || network == this->network_txt_)
return;
if (!this->publish_network_txt_(network)) {
// The _esphomelib service is missing, so a retry would fail too
this->network_txt_ = nullptr;
return;
}
ESP_LOGD(TAG, "Network TXT: %s", network);
this->network_txt_ = network;
#ifdef USE_MDNS_STORE_SERVICES
this->network_record_->value = MDNS_STR(network);
#endif
}
#endif
void MDNSComponent::setup_buffers_and_register_(PlatformRegisterFn platform_register) {
#ifdef USE_MDNS_STORE_SERVICES
auto &services = this->services_;
@@ -160,7 +203,15 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
txt_records.push_back({MDNS_STR(TXT_BOARD), MDNS_STR(VALUE_BOARD)});
#if defined(USE_WIFI)
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
const char *network = this->connected_network_();
this->network_txt_ = network != nullptr ? network : NETWORK_WIFI;
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(this->network_txt_)});
#ifdef USE_MDNS_STORE_SERVICES
// services is services_ here, which never reallocates
this->network_record_ = &txt_records.back();
#endif
#elif defined(USE_WIFI)
MDNS_STATIC_CONST_CHAR(NETWORK_WIFI, "wifi");
txt_records.push_back({MDNS_STR(TXT_NETWORK), MDNS_STR(NETWORK_WIFI)});
#elif defined(USE_ETHERNET)
+22
View File
@@ -32,6 +32,12 @@
#define USE_MDNS_DEVICE_INFO_TXT
#endif
// With both wifi and ethernet, the _esphomelib "network" TXT value follows the connected interface.
// OpenThread hands its services to the SRP client once, so it keeps the fixed value.
#if defined(USE_API) && defined(USE_WIFI) && defined(USE_ETHERNET) && !defined(USE_OPENTHREAD)
#define USE_MDNS_DYNAMIC_NETWORK_TXT
#endif
namespace esphome::mdns {
// Helper struct that identifies strings that may be stored in flash storage (similar to LogString)
@@ -150,6 +156,22 @@ class MDNSComponent final : public Component
void setup_buffers_and_register_(PlatformRegisterFn platform_register);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
static constexpr uint32_t NETWORK_TXT_CHECK_INTERVAL_MS = 1000;
/// The first connected interface in network: priority order ("ethernet" or "wifi"), else nullptr.
const char *connected_network_() const;
/// Check the connected interface and publish a changed "network" TXT value.
void update_network_txt_();
/// Platform-specific: replace the "network" TXT value of the registered _esphomelib service.
bool publish_network_txt_(const char *network);
/// Advertised value; nullptr until the _esphomelib service is registered, and after a failed publish.
const char *network_txt_{nullptr};
#ifdef USE_MDNS_STORE_SERVICES
/// The "network" record in services_, kept in step with network_txt_.
MDNSTXTRecord *network_record_{nullptr};
#endif
#endif
#ifdef USE_MDNS_DYNAMIC_TXT
/// Storage for runtime-generated TXT values from user lambdas
/// Pre-sized at compile time via MDNS_DYNAMIC_TXT_COUNT to avoid heap allocations.
+18 -1
View File
@@ -89,7 +89,24 @@ bool MDNSComponent::set_service_enabled(const char *service_type, const char *pr
}
#endif // USE_MDNS_SUPPORTS_ENABLE_DISABLE && !USE_OPENTHREAD
void MDNSComponent::setup() { this->setup_buffers_and_register_(register_esp32); }
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
bool MDNSComponent::publish_network_txt_(const char *network) {
// ESP-IDF copies the value and announces the changed record
esp_err_t err = mdns_service_txt_item_set("_esphomelib", "_tcp", "network", network);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Failed to update network TXT: %s", esp_err_to_name(err));
return false;
}
return true;
}
#endif
void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_esp32);
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS, [this]() { this->update_network_txt_(); });
#endif
}
void MDNSComponent::on_shutdown() {
#ifndef USE_OPENTHREAD
+19
View File
@@ -46,7 +46,26 @@ void MDNSComponent::start_polling_window_() {
}
#endif
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
ESPHOME_LOG_TAG(TAG, "mdns");
bool MDNSComponent::publish_network_txt_(const char *network) {
// LEAmDNS replaces the value of an existing key; announce() sends the change
if (!MDNS.addServiceTxt("esphomelib", "tcp", "network", network)) {
ESP_LOGW(TAG, "Failed to update network TXT");
return false;
}
MDNS.announce();
this->start_polling_window_();
return true;
}
#endif
void MDNSComponent::setup() {
#ifdef USE_MDNS_DYNAMIC_NETWORK_TXT
// No-op until the first IP-up event registers the services
this->set_interval(NETWORK_TXT_CHECK_INTERVAL_MS, [this]() { this->update_network_txt_(); });
#endif
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
// own IP state listener. Both WiFi and Ethernet have the same listener signature —
@@ -9,6 +9,10 @@ class RgbDriverChip(DriverChip):
def transforms(self) -> set[str]:
"""
Return the set of transformations supported by this driver chip.
RGB displays do not support axis swapping, so this method removes CONF_SWAP_XY
RGB displays do not support axis swapping, so this method removes CONF_SWAP_XY.
A model with an empty init sequence has no command interface at all, so it
cannot apply any transform in hardware.
"""
if self.initsequence is not None and len(self.initsequence) == 0:
return set()
return super().transforms - {CONF_SWAP_XY}
+1 -57
View File
@@ -16,6 +16,7 @@
#include "lwip/dns.h"
#include "lwip/err.h"
#include "mqtt_component.h"
#include "mqtt_topic_match.h"
#ifdef USE_API
#include "esphome/components/api/api_server.h"
@@ -581,63 +582,6 @@ void MQTTClientComponent::disable() {
this->on_shutdown();
}
/** Check if the message topic matches the given subscription topic
*
* INFO: MQTT spec mandates that topics must not be empty and must be valid NULL-terminated UTF-8 strings.
*
* @param message The message topic that was received from the MQTT server. Note: this must not contain
* wildcard characters as mandated by the MQTT spec.
* @param subscription The subscription topic we are matching against.
* @param is_normal Is this a "normal" topic - Does the message topic not begin with a "$".
* @param past_separator Are we past the first '/' topic separator.
* @return true if the subscription topic matches the message topic, false otherwise.
*/
static bool topic_match(const char *message, const char *subscription, bool is_normal, bool past_separator) {
// Reached end of both strings at the same time, this means we have a successful match
if (*message == '\0' && *subscription == '\0')
return true;
// Either the message or the subscribe are at the end. This means they don't match.
if (*message == '\0' || *subscription == '\0')
return false;
bool do_wildcards = is_normal || past_separator;
if (*subscription == '+' && do_wildcards) {
// single level wildcard
// consume + from subscription
subscription++;
// consume everything from message until '/' found or end of string
while (*message != '\0' && *message != '/') {
message++;
}
// after this, both pointers will point to a '/' or to the end of the string
return topic_match(message, subscription, is_normal, true);
}
if (*subscription == '#' && do_wildcards) {
// multilevel wildcard - MQTT mandates that this must be at end of subscribe topic
return true;
}
// this handles '/' and normal characters at the same time.
if (*message != *subscription)
return false;
past_separator = past_separator || *subscription == '/';
// consume characters
subscription++;
message++;
return topic_match(message, subscription, is_normal, past_separator);
}
static bool topic_match(const char *message, const char *subscription) {
return topic_match(message, subscription, *message != '\0' && *message != '$', false);
}
void MQTTClientComponent::on_message(const std::string &topic, const std::string &payload) {
#ifdef USE_ESP8266
// IMPORTANT: This defer is REQUIRED to prevent stack overflow crashes on ESP8266.
+2
View File
@@ -273,6 +273,8 @@ class MQTTComponent : public Component {
void subscribe_json(const std::string &topic, const mqtt_json_callback_t &callback, uint8_t qos = 0);
protected:
static constexpr uint32_t SEND_DEFER_ID = 0;
/// Helper method to get the discovery topic for this component into a buffer.
StringRef get_discovery_topic_to_(std::span<char, MQTT_DISCOVERY_TOPIC_MAX_LEN> buf,
const MQTTDiscoveryInfo &discovery_info) const;
+2 -3
View File
@@ -121,7 +121,7 @@ void MQTTFanComponent::setup() {
});
}
this->state_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); });
this->state_->add_on_state_callback([this]() { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
}
void MQTTFanComponent::dump_config() {
@@ -169,8 +169,7 @@ bool MQTTFanComponent::publish_state() {
char topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
const char *state_s = this->state_->state ? "ON" : "OFF";
ESP_LOGD(TAG, "'%s' Sending state %s.", this->state_->get_name().c_str(), state_s);
this->publish(this->get_state_topic_to_(topic_buf), state_s);
bool failed = false;
bool failed = !this->publish(this->get_state_topic_to_(topic_buf), state_s);
if (this->state_->get_traits().supports_direction()) {
bool success = this->publish(this->get_direction_state_topic_to(topic_buf),
fan_direction_to_mqtt_str(this->state_->direction));
+1 -1
View File
@@ -28,7 +28,7 @@ void MQTTJSONLightComponent::setup() {
}
void MQTTJSONLightComponent::on_light_remote_values_update() {
this->defer("send", [this]() { this->publish_state_(); });
this->defer(SEND_DEFER_ID, [this]() { this->publish_state_(); });
}
MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {}
+1 -1
View File
@@ -29,7 +29,7 @@ void MQTTLockComponent::setup() {
}
});
this->lock_->add_on_state_callback(
[this](LockState /*state*/) { this->defer("send", [this]() { this->publish_state(); }); });
[this](LockState /*state*/) { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
}
void MQTTLockComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Lock '%s': ", this->lock_->get_name().c_str());
+1 -1
View File
@@ -34,7 +34,7 @@ void MQTTSwitchComponent::setup() {
}
});
this->switch_->add_on_state_callback(
[this](bool enabled) { this->defer("send", [this, enabled]() { this->publish_state(enabled); }); });
[this](bool enabled) { this->defer(SEND_DEFER_ID, [this, enabled]() { this->publish_state(enabled); }); });
}
void MQTTSwitchComponent::dump_config() {
ESP_LOGCONFIG(TAG, "MQTT Switch '%s': ", this->switch_->get_name().c_str());
@@ -0,0 +1,28 @@
#pragma once
namespace esphome::mqtt {
/// True if the message topic matches the subscription, with + and # wildcards.
/// A loop on purpose: recursing per character overflowed the ESP32 loop task's stack on long topics.
inline bool topic_match(const char *message, const char *subscription) {
// First-level wildcards do not match topics that begin with '$'
bool wildcards = *message != '$';
while (*message != '\0' && *subscription != '\0') {
if (wildcards && *subscription == '#')
return true;
if (wildcards && *subscription == '+') {
subscription++;
while (*message != '\0' && *message != '/')
message++;
continue;
}
if (*message != *subscription)
return false;
wildcards |= *subscription == '/';
message++;
subscription++;
}
return *message == *subscription; // a match only if both ended
}
} // namespace esphome::mqtt
+1 -1
View File
@@ -24,7 +24,7 @@ void MQTTUpdateComponent::setup() {
}
});
this->update_->add_on_state_callback([this]() { this->defer("send", [this]() { this->publish_state(); }); });
this->update_->add_on_state_callback([this]() { this->defer(SEND_DEFER_ID, [this]() { this->publish_state(); }); });
}
bool MQTTUpdateComponent::publish_state() {
@@ -37,6 +37,7 @@ int NoiseResponderHandshake::init(const NoiseContext &ctx, const uint8_t *prolog
.cipher_id = NOISE_CIPHER_CHACHAPOLY,
.hash_id = NOISE_HASH_SHA256,
.hybrid_id = NOISE_DH_NONE,
.reserved = {},
};
int err = noise_handshakestate_new_by_id(&this->handshake_, &nid, NOISE_ROLE_RESPONDER);
+46 -8
View File
@@ -12,6 +12,7 @@ from esphome.build_helpers import pch
from esphome.build_helpers.ccache import resolve_ccache_path
import esphome.codegen as cg
from esphome.components.zephyr import (
Section,
add_extra_script,
copy_files as zephyr_copy_files,
zephyr_add_overlay,
@@ -125,11 +126,37 @@ def set_core_data(config: ConfigType) -> ConfigType:
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG:
zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]])
sections = BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]]
if CORE.testing_mode:
sections = _testing_mode_sections(sections)
zephyr_add_pm_static(sections)
return config
# In testing mode, fake a larger flash to allow linking grouped component
# tests. The nRF52840 has 1 MB and a mcumgr OTA config halves the app slot,
# which an openthread config alone fills; CI images are never flashed.
NRF52840_FLASH_SIZE = 0x100000
TESTING_FLASH_SIZE = 0x400000
def _testing_mode_sections(sections: list[Section]) -> list[Section]:
"""Move a bootloader pinned to the end of the real flash to the end of the
faked one, so the partition manager still sees a single gap for the app."""
return [
Section(
section.name,
section.address + TESTING_FLASH_SIZE - NRF52840_FLASH_SIZE,
section.size,
section.region,
)
if section.end_address == NRF52840_FLASH_SIZE
else section
for section in sections
]
_TOOLCHAINS = (Toolchain.PLATFORMIO, Toolchain.SDK_NRF)
_resolve_toolchain = cv.resolve_toolchain("nRF52", _TOOLCHAINS, Toolchain.SDK_NRF)
@@ -380,6 +407,15 @@ async def to_code(config: ConfigType) -> None:
zephyr_setup_preferences()
zephyr_to_code(config)
if CORE.testing_mode:
zephyr_add_overlay(
f"""
&flash0 {{
reg = <0x0 {TESTING_FLASH_SIZE:#x}>;
}};
"""
)
if dfu_config := config.get(CONF_DFU):
CORE.add_job(_dfu_to_code, dfu_config)
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
@@ -695,13 +731,13 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
return True # Handled: PYOCD upload
# Deferred imports: bleak/smpclient are heavy, only load for BLE/mcumgr paths
from .ble_logger import is_mac_address
from .ble_logger import is_ble_address
from .ota import smpmgr_scan, smpmgr_upload
if host == "BLE":
mcumgr_device = asyncio.run(smpmgr_scan(CORE.name))
if is_mac_address(host):
if is_ble_address(host):
mcumgr_device = host
if mcumgr_device:
@@ -716,7 +752,7 @@ def upload_program(config: ConfigType, args, host: str) -> bool:
def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
address = devices[0]
from .ble_logger import is_mac_address, logger_connect, logger_scan
from .ble_logger import is_ble_address, logger_connect, logger_scan
if devices[0] == "BLE":
ble_device = asyncio.run(logger_scan(CORE.name))
@@ -725,7 +761,7 @@ def show_logs(config: ConfigType, args, devices: list[str]) -> bool:
else:
return True
if is_mac_address(address):
if is_ble_address(address):
asyncio.run(logger_connect(address))
return True
return False
@@ -798,7 +834,7 @@ _PCH_CMAKE_LINES = [
"set(esphome_kept_options)",
"set(esphome_pch_headers)",
"foreach(option IN LISTS esphome_options)",
' if(option MATCHES "imacros> (.+)$")',
' if(option MATCHES "imacros> ([^>]+)")',
' list(APPEND esphome_pch_headers "${CMAKE_MATCH_1}")',
" list(APPEND esphome_kept_options",
' "$<$<NOT:$<AND:$<COMPILE_LANGUAGE:CXX>,$<STREQUAL:$<TARGET_PROPERTY:NAME>,app>>>:${option}>")',
@@ -1003,11 +1039,13 @@ def run_compile(args, config: ConfigType) -> bool:
framework_ver = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
bootloader = zephyr_data()[KEY_BOOTLOADER]
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes
# (dev_type, sd_req) per bootloader — values from Nordic SoftDevice release notes.
# The bootloader resets on a package whose sd_req does not list its SoftDevice,
# so v7 accepts S140 7.0.1, 7.2.0 and 7.3.0 (e.g. Seeed XIAO nRF52840).
_GENPKG_PARAMS = {
BOOTLOADER_ADAFRUIT_NRF52_SD132: ("0x0051", "0x009D"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V6: ("0x0052", "0x00B6"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA"),
BOOTLOADER_ADAFRUIT_NRF52_SD140_V7: ("0x0052", "0x00CA,0x0100,0x0123"),
}
# UF2 family IDs — nRF52832 vs nRF52840 per SoftDevice variant
_UF2_FAMILY_IDS = {
+20 -3
View File
@@ -4,6 +4,7 @@ import re
from typing import Final
from bleak import BleakClient, BleakScanner, BLEDevice
from bleak.backends.scanner import AdvertisementData
from bleak.exc import (
BleakCharacteristicNotFoundError,
BleakDBusError,
@@ -19,15 +20,31 @@ NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
MAC_ADDRESS_PATTERN: Final = re.compile(
r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE
)
# macOS identifies BLE peripherals by a CoreBluetooth UUID instead of a MAC address
UUID_PATTERN: Final = re.compile(
r"[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$",
flags=re.IGNORECASE,
)
def is_mac_address(value: str) -> bool:
return MAC_ADDRESS_PATTERN.match(value)
def is_ble_address(value: str) -> bool:
return bool(MAC_ADDRESS_PATTERN.match(value) or UUID_PATTERN.match(value))
def _name_matches(device: BLEDevice, adv: AdvertisementData, name: str) -> bool:
# device.name can be a stale cached name on macOS; adv.local_name is what is on the air now
return name in (device.name, adv.local_name)
async def find_device_by_name(name: str, timeout: float = 10.0) -> BLEDevice | None:
return await BleakScanner.find_device_by_filter(
lambda device, adv: _name_matches(device, adv, name), timeout=timeout
)
async def logger_scan(name: str) -> BLEDevice | None:
_LOGGER.info("Scanning bluetooth for %s...", name)
device = await BleakScanner.find_device_by_name(name)
device = await find_device_by_name(name)
if not device:
_LOGGER.error("%s Bluetooth LE device was not found!", name)
return device
+10 -11
View File
@@ -4,7 +4,6 @@ import json
import logging
from pathlib import Path
from bleak import BleakScanner
from bleak.exc import BleakDBusError, BleakDeviceNotFoundError
from smp.exceptions import SMPBadStartDelimiter
from smpclient import SMPClient
@@ -23,9 +22,8 @@ from smpclient.transport.serial import SMPSerialTransport
from esphome.core import EsphomeError
from esphome.espota2 import ProgressBar
from .ble_logger import is_mac_address
from .ble_logger import find_device_by_name, is_ble_address
SMP_SERVICE_UUID = "8D53DC1D-1DB7-4CD3-868B-8A527460AA84"
BLE_SCAN_TIMEOUT = 10.0 # seconds
RESET_DELAY = 2.0 # seconds to wait before reset, allows on_end action to execute
@@ -45,12 +43,12 @@ def _json_state(o: object) -> object:
async def smpmgr_scan(name: str) -> str:
_LOGGER.info("Scanning bluetooth for %s...", name)
for device in await BleakScanner.discover(
timeout=BLE_SCAN_TIMEOUT, service_uuids=[SMP_SERVICE_UUID]
):
if device.name == name:
return device.address
raise EsphomeError(f"BLE device {name} with OTA service not found")
# No service filter: the SMP UUID only fits in the scan response, which macOS does not filter on.
# The SMP service is checked on connect.
device = await find_device_by_name(name, timeout=BLE_SCAN_TIMEOUT)
if device is None:
raise EsphomeError(f"BLE device {name} not found")
return device.address
async def smpmgr_upload(device: str, firmware: Path) -> None:
@@ -88,7 +86,7 @@ def _get_image_tlv_sha256(file: Path) -> bytes:
async def _smpmgr_upload(device: str, firmware: Path) -> None:
image_tlv_sha256 = _get_image_tlv_sha256(firmware)
if is_mac_address(device):
if is_ble_address(device):
smp_client = SMPClient(SMPBLETransport(), device)
else:
smp_client = SMPClient(SMPSerialTransport(), device)
@@ -105,7 +103,8 @@ async def _smpmgr_upload(device: str, firmware: Path) -> None:
) from exc
raise EsphomeError(f"BLE error connecting to {device}: {exc}") from exc
except SMPBLETransportException as exc:
raise EsphomeError(f"Connection error with {device}") from exc
# Reached when a device with the right name has no SMP service, among other causes
raise EsphomeError(f"Connection error with {device}: {exc}") from exc
_LOGGER.info("Connected %s...", device)
try:
+3 -2
View File
@@ -98,8 +98,9 @@ enum OTAType : uint8_t {
// The OTA backend method surface. Exactly one backend exists per build,
// selected in ota_backend_factory.h where this concept is asserted on
// make_ota_backend()'s return type. Semantics beyond the signatures:
// - begin: prepare for an image of the given size; ota_type defaults to an
// app update, so both call forms must be accepted.
// - begin: prepare for an image of the given size; 0 means the size is unknown
// (web_server upload) and the backend allows the largest image it can take.
// ota_type defaults to an app update, so both call forms must be accepted.
// - set_update_md5: expected digest of the incoming image, hex string.
// - write: consume the next chunk; end: finalize and mark bootable.
// - abort: safe to call in any state, including after end().
@@ -9,6 +9,12 @@
#include <Updater.h>
// LittleFS region bounds, provided by the arduino-pico linker script.
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
extern "C" uint8_t _FS_start;
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp)
extern "C" uint8_t _FS_end;
namespace esphome::ota {
static const char *const TAG = "ota";
@@ -19,8 +25,11 @@ OTAResponseTypes ArduinoRP2OTABackend::begin(size_t image_size, OTAType ota_type
if (ota_type != OTA_TYPE_UPDATE_APP) {
return OTA_RESPONSE_ERROR_UNSUPPORTED_OTA_TYPE;
}
// OTA size of 0 is not currently handled, but
// web_server is not supported for RP2040, so this is not an issue.
// 0 means the size is unknown (web_server upload). The updater only checks the size
// against the LittleFS region it stages the image in, so ask for all of it.
if (image_size == 0) {
image_size = reinterpret_cast<uintptr_t>(&_FS_end) - reinterpret_cast<uintptr_t>(&_FS_start);
}
bool ret = Update.begin(image_size, U_FLASH);
if (ret) {
rp2::preferences_prevent_write(true);
@@ -34,7 +43,7 @@ OTAResponseTypes ArduinoRP2OTABackend::begin(size_t image_size, OTAType ota_type
return OTA_RESPONSE_ERROR_WRONG_NEW_FLASH_CONFIG;
if (error == UPDATE_ERROR_FLASH_CONFIG)
return OTA_RESPONSE_ERROR_WRONG_CURRENT_FLASH_CONFIG;
if (error == UPDATE_ERROR_SPACE)
if (error == UPDATE_ERROR_SPACE || error == UPDATE_ERROR_SIZE)
return OTA_RESPONSE_ERROR_RP2040_NOT_ENOUGH_SPACE;
ESP_LOGE(TAG, "Begin error: %d", error);
+2
View File
@@ -194,6 +194,8 @@ def _parse_platform_version(value: Any) -> str:
# * The new version needs to be thoroughly validated before changing the
# recommended version as otherwise a bunch of devices could be bricked
# * For all constants below, update platformio.ini (in this repo)
# * rp2_pio pins its own pioasm build (PIOASM_REPO_VERSION in rp2_pio/__init__.py); check
# https://github.com/earlephilhower/pico-quick-toolchain/releases for a matching release
# The default/recommended arduino framework version
# - https://github.com/earlephilhower/arduino-pico/releases
+4 -4
View File
@@ -7,9 +7,9 @@ from esphome.types import ConfigType
DEPENDENCIES = ["rp2"]
PIOASM_REPO_VERSION = "1.5.0-b"
PIOASM_REPO_VERSION = "4.1.0"
PIOASM_REPO_BASE = f"https://github.com/earlephilhower/pico-quick-toolchain/releases/download/{PIOASM_REPO_VERSION}"
PIOASM_VERSION = "pioasm-2e6142b.230216"
PIOASM_VERSION = "pioasm-9fdfe11.250530"
PIOASM_DOWNLOADS = {
"linux": {
"aarch64": f"aarch64-linux-gnu.{PIOASM_VERSION}.tar.gz",
@@ -20,8 +20,8 @@ PIOASM_DOWNLOADS = {
"amd64": f"x86_64-w64-mingw32.{PIOASM_VERSION}.zip",
},
"darwin": {
"x86_64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz",
"arm64": f"x86_64-apple-darwin14.{PIOASM_VERSION}.tar.gz",
"x86_64": f"x86_64-apple-darwin20.4.{PIOASM_VERSION}.tar.gz",
"arm64": f"aarch64-apple-darwin20.4.{PIOASM_VERSION}.tar.gz",
},
}
+8 -8
View File
@@ -295,8 +295,11 @@ template<class C, typename... Ts> class ScriptWaitAction final : public Action<T
}
void loop() override {
if (this->num_running_ == 0)
if (this->num_running_ == 0) {
// Nothing left to wait for, e.g. once the last queued item has run
this->disable_loop();
return;
}
if (this->script_->is_running())
return;
@@ -305,9 +308,10 @@ template<class C, typename... Ts> class ScriptWaitAction final : public Action<T
// Processing all items in a while loop causes infinite loops because
// play_next_() can trigger more items to be queued
if (!this->param_queue_.empty()) {
auto &params = this->param_queue_.front();
this->play_next_tuple_(params, std::make_index_sequence<sizeof...(Ts)>{});
// Take the item out first: the chain may call stop(), which clears the queue
auto params = std::move(this->param_queue_.front());
this->param_queue_.pop_front();
std::apply([this](auto &...args) { this->play_next_(args...); }, params);
} else {
// Queue is now empty - disable loop until next play_complex
this->disable_loop();
@@ -323,12 +327,8 @@ template<class C, typename... Ts> class ScriptWaitAction final : public Action<T
}
protected:
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
this->play_next_(std::get<S>(tuple)...);
}
C *script_;
std::list<std::tuple<Ts...>> param_queue_;
std::list<std::tuple<stored_trigger_arg_t<Ts>...>> param_queue_;
};
} // namespace esphome::script
@@ -9,6 +9,8 @@ namespace esphome::seeed_mr24hpc1 {
static const char *const TAG = "seeed_mr24hpc1";
static constexpr uint32_t STARTUP_DELAY_MS = 1000;
// Prints the component's configuration data. dump_config() prints all of the component's configuration
// items in an easy-to-read format, including the configuration key-value pairs.
void MR24HPC1Component::dump_config() {
@@ -76,8 +78,7 @@ void MR24HPC1Component::setup() {
this->custom_mode_end_text_sensor_->publish_state("Not in custom mode");
}
#endif
this->set_custom_end_mode();
this->poll_time_base_func_check_ = true;
this->poll_time_base_func_check_ = false;
this->check_dev_inf_sign_ = true;
this->sg_start_query_data_ = STANDARD_FUNCTION_QUERY_PRODUCT_MODE;
this->sg_data_len_ = 0;
@@ -92,7 +93,13 @@ void MR24HPC1Component::setup() {
memset(this->sg_frame_prase_buf_, 0, FRAME_BUF_MAX_SIZE);
memset(this->sg_frame_buf_, 0, FRAME_BUF_MAX_SIZE);
this->set_interval(8000, [this]() { this->update_(); });
// The module locks up when queried during its own boot, so hold all TX for a second
this->set_timeout(STARTUP_DELAY_MS, [this]() {
this->module_ready_ = true;
this->set_custom_end_mode();
this->poll_time_base_func_check_ = true;
this->set_interval(8000, [this]() { this->update_(); });
});
}
// Timed polling of radar data
@@ -686,6 +693,8 @@ void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) {
// Sending data frames
void MR24HPC1Component::send_query_(const uint8_t *query, size_t string_length) {
if (!this->module_ready_)
return;
this->write_array(query, string_length);
}
@@ -153,6 +153,7 @@ class MR24HPC1Component final : public Component,
int sg_start_query_data_;
bool check_dev_inf_sign_;
bool poll_time_base_func_check_;
bool module_ready_{false};
void update_();
void r24_split_data_frame_(uint8_t value);
@@ -243,6 +243,14 @@ std::optional<std::string> SendspinHub::get_pairing_token() const {
return this->client_->pairing_token();
}
// THREAD CONTEXT: Main loop
StringRef SendspinHub::get_client_id() const {
if (this->client_ == nullptr) {
return StringRef();
}
return StringRef(this->client_->client_id());
}
// THREAD CONTEXT: Main loop (invoked from codegen before setup(), or from Sendspin components)
void SendspinHub::set_unpaired_access_enabled(bool enabled) {
this->unpaired_access_ = enabled;
@@ -198,6 +198,12 @@ class SendspinHub final : public Component,
/// started once; it stays available after the client stops. Main loop only.
std::optional<std::string> get_pairing_token() const;
/// @brief Returns the Sendspin client_id: base64url of the client's static public key.
///
/// Empty until the client has started once; it stays set after the client stops. The returned StringRef points into
/// the client's string, which start() may rewrite, so do not keep it past the current call. Main loop only.
StringRef get_client_id() const;
/// @brief Sets the device information reported to the server in the `client/hello` message.
///
/// Each takes a pointer to a string literal emitted by codegen, so it must stay valid for the
@@ -412,6 +412,8 @@ void SerialProxy::fill_identity_([[maybe_unused]] IdentityScratch &scratch, api:
msg.usb.product_id = scratch.product_id;
msg.usb.bcd_device = scratch.bcd_device;
msg.usb.interface_number = this->usb_channel_->get_interface_number();
msg.usb.interface_class = this->usb_channel_->get_interface_class();
msg.usb.port_number = this->usb_channel_->get_port_number();
msg.manufacturer = StringRef(scratch.manufacturer);
msg.product = StringRef(scratch.product);
msg.serial_number = StringRef(scratch.serial_number);
+5 -25
View File
@@ -1,8 +1,7 @@
import esphome.codegen as cg
from esphome.components import binary_sensor, sensor, socket, uart
from esphome.components import socket, uart
from esphome.components.const import (
CONF_ALLOWED_IPS,
CONF_CONNECTED,
CONF_DATA_BITS,
CONF_HOST,
CONF_PARITY,
@@ -11,22 +10,16 @@ from esphome.components.const import (
CONF_STOP_BITS,
)
import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_ID,
CONF_PORT,
DEVICE_CLASS_CONNECTIVITY,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_TOTAL_INCREASING,
)
from esphome.const import CONF_BAUD_RATE, CONF_ID, CONF_PORT
from esphome.types import ConfigType
CODEOWNERS = ["@Bascht74"]
DEPENDENCIES = ["network"]
AUTO_LOAD = ["uart", "binary_sensor", "sensor", "socket"]
AUTO_LOAD = ["uart", "socket"]
DOMAIN = "tcp_uart"
MULTI_CONF = True
CONF_DISCONNECTS = "disconnects"
CONF_TCP_UART_ID = "tcp_uart_id"
tcp_uart_ns = cg.esphome_ns.namespace("tcp_uart")
TcpUart = tcp_uart_ns.class_("TcpUart", uart.UARTComponent, cg.Component)
@@ -45,15 +38,6 @@ BASE_SCHEMA = cv.Schema(
cv.Optional(
CONF_RECONNECT_INTERVAL, default="5s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_DISCONNECTS): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -99,7 +83,3 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_parity(config[CONF_PARITY]))
if (host := config.get(CONF_HOST)) is not None:
cg.add(var.set_host(host))
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_CONNECTED, var.set_connected_sensor)
sensors = sensor.sub_sensors(config)
await sensors(CONF_DISCONNECTS, var.set_disconnects_sensor)
@@ -0,0 +1,25 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
from esphome.components.const import CONF_CONNECTED
import esphome.config_validation as cv
from esphome.const import CONF_ID, DEVICE_CLASS_CONNECTIVITY, ENTITY_CATEGORY_DIAGNOSTIC
from esphome.types import ConfigType
from . import CONF_TCP_UART_ID, TcpUart
DEPENDENCIES = ["tcp_uart"]
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_TCP_UART_ID): cv.use_id(TcpUart),
cv.Optional(CONF_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_TCP_UART_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_CONNECTED, hub.set_connected_sensor)
+31
View File
@@ -0,0 +1,31 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_TOTAL_INCREASING,
)
from esphome.types import ConfigType
from . import CONF_TCP_UART_ID, TcpUart
DEPENDENCIES = ["tcp_uart"]
CONF_DISCONNECTS = "disconnects"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_TCP_UART_ID): cv.use_id(TcpUart),
cv.Optional(CONF_DISCONNECTS): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_TCP_UART_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_DISCONNECTS, hub.set_disconnects_sensor)
+6
View File
@@ -17,9 +17,11 @@ void TcpUart::setup() {
#ifdef USE_SOCKET_TCP_LISTENER
this->listener_.begin(TAG);
#endif
#ifdef USE_BINARY_SENSOR
if (this->connected_sensor_ != nullptr) {
this->connected_sensor_->publish_state(false);
}
#endif
#ifdef USE_SENSOR
if (this->disconnects_sensor_ != nullptr) {
this->disconnects_sensor_->publish_state(0);
@@ -38,7 +40,9 @@ void TcpUart::dump_config() {
#ifdef USE_SOCKET_TCP_LISTENER
this->listener_.dump_config();
#endif
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR(" ", "Disconnects", this->disconnects_sensor_);
#endif
@@ -58,9 +62,11 @@ void TcpUart::sync_link_() {
// Unread bytes of the last session stay readable while down, never into the next one.
this->rx_start_ = this->rx_end_ = 0;
}
#ifdef USE_BINARY_SENSOR
if (this->connected_sensor_ != nullptr) {
this->connected_sensor_->publish_state(up);
}
#endif
#ifdef USE_SENSOR
// Only edges get here, so down is the falling edge.
if (!up && this->disconnects_sensor_ != nullptr) {
+7
View File
@@ -1,6 +1,9 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
@@ -23,7 +26,9 @@ class TcpUart : public uart::UARTComponent, public Component {
void set_host(const char *host) { this->link_.set_host(host); }
void set_port(uint16_t port) { this->link_.set_port(port); }
void set_reconnect_interval(uint32_t ms) { this->link_.set_reconnect_interval(ms); }
#ifdef USE_BINARY_SENSOR
void set_connected_sensor(binary_sensor::BinarySensor *sensor) { this->connected_sensor_ = sensor; }
#endif
#ifdef USE_SENSOR
void set_disconnects_sensor(sensor::Sensor *sensor) { this->disconnects_sensor_ = sensor; }
#endif
@@ -64,7 +69,9 @@ class TcpUart : public uart::UARTComponent, public Component {
#ifdef USE_SOCKET_TCP_LISTENER
socket::TcpListener listener_;
#endif
#ifdef USE_BINARY_SENSOR
binary_sensor::BinarySensor *connected_sensor_{nullptr};
#endif
#ifdef USE_SENSOR
sensor::Sensor *disconnects_sensor_{nullptr};
uint32_t disconnects_{0};
+2 -1
View File
@@ -4,7 +4,8 @@ import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
CODEOWNERS = ["@0hax"]
CODEOWNERS = ["@0hax", "@FredM67"]
DOMAIN = "teleinfo"
MULTI_CONF = True
teleinfo_ns = cg.esphome_ns.namespace("teleinfo")
+136 -8
View File
@@ -1,10 +1,32 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_DEVICE_CLASS,
CONF_ICON,
CONF_ID,
CONF_STATE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_REACTIVE_ENERGY,
DEVICE_CLASS_VOLTAGE,
ICON_FLASH,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_KILOVOLT_AMPS,
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
UNIT_KILOWATT,
UNIT_KILOWATT_HOURS,
UNIT_MILLIAMP,
UNIT_MILLIVOLT,
UNIT_VOLT,
UNIT_VOLT_AMPS,
UNIT_VOLT_AMPS_REACTIVE_HOURS,
UNIT_WATT,
UNIT_WATT_HOURS,
)
from esphome.types import ConfigType
@@ -13,19 +35,125 @@ from .. import CONF_TAG_NAME, CONF_TELEINFO_ID, TELEINFO_LISTENER_SCHEMA, telein
TeleInfoSensor = teleinfo_ns.class_("TeleInfoSensor", sensor.Sensor, cg.Component)
CONFIG_SCHEMA = sensor.sensor_schema(
TeleInfoSensor,
unit_of_measurement=UNIT_WATT_HOURS,
icon=ICON_FLASH,
accuracy_decimals=0,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
).extend(TELEINFO_LISTENER_SCHEMA)
def _preset(unit: str, device_class: str, state_class: str) -> dict[str, str]:
return {
CONF_UNIT_OF_MEASUREMENT: unit,
CONF_DEVICE_CLASS: device_class,
CONF_STATE_CLASS: state_class,
}
_ENERGY = _preset(UNIT_WATT_HOURS, DEVICE_CLASS_ENERGY, STATE_CLASS_TOTAL_INCREASING)
_REACTIVE_ENERGY = _preset(
UNIT_VOLT_AMPS_REACTIVE_HOURS,
DEVICE_CLASS_REACTIVE_ENERGY,
STATE_CLASS_TOTAL_INCREASING,
)
_CURRENT = _preset(UNIT_AMPERE, DEVICE_CLASS_CURRENT, STATE_CLASS_MEASUREMENT)
_VOLTAGE = _preset(UNIT_VOLT, DEVICE_CLASS_VOLTAGE, STATE_CLASS_MEASUREMENT)
_APPARENT_POWER_VA = _preset(
UNIT_VOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
)
_APPARENT_POWER_KVA = _preset(
UNIT_KILOVOLT_AMPS, DEVICE_CLASS_APPARENT_POWER, STATE_CLASS_MEASUREMENT
)
_POWER = _preset(UNIT_WATT, DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT)
# Legacy defaults, kept for tags without a known preset
_LEGACY_DEFAULT = {**_ENERGY, CONF_ICON: ICON_FLASH}
# Units that are valid for each device class, used to decide whether a preset's
# device class can be kept when the user overrides the unit (and vice versa)
_COMPATIBLE_UNITS = {
DEVICE_CLASS_ENERGY: {UNIT_WATT_HOURS, UNIT_KILOWATT_HOURS},
DEVICE_CLASS_REACTIVE_ENERGY: {
UNIT_VOLT_AMPS_REACTIVE_HOURS,
UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
},
DEVICE_CLASS_CURRENT: {UNIT_AMPERE, UNIT_MILLIAMP},
DEVICE_CLASS_VOLTAGE: {UNIT_VOLT, UNIT_MILLIVOLT},
DEVICE_CLASS_APPARENT_POWER: {UNIT_VOLT_AMPS, UNIT_KILOVOLT_AMPS},
DEVICE_CLASS_POWER: {UNIT_WATT, UNIT_KILOWATT},
}
# Presets by tag prefix. No prefix is a prefix of another, so order is irrelevant.
TIC_TAG_CONFIGS = {
# Standard mode
"EA": _ENERGY,
"ER": _REACTIVE_ENERGY,
"IRMS": _CURRENT,
"URMS": _VOLTAGE,
"UMOY": _VOLTAGE,
"SINST": _APPARENT_POWER_VA,
"SMAX": _APPARENT_POWER_VA,
"CC": _POWER,
"PREF": _APPARENT_POWER_KVA,
"PCOUP": _APPARENT_POWER_KVA,
# Historical mode
"BASE": _ENERGY,
"HCH": _ENERGY,
"EJP": _ENERGY,
"BBRH": _ENERGY,
"IINST": _CURRENT,
"IMAX": _CURRENT,
"ISOUSC": _CURRENT,
"ADPS": _CURRENT,
"ADIR": _CURRENT,
"PAPP": _APPARENT_POWER_VA,
"PMAX": _POWER,
}
def apply_tag_config(config: ConfigType) -> ConfigType:
"""Apply preset configurations based on the tag name.
Only keys not set by the user are filled in. For known tags, a preset unit or
device class is dropped if it is incompatible with the one set by the user.
Unknown tags get the legacy defaults, merged key by key as before.
"""
if CONF_TAG_NAME not in config:
return config
# Runs before cv.string, so the raw value may not be a string yet
tag_name = str(config[CONF_TAG_NAME])
preset = next(
(p for prefix, p in TIC_TAG_CONFIGS.items() if tag_name.startswith(prefix)),
None,
)
config = dict(config)
if preset is None:
for key, value in _LEGACY_DEFAULT.items():
config.setdefault(key, value)
return config
unit = config.get(CONF_UNIT_OF_MEASUREMENT, preset[CONF_UNIT_OF_MEASUREMENT])
device_class = config.get(CONF_DEVICE_CLASS, preset[CONF_DEVICE_CLASS])
compatible = unit in _COMPATIBLE_UNITS.get(device_class, ())
for key, value in preset.items():
if key in config:
continue
if key in (CONF_UNIT_OF_MEASUREMENT, CONF_DEVICE_CLASS) and not compatible:
continue
config[key] = value
return config
CONFIG_SCHEMA = cv.All(
apply_tag_config,
sensor.sensor_schema(
TeleInfoSensor,
accuracy_decimals=0,
).extend(TELEINFO_LISTENER_SCHEMA),
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID], config[CONF_TAG_NAME])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
teleinfo = await cg.get_variable(config[CONF_TELEINFO_ID])
cg.add(teleinfo.register_teleinfo_listener(var))
+3 -4
View File
@@ -116,8 +116,7 @@ void TeleInfo::loop() {
* 0xa | Tag | 0x9 | Timestamp | 0x9 | 0x9 | CRC | 0xd
*
*/
while ((buf_finger = static_cast<char *>(memchr(buf_finger, (int) 0xa, buf_index_ - 1))) &&
((buf_finger - buf_) < buf_index_)) { // NOLINT(clang-diagnostic-sign-compare)
while ((buf_finger = static_cast<char *>(memchr(buf_finger, (int) 0xa, buf_end - buf_finger)))) {
/*
* Make sure timesamp is nullified between each tag as some tags don't
* have a timestamp
@@ -168,8 +167,8 @@ void TeleInfo::loop() {
continue;
}
/* Advance buf_finger to end of group */
buf_finger += field_len + 1 + 1 + 1;
/* Resume after the group's 0xd; grp_end < buf_end, so this stays in bounds. */
buf_finger = grp_end + 1;
publish_value_(std::string(tag_), std::string(val_));
}
+8
View File
@@ -26,6 +26,8 @@ from esphome.const import (
CONF_SECONDS,
CONF_TIMEZONE,
CONF_TRIGGER_ID,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
@@ -462,6 +464,12 @@ async def register_time(time_var, config):
async def to_code(config):
if CORE.using_zephyr:
zephyr_add_prj_conf("POSIX_CLOCK", True)
# From sdk-nrf 2.8.0 newlib time() gets the clock through gettimeofday(),
# which Zephyr only provides with XSI_SINGLE_PROCESS. Without it time()
# always returns -1.
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(2, 8, 0):
zephyr_add_prj_conf("POSIX_SINGLE_PROCESS", True)
zephyr_add_prj_conf("XSI_SINGLE_PROCESS", True)
cg.add_define("USE_TIME")
cg.add_global(time_ns.using)
@@ -0,0 +1,179 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include "esphome/core/helpers.h"
namespace esphome::uart {
/// Software serial RX decoder: on_edge() counts bits from the time between edges;
/// finalize() completes a byte whose idle high tail has no closing edge.
/// Times are in any steady tick unit.
class SoftwareSerialRxDecoder {
public:
static constexpr uint8_t RX_IDLE = 0xFF;
/// Configure framing and buffer; drops all state. Follow with reset().
void setup(uint32_t bit_ticks, uint8_t data_bits, bool parity, uint8_t stop_bits, uint8_t *buffer,
size_t buffer_size) {
this->bit_ticks_ = bit_ticks;
this->data_bits_ = data_bits;
this->stop_bit_ = data_bits + (parity ? 1 : 0);
this->max_run_ticks_ = bit_ticks * (this->stop_bit_ + stop_bits + 2);
this->buffer_ = buffer;
this->buffer_size_ = buffer_size;
this->in_pos_ = 0;
this->out_pos_ = 0;
this->reset(0, true);
}
/// Forget any partial frame; `level` is the current line level.
void reset(uint32_t now, bool level) {
this->bit_ = RX_IDLE;
this->cur_byte_ = 0;
this->last_level_ = level;
this->last_edge_ = now;
}
/// ISR. True to wake the loop: a byte was pushed, or the line went high in an
/// open frame (a data 1 and the idle tail look alike) so finalize() may be needed.
bool ESPHOME_ALWAYS_INLINE on_edge(uint32_t now, bool level) {
const bool last_level = this->last_level_;
// Collapsed edge
if (level == last_level)
return false;
// LX106 has no divider, so count
uint32_t delta = now - this->last_edge_;
if (delta > this->max_run_ticks_)
delta = this->max_run_ticks_;
delta += this->bit_ticks_ / 2;
uint32_t bits = 0;
while (delta >= this->bit_ticks_) {
delta -= this->bit_ticks_;
bits++;
}
const bool pushed = this->consume_run_(bits, last_level);
this->last_edge_ = now;
this->last_level_ = level;
return pushed || (level && this->bit_ != RX_IDLE);
}
bool pending() const { return this->bit_ != RX_IDLE && this->last_level_; }
bool finalize_due(uint32_t now) const {
const uint8_t bit = this->bit_;
if (bit == RX_IDLE)
return false;
return now - this->last_edge_ >= this->tail_ticks_(bit);
}
/// Call with the ISR masked.
void finalize(uint32_t now) {
if (!this->last_level_ || !this->finalize_due(now))
return;
// Remaining bits are all ones
const uint8_t bit = this->bit_;
uint8_t cur = this->cur_byte_;
if (bit < this->data_bits_)
cur |= ((1U << this->data_bits_) - 1) & ~((1U << bit) - 1);
this->push_byte(cur);
this->bit_ = RX_IDLE;
}
/// Drops the byte when full; holds buffer_size - 1 bytes.
bool ESPHOME_ALWAYS_INLINE push_byte(uint8_t data) {
size_t in = this->in_pos_;
size_t next = in + 1;
if (next == this->buffer_size_)
next = 0;
if (next == this->out_pos_)
return false;
this->buffer_[in] = data;
this->in_pos_ = next;
return true;
}
uint8_t *buffer() const { return this->buffer_; }
size_t buffer_size() const { return this->buffer_size_; }
size_t available() const {
// Read volatile in_pos_ once to avoid TOCTOU race with ISR.
size_t in = this->in_pos_;
if (in >= this->out_pos_)
return in - this->out_pos_;
return this->buffer_size_ - this->out_pos_ + in;
}
uint8_t peek_byte() const {
if (this->in_pos_ == this->out_pos_)
return 0;
return this->buffer_[this->out_pos_];
}
uint8_t read_byte() {
if (this->in_pos_ == this->out_pos_)
return 0;
uint8_t data = this->buffer_[this->out_pos_];
size_t next = this->out_pos_ + 1;
this->out_pos_ = next == this->buffer_size_ ? 0 : next;
return data;
}
protected:
uint32_t tail_ticks_(uint8_t bit) const {
return (this->stop_bit_ + 1 - bit) * this->bit_ticks_ + this->bit_ticks_ / 2;
}
bool ESPHOME_ALWAYS_INLINE consume_run_(uint32_t bits, bool level) {
uint8_t bit = this->bit_;
uint8_t cur = this->cur_byte_;
bool pushed = false;
while (bits > 0) {
if (bit == RX_IDLE) {
if (level)
break;
// Start bit
bit = 0;
cur = 0;
bits--;
} else if (bit < this->data_bits_) {
uint8_t n = this->data_bits_ - bit;
if (n > bits)
n = bits;
if (level)
cur |= ((1U << n) - 1) << bit;
bit += n;
bits -= n;
} else if (bit < this->stop_bit_) {
// Parity, not checked
bit++;
bits--;
} else {
// Stop bit; low is a framing error
if (level)
pushed = this->push_byte(cur);
bit = RX_IDLE;
break;
}
}
this->bit_ = bit;
this->cur_byte_ = cur;
return pushed;
}
// Members ordered largest to smallest to minimize padding
uint32_t bit_ticks_{0};
uint32_t max_run_ticks_{0};
volatile uint32_t last_edge_{0};
uint8_t *buffer_{nullptr};
size_t buffer_size_{0};
volatile size_t in_pos_{0};
size_t out_pos_{0}; // reader only
/// Next frame bit (data, parity, then stop at stop_bit_) or RX_IDLE.
volatile uint8_t bit_{RX_IDLE};
volatile uint8_t cur_byte_{0};
volatile bool last_level_{true};
uint8_t data_bits_{8};
uint8_t stop_bit_{8};
};
} // namespace esphome::uart
+2 -2
View File
@@ -10,8 +10,8 @@ namespace esphome::uart {
static const char *const TAG = "uart";
// UART parity strings indexed by UARTParityOptions enum (0-2): NONE, EVEN, ODD
PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "UNKNOWN");
// UART parity strings indexed by UARTParityOptions enum (0-4): NONE, EVEN, ODD, MARK, SPACE
PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "MARK", "SPACE", "UNKNOWN");
void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity,
uint8_t data_bits) {
+3
View File
@@ -18,6 +18,9 @@ enum UARTParityOptions {
UART_CONFIG_PARITY_NONE,
UART_CONFIG_PARITY_EVEN,
UART_CONFIG_PARITY_ODD,
// Only USB serial adapters (usb_uart) support these
UART_CONFIG_PARITY_MARK,
UART_CONFIG_PARITY_SPACE,
};
#ifdef USE_UART_DEBUGGER
@@ -15,7 +15,13 @@
namespace esphome::uart {
static const char *const TAG = "uart";
ESPHOME_LOG_TAG(TAG, "uart");
// Edge decoder up to this baud rate; at 38400 WiFi interrupt latency corrupts bytes.
static constexpr uint32_t SW_SERIAL_EDGE_MODE_MAX_BAUD = 19200;
// Edge decoder time base: 1/16 us from the 1 MHz system timer, which keeps its rate during a
// CpuFrequencyBoost; the cycle counter does not.
static constexpr uint32_t RX_TICKS_PER_SECOND = 16000000;
__attribute__((always_inline)) static inline uint32_t rx_now() { return micros() << 4; }
bool ESP8266UartComponent::serial0_in_use = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
uint32_t ESP8266UartComponent::get_config() {
@@ -158,6 +164,11 @@ void ESP8266UartComponent::dump_config() {
"\n Wake on data RX: ENABLED"
#endif
);
if (this->rx_pin_ != nullptr) {
ESP_LOGCONFIG(TAG, " RX decoder: %s",
this->baud_rate_ <= SW_SERIAL_EDGE_MODE_MAX_BAUD ? LOG_STR_LITERAL("edge")
: LOG_STR_LITERAL("start bit sampler"));
}
}
this->check_logger_conflict();
}
@@ -239,23 +250,39 @@ UARTFlushResult ESP8266UartComponent::flush() {
void ESP8266SoftwareSerial::setup(InternalGPIOPin *tx_pin, InternalGPIOPin *rx_pin, uint32_t baud_rate,
uint8_t stop_bits, uint32_t data_bits, UARTParityOptions parity,
size_t rx_buffer_size) {
// load_settings() re-enters here
if (this->gpio_rx_pin_ != nullptr)
this->gpio_rx_pin_->detach_interrupt();
this->bit_time_ = F_CPU / baud_rate;
this->rx_buffer_size_ = rx_buffer_size;
this->stop_bits_ = stop_bits;
this->data_bits_ = data_bits;
this->parity_ = parity;
// to_isr() allocates, so call it only for a new or changed pin
if (tx_pin != nullptr) {
if (tx_pin != this->gpio_tx_pin_)
this->tx_pin_ = tx_pin->to_isr();
gpio_tx_pin_ = tx_pin;
gpio_tx_pin_->setup();
tx_pin_ = gpio_tx_pin_->to_isr();
tx_pin_.digital_write(true);
}
if (rx_pin != nullptr) {
if (rx_pin != this->gpio_rx_pin_)
this->rx_pin_ = rx_pin->to_isr();
gpio_rx_pin_ = rx_pin;
gpio_rx_pin_->setup();
rx_pin_ = gpio_rx_pin_->to_isr();
rx_buffer_ = new uint8_t[this->rx_buffer_size_]; // NOLINT
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
uint8_t *buffer = this->rx_.buffer();
if (this->rx_.buffer_size() != rx_buffer_size) {
delete[] buffer; // NOLINT
buffer = new uint8_t[rx_buffer_size]; // NOLINT
}
this->rx_.setup((RX_TICKS_PER_SECOND + baud_rate / 2) / baud_rate, data_bits, parity != UART_CONFIG_PARITY_NONE,
stop_bits, buffer, rx_buffer_size);
this->rx_.reset(rx_now(), this->rx_pin_.digital_read());
if (baud_rate <= SW_SERIAL_EDGE_MODE_MAX_BAUD) {
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr_edge, this, gpio::INTERRUPT_ANY_EDGE);
} else {
gpio_rx_pin_->attach_interrupt(ESP8266SoftwareSerial::gpio_intr, this, gpio::INTERRUPT_FALLING_EDGE);
}
}
}
// A byte can arrive while a CpuFrequencyBoost has an 80 MHz build at 160 MHz; the clock select bit doubles
@@ -288,8 +315,7 @@ void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
if (arg->stop_bits_ == 2)
arg->wait_(&wait, start, bit_time);
arg->rx_buffer_[arg->rx_in_pos_] = rec;
arg->rx_in_pos_ = (arg->rx_in_pos_ + 1) % arg->rx_buffer_size_;
arg->rx_.push_byte(rec);
// Clear RX pin so that the interrupt doesn't re-trigger right away again.
arg->rx_pin_.clear_interrupt();
#ifdef USE_UART_WAKE_LOOP_ON_RX
@@ -299,6 +325,27 @@ void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr(ESP8266SoftwareSerial *arg) {
wake_loop_isrsafe();
#endif
}
void IRAM_ATTR ESP8266SoftwareSerial::gpio_intr_edge(ESP8266SoftwareSerial *arg) {
const uint32_t now = rx_now();
const bool level = arg->rx_pin_.digital_read();
[[maybe_unused]] const bool wake = arg->rx_.on_edge(now, level);
#ifdef USE_UART_WAKE_LOOP_ON_RX
if (wake)
wake_loop_isrsafe();
#endif
}
void ESP8266SoftwareSerial::rx_finalize_pending_() {
if (!this->rx_.finalize_due(rx_now())) {
#ifdef USE_UART_WAKE_LOOP_ON_RX
// Tail not elapsed; wake again only once drained
if (this->rx_.available() == 0)
wake_loop_threadsafe();
#endif
return;
}
InterruptLock lock;
this->rx_.finalize(rx_now());
}
void IRAM_ATTR HOT ESP8266SoftwareSerial::write_byte(uint8_t data) {
if (this->gpio_tx_pin_ == nullptr) {
ESP_LOGE(TAG, "UART doesn't have TX pins set!");
@@ -350,28 +397,19 @@ void IRAM_ATTR ESP8266SoftwareSerial::write_bit_(bool bit, uint32_t *wait, const
this->wait_(wait, start, bit_time);
}
uint8_t ESP8266SoftwareSerial::read_byte() {
if (this->rx_in_pos_ == this->rx_out_pos_)
return 0;
uint8_t data = this->rx_buffer_[this->rx_out_pos_];
this->rx_out_pos_ = (this->rx_out_pos_ + 1) % this->rx_buffer_size_;
return data;
this->rx_sync_();
return this->rx_.read_byte();
}
uint8_t ESP8266SoftwareSerial::peek_byte() {
if (this->rx_in_pos_ == this->rx_out_pos_)
return 0;
return this->rx_buffer_[this->rx_out_pos_];
this->rx_sync_();
return this->rx_.peek_byte();
}
void ESP8266SoftwareSerial::flush() {
// Flush is a NO-OP with software serial, all bytes are written immediately.
}
size_t ESP8266SoftwareSerial::available() {
// Read volatile rx_in_pos_ once to avoid TOCTOU race with ISR.
// When in >= out, data is contiguous: [out..in).
// When in < out, data wraps: [out..buf_size) + [0..in).
size_t in = this->rx_in_pos_;
if (in >= this->rx_out_pos_)
return in - this->rx_out_pos_;
return this->rx_buffer_size_ - this->rx_out_pos_ + in;
this->rx_sync_();
return this->rx_.available();
}
} // namespace esphome::uart
@@ -7,6 +7,7 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "software_serial_rx_decoder.h"
#include "uart_component.h"
namespace esphome::uart {
@@ -26,24 +27,30 @@ class ESP8266SoftwareSerial {
size_t available();
protected:
/// Above SW_SERIAL_EDGE_MODE_MAX_BAUD; reads the whole byte in the ISR.
static void gpio_intr(ESP8266SoftwareSerial *arg);
static void gpio_intr_edge(ESP8266SoftwareSerial *arg);
void wait_(uint32_t *wait, const uint32_t &start, uint32_t bit_time);
bool read_bit_(uint32_t *wait, const uint32_t &start, uint32_t bit_time);
void write_bit_(bool bit, uint32_t *wait, const uint32_t &start, uint32_t bit_time);
void rx_finalize_pending_();
void ESPHOME_ALWAYS_INLINE rx_sync_() {
if (this->rx_.pending())
this->rx_finalize_pending_();
}
// Members ordered largest to smallest to minimize padding
uint32_t bit_time_{0};
uint8_t *rx_buffer_{nullptr};
size_t rx_buffer_size_;
volatile size_t rx_in_pos_{0};
size_t rx_out_pos_{0};
uint8_t stop_bits_;
uint8_t data_bits_;
UARTParityOptions parity_;
InternalGPIOPin *gpio_tx_pin_{nullptr};
ISRInternalGPIOPin tx_pin_;
InternalGPIOPin *gpio_rx_pin_{nullptr};
ISRInternalGPIOPin rx_pin_;
SoftwareSerialRxDecoder rx_;
UARTParityOptions parity_;
uint8_t stop_bits_;
uint8_t data_bits_;
};
class ESP8266UartComponent final : public UARTComponent, public Component {
@@ -158,6 +158,8 @@ void HostUartComponent::setup() {
options.c_cflag |= PARENB;
options.c_cflag |= PARODD;
break;
default:
break;
};
// Set stop bits
if (this->stop_bits_ == 2) {
@@ -38,6 +38,8 @@ uint16_t LibreTinyUARTComponent::get_config() {
case UART_CONFIG_PARITY_ODD:
config |= SERIAL_PARITY_ODD;
break;
default:
break;
}
config |= (this->data_bits_ - 4) << 8;
+3 -20
View File
@@ -1,8 +1,7 @@
import esphome.codegen as cg
from esphome.components import binary_sensor, sensor, socket, uart
from esphome.components import socket, uart
from esphome.components.const import (
CONF_ALLOWED_IPS,
CONF_CONNECTED,
CONF_HOST,
CONF_RECONNECT_INTERVAL,
CONF_ROLE,
@@ -14,9 +13,6 @@ from esphome.const import (
CONF_ID,
CONF_PORT,
CONF_UART_ID,
DEVICE_CLASS_CONNECTIVITY,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_TOTAL_INCREASING,
)
from esphome.core import CORE
import esphome.final_validate as fv
@@ -25,10 +21,10 @@ from esphome.types import ConfigType
CODEOWNERS = ["@Bascht74"]
DOMAIN = "uart_tcp"
DEPENDENCIES = ["network", "uart"]
AUTO_LOAD = ["binary_sensor", "sensor", "socket"]
AUTO_LOAD = ["socket"]
MULTI_CONF = True
CONF_DISCONNECTS = "disconnects"
CONF_UART_TCP_ID = "uart_tcp_id"
uart_tcp_ns = cg.esphome_ns.namespace("uart_tcp")
UartTcp = uart_tcp_ns.class_("UartTcp", cg.Component, uart.UARTDevice)
@@ -42,15 +38,6 @@ BASE_SCHEMA = cv.Schema(
cv.Optional(
CONF_RECONNECT_INTERVAL, default="5s"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
cv.Optional(CONF_DISCONNECTS): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -155,7 +142,3 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_reconnect_interval(config[CONF_RECONNECT_INTERVAL]))
if (host := config.get(CONF_HOST)) is not None:
cg.add(var.set_host(host))
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_CONNECTED, var.set_connected_sensor)
sensors = sensor.sub_sensors(config)
await sensors(CONF_DISCONNECTS, var.set_disconnects_sensor)
@@ -0,0 +1,25 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
from esphome.components.const import CONF_CONNECTED
import esphome.config_validation as cv
from esphome.const import CONF_ID, DEVICE_CLASS_CONNECTIVITY, ENTITY_CATEGORY_DIAGNOSTIC
from esphome.types import ConfigType
from . import CONF_UART_TCP_ID, UartTcp
DEPENDENCIES = ["uart_tcp"]
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_UART_TCP_ID): cv.use_id(UartTcp),
cv.Optional(CONF_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_UART_TCP_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_CONNECTED, hub.set_connected_sensor)
+31
View File
@@ -0,0 +1,31 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
ENTITY_CATEGORY_DIAGNOSTIC,
STATE_CLASS_TOTAL_INCREASING,
)
from esphome.types import ConfigType
from . import CONF_UART_TCP_ID, UartTcp
DEPENDENCIES = ["uart_tcp"]
CONF_DISCONNECTS = "disconnects"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_UART_TCP_ID): cv.use_id(UartTcp),
cv.Optional(CONF_DISCONNECTS): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_TOTAL_INCREASING,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_UART_TCP_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_DISCONNECTS, hub.set_disconnects_sensor)
+6
View File
@@ -19,9 +19,11 @@ void UartTcp::setup() {
#ifdef USE_SOCKET_TCP_LISTENER
this->listener_.begin(TAG);
#endif
#ifdef USE_BINARY_SENSOR
if (this->connected_sensor_ != nullptr) {
this->connected_sensor_->publish_state(false);
}
#endif
#ifdef USE_SENSOR
if (this->disconnects_sensor_ != nullptr) {
this->disconnects_sensor_->publish_state(0);
@@ -40,7 +42,9 @@ void UartTcp::dump_config() {
#ifdef USE_SOCKET_TCP_LISTENER
this->listener_.dump_config();
#endif
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Connected", this->connected_sensor_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR(" ", "Disconnects", this->disconnects_sensor_);
#endif
@@ -60,9 +64,11 @@ void UartTcp::sync_link_() {
// The driver kept whatever arrived while the link was down.
this->discard_uart_();
}
#ifdef USE_BINARY_SENSOR
if (this->connected_sensor_ != nullptr) {
this->connected_sensor_->publish_state(up);
}
#endif
#ifdef USE_SENSOR
// Only edges get here, so down is the falling edge.
if (!up && this->disconnects_sensor_ != nullptr) {
Loaded 100 of 241 files, more files were not shown because too many files have changed in this diff. Show more