1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-04 09:01:49 +00:00

Compare commits

..

131 Commits

Author SHA1 Message Date
J. Nick Koston
c6858163a7 Merge remote-tracking branch 'upstream/dev' into add_api_stats 2025-05-27 09:10:13 -05:00
Jesse Hills
d585440d54 Merge branch 'release' into dev 2025-05-27 21:02:03 +12:00
Jesse Hills
f74f89c6b5 Merge pull request #8913 from esphome/bump-2025.5.1
2025.5.1
2025-05-27 21:01:19 +12:00
dependabot[bot]
7d049a61bb Bump pytest-xdist from 3.6.1 to 3.7.0 (#8916)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 06:55:47 +00:00
dependabot[bot]
f2e4dc7907 Bump setuptools from 80.8.0 to 80.9.0 (#8915)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 06:54:33 +00:00
dependabot[bot]
0c7589caeb Bump pytest-mock from 3.14.0 to 3.14.1 (#8909)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 17:26:14 +12:00
dependabot[bot]
321411e355 Bump ruamel-yaml from 0.18.10 to 0.18.11 (#8910)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 17:26:08 +12:00
Samuel Sieb
361de22370 [sx1509] add support for keys (#8413)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-27 17:16:27 +12:00
Jesse Hills
95a17387a8 Bump actions/checkout from 4.1.7 to 4.2.2 (#8904) 2025-05-27 16:26:01 +12:00
J. Nick Koston
caf9930ff9 Fix flakey tests (#8914) 2025-05-27 16:20:14 +12:00
Jesse Hills
42390faf4a Bump version to 2025.5.1 2025-05-27 14:31:38 +12:00
Jesse Hills
fdc6c4a219 [web_server] Fix download list where external_components has a substitution value (#8911)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-27 14:31:38 +12:00
Jesse Hills
6c08f5e343 [api] Fix crash with gcc compiler on host (#8902) 2025-05-27 14:31:38 +12:00
Keith Burzinski
e0e4ba9592 [esp32] Fix building on IDF 4 (#8892) 2025-05-27 14:31:38 +12:00
Jesse Hills
ad20825f31 [logger] Fix options in select (#8875)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2025-05-27 14:31:38 +12:00
Kevin Ahrendt
e4f3a952d5 [speaker] ensure the pipeline returns an error state before returning its stopped (#8878) 2025-05-27 14:31:38 +12:00
Kevin Ahrendt
90e3c5bba2 [micro_wake_word] avoid duplicated detections from same event (#8877) 2025-05-27 14:31:38 +12:00
Clyde Stubbs
b1d5ad27f3 [lvgl] Improve error messages from text validation (#8872) 2025-05-27 14:31:38 +12:00
Jesse Hills
5c54f75b7a [online_image] Allocate pngle manually to potentially use psram (#8354)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-05-27 14:31:38 +12:00
Cossid
a5f85b4437 [tuya_select] - Fix datapoint config error. (#8871) 2025-05-27 14:31:38 +12:00
Jesse Hills
da4e710249 [core] Add some missing includes (#8864) 2025-05-27 14:31:38 +12:00
J. Nick Koston
4ac433fddb Add integration tests for host (#8912) 2025-05-26 21:31:32 -05:00
Jesse Hills
73771d5c50 [web_server] Fix download list where external_components has a substitution value (#8911)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-27 09:08:16 +12:00
Jesse Hills
af7b1a3a23 [api] Fix crash with gcc compiler on host (#8902) 2025-05-27 06:46:51 +12:00
dependabot[bot]
430f63fcbb Bump pyupgrade from 3.19.1 to 3.20.0 (#8891)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-26 08:13:45 +00:00
Emmanuel Ferdman
5921a9cd68 Resolve regex library warnings (#8890) 2025-05-26 19:45:47 +12:00
Jonathan Swoboda
ca0037d076 [esp32, logger, core] Add initial c5 support (#8895) 2025-05-26 13:33:41 +12:00
Jesse Hills
1e18d0b06c [i2s_audio] Add basic support for esp32-p4 (#8887) 2025-05-26 11:55:51 +12:00
luar123
4b5c3e7e2b [bme68x_bsec2_i2c] Remove arduino dependency (#7815) 2025-05-25 03:08:51 -05:00
Keith Burzinski
d4c4b75eb3 [esp32] Fix building on IDF 4 (#8892) 2025-05-25 02:15:24 +12:00
Jesse Hills
9dd4045984 [const] Move `CONF_RESET` to const.py (#8889) 2025-05-23 21:54:06 -05:00
gotnone
19e2460af2 [modbus_controller] Add assumed_state to switch (#8880)
Co-authored-by: Stanley Pinchak <stanley.pinchak@gmail.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-23 21:34:10 +12:00
Rodrigo Martín
149f787035 feat: wifi.configure now emits error after reconnecting to old AP (#8653) 2025-05-23 21:32:47 +12:00
J. Nick Koston
0a1f3e813c more stats 2025-05-22 21:58:16 -05:00
J. Nick Koston
663f38d2ec merge 2025-05-22 21:31:31 -05:00
J. Nick Koston
f0b311f839 Merge remote-tracking branch 'upstream/dev' into add_api_stats 2025-05-22 21:29:11 -05:00
J. Nick Koston
2ab1fe1abf Use UINT16_MAX instead of hard coded 65535 in api (#8884)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-22 23:51:06 +00:00
Jesse Hills
926b42ba1c [logger] Fix options in select (#8875)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2025-05-23 09:33:38 +10:00
J. Nick Koston
1c06137ae0 Merge remote-tracking branch 'upstream/dev' into add_api_stats 2025-05-22 18:04:25 -05:00
J. Nick Koston
377ed2e212 Optimize API frame helper buffer management (#8805)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-22 21:11:16 +00:00
esphomebot
42912447fb Synchronise Device Classes from Home Assistant (#8874) 2025-05-23 08:50:31 +12:00
Pi57
25ead44f1c Add const DEVICE_CLASS_WIND_DIRECTION (#8870)
Co-authored-by: PierreYvesHB <pierre-yves.henius-beck@act-blue.eu>
2025-05-23 08:49:37 +12:00
dependabot[bot]
03b003af47 Bump ruff from 0.11.10 to 0.11.11 (#8883)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-22 20:22:04 +00:00
dependabot[bot]
5baccf0ce7 Bump tornado from 6.4.2 to 6.5.1 (#8882)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-22 20:21:46 +00:00
Kevin Ahrendt
e95c92773c [speaker] ensure the pipeline returns an error state before returning its stopped (#8878) 2025-05-23 07:20:15 +12:00
Kevin Ahrendt
c23ea384fb [micro_wake_word] avoid duplicated detections from same event (#8877) 2025-05-23 07:19:16 +12:00
Lưu Oa Oa (宰相劉羅鍋)
69da17742f OTA: Close and clean up client when setsockopt fails (#8865)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
2025-05-22 11:37:42 -05:00
Clyde Stubbs
1ec57a74b5 [usb_uart] Implement USB Host mode UART (#8334) 2025-05-22 13:54:40 +12:00
Clyde Stubbs
d1e55252d0 [lvgl] Improve error messages from text validation (#8872) 2025-05-22 13:49:56 +12:00
Clyde Stubbs
090feb55e9 [lvgl] Add content styling to tabview (#8823) 2025-05-22 13:47:38 +12:00
Clyde Stubbs
6109acb6f3 [lvgl] Try to allocate smaller buffer on failure (#8814)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-22 13:45:56 +12:00
Jesse Hills
5aa13db815 [online_image] Allocate pngle manually to potentially use psram (#8354)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-05-22 13:40:53 +12:00
Jesse Hills
1b67dd4232 [sync] Update and fix sync workflow (#8873) 2025-05-22 13:32:19 +12:00
Cossid
ba6efcedcb [tuya_select] - Fix datapoint config error. (#8871) 2025-05-22 13:26:19 +12:00
Jesse Hills
bd7c2a680c Updates for development environment (#8801) 2025-05-22 13:24:34 +12:00
Andrew J.Swan
1466aa7703 Add CUBIC CM1106 Single Beam NDIR CO2 Sensor Module (#8293)
Co-authored-by: Djordje <6750655+DjordjeMandic@users.noreply.github.com>
Co-authored-by: Patrick <info@patagona.dev>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-22 13:23:54 +12:00
Jonathan Swoboda
787f4860db [esp32, logger] Add initial P4 support (#8439)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-22 13:22:01 +12:00
Thomas Rupprecht
aeb4e63950 update minimal python version to 3.10 (#8850) 2025-05-22 13:21:43 +12:00
Jonathan Swoboda
026f47bfb3 [esp32] Use IDF 5.3.2 as default for IDF builds (#8464)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2025-05-22 13:21:27 +12:00
Jesse Hills
dd47d063b5 Merge branch 'release' into dev 2025-05-21 20:33:34 +12:00
Jesse Hills
cdcd1cd292 Merge pull request #8863 from esphome/bump-2025.5.0
2025.5.0
2025-05-21 20:32:40 +12:00
Jesse Hills
a6fa963605 [core] Add some missing includes (#8864) 2025-05-21 20:02:14 +12:00
Jesse Hills
1cba22175f Bump version to 2025.5.0 2025-05-21 15:26:55 +12:00
Jesse Hills
f2d7720a4e Merge branch 'beta' into dev 2025-05-21 13:09:35 +12:00
Jesse Hills
801138da27 Merge pull request #8862 from esphome/bump-2025.5.0b6
2025.5.0b6
2025-05-21 13:09:04 +12:00
Jesse Hills
51740a2e99 Bump version to 2025.5.0b6 2025-05-21 11:54:08 +12:00
Jesse Hills
d68a391e67 [api-docs] Move netlify.toml to root (#8861) 2025-05-21 11:54:07 +12:00
Jesse Hills
e9d832d64a [api-docs] Move netlify.toml to root (#8861) 2025-05-21 11:43:19 +12:00
Jesse Hills
f8f09bca02 Merge branch 'beta' into dev 2025-05-21 11:26:20 +12:00
Jesse Hills
756aa13779 Merge pull request #8860 from esphome/bump-2025.5.0b5
2025.5.0b5
2025-05-21 11:25:48 +12:00
Jesse Hills
25bbc0c221 Bump version to 2025.5.0b5 2025-05-21 10:05:54 +12:00
Gustavo Ambrozio
220a14e1f8 [at581x] Fix issue with methods not being public (#8852) 2025-05-21 10:05:53 +12:00
Clyde Stubbs
ac74b25c46 Fix #ifdefs (#8853) 2025-05-21 10:05:53 +12:00
dependabot[bot]
c5d809b3dd Bump setuptools from 80.7.1 to 80.8.0 (#8858)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 16:27:53 -04:00
Thomas Rupprecht
b1cf08b261 add python 3.13 to ci pipeline (#8855) 2025-05-20 10:04:09 -04:00
Gustavo Ambrozio
6ae83dfe3d [at581x] Fix issue with methods not being public (#8852) 2025-05-20 02:36:44 -05:00
Thomas Rupprecht
0932e83b15 update ruff version to `0.11.10 in .pre-commit-config.yaml` (#8851) 2025-05-20 00:42:43 -04:00
Clyde Stubbs
86670c4d39 Fix #ifdefs (#8853) 2025-05-20 13:19:24 +10:00
dependabot[bot]
4ce55b94ec Bump aioesphomeapi from 31.0.1 to 31.1.0 (#8849) 2025-05-19 20:30:30 -04:00
Jesse Hills
1c5dc63eb4 Merge branch 'beta' into dev 2025-05-20 01:19:32 +12:00
Jesse Hills
ef7a22ff04 [api-docs] Run using netlify builders (#8842)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-19 19:48:04 +12:00
Jesse Hills
dfda0e5c7c [docker] Update pip on build (#8835)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2025-05-19 18:22:24 +12:00
J. Nick Koston
78c63311c6 Fix ethernet connection timeout issue caused by incorrect time value during setup (#8841) 2025-05-19 00:25:21 -05:00
Jesse Hills
1ac51e7b3e Merge branch 'beta' into dev 2025-05-19 16:03:18 +12:00
J. Nick Koston
5b552b9ec5 Fix API connection sending ping too early after connection establishment (#8840) 2025-05-19 15:22:36 +12:00
Jesse Hills
d36ce7c010 [release] Don't wait for docker to be finished before deploying schema (#8838) 2025-05-19 14:17:01 +12:00
Jesse Hills
b8a96f59f0 [release] Fix output value (#8839) 2025-05-19 14:16:39 +12:00
Jesse Hills
2e15ee232d Deploy doxygen docs to netlify (#8837) 2025-05-19 14:09:38 +12:00
Jesse Hills
904495e1b8 Fix api doc homepage (#8836)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-19 13:57:41 +12:00
Fexiven
99c4f88c3f Update esp32-camera library version (#8832) 2025-05-19 11:01:31 +12:00
DanielV
87a9dd18c8 Improve stability for a test that crashes intermittently in CI (#8699)
Co-authored-by: NP v/d Spek <github_mail@lumensoft.nl>
2025-05-19 10:01:30 +12:00
Thomas Rupprecht
dbce54477a unify and add missing metric suffixes (#8816) 2025-05-18 21:44:33 +00:00
dependabot[bot]
660030d157 Bump docker/build-push-action from 6.16.0 to 6.17.0 in /.github/actions/build-image (#8810)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 09:20:29 +12:00
dependabot[bot]
24fbe602dd Bump codecov/codecov-action from 5.4.2 to 5.4.3 (#8820)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 09:13:20 +12:00
J. Nick Koston
b0c1e0e28c Refactor API frame helpers to enable buffer reuse (#8825) 2025-05-19 09:05:20 +12:00
J. Nick Koston
574aabdede Reduce number of calls to fetch time in the main loop (#8804) 2025-05-19 07:48:57 +12:00
J. Nick Koston
e47741d471 Fix ESP32 console logging corruption and message loss in multi-task (#8806) 2025-05-19 07:43:41 +12:00
Kent Gibson
a78bea78f9 Fix misspelling of climate in climate_ir.climate_ir_with_receiver_schema (#8829) 2025-05-18 03:45:12 +00:00
Clyde Stubbs
44470f31f6 Revert "[binary_sensor] initial state refactor" (#8828) 2025-05-18 03:30:08 +00:00
Anton Sergunov
18ac1b7c54 Fix the case of single error (#8824) 2025-05-18 15:11:09 +12:00
Keith Burzinski
e87b659483 [sen5x] Fix validation for values read from hardware (#8769) 2025-05-18 15:05:03 +12:00
J. Nick Koston
fefcb45e1f Bump cryptography to 45.0.1 (#8826) 2025-05-18 14:50:06 +12:00
J. Nick Koston
ab415eb3de stats 2025-05-17 17:05:49 -04:00
Clyde Stubbs
5c92367ca2 [script] Use local import for zephyr (#8822) 2025-05-16 23:41:19 +00:00
dependabot[bot]
b469a504e4 Bump cairosvg from 2.8.1 to 2.8.2 (#8817)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 15:42:51 -04:00
dependabot[bot]
218f8e0caf Bump ruff from 0.11.9 to 0.11.10 (#8818)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-16 15:42:44 -04:00
J. Nick Koston
7965558d5e Fix ESP32 Camera class inheritance (#8811) 2025-05-16 11:42:54 +12:00
dependabot[bot]
d9b860088e Bump setuptools from 80.4.0 to 80.7.1 (#8808)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-15 15:45:01 -05:00
dependabot[bot]
115975c409 Bump aioesphomeapi from 31.0.0 to 31.0.1 (#8809)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-15 15:44:47 -05:00
Thomas Rupprecht
4761ffe023 [gps] update lib, improve code/tests/config (#8768) 2025-05-15 22:07:41 +12:00
Thomas Rupprecht
88edddf07a [log] improve/refactor log (#8708) 2025-05-15 21:45:07 +12:00
J. Nick Koston
0b77cb1d16 Logger Recursion Guard per Task on ESP32 (#8765) 2025-05-15 21:36:28 +12:00
J. Nick Koston
efa6745a5e Optimize protobuf varint decoder for ESPHome use case (#8791) 2025-05-15 17:16:25 +12:00
J. Nick Koston
dd8d8ad952 Use fixed buffer for plaintext protocol like noise protocol (#8800) 2025-05-15 17:16:08 +12:00
dependabot[bot]
57284b1ac3 Bump cairosvg from 2.8.0 to 2.8.1 (#8799)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-14 23:26:28 -05:00
Jesse Hills
1a651ce66d Update some sensor schemas to be Optional (#8803) 2025-05-15 02:40:11 +00:00
Jesse Hills
730441c120 [api] Update api proto to add legacy value (#8802) 2025-05-14 21:26:21 -05:00
J. Nick Koston
bb1f24ab43 Avoid protobuf message construction when tx buffer is full (#8787)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-14 21:25:44 -05:00
NP v/d Spek
edb8d187be add actions to the MAX7219Component (#6462)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-05-15 09:15:04 +12:00
Jesse Hills
e7b6081c5c Merge branch 'beta' into dev 2025-05-15 06:51:16 +12:00
dependabot[bot]
5454500024 Bump cairosvg from 2.7.1 to 2.8.0 (#8780)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-14 11:20:22 -05:00
Jesse Hills
191afd3e69 Bump esphome-dashboard to 20250514.0 (#8790) 2025-05-14 02:08:16 -05:00
Jesse Hills
de27ce79dc [climate] Update components to use `climate_schema(...)` (#8788) 2025-05-14 05:36:21 +00:00
Jesse Hills
a12bd78ceb Fix release to pypi (#8789) 2025-05-14 16:35:30 +12:00
J. Nick Koston
ddb986b4fa Improve batching of BLE advertisements for better airtime efficiency (#8778) 2025-05-14 04:34:33 +00:00
Jesse Hills
c98c78e368 Merge branch 'beta' into dev 2025-05-14 15:55:25 +12:00
dependabot[bot]
5570a788fd Bump aioesphomeapi from 30.2.0 to 31.0.0 (#8779)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 21:23:32 -05:00
Jesse Hills
42c355e6d7 [fan] Update components to use `fan_schema(...)` (#8786) 2025-05-13 20:30:11 -05:00
Jesse Hills
a835ab48bc [schema] Get component name if available for deprecation warning (#8785) 2025-05-13 20:25:21 -05:00
Jesse Hills
f28a373898 [media_player] Deprecate `MEDIA_PLAYER_SCHEMA` (#8784) 2025-05-13 23:48:54 +00:00
Jesse Hills
28e29efd98 Bump version to 2025.6.0-dev 2025-05-14 09:54:26 +12:00
153 changed files with 4051 additions and 710 deletions

View File

@@ -1,2 +1,4 @@
[run]
omit = esphome/components/*
omit =
esphome/components/*
tests/integration/*

37
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
ARG BUILD_BASE_VERSION=2025.04.0
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
RUN git config --system --add safe.directory "*"
RUN apt update \
&& apt install -y \
protobuf-compiler
RUN pip install uv
RUN useradd esphome -m
USER esphome
ENV VIRTUAL_ENV=/home/esphome/.local/esphome-venv
RUN uv venv $VIRTUAL_ENV
ENV PATH="$VIRTUAL_ENV/bin:$PATH"
# Override this set to true in the docker-base image
ENV UV_SYSTEM_PYTHON=false
WORKDIR /tmp
COPY requirements.txt ./
RUN uv pip install -r requirements.txt
COPY requirements_dev.txt requirements_test.txt ./
RUN uv pip install -r requirements_dev.txt -r requirements_test.txt
RUN \
platformio settings set enable_telemetry No \
&& platformio settings set check_platformio_interval 1000000
COPY script/platformio_install_deps.py platformio.ini ./
RUN ./platformio_install_deps.py platformio.ini --libraries --platforms --tools
WORKDIR /workspaces

View File

@@ -1,18 +1,17 @@
{
"name": "ESPHome Dev",
"image": "ghcr.io/esphome/esphome-lint:dev",
"context": "..",
"dockerFile": "Dockerfile",
"postCreateCommand": [
"script/devcontainer-post-create"
],
"containerEnv": {
"DEVCONTAINER": "1",
"PIP_BREAK_SYSTEM_PACKAGES": "1",
"PIP_ROOT_USER_ACTION": "ignore"
"features": {
"ghcr.io/devcontainers/features/github-cli:1": {}
},
"runArgs": [
"--privileged",
"-e",
"ESPHOME_DASHBOARD_USE_PING=1"
"GIT_EDITOR=code --wait"
// uncomment and edit the path in order to pass though local USB serial to the conatiner
// , "--device=/dev/ttyACM0"
],

View File

@@ -21,7 +21,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Set up Python
uses: actions/setup-python@v5.6.0
with:

View File

@@ -43,11 +43,11 @@ jobs:
- "docker"
# - "lint"
steps:
- uses: actions/checkout@v4.1.7
- uses: actions/checkout@v4.2.2
- name: Set up Python
uses: actions/setup-python@v5.6.0
with:
python-version: "3.9"
python-version: "3.10"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0

View File

@@ -20,8 +20,8 @@ permissions:
contents: read
env:
DEFAULT_PYTHON: "3.9"
PYUPGRADE_TARGET: "--py39-plus"
DEFAULT_PYTHON: "3.10"
PYUPGRADE_TARGET: "--py310-plus"
concurrency:
# yamllint disable-line rule:line-length
@@ -36,7 +36,7 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt') }}" >> $GITHUB_OUTPUT
@@ -68,7 +68,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -89,7 +89,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -110,7 +110,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -131,7 +131,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -152,7 +152,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -173,10 +173,10 @@ jobs:
fail-fast: false
matrix:
python-version:
- "3.9"
- "3.10"
- "3.11"
- "3.12"
- "3.13"
os:
- ubuntu-latest
- macOS-latest
@@ -185,24 +185,24 @@ jobs:
# Minimize CI resource usage
# by only running the Python version
# version used for docker images on Windows and macOS
- python-version: "3.13"
os: windows-latest
- python-version: "3.12"
os: windows-latest
- python-version: "3.10"
os: windows-latest
- python-version: "3.9"
os: windows-latest
- python-version: "3.13"
os: macOS-latest
- python-version: "3.12"
os: macOS-latest
- python-version: "3.10"
os: macOS-latest
- python-version: "3.9"
os: macOS-latest
runs-on: ${{ matrix.os }}
needs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -214,14 +214,14 @@ jobs:
if: matrix.os == 'windows-latest'
run: |
./venv/Scripts/activate
pytest -vv --cov-report=xml --tb=native tests
pytest -vv --cov-report=xml --tb=native -n auto tests
- name: Run pytest
if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
run: |
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native tests
pytest -vv --cov-report=xml --tb=native -n auto tests
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5.4.2
uses: codecov/codecov-action@v5.4.3
with:
token: ${{ secrets.CODECOV_TOKEN }}
@@ -232,7 +232,7 @@ jobs:
- common
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -300,7 +300,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -356,7 +356,7 @@ jobs:
count: ${{ steps.list-components.outputs.count }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
with:
# Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
fetch-depth: 500
@@ -406,7 +406,7 @@ jobs:
sudo apt-get install libsdl2-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
@@ -432,7 +432,7 @@ jobs:
matrix: ${{ steps.split.outputs.components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Split components into 20 groups
id: split
run: |
@@ -462,7 +462,7 @@ jobs:
sudo apt-get install libsdl2-dev
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:

View File

@@ -20,7 +20,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@v4.1.7
- uses: actions/checkout@v4.2.2
- name: Get tag
id: tag
# yamllint disable rule:line-length
@@ -60,7 +60,7 @@ jobs:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4.1.7
- uses: actions/checkout@v4.2.2
- name: Set up Python
uses: actions/setup-python@v5.6.0
with:
@@ -92,11 +92,11 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@v4.1.7
- uses: actions/checkout@v4.2.2
- name: Set up Python
uses: actions/setup-python@v5.6.0
with:
python-version: "3.9"
python-version: "3.10"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
@@ -168,7 +168,7 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@v4.1.7
- uses: actions/checkout@v4.2.2
- name: Download digests
uses: actions/download-artifact@v4.3.0

View File

@@ -13,10 +13,10 @@ jobs:
if: github.repository == 'esphome/esphome'
steps:
- name: Checkout
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Checkout Home Assistant
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
with:
repository: home-assistant/core
path: lib/home-assistant
@@ -24,7 +24,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v5.6.0
with:
python-version: 3.12
python-version: 3.13
- name: Install Home Assistant
run: |

View File

@@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@v4.1.7
uses: actions/checkout@v4.2.2
- name: Run yamllint
uses: frenck/action-yamllint@v1.5.0
with:

View File

@@ -4,7 +4,7 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.9
rev: v0.11.10
hooks:
# Run the linter.
- id: ruff
@@ -28,10 +28,10 @@ repos:
- --branch=release
- --branch=beta
- repo: https://github.com/asottile/pyupgrade
rev: v3.15.2
rev: v3.20.0
hooks:
- id: pyupgrade
args: [--py39-plus]
args: [--py310-plus]
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.37.1
hooks:

View File

@@ -96,6 +96,7 @@ esphome/components/ch422g/* @clydebarrow @jesterret
esphome/components/chsc6x/* @kkosik20
esphome/components/climate/* @esphome/core
esphome/components/climate_ir/* @glmnet
esphome/components/cm1106/* @andrewjswan
esphome/components/color_temperature/* @jesserockz
esphome/components/combination/* @Cat-Ion @kahrendt
esphome/components/const/* @esphome/core
@@ -478,6 +479,8 @@ esphome/components/ufire_ise/* @pvizeli
esphome/components/ultrasonic/* @OttoWinter
esphome/components/update/* @jesserockz
esphome/components/uponor_smatrix/* @kroimon
esphome/components/usb_host/* @clydebarrow
esphome/components/usb_uart/* @clydebarrow
esphome/components/valve/* @esphome/core
esphome/components/vbus/* @ssieb
esphome/components/veml3235/* @kbx81

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 = 2025.5.0b4
PROJECT_NUMBER = 2025.6.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -432,7 +432,8 @@ message FanCommandRequest {
enum ColorMode {
COLOR_MODE_UNKNOWN = 0;
COLOR_MODE_ON_OFF = 1;
COLOR_MODE_BRIGHTNESS = 2;
COLOR_MODE_LEGACY_BRIGHTNESS = 2;
COLOR_MODE_BRIGHTNESS = 3;
COLOR_MODE_WHITE = 7;
COLOR_MODE_COLOR_TEMPERATURE = 11;
COLOR_MODE_COLD_WARM_WHITE = 19;

View File

@@ -3,12 +3,15 @@
#include <cerrno>
#include <cinttypes>
#include <utility>
#include <algorithm>
#include <map>
#include <string>
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/version.h"
#include "esphome/core/application.h"
#ifdef USE_DEEP_SLEEP
#include "esphome/components/deep_sleep/deep_sleep_component.h"
@@ -85,6 +88,9 @@ void APIConnection::start() {
// This ensures the first ping happens after the keepalive period
this->next_ping_retry_ = this->last_traffic_ + KEEPALIVE_TIMEOUT_MS;
// Pass stats collection to the helper for detailed timing
this->helper_->set_section_stats(&this->section_stats_);
APIError err = this->helper_->init();
if (err != APIError::OK) {
on_fatal_error();
@@ -111,6 +117,9 @@ APIConnection::~APIConnection() {
}
void APIConnection::loop() {
// Measure total time for entire loop function
const uint32_t loop_start_time = millis();
if (this->remove_)
return;
@@ -128,15 +137,30 @@ void APIConnection::loop() {
return;
}
const uint32_t now = millis();
uint32_t start_time;
uint32_t duration;
// Section: Helper Loop
start_time = millis();
APIError err = this->helper_->loop();
duration = millis() - start_time;
this->section_stats_["helper_loop"].record_time(duration);
if (err != APIError::OK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
api_error_to_str(err), errno);
return;
}
// Section: Read Packet
start_time = millis();
ReadPacketBuffer buffer;
err = this->helper_->read_packet(&buffer);
duration = millis() - start_time;
this->section_stats_["read_packet"].record_time(duration);
if (err == APIError::WOULD_BLOCK) {
// pass
} else if (err != APIError::OK) {
@@ -152,24 +176,42 @@ void APIConnection::loop() {
return;
} else {
this->last_traffic_ = App.get_loop_component_start_time();
// read a packet
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
// Section: Process Message
start_time = millis();
if (buffer.data_len > 0) {
this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
} else {
this->read_message(0, buffer.type, nullptr);
}
duration = millis() - start_time;
this->section_stats_["process_message"].record_time(duration);
if (this->remove_)
return;
}
// Section: Process Queue
start_time = millis();
if (!this->deferred_message_queue_.empty() && this->helper_->can_write_without_blocking()) {
this->deferred_message_queue_.process_queue();
}
duration = millis() - start_time;
this->section_stats_["process_queue"].record_time(duration);
// Section: Iterator Advance
start_time = millis();
if (!this->list_entities_iterator_.completed())
this->list_entities_iterator_.advance();
if (!this->initial_state_iterator_.completed() && this->list_entities_iterator_.completed())
this->initial_state_iterator_.advance();
duration = millis() - start_time;
this->section_stats_["iterator_advance"].record_time(duration);
// Section: Keepalive
start_time = millis();
static uint8_t max_ping_retries = 60;
static uint16_t ping_retry_interval = 1000;
const uint32_t now = App.get_loop_component_start_time();
if (this->sent_ping_) {
// Disconnect if not responded within 2.5*keepalive
if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) {
@@ -195,8 +237,12 @@ void APIConnection::loop() {
}
}
}
duration = millis() - start_time;
this->section_stats_["keepalive"].record_time(duration);
#ifdef USE_ESP32_CAMERA
// Section: Camera
start_time = millis();
if (this->image_reader_.available() && this->helper_->can_write_without_blocking()) {
// Message will use 8 more bytes than the minimum size, and typical
// MTU is 1500. Sometimes users will see as low as 1460 MTU.
@@ -235,8 +281,12 @@ void APIConnection::loop() {
this->image_reader_.return_image();
}
}
duration = millis() - start_time;
this->section_stats_["camera"].record_time(duration);
#endif
// Section: State Subscriptions
start_time = millis();
if (state_subs_at_ != -1) {
const auto &subs = this->parent_->get_state_subs();
if (state_subs_at_ >= (int) subs.size()) {
@@ -252,6 +302,24 @@ void APIConnection::loop() {
}
}
}
duration = millis() - start_time;
this->section_stats_["state_subs"].record_time(duration);
// Log stats periodically
if (this->stats_enabled_) {
// If next_stats_log_ is 0, initialize it
if (this->next_stats_log_ == 0) {
this->next_stats_log_ = now + this->stats_log_interval_;
} else if (now >= this->next_stats_log_) {
this->log_section_stats_();
this->reset_section_stats_();
this->next_stats_log_ = now + this->stats_log_interval_;
}
}
// Record total loop execution time
const uint32_t total_loop_duration = millis() - loop_start_time;
this->section_stats_["total_loop"].record_time(total_loop_duration);
}
std::string get_default_unique_id(const std::string &component_type, EntityBase *entity) {
@@ -1628,8 +1696,14 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
return false;
if (this->helper_->can_write_without_blocking())
return true;
// Track try_to_clear_buffer time
const uint32_t start_time = millis();
delay(0);
APIError err = this->helper_->loop();
const uint32_t duration = millis() - start_time;
this->section_stats_["try_to_clear_buffer"].record_time(duration);
if (err != APIError::OK) {
on_fatal_error();
ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(),
@@ -1644,11 +1718,17 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
return false;
}
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type) {
// Track send_buffer time
const uint32_t start_time = millis();
if (!this->try_to_clear_buffer(message_type != 29)) { // SubscribeLogsResponse
return false;
}
uint32_t write_start = millis();
APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
uint32_t write_duration = millis() - write_start;
this->section_stats_["write_packet"].record_time(write_duration);
if (err == APIError::WOULD_BLOCK)
return false;
if (err != APIError::OK) {
@@ -1661,6 +1741,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint32_t message_type)
}
return false;
}
// Measure total send_buffer function time
uint32_t total_duration = millis() - start_time;
this->section_stats_["send_buffer_total"].record_time(total_duration);
// Do not set last_traffic_ on send
return true;
}
@@ -1677,6 +1762,90 @@ void APIConnection::on_fatal_error() {
this->remove_ = true;
}
void APIConnection::log_section_stats_() {
const char *STATS_TAG = "api.stats";
ESP_LOGI(STATS_TAG, "Logging API section stats now (current time: %" PRIu32 ", scheduled time: %" PRIu32 ")",
millis(), this->next_stats_log_);
ESP_LOGI(STATS_TAG, "Stats collection status: enabled=%d, sections=%zu", this->stats_enabled_,
this->section_stats_.size());
// Check if we have minimal data
bool has_data = false;
for (const auto &it : this->section_stats_) {
if (it.second.get_period_count() > 0) {
has_data = true;
break;
}
}
if (has_data) {
size_t helper_count = 0;
size_t read_count = 0;
size_t total_count = 0;
if (this->section_stats_.count("helper_loop") > 0)
helper_count = this->section_stats_["helper_loop"].get_period_count();
if (this->section_stats_.count("read_packet") > 0)
read_count = this->section_stats_["read_packet"].get_period_count();
if (this->section_stats_.count("total_loop") > 0)
total_count = this->section_stats_["total_loop"].get_period_count();
ESP_LOGI(STATS_TAG, "Record count for key sections: helper_loop=%zu, read_packet=%zu, total_loop=%zu", helper_count,
read_count, total_count);
}
ESP_LOGI(STATS_TAG, "API Connection Section Runtime Statistics");
ESP_LOGI(STATS_TAG, "Period stats (last %" PRIu32 "ms):", this->stats_log_interval_);
// First collect stats we want to display
std::vector<std::pair<std::string, const APISectionStats *>> stats_to_display;
for (const auto &it : this->section_stats_) {
const APISectionStats &stats = it.second;
if (stats.get_period_count() > 0) {
stats_to_display.push_back({it.first, &stats});
}
}
// Sort by period runtime (descending)
std::sort(stats_to_display.begin(), stats_to_display.end(), [](const auto &a, const auto &b) {
return a.second->get_period_time_ms() > b.second->get_period_time_ms();
});
// Log top components by period runtime
for (const auto &it : stats_to_display) {
const std::string &section = it.first;
const APISectionStats *stats = it.second;
ESP_LOGI(STATS_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", section.c_str(),
stats->get_period_count(), stats->get_period_avg_time_ms(), stats->get_period_max_time_ms(),
stats->get_period_time_ms());
}
// Log total stats since boot
ESP_LOGI(STATS_TAG, "Total stats (since boot):");
// Re-sort by total runtime for all-time stats
std::sort(stats_to_display.begin(), stats_to_display.end(),
[](const auto &a, const auto &b) { return a.second->get_total_time_ms() > b.second->get_total_time_ms(); });
for (const auto &it : stats_to_display) {
const std::string &section = it.first;
const APISectionStats *stats = it.second;
ESP_LOGI(STATS_TAG, " %s: count=%" PRIu32 ", avg=%.2fms, max=%" PRIu32 "ms, total=%" PRIu32 "ms", section.c_str(),
stats->get_total_count(), stats->get_total_avg_time_ms(), stats->get_total_max_time_ms(),
stats->get_total_time_ms());
}
ESP_LOGD(STATS_TAG, "Resetting API section stats, sections count: %zu", this->section_stats_.size());
}
void APIConnection::reset_section_stats_() {
for (auto &it : this->section_stats_) {
it.second.reset_period_stats();
}
}
} // namespace api
} // namespace esphome
#endif

View File

@@ -9,8 +9,12 @@
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <vector>
#include <map>
#include <string>
namespace esphome {
namespace api {
@@ -64,6 +68,9 @@ class APIConnection : public APIServerConnection {
APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent);
virtual ~APIConnection();
// Use the APISectionStats from api_frame_helper.h to avoid duplication
using APISectionStats = ::esphome::api::APISectionStats;
void start();
void loop();
@@ -556,6 +563,14 @@ class APIConnection : public APIServerConnection {
InitialStateIterator initial_state_iterator_;
ListEntitiesIterator list_entities_iterator_;
int state_subs_at_ = -1;
// API loop section performance statistics
std::map<std::string, APISectionStats> section_stats_;
uint32_t stats_log_interval_{60000}; // 60 seconds default
uint32_t next_stats_log_{0};
bool stats_enabled_{true};
void log_section_stats_();
void reset_section_stats_();
};
} // namespace api

View File

@@ -7,20 +7,13 @@
#include "proto.h"
#include "api_pb2_size.h"
#include <cstring>
#include <cinttypes>
namespace esphome {
namespace api {
static const char *const TAG = "api.socket";
/// Is the given return value (from write syscalls) a wouldblock error?
bool is_would_block(ssize_t ret) {
if (ret == -1) {
return errno == EWOULDBLOCK || errno == EAGAIN;
}
return ret == 0;
}
const char *api_error_to_str(APIError err) {
// not using switch to ensure compiler doesn't try to build a big table out of it
if (err == APIError::OK) {
@@ -73,92 +66,164 @@ const char *api_error_to_str(APIError err) {
return "UNKNOWN";
}
// Common implementation for writing raw data to socket
template<typename StateEnum>
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket,
std::vector<uint8_t> &tx_buf, const std::string &info, StateEnum &state,
StateEnum failed_state) {
// This method writes data to socket or buffers it
// Helper method to buffer data from IOVs
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len) {
SendBuffer buffer;
buffer.data.reserve(total_write_len);
for (int i = 0; i < iovcnt; i++) {
const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base);
buffer.data.insert(buffer.data.end(), data, data + iov[i].iov_len);
}
this->tx_buf_.push_back(std::move(buffer));
}
// This method writes data to socket or buffers it
APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt) {
// Returns APIError::OK if successful (or would block, but data has been buffered)
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to failed_state
// Returns APIError::SOCKET_WRITE_FAILED if socket write failed, and sets state to FAILED
if (iovcnt == 0)
return APIError::OK; // Nothing to do, success
size_t total_write_len = 0;
uint16_t total_write_len = 0;
for (int i = 0; i < iovcnt; i++) {
#ifdef HELPER_LOG_PACKETS
ESP_LOGVV(TAG, "Sending raw: %s",
format_hex_pretty(reinterpret_cast<uint8_t *>(iov[i].iov_base), iov[i].iov_len).c_str());
#endif
total_write_len += iov[i].iov_len;
total_write_len += static_cast<uint16_t>(iov[i].iov_len);
}
if (!tx_buf.empty()) {
// try to empty tx_buf first
while (!tx_buf.empty()) {
ssize_t sent = socket->write(tx_buf.data(), tx_buf.size());
if (is_would_block(sent)) {
break;
} else if (sent == -1) {
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
state = failed_state;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf.erase(tx_buf.begin(), tx_buf.begin() + sent);
// Try to send any existing buffered data first if there is any
if (!this->tx_buf_.empty()) {
APIError send_result = try_send_tx_buf_();
// If real error occurred (not just WOULD_BLOCK), return it
if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
return send_result;
}
// If there is still data in the buffer, we can't send, buffer
// the new data and return
if (!this->tx_buf_.empty()) {
this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
return APIError::OK; // Success, data buffered
}
}
if (!tx_buf.empty()) {
// tx buf not empty, can't write now because then stream would be inconsistent
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + total_write_len);
for (int i = 0; i < iovcnt; i++) {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
}
return APIError::OK; // Success, data buffered
// Try to send directly if no buffered data
uint32_t write_start = millis();
ssize_t sent = this->socket_->writev(iov, iovcnt);
uint32_t write_duration = millis() - write_start;
if (write_duration > 0 && section_stats_) {
(*section_stats_)["write_packet.socket_writev"].record_time(write_duration);
}
ssize_t sent = socket->writev(iov, iovcnt);
if (is_would_block(sent)) {
// operation would block, add buffer to tx_buf
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + total_write_len);
for (int i = 0; i < iovcnt; i++) {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base),
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
if (sent == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
// Socket would block, buffer the data
this->buffer_data_from_iov_(iov, iovcnt, total_write_len);
return APIError::OK; // Success, data buffered
}
return APIError::OK; // Success, data buffered
} else if (sent == -1) {
// an error occurred
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", info.c_str(), errno);
state = failed_state;
// Socket error
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
} else if ((size_t) sent != total_write_len) {
// partially sent, add end to tx_buf
size_t remaining = total_write_len - sent;
// Reserve space upfront to avoid multiple reallocations
tx_buf.reserve(tx_buf.size() + remaining);
} else if (static_cast<uint16_t>(sent) < total_write_len) {
// Partially sent, buffer the remaining data
SendBuffer buffer;
uint16_t to_consume = static_cast<uint16_t>(sent);
uint16_t remaining = total_write_len - static_cast<uint16_t>(sent);
buffer.data.reserve(remaining);
size_t to_consume = sent;
for (int i = 0; i < iovcnt; i++) {
if (to_consume >= iov[i].iov_len) {
to_consume -= iov[i].iov_len;
// This segment was fully sent
to_consume -= static_cast<uint16_t>(iov[i].iov_len);
} else {
tx_buf.insert(tx_buf.end(), reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume,
reinterpret_cast<uint8_t *>(iov[i].iov_base) + iov[i].iov_len);
// This segment was partially sent or not sent at all
const uint8_t *data = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_consume;
uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_consume;
buffer.data.insert(buffer.data.end(), data, data + len);
to_consume = 0;
}
}
return APIError::OK; // Success, data buffered
this->tx_buf_.push_back(std::move(buffer));
}
return APIError::OK; // Success, all data sent
return APIError::OK; // Success, all data sent or buffered
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, info_.c_str(), ##__VA_ARGS__)
// Common implementation for trying to send buffered data
// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method
APIError APIFrameHelper::try_send_tx_buf_() {
// Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
bool tx_buf_empty = false;
while (!tx_buf_empty) {
// Get the first buffer in the queue
SendBuffer &front_buffer = this->tx_buf_.front();
// Try to send the remaining data in this buffer
uint32_t write_start = millis();
ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
uint32_t write_duration = millis() - write_start;
if (write_duration > 0 && section_stats_) {
(*section_stats_)["send_buffer_total.socket_write"].record_time(write_duration);
}
if (sent == -1) {
if (errno != EWOULDBLOCK && errno != EAGAIN) {
// Real socket error (not just would block)
ESP_LOGVV(TAG, "%s: Socket write failed with errno %d", this->info_.c_str(), errno);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED; // Socket write failed
}
// Socket would block, we'll try again later
return APIError::WOULD_BLOCK;
} else if (sent == 0) {
// Nothing sent but not an error
return APIError::WOULD_BLOCK;
} else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) {
// Partially sent, update offset
// Cast to ensure no overflow issues with uint16_t
front_buffer.offset += static_cast<uint16_t>(sent);
return APIError::WOULD_BLOCK; // Stop processing more buffers if we couldn't send a complete buffer
} else {
// Buffer completely sent, remove it from the queue
this->tx_buf_.pop_front();
// Update empty status for the loop condition
tx_buf_empty = this->tx_buf_.empty();
// Continue loop to try sending the next buffer
}
}
return APIError::OK; // All buffers sent successfully
}
APIError APIFrameHelper::init_common_() {
if (state_ != State::INITIALIZE || this->socket_ == nullptr) {
ESP_LOGVV(TAG, "%s: Bad state for init %d", this->info_.c_str(), (int) state_);
return APIError::BAD_STATE;
}
int err = this->socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
ESP_LOGVV(TAG, "%s: Setting nonblocking failed with errno %d", this->info_.c_str(), errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
ESP_LOGVV(TAG, "%s: Setting nodelay failed with errno %d", this->info_.c_str(), errno);
return APIError::TCP_NODELAY_FAILED;
}
return APIError::OK;
}
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->info_.c_str(), ##__VA_ARGS__)
// uncomment to log raw packets
//#define HELPER_LOG_PACKETS
@@ -206,23 +271,9 @@ std::string noise_err_to_str(int err) {
/// Initialize the frame helper, returns OK if successful.
APIError APINoiseFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
int err = socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED;
APIError err = init_common_();
if (err != APIError::OK) {
return err;
}
// init prologue
@@ -234,17 +285,16 @@ APIError APINoiseFrameHelper::init() {
/// Run through handshake messages (if in that phase)
APIError APINoiseFrameHelper::loop() {
APIError err = state_action_();
if (err == APIError::WOULD_BLOCK)
return APIError::OK;
if (err != APIError::OK)
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
if (!tx_buf_.empty()) {
}
if (!this->tx_buf_.empty()) {
err = try_send_tx_buf_();
if (err != APIError::OK) {
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
}
return APIError::OK;
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@@ -270,8 +320,13 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
// read header
if (rx_header_buf_len_ < 3) {
// no header information yet
size_t to_read = 3 - rx_header_buf_len_;
ssize_t received = socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
uint8_t to_read = 3 - rx_header_buf_len_;
uint32_t socket_start = millis();
ssize_t received = this->socket_->read(&rx_header_buf_[rx_header_buf_len_], to_read);
uint32_t socket_duration = millis() - socket_start;
if (socket_duration > 0 && section_stats_) {
(*section_stats_)["read_packet.socket_read_header"].record_time(socket_duration);
}
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
@@ -284,8 +339,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
}
rx_header_buf_len_ += received;
if ((size_t) received != to_read) {
rx_header_buf_len_ += static_cast<uint8_t>(received);
if (static_cast<uint8_t>(received) != to_read) {
// not a full read
return APIError::WOULD_BLOCK;
}
@@ -312,13 +367,23 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
// reserve space for body
if (rx_buf_.size() != msg_size) {
uint32_t resize_start = millis();
rx_buf_.resize(msg_size);
uint32_t resize_duration = millis() - resize_start;
if (resize_duration > 0 && section_stats_) {
(*section_stats_)["read_packet.buffer_resize"].record_time(resize_duration);
}
}
if (rx_buf_len_ < msg_size) {
// more data to read
size_t to_read = msg_size - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
uint16_t to_read = msg_size - rx_buf_len_;
uint32_t socket_start = millis();
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
uint32_t socket_duration = millis() - socket_start;
if (socket_duration > 0 && section_stats_) {
(*section_stats_)["read_packet.socket_read_body"].record_time(socket_duration);
}
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
@@ -331,8 +396,8 @@ APIError APINoiseFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
}
rx_buf_len_ += received;
if ((size_t) received != to_read) {
rx_buf_len_ += static_cast<uint16_t>(received);
if (static_cast<uint16_t>(received) != to_read) {
// not all read
return APIError::WOULD_BLOCK;
}
@@ -381,6 +446,8 @@ APIError APINoiseFrameHelper::state_action_() {
if (aerr != APIError::OK)
return aerr;
// ignore contents, may be used in future for flags
// Reserve space for: existing prologue + 2 size bytes + frame data
prologue_.reserve(prologue_.size() + 2 + frame.msg.size());
prologue_.push_back((uint8_t) (frame.msg.size() >> 8));
prologue_.push_back((uint8_t) frame.msg.size());
prologue_.insert(prologue_.end(), frame.msg.begin(), frame.msg.end());
@@ -389,16 +456,20 @@ APIError APINoiseFrameHelper::state_action_() {
}
if (state_ == State::SERVER_HELLO) {
// send server hello
const std::string &name = App.get_name();
const std::string &mac = get_mac_address();
std::vector<uint8_t> msg;
// Reserve space for: 1 byte proto + name + null + mac + null
msg.reserve(1 + name.size() + 1 + mac.size() + 1);
// chosen proto
msg.push_back(0x01);
// node name, terminated by null byte
const std::string &name = App.get_name();
const uint8_t *name_ptr = reinterpret_cast<const uint8_t *>(name.c_str());
msg.insert(msg.end(), name_ptr, name_ptr + name.size() + 1);
// node mac, terminated by null byte
const std::string &mac = get_mac_address();
const uint8_t *mac_ptr = reinterpret_cast<const uint8_t *>(mac.c_str());
msg.insert(msg.end(), mac_ptr, mac_ptr + mac.size() + 1);
@@ -505,11 +576,18 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const std::string &rea
write_frame_(data.data(), data.size());
state_ = orig_state;
}
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
int err;
APIError aerr;
uint32_t start_time, duration;
// Track state_action timing
start_time = millis();
aerr = state_action_();
duration = millis() - start_time;
if (duration > 0 && section_stats_) {
(*section_stats_)["read_packet.state_action"].record_time(duration);
}
if (aerr != APIError::OK) {
return aerr;
}
@@ -518,22 +596,34 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
return APIError::WOULD_BLOCK;
}
// Track frame reading timing
start_time = millis();
ParsedFrame frame;
aerr = try_read_frame_(&frame);
duration = millis() - start_time;
if (duration > 0 && section_stats_) {
(*section_stats_)["read_packet.try_read_frame"].record_time(duration);
}
if (aerr != APIError::OK)
return aerr;
// Track decryption timing
start_time = millis();
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_inout(mbuf, frame.msg.data(), frame.msg.size(), frame.msg.size());
err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
duration = millis() - start_time;
if (duration > 0 && section_stats_) {
(*section_stats_)["read_packet.decrypt"].record_time(duration);
}
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("noise_cipherstate_decrypt failed: %s", noise_err_to_str(err).c_str());
return APIError::CIPHERSTATE_DECRYPT_FAILED;
}
size_t msg_size = mbuf.size;
uint16_t msg_size = mbuf.size;
uint8_t *msg_data = frame.msg.data();
if (msg_size < 4) {
state_ = State::FAILED;
@@ -559,7 +649,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type;
return APIError::OK;
}
bool APINoiseFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
int err;
APIError aerr;
@@ -574,9 +663,9 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
// Message data starts after padding
size_t payload_len = raw_buffer->size() - frame_header_padding_;
size_t padding = 0;
size_t msg_len = 4 + payload_len + padding;
uint16_t payload_len = raw_buffer->size() - frame_header_padding_;
uint16_t padding = 0;
uint16_t msg_len = 4 + payload_len + padding;
// We need to resize to include MAC space, but we already reserved it in create_buffer
raw_buffer->resize(raw_buffer->size() + frame_footer_size_);
@@ -609,7 +698,7 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
return APIError::CIPHERSTATE_ENCRYPT_FAILED;
}
size_t total_len = 3 + mbuf.size;
uint16_t total_len = 3 + mbuf.size;
buf_start[1] = (uint8_t) (mbuf.size >> 8);
buf_start[2] = (uint8_t) mbuf.size;
@@ -620,29 +709,9 @@ APIError APINoiseFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuf
iov.iov_len = total_len;
// write raw to not have two packets sent if NAGLE disabled
return write_raw_(&iov, 1);
return this->write_raw_(&iov, 1);
}
APIError APINoiseFrameHelper::try_send_tx_buf_() {
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (sent == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN)
break;
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
} else if (sent == 0) {
break;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
}
return APIError::OK;
}
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, uint16_t len) {
uint8_t header[3];
header[0] = 0x01; // indicator
header[1] = (uint8_t) (len >> 8);
@@ -652,12 +721,12 @@ APIError APINoiseFrameHelper::write_frame_(const uint8_t *data, size_t len) {
iov[0].iov_base = header;
iov[0].iov_len = 3;
if (len == 0) {
return write_raw_(iov, 1);
return this->write_raw_(iov, 1);
}
iov[1].iov_base = const_cast<uint8_t *>(data);
iov[1].iov_len = len;
return write_raw_(iov, 2);
return this->write_raw_(iov, 2);
}
/** Initiate the data structures for the handshake.
@@ -752,22 +821,6 @@ APINoiseFrameHelper::~APINoiseFrameHelper() {
}
}
APIError APINoiseFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APINoiseFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
extern "C" {
// declare how noise generates random bytes (here with a good HWRNG based on the RF system)
void noise_rand_bytes(void *output, size_t len) {
@@ -778,32 +831,15 @@ void noise_rand_bytes(void *output, size_t len) {
}
}
// Explicit template instantiation for Noise
template APIError APIFrameHelper::write_raw_<APINoiseFrameHelper::State>(
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
APINoiseFrameHelper::State &state, APINoiseFrameHelper::State failed_state);
#endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT
/// Initialize the frame helper, returns OK if successful.
APIError APIPlaintextFrameHelper::init() {
if (state_ != State::INITIALIZE || socket_ == nullptr) {
HELPER_LOG("Bad state for init %d", (int) state_);
return APIError::BAD_STATE;
}
int err = socket_->setblocking(false);
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nonblocking failed with errno %d", errno);
return APIError::TCP_NONBLOCKING_FAILED;
}
int enable = 1;
err = socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
state_ = State::FAILED;
HELPER_LOG("Setting nodelay failed with errno %d", errno);
return APIError::TCP_NODELAY_FAILED;
APIError err = init_common_();
if (err != APIError::OK) {
return err;
}
state_ = State::DATA;
@@ -814,14 +850,13 @@ APIError APIPlaintextFrameHelper::loop() {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
}
// try send pending TX data
if (!tx_buf_.empty()) {
if (!this->tx_buf_.empty()) {
APIError err = try_send_tx_buf_();
if (err != APIError::OK) {
if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
return err;
}
}
return APIError::OK;
return APIError::OK; // Convert WOULD_BLOCK to OK to avoid connection termination
}
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
@@ -846,7 +881,12 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// there is no data on the wire (which is the common case).
// This results in faster failure detection compared to
// attempting to read multiple bytes at once.
ssize_t received = socket_->read(&data, 1);
uint32_t socket_start = millis();
ssize_t received = this->socket_->read(&data, 1);
uint32_t socket_duration = millis() - socket_start;
if (socket_duration > 0 && section_stats_) {
(*section_stats_)["read_packet.socket_read_header"].record_time(socket_duration);
}
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
@@ -897,7 +937,7 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
// - At least 2 bytes in the buffer for the varints
// Buffer layout:
// First 1-3 bytes: Message size varint (variable length)
// - 2 bytes would only allow up to 16383, which is less than noise's 65535
// - 2 bytes would only allow up to 16383, which is less than noise's UINT16_MAX (65535)
// - 3 bytes allows up to 2097151, ensuring we support at least as much as noise
// Remaining 1-2 bytes: Message type varint (variable length)
// We now attempt to parse both varints. If either is incomplete,
@@ -910,27 +950,49 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
continue;
}
rx_header_parsed_len_ = msg_size_varint->as_uint32();
if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_len_ = msg_size_varint->as_uint16();
auto msg_type_varint = ProtoVarInt::parse(&rx_header_buf_[consumed], rx_header_buf_pos_ - 1 - consumed, &consumed);
if (!msg_type_varint.has_value()) {
// not enough data there yet
continue;
}
rx_header_parsed_type_ = msg_type_varint->as_uint32();
if (msg_type_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
state_ = State::FAILED;
HELPER_LOG("Bad packet: message type %" PRIu32 " exceeds maximum %u", msg_type_varint->as_uint32(),
std::numeric_limits<uint16_t>::max());
return APIError::BAD_DATA_PACKET;
}
rx_header_parsed_type_ = msg_type_varint->as_uint16();
rx_header_parsed_ = true;
}
// header reading done
// reserve space for body
if (rx_buf_.size() != rx_header_parsed_len_) {
uint32_t resize_start = millis();
rx_buf_.resize(rx_header_parsed_len_);
uint32_t resize_duration = millis() - resize_start;
if (resize_duration > 0 && section_stats_) {
(*section_stats_)["read_packet.buffer_resize"].record_time(resize_duration);
}
}
if (rx_buf_len_ < rx_header_parsed_len_) {
// more data to read
size_t to_read = rx_header_parsed_len_ - rx_buf_len_;
ssize_t received = socket_->read(&rx_buf_[rx_buf_len_], to_read);
uint16_t to_read = rx_header_parsed_len_ - rx_buf_len_;
uint32_t socket_start = millis();
ssize_t received = this->socket_->read(&rx_buf_[rx_buf_len_], to_read);
uint32_t socket_duration = millis() - socket_start;
if (socket_duration > 0 && section_stats_) {
(*section_stats_)["read_packet.socket_read_body"].record_time(socket_duration);
}
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
return APIError::WOULD_BLOCK;
@@ -943,8 +1005,8 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
HELPER_LOG("Connection closed");
return APIError::CONNECTION_CLOSED;
}
rx_buf_len_ += received;
if ((size_t) received != to_read) {
rx_buf_len_ += static_cast<uint16_t>(received);
if (static_cast<uint16_t>(received) != to_read) {
// not all read
return APIError::WOULD_BLOCK;
}
@@ -962,16 +1024,22 @@ APIError APIPlaintextFrameHelper::try_read_frame_(ParsedFrame *frame) {
rx_header_parsed_ = false;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
APIError aerr;
uint32_t start_time, duration;
if (state_ != State::DATA) {
return APIError::WOULD_BLOCK;
}
// Track frame reading timing
start_time = millis();
ParsedFrame frame;
aerr = try_read_frame_(&frame);
duration = millis() - start_time;
if (duration > 0 && section_stats_) {
(*section_stats_)["read_packet.try_read_frame"].record_time(duration);
}
if (aerr != APIError::OK) {
if (aerr == APIError::BAD_INDICATOR) {
// Make sure to tell the remote that we don't
@@ -990,7 +1058,7 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
"Bad indicator byte";
iov[0].iov_base = (void *) msg;
iov[0].iov_len = 19;
write_raw_(iov, 1);
this->write_raw_(iov, 1);
}
return aerr;
}
@@ -1001,7 +1069,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = rx_header_parsed_type_;
return APIError::OK;
}
bool APIPlaintextFrameHelper::can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) {
if (state_ != State::DATA) {
return APIError::BAD_STATE;
@@ -1009,12 +1076,12 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWrit
std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
// Message data starts after padding (frame_header_padding_ = 6)
size_t payload_len = raw_buffer->size() - frame_header_padding_;
uint16_t payload_len = static_cast<uint16_t>(raw_buffer->size() - frame_header_padding_);
// Calculate varint sizes for header components
size_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
size_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
size_t total_header_len = 1 + size_varint_len + type_varint_len;
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(payload_len));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(type));
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
if (total_header_len > frame_header_padding_) {
// Header is too large to fit in the padding
@@ -1044,7 +1111,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWrit
// [4-5] - Message type varint (2 bytes, for types 128-32767)
// [6...] - Actual payload data
uint8_t *buf_start = raw_buffer->data();
size_t header_offset = frame_header_padding_ - total_header_len;
uint8_t header_offset = frame_header_padding_ - total_header_len;
// Write the plaintext header
buf_start[header_offset] = 0x00; // indicator
@@ -1063,46 +1130,7 @@ APIError APIPlaintextFrameHelper::write_protobuf_packet(uint16_t type, ProtoWrit
return write_raw_(&iov, 1);
}
APIError APIPlaintextFrameHelper::try_send_tx_buf_() {
// try send from tx_buf
while (state_ != State::CLOSED && !tx_buf_.empty()) {
ssize_t sent = socket_->write(tx_buf_.data(), tx_buf_.size());
if (is_would_block(sent)) {
break;
} else if (sent == -1) {
state_ = State::FAILED;
HELPER_LOG("Socket write failed with errno %d", errno);
return APIError::SOCKET_WRITE_FAILED;
}
// TODO: inefficient if multiple packets in txbuf
// replace with deque of buffers
tx_buf_.erase(tx_buf_.begin(), tx_buf_.begin() + sent);
}
return APIError::OK;
}
APIError APIPlaintextFrameHelper::close() {
state_ = State::CLOSED;
int err = socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::shutdown(int how) {
int err = socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
// Explicit template instantiation for Plaintext
template APIError APIFrameHelper::write_raw_<APIPlaintextFrameHelper::State>(
const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf_, const std::string &info,
APIPlaintextFrameHelper::State &state, APIPlaintextFrameHelper::State failed_state);
#endif // USE_API_PLAINTEXT
} // namespace api

View File

@@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include <deque>
#include <limits>
#include <utility>
#include <vector>
@@ -12,24 +13,78 @@
#include "api_noise_context.h"
#include "esphome/components/socket/socket.h"
#include <map>
#include <string>
namespace esphome {
namespace api {
// Forward declaration from api_connection.h
class APIConnection;
// Stats class definition (copied from api_connection.h to avoid circular dependency)
class APISectionStats {
public:
APISectionStats()
: period_count_(0),
total_count_(0),
period_time_ms_(0),
total_time_ms_(0),
period_max_time_ms_(0),
total_max_time_ms_(0) {}
void record_time(uint32_t duration_ms) {
// Update period counters
this->period_count_++;
this->period_time_ms_ += duration_ms;
if (duration_ms > this->period_max_time_ms_)
this->period_max_time_ms_ = duration_ms;
// Update total counters
this->total_count_++;
this->total_time_ms_ += duration_ms;
if (duration_ms > this->total_max_time_ms_)
this->total_max_time_ms_ = duration_ms;
}
void reset_period_stats() {
this->period_count_ = 0;
this->period_time_ms_ = 0;
this->period_max_time_ms_ = 0;
}
// Getters for period stats
uint32_t get_period_count() const { return this->period_count_; }
uint32_t get_period_time_ms() const { return this->period_time_ms_; }
uint32_t get_period_max_time_ms() const { return this->period_max_time_ms_; }
float get_period_avg_time_ms() const {
return this->period_count_ > 0 ? static_cast<float>(this->period_time_ms_) / this->period_count_ : 0.0f;
}
// Getters for total stats
uint32_t get_total_count() const { return this->total_count_; }
uint32_t get_total_time_ms() const { return this->total_time_ms_; }
uint32_t get_total_max_time_ms() const { return this->total_max_time_ms_; }
float get_total_avg_time_ms() const {
return this->total_count_ > 0 ? static_cast<float>(this->total_time_ms_) / this->total_count_ : 0.0f;
}
private:
uint32_t period_count_;
uint32_t total_count_;
uint32_t period_time_ms_;
uint32_t total_time_ms_;
uint32_t period_max_time_ms_;
uint32_t total_max_time_ms_;
};
class ProtoWriteBuffer;
struct ReadPacketBuffer {
std::vector<uint8_t> container;
uint16_t type;
size_t data_offset;
size_t data_len;
};
struct PacketBuffer {
const std::vector<uint8_t> container;
uint16_t type;
uint8_t data_offset;
uint8_t data_len;
uint16_t data_offset;
uint16_t data_len;
};
enum class APIError : int {
@@ -62,38 +117,122 @@ const char *api_error_to_str(APIError err);
class APIFrameHelper {
public:
APIFrameHelper() = default;
explicit APIFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_owned_(std::move(socket)) {
socket_ = socket_owned_.get();
}
virtual ~APIFrameHelper() = default;
virtual APIError init() = 0;
virtual APIError loop() = 0;
virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
virtual bool can_write_without_blocking() = 0;
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
virtual std::string getpeername() = 0;
virtual int getpeername(struct sockaddr *addr, socklen_t *addrlen) = 0;
virtual APIError close() = 0;
virtual APIError shutdown(int how) = 0;
bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
std::string getpeername() { return socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
APIError close() {
state_ = State::CLOSED;
int err = this->socket_->close();
if (err == -1)
return APIError::CLOSE_FAILED;
return APIError::OK;
}
APIError shutdown(int how) {
int err = this->socket_->shutdown(how);
if (err == -1)
return APIError::SHUTDOWN_FAILED;
if (how == SHUT_RDWR) {
state_ = State::CLOSED;
}
return APIError::OK;
}
// Give this helper a name for logging
virtual void set_log_info(std::string info) = 0;
void set_log_info(std::string info) { info_ = std::move(info); }
// Set stats collection for detailed timing
void set_section_stats(std::map<std::string, APISectionStats> *stats) { section_stats_ = stats; }
virtual APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) = 0;
// Get the frame header padding required by this protocol
virtual uint8_t frame_header_padding() = 0;
// Get the frame footer size required by this protocol
virtual uint8_t frame_footer_size() = 0;
protected:
// Struct for holding parsed frame data
struct ParsedFrame {
std::vector<uint8_t> msg;
};
// Buffer containing data to be sent
struct SendBuffer {
std::vector<uint8_t> data;
uint16_t offset{0}; // Current offset within the buffer (uint16_t to reduce memory usage)
// Using uint16_t reduces memory usage since ESPHome API messages are limited to UINT16_MAX (65535) bytes
uint16_t remaining() const { return static_cast<uint16_t>(data.size()) - offset; }
const uint8_t *current_data() const { return data.data() + offset; }
};
// Queue of data buffers to be sent
std::deque<SendBuffer> tx_buf_;
// Common state enum for all frame helpers
// Note: Not all states are used by all implementations
// - INITIALIZE: Used by both Noise and Plaintext
// - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol
// - DATA: Used by both Noise and Plaintext
// - CLOSED: Used by both Noise and Plaintext
// - FAILED: Used by both Noise and Plaintext
// - EXPLICIT_REJECT: Only used by Noise protocol
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2, // Noise only
SERVER_HELLO = 3, // Noise only
HANDSHAKE = 4, // Noise only
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8, // Noise only
};
// Current state of the frame helper
State state_{State::INITIALIZE};
// Helper name for logging
std::string info_;
// Socket for communication
socket::Socket *socket_{nullptr};
std::unique_ptr<socket::Socket> socket_owned_;
// Common implementation for writing raw data to socket
APIError write_raw_(const struct iovec *iov, int iovcnt);
// Try to send data from the tx buffer
APIError try_send_tx_buf_();
// Helper method to buffer data from IOVs
void buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len);
template<typename StateEnum>
APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf,
const std::string &info, StateEnum &state, StateEnum failed_state);
uint8_t frame_header_padding_{0};
uint8_t frame_footer_size_{0};
// Receive buffer for reading frame data
std::vector<uint8_t> rx_buf_;
uint16_t rx_buf_len_ = 0;
// Common initialization for both plaintext and noise protocols
APIError init_common_();
// Stats collection pointer - shared from APIConnection
std::map<std::string, APISectionStats> *section_stats_{nullptr};
};
#ifdef USE_API_NOISE
class APINoiseFrameHelper : public APIFrameHelper {
public:
APINoiseFrameHelper(std::unique_ptr<socket::Socket> socket, std::shared_ptr<APINoiseContext> ctx)
: socket_(std::move(socket)), ctx_(std::move(ctx)) {
: APIFrameHelper(std::move(socket)), ctx_(std::move(ctx)) {
// Noise header structure:
// Pos 0: indicator (0x01)
// Pos 1-2: encrypted payload size (16-bit big-endian)
@@ -105,49 +244,25 @@ class APINoiseFrameHelper : public APIFrameHelper {
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override;
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
std::string getpeername() override { return this->socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return this->socket_->getpeername(addr, addrlen);
}
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError state_action_();
APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_();
APIError write_frame_(const uint8_t *data, size_t len);
inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
}
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError init_handshake_();
APIError check_handshake_finished_();
void send_explicit_handshake_reject_(const std::string &reason);
std::unique_ptr<socket::Socket> socket_;
std::string info_;
// Fixed-size header buffer for noise protocol:
// 1 byte for indicator + 2 bytes for message size (16-bit value, not varint)
// Note: Maximum message size is 65535, with a limit of 128 bytes during handshake phase
// Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase
uint8_t rx_header_buf_[3];
size_t rx_header_buf_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
uint8_t rx_header_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
std::vector<uint8_t> prologue_;
std::shared_ptr<APINoiseContext> ctx_;
@@ -155,24 +270,13 @@ class APINoiseFrameHelper : public APIFrameHelper {
NoiseCipherState *send_cipher_{nullptr};
NoiseCipherState *recv_cipher_{nullptr};
NoiseProtocolId nid_;
enum class State {
INITIALIZE = 1,
CLIENT_HELLO = 2,
SERVER_HELLO = 3,
HANDSHAKE = 4,
DATA = 5,
CLOSED = 6,
FAILED = 7,
EXPLICIT_REJECT = 8,
} state_ = State::INITIALIZE;
};
#endif // USE_API_NOISE
#ifdef USE_API_PLAINTEXT
class APIPlaintextFrameHelper : public APIFrameHelper {
public:
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : socket_(std::move(socket)) {
APIPlaintextFrameHelper(std::unique_ptr<socket::Socket> socket) : APIFrameHelper(std::move(socket)) {
// Plaintext header structure (worst case):
// Pos 0: indicator (0x00)
// Pos 1-3: payload size varint (up to 3 bytes)
@@ -184,38 +288,16 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
bool can_write_without_blocking() override;
APIError write_protobuf_packet(uint16_t type, ProtoWriteBuffer buffer) override;
std::string getpeername() override { return this->socket_->getpeername(); }
int getpeername(struct sockaddr *addr, socklen_t *addrlen) override {
return this->socket_->getpeername(addr, addrlen);
}
APIError close() override;
APIError shutdown(int how) override;
// Give this helper a name for logging
void set_log_info(std::string info) override { info_ = std::move(info); }
// Get the frame header padding required by this protocol
uint8_t frame_header_padding() override { return frame_header_padding_; }
// Get the frame footer size required by this protocol
uint8_t frame_footer_size() override { return frame_footer_size_; }
protected:
struct ParsedFrame {
std::vector<uint8_t> msg;
};
APIError try_read_frame_(ParsedFrame *frame);
APIError try_send_tx_buf_();
inline APIError write_raw_(const struct iovec *iov, int iovcnt) {
return APIFrameHelper::write_raw_(iov, iovcnt, socket_.get(), tx_buf_, info_, state_, State::FAILED);
}
std::unique_ptr<socket::Socket> socket_;
std::string info_;
// Fixed-size header buffer for plaintext protocol:
// We only need space for the two varints since we validate the indicator byte separately.
// To match noise protocol's maximum message size (65535), we need:
// To match noise protocol's maximum message size (UINT16_MAX = 65535), we need:
// 3 bytes for message size varint (supports up to 2097151) + 2 bytes for message type varint
//
// While varints could theoretically be up to 10 bytes each for 64-bit values,
@@ -224,20 +306,8 @@ class APIPlaintextFrameHelper : public APIFrameHelper {
uint8_t rx_header_buf_[5]; // 5 bytes for varints (3 for size + 2 for type)
uint8_t rx_header_buf_pos_ = 0;
bool rx_header_parsed_ = false;
uint32_t rx_header_parsed_type_ = 0;
uint32_t rx_header_parsed_len_ = 0;
std::vector<uint8_t> rx_buf_;
size_t rx_buf_len_ = 0;
std::vector<uint8_t> tx_buf_;
enum class State {
INITIALIZE = 1,
DATA = 2,
CLOSED = 3,
FAILED = 4,
} state_ = State::INITIALIZE;
uint16_t rx_header_parsed_type_ = 0;
uint16_t rx_header_parsed_len_ = 0;
};
#endif

View File

@@ -96,6 +96,8 @@ template<> const char *proto_enum_to_string<enums::ColorMode>(enums::ColorMode v
return "COLOR_MODE_UNKNOWN";
case enums::COLOR_MODE_ON_OFF:
return "COLOR_MODE_ON_OFF";
case enums::COLOR_MODE_LEGACY_BRIGHTNESS:
return "COLOR_MODE_LEGACY_BRIGHTNESS";
case enums::COLOR_MODE_BRIGHTNESS:
return "COLOR_MODE_BRIGHTNESS";
case enums::COLOR_MODE_WHITE:

View File

@@ -41,7 +41,8 @@ enum FanDirection : uint32_t {
enum ColorMode : uint32_t {
COLOR_MODE_UNKNOWN = 0,
COLOR_MODE_ON_OFF = 1,
COLOR_MODE_BRIGHTNESS = 2,
COLOR_MODE_LEGACY_BRIGHTNESS = 2,
COLOR_MODE_BRIGHTNESS = 3,
COLOR_MODE_WHITE = 7,
COLOR_MODE_COLOR_TEMPERATURE = 11,
COLOR_MODE_COLD_WARM_WHITE = 19,

View File

@@ -55,6 +55,7 @@ class ProtoVarInt {
return {}; // Incomplete or invalid varint
}
uint16_t as_uint16() const { return this->value_; }
uint32_t as_uint32() const { return this->value_; }
uint64_t as_uint64() const { return this->value_; }
bool as_bool() const { return this->value_; }

View File

@@ -14,11 +14,8 @@ namespace esphome {
namespace at581x {
class AT581XComponent : public Component, public i2c::I2CDevice {
#ifdef USE_SWITCH
protected:
switch_::Switch *rf_power_switch_{nullptr};
public:
#ifdef USE_SWITCH
void set_rf_power_switch(switch_::Switch *s) {
this->rf_power_switch_ = s;
s->turn_on();
@@ -48,6 +45,9 @@ class AT581XComponent : public Component, public i2c::I2CDevice {
bool i2c_read_reg(uint8_t addr, uint8_t &data);
protected:
#ifdef USE_SWITCH
switch_::Switch *rf_power_switch_{nullptr};
#endif
int freq_;
int self_check_time_ms_; /*!< Power-on self-test time, range: 0 ~ 65536 ms */
int protect_time_ms_; /*!< Protection time, recommended 1000 ms */

View File

@@ -9,6 +9,7 @@ from esphome.const import (
CONF_ID,
CONF_LINE_FREQUENCY,
CONF_POWER,
CONF_RESET,
CONF_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
@@ -27,7 +28,6 @@ from esphome.const import (
CONF_CURRENT_REFERENCE = "current_reference"
CONF_ENERGY_REFERENCE = "energy_reference"
CONF_POWER_REFERENCE = "power_reference"
CONF_RESET = "reset"
CONF_VOLTAGE_REFERENCE = "voltage_reference"
DEPENDENCIES = ["uart"]

View File

@@ -16,7 +16,7 @@ CODEOWNERS = ["@neffs", "@kbx81"]
DOMAIN = "bme68x_bsec2"
BSEC2_LIBRARY_VERSION = "v1.8.2610"
BSEC2_LIBRARY_VERSION = "1.10.2610"
CONF_ALGORITHM_OUTPUT = "algorithm_output"
CONF_BME68X_BSEC2_ID = "bme68x_bsec2_id"
@@ -145,7 +145,6 @@ CONFIG_SCHEMA_BASE = (
): cv.positive_time_period_minutes,
},
)
.add_extra(cv.only_with_arduino)
.add_extra(validate_bme68x)
.add_extra(download_bme68x_blob)
)
@@ -179,11 +178,13 @@ async def to_code_base(config):
bsec2_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
cg.add(var.set_bsec2_configuration(bsec2_arr, len(rhs)))
# Although this component does not use SPI, the BSEC2 library requires the SPI library
cg.add_library("SPI", None)
# Although this component does not use SPI, the BSEC2 Arduino library requires the SPI library
if core.CORE.using_arduino:
cg.add_library("SPI", None)
cg.add_library(
"BME68x Sensor library",
"1.1.40407",
"1.3.40408",
"https://github.com/boschsensortec/Bosch-BME68x-Library",
)
cg.add_library(
"BSEC2 Software Library",

View File

@@ -1,4 +1,5 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"

View File

@@ -1,4 +1,5 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"

View File

@@ -0,0 +1 @@
"""CM1106 component for ESPHome."""

View File

@@ -0,0 +1,112 @@
#include "cm1106.h"
#include "esphome/core/log.h"
#include <cinttypes>
namespace esphome {
namespace cm1106 {
static const char *const TAG = "cm1106";
static const uint8_t C_M1106_CMD_GET_CO2[4] = {0x11, 0x01, 0x01, 0xED};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB[6] = {0x11, 0x03, 0x03, 0x00, 0x00, 0x00};
static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03, 0xE6};
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
uint8_t crc = 0;
for (int i = 0; i < len - 1; i++) {
crc -= response[i];
}
return crc;
}
void CM1106Component::setup() {
ESP_LOGCONFIG(TAG, "Setting up CM1106...");
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
ESP_LOGE(TAG, "Communication with CM1106 failed!");
this->mark_failed();
return;
}
}
void CM1106Component::update() {
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
this->status_set_warning();
return;
}
if (response[0] != 0x16 || response[1] != 0x05 || response[2] != 0x01) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X...", response[0], response[1], response[2],
response[3]);
this->status_set_warning();
return;
}
uint8_t checksum = cm1106_checksum(response, sizeof(response));
if (response[7] != checksum) {
ESP_LOGW(TAG, "CM1106 Checksum doesn't match: 0x%02X!=0x%02X", response[7], checksum);
this->status_set_warning();
return;
}
this->status_clear_warning();
uint16_t ppm = response[3] << 8 | response[4];
ESP_LOGD(TAG, "CM1106 Received CO₂=%uppm DF3=%02X DF4=%02X", ppm, response[5], response[6]);
if (this->co2_sensor_ != nullptr)
this->co2_sensor_->publish_state(ppm);
}
void CM1106Component::calibrate_zero(uint16_t ppm) {
uint8_t cmd[6];
memcpy(cmd, C_M1106_CMD_SET_CO2_CALIB, sizeof(cmd));
cmd[3] = ppm >> 8;
cmd[4] = ppm & 0xFF;
uint8_t response[4] = {0};
if (!this->cm1106_write_command_(cmd, sizeof(cmd), response, sizeof(response))) {
ESP_LOGW(TAG, "Reading data from CM1106 failed!");
this->status_set_warning();
return;
}
// check if correct response received
if (memcmp(response, C_M1106_CMD_SET_CO2_CALIB_RESPONSE, sizeof(response)) != 0) {
ESP_LOGW(TAG, "Got wrong UART response from CM1106: %02X %02X %02X %02X", response[0], response[1], response[2],
response[3]);
this->status_set_warning();
return;
}
this->status_clear_warning();
ESP_LOGD(TAG, "CM1106 Successfully calibrated sensor to %uppm", ppm);
}
bool CM1106Component::cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response,
size_t response_len) {
// Empty RX Buffer
while (this->available())
this->read();
this->write_array(command, command_len - 1);
this->write_byte(cm1106_checksum(command, command_len));
this->flush();
if (response == nullptr)
return true;
return this->read_array(response, response_len);
}
void CM1106Component::dump_config() {
ESP_LOGCONFIG(TAG, "CM1106:");
LOG_SENSOR(" ", "CO2", this->co2_sensor_);
this->check_uart_settings(9600);
if (this->is_failed()) {
ESP_LOGE(TAG, "Communication with CM1106 failed!");
}
}
} // namespace cm1106
} // namespace esphome

View File

@@ -0,0 +1,40 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/uart/uart.h"
namespace esphome {
namespace cm1106 {
class CM1106Component : public PollingComponent, public uart::UARTDevice {
public:
float get_setup_priority() const override { return esphome::setup_priority::DATA; }
void setup() override;
void update() override;
void dump_config() override;
void calibrate_zero(uint16_t ppm);
void set_co2_sensor(sensor::Sensor *co2_sensor) { this->co2_sensor_ = co2_sensor; }
protected:
sensor::Sensor *co2_sensor_{nullptr};
bool cm1106_write_command_(const uint8_t *command, size_t command_len, uint8_t *response, size_t response_len);
};
template<typename... Ts> class CM1106CalibrateZeroAction : public Action<Ts...> {
public:
CM1106CalibrateZeroAction(CM1106Component *cm1106) : cm1106_(cm1106) {}
void play(Ts... x) override { this->cm1106_->calibrate_zero(400); }
protected:
CM1106Component *cm1106_;
};
} // namespace cm1106
} // namespace esphome

View File

@@ -0,0 +1,72 @@
"""CM1106 Sensor component for ESPHome."""
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome import automation
from esphome.automation import maybe_simple_id
from esphome.components import sensor, uart
from esphome.const import (
CONF_CO2,
CONF_ID,
DEVICE_CLASS_CARBON_DIOXIDE,
ICON_MOLECULE_CO2,
STATE_CLASS_MEASUREMENT,
UNIT_PARTS_PER_MILLION,
)
DEPENDENCIES = ["uart"]
CODEOWNERS = ["@andrewjswan"]
cm1106_ns = cg.esphome_ns.namespace("cm1106")
CM1106Component = cm1106_ns.class_(
"CM1106Component", cg.PollingComponent, uart.UARTDevice
)
CM1106CalibrateZeroAction = cm1106_ns.class_(
"CM1106CalibrateZeroAction",
automation.Action,
)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(CM1106Component),
cv.Optional(CONF_CO2): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_MILLION,
icon=ICON_MOLECULE_CO2,
accuracy_decimals=0,
device_class=DEVICE_CLASS_CARBON_DIOXIDE,
state_class=STATE_CLASS_MEASUREMENT,
),
},
)
.extend(cv.polling_component_schema("60s"))
.extend(uart.UART_DEVICE_SCHEMA)
)
async def to_code(config) -> None:
"""Code generation entry point."""
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
if co2_config := config.get(CONF_CO2):
sens = await sensor.new_sensor(co2_config)
cg.add(var.set_co2_sensor(sens))
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
{
cv.GenerateID(): cv.use_id(CM1106Component),
},
)
@automation.register_action(
"cm1106.calibrate_zero",
CM1106CalibrateZeroAction,
CALIBRATION_ACTION_SCHEMA,
)
async def cm1106_calibration_to_code(config, action_id, template_arg, args) -> None:
"""Service code generation entry point."""
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)

View File

@@ -2,7 +2,6 @@ import base64
from pathlib import Path
import re
import secrets
from typing import Optional
import requests
from ruamel.yaml import YAML
@@ -84,7 +83,7 @@ async def to_code(config):
def import_config(
path: str,
name: str,
friendly_name: Optional[str],
friendly_name: str | None,
project_name: str,
import_url: str,
network: str = CONF_WIFI,

View File

@@ -34,13 +34,15 @@ class DebugComponent : public PollingComponent {
#endif
void set_loop_time_sensor(sensor::Sensor *loop_time_sensor) { loop_time_sensor_ = loop_time_sensor; }
#ifdef USE_ESP32
void on_shutdown() override;
void set_psram_sensor(sensor::Sensor *psram_sensor) { this->psram_sensor_ = psram_sensor; }
#endif // USE_ESP32
void set_cpu_frequency_sensor(sensor::Sensor *cpu_frequency_sensor) {
this->cpu_frequency_sensor_ = cpu_frequency_sensor;
}
#endif // USE_SENSOR
#ifdef USE_ESP32
void on_shutdown() override;
#endif // USE_ESP32
protected:
uint32_t free_heap_{};

View File

@@ -3,7 +3,6 @@ import itertools
import logging
import os
from pathlib import Path
from typing import Optional, Union
from esphome import git
import esphome.codegen as cg
@@ -58,8 +57,10 @@ from .const import ( # noqa
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_FRIENDLY,
@@ -88,8 +89,10 @@ CPU_FREQUENCIES = {
VARIANT_ESP32S3: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32C2: get_cpu_frequencies(80, 120),
VARIANT_ESP32C3: get_cpu_frequencies(80, 160),
VARIANT_ESP32C5: get_cpu_frequencies(80, 160, 240),
VARIANT_ESP32C6: get_cpu_frequencies(80, 120, 160),
VARIANT_ESP32H2: get_cpu_frequencies(16, 32, 48, 64, 96),
VARIANT_ESP32P4: get_cpu_frequencies(40, 360, 400),
}
# Make sure not missed here if a new variant added.
@@ -189,7 +192,7 @@ class RawSdkconfigValue:
value: str
SdkconfigValueType = Union[bool, int, HexInt, str, RawSdkconfigValue]
SdkconfigValueType = bool | int | HexInt | str | RawSdkconfigValue
def add_idf_sdkconfig_option(name: str, value: SdkconfigValueType):
@@ -206,8 +209,8 @@ def add_idf_component(
ref: str = None,
path: str = None,
refresh: TimePeriod = None,
components: Optional[list[str]] = None,
submodules: Optional[list[str]] = None,
components: list[str] | None = None,
submodules: list[str] | None = None,
):
"""Add an esp-idf component to the project."""
if not CORE.using_esp_idf:
@@ -296,11 +299,11 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
# The default/recommended esp-idf framework version
# - https://github.com/espressif/esp-idf/releases
# - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 1, 6)
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 3, 2)
# The platformio/espressif32 version to use for esp-idf frameworks
# - https://github.com/platformio/platform-espressif32/releases
# - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
ESP_IDF_PLATFORM_VERSION = cv.Version(51, 3, 7)
ESP_IDF_PLATFORM_VERSION = cv.Version(53, 3, 13)
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
@@ -369,8 +372,8 @@ def _arduino_check_versions(value):
def _esp_idf_check_versions(value):
value = value.copy()
lookups = {
"dev": (cv.Version(5, 1, 6), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 1, 6), None),
"dev": (cv.Version(5, 3, 2), "https://github.com/espressif/esp-idf.git"),
"latest": (cv.Version(5, 3, 2), None),
"recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
}

View File

@@ -2,8 +2,10 @@ from .const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
@@ -1592,6 +1594,10 @@ BOARDS = {
"name": "Ai-Thinker ESP-C3-M1-I-Kit",
"variant": VARIANT_ESP32C3,
},
"esp32-c5-devkitc-1": {
"name": "Espressif ESP32-C5-DevKitC-1",
"variant": VARIANT_ESP32C5,
},
"esp32-c6-devkitc-1": {
"name": "Espressif ESP32-C6-DevKitC-1",
"variant": VARIANT_ESP32C6,
@@ -1632,6 +1638,14 @@ BOARDS = {
"name": "Espressif ESP32-H2-DevKit",
"variant": VARIANT_ESP32H2,
},
"esp32-p4": {
"name": "Espressif ESP32-P4 generic",
"variant": VARIANT_ESP32P4,
},
"esp32-p4-evboard": {
"name": "Espressif ESP32-P4 Function EV Board",
"variant": VARIANT_ESP32P4,
},
"esp32-pico-devkitm-2": {
"name": "Espressif ESP32-PICO-DevKitM-2",
"variant": VARIANT_ESP32,

View File

@@ -17,16 +17,20 @@ VARIANT_ESP32S2 = "ESP32S2"
VARIANT_ESP32S3 = "ESP32S3"
VARIANT_ESP32C2 = "ESP32C2"
VARIANT_ESP32C3 = "ESP32C3"
VARIANT_ESP32C5 = "ESP32C5"
VARIANT_ESP32C6 = "ESP32C6"
VARIANT_ESP32H2 = "ESP32H2"
VARIANT_ESP32P4 = "ESP32P4"
VARIANTS = [
VARIANT_ESP32,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
]
VARIANT_FRIENDLY = {
@@ -35,8 +39,10 @@ VARIANT_FRIENDLY = {
VARIANT_ESP32S3: "ESP32-S3",
VARIANT_ESP32C2: "ESP32-C2",
VARIANT_ESP32C3: "ESP32-C3",
VARIANT_ESP32C5: "ESP32-C5",
VARIANT_ESP32C6: "ESP32-C6",
VARIANT_ESP32H2: "ESP32-H2",
VARIANT_ESP32P4: "ESP32-P4",
}
esp32_ns = cg.esphome_ns.namespace("esp32")

View File

@@ -15,8 +15,9 @@
#ifdef USE_ARDUINO
#include <Esp.h>
#else
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
#include <esp_clk_tree.h>
#endif
void setup();
void loop();
#endif
@@ -63,7 +64,13 @@ uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
#ifdef USE_ESP_IDF
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0)
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
#else
rtc_cpu_freq_config_t config;
rtc_clk_cpu_freq_get_config(&config);
freq = config.freq_mhz * 1000000U;
#endif
#elif defined(USE_ARDUINO)
freq = ESP.getCpuFreqMHz() * 1000000;
#endif

View File

@@ -1,6 +1,7 @@
from collections.abc import Callable
from dataclasses import dataclass
import logging
from typing import Any, Callable
from typing import Any
from esphome import pins
import esphome.codegen as cg
@@ -26,8 +27,10 @@ from .const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
esp32_ns,
@@ -35,8 +38,10 @@ from .const import (
from .gpio_esp32 import esp32_validate_gpio_pin, esp32_validate_supports
from .gpio_esp32_c2 import esp32_c2_validate_gpio_pin, esp32_c2_validate_supports
from .gpio_esp32_c3 import esp32_c3_validate_gpio_pin, esp32_c3_validate_supports
from .gpio_esp32_c5 import esp32_c5_validate_gpio_pin, esp32_c5_validate_supports
from .gpio_esp32_c6 import esp32_c6_validate_gpio_pin, esp32_c6_validate_supports
from .gpio_esp32_h2 import esp32_h2_validate_gpio_pin, esp32_h2_validate_supports
from .gpio_esp32_p4 import esp32_p4_validate_gpio_pin, esp32_p4_validate_supports
from .gpio_esp32_s2 import esp32_s2_validate_gpio_pin, esp32_s2_validate_supports
from .gpio_esp32_s3 import esp32_s3_validate_gpio_pin, esp32_s3_validate_supports
@@ -97,6 +102,10 @@ _esp32_validations = {
pin_validation=esp32_c3_validate_gpio_pin,
usage_validation=esp32_c3_validate_supports,
),
VARIANT_ESP32C5: ESP32ValidationFunctions(
pin_validation=esp32_c5_validate_gpio_pin,
usage_validation=esp32_c5_validate_supports,
),
VARIANT_ESP32C6: ESP32ValidationFunctions(
pin_validation=esp32_c6_validate_gpio_pin,
usage_validation=esp32_c6_validate_supports,
@@ -105,6 +114,10 @@ _esp32_validations = {
pin_validation=esp32_h2_validate_gpio_pin,
usage_validation=esp32_h2_validate_supports,
),
VARIANT_ESP32P4: ESP32ValidationFunctions(
pin_validation=esp32_p4_validate_gpio_pin,
usage_validation=esp32_p4_validate_supports,
),
VARIANT_ESP32S2: ESP32ValidationFunctions(
pin_validation=esp32_s2_validate_gpio_pin,
usage_validation=esp32_s2_validate_supports,

View File

@@ -0,0 +1,45 @@
import logging
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
from esphome.pins import check_strapping_pin
_ESP32C5_SPI_PSRAM_PINS = {
16: "SPICS0",
17: "SPIQ",
18: "SPIWP",
19: "VDD_SPI",
20: "SPIHD",
21: "SPICLK",
22: "SPID",
}
_ESP32C5_STRAPPING_PINS = {2, 7, 27, 28}
_LOGGER = logging.getLogger(__name__)
def esp32_c5_validate_gpio_pin(value):
if value < 0 or value > 28:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-28)")
if value in _ESP32C5_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C5s and is already used by the SPI/PSRAM interface (function: {_ESP32C5_SPI_PSRAM_PINS[value]})"
)
return value
def esp32_c5_validate_supports(value):
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 28:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-28)")
if is_input:
# All ESP32 pins support input mode
pass
check_strapping_pin(value, _ESP32C5_STRAPPING_PINS, _LOGGER)
return value

View File

@@ -0,0 +1,43 @@
import logging
import esphome.config_validation as cv
from esphome.const import CONF_INPUT, CONF_MODE, CONF_NUMBER
_ESP32P4_USB_JTAG_PINS = {24, 25}
_ESP32P4_STRAPPING_PINS = {34, 35, 36, 37, 38}
_LOGGER = logging.getLogger(__name__)
def esp32_p4_validate_gpio_pin(value):
if value < 0 or value > 54:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
if value in _ESP32P4_STRAPPING_PINS:
_LOGGER.warning(
"GPIO%d is a Strapping PIN and should be avoided.\n"
"Attaching external pullup/down resistors to strapping pins can cause unexpected failures.\n"
"See https://esphome.io/guides/faq.html#why-am-i-getting-a-warning-about-strapping-pins",
value,
)
if value in _ESP32P4_USB_JTAG_PINS:
_LOGGER.warning(
"GPIO%d is reserved for the USB-Serial-JTAG interface.\n"
"To use this pin as GPIO, USB-Serial-JTAG will be disabled.",
value,
)
return value
def esp32_p4_validate_supports(value):
num = value[CONF_NUMBER]
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 54:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-54)")
if is_input:
# All ESP32 pins support input mode
pass
return value

View File

@@ -1,8 +1,8 @@
from __future__ import annotations
from collections.abc import MutableMapping
from collections.abc import Callable, MutableMapping
import logging
from typing import Any, Callable
from typing import Any
from esphome import automation
import esphome.codegen as cg

View File

@@ -111,6 +111,8 @@ void ESPHomeOTAComponent::handle_() {
int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
if (err != 0) {
ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno);
client_->close();
client_ = nullptr;
return;
}

View File

@@ -4,6 +4,7 @@ from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
@@ -74,6 +75,7 @@ I2S_PORTS = {
VARIANT_ESP32S2: 1,
VARIANT_ESP32S3: 2,
VARIANT_ESP32C3: 1,
VARIANT_ESP32P4: 3,
}
i2s_channel_fmt_t = cg.global_ns.enum("i2s_channel_fmt_t")

View File

@@ -1,5 +1,5 @@
from collections.abc import Callable
from dataclasses import dataclass
from typing import Callable
import esphome.codegen as cg

View File

@@ -8,8 +8,10 @@ from esphome.components.esp32.const import (
VARIANT_ESP32,
VARIANT_ESP32C2,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32H2,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
)
@@ -24,6 +26,7 @@ from esphome.const import (
CONF_HARDWARE_UART,
CONF_ID,
CONF_LEVEL,
CONF_LOGGER,
CONF_LOGS,
CONF_ON_MESSAGE,
CONF_TAG,
@@ -87,8 +90,10 @@ UART_SELECTION_ESP32 = {
VARIANT_ESP32S3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32C3: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32C2: [UART0, UART1],
VARIANT_ESP32C5: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32C6: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32H2: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
VARIANT_ESP32P4: [UART0, UART1, USB_CDC, USB_SERIAL_JTAG],
}
UART_SELECTION_ESP8266 = [UART0, UART0_SWAP, UART1]
@@ -204,8 +209,10 @@ CONFIG_SCHEMA = cv.All(
esp32_s3_idf=USB_SERIAL_JTAG,
esp32_c3_arduino=USB_CDC,
esp32_c3_idf=USB_SERIAL_JTAG,
esp32_c5_idf=USB_SERIAL_JTAG,
esp32_c6_arduino=USB_CDC,
esp32_c6_idf=USB_SERIAL_JTAG,
esp32_p4_idf=USB_SERIAL_JTAG,
rp2040=USB_CDC,
bk72xx=DEFAULT,
rtl87xx=DEFAULT,
@@ -247,6 +254,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
baud_rate = config[CONF_BAUD_RATE]
level = config[CONF_LEVEL]
CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level
initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)]
log = cg.new_Pvariable(
config[CONF_ID],

View File

@@ -18,12 +18,12 @@
#endif
#endif
#include "freertos/FreeRTOS.h"
#include "esp_idf_version.h"
#include "freertos/FreeRTOS.h"
#include <fcntl.h>
#include <cstdint>
#include <cstdio>
#include <fcntl.h>
#endif // USE_ESP_IDF
@@ -174,11 +174,11 @@ void Logger::pre_setup() {
#ifdef USE_ESP_IDF
void HOT Logger::write_msg_(const char *msg) {
if (
#if defined(USE_ESP32_VARIANT_ESP32S2)
#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC
#elif defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2)
#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC)
this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
#elif defined(USE_ESP32_VARIANT_ESP32S3)
#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
#else
/* DISABLES CODE */ (false) // NOLINT

View File

@@ -5,7 +5,7 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_
from esphome.core import CORE
from esphome.cpp_helpers import register_component, register_parented
from .. import CONF_LOGGER_ID, LOG_LEVEL_SEVERITY, Logger, logger_ns
from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns
CODEOWNERS = ["@clydebarrow"]
@@ -21,9 +21,10 @@ CONFIG_SCHEMA = select.select_schema(
async def to_code(config):
levels = LOG_LEVEL_SEVERITY
index = levels.index(CORE.config[CONF_LOGGER][CONF_LEVEL])
parent = await cg.get_variable(config[CONF_LOGGER_ID])
levels = list(LOG_LEVELS)
index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL])
levels = levels[: index + 1]
var = await select.new_select(config, options=levels)
await register_parented(var, config[CONF_LOGGER_ID])
await register_parented(var, parent)
await register_component(var, config)

View File

@@ -321,7 +321,7 @@ async def to_code(configs):
frac = 2
elif frac > 0.19:
frac = 4
else:
elif frac != 0:
frac = 8
displays = [
await cg.get_variable(display) for display in config[df.CONF_DISPLAYS]
@@ -422,7 +422,7 @@ LVGL_SCHEMA = cv.All(
): lvalid.lv_font,
cv.Optional(df.CONF_FULL_REFRESH, default=False): cv.boolean,
cv.Optional(CONF_DRAW_ROUNDING, default=2): cv.positive_int,
cv.Optional(CONF_BUFFER_SIZE, default="100%"): cv.percentage,
cv.Optional(CONF_BUFFER_SIZE, default=0): cv.percentage,
cv.Optional(df.CONF_LOG_LEVEL, default="WARN"): cv.one_of(
*df.LV_LOG_LEVELS, upper=True
),

View File

@@ -1,4 +1,5 @@
from typing import Any, Callable
from collections.abc import Callable
from typing import Any
from esphome import automation
import esphome.codegen as cg

View File

@@ -1,5 +1,3 @@
from typing import Union
import esphome.codegen as cg
from esphome.components import image
from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw
@@ -361,7 +359,7 @@ lv_image_list = LValidator(
lv_bool = LValidator(cv.boolean, cg.bool_, retmapper=literal)
def lv_pct(value: Union[int, float]):
def lv_pct(value: int | float):
if isinstance(value, float):
value = int(value * 100)
return literal(f"lv_pct({value})")

View File

@@ -1,5 +1,4 @@
import abc
from typing import Union
from esphome import codegen as cg
from esphome.config import Config
@@ -75,7 +74,7 @@ class CodeContext(abc.ABC):
code_context = None
@abc.abstractmethod
def add(self, expression: Union[Expression, Statement]):
def add(self, expression: Expression | Statement):
pass
@staticmethod
@@ -89,13 +88,13 @@ class CodeContext(abc.ABC):
CodeContext.append(RawStatement("}"))
@staticmethod
def append(expression: Union[Expression, Statement]):
def append(expression: Expression | Statement):
if CodeContext.code_context is not None:
CodeContext.code_context.add(expression)
return expression
def __init__(self):
self.previous: Union[CodeContext | None] = None
self.previous: CodeContext | None = None
self.indent_level = 0
async def __aenter__(self):
@@ -121,7 +120,7 @@ class MainContext(CodeContext):
Code generation into the main() function
"""
def add(self, expression: Union[Expression, Statement]):
def add(self, expression: Expression | Statement):
return cg.add(self.indented_statement(expression))
@@ -144,7 +143,7 @@ class LambdaContext(CodeContext):
self.capture = capture
self.where = where
def add(self, expression: Union[Expression, Statement]):
def add(self, expression: Expression | Statement):
self.code_list.append(self.indented_statement(expression))
return expression
@@ -186,7 +185,7 @@ class LvContext(LambdaContext):
async def __aexit__(self, exc_type, exc_val, exc_tb):
await super().__aexit__(exc_type, exc_val, exc_tb)
def add(self, expression: Union[Expression, Statement]):
def add(self, expression: Expression | Statement):
cg.add(expression)
return expression
@@ -303,7 +302,7 @@ lvgl_static = MockObj("LvglComponent", "::")
# equivalent to cg.add() for the current code context
def lv_add(expression: Union[Expression, Statement]):
def lv_add(expression: Expression | Statement):
return CodeContext.append(expression)

View File

@@ -11,6 +11,8 @@ namespace esphome {
namespace lvgl {
static const char *const TAG = "lvgl";
static const size_t MIN_BUFFER_FRAC = 8;
static const char *const EVENT_NAMES[] = {
"NONE",
"PRESSED",
@@ -85,6 +87,7 @@ lv_event_code_t lv_update_event; // NOLINT
void LvglComponent::dump_config() {
ESP_LOGCONFIG(TAG, "LVGL:");
ESP_LOGCONFIG(TAG, " Display width/height: %d x %d", this->disp_drv_.hor_res, this->disp_drv_.ver_res);
ESP_LOGCONFIG(TAG, " Buffer size: %zu%%", 100 / this->buffer_frac_);
ESP_LOGCONFIG(TAG, " Rotation: %d", this->rotation);
ESP_LOGCONFIG(TAG, " Draw rounding: %d", (int) this->draw_rounding);
}
@@ -432,18 +435,28 @@ void LvglComponent::setup() {
auto *display = this->displays_[0];
auto width = display->get_width();
auto height = display->get_height();
size_t buffer_pixels = width * height / this->buffer_frac_;
auto frac = this->buffer_frac_;
if (frac == 0)
frac = 1;
size_t buffer_pixels = width * height / frac;
auto buf_bytes = buffer_pixels * LV_COLOR_DEPTH / 8;
void *buffer = nullptr;
if (this->buffer_frac_ >= 4)
if (this->buffer_frac_ >= MIN_BUFFER_FRAC / 2)
buffer = malloc(buf_bytes); // NOLINT
if (buffer == nullptr)
buffer = lv_custom_mem_alloc(buf_bytes); // NOLINT
// if specific buffer size not set and can't get 100%, try for a smaller one
if (buffer == nullptr && this->buffer_frac_ == 0) {
frac = MIN_BUFFER_FRAC;
buffer_pixels /= MIN_BUFFER_FRAC;
buffer = lv_custom_mem_alloc(buf_bytes / MIN_BUFFER_FRAC); // NOLINT
}
if (buffer == nullptr) {
this->mark_failed();
this->status_set_error("Memory allocation failure");
this->mark_failed();
return;
}
this->buffer_frac_ = frac;
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels);
this->disp_drv_.hor_res = width;
this->disp_drv_.ver_res = height;
@@ -453,8 +466,8 @@ void LvglComponent::setup() {
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes)); // NOLINT
if (this->rotate_buf_ == nullptr) {
this->mark_failed();
this->status_set_error("Memory allocation failure");
this->mark_failed();
return;
}
}

View File

@@ -36,29 +36,43 @@ from .types import (
# this will be populated later, in __init__.py to avoid circular imports.
WIDGET_TYPES: dict = {}
TIME_TEXT_SCHEMA = cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
}
)
PRINTF_TEXT_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
},
),
validate_printf,
)
def _validate_text(value):
"""
Do some sanity checking of the format to get better error messages
than using cv.Any
"""
if value is None:
raise cv.Invalid("No text specified")
if isinstance(value, dict):
if CONF_TIME_FORMAT in value:
return TIME_TEXT_SCHEMA(value)
return PRINTF_TEXT_SCHEMA(value)
return cv.templatable(cv.string)(value)
# A schema for text properties
TEXT_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TEXT): cv.Any(
cv.All(
cv.Schema(
{
cv.Required(CONF_FORMAT): cv.string,
cv.Optional(CONF_ARGS, default=list): cv.ensure_list(
cv.lambda_
),
},
),
validate_printf,
),
cv.Schema(
{
cv.Required(CONF_TIME_FORMAT): cv.string,
cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)),
}
),
cv.templatable(cv.string),
)
cv.Optional(CONF_TEXT): _validate_text,
}
)
@@ -247,11 +261,13 @@ FLAG_LIST = cv.ensure_list(df.LvConstant("LV_OBJ_FLAG_", *df.OBJ_FLAGS).one_of)
def part_schema(parts):
"""
Generate a schema for the various parts (e.g. main:, indicator:) of a widget type
:param parts: The parts to include in the schema
:param parts: The parts to include
:return: The schema
"""
return cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts}).extend(
STATE_SCHEMA
return (
cv.Schema({cv.Optional(part): STATE_SCHEMA for part in parts})
.extend(STATE_SCHEMA)
.extend(FLAG_SCHEMA)
)
@@ -288,22 +304,18 @@ def base_update_schema(widget_type, parts):
:param parts: The allowable parts to specify
:return:
"""
return (
part_schema(parts)
.extend(
{
cv.Required(CONF_ID): cv.ensure_list(
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(widget_type),
},
key=CONF_ID,
)
),
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
}
)
.extend(FLAG_SCHEMA)
return part_schema(parts).extend(
{
cv.Required(CONF_ID): cv.ensure_list(
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(widget_type),
},
key=CONF_ID,
)
),
cv.Optional(CONF_STATE): SET_STATE_SCHEMA,
}
)
@@ -321,7 +333,6 @@ def obj_schema(widget_type: WidgetType):
"""
return (
part_schema(widget_type.parts)
.extend(FLAG_SCHEMA)
.extend(LAYOUT_SCHEMA)
.extend(ALIGN_TO_SCHEMA)
.extend(automation_schema(widget_type.w_type))

View File

@@ -1,5 +1,5 @@
import sys
from typing import Any, Union
from typing import Any
from esphome import codegen as cg, config_validation as cv
from esphome.config_validation import Invalid
@@ -262,7 +262,7 @@ async def wait_for_widgets():
await FakeAwaitable(widgets_wait_generator())
async def get_widgets(config: Union[dict, list], id: str = CONF_ID) -> list[Widget]:
async def get_widgets(config: dict | list, id: str = CONF_ID) -> list[Widget]:
if not config:
return []
if not isinstance(config, list):

View File

@@ -24,6 +24,7 @@ from .obj import obj_spec
CONF_TABVIEW = "tabview"
CONF_TAB_STYLE = "tab_style"
CONF_CONTENT_STYLE = "content_style"
lv_tab_t = LvType("lv_obj_t")
@@ -39,6 +40,7 @@ TABVIEW_SCHEMA = cv.Schema(
)
),
cv.Optional(CONF_TAB_STYLE): part_schema(buttonmatrix_spec.parts),
cv.Optional(CONF_CONTENT_STYLE): part_schema(obj_spec.parts),
cv.Optional(CONF_POSITION, default="top"): DIRECTIONS.one_of,
cv.Optional(CONF_SIZE, default="10%"): size,
}
@@ -79,6 +81,11 @@ class TabviewType(WidgetType):
"tabview_btnmatrix", lv_obj_t, rhs=lv_expr.tabview_get_tab_btns(w.obj)
) as btnmatrix_obj:
await set_obj_properties(Widget(btnmatrix_obj, obj_spec), button_style)
if content_style := config.get(CONF_CONTENT_STYLE):
with LocalVariable(
"tabview_content", lv_obj_t, rhs=lv_expr.tabview_get_content(w.obj)
) as content_obj:
await set_obj_properties(Widget(content_obj, obj_spec), content_style)
def obj_creator(self, parent: MockObjClass, config: dict):
return lv_expr.call(

View File

@@ -0,0 +1,52 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "max7219digit.h"
namespace esphome {
namespace max7219digit {
template<typename... Ts> class DisplayInvertAction : public Action<Ts...>, public Parented<MAX7219Component> {
public:
TEMPLATABLE_VALUE(bool, state)
void play(Ts... x) override {
bool state = this->state_.value(x...);
this->parent_->invert_on_off(state);
}
};
template<typename... Ts> class DisplayVisibilityAction : public Action<Ts...>, public Parented<MAX7219Component> {
public:
TEMPLATABLE_VALUE(bool, state)
void play(Ts... x) override {
bool state = this->state_.value(x...);
this->parent_->turn_on_off(state);
}
};
template<typename... Ts> class DisplayReverseAction : public Action<Ts...>, public Parented<MAX7219Component> {
public:
TEMPLATABLE_VALUE(bool, state)
void play(Ts... x) override {
bool state = this->state_.value(x...);
this->parent_->set_reverse(state);
}
};
template<typename... Ts> class DisplayIntensityAction : public Action<Ts...>, public Parented<MAX7219Component> {
public:
TEMPLATABLE_VALUE(uint8_t, state)
void play(Ts... x) override {
uint8_t state = this->state_.value(x...);
this->parent_->set_intensity(state);
}
};
} // namespace max7219digit
} // namespace esphome

View File

@@ -1,7 +1,14 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import display, spi
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_INTENSITY, CONF_LAMBDA, CONF_NUM_CHIPS
from esphome.const import (
CONF_ID,
CONF_INTENSITY,
CONF_LAMBDA,
CONF_NUM_CHIPS,
CONF_STATE,
)
CODEOWNERS = ["@rspaargaren"]
DEPENDENCIES = ["spi"]
@@ -17,6 +24,7 @@ CONF_REVERSE_ENABLE = "reverse_enable"
CONF_NUM_CHIP_LINES = "num_chip_lines"
CONF_CHIP_LINES_STYLE = "chip_lines_style"
integration_ns = cg.esphome_ns.namespace("max7219digit")
ChipLinesStyle = integration_ns.enum("ChipLinesStyle")
CHIP_LINES_STYLE = {
@@ -99,3 +107,87 @@ async def to_code(config):
config[CONF_LAMBDA], [(MAX7219ComponentRef, "it")], return_type=cg.void
)
cg.add(var.set_writer(lambda_))
DisplayInvertAction = max7219_ns.class_("DisplayInvertAction", automation.Action)
DisplayVisibilityAction = max7219_ns.class_(
"DisplayVisibilityAction", automation.Action
)
DisplayReverseAction = max7219_ns.class_("DisplayReverseAction", automation.Action)
DisplayIntensityAction = max7219_ns.class_("DisplayIntensityAction", automation.Action)
MAX7219_OFF_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(MAX7219Component),
cv.Optional(CONF_STATE, default=False): False,
}
)
MAX7219_ON_ACTION_SCHEMA = automation.maybe_simple_id(
{
cv.GenerateID(): cv.use_id(MAX7219Component),
cv.Optional(CONF_STATE, default=True): True,
}
)
@automation.register_action(
"max7129digit.invert_off", DisplayInvertAction, MAX7219_OFF_ACTION_SCHEMA
)
@automation.register_action(
"max7129digit.invert_on", DisplayInvertAction, MAX7219_ON_ACTION_SCHEMA
)
async def max7129digit_invert_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(config[CONF_STATE]))
return var
@automation.register_action(
"max7129digit.turn_off", DisplayVisibilityAction, MAX7219_OFF_ACTION_SCHEMA
)
@automation.register_action(
"max7129digit.turn_on", DisplayVisibilityAction, MAX7219_ON_ACTION_SCHEMA
)
async def max7129digit_visible_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(config[CONF_STATE]))
return var
@automation.register_action(
"max7129digit.reverse_off", DisplayReverseAction, MAX7219_OFF_ACTION_SCHEMA
)
@automation.register_action(
"max7129digit.reverse_on", DisplayReverseAction, MAX7219_ON_ACTION_SCHEMA
)
async def max7129digit_reverse_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
cg.add(var.set_state(config[CONF_STATE]))
return var
MAX7219_INTENSITY_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(): cv.use_id(MAX7219Component),
cv.Optional(CONF_INTENSITY, default=15): cv.templatable(
cv.int_range(min=0, max=15)
),
},
key=CONF_INTENSITY,
)
@automation.register_action(
"max7129digit.intensity", DisplayIntensityAction, MAX7219_INTENSITY_SCHEMA
)
async def max7129digit_intensity_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_INTENSITY], args, cg.uint8)
cg.add(var.set_state(template_))
return var

View File

@@ -147,7 +147,11 @@ bool StreamingModel::perform_streaming_inference(const int8_t features[PREPROCES
this->recent_streaming_probabilities_[this->last_n_index_] = output->data.uint8[0]; // probability;
this->unprocessed_probability_status_ = true;
}
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
if (this->recent_streaming_probabilities_[this->last_n_index_] < this->probability_cutoff_) {
// Only increment ignore windows if less than the probability cutoff; this forces the model to "cool-off" from a
// previous detection and calling ``reset_probabilities`` so it avoids duplicate detections
this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0);
}
}
return true;
}

View File

@@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import switch
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID
from esphome.const import CONF_ADDRESS, CONF_ASSUMED_STATE, CONF_ID
from .. import (
MODBUS_REGISTER_TYPE,
@@ -36,6 +36,7 @@ CONFIG_SCHEMA = cv.All(
.extend(ModbusItemBaseSchema)
.extend(
{
cv.Optional(CONF_ASSUMED_STATE, default=False): cv.boolean,
cv.Optional(CONF_REGISTER_TYPE): cv.enum(MODBUS_REGISTER_TYPE),
cv.Optional(CONF_USE_WRITE_MULTIPLE, default=False): cv.boolean,
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
@@ -62,7 +63,10 @@ async def to_code(config):
paren = await cg.get_variable(config[CONF_MODBUS_CONTROLLER_ID])
cg.add(var.set_parent(paren))
cg.add(var.set_use_write_mutiple(config[CONF_USE_WRITE_MULTIPLE]))
cg.add(paren.add_sensor_item(var))
assumed_state = config[CONF_ASSUMED_STATE]
cg.add(var.set_assumed_state(assumed_state))
if not assumed_state:
cg.add(paren.add_sensor_item(var))
if CONF_WRITE_LAMBDA in config:
template_ = await cg.process_lambda(
config[CONF_WRITE_LAMBDA],

View File

@@ -19,6 +19,10 @@ void ModbusSwitch::setup() {
}
void ModbusSwitch::dump_config() { LOG_SWITCH(TAG, "Modbus Controller Switch", this); }
void ModbusSwitch::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; }
bool ModbusSwitch::assumed_state() { return this->assumed_state_; }
void ModbusSwitch::parse_and_publish(const std::vector<uint8_t> &data) {
bool value = false;
switch (this->register_type) {

View File

@@ -29,6 +29,7 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem
void setup() override;
void write_state(bool state) override;
void dump_config() override;
void set_assumed_state(bool assumed_state);
void set_state(bool state) { this->state = state; }
void parse_and_publish(const std::vector<uint8_t> &data) override;
void set_parent(ModbusController *parent) { this->parent_ = parent; }
@@ -40,10 +41,12 @@ class ModbusSwitch : public Component, public switch_::Switch, public SensorItem
void set_use_write_mutiple(bool use_write_multiple) { this->use_write_multiple_ = use_write_multiple; }
protected:
bool assumed_state() override;
ModbusController *parent_{nullptr};
bool use_write_multiple_{false};
optional<transform_func_t> publish_transform_func_{nullopt};
optional<write_transform_func_t> write_transform_func_{nullopt};
bool assumed_state_{false};
};
} // namespace modbus_controller

View File

@@ -21,8 +21,10 @@ from esphome.const import (
CONF_WEB_SERVER,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CONDUCTIVITY,
@@ -33,6 +35,7 @@ from esphome.const import (
DEVICE_CLASS_DURATION,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_ENERGY_DISTANCE,
DEVICE_CLASS_ENERGY_STORAGE,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS,
@@ -54,6 +57,7 @@ from esphome.const import (
DEVICE_CLASS_PRECIPITATION,
DEVICE_CLASS_PRECIPITATION_INTENSITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_REACTIVE_ENERGY,
DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_SOUND_PRESSURE,
@@ -68,6 +72,7 @@ from esphome.const import (
DEVICE_CLASS_VOLUME_STORAGE,
DEVICE_CLASS_WATER,
DEVICE_CLASS_WEIGHT,
DEVICE_CLASS_WIND_DIRECTION,
DEVICE_CLASS_WIND_SPEED,
)
from esphome.core import CORE, coroutine_with_priority
@@ -78,8 +83,10 @@ CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CONDUCTIVITY,
@@ -90,6 +97,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_DURATION,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_ENERGY_DISTANCE,
DEVICE_CLASS_ENERGY_STORAGE,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS,
@@ -111,6 +119,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_PRECIPITATION,
DEVICE_CLASS_PRECIPITATION_INTENSITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_REACTIVE_ENERGY,
DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_SOUND_PRESSURE,
@@ -125,6 +134,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_VOLUME_STORAGE,
DEVICE_CLASS_WATER,
DEVICE_CLASS_WEIGHT,
DEVICE_CLASS_WIND_DIRECTION,
DEVICE_CLASS_WIND_SPEED,
]
IS_PLATFORM_COMPONENT = True

View File

@@ -75,7 +75,7 @@ class PNGFormat(Format):
def actions(self):
cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT")
cg.add_library("pngle", "1.0.2")
cg.add_library("pngle", "1.1.0")
IMAGE_FORMATS = {

View File

@@ -34,12 +34,32 @@ static void init_callback(pngle_t *pngle, uint32_t w, uint32_t h) {
* @param h The height of the rectangle to draw.
* @param rgba The color to paint the rectangle in.
*/
static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) {
static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const uint8_t rgba[4]) {
PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle);
Color color(rgba[0], rgba[1], rgba[2], rgba[3]);
decoder->draw(x, y, w, h, color);
}
PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) {
{
pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE);
if (!pngle) {
ESP_LOGE(TAG, "Failed to allocate memory for PNGLE engine!");
return;
}
memset(pngle, 0, PNGLE_T_SIZE);
pngle_reset(pngle);
this->pngle_ = pngle;
}
}
PngDecoder::~PngDecoder() {
if (this->pngle_) {
pngle_reset(this->pngle_);
this->allocator_.deallocate(this->pngle_, PNGLE_T_SIZE);
}
}
int PngDecoder::prepare(size_t download_size) {
ImageDecoder::prepare(download_size);
if (!this->pngle_) {

View File

@@ -1,7 +1,8 @@
#pragma once
#include "image_decoder.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "image_decoder.h"
#ifdef USE_ONLINE_IMAGE_PNG_SUPPORT
#include <pngle.h>
@@ -18,13 +19,14 @@ class PngDecoder : public ImageDecoder {
*
* @param display The image to decode the stream into.
*/
PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {}
~PngDecoder() override { pngle_destroy(this->pngle_); }
PngDecoder(OnlineImage *image);
~PngDecoder() override;
int prepare(size_t download_size) override;
int HOT decode(uint8_t *buffer, size_t size) override;
protected:
RAMAllocator<pngle_t> allocator_;
pngle_t *pngle_;
};

View File

@@ -1,5 +1,5 @@
from collections.abc import Awaitable
from typing import Any, Callable, Optional
from collections.abc import Awaitable, Callable
from typing import Any
import esphome.codegen as cg
from esphome.const import CONF_ID
@@ -103,7 +103,7 @@ def define_setting_readers(component_type: str, keys: list[str]) -> None:
def add_messages(hub: cg.MockObj, keys: list[str], schemas: dict[str, TSchema]):
messages: dict[str, tuple[bool, Optional[int]]] = {}
messages: dict[str, tuple[bool, int | None]] = {}
for key in keys:
messages[schemas[key].message] = (
schemas[key].keep_updated,

View File

@@ -2,7 +2,7 @@
# inputs of the OpenTherm component.
from dataclasses import dataclass
from typing import Any, Optional, TypeVar
from typing import Any, TypeVar
import esphome.config_validation as cv
from esphome.const import (
@@ -61,11 +61,11 @@ TSchema = TypeVar("TSchema", bound=EntitySchema)
class SensorSchema(EntitySchema):
accuracy_decimals: int
state_class: str
unit_of_measurement: Optional[str] = None
icon: Optional[str] = None
device_class: Optional[str] = None
unit_of_measurement: str | None = None
icon: str | None = None
device_class: str | None = None
disabled_by_default: bool = False
order: Optional[int] = None
order: int | None = None
SENSORS: dict[str, SensorSchema] = {
@@ -461,9 +461,9 @@ SENSORS: dict[str, SensorSchema] = {
@dataclass
class BinarySensorSchema(EntitySchema):
icon: Optional[str] = None
device_class: Optional[str] = None
order: Optional[int] = None
icon: str | None = None
device_class: str | None = None
order: int | None = None
BINARY_SENSORS: dict[str, BinarySensorSchema] = {
@@ -654,7 +654,7 @@ BINARY_SENSORS: dict[str, BinarySensorSchema] = {
@dataclass
class SwitchSchema(EntitySchema):
default_mode: Optional[str] = None
default_mode: str | None = None
SWITCHES: dict[str, SwitchSchema] = {
@@ -721,9 +721,9 @@ class InputSchema(EntitySchema):
unit_of_measurement: str
step: float
range: tuple[int, int]
icon: Optional[str] = None
auto_max_value: Optional[AutoConfigure] = None
auto_min_value: Optional[AutoConfigure] = None
icon: str | None = None
auto_max_value: AutoConfigure | None = None
auto_min_value: AutoConfigure | None = None
INPUTS: dict[str, InputSchema] = {
@@ -834,7 +834,7 @@ class SettingSchema(EntitySchema):
backing_type: str
validation_schema: cv.Schema
default_value: Any
order: Optional[int] = None
order: int | None = None
SETTINGS: dict[str, SettingSchema] = {

View File

@@ -1,4 +1,4 @@
from typing import Callable
from collections.abc import Callable
from voluptuous import Schema

View File

@@ -2,6 +2,7 @@ import logging
import esphome.codegen as cg
from esphome.components.esp32 import (
CONF_CPU_FREQUENCY,
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES,
VARIANT_ESP32,
add_idf_sdkconfig_option,
@@ -50,18 +51,23 @@ SPIRAM_SPEEDS = {
def validate_psram_mode(config):
if config[CONF_MODE] == TYPE_OCTAL and config[CONF_SPEED] == 120e6:
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
if (
esp32_config[CONF_FRAMEWORK]
.get(CONF_ADVANCED, {})
.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES)
):
_LOGGER.warning(
"120MHz PSRAM in octal mode is an experimental feature - use at your own risk"
esp32_config = fv.full_config.get()[PLATFORM_ESP32]
if config[CONF_SPEED] == 120e6:
if esp32_config[CONF_CPU_FREQUENCY] != "240MHZ":
raise cv.Invalid(
"PSRAM 120MHz requires 240MHz CPU frequency (set in esp32 component)"
)
else:
raise cv.Invalid("PSRAM 120MHz is not supported in octal mode")
if config[CONF_MODE] == TYPE_OCTAL:
if (
esp32_config[CONF_FRAMEWORK]
.get(CONF_ADVANCED, {})
.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES)
):
_LOGGER.warning(
"120MHz PSRAM in octal mode is an experimental feature - use at your own risk"
)
else:
raise cv.Invalid("PSRAM 120MHz is not supported in octal mode")
if config[CONF_MODE] != TYPE_OCTAL and config[CONF_ENABLE_ECC]:
raise cv.Invalid("ECC is only available in octal mode.")
if config[CONF_MODE] == TYPE_OCTAL:
@@ -112,7 +118,7 @@ async def to_code(config):
add_idf_sdkconfig_option(f"{SPIRAM_MODES[config[CONF_MODE]]}", True)
add_idf_sdkconfig_option(f"{SPIRAM_SPEEDS[config[CONF_SPEED]]}", True)
if config[CONF_MODE] == TYPE_OCTAL and config[CONF_SPEED] == 120e6:
add_idf_sdkconfig_option("CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240", True)
add_idf_sdkconfig_option("CONFIG_ESPTOOLPY_FLASHFREQ_120M", True)
if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(5, 4, 0):
add_idf_sdkconfig_option(
"CONFIG_SPIRAM_TIMING_TUNING_POINT_VIA_TEMPERATURE_SENSOR", True

View File

@@ -43,8 +43,10 @@ from esphome.const import (
CONF_WINDOW_SIZE,
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CONDUCTIVITY,
@@ -56,6 +58,7 @@ from esphome.const import (
DEVICE_CLASS_DURATION,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_ENERGY_DISTANCE,
DEVICE_CLASS_ENERGY_STORAGE,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS,
@@ -77,6 +80,7 @@ from esphome.const import (
DEVICE_CLASS_PRECIPITATION,
DEVICE_CLASS_PRECIPITATION_INTENSITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_REACTIVE_ENERGY,
DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_SOUND_PRESSURE,
@@ -92,6 +96,7 @@ from esphome.const import (
DEVICE_CLASS_VOLUME_STORAGE,
DEVICE_CLASS_WATER,
DEVICE_CLASS_WEIGHT,
DEVICE_CLASS_WIND_DIRECTION,
DEVICE_CLASS_WIND_SPEED,
ENTITY_CATEGORY_CONFIG,
)
@@ -104,8 +109,10 @@ CODEOWNERS = ["@esphome/core"]
DEVICE_CLASSES = [
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_AQI,
DEVICE_CLASS_AREA,
DEVICE_CLASS_ATMOSPHERIC_PRESSURE,
DEVICE_CLASS_BATTERY,
DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION,
DEVICE_CLASS_CARBON_DIOXIDE,
DEVICE_CLASS_CARBON_MONOXIDE,
DEVICE_CLASS_CONDUCTIVITY,
@@ -117,6 +124,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_DURATION,
DEVICE_CLASS_EMPTY,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_ENERGY_DISTANCE,
DEVICE_CLASS_ENERGY_STORAGE,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS,
@@ -138,6 +146,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_PRECIPITATION,
DEVICE_CLASS_PRECIPITATION_INTENSITY,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_REACTIVE_ENERGY,
DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_SIGNAL_STRENGTH,
DEVICE_CLASS_SOUND_PRESSURE,
@@ -153,6 +162,7 @@ DEVICE_CLASSES = [
DEVICE_CLASS_VOLUME_STORAGE,
DEVICE_CLASS_WATER,
DEVICE_CLASS_WEIGHT,
DEVICE_CLASS_WIND_DIRECTION,
DEVICE_CLASS_WIND_SPEED,
]

View File

@@ -174,6 +174,16 @@ AudioPipelineState AudioPipeline::process_state() {
}
}
if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) {
xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR);
return AudioPipelineState::ERROR_READING;
}
if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) {
xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR);
return AudioPipelineState::ERROR_DECODING;
}
if ((event_bits & EventGroupBits::READER_MESSAGE_FINISHED) &&
(!(event_bits & EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE) &&
(event_bits & EventGroupBits::DECODER_MESSAGE_FINISHED))) {
@@ -203,16 +213,6 @@ AudioPipelineState AudioPipeline::process_state() {
return AudioPipelineState::STOPPED;
}
if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) {
xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR);
return AudioPipelineState::ERROR_READING;
}
if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) {
xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR);
return AudioPipelineState::ERROR_DECODING;
}
if (this->pause_state_) {
return AudioPipelineState::PAUSED;
}

View File

@@ -1,6 +1,6 @@
from esphome import pins
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components import i2c, key_provider
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
@@ -8,13 +8,16 @@ from esphome.const import (
CONF_INVERTED,
CONF_MODE,
CONF_NUMBER,
CONF_ON_KEY,
CONF_OPEN_DRAIN,
CONF_OUTPUT,
CONF_PULLDOWN,
CONF_PULLUP,
CONF_TRIGGER_ID,
)
CONF_KEYPAD = "keypad"
CONF_KEYS = "keys"
CONF_KEY_ROWS = "key_rows"
CONF_KEY_COLUMNS = "key_columns"
CONF_SLEEP_TIME = "sleep_time"
@@ -22,22 +25,47 @@ CONF_SCAN_TIME = "scan_time"
CONF_DEBOUNCE_TIME = "debounce_time"
CONF_SX1509_ID = "sx1509_id"
AUTO_LOAD = ["key_provider"]
DEPENDENCIES = ["i2c"]
MULTI_CONF = True
sx1509_ns = cg.esphome_ns.namespace("sx1509")
SX1509Component = sx1509_ns.class_("SX1509Component", cg.Component, i2c.I2CDevice)
SX1509Component = sx1509_ns.class_(
"SX1509Component", cg.Component, i2c.I2CDevice, key_provider.KeyProvider
)
SX1509GPIOPin = sx1509_ns.class_("SX1509GPIOPin", cg.GPIOPin)
SX1509KeyTrigger = sx1509_ns.class_(
"SX1509KeyTrigger", automation.Trigger.template(cg.uint8)
)
KEYPAD_SCHEMA = cv.Schema(
{
cv.Required(CONF_KEY_ROWS): cv.int_range(min=1, max=8),
cv.Required(CONF_KEY_COLUMNS): cv.int_range(min=1, max=8),
cv.Optional(CONF_SLEEP_TIME): cv.int_range(min=128, max=8192),
cv.Optional(CONF_SCAN_TIME): cv.int_range(min=1, max=128),
cv.Optional(CONF_DEBOUNCE_TIME): cv.int_range(min=1, max=64),
}
def check_keys(config):
if CONF_KEYS in config:
if len(config[CONF_KEYS]) != config[CONF_KEY_ROWS] * config[CONF_KEY_COLUMNS]:
raise cv.Invalid(
"The number of key codes must equal the number of rows * columns"
)
return config
KEYPAD_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_KEY_ROWS): cv.int_range(min=2, max=8),
cv.Required(CONF_KEY_COLUMNS): cv.int_range(min=1, max=8),
cv.Optional(CONF_SLEEP_TIME): cv.int_range(min=128, max=8192),
cv.Optional(CONF_SCAN_TIME): cv.int_range(min=1, max=128),
cv.Optional(CONF_DEBOUNCE_TIME): cv.int_range(min=1, max=64),
cv.Optional(CONF_KEYS): cv.string,
cv.Optional(CONF_ON_KEY): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(SX1509KeyTrigger),
}
),
}
),
check_keys,
)
CONFIG_SCHEMA = (
@@ -56,17 +84,22 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
if CONF_KEYPAD in config:
keypad = config[CONF_KEYPAD]
cg.add(var.set_rows_cols(keypad[CONF_KEY_ROWS], keypad[CONF_KEY_COLUMNS]))
if conf := config.get(CONF_KEYPAD):
cg.add(var.set_rows_cols(conf[CONF_KEY_ROWS], conf[CONF_KEY_COLUMNS]))
if (
CONF_SLEEP_TIME in keypad
and CONF_SCAN_TIME in keypad
and CONF_DEBOUNCE_TIME in keypad
CONF_SLEEP_TIME in conf
and CONF_SCAN_TIME in conf
and CONF_DEBOUNCE_TIME in conf
):
cg.add(var.set_sleep_time(keypad[CONF_SLEEP_TIME]))
cg.add(var.set_scan_time(keypad[CONF_SCAN_TIME]))
cg.add(var.set_debounce_time(keypad[CONF_DEBOUNCE_TIME]))
cg.add(var.set_sleep_time(conf[CONF_SLEEP_TIME]))
cg.add(var.set_scan_time(conf[CONF_SCAN_TIME]))
cg.add(var.set_debounce_time(conf[CONF_DEBOUNCE_TIME]))
if keys := conf.get(CONF_KEYS):
cg.add(var.set_keys(keys))
for tconf in conf.get(CONF_ON_KEY, []):
trigger = cg.new_Pvariable(tconf[CONF_TRIGGER_ID])
cg.add(var.register_key_trigger(trigger))
await automation.build_automation(trigger, [(cg.uint8, "x")], tconf)
def validate_mode(value):

View File

@@ -48,6 +48,30 @@ void SX1509Component::loop() {
uint16_t key_data = this->read_key_data();
for (auto *binary_sensor : this->keypad_binary_sensors_)
binary_sensor->process(key_data);
if (this->keys_.empty())
return;
if (key_data == 0) {
this->last_key_ = 0;
return;
}
int row, col;
for (row = 0; row < 7; row++) {
if (key_data & (1 << row))
break;
}
for (col = 8; col < 15; col++) {
if (key_data & (1 << col))
break;
}
col -= 8;
uint8_t key = this->keys_[row * this->cols_ + col];
if (key == this->last_key_)
return;
this->last_key_ = key;
ESP_LOGV(TAG, "row %d, col %d, key '%c'", row, col, key);
for (auto &trigger : this->key_triggers_)
trigger->trigger(key);
this->send_key_(key);
}
}
@@ -230,9 +254,9 @@ void SX1509Component::setup_keypad_() {
scan_time_bits &= 0b111; // Scan time is bits 2:0
temp_byte = sleep_time_ | scan_time_bits;
this->write_byte(REG_KEY_CONFIG_1, temp_byte);
rows_ = (rows_ - 1) & 0b111; // 0 = off, 0b001 = 2 rows, 0b111 = 8 rows, etc.
cols_ = (cols_ - 1) & 0b111; // 0b000 = 1 column, ob111 = 8 columns, etc.
this->write_byte(REG_KEY_CONFIG_2, (rows_ << 3) | cols_);
temp_byte = ((this->rows_ - 1) & 0b111) << 3; // 0 = off, 0b001 = 2 rows, 0b111 = 8 rows, etc.
temp_byte |= (this->cols_ - 1) & 0b111; // 0b000 = 1 column, ob111 = 8 columns, etc.
this->write_byte(REG_KEY_CONFIG_2, temp_byte);
}
uint16_t SX1509Component::read_key_data() {

View File

@@ -1,6 +1,7 @@
#pragma once
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/key_provider/key_provider.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "sx1509_gpio_pin.h"
@@ -27,7 +28,9 @@ class SX1509Processor {
virtual void process(uint16_t data){};
};
class SX1509Component : public Component, public i2c::I2CDevice {
class SX1509KeyTrigger : public Trigger<uint8_t> {};
class SX1509Component : public Component, public i2c::I2CDevice, public key_provider::KeyProvider {
public:
SX1509Component() = default;
@@ -47,12 +50,14 @@ class SX1509Component : public Component, public i2c::I2CDevice {
this->cols_ = cols;
this->has_keypad_ = true;
};
void set_keys(std::string keys) { this->keys_ = std::move(keys); };
void set_sleep_time(uint16_t sleep_time) { this->sleep_time_ = sleep_time; };
void set_scan_time(uint8_t scan_time) { this->scan_time_ = scan_time; };
void set_debounce_time(uint8_t debounce_time = 1) { this->debounce_time_ = debounce_time; };
void register_keypad_binary_sensor(SX1509Processor *binary_sensor) {
this->keypad_binary_sensors_.push_back(binary_sensor);
}
void register_key_trigger(SX1509KeyTrigger *trig) { this->key_triggers_.push_back(trig); };
void setup_led_driver(uint8_t pin);
protected:
@@ -65,10 +70,13 @@ class SX1509Component : public Component, public i2c::I2CDevice {
bool has_keypad_ = false;
uint8_t rows_ = 0;
uint8_t cols_ = 0;
std::string keys_;
uint16_t sleep_time_ = 128;
uint8_t scan_time_ = 1;
uint8_t debounce_time_ = 1;
uint8_t last_key_ = 0;
std::vector<SX1509Processor *> keypad_binary_sensors_;
std::vector<SX1509KeyTrigger *> key_triggers_;
uint32_t last_loop_timestamp_ = 0;
const uint32_t min_loop_period_ = 15; // ms

View File

@@ -1,5 +1,3 @@
from typing import Optional
from esphome import automation
import esphome.codegen as cg
from esphome.components import mqtt, web_server
@@ -92,9 +90,9 @@ async def setup_text_core_(
var,
config,
*,
min_length: Optional[int],
max_length: Optional[int],
pattern: Optional[str],
min_length: int | None,
max_length: int | None,
pattern: str | None,
):
await setup_entity(var, config)
@@ -121,9 +119,9 @@ async def register_text(
var,
config,
*,
min_length: Optional[int] = 0,
max_length: Optional[int] = 255,
pattern: Optional[str] = None,
min_length: int | None = 0,
max_length: int | None = 255,
pattern: str | None = None,
):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
@@ -136,9 +134,9 @@ async def register_text(
async def new_text(
config,
*,
min_length: Optional[int] = 0,
max_length: Optional[int] = 255,
pattern: Optional[str] = None,
min_length: int | None = 0,
max_length: int | None = 255,
pattern: str | None = None,
):
var = cg.new_Pvariable(config[CONF_ID])
await register_text(

View File

@@ -1,6 +1,5 @@
from importlib import resources
import logging
from typing import Optional
import tzlocal
@@ -40,7 +39,7 @@ SyncTrigger = time_ns.class_("SyncTrigger", automation.Trigger.template(), cg.Co
TimeHasTimeCondition = time_ns.class_("TimeHasTimeCondition", Condition)
def _load_tzdata(iana_key: str) -> Optional[bytes]:
def _load_tzdata(iana_key: str) -> bytes | None:
# From https://tzdata.readthedocs.io/en/latest/#examples
try:
package_loc, resource = iana_key.rsplit("/", 1)

View File

@@ -54,8 +54,8 @@ async def to_code(config):
cg.add(var.set_select_mappings(list(options_map.keys())))
parent = await cg.get_variable(config[CONF_TUYA_ID])
cg.add(var.set_tuya_parent(parent))
if enum_datapoint := config.get(CONF_ENUM_DATAPOINT, None) is not None:
if (enum_datapoint := config.get(CONF_ENUM_DATAPOINT, None)) is not None:
cg.add(var.set_select_id(enum_datapoint, False))
if int_datapoint := config.get(CONF_INT_DATAPOINT, None) is not None:
if (int_datapoint := config.get(CONF_INT_DATAPOINT, None)) is not None:
cg.add(var.set_select_id(int_datapoint, True))
cg.add(var.set_optimistic(config[CONF_OPTIMISTIC]))

View File

@@ -1,5 +1,4 @@
import re
from typing import Optional
from esphome import automation, pins
import esphome.codegen as cg
@@ -322,12 +321,12 @@ def final_validate_device_schema(
name: str,
*,
uart_bus: str = CONF_UART_ID,
baud_rate: Optional[int] = None,
baud_rate: int | None = None,
require_tx: bool = False,
require_rx: bool = False,
data_bits: Optional[int] = None,
parity: Optional[str] = None,
stop_bits: Optional[int] = None,
data_bits: int | None = None,
parity: str | None = None,
stop_bits: int | None = None,
):
def validate_baud_rate(value):
if value != baud_rate:

View File

@@ -0,0 +1,64 @@
import esphome.codegen as cg
from esphome.components.esp32 import (
VARIANT_ESP32S2,
VARIANT_ESP32S3,
add_idf_sdkconfig_option,
only_on_variant,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.cpp_types import Component
AUTO_LOAD = ["bytebuffer"]
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["esp32"]
usb_host_ns = cg.esphome_ns.namespace("usb_host")
USBHost = usb_host_ns.class_("USBHost", Component)
USBClient = usb_host_ns.class_("USBClient", Component)
CONF_DEVICES = "devices"
CONF_VID = "vid"
CONF_PID = "pid"
def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema:
schema = cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(cls),
}
)
if vid:
schema = schema.extend({cv.Optional(CONF_VID, default=vid): cv.hex_uint16_t})
else:
schema = schema.extend({cv.Required(CONF_VID): cv.hex_uint16_t})
if pid:
schema = schema.extend({cv.Optional(CONF_PID, default=pid): cv.hex_uint16_t})
else:
schema = schema.extend({cv.Required(CONF_PID): cv.hex_uint16_t})
return schema
CONFIG_SCHEMA = cv.All(
cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(USBHost),
cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()),
}
),
cv.only_with_esp_idf,
only_on_variant(supported=[VARIANT_ESP32S2, VARIANT_ESP32S3]),
)
async def register_usb_client(config):
var = cg.new_Pvariable(config[CONF_ID], config[CONF_VID], config[CONF_PID])
await cg.register_component(var, config)
return var
async def to_code(config):
add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024)
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
for device in config.get(CONF_DEVICES) or ():
await register_usb_client(device)

View File

@@ -0,0 +1,116 @@
#pragma once
// Should not be needed, but it's required to pass CI clang-tidy checks
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "esphome/core/component.h"
#include <vector>
#include "usb/usb_host.h"
#include <list>
namespace esphome {
namespace usb_host {
static const char *const TAG = "usb_host";
// constants for setup packet type
static const uint8_t USB_RECIP_DEVICE = 0;
static const uint8_t USB_RECIP_INTERFACE = 1;
static const uint8_t USB_RECIP_ENDPOINT = 2;
static const uint8_t USB_TYPE_STANDARD = 0 << 5;
static const uint8_t USB_TYPE_CLASS = 1 << 5;
static const uint8_t USB_TYPE_VENDOR = 2 << 5;
static const uint8_t USB_DIR_MASK = 1 << 7;
static const uint8_t USB_DIR_IN = 1 << 7;
static const uint8_t USB_DIR_OUT = 0;
static const size_t SETUP_PACKET_SIZE = 8;
static const size_t MAX_REQUESTS = 16; // maximum number of outstanding requests possible.
// used to report a transfer status
struct TransferStatus {
bool success;
uint16_t error_code;
uint8_t *data;
size_t data_len;
uint8_t endpoint;
void *user_data;
};
using transfer_cb_t = std::function<void(const TransferStatus &)>;
class USBClient;
// struct used to capture all data needed for a transfer
struct TransferRequest {
usb_transfer_t *transfer;
transfer_cb_t callback;
TransferStatus status;
USBClient *client;
};
// callback function type.
enum ClientState {
USB_CLIENT_INIT = 0,
USB_CLIENT_OPEN,
USB_CLIENT_CLOSE,
USB_CLIENT_GET_DESC,
USB_CLIENT_GET_INFO,
USB_CLIENT_CONNECTED,
};
class USBClient : public Component {
friend class USBHost;
public:
USBClient(uint16_t vid, uint16_t pid) : vid_(vid), pid_(pid) { init_pool(); }
void init_pool() {
this->trq_pool_.clear();
for (size_t i = 0; i != MAX_REQUESTS; i++)
this->trq_pool_.push_back(&this->requests_[i]);
}
void setup() override;
void loop() override;
// setup must happen after the host bus has been setup
float get_setup_priority() const override { return setup_priority::IO; }
void on_opened(uint8_t addr);
void on_removed(usb_device_handle_t handle);
void control_transfer_callback(const usb_transfer_t *xfer) const;
void transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length);
void transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length);
void dump_config() override;
void release_trq(TransferRequest *trq);
bool control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index, const transfer_cb_t &callback,
const std::vector<uint8_t> &data = {});
protected:
bool register_();
TransferRequest *get_trq_();
virtual void disconnect();
virtual void on_connected() {}
virtual void on_disconnected() { this->init_pool(); }
usb_host_client_handle_t handle_{};
usb_device_handle_t device_handle_{};
int device_addr_{-1};
int state_{USB_CLIENT_INIT};
uint16_t vid_{};
uint16_t pid_{};
std::list<TransferRequest *> trq_pool_{};
TransferRequest requests_[MAX_REQUESTS]{};
};
class USBHost : public Component {
public:
float get_setup_priority() const override { return setup_priority::BUS; }
void loop() override;
void setup() override;
protected:
std::vector<USBClient *> clients_{};
};
} // namespace usb_host
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3

View File

@@ -0,0 +1,392 @@
// Should not be needed, but it's required to pass CI clang-tidy checks
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "usb_host.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
#include <cinttypes>
#include <cstring>
namespace esphome {
namespace usb_host {
#pragma GCC diagnostic ignored "-Wparentheses"
using namespace bytebuffer;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
static void print_ep_desc(const usb_ep_desc_t *ep_desc) {
const char *ep_type_str;
int type = ep_desc->bmAttributes & USB_BM_ATTRIBUTES_XFERTYPE_MASK;
switch (type) {
case USB_BM_ATTRIBUTES_XFER_CONTROL:
ep_type_str = "CTRL";
break;
case USB_BM_ATTRIBUTES_XFER_ISOC:
ep_type_str = "ISOC";
break;
case USB_BM_ATTRIBUTES_XFER_BULK:
ep_type_str = "BULK";
break;
case USB_BM_ATTRIBUTES_XFER_INT:
ep_type_str = "INT";
break;
default:
ep_type_str = NULL;
break;
}
ESP_LOGV(TAG, "\t\t*** Endpoint descriptor ***");
ESP_LOGV(TAG, "\t\tbLength %d", ep_desc->bLength);
ESP_LOGV(TAG, "\t\tbDescriptorType %d", ep_desc->bDescriptorType);
ESP_LOGV(TAG, "\t\tbEndpointAddress 0x%x\tEP %d %s", ep_desc->bEndpointAddress, USB_EP_DESC_GET_EP_NUM(ep_desc),
USB_EP_DESC_GET_EP_DIR(ep_desc) ? "IN" : "OUT");
ESP_LOGV(TAG, "\t\tbmAttributes 0x%x\t%s", ep_desc->bmAttributes, ep_type_str);
ESP_LOGV(TAG, "\t\twMaxPacketSize %d", ep_desc->wMaxPacketSize);
ESP_LOGV(TAG, "\t\tbInterval %d", ep_desc->bInterval);
}
static void usbh_print_intf_desc(const usb_intf_desc_t *intf_desc) {
ESP_LOGV(TAG, "\t*** Interface descriptor ***");
ESP_LOGV(TAG, "\tbLength %d", intf_desc->bLength);
ESP_LOGV(TAG, "\tbDescriptorType %d", intf_desc->bDescriptorType);
ESP_LOGV(TAG, "\tbInterfaceNumber %d", intf_desc->bInterfaceNumber);
ESP_LOGV(TAG, "\tbAlternateSetting %d", intf_desc->bAlternateSetting);
ESP_LOGV(TAG, "\tbNumEndpoints %d", intf_desc->bNumEndpoints);
ESP_LOGV(TAG, "\tbInterfaceClass 0x%x", intf_desc->bInterfaceProtocol);
ESP_LOGV(TAG, "\tiInterface %d", intf_desc->iInterface);
}
static void usbh_print_cfg_desc(const usb_config_desc_t *cfg_desc) {
ESP_LOGV(TAG, "*** Configuration descriptor ***");
ESP_LOGV(TAG, "bLength %d", cfg_desc->bLength);
ESP_LOGV(TAG, "bDescriptorType %d", cfg_desc->bDescriptorType);
ESP_LOGV(TAG, "wTotalLength %d", cfg_desc->wTotalLength);
ESP_LOGV(TAG, "bNumInterfaces %d", cfg_desc->bNumInterfaces);
ESP_LOGV(TAG, "bConfigurationValue %d", cfg_desc->bConfigurationValue);
ESP_LOGV(TAG, "iConfiguration %d", cfg_desc->iConfiguration);
ESP_LOGV(TAG, "bmAttributes 0x%x", cfg_desc->bmAttributes);
ESP_LOGV(TAG, "bMaxPower %dmA", cfg_desc->bMaxPower * 2);
}
void usb_client_print_device_descriptor(const usb_device_desc_t *devc_desc) {
if (devc_desc == NULL) {
return;
}
ESP_LOGV(TAG, "*** Device descriptor ***");
ESP_LOGV(TAG, "bLength %d", devc_desc->bLength);
ESP_LOGV(TAG, "bDescriptorType %d", devc_desc->bDescriptorType);
ESP_LOGV(TAG, "bcdUSB %d.%d0", ((devc_desc->bcdUSB >> 8) & 0xF), ((devc_desc->bcdUSB >> 4) & 0xF));
ESP_LOGV(TAG, "bDeviceClass 0x%x", devc_desc->bDeviceClass);
ESP_LOGV(TAG, "bDeviceSubClass 0x%x", devc_desc->bDeviceSubClass);
ESP_LOGV(TAG, "bDeviceProtocol 0x%x", devc_desc->bDeviceProtocol);
ESP_LOGV(TAG, "bMaxPacketSize0 %d", devc_desc->bMaxPacketSize0);
ESP_LOGV(TAG, "idVendor 0x%x", devc_desc->idVendor);
ESP_LOGV(TAG, "idProduct 0x%x", devc_desc->idProduct);
ESP_LOGV(TAG, "bcdDevice %d.%d0", ((devc_desc->bcdDevice >> 8) & 0xF), ((devc_desc->bcdDevice >> 4) & 0xF));
ESP_LOGV(TAG, "iManufacturer %d", devc_desc->iManufacturer);
ESP_LOGV(TAG, "iProduct %d", devc_desc->iProduct);
ESP_LOGV(TAG, "iSerialNumber %d", devc_desc->iSerialNumber);
ESP_LOGV(TAG, "bNumConfigurations %d", devc_desc->bNumConfigurations);
}
void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc,
print_class_descriptor_cb class_specific_cb) {
if (cfg_desc == nullptr) {
return;
}
int offset = 0;
uint16_t w_total_length = cfg_desc->wTotalLength;
const usb_standard_desc_t *next_desc = (const usb_standard_desc_t *) cfg_desc;
do {
switch (next_desc->bDescriptorType) {
case USB_W_VALUE_DT_CONFIG:
usbh_print_cfg_desc((const usb_config_desc_t *) next_desc);
break;
case USB_W_VALUE_DT_INTERFACE:
usbh_print_intf_desc((const usb_intf_desc_t *) next_desc);
break;
case USB_W_VALUE_DT_ENDPOINT:
print_ep_desc((const usb_ep_desc_t *) next_desc);
break;
default:
if (class_specific_cb) {
class_specific_cb(next_desc);
}
break;
}
next_desc = usb_parse_next_descriptor(next_desc, w_total_length, &offset);
} while (next_desc != NULL);
}
#endif
static std::string get_descriptor_string(const usb_str_desc_t *desc) {
char buffer[256];
if (desc == nullptr)
return "(unknown)";
char *p = buffer;
for (size_t i = 0; i != desc->bLength / 2; i++) {
auto c = desc->wData[i];
if (c < 0x100)
*p++ = static_cast<char>(c);
}
*p = '\0';
return {buffer};
}
static void client_event_cb(const usb_host_client_event_msg_t *event_msg, void *ptr) {
auto *client = static_cast<USBClient *>(ptr);
switch (event_msg->event) {
case USB_HOST_CLIENT_EVENT_NEW_DEV: {
auto addr = event_msg->new_dev.address;
ESP_LOGD(TAG, "New device %d", event_msg->new_dev.address);
client->on_opened(addr);
break;
}
case USB_HOST_CLIENT_EVENT_DEV_GONE: {
client->on_removed(event_msg->dev_gone.dev_hdl);
ESP_LOGD(TAG, "Device gone %d", event_msg->new_dev.address);
break;
}
default:
ESP_LOGD(TAG, "Unknown event %d", event_msg->event);
break;
}
}
void USBClient::setup() {
usb_host_client_config_t config{.is_synchronous = false,
.max_num_event_msg = 5,
.async = {.client_event_callback = client_event_cb, .callback_arg = this}};
auto err = usb_host_client_register(&config, &this->handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "client register failed: %s", esp_err_to_name(err));
this->status_set_error("Client register failed");
this->mark_failed();
return;
}
for (auto trq : this->trq_pool_) {
usb_host_transfer_alloc(64, 0, &trq->transfer);
trq->client = this;
}
ESP_LOGCONFIG(TAG, "client setup complete");
}
void USBClient::loop() {
switch (this->state_) {
case USB_CLIENT_OPEN: {
int err;
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
this->state_ = USB_CLIENT_INIT;
break;
}
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
const usb_device_desc_t *desc;
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
this->disconnect();
} else {
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) {
usb_device_info_t dev_info;
if ((err = usb_host_device_info(this->device_handle_, &dev_info)) != ESP_OK) {
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
this->disconnect();
break;
}
this->state_ = USB_CLIENT_CONNECTED;
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
get_descriptor_string(dev_info.str_desc_manufacturer).c_str(),
get_descriptor_string(dev_info.str_desc_product).c_str(),
get_descriptor_string(dev_info.str_desc_serial_num).c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
const usb_device_desc_t *device_desc;
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
if (err == ESP_OK)
usb_client_print_device_descriptor(device_desc);
const usb_config_desc_t *config_desc;
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
if (err == ESP_OK)
usb_client_print_config_descriptor(config_desc, nullptr);
#endif
this->on_connected();
} else {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
}
}
break;
}
default:
usb_host_client_handle_events(this->handle_, 0);
break;
}
}
void USBClient::on_opened(uint8_t addr) {
if (this->state_ == USB_CLIENT_INIT) {
this->device_addr_ = addr;
this->state_ = USB_CLIENT_OPEN;
}
}
void USBClient::on_removed(usb_device_handle_t handle) {
if (this->device_handle_ == handle) {
this->disconnect();
}
}
static void control_callback(const usb_transfer_t *xfer) {
auto *trq = static_cast<TransferRequest *>(xfer->context);
trq->status.error_code = xfer->status;
trq->status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED;
trq->status.endpoint = xfer->bEndpointAddress;
trq->status.data = xfer->data_buffer;
trq->status.data_len = xfer->actual_num_bytes;
if (trq->callback != nullptr)
trq->callback(trq->status);
trq->client->release_trq(trq);
}
TransferRequest *USBClient::get_trq_() {
if (this->trq_pool_.empty()) {
ESP_LOGE(TAG, "Too many requests queued");
return nullptr;
}
auto *trq = this->trq_pool_.front();
this->trq_pool_.pop_front();
trq->client = this;
trq->transfer->context = trq;
trq->transfer->device_handle = this->device_handle_;
return trq;
}
void USBClient::disconnect() {
this->on_disconnected();
auto err = usb_host_device_close(this->handle_, this->device_handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Device close failed: %s", esp_err_to_name(err));
}
this->state_ = USB_CLIENT_INIT;
this->device_handle_ = nullptr;
this->device_addr_ = -1;
}
bool USBClient::control_transfer(uint8_t type, uint8_t request, uint16_t value, uint16_t index,
const transfer_cb_t &callback, const std::vector<uint8_t> &data) {
auto *trq = this->get_trq_();
if (trq == nullptr)
return false;
auto length = data.size();
if (length > sizeof(trq->transfer->data_buffer_size) - SETUP_PACKET_SIZE) {
ESP_LOGE(TAG, "Control transfer data size too large: %u > %u", length,
sizeof(trq->transfer->data_buffer_size) - sizeof(usb_setup_packet_t));
this->release_trq(trq);
return false;
}
auto control_packet = ByteBuffer(SETUP_PACKET_SIZE, LITTLE);
control_packet.put_uint8(type);
control_packet.put_uint8(request);
control_packet.put_uint16(value);
control_packet.put_uint16(index);
control_packet.put_uint16(length);
memcpy(trq->transfer->data_buffer, control_packet.get_data().data(), SETUP_PACKET_SIZE);
if (length != 0 && !(type & USB_DIR_IN)) {
memcpy(trq->transfer->data_buffer + SETUP_PACKET_SIZE, data.data(), length);
}
trq->callback = callback;
trq->transfer->bEndpointAddress = type & USB_DIR_MASK;
trq->transfer->num_bytes = static_cast<int>(length + SETUP_PACKET_SIZE);
trq->transfer->callback = reinterpret_cast<usb_transfer_cb_t>(control_callback);
auto err = usb_host_transfer_submit_control(this->handle_, trq->transfer);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit control transfer, err=%s", esp_err_to_name(err));
this->release_trq(trq);
return false;
}
return true;
}
static void transfer_callback(usb_transfer_t *xfer) {
auto *trq = static_cast<TransferRequest *>(xfer->context);
trq->status.error_code = xfer->status;
trq->status.success = xfer->status == USB_TRANSFER_STATUS_COMPLETED;
trq->status.endpoint = xfer->bEndpointAddress;
trq->status.data = xfer->data_buffer;
trq->status.data_len = xfer->actual_num_bytes;
if (trq->callback != nullptr)
trq->callback(trq->status);
trq->client->release_trq(trq);
}
/**
* Performs a transfer input operation.
*
* @param ep_address The endpoint address.
* @param callback The callback function to be called when the transfer is complete.
* @param length The length of the data to be transferred.
*
* @throws None.
*/
void USBClient::transfer_in(uint8_t ep_address, const transfer_cb_t &callback, uint16_t length) {
auto trq = this->get_trq_();
if (trq == nullptr) {
ESP_LOGE(TAG, "Too many requests queued");
return;
}
trq->callback = callback;
trq->transfer->callback = transfer_callback;
trq->transfer->bEndpointAddress = ep_address | USB_DIR_IN;
trq->transfer->num_bytes = length;
auto err = usb_host_transfer_submit(trq->transfer);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
this->release_trq(trq);
this->disconnect();
}
}
/**
* Performs an output transfer operation.
*
* @param ep_address The endpoint address.
* @param callback The callback function to be called when the transfer is complete.
* @param data The data to be transferred.
* @param length The length of the data to be transferred.
*
* @throws None.
*/
void USBClient::transfer_out(uint8_t ep_address, const transfer_cb_t &callback, const uint8_t *data, uint16_t length) {
auto trq = this->get_trq_();
if (trq == nullptr) {
ESP_LOGE(TAG, "Too many requests queued");
return;
}
trq->callback = callback;
trq->transfer->callback = transfer_callback;
trq->transfer->bEndpointAddress = ep_address | USB_DIR_OUT;
trq->transfer->num_bytes = length;
memcpy(trq->transfer->data_buffer, data, length);
auto err = usb_host_transfer_submit(trq->transfer);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to submit transfer, address=%x, length=%d, err=%x", ep_address, length, err);
this->release_trq(trq);
}
}
void USBClient::dump_config() {
ESP_LOGCONFIG(TAG, "USBClient");
ESP_LOGCONFIG(TAG, " Vendor id %04X", this->vid_);
ESP_LOGCONFIG(TAG, " Product id %04X", this->pid_);
}
void USBClient::release_trq(TransferRequest *trq) { this->trq_pool_.push_back(trq); }
} // namespace usb_host
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3

View File

@@ -0,0 +1,35 @@
// Should not be needed, but it's required to pass CI clang-tidy checks
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "usb_host.h"
#include <cinttypes>
#include "esphome/core/log.h"
namespace esphome {
namespace usb_host {
void USBHost::setup() {
ESP_LOGCONFIG(TAG, "Setup starts");
usb_host_config_t config{};
if (usb_host_install(&config) != ESP_OK) {
this->status_set_error("usb_host_install failed");
this->mark_failed();
return;
}
}
void USBHost::loop() {
int err;
uint32_t event_flags;
err = usb_host_lib_handle_events(0, &event_flags);
if (err != ESP_OK && err != ESP_ERR_TIMEOUT) {
ESP_LOGD(TAG, "lib_handle_events failed failed: %s", esp_err_to_name(err));
}
if (event_flags != 0) {
ESP_LOGD(TAG, "Event flags %" PRIu32 "X", event_flags);
}
}
} // namespace usb_host
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3

View File

@@ -0,0 +1,134 @@
import esphome.codegen as cg
from esphome.components.uart import (
CONF_DATA_BITS,
CONF_PARITY,
CONF_STOP_BITS,
UARTComponent,
)
from esphome.components.usb_host import register_usb_client, usb_device_schema
import esphome.config_validation as cv
from esphome.const import (
CONF_BAUD_RATE,
CONF_BUFFER_SIZE,
CONF_CHANNELS,
CONF_DEBUG,
CONF_DUMMY_RECEIVER,
CONF_ID,
)
from esphome.cpp_types import Component
AUTO_LOAD = ["uart", "usb_host", "bytebuffer"]
CODEOWNERS = ["@clydebarrow"]
usb_uart_ns = cg.esphome_ns.namespace("usb_uart")
USBUartComponent = usb_uart_ns.class_("USBUartComponent", Component)
USBUartChannel = usb_uart_ns.class_("USBUartChannel", UARTComponent)
UARTParityOptions = usb_uart_ns.enum("UARTParityOptions")
UART_PARITY_OPTIONS = {
"NONE": UARTParityOptions.UART_CONFIG_PARITY_NONE,
"EVEN": UARTParityOptions.UART_CONFIG_PARITY_EVEN,
"ODD": UARTParityOptions.UART_CONFIG_PARITY_ODD,
"MARK": UARTParityOptions.UART_CONFIG_PARITY_MARK,
"SPACE": UARTParityOptions.UART_CONFIG_PARITY_SPACE,
}
UARTStopBitsOptions = usb_uart_ns.enum("UARTStopBitsOptions")
UART_STOP_BITS_OPTIONS = {
"1": UARTStopBitsOptions.UART_CONFIG_STOP_BITS_1,
"1.5": UARTStopBitsOptions.UART_CONFIG_STOP_BITS_1_5,
"2": UARTStopBitsOptions.UART_CONFIG_STOP_BITS_2,
}
DEFAULT_BAUD_RATE = 9600
class Type:
def __init__(self, name, vid, pid, cls, max_channels=1, baud_rate_required=True):
self.name = name
cls = cls or name
self.vid = vid
self.pid = pid
self.cls = usb_uart_ns.class_(f"USBUartType{cls}", USBUartComponent)
self.max_channels = max_channels
self.baud_rate_required = baud_rate_required
uart_types = (
Type("CH34X", 0x1A86, 0x55D5, "CH34X", 3),
Type("CH340", 0x1A86, 0x7523, "CH34X", 1),
Type("ESP_JTAG", 0x303A, 0x1001, "CdcAcm", 1, baud_rate_required=False),
Type("STM32_VCP", 0x0483, 0x5740, "CdcAcm", 1, baud_rate_required=False),
Type("CDC_ACM", 0, 0, "CdcAcm", 1, baud_rate_required=False),
Type("CP210X", 0x10C4, 0xEA60, "CP210X", 3),
)
def channel_schema(channels, baud_rate_required):
return cv.Schema(
{
cv.Required(CONF_CHANNELS): cv.All(
cv.ensure_list(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(USBUartChannel),
cv.Optional(CONF_BUFFER_SIZE, default=256): cv.int_range(
min=64, max=8192
),
(
cv.Required(CONF_BAUD_RATE)
if baud_rate_required
else cv.Optional(
CONF_BAUD_RATE, default=DEFAULT_BAUD_RATE
)
): cv.int_range(min=300, max=1000000),
cv.Optional(CONF_STOP_BITS, default="1"): cv.enum(
UART_STOP_BITS_OPTIONS, upper=True
),
cv.Optional(CONF_PARITY, default="NONE"): cv.enum(
UART_PARITY_OPTIONS, upper=True
),
cv.Optional(CONF_DATA_BITS, default=8): cv.int_range(
min=5, max=8
),
cv.Optional(CONF_DUMMY_RECEIVER, default=False): cv.boolean,
cv.Optional(CONF_DEBUG, default=False): cv.boolean,
}
)
),
cv.Length(max=channels),
)
}
)
CONFIG_SCHEMA = cv.ensure_list(
cv.typed_schema(
{
it.name: usb_device_schema(it.cls, it.vid, it.pid).extend(
channel_schema(it.max_channels, it.baud_rate_required)
)
for it in uart_types
},
upper=True,
)
)
async def to_code(config):
for device in config:
var = await register_usb_client(device)
for index, channel in enumerate(device[CONF_CHANNELS]):
chvar = cg.new_Pvariable(channel[CONF_ID], index, channel[CONF_BUFFER_SIZE])
await cg.register_parented(chvar, var)
cg.add(chvar.set_rx_buffer_size(channel[CONF_BUFFER_SIZE]))
cg.add(chvar.set_stop_bits(channel[CONF_STOP_BITS]))
cg.add(chvar.set_data_bits(channel[CONF_DATA_BITS]))
cg.add(chvar.set_parity(channel[CONF_PARITY]))
cg.add(chvar.set_baud_rate(channel[CONF_BAUD_RATE]))
cg.add(chvar.set_dummy_receiver(channel[CONF_DUMMY_RECEIVER]))
cg.add(chvar.set_debug(channel[CONF_DEBUG]))
cg.add(var.add_channel(chvar))
if channel[CONF_DEBUG]:
cg.add_define("USE_UART_DEBUGGER")

View File

@@ -0,0 +1,80 @@
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "usb_uart.h"
#include "usb/usb_host.h"
#include "esphome/core/log.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
namespace esphome {
namespace usb_uart {
using namespace bytebuffer;
/**
* CH34x
*/
void USBUartTypeCH34X::enable_channels() {
// enable the channels
for (auto channel : this->channels_) {
if (!channel->initialised_)
continue;
usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) {
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
channel->initialised_ = false;
}
};
uint8_t divisor = 7;
uint32_t clk = 12000000;
auto baud_rate = channel->baud_rate_;
if (baud_rate < 256000) {
if (baud_rate > 6000000 / 255) {
divisor = 3;
clk = 6000000;
} else if (baud_rate > 750000 / 255) {
divisor = 2;
clk = 750000;
} else if (baud_rate > 93750 / 255) {
divisor = 1;
clk = 93750;
} else {
divisor = 0;
clk = 11719;
}
}
ESP_LOGV(TAG, "baud_rate: %" PRIu32 ", divisor: %d, clk: %" PRIu32, baud_rate, divisor, clk);
auto factor = static_cast<uint8_t>(clk / baud_rate);
if (factor == 0 || factor == 0xFF) {
ESP_LOGE(TAG, "Invalid baud rate %" PRIu32, baud_rate);
channel->initialised_ = false;
continue;
}
if ((clk / factor - baud_rate) > (baud_rate - clk / (factor + 1)))
factor++;
factor = 256 - factor;
uint16_t value = 0xC0;
if (channel->stop_bits_ == UART_CONFIG_STOP_BITS_2)
value |= 4;
switch (channel->parity_) {
case UART_CONFIG_PARITY_NONE:
break;
default:
value |= 8 | ((channel->parity_ - 1) << 4);
break;
}
value |= channel->data_bits_ - 5;
value <<= 8;
value |= 0x8C;
uint8_t cmd = 0xA1 + channel->index_;
if (channel->index_ >= 2)
cmd += 0xE;
this->control_transfer(USB_VENDOR_DEV | usb_host::USB_DIR_OUT, cmd, value, (factor << 8) | divisor, callback);
}
USBUartTypeCdcAcm::enable_channels();
}
} // namespace usb_uart
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3

View File

@@ -0,0 +1,126 @@
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "usb_uart.h"
#include "usb/usb_host.h"
#include "esphome/core/log.h"
#include "esphome/components/bytebuffer/bytebuffer.h"
namespace esphome {
namespace usb_uart {
using namespace bytebuffer;
/**
* Silabs CP210x Commands
*/
static constexpr uint8_t IFC_ENABLE = 0x00; // Enable or disable the interface.
static constexpr uint8_t SET_BAUDDIV = 0x01; // Set the baud rate divisor.
static constexpr uint8_t GET_BAUDDIV = 0x02; // Get the baud rate divisor.
static constexpr uint8_t SET_LINE_CTL = 0x03; // Set the line control.
static constexpr uint8_t GET_LINE_CTL = 0x04; // Get the line control.
static constexpr uint8_t SET_BREAK = 0x05; // Set a BREAK.
static constexpr uint8_t IMM_CHAR = 0x06; // Send character out of order.
static constexpr uint8_t SET_MHS = 0x07; // Set modem handshaking.
static constexpr uint8_t GET_MDMSTS = 0x08; // Get modem status.
static constexpr uint8_t SET_XON = 0x09; // Emulate XON.
static constexpr uint8_t SET_XOFF = 0x0A; // Emulate XOFF.
static constexpr uint8_t SET_EVENTMASK = 0x0B; // Set the event mask.
static constexpr uint8_t GET_EVENTMASK = 0x0C; // Get the event mask.
static constexpr uint8_t GET_EVENTSTATE = 0x16; // Get the event state.
static constexpr uint8_t SET_RECEIVE = 0x17; // Set receiver max timeout.
static constexpr uint8_t GET_RECEIVE = 0x18; // Get receiver max timeout.
static constexpr uint8_t SET_CHAR = 0x0D; // Set special character individually.
static constexpr uint8_t GET_CHARS = 0x0E; // Get special characters.
static constexpr uint8_t GET_PROPS = 0x0F; // Get properties.
static constexpr uint8_t GET_COMM_STATUS = 0x10; // Get the serial status.
static constexpr uint8_t RESET = 0x11; // Reset.
static constexpr uint8_t PURGE = 0x12; // Purge.
static constexpr uint8_t SET_FLOW = 0x13; // Set flow control.
static constexpr uint8_t GET_FLOW = 0x14; // Get flow control.
static constexpr uint8_t EMBED_EVENTS = 0x15; // Control embedding of events in the data stream.
static constexpr uint8_t GET_BAUDRATE = 0x1D; // Get the baud rate.
static constexpr uint8_t SET_BAUDRATE = 0x1E; // Set the baud rate.
static constexpr uint8_t SET_CHARS = 0x19; // Set special characters.
static constexpr uint8_t VENDOR_SPECIFIC = 0xFF; // Vendor specific command.
std::vector<CdcEps> USBUartTypeCP210X::parse_descriptors_(usb_device_handle_t dev_hdl) {
const usb_config_desc_t *config_desc;
const usb_device_desc_t *device_desc;
int conf_offset = 0, ep_offset;
std::vector<CdcEps> cdc_devs{};
// Get required descriptors
if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) {
ESP_LOGE(TAG, "get_device_descriptor failed");
return {};
}
if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) {
ESP_LOGE(TAG, "get_active_config_descriptor failed");
return {};
}
ESP_LOGD(TAG, "bDeviceClass: %u, bDeviceSubClass: %u", device_desc->bDeviceClass, device_desc->bDeviceSubClass);
ESP_LOGD(TAG, "bNumInterfaces: %u", config_desc->bNumInterfaces);
if (device_desc->bDeviceClass != 0) {
ESP_LOGE(TAG, "bDeviceClass != 0");
return {};
}
for (uint8_t i = 0; i != config_desc->bNumInterfaces; i++) {
auto data_desc = usb_parse_interface_descriptor(config_desc, 0, 0, &conf_offset);
if (!data_desc) {
ESP_LOGE(TAG, "data_desc: usb_parse_interface_descriptor failed");
break;
}
if (data_desc->bNumEndpoints != 2 || data_desc->bInterfaceClass != USB_CLASS_VENDOR_SPEC) {
ESP_LOGE(TAG, "data_desc: bInterfaceClass == %u, bInterfaceSubClass == %u, bNumEndpoints == %u",
data_desc->bInterfaceClass, data_desc->bInterfaceSubClass, data_desc->bNumEndpoints);
continue;
}
ep_offset = conf_offset;
auto out_ep = usb_parse_endpoint_descriptor_by_index(data_desc, 0, config_desc->wTotalLength, &ep_offset);
if (!out_ep) {
ESP_LOGE(TAG, "out_ep: usb_parse_endpoint_descriptor_by_index failed");
continue;
}
ep_offset = conf_offset;
auto in_ep = usb_parse_endpoint_descriptor_by_index(data_desc, 1, config_desc->wTotalLength, &ep_offset);
if (!in_ep) {
ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed");
continue;
}
if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN) {
cdc_devs.push_back({CdcEps{nullptr, in_ep, out_ep, data_desc->bInterfaceNumber}});
} else {
cdc_devs.push_back({CdcEps{nullptr, out_ep, in_ep, data_desc->bInterfaceNumber}});
}
}
return cdc_devs;
}
void USBUartTypeCP210X::enable_channels() {
// enable the channels
for (auto channel : this->channels_) {
if (!channel->initialised_)
continue;
usb_host::transfer_cb_t callback = [=](const usb_host::TransferStatus &status) {
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
channel->initialised_ = false;
}
};
this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, IFC_ENABLE, 1, channel->index_, callback);
uint16_t line_control = channel->stop_bits_;
line_control |= static_cast<uint8_t>(channel->parity_) << 4;
line_control |= channel->data_bits_ << 8;
ESP_LOGD(TAG, "Line control value 0x%X", line_control);
this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_LINE_CTL, line_control, channel->index_,
callback);
auto baud = ByteBuffer::wrap(channel->baud_rate_, LITTLE);
this->control_transfer(USB_VENDOR_IFC | usb_host::USB_DIR_OUT, SET_BAUDRATE, 0, channel->index_, callback,
baud.get_data());
}
USBUartTypeCdcAcm::enable_channels();
}
} // namespace usb_uart
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3

View File

@@ -0,0 +1,325 @@
// Should not be needed, but it's required to pass CI clang-tidy checks
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "usb_uart.h"
#include "esphome/core/log.h"
#include "esphome/components/uart/uart_debugger.h"
#include <cinttypes>
namespace esphome {
namespace usb_uart {
/**
*
* Given a configuration, look for the required interfaces defining a CDC-ACM device
* @param config_desc The configuration descriptor
* @param intf_idx The index of the interface to be examined
* @return
*/
static optional<CdcEps> get_cdc(const usb_config_desc_t *config_desc, uint8_t intf_idx) {
int conf_offset, ep_offset;
const usb_ep_desc_t *notify_ep{}, *in_ep{}, *out_ep{};
uint8_t interface_number = 0;
// look for an interface with one interrupt endpoint (notify), and an interface with two bulk endpoints (data in/out)
for (;;) {
auto intf_desc = usb_parse_interface_descriptor(config_desc, intf_idx++, 0, &conf_offset);
if (!intf_desc) {
ESP_LOGE(TAG, "usb_parse_interface_descriptor failed");
return nullopt;
}
if (intf_desc->bNumEndpoints == 1) {
ep_offset = conf_offset;
notify_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 0, config_desc->wTotalLength, &ep_offset);
if (!notify_ep) {
ESP_LOGE(TAG, "notify_ep: usb_parse_endpoint_descriptor_by_index failed");
return nullopt;
}
if (notify_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_INT)
notify_ep = nullptr;
} else if (USB_CLASS_CDC_DATA && intf_desc->bNumEndpoints == 2) {
interface_number = intf_desc->bInterfaceNumber;
ep_offset = conf_offset;
out_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 0, config_desc->wTotalLength, &ep_offset);
if (!out_ep) {
ESP_LOGE(TAG, "out_ep: usb_parse_endpoint_descriptor_by_index failed");
return nullopt;
}
if (out_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_BULK)
out_ep = nullptr;
ep_offset = conf_offset;
in_ep = usb_parse_endpoint_descriptor_by_index(intf_desc, 1, config_desc->wTotalLength, &ep_offset);
if (!in_ep) {
ESP_LOGE(TAG, "in_ep: usb_parse_endpoint_descriptor_by_index failed");
return nullopt;
}
if (in_ep->bmAttributes != USB_BM_ATTRIBUTES_XFER_BULK)
in_ep = nullptr;
}
if (in_ep != nullptr && out_ep != nullptr && notify_ep != nullptr)
break;
}
if (in_ep->bEndpointAddress & usb_host::USB_DIR_IN)
return CdcEps{notify_ep, in_ep, out_ep, interface_number};
return CdcEps{notify_ep, out_ep, in_ep, interface_number};
}
std::vector<CdcEps> USBUartTypeCdcAcm::parse_descriptors_(usb_device_handle_t dev_hdl) {
const usb_config_desc_t *config_desc;
const usb_device_desc_t *device_desc;
int desc_offset = 0;
std::vector<CdcEps> cdc_devs{};
// Get required descriptors
if (usb_host_get_device_descriptor(dev_hdl, &device_desc) != ESP_OK) {
ESP_LOGE(TAG, "get_device_descriptor failed");
return {};
}
if (usb_host_get_active_config_descriptor(dev_hdl, &config_desc) != ESP_OK) {
ESP_LOGE(TAG, "get_active_config_descriptor failed");
return {};
}
if (device_desc->bDeviceClass == USB_CLASS_COMM) {
// single CDC-ACM device
if (auto eps = get_cdc(config_desc, 0)) {
ESP_LOGV(TAG, "Found CDC-ACM device");
cdc_devs.push_back(*eps);
}
return cdc_devs;
}
if (((device_desc->bDeviceClass == USB_CLASS_MISC) && (device_desc->bDeviceSubClass == USB_SUBCLASS_COMMON) &&
(device_desc->bDeviceProtocol == USB_DEVICE_PROTOCOL_IAD)) ||
((device_desc->bDeviceClass == USB_CLASS_PER_INTERFACE) && (device_desc->bDeviceSubClass == USB_SUBCLASS_NULL) &&
(device_desc->bDeviceProtocol == USB_PROTOCOL_NULL))) {
// This is a composite device, that uses Interface Association Descriptor
const auto *this_desc = reinterpret_cast<const usb_standard_desc_t *>(config_desc);
for (;;) {
this_desc = usb_parse_next_descriptor_of_type(this_desc, config_desc->wTotalLength,
USB_B_DESCRIPTOR_TYPE_INTERFACE_ASSOCIATION, &desc_offset);
if (!this_desc)
break;
const auto *iad_desc = reinterpret_cast<const usb_iad_desc_t *>(this_desc);
if (iad_desc->bFunctionClass == USB_CLASS_COMM && iad_desc->bFunctionSubClass == USB_CDC_SUBCLASS_ACM) {
ESP_LOGV(TAG, "Found CDC-ACM device in composite device");
if (auto eps = get_cdc(config_desc, iad_desc->bFirstInterface))
cdc_devs.push_back(*eps);
}
}
}
return cdc_devs;
}
void RingBuffer::push(uint8_t item) {
this->buffer_[this->insert_pos_] = item;
this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
}
void RingBuffer::push(const uint8_t *data, size_t len) {
for (size_t i = 0; i != len; i++) {
this->buffer_[this->insert_pos_] = *data++;
this->insert_pos_ = (this->insert_pos_ + 1) % this->buffer_size_;
}
}
uint8_t RingBuffer::pop() {
uint8_t item = this->buffer_[this->read_pos_];
this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_;
return item;
}
size_t RingBuffer::pop(uint8_t *data, size_t len) {
len = std::min(len, this->get_available());
for (size_t i = 0; i != len; i++) {
*data++ = this->buffer_[this->read_pos_];
this->read_pos_ = (this->read_pos_ + 1) % this->buffer_size_;
}
return len;
}
void USBUartChannel::write_array(const uint8_t *data, size_t len) {
if (!this->initialised_) {
ESP_LOGV(TAG, "Channel not initialised - write ignored");
return;
}
while (this->output_buffer_.get_free_space() != 0 && len-- != 0) {
this->output_buffer_.push(*data++);
}
len++;
if (len > 0) {
ESP_LOGE(TAG, "Buffer full - failed to write %d bytes", len);
}
this->parent_->start_output(this);
}
bool USBUartChannel::peek_byte(uint8_t *data) {
if (this->input_buffer_.is_empty()) {
return false;
}
*data = this->input_buffer_.peek();
return true;
}
bool USBUartChannel::read_array(uint8_t *data, size_t len) {
if (!this->initialised_) {
ESP_LOGV(TAG, "Channel not initialised - read ignored");
return false;
}
auto available = this->available();
bool status = true;
if (len > available) {
ESP_LOGV(TAG, "underflow: requested %zu but returned %d, bytes", len, available);
len = available;
status = false;
}
for (size_t i = 0; i != len; i++) {
*data++ = this->input_buffer_.pop();
}
this->parent_->start_input(this);
return status;
}
void USBUartComponent::setup() { USBClient::setup(); }
void USBUartComponent::loop() { USBClient::loop(); }
void USBUartComponent::dump_config() {
USBClient::dump_config();
for (auto &channel : this->channels_) {
ESP_LOGCONFIG(TAG, " UART Channel %d", channel->index_);
ESP_LOGCONFIG(TAG, " Baud Rate: %" PRIu32 " baud", channel->baud_rate_);
ESP_LOGCONFIG(TAG, " Data Bits: %u", channel->data_bits_);
ESP_LOGCONFIG(TAG, " Parity: %s", PARITY_NAMES[channel->parity_]);
ESP_LOGCONFIG(TAG, " Stop bits: %s", STOP_BITS_NAMES[channel->stop_bits_]);
ESP_LOGCONFIG(TAG, " Debug: %s", YESNO(channel->debug_));
ESP_LOGCONFIG(TAG, " Dummy receiver: %s", YESNO(channel->dummy_receiver_));
}
}
void USBUartComponent::start_input(USBUartChannel *channel) {
if (!channel->initialised_ || channel->input_started_ ||
channel->input_buffer_.get_free_space() < channel->cdc_dev_.in_ep->wMaxPacketSize)
return;
auto ep = channel->cdc_dev_.in_ep;
auto callback = [this, channel](const usb_host::TransferStatus &status) {
ESP_LOGV(TAG, "Transfer result: length: %u; status %X", status.data_len, status.error_code);
if (!status.success) {
ESP_LOGE(TAG, "Control transfer failed, status=%s", esp_err_to_name(status.error_code));
return;
}
#ifdef USE_UART_DEBUGGER
if (channel->debug_) {
uart::UARTDebug::log_hex(uart::UART_DIRECTION_RX,
std::vector<uint8_t>(status.data, status.data + status.data_len), ','); // NOLINT()
}
#endif
channel->input_started_ = false;
if (!channel->dummy_receiver_) {
for (size_t i = 0; i != status.data_len; i++) {
channel->input_buffer_.push(status.data[i]);
}
}
if (channel->input_buffer_.get_free_space() >= channel->cdc_dev_.in_ep->wMaxPacketSize) {
this->defer([this, channel] { this->start_input(channel); });
}
};
channel->input_started_ = true;
this->transfer_in(ep->bEndpointAddress, callback, ep->wMaxPacketSize);
}
void USBUartComponent::start_output(USBUartChannel *channel) {
if (channel->output_started_)
return;
if (channel->output_buffer_.is_empty()) {
return;
}
auto ep = channel->cdc_dev_.out_ep;
auto callback = [this, channel](const usb_host::TransferStatus &status) {
ESP_LOGV(TAG, "Output Transfer result: length: %u; status %X", status.data_len, status.error_code);
channel->output_started_ = false;
this->defer([this, channel] { this->start_output(channel); });
};
channel->output_started_ = true;
uint8_t data[ep->wMaxPacketSize];
auto len = channel->output_buffer_.pop(data, ep->wMaxPacketSize);
this->transfer_out(ep->bEndpointAddress, callback, data, len);
#ifdef USE_UART_DEBUGGER
if (channel->debug_) {
uart::UARTDebug::log_hex(uart::UART_DIRECTION_TX, std::vector<uint8_t>(data, data + len), ','); // NOLINT()
}
#endif
ESP_LOGV(TAG, "Output %d bytes started", len);
}
/**
* Hacky fix for some devices that report incorrect MPS values
* @param ep The endpoint descriptor
*/
static void fix_mps(const usb_ep_desc_t *ep) {
if (ep != nullptr) {
auto *ep_mutable = const_cast<usb_ep_desc_t *>(ep);
if (ep->wMaxPacketSize > 64) {
ESP_LOGW(TAG, "Corrected MPS of EP %u from %u to 64", ep->bEndpointAddress, ep->wMaxPacketSize);
ep_mutable->wMaxPacketSize = 64;
}
}
}
void USBUartTypeCdcAcm::on_connected() {
auto cdc_devs = this->parse_descriptors_(this->device_handle_);
if (cdc_devs.empty()) {
this->status_set_error("No CDC-ACM device found");
this->disconnect();
return;
}
ESP_LOGD(TAG, "Found %zu CDC-ACM devices", cdc_devs.size());
auto i = 0;
for (auto channel : this->channels_) {
if (i == cdc_devs.size()) {
ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_);
this->status_set_warning("No configuration found for channel");
break;
}
channel->cdc_dev_ = cdc_devs[i++];
fix_mps(channel->cdc_dev_.in_ep);
fix_mps(channel->cdc_dev_.out_ep);
channel->initialised_ = true;
auto err = usb_host_interface_claim(this->handle_, this->device_handle_, channel->cdc_dev_.interface_number, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "usb_host_interface_claim failed: %s, channel=%d, intf=%d", esp_err_to_name(err), channel->index_,
channel->cdc_dev_.interface_number);
this->status_set_error("usb_host_interface_claim failed");
this->disconnect();
return;
}
}
this->enable_channels();
}
void USBUartTypeCdcAcm::on_disconnected() {
for (auto channel : this->channels_) {
if (channel->cdc_dev_.in_ep != nullptr) {
usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress);
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.in_ep->bEndpointAddress);
}
if (channel->cdc_dev_.out_ep != nullptr) {
usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.out_ep->bEndpointAddress);
}
if (channel->cdc_dev_.notify_ep != nullptr) {
usb_host_endpoint_halt(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
usb_host_endpoint_flush(this->device_handle_, channel->cdc_dev_.notify_ep->bEndpointAddress);
}
usb_host_interface_release(this->handle_, this->device_handle_, channel->cdc_dev_.interface_number);
channel->initialised_ = false;
channel->input_started_ = false;
channel->output_started_ = false;
channel->input_buffer_.clear();
channel->output_buffer_.clear();
}
USBClient::on_disconnected();
}
void USBUartTypeCdcAcm::enable_channels() {
for (auto channel : this->channels_) {
if (!channel->initialised_)
continue;
channel->input_started_ = false;
channel->output_started_ = false;
this->start_input(channel);
}
}
} // namespace usb_uart
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3

View File

@@ -0,0 +1,151 @@
#pragma once
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
#include "esphome/components/uart/uart_component.h"
#include "esphome/components/usb_host/usb_host.h"
namespace esphome {
namespace usb_uart {
class USBUartTypeCdcAcm;
class USBUartComponent;
static const char *const TAG = "usb_uart";
static constexpr uint8_t USB_CDC_SUBCLASS_ACM = 0x02;
static constexpr uint8_t USB_SUBCLASS_COMMON = 0x02;
static constexpr uint8_t USB_SUBCLASS_NULL = 0x00;
static constexpr uint8_t USB_PROTOCOL_NULL = 0x00;
static constexpr uint8_t USB_DEVICE_PROTOCOL_IAD = 0x01;
static constexpr uint8_t USB_VENDOR_IFC = usb_host::USB_TYPE_VENDOR | usb_host::USB_RECIP_INTERFACE;
static constexpr uint8_t USB_VENDOR_DEV = usb_host::USB_TYPE_VENDOR | usb_host::USB_RECIP_DEVICE;
struct CdcEps {
const usb_ep_desc_t *notify_ep;
const usb_ep_desc_t *in_ep;
const usb_ep_desc_t *out_ep;
uint8_t interface_number;
};
enum UARTParityOptions {
UART_CONFIG_PARITY_NONE = 0,
UART_CONFIG_PARITY_ODD,
UART_CONFIG_PARITY_EVEN,
UART_CONFIG_PARITY_MARK,
UART_CONFIG_PARITY_SPACE,
};
enum UARTStopBitsOptions {
UART_CONFIG_STOP_BITS_1 = 0,
UART_CONFIG_STOP_BITS_1_5,
UART_CONFIG_STOP_BITS_2,
};
static const char *const PARITY_NAMES[] = {"NONE", "ODD", "EVEN", "MARK", "SPACE"};
static const char *const STOP_BITS_NAMES[] = {"1", "1.5", "2"};
class RingBuffer {
public:
RingBuffer(uint16_t buffer_size) : buffer_size_(buffer_size), buffer_(new uint8_t[buffer_size]) {}
bool is_empty() const { return this->read_pos_ == this->insert_pos_; }
size_t get_available() const {
return (this->insert_pos_ + this->buffer_size_ - this->read_pos_) % this->buffer_size_;
};
size_t get_free_space() const { return this->buffer_size_ - 1 - this->get_available(); }
uint8_t peek() const { return this->buffer_[this->read_pos_]; }
void push(uint8_t item);
void push(const uint8_t *data, size_t len);
uint8_t pop();
size_t pop(uint8_t *data, size_t len);
void clear() { this->read_pos_ = this->insert_pos_ = 0; }
protected:
uint16_t insert_pos_ = 0;
uint16_t read_pos_ = 0;
uint16_t buffer_size_;
uint8_t *buffer_;
};
class USBUartChannel : public uart::UARTComponent, public Parented<USBUartComponent> {
friend class USBUartComponent;
friend class USBUartTypeCdcAcm;
friend class USBUartTypeCP210X;
friend class USBUartTypeCH34X;
public:
USBUartChannel(uint8_t index, uint16_t buffer_size)
: index_(index), input_buffer_(RingBuffer(buffer_size)), output_buffer_(RingBuffer(buffer_size)) {}
void write_array(const uint8_t *data, size_t len) override;
;
bool peek_byte(uint8_t *data) override;
;
bool read_array(uint8_t *data, size_t len) override;
int available() override { return static_cast<int>(this->input_buffer_.get_available()); }
void flush() override {}
void check_logger_conflict() override {}
void set_parity(UARTParityOptions parity) { this->parity_ = parity; }
void set_debug(bool debug) { this->debug_ = debug; }
void set_dummy_receiver(bool dummy_receiver) { this->dummy_receiver_ = dummy_receiver; }
protected:
const uint8_t index_;
RingBuffer input_buffer_;
RingBuffer output_buffer_;
UARTParityOptions parity_{UART_CONFIG_PARITY_NONE};
bool input_started_{true};
bool output_started_{true};
CdcEps cdc_dev_{};
bool debug_{};
bool dummy_receiver_{};
bool initialised_{};
};
class USBUartComponent : public usb_host::USBClient {
public:
USBUartComponent(uint16_t vid, uint16_t pid) : usb_host::USBClient(vid, pid) {}
void setup() override;
void loop() override;
void dump_config() override;
std::vector<USBUartChannel *> get_channels() { return this->channels_; }
void add_channel(USBUartChannel *channel) { this->channels_.push_back(channel); }
void start_input(USBUartChannel *channel);
void start_output(USBUartChannel *channel);
protected:
std::vector<USBUartChannel *> channels_{};
};
class USBUartTypeCdcAcm : public USBUartComponent {
public:
USBUartTypeCdcAcm(uint16_t vid, uint16_t pid) : USBUartComponent(vid, pid) {}
protected:
virtual std::vector<CdcEps> parse_descriptors_(usb_device_handle_t dev_hdl);
void on_connected() override;
virtual void enable_channels();
void on_disconnected() override;
};
class USBUartTypeCP210X : public USBUartTypeCdcAcm {
public:
USBUartTypeCP210X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {}
protected:
std::vector<CdcEps> parse_descriptors_(usb_device_handle_t dev_hdl) override;
void enable_channels() override;
};
class USBUartTypeCH34X : public USBUartTypeCdcAcm {
public:
USBUartTypeCH34X(uint16_t vid, uint16_t pid) : USBUartTypeCdcAcm(vid, pid) {}
protected:
void enable_channels() override;
};
} // namespace usb_uart
} // namespace esphome
#endif // USE_ESP32_VARIANT_ESP32S2 || USE_ESP32_VARIANT_ESP32S3

View File

@@ -483,14 +483,16 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
// Enable WiFi
global_wifi_component->enable();
// Set timeout for the connection
this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this]() {
this->connecting_ = false;
this->set_timeout("wifi-connect-timeout", this->connection_timeout_.value(x...), [this, x...]() {
// If the timeout is reached, stop connecting and revert to the old AP
global_wifi_component->disable();
global_wifi_component->save_wifi_sta(old_sta_.get_ssid(), old_sta_.get_password());
global_wifi_component->enable();
// Callback to notify the user that the connection failed
this->error_trigger_->trigger();
// Start a timeout for the fallback if the connection to the old AP fails
this->set_timeout("wifi-fallback-timeout", this->connection_timeout_.value(x...), [this]() {
this->connecting_ = false;
this->error_trigger_->trigger();
});
});
}
@@ -503,6 +505,7 @@ template<typename... Ts> class WiFiConfigureAction : public Action<Ts...>, publi
if (global_wifi_component->is_connected()) {
// The WiFi is connected, stop the timeout and reset the connecting flag
this->cancel_timeout("wifi-connect-timeout");
this->cancel_timeout("wifi-fallback-timeout");
this->connecting_ = false;
if (global_wifi_component->wifi_ssid() == this->new_sta_.get_ssid()) {
// Callback to notify the user that the connection was successful

View File

@@ -1,11 +1,10 @@
import esphome.codegen as cg
from esphome.components import text_sensor, uart
import esphome.config_validation as cv
from esphome.const import ICON_FINGERPRINT
from esphome.const import CONF_RESET, ICON_FINGERPRINT
CODEOWNERS = ["@hobbypunk90"]
DEPENDENCIES = ["uart"]
CONF_RESET = "reset"
wl134_ns = cg.esphome_ns.namespace("wl_134")
Wl134Component = wl134_ns.class_(

View File

@@ -7,7 +7,7 @@ import functools
import heapq
import logging
import re
from typing import Any, Union
from typing import Any
import voluptuous as vol
@@ -63,7 +63,7 @@ def iter_component_configs(config):
yield p_name, platform, p_config
ConfigPath = list[Union[str, int]]
ConfigPath = list[str | int]
path_context = contextvars.ContextVar("Config path")

View File

@@ -982,23 +982,32 @@ def uuid(value):
METRIC_SUFFIXES = {
"E": 1e18,
"P": 1e15,
"T": 1e12,
"G": 1e9,
"M": 1e6,
"k": 1e3,
"da": 10,
"d": 1e-1,
"c": 1e-2,
"m": 0.001,
"µ": 1e-6,
"u": 1e-6,
"n": 1e-9,
"p": 1e-12,
"f": 1e-15,
"a": 1e-18,
"": 1,
"Q": 1e30, # Quetta
"R": 1e27, # Ronna
"Y": 1e24, # Yotta
"Z": 1e21, # Zetta
"E": 1e18, # Exa
"P": 1e15, # Peta
"T": 1e12, # Tera
"G": 1e9, # Giga
"M": 1e6, # Mega
"k": 1e3, # Kilo
"h": 1e2, # Hecto
"da": 1e1, # Deca
"": 1e0, # No prefix
"d": 1e-1, # Deci
"c": 1e-2, # Centi
"m": 1e-3, # Milli
"µ": 1e-6, # Micro
"u": 1e-6, # Micro (same as µ)
"n": 1e-9, # Nano
"p": 1e-12, # Pico
"f": 1e-15, # Femto
"a": 1e-18, # Atto
"z": 1e-21, # Zepto
"y": 1e-24, # Yocto
"r": 1e-27, # Ronto
"q": 1e-30, # Quecto
}

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2025.5.0b4"
__version__ = "2025.6.0-dev"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
@@ -735,6 +735,7 @@ CONF_REFRESH = "refresh"
CONF_RELABEL = "relabel"
CONF_REPEAT = "repeat"
CONF_REPOSITORY = "repository"
CONF_RESET = "reset"
CONF_RESET_DURATION = "reset_duration"
CONF_RESET_PIN = "reset_pin"
CONF_RESIZE = "resize"
@@ -1130,11 +1131,13 @@ UNIT_WATT_HOURS = "Wh"
# device classes
DEVICE_CLASS_APPARENT_POWER = "apparent_power"
DEVICE_CLASS_AQI = "aqi"
DEVICE_CLASS_AREA = "area"
DEVICE_CLASS_ATMOSPHERIC_PRESSURE = "atmospheric_pressure"
DEVICE_CLASS_AWNING = "awning"
DEVICE_CLASS_BATTERY = "battery"
DEVICE_CLASS_BATTERY_CHARGING = "battery_charging"
DEVICE_CLASS_BLIND = "blind"
DEVICE_CLASS_BLOOD_GLUCOSE_CONCENTRATION = "blood_glucose_concentration"
DEVICE_CLASS_BUTTON = "button"
DEVICE_CLASS_CARBON_DIOXIDE = "carbon_dioxide"
DEVICE_CLASS_CARBON_MONOXIDE = "carbon_monoxide"
@@ -1153,6 +1156,7 @@ DEVICE_CLASS_DOORBELL = "doorbell"
DEVICE_CLASS_DURATION = "duration"
DEVICE_CLASS_EMPTY = ""
DEVICE_CLASS_ENERGY = "energy"
DEVICE_CLASS_ENERGY_DISTANCE = "energy_distance"
DEVICE_CLASS_ENERGY_STORAGE = "energy_storage"
DEVICE_CLASS_FIRMWARE = "firmware"
DEVICE_CLASS_FREQUENCY = "frequency"
@@ -1190,6 +1194,7 @@ DEVICE_CLASS_PRECIPITATION_INTENSITY = "precipitation_intensity"
DEVICE_CLASS_PRESENCE = "presence"
DEVICE_CLASS_PRESSURE = "pressure"
DEVICE_CLASS_PROBLEM = "problem"
DEVICE_CLASS_REACTIVE_ENERGY = "reactive_energy"
DEVICE_CLASS_REACTIVE_POWER = "reactive_power"
DEVICE_CLASS_RESTART = "restart"
DEVICE_CLASS_RUNNING = "running"
@@ -1217,6 +1222,7 @@ DEVICE_CLASS_VOLUME_STORAGE = "volume_storage"
DEVICE_CLASS_WATER = "water"
DEVICE_CLASS_WEIGHT = "weight"
DEVICE_CLASS_WINDOW = "window"
DEVICE_CLASS_WIND_DIRECTION = "wind_direction"
DEVICE_CLASS_WIND_SPEED = "wind_speed"
# state classes

View File

@@ -2,7 +2,7 @@ import logging
import math
import os
import re
from typing import TYPE_CHECKING, Optional, Union
from typing import TYPE_CHECKING
from esphome.const import (
CONF_COMMENT,
@@ -326,7 +326,7 @@ class ID:
else:
self.is_manual = is_manual
self.is_declaration = is_declaration
self.type: Optional[MockObjClass] = type
self.type: MockObjClass | None = type
def resolve(self, registered_ids):
from esphome.config_validation import RESERVED_IDS
@@ -476,20 +476,20 @@ class EsphomeCore:
# True if command is run from vscode api
self.vscode = False
# The name of the node
self.name: Optional[str] = None
self.name: str | None = None
# The friendly name of the node
self.friendly_name: Optional[str] = None
self.friendly_name: str | None = None
# The area / zone of the node
self.area: Optional[str] = None
self.area: str | None = None
# Additional data components can store temporary data in
# The first key to this dict should always be the integration name
self.data = {}
# The relative path to the configuration YAML
self.config_path: Optional[str] = None
self.config_path: str | None = None
# The relative path to where all build files are stored
self.build_path: Optional[str] = None
self.build_path: str | None = None
# The validated configuration, this is None until the config has been validated
self.config: Optional[ConfigType] = None
self.config: ConfigType | None = None
# The pending tasks in the task queue (mostly for C++ generation)
# This is a priority queue (with heapq)
# Each item is a tuple of form: (-priority, unique number, task)
@@ -509,7 +509,7 @@ class EsphomeCore:
# A set of defines to set for the compile process in esphome/core/defines.h
self.defines: set[Define] = set()
# A map of all platformio options to apply
self.platformio_options: dict[str, Union[str, list[str]]] = {}
self.platformio_options: dict[str, str | list[str]] = {}
# A set of strings of names of loaded integrations, used to find namespace ID conflicts
self.loaded_integrations = set()
# A set of component IDs to track what Component subclasses are declared
@@ -546,7 +546,7 @@ class EsphomeCore:
PIN_SCHEMA_REGISTRY.reset()
@property
def address(self) -> Optional[str]:
def address(self) -> str | None:
if self.config is None:
raise ValueError("Config has not been loaded yet")
@@ -559,7 +559,7 @@ class EsphomeCore:
return None
@property
def web_port(self) -> Optional[int]:
def web_port(self) -> int | None:
if self.config is None:
raise ValueError("Config has not been loaded yet")
@@ -572,7 +572,7 @@ class EsphomeCore:
return None
@property
def comment(self) -> Optional[str]:
def comment(self) -> str | None:
if self.config is None:
raise ValueError("Config has not been loaded yet")
@@ -773,7 +773,7 @@ class EsphomeCore:
_LOGGER.debug("Adding define: %s", define)
return define
def add_platformio_option(self, key: str, value: Union[str, list[str]]) -> None:
def add_platformio_option(self, key: str, value: str | list[str]) -> None:
new_val = value
old_val = self.platformio_options.get(key)
if isinstance(old_val, list):

View File

@@ -160,7 +160,8 @@
#if defined(USE_ESP32_VARIANT_ESP32S2)
#define USE_LOGGER_USB_CDC
#elif defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32C3) || \
defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2)
defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32H2) || \
defined(USE_ESP32_VARIANT_ESP32P4)
#define USE_LOGGER_USB_CDC
#define USE_LOGGER_USB_SERIAL_JTAG
#endif

View File

@@ -1,6 +1,7 @@
#pragma once
#include <cmath>
#include <cstdint>
#include <cstring>
#include <functional>
#include <limits>

View File

@@ -1,5 +1,6 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <iterator>
#include <memory>

View File

@@ -42,14 +42,13 @@ Here everything is combined in `yield` expressions. You await other coroutines u
the last `yield` expression defines what is returned.
"""
import collections
from collections.abc import Awaitable, Generator, Iterator
from collections.abc import Awaitable, Callable, Generator, Iterator
import functools
import heapq
import inspect
import logging
import types
from typing import Any, Callable
from typing import Any
_LOGGER = logging.getLogger(__name__)
@@ -126,7 +125,7 @@ def _flatten_generator(gen: Generator[Any, Any, Any]):
ret = to_send if e.value is None else e.value
return ret
if isinstance(val, collections.abc.Awaitable):
if isinstance(val, Awaitable):
# yielded object that is awaitable (like `yield some_new_style_method()`)
# yield from __await__() like actual coroutines would.
to_send = yield from val.__await__()

View File

@@ -1,9 +1,9 @@
import abc
from collections.abc import Sequence
from collections.abc import Callable, Sequence
import inspect
import math
import re
from typing import Any, Callable, Optional, Union
from typing import Any
from esphome.core import (
CORE,
@@ -35,19 +35,19 @@ class Expression(abc.ABC):
"""
SafeExpType = Union[
Expression,
bool,
str,
str,
int,
float,
TimePeriod,
type[bool],
type[int],
type[float],
Sequence[Any],
]
SafeExpType = (
Expression
| bool
| str
| str
| int
| float
| TimePeriod
| type[bool]
| type[int]
| type[float]
| Sequence[Any]
)
class RawExpression(Expression):
@@ -90,7 +90,7 @@ class VariableDeclarationExpression(Expression):
class ExpressionList(Expression):
__slots__ = ("args",)
def __init__(self, *args: Optional[SafeExpType]):
def __init__(self, *args: SafeExpType | None):
# Remove every None on end
args = list(args)
while args and args[-1] is None:
@@ -139,7 +139,7 @@ class CallExpression(Expression):
class StructInitializer(Expression):
__slots__ = ("base", "args")
def __init__(self, base: Expression, *args: tuple[str, Optional[SafeExpType]]):
def __init__(self, base: Expression, *args: tuple[str, SafeExpType | None]):
self.base = base
# TODO: args is always a Tuple, is this check required?
if not isinstance(args, OrderedDict):
@@ -197,9 +197,7 @@ class ParameterExpression(Expression):
class ParameterListExpression(Expression):
__slots__ = ("parameters",)
def __init__(
self, *parameters: Union[ParameterExpression, tuple[SafeExpType, str]]
):
def __init__(self, *parameters: ParameterExpression | tuple[SafeExpType, str]):
self.parameters = []
for parameter in parameters:
if not isinstance(parameter, ParameterExpression):
@@ -362,7 +360,7 @@ def safe_exp(obj: SafeExpType) -> Expression:
return IntLiteral(int(obj.total_seconds))
if isinstance(obj, TimePeriodMinutes):
return IntLiteral(int(obj.total_minutes))
if isinstance(obj, (tuple, list)):
if isinstance(obj, tuple | list):
return ArrayInitializer(*[safe_exp(o) for o in obj])
if obj is bool:
return bool_
@@ -418,7 +416,9 @@ class LineComment(Statement):
self.value = value
def __str__(self):
parts = re.sub(r"\\\s*\n", r"<cont>\n", self.value, re.MULTILINE).split("\n")
parts = re.sub(r"\\\s*\n", r"<cont>\n", self.value, flags=re.MULTILINE).split(
"\n"
)
parts = [f"// {x}" for x in parts]
return "\n".join(parts)
@@ -461,7 +461,7 @@ def static_const_array(id_, rhs) -> "MockObj":
return obj
def statement(expression: Union[Expression, Statement]) -> Statement:
def statement(expression: Expression | Statement) -> Statement:
"""Convert expression into a statement unless is already a statement."""
if isinstance(expression, Statement):
return expression
@@ -579,7 +579,7 @@ def new_Pvariable(id_: ID, *args: SafeExpType) -> Pvariable:
return Pvariable(id_, rhs)
def add(expression: Union[Expression, Statement]):
def add(expression: Expression | Statement):
"""Add an expression to the codegen section.
After this is called, the given given expression will
@@ -588,12 +588,12 @@ def add(expression: Union[Expression, Statement]):
CORE.add(expression)
def add_global(expression: Union[SafeExpType, Statement], prepend: bool = False):
def add_global(expression: SafeExpType | Statement, prepend: bool = False):
"""Add an expression to the codegen global storage (above setup())."""
CORE.add_global(expression, prepend)
def add_library(name: str, version: Optional[str], repository: Optional[str] = None):
def add_library(name: str, version: str | None, repository: str | None = None):
"""Add a library to the codegen library storage.
:param name: The name of the library (for example 'AsyncTCP')
@@ -619,7 +619,7 @@ def add_define(name: str, value: SafeExpType = None):
CORE.add_define(Define(name, safe_exp(value)))
def add_platformio_option(key: str, value: Union[str, list[str]]):
def add_platformio_option(key: str, value: str | list[str]):
CORE.add_platformio_option(key, value)
@@ -654,7 +654,7 @@ async def process_lambda(
parameters: list[tuple[SafeExpType, str]],
capture: str = "=",
return_type: SafeExpType = None,
) -> Union[LambdaExpression, None]:
) -> LambdaExpression | None:
"""Process the given lambda value into a LambdaExpression.
This is a coroutine because lambdas can depend on other IDs,
@@ -711,8 +711,8 @@ def is_template(value):
async def templatable(
value: Any,
args: list[tuple[SafeExpType, str]],
output_type: Optional[SafeExpType],
to_exp: Union[Callable, dict] = None,
output_type: SafeExpType | None,
to_exp: Callable | dict = None,
):
"""Generate code for a templatable config option.
@@ -821,7 +821,7 @@ class MockObj(Expression):
assert self.op == "::"
return MockObj(f"using namespace {self.base}")
def __getitem__(self, item: Union[str, Expression]) -> "MockObj":
def __getitem__(self, item: str | Expression) -> "MockObj":
next_op = "."
if isinstance(item, str) and item.startswith("P"):
item = item[1:]

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import asyncio
from collections.abc import Coroutine
from collections.abc import Callable, Coroutine
import contextlib
from dataclasses import dataclass
from functools import partial
@@ -9,7 +9,7 @@ import json
import logging
from pathlib import Path
import threading
from typing import Any, Callable
from typing import Any
from esphome.storage_json import ignored_devices_storage_path

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import asyncio
import base64
from collections.abc import Iterable
from collections.abc import Callable, Iterable
import datetime
import functools
import gzip
@@ -17,7 +17,7 @@ import shutil
import subprocess
import threading
import time
from typing import TYPE_CHECKING, Any, Callable, TypeVar
from typing import TYPE_CHECKING, Any, TypeVar
from urllib.parse import urlparse
import tornado
@@ -601,10 +601,12 @@ class DownloadListRequestHandler(BaseHandler):
loop = asyncio.get_running_loop()
try:
downloads_json = await loop.run_in_executor(None, self._get, configuration)
except vol.Invalid:
except vol.Invalid as exc:
_LOGGER.exception("Error while fetching downloads", exc_info=exc)
self.send_error(404)
return
if downloads_json is None:
_LOGGER.error("Configuration %s not found", configuration)
self.send_error(404)
return
self.set_status(200)
@@ -618,14 +620,17 @@ class DownloadListRequestHandler(BaseHandler):
if storage_json is None:
return None
config = yaml_util.load_yaml(settings.rel_path(configuration))
try:
config = yaml_util.load_yaml(settings.rel_path(configuration))
if const.CONF_EXTERNAL_COMPONENTS in config:
from esphome.components.external_components import (
do_external_components_pass,
)
if const.CONF_EXTERNAL_COMPONENTS in config:
from esphome.components.external_components import (
do_external_components_pass,
)
do_external_components_pass(config)
do_external_components_pass(config)
except vol.Invalid:
_LOGGER.info("Could not parse `external_components`, skipping")
from esphome.components.esp32 import VARIANTS as ESP32_VARIANTS

View File

@@ -1,3 +1,4 @@
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime
import hashlib
@@ -5,7 +6,6 @@ import logging
from pathlib import Path
import re
import subprocess
from typing import Callable, Optional
import urllib.parse
import esphome.config_validation as cv
@@ -45,12 +45,12 @@ def clone_or_update(
*,
url: str,
ref: str = None,
refresh: Optional[TimePeriodSeconds],
refresh: TimePeriodSeconds | None,
domain: str,
username: str = None,
password: str = None,
submodules: Optional[list[str]] = None,
) -> tuple[Path, Optional[Callable[[], None]]]:
submodules: list[str] | None = None,
) -> tuple[Path, Callable[[], None] | None]:
key = f"{url}@{ref}"
if username is not None and password is not None:

View File

@@ -7,7 +7,6 @@ from pathlib import Path
import platform
import re
import tempfile
from typing import Union
from urllib.parse import urlparse
_LOGGER = logging.getLogger(__name__)
@@ -219,8 +218,8 @@ def sort_ip_addresses(address_list: list[str]) -> list[str]:
int,
int,
int,
Union[str, None],
Union[tuple[str, int], tuple[str, int, int, int]],
str | None,
tuple[str, int] | tuple[str, int, int, int],
]
] = []
for addr in address_list:
@@ -282,7 +281,7 @@ def read_file(path):
raise EsphomeError(f"Error reading file {path}: {err}") from err
def _write_file(path: Union[Path, str], text: Union[str, bytes]):
def _write_file(path: Path | str, text: str | bytes):
"""Atomically writes `text` to the given path.
Automatically creates all parent directories.
@@ -315,7 +314,7 @@ def _write_file(path: Union[Path, str], text: Union[str, bytes]):
_LOGGER.error("Write file cleanup failed: %s", err)
def write_file(path: Union[Path, str], text: str):
def write_file(path: Path | str, text: str):
try:
_write_file(path, text)
except OSError as err:
@@ -324,7 +323,7 @@ def write_file(path: Union[Path, str], text: str):
raise EsphomeError(f"Could not write file at {path}") from err
def write_file_if_changed(path: Union[Path, str], text: str) -> bool:
def write_file_if_changed(path: Path | str, text: str) -> bool:
"""Write text to the given path, but not if the contents match already.
Returns true if the file was changed.

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