Compare commits

...
Author SHA1 Message Date
J. Nick Koston 97866e2891 Name the MBR page in the SoftDevice layouts so the app follows the SoftDevice 2026-10-15 09:25:16 -10:00
J. Nick Koston 1adebf9356 Note the older MDK dongle bootloader that carries S140 6.1.1 2026-10-15 09:20:59 -10:00
J. Nick Koston 09baebcf00 Share the Adafruit bootloader section and look SoftDevices up in a table 2026-10-15 09:16:09 -10:00
J. Nick Koston e9a5f4be67 [nrf52] Add the Makerdiary nRF52840 MDK USB Dongle 2026-10-15 09:12:08 -10:00
45ea5f4220 [nrf52] nrf52840dongle support (#19009)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-15 09:03:53 -10:00
techdregsandJ. Nick Koston 108ed67d31 [rx8111] Add basic RX8111CE real-time clock support (#20785)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-15 08:30:25 -10:00
Remco van EssenandClaude Opus 5.5 6356458081 [dfrobot_sen0609] Add DFRobot SEN0609/SEN0610 radar (#20809)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:27:57 +00:00
Panoplian bf67b49104 [i2c] Let host unit tests with I2C build on macOS (#20738) 2026-10-15 07:47:52 -10:00
Panoplian 1947c3d355 [improv_ble] Start Improv again after a session has stopped (#20808) 2026-10-15 07:47:14 -10:00
J. Nick Koston cf4e69273c [shelly_dimmer] Keep the STM32 device table in flash (#20643) 2026-10-15 07:28:55 -10:00
J. Nick Koston c433e96734 [homeassistant] Share the Python list parser between the platforms (#20793) 2026-10-15 07:28:27 -10:00
J. Nick Koston 93dc80d895 [esp8266] Refuse two components that drive hardware timer 1 (#20794) 2026-10-15 07:28:07 -10:00
J. Nick Koston 7f342054fe [wifi] Retry a failed fallback AP once per ap_timeout and start the captive portal once (#20784) 2026-10-15 07:27:19 -10:00
Jeroenandjeroen85 d924d2daf7 [core] Extend fast value formatting to four decimal places (#20800)
Co-authored-by: jeroen85 <26403565+jeroen85@users.noreply.github.com>
2026-10-15 07:15:11 -10:00
Clyde Stubbs 49b12daa58 [epaper_spi] Ensure test card gets drawn (#20804) 2026-10-15 10:55:19 +00:00
Clyde Stubbs d3faafc578 [display] Always treat 888 pixel blocks as b,g,r in memory (#20799) 2026-10-15 20:49:50 +11:00
Remco van Essen 7b08e738d3 [fusb302b] Add sensors, contract text sensor and automations (#20796) 2026-10-14 22:13:58 -10:00
J. Nick Koston 62e3c25c83 [lvgl] Fix the menu test after lambdas gained a line reset (#20798) 2026-10-14 21:50:47 -10:00
6a50f5c166 [apc_proteous] Cover component for APC Proteous gate motors (#13403)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-15 17:46:40 +11:00
Clyde Stubbs bb0837d814 [lvgl] Implement menu widget (#18022) 2026-10-15 17:44:23 +11:00
Bonne Egglestonandclydebarrow ecd5980c5d [epaper_spi] Pass bits per pixel and plane count to the base constructor (#20683)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-10-15 17:41:37 +11:00
8d963bde5b [openthread] Add ESP32 OpenThread Border Router support (#18104)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 20:02:33 -10:00
J. Nick Koston ea6d5499b3 [lsm6ds] Add the LSM6DSO and match all chips to their datasheets (#20733) 2026-10-14 19:24:00 -10:00
41e82b0295 [hdmi_cec] Add HDMI-CEC bus component (part 1: bus core) (#9189)
Co-authored-by: Stéphane Lepin <stephane.lepin@gmail.com>
Co-authored-by: Stéphane Lepin <Palakis@users.noreply.github.com>
Co-authored-by: Niko <nikokirste@hotmail.de>
Co-authored-by: Minims <github@minims.fr>
Co-authored-by: Nicholas Pace <segfly@users.noreply.github.com>
Co-authored-by: Nicholas Pace <3300476+segfly@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-15 04:16:09 +00:00
J. Nick Koston 39705a65e3 [bmi270] Keep the range name and scale tables in flash (#20715) 2026-10-14 17:44:46 -10:00
J. Nick Koston a522553e82 [lsm6ds] Keep the range and chip names in flash (#20718) 2026-10-14 17:43:54 -10:00
J. Nick Koston a07a1ab728 [qmi8658] Keep the range name tables in flash (#20714) 2026-10-14 17:43:34 -10:00
J. Nick Koston 9a5d2d9753 [api] Encode action strings straight from flash (#20425) 2026-10-14 17:38:39 -10:00
J. Nick Koston 3188e0d36f [ota] Fall back to a UDP data phase when the link is lossy (#20498) 2026-10-15 16:30:56 +13:00
Jonathan Swoboda f3eaf698d6 [mipi_rgb] Rotate pure RGB panels in software (#20757) 2026-10-14 22:05:01 -04:00
J. Nick Koston f7057e1270 [tests] Reset saved preferences before every C++ unit test (#20771) 2026-10-14 16:01:45 -10:00
J. Nick Koston 6edfbb594d [bluetooth_connection] Value-initialize the connection parameters for ESP-IDF 6.1 (#20774) 2026-10-14 22:00:07 -04:00
J. Nick Koston 22defcd704 [esp32_ble_client] Value-initialize the connection parameters for ESP-IDF 6.1 (#20776) 2026-10-14 21:59:56 -04:00
Jonathan Swoboda 0f408234d0 [core] Add ksz8851snl to the static analysis manifest for IDF 6 (#20788) 2026-10-14 21:53:17 -04:00
Jonathan Swoboda 92e0e6db6b [wifi] Call esp_supp_dpp_init without a callback on ESP-IDF 6 (#20789) 2026-10-15 01:40:07 +00:00
908c2f6778 [wifi] Add Wi-Fi Easy Connect (DPP) provisioning on the ESP32 (#17934)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 14:29:47 -10:00
Keith BurzinskiandClaude Opus 5.5 af709ac9a5 [openthread] Fail instead of aborting when NVS is unavailable (#20717)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:30:36 -10:00
Panoplian fe57eef3f6 [core] Test that includes inside a package resolve next to the package (#20739) 2026-10-14 13:28:01 -10:00
d0b9f195b0 [wifi] Apply reboot_timeout with a fallback AP unless a station is connected to it (#9958)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 13:23:37 -10:00
eafeef7cb8 [fusb302b] Add FUSB302B driver (#20760)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 13:18:20 -10:00
471774cdbf [ecocomfort2] Fix the clock sync and read boost from the unit state (#20782)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-10-14 13:14:04 -10:00
Kevin AhrendtandJ. Nick Koston 4df6291feb [api][sendspin] Add the Sendspin client id to the capabilities message (#20775)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 22:56:51 +00:00
d8ca4a9460 [audio_dac] Add ES9038Q2M Katana platform (#15017)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 22:50:41 +00:00
48a0a41cce [wifi] Implement access point WISP mode (#13633)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 12:49:37 -10:00
esphome[bot] 0e2c4cf5c2 Bump aioesphomeapi from 46.11.0 to 46.12.0 (#20781)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-14 12:43:07 -10:00
ea76073d86 [micro_wake_word] Allow selecting the inference task core (#20126)
Co-authored-by: n-IA-hane <n-IA-hane@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 12:42:24 -10:00
J. Nick Koston dcb943ded3 [noise] Initialize the protocol id's reserved field for ESP-IDF 6.1 (#20779) 2026-10-14 11:45:54 -10:00
dependabot[bot] 2c6f59738c Bump filelock from 4.1.0 to 4.1.1 (#20778)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 21:41:28 +00:00
puddlyandJ. Nick Koston 04715fb32b [usb_uart] Expose interface_class and port_number (#20659)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 21:41:12 +00:00
nullable.ethandJ. Nick Koston 3df9360876 [i2s_audio] Add keep_alive option to speaker to prevent pop/click on audio start (#15565)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 11:39:30 -10:00
dependabot[bot] 61f3de0bc0 Bump pylint from 4.1.2 to 4.1.3 (#20773)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 21:30:26 +00:00
dependabot[bot] cd97c9a9cb Bump platformdirs from 4.12.4 to 4.13.0 (#20772)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 21:29:17 +00:00
J. Nick Koston d3668a7186 [mqtt] Match subscription topics in a loop instead of recursing per character (#20767) 2026-10-14 11:13:32 -10:00
J. Nick Koston ac74796263 [friedrich] Clear saved preferences in the test fixture (#20770) 2026-10-14 10:52:13 -10:00
c56bb14037 [mqtt] Print crash info over MQTT (#17935)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 10:39:55 -10:00
J. Nick Koston a4278b8dec [ci] Run benchmarks when a component the Python import benchmark imports changes (#20763) 2026-10-14 10:35:33 -10:00
Panoplian 6d173c7371 [bq25186] Set i2c_id in the component tests (#20742) 2026-10-14 10:35:01 -10:00
esphome[bot] 1779b011c9 Bump aioesphomeapi from 46.10.0 to 46.11.0 (#20766)
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-14 20:21:45 +00:00
PanoplianandJ. Nick Koston 3c4cf50cb6 [core] Point compile errors after a lambda at the right line (#20740)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 10:02:51 -10:00
J. Nick Koston e869d1f7ab [ci] Give the BK72xx clang-tidy environment the static allocation flag the real build has (#20764) 2026-10-14 09:56:09 -10:00
e1a996435d [ld2450] Add polygon zones evaluated on the device (#20154)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 19:29:16 +00:00
Jonathan Swoboda c5a624a1f2 [tests] Count OTA key probe retries on a fake clock (#20761) 2026-10-14 15:21:35 -04:00
Remco van Essen 15fb7f775e [tmc2209] Add chopper and PWM tuning actions (#20753) 2026-10-14 08:59:21 -10:00
J. Nick Koston 3fa72e61c6 [mqtt] Build the mqtt triggers with build_trigger_callback (#20433) 2026-10-14 08:55:39 -10:00
Kevin Ahrendt 163b09a473 [microphone] Speed up MicrophoneSource audio conversion (#20755) 2026-10-14 08:47:25 -10:00
J. Nick Koston 7afc2768f8 [esp8266] Check that every component's saved values fit together (#20686) 2026-10-14 08:45:51 -10:00
56f1a2bf46 [midea_portasplit] Add Midea PortaSplit climate component (#18033)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 08:45:21 -10:00
Panoplian fee47fe2f1 [pcf85063] Convert the weekday to and from the chip's 0-6 range (#20736) 2026-10-14 08:43:23 -10:00
Panoplian b994e081c3 [bm8563] Convert the weekday to and from the chip's 0-6 range (#20737) 2026-10-14 08:42:47 -10:00
Jonathan Swoboda a1f740b71b [lvgl] Keep LV_CONF_PATH one argument when the build path has a space (#20758) 2026-10-14 08:36:17 -10:00
Remco van EssenandClaude Opus 5.5 7f144365ba [fusb302b] Add USB PD sink protocol (#20632)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:35:29 -10:00
Marcus VoßandJ. Nick Koston 928459d16f [cse7766] Keep last power reading between measurements at low load (#20631)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 08:33:11 -10:00
Jonathan Swoboda 9163359445 [core] Analyze the ethernet component under ESP-IDF (#20745) 2026-10-14 08:31:39 -10:00
Jonathan Swoboda 67307f0eeb [ci] Select the feature-gated files the platform clang-tidy jobs were missing (#20743) 2026-10-14 08:31:06 -10:00
91e0edc11d [pn532] Fix NDEF parsing greater than 255 bytes (#18188)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 08:15:48 -10:00
Jonathan Swoboda 9fe75e262f [ci] Add a host clang-tidy environment and job (#20747) 2026-10-14 08:09:53 -10:00
Kevin Ahrendt 0132f722a3 [audio] Bump esp-audio-libs to v4.1.0 (#20754) 2026-10-14 13:24:36 -04:00
Jonathan Swoboda 7d7a78308d [ci] Test tinyusb_keyboard in isolation (#20749) 2026-10-14 10:05:02 -04:00
J. Nick Koston 9c568525b0 [core] Add CodSpeed benchmarks for importing entity components (#20648) 2026-10-14 09:08:41 -04:00
J. Nick Koston 10f42e3e7c [shelly_dimmer] Build the STM32 firmware update path in CI (#20677) 2026-10-14 09:03:34 -04:00
J. Nick Koston 25d4b88605 [nfc] Add unit tests for the URI record prefix codes (#20724) 2026-10-14 09:01:59 -04:00
J. Nick Koston d6abf35138 [sx126x][sx127x] Stop the watchdog reset loop when a packet is sent before setup (#20727) 2026-10-14 09:01:10 -04:00
J. Nick Koston 66ea99ce65 [seeed_mr60fda2] Compile without binary sensors or selects (#20726) 2026-10-14 09:00:08 -04:00
J. Nick Koston d9298cb7b9 [seeed_mr60bha2] Compile without sensors or binary sensors (#20729) 2026-10-14 08:59:27 -04:00
J. Nick Koston 34e1cf96d5 [bmi270] Build the component on ESP8266 in CI (#20731) 2026-10-14 08:58:56 -04:00
56cc03c4d4 [haier] Haier Select component added (#16148)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Pavlo Dudnytskyi <Pavlo.Dudnytskyi@addsecure.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 23:27:18 -10:00
35b416c6c0 [web_server_idf] Keep active SSE sessions out of the LRU purge (#17383)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 22:19:06 -10:00
Petar Petrov 7f2c3ba3f5 [display] Let other components draw into a display (#20622) 2026-10-14 06:11:47 +00:00
Keith BurzinskiandClaude Opus 5.5 a97604d574 [esp32_ble] Rely on preferences for NVS init (#20708)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:10:51 -10:00
Petar Petrov 1146b77836 [socket] Add TcpClientLink::connecting() (#20722) 2026-10-13 19:19:17 -10:00
J. Nick Koston 1338902ac5 [heatpumpir] Link only the configured protocol classes (#20707) 2026-10-14 17:06:22 +13:00
60a1d70805 [core] Move deferred include processing to parse_yaml (#16344)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 17:46:54 -10:00
Ryan LudwigandCursor 887d4c3a83 [esp32_can] Add NO_ACK mode (#20706)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-13 17:36:15 -10:00
J. Nick Koston c1fa73470e [display] Walk the blit fallback in frame buffer order and resolve the bitness once (#20651) 2026-10-13 17:31:13 -10:00
J. Nick Koston e41d16ba7e [haier] Keep the alarm message table in flash (#20641) 2026-10-14 03:23:28 +00:00
J. Nick Koston 5171113c87 [core] Let PROGMEM_STRING_TABLE hold more than 255 bytes (#20646) 2026-10-13 17:17:48 -10:00
techdregsandJ. Nick Koston 5718e4b779 [tmp102] Add text sensor for diagnostics (#20629)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 17:06:56 -10:00
J. Nick Koston 40e12cd0d8 [image] Add CodSpeed benchmarks for image, runtime image and display block draws (#20649) 2026-10-13 17:01:47 -10:00
Brandon HarveyandJ. Nick Koston 07029d0077 [sen6x] Add measurement control and maintenance actions (#18783)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 15:47:31 +13:00
Christopher Reimer bb1119d83f [esp32_can] Support 20 kbit/s on ESP32 revision 2 or later (#20426) 2026-10-13 22:34:03 -04:00
7b591b26d6 [sy6970] Service the I2C watchdog so charge/LED settings persist (#19424)
Co-authored-by: Willem Oldemans <>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 16:19:58 -10:00
f45e6f0bf4 [midea] Add display_control and send the display toggle over UART without a transmitter (#18671)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 16:03:18 -10:00
Keith BurzinskiandClaude Opus 5.5 4b8af08155 [network] Notify listeners when the active interface changes (#20684)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:57:59 -05:00
613973b432 Bump ruff from 0.16.10 to 0.17.0 (#20694)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-10-13 21:53:44 -04:00
dependabot[bot] 9ee4314bc2 Bump esptool from 5.4.0 to 5.5.0 (#20693)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:41:30 -10:00
fredkotzeandJ. Nick Koston 59f55dbe9e [epaper_spi] Add Waveshare 2.13in-G (JD79660) display models (#18141)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 15:40:28 -10:00
dependabot[bot] 8fce95454a Bump github/codeql-action/init from 4.38.2 to 4.38.3 (#20701)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:49 -10:00
dependabot[bot] 16134ee993 Bump astral-sh/setup-uv from 10.2.0 to 10.3.0 (#20704)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:38 -10:00
dependabot[bot] bc39d56bb4 Bump CodSpeedHQ/action from 5.4.0 to 5.4.2 (#20705)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:39:29 -10:00
dependabot[bot] c5522abc71 Bump github/codeql-action/analyze from 4.38.2 to 4.38.3 (#20699)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 21:39:11 -04:00
dependabot[bot] 7cb5906d2a Bump actions/upload-artifact from 7.0.1 to 7.0.2 (#20702)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 01:38:42 +00:00
dependabot[bot] d452dbce30 Bump actions/download-artifact from 8.0.1 to 8.0.2 (#20703)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-14 01:38:39 +00:00
dependabot[bot] 81e5b50087 Bump backports-zstd from 1.7.0 to 1.8.0 (#20695)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:38:35 -10:00
dependabot[bot] 426a4a2dce Bump filelock from 4.0.12 to 4.1.0 (#20696)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:38:26 -10:00
dependabot[bot]andesphome[bot] c95c17c4e7 Bump pyupgrade from 3.21.2 to 3.22.0 (#20698)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
2026-10-13 15:38:17 -10:00
dependabot[bot] 576d3325fe Bump astral-sh/setup-uv from 10.2.0 to 10.3.0 in /.github/actions/restore-python (#20700)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 15:37:47 -10:00
dependabot[bot] c9cf0ad92e Bump platformdirs from 4.12.3 to 4.12.4 (#20697)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-13 21:37:36 -04:00
Felipe Guedes f505212e64 [epaper_spi] Add Waveshare 2.66" e-Paper Module (B) support (#20546) 2026-10-13 15:03:05 -10:00
J. Nick Koston 52a5e6a007 [rpi_dpi_rgb] Set removal of the deprecated component for 2027.5.0 (#20691) 2026-10-14 13:32:18 +13:00
J. Nick Koston 3b261d2cf0 [st7789v] Set removal of the deprecated component for 2027.5.0 (#20689) 2026-10-14 13:30:52 +13:00
J. Nick Koston 9238fcc727 [st7735] Set removal of the deprecated component for 2027.5.0 (#20688) 2026-10-14 13:30:21 +13:00
J. Nick Koston 96902cc122 [qspi_dbi] Set removal of the deprecated component for 2027.5.0 (#20690) 2026-10-14 13:29:52 +13:00
J. Nick Koston 95711d83d1 [ili9xxx] Set removal of the deprecated component for 2027.5.0 (#20687) 2026-10-14 13:29:29 +13:00
Panoplian 3cdcf0417a [lvgl] Make the list_outside_block tests work offline (#20676) 2026-10-13 13:31:08 -10:00
J. Nick Koston c6e90de262 [ci] Catch out-of-class member log tags and keep midea's in flash (#20674) 2026-10-14 12:21:24 +13:00
J. Nick Koston 92f6ab8367 [ci] Lint log tags in headers and core (#20668) 2026-10-14 12:21:24 +13:00
J. Nick Koston f61955baa2 [core] Declare the remaining log tags with ESPHOME_LOG_TAG (#20666) 2026-10-14 12:21:23 +13:00
J. Nick Koston 36e1d831d3 [core] Add a decimal fast path to parse_number<float> (#20639) 2026-10-14 12:18:35 +13:00
J. Nick Koston 63616bf642 [rtttl] Play a configured song from static storage without copying it (#19569) 2026-10-14 12:06:27 +13:00
d275926f65 [mdns] Advertise the connected interface in the network TXT record (#20673)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 17:55:22 -05:00
Andrej Friesen f265c46c21 [PCA9505] Add PCA9505/PCA9506 40-channel GPIO expander (#20667) 2026-10-13 12:50:43 -10:00
Frédéric MetrichandClaude Opus 5.5 7d735f0677 [emontx] Lock the emonTx on reboot when its firmware supports it (#20621)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:37:09 -10:00
Remco van Essen 5c23a827b6 [tmc2209] Add current and diagnostic sensors (#20638) 2026-10-13 12:35:55 -10:00
Jonathan Swoboda e265c0c0fc [core] Sync the static analysis dependency pins with the code (#20681) 2026-10-13 12:30:42 -10:00
J. Nick Koston a0c7d754cb [syslog] Keep the log level to severity table in flash (#20662) 2026-10-13 12:22:35 -10:00
Jesse Hills a6dbd72c53 Merge branch 'beta' into dev 2026-10-14 11:15:09 +13:00
Jesse Hills 9d530f5c56 Merge pull request #20680 from esphome/bump-2026.10.0b4
2026.10.0b4
2026-10-14 11:14:49 +13:00
Panoplian 6562c49686 [thermostat] Allow a dry-only thermostat without cool_action (#20682) 2026-10-13 12:08:13 -10:00
guillempages a97ce3a201 [image] Fix pillow deprecation warning (#20679) 2026-10-13 11:58:21 -10:00
Jesse Hills 5a78f636b9 Bump version to 2026.10.0b4 2026-10-14 10:43:50 +13:00
tomaszduda23andClaude 53d504137b [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-14 10:43:49 +13:00
bb0546fc0b [script] Keep the running script.wait item out of the queue while its chain runs (#20570)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-14 10:43:49 +13:00
emes30andJ. Nick Koston b43630cf8e [rp2_pio] Use pioasm 4.1.0 so PIO programs run on RP2350 (#19260)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-14 10:43:49 +13:00
Firat HajiyevandFirat17aze c9664d4e42 [wifi] Reject unsupported WPA3 on ESP8266 (#20592)
Co-authored-by: Firat17aze <187820330+Firat17aze@users.noreply.github.com>
2026-10-14 10:43:49 +13:00
Kevin Ahrendt 39ea6a3220 [audio] Bump microFLAC to v0.3.0 (#20591) 2026-10-14 10:43:49 +13:00
J. Nick Koston f4a6d6f18b [core] Create the PlatformIO HTTP cache dir before the prefetch resolve threads run (#20579) 2026-10-14 10:43:49 +13:00
esphome[bot] 6c24d4aa43 Bump aioesphomeapi from 46.9.0 to 46.10.0 (#20563) 2026-10-14 10:43:49 +13:00
J. Nick Koston 0dc1d3b5a1 [toshiba] Move the lookup tables to PROGMEM (#20636) 2026-10-13 11:25:18 -10:00
tomaszduda23andClaude 42341d05f9 [nrf52] Accept S140 7.2.0 and 7.3.0 in the sd140_v7 DFU package (#20661)
Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 11:12:08 -10:00
J. Nick Koston f941fee5ed [homeassistant] Add sensor platform benchmarks (#20637) 2026-10-13 11:10:14 -10:00
Bonne Eggleston 8721976066 [epaper_spi] Abandon an update when the busy pin never clears (#20555) 2026-10-14 07:47:09 +11:00
Bonne Eggleston 05bf7d3440 [epaper_spi] Write pixel blocks straight into mono buffers (#20552) 2026-10-14 07:40:56 +11:00
918b7d2553 [script] Keep the running script.wait item out of the queue while its chain runs (#20570)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 09:31:19 -10:00
rwrozelleandClaude Opus 5.5 417ab4d2e9 [openthread] Add core_options for OpenThread compile-time options (#20595)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:00:45 -04:00
Jonathan Swoboda 0821c84b7a [core] Let a nested repeat shadow the outer iteration variable (#20634) 2026-10-13 14:35:24 -04:00
Clyde Stubbs a827058928 [lvgl] Change deprecated calls to current version (#20620) 2026-10-13 14:27:50 -04:00
J. Nick Koston 3063b76210 [haier_tundra] Build the IR frame on the stack (#20635) 2026-10-13 14:25:36 -04:00
Remco van EssenandClaude Opus 5.5 55261ae7e5 [tmc2209] Add driver diagnostics and stall detection (#20618)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:17:25 -10:00
cociwebandJ. Nick Koston 1d8423edd8 [es8388] Add initial option configuration for DAC output and ADC input mic (#16342)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:14:43 -10:00
n-IA-haneandn-IA-hane d27ec8ec49 [voice_assistant] Make TTS playback start timeout configurable (#18707)
Co-authored-by: n-IA-hane <n-IA-hane@users.noreply.github.com>
2026-10-13 08:13:26 -10:00
Ivan KruglovandJ. Nick Koston 5acbd44067 [pcf8574] Add reset option to preserve outputs across MCU reboots (#20589)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:12:28 -10:00
40271fb597 [haier_tundra] New Climate IR component (#20469)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:10:36 -10:00
95766ed3ad [icm20689] Add ICM-20689 6-axis accelerometer/gyroscope (SPI) (#19234)
Co-authored-by: Ludovic BOUÉ <938089+lboue@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-10-13 08:07:32 -10:00
4a80d785a8 [filter_lifetime] Add component with sensor/lambda support for is_on … (#13914)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-10-13 07:52:46 -10:00
645 changed files with 27707 additions and 2408 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
@@ -97,7 +97,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: generated-proto-files
path: |
+2 -2
View File
@@ -128,7 +128,7 @@ jobs:
- name: Upload compile-test image artifact
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
# The tar is already compressed, so upload it as-is. archive: false
# skips the redundant zip and makes the file name the artifact name
@@ -210,7 +210,7 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download image artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: compile-test-image.tar.zst
- name: Load image
+36 -22
View File
@@ -49,7 +49,7 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -81,8 +81,8 @@ jobs:
- name: Install apt packages (cached)
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
determine-jobs:
name: Determine which jobs to run
@@ -415,8 +415,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Set up Python 3.13
id: python
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
@@ -438,7 +438,7 @@ jobs:
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -477,7 +477,7 @@ jobs:
# Tests that compare rendered output write the image they actually got here, so a
# failure can be looked at without reproducing the whole build locally.
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/
@@ -488,7 +488,7 @@ jobs:
# script/update_integration_test_durations.py; only full matrix dev
# runs produce usable data.
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: junit-integration-${{ strategy.job-index }}
path: junit-integration.xml
@@ -528,7 +528,7 @@ jobs:
script/check_import_time.py --check --har importtime.har
- name: Upload waterfall HAR
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: import-time-waterfall
path: importtime.har
@@ -626,7 +626,7 @@ jobs:
apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@c4fd08a3a159bd0cc208da1e0edf32b8c47d75e5 # v5.4.0
uses: CodSpeedHQ/action@f842b9a3ffe213dbb81768fd7f7eeb4dfcf1c369 # v5.4.2
with:
run: |
. venv/bin/activate
@@ -680,7 +680,7 @@ jobs:
include:
- id: clang-tidy
name: Run script/clang-tidy for ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266
options: --environment esp8266-arduino-tidy --grep USE_ESP8266 --grep USE_SOCKET_IMPL_LWIP_TCP --grep USE_SHD_FIRMWARE_DATA
pio_cache_key: tidyesp8266
- id: clang-tidy
name: Run script/clang-tidy for ESP32 Arduino
@@ -693,12 +693,16 @@ jobs:
ignore_errors: false
- id: clang-tidy
name: Run script/clang-tidy for RP2
options: --environment rp2-tidy --grep USE_RP2
options: --environment rp2-tidy --grep USE_RP2 --grep USE_BLE_SCAN_RESPONSE_MERGER
pio_cache_key: tidyrp2
- id: clang-tidy
name: Run script/clang-tidy for host
options: --environment host-tidy --grep USE_HOST
pio_cache_key: tidyhost
- id: clang-tidy
name: Run script/clang-tidy for LibreTiny
environments: bk72xx-tidy ln882h-tidy rtl87xxb-tidy rtl87xxc-tidy
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X
options: --grep USE_LIBRETINY --grep USE_BK72XX --grep USE_RTL87XX --grep USE_LN882X --grep USE_LN882H_BLE --grep USE_SOCKET_IMPL_LWIP_SOCKETS
pio_cache_key: tidylibretiny
steps:
@@ -707,6 +711,16 @@ jobs:
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Install apt packages (cached)
if: matrix.pio_cache_key == 'tidyhost'
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
# by dsmr and dlms_meter. Packages and version must match seed-apt-cache
# exactly.
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Restore Python
id: restore-python
@@ -989,14 +1003,14 @@ jobs:
- id: clang-tidy
name: Run script/clang-tidy for ESP32 S3
# yamllint disable-line rule:line-length
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC
options: --environment esp32s3-idf-tidy --grep SOC_TEMP_SENSOR_SUPPORTED --grep USE_ESP32_VARIANT_ESP32S3 --grep USE_LOGGER_USB_CDC --grep SOC_I2S_SUPPORTS_TDM --grep USE_OPENTHREAD
tidy_environment: esp32s3-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 P4
# P4 has no native Wi-Fi/BLE; those run over the hosted co-processor,
# so their code paths differ -- lint them under the P4 build too.
# yamllint disable-line rule:line-length
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
options: --environment esp32p4-idf-tidy --grep USE_ESP32_VARIANT_ESP32P4 --grep CONFIG_IDF_TARGET_ESP32P4 --grep USE_ESP32_HOSTED --grep USE_WIFI --grep USE_BLE
tidy_environment: esp32p4-idf-tidy
- id: clang-tidy
name: Run script/clang-tidy for ESP32 C6
@@ -1102,8 +1116,8 @@ jobs:
timeout-minutes: 10
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
with:
packages: libsdl2-dev ccache
version: 1.1
packages: libsdl2-dev libmbedtls-dev ccache
version: 1.2
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -1448,7 +1462,7 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -1647,7 +1661,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -1710,7 +1724,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -1740,13 +1754,13 @@ jobs:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: memory-analysis-pr
path: ./memory-analysis
+2 -2
View File
@@ -56,7 +56,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
with:
category: "/language:${{matrix.language}}"
+2 -2
View File
@@ -162,7 +162,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -195,7 +195,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download digests
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: digests-*
path: /tmp/digests
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
+2 -2
View File
@@ -10,7 +10,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.16.10
rev: v0.17.0
hooks:
# Run the linter.
- id: ruff
@@ -36,7 +36,7 @@ repos:
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/asottile/pyupgrade
rev: v3.21.2
rev: v3.22.0
hooks:
- id: pyupgrade
args: [--py312-plus]
+14
View File
@@ -40,6 +40,7 @@ esphome/components/analog_threshold/* @ianchi
esphome/components/animation/* @syndlex
esphome/components/anova/* @buxtronix
esphome/components/apc1/* @ademuri
esphome/components/apc_proteous/* @clydebarrow
esphome/components/apds9306/* @aodrenah
esphome/components/apds9930/* @iyesin
esphome/components/api/* @esphome/core
@@ -155,6 +156,7 @@ esphome/components/delonghi/* @grob6000
esphome/components/dew_point/* @CFlix
esphome/components/dfplayer/* @glmnet
esphome/components/dfrobot_sen0395/* @niklasweber
esphome/components/dfrobot_sen0609/* @remcom
esphome/components/dht/* @OttoWinter
esphome/components/display_menu_base/* @numo68
esphome/components/dlms_meter/* @latonita @PolarGoose @SimonFischer04 @Tomer27cz
@@ -183,6 +185,7 @@ esphome/components/es7243e/* @kbx81
esphome/components/es8156/* @kbx81
esphome/components/es8311/* @kahrendt @kroimon
esphome/components/es8388/* @P4uLT
esphome/components/es9038q2m_katana/* @GioMez
esphome/components/esp32/* @esphome/core
esphome/components/esp32_ble/* @bdraco @jesserockz @Rapsssito
esphome/components/esp32_ble_client/* @bdraco @jesserockz
@@ -212,12 +215,14 @@ esphome/components/factory_reset/* @anatoly-savchenkov
esphome/components/fastled_base/* @OttoWinter
esphome/components/feedback/* @ianchi
esphome/components/file/* @esphome/core
esphome/components/filter_lifetime/* @tuct
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
esphome/components/font/* @clydebarrow @esphome/core
esphome/components/friedrich/* @rwrozelle
esphome/components/fs3000/* @kahrendt
esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio
esphome/components/fusb302b/* @remcom
esphome/components/gcja5/* @gcormier
esphome/components/gdk101/* @Szewcson
esphome/components/gl_r01_i2c/* @pkejval
@@ -239,9 +244,13 @@ esphome/components/gt911/* @clydebarrow @jesserockz
esphome/components/haier/* @paveldn
esphome/components/haier/binary_sensor/* @paveldn
esphome/components/haier/button/* @paveldn
esphome/components/haier/select/* @paveldn
esphome/components/haier/sensor/* @paveldn
esphome/components/haier/switch/* @paveldn
esphome/components/haier/text_sensor/* @paveldn
esphome/components/haier_tundra/* @fauxpark
esphome/components/haier_tundra/button/* @fauxpark
esphome/components/haier_tundra/switch/* @fauxpark
esphome/components/havells_solar/* @sourabhjaiswal
esphome/components/hbridge/fan/* @WeekendWarrior
esphome/components/hbridge/light/* @DotNetDann
@@ -250,6 +259,7 @@ esphome/components/hc8/* @omartijn
esphome/components/hdc2010/* @optimusprimespace @ssieb
esphome/components/hdc2080/* @G-Pereira @jesserockz
esphome/components/hdc302x/* @joshuasing
esphome/components/hdmi_cec/* @JosVanEijndhoven
esphome/components/he60r/* @clydebarrow
esphome/components/heatpumpir/* @rob-deutsch
esphome/components/hitachi_ac424/* @sourabhjaiswal
@@ -286,6 +296,7 @@ esphome/components/i2s_audio/microphone/* @jesserockz
esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt
esphome/components/i2s_clockless_led_strip/* @j9brown
esphome/components/iaqcore/* @yozik04
esphome/components/icm20689/* @lboue
esphome/components/icnt86/* @danepowell
esphome/components/ili9xxx/* @clydebarrow @nielsnl68
esphome/components/improv_base/* @esphome/core
@@ -380,6 +391,7 @@ esphome/components/microphone/* @jesserockz @kahrendt
esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov
esphome/components/midea_portasplit/* @Fexiven
esphome/components/mipi_dsi/* @clydebarrow
esphome/components/mipi_rgb/* @clydebarrow
esphome/components/mipi_spi/* @clydebarrow
@@ -434,6 +446,7 @@ esphome/components/ota/* @esphome/core
esphome/components/output/* @esphome/core
esphome/components/packet_transport/* @clydebarrow
esphome/components/pca6416a/* @Mat931
esphome/components/pca9505/* @ajfriesen @ssieb
esphome/components/pca9554/* @bdraco @clydebarrow @hwstar
esphome/components/pcf85063/* @brogon
esphome/components/pcf8563/* @KoenBreeman
@@ -497,6 +510,7 @@ esphome/components/rtttl/* @glmnet @ximex
esphome/components/runtime_image/* @clydebarrow @guillempages @kahrendt
esphome/components/runtime_stats/* @bdraco
esphome/components/rx8025t/* @remcom
esphome/components/rx8111/* @techdregs
esphome/components/rx8130/* @beormund
esphome/components/safe_mode/* @jsuanet @kbx81 @paulmonigatti
esphome/components/samsung/* @jorofi
+16 -1
View File
@@ -906,6 +906,21 @@ async def while_action_to_code(
return var
def _shadow_iteration(args: TemplateArgsType) -> TemplateArgsType:
"""Rename an inherited `iteration` so the inner repeat's own counter can shadow it."""
names = {name for _, name in args}
renamed: TemplateArgsType = []
for type_, name in args:
if name == "iteration":
depth = 1
while f"iteration_{depth}" in names:
depth += 1
name = f"iteration_{depth}"
names.add(name)
renamed.append((type_, name))
return renamed
@register_action(
"repeat",
RepeatAction,
@@ -929,7 +944,7 @@ async def repeat_action_to_code(
actions = await build_action_list(
config[CONF_THEN],
cg.TemplateArguments(cg.uint32, *template_arg.args),
[(cg.uint32, "iteration"), *args],
[(cg.uint32, "iteration"), *_shadow_iteration(args)],
)
for action in actions:
cg.add(var.add_then_action(action))
@@ -0,0 +1,3 @@
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["uart"]
DOMAIN = "apc_proteous"
@@ -0,0 +1,242 @@
#include "apc_proteous_cover.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::apc_proteous {
ESPHOME_LOG_TAG(TAG, "apc_proteous.cover");
static constexpr const char *START_CMD = "*1\r"; // start/stop toggle
static constexpr const char *OPEN_CMD = "*6\r";
static constexpr const char *CLOSE_CMD = "*7\r";
static constexpr const char *QUERY_S = "?s\r";
static constexpr const char *QUERY_X = "?x\r";
// Depending on calibration the reported position may stop a little short of the physical limits
// (e.g. 99% at the fully open reed switch), so readings this close to an end count as that end.
static constexpr uint8_t ENDPOINT_MARGIN = 2;
using namespace esphome::cover;
void APCProteousCover::setup() {
auto restore = this->restore_state_();
if (restore.has_value()) {
restore->apply(this);
} else {
this->position = 0.5f;
}
}
void APCProteousCover::loop() {
uint8_t data;
while (this->available() > 0) {
if (!this->read_byte(&data)) {
continue;
}
if (data == '\r') {
if (this->rx_len_ != 0) {
this->rx_buffer_[this->rx_len_] = '\0';
ESP_LOGVV(TAG, "rx: '%s'", this->rx_buffer_);
this->parse_response_();
this->rx_len_ = 0;
}
} else if (data != '\n') {
if (this->rx_len_ >= MAX_RESPONSE_LEN) {
ESP_LOGVV(TAG, "rx overflow, discarding");
this->rx_len_ = 0;
}
this->rx_buffer_[this->rx_len_++] = static_cast<char>(data);
}
}
}
void APCProteousCover::parse_response_() {
// Expected format: "?s=XX" or "?x=XX" where XX is a hex value
if (this->rx_len_ < 4 || this->rx_buffer_[0] != '?' || this->rx_buffer_[2] != '=') {
ESP_LOGV(TAG, "Invalid response: %s", this->rx_buffer_);
return;
}
const char *hex_str = this->rx_buffer_ + 3;
char *end_ptr;
uint8_t value = static_cast<uint8_t>(strtol(hex_str, &end_ptr, 16));
if (end_ptr == hex_str) {
ESP_LOGW(TAG, "Failed to parse hex value: %s", this->rx_buffer_);
return;
}
bool state_changed = false;
char type = this->rx_buffer_[1];
if (type == 's') {
// bit 0 = operating, bit 1 = direction (0 = opening, 1 = closing)
CoverOperation new_operation = COVER_OPERATION_IDLE;
if (value & 0x01) {
new_operation = (value & 0x02) ? COVER_OPERATION_CLOSING : COVER_OPERATION_OPENING;
}
if (this->current_operation != new_operation) {
this->current_operation = new_operation;
state_changed = true;
}
// Motion in the commanded direction acknowledges the pending command
if ((this->pending_command_ == OPEN_CMD && new_operation == COVER_OPERATION_OPENING) ||
(this->pending_command_ == CLOSE_CMD && new_operation == COVER_OPERATION_CLOSING)) {
this->clear_pending_();
}
// A partial move is over once the acknowledged motion has stopped, wherever it stopped, so an
// old target cannot halt a movement started from the wall button or the remote later on
if (new_operation == COVER_OPERATION_IDLE && this->pending_command_ == nullptr) {
this->target_position_.reset();
}
ESP_LOGV(TAG, "s-status: 0x%02X (operation=%d)", value, new_operation);
} else if (type == 'x') {
// Position percentage, snapped to the ends so is_open/is_closed can latch despite calibration offsets
float new_position;
if (value + ENDPOINT_MARGIN >= 100) {
new_position = COVER_OPEN;
} else if (value <= ENDPOINT_MARGIN) {
new_position = COVER_CLOSED;
} else {
new_position = value / 100.0f;
}
if (this->position != new_position) {
this->position = new_position;
state_changed = true;
// Until the controller has confirmed motion in the commanded direction, current_operation may
// still show the previous one, so a reversing partial move must not be judged against it
if (this->target_position_.has_value() && this->pending_command_ == nullptr &&
((this->current_operation == COVER_OPERATION_OPENING && this->position >= *this->target_position_) ||
(this->current_operation == COVER_OPERATION_CLOSING && this->position <= *this->target_position_))) {
ESP_LOGD(TAG, "Target position %.2f reached", *this->target_position_);
this->stop_cmd_(App.get_loop_component_start_time());
}
}
ESP_LOGV(TAG, "x-status: %d%% (position=%.2f)", value, this->position);
}
if (state_changed) {
this->publish_state();
}
}
void APCProteousCover::clear_pending_() {
this->pending_command_ = nullptr;
this->command_retries_ = 0;
}
void APCProteousCover::retry_pending_command_(uint32_t now) {
if (this->pending_command_ == nullptr || (now - this->pending_command_time_) < COMMAND_ACK_MS) {
return;
}
bool is_open_cmd = this->pending_command_ == OPEN_CMD;
// Nothing to resend once the gate is at the commanded end
if ((is_open_cmd && this->position >= COVER_OPEN) || (!is_open_cmd && this->position <= COVER_CLOSED)) {
this->clear_pending_();
return;
}
const LogString *label = is_open_cmd ? LOG_STR("open") : LOG_STR("close");
if (this->command_retries_ >= MAX_COMMAND_RETRIES) {
ESP_LOGW(TAG, "Gate did not acknowledge %s command after %u retries", LOG_STR_ARG(label), this->command_retries_);
this->clear_pending_();
this->target_position_.reset();
return;
}
this->command_retries_++;
ESP_LOGD(TAG, "Gate did not respond, resending %s command (retry %u)", LOG_STR_ARG(label), this->command_retries_);
this->pending_command_time_ = now;
this->write_command_(this->pending_command_, now);
}
void APCProteousCover::update() {
const uint32_t now = App.get_loop_component_start_time();
this->retry_pending_command_(now);
// Let the command and its echo clear the line before the next query
if ((now - this->last_command_tx_) < COMMAND_QUIET_MS) {
return;
}
// The line is idle, so anything left in the buffer is an unterminated echo
if (this->rx_len_ != 0) {
this->rx_buffer_[this->rx_len_] = '\0';
ESP_LOGVV(TAG, "discarding stale rx '%s' before query", this->rx_buffer_);
this->rx_len_ = 0;
}
const char *query = this->query_s_next_ ? QUERY_S : QUERY_X;
ESP_LOGVV(TAG, "tx: %s", query);
this->write_str(query);
this->query_s_next_ = !this->query_s_next_;
}
void APCProteousCover::dump_config() {
LOG_COVER("", "APC Proteous Cover", this);
LOG_UPDATE_INTERVAL(this);
}
CoverTraits APCProteousCover::get_traits() {
auto traits = CoverTraits();
traits.set_supports_position(true);
traits.set_supports_stop(true);
traits.set_supports_toggle(true);
return traits;
}
void APCProteousCover::write_command_(const char *cmd, uint32_t now) {
ESP_LOGVV(TAG, "tx: %s", cmd);
this->last_command_tx_ = now;
this->write_str(cmd);
}
void APCProteousCover::send_command_(const char *cmd) {
// The published state comes only from status reads; this just records the command so a stop
// can cancel it before the controller reports motion, and so it can be resent if dropped.
// The commands are idempotent, so only skip one when the gate is already moving that way.
CoverOperation operation = cmd == OPEN_CMD ? COVER_OPERATION_OPENING : COVER_OPERATION_CLOSING;
if (this->current_operation == operation) {
return;
}
ESP_LOGD(TAG, "Sending %s command",
operation == COVER_OPERATION_OPENING ? LOG_STR_LITERAL("open") : LOG_STR_LITERAL("close"));
const uint32_t now = App.get_loop_component_start_time();
this->pending_command_ = cmd;
this->pending_command_time_ = now;
this->command_retries_ = 0;
this->write_command_(cmd, now);
}
void APCProteousCover::stop_cmd_(uint32_t now) {
// START_CMD is a start/stop toggle, so it is only sent while the gate is moving, or while a
// movement command is in flight that the controller has not reported yet; otherwise it would start the gate.
if (this->current_operation == COVER_OPERATION_IDLE && this->pending_command_ == nullptr) {
ESP_LOGD(TAG, "Cover already idle, ignoring stop command");
return;
}
ESP_LOGD(TAG, "Sending stop command");
this->write_command_(START_CMD, now);
this->clear_pending_();
this->target_position_.reset();
}
void APCProteousCover::control(const CoverCall &call) {
if (call.get_stop()) {
this->stop_cmd_(App.get_loop_component_start_time());
} else if (call.get_position().has_value()) {
float target = *call.get_position();
ESP_LOGD(TAG, "Target position: %.2f, current: %.2f", target, this->position);
// There is no position command: a partial target is reached by moving towards it and stopping
// once the reported position gets there
this->target_position_.reset();
if (target == COVER_OPEN) {
this->send_command_(OPEN_CMD);
} else if (target == COVER_CLOSED) {
this->send_command_(CLOSE_CMD);
} else if (target != this->position) {
this->target_position_ = target;
this->send_command_(target > this->position ? OPEN_CMD : CLOSE_CMD);
}
} else if (call.get_toggle()) {
this->clear_pending_();
this->target_position_.reset();
this->write_command_(START_CMD, App.get_loop_component_start_time());
}
}
} // namespace esphome::apc_proteous
@@ -0,0 +1,52 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/cover/cover.h"
namespace esphome::apc_proteous {
class APCProteousCover : public cover::Cover, public PollingComponent, public uart::UARTDevice {
public:
void setup() override;
void loop() override;
void update() override;
void dump_config() override;
cover::CoverTraits get_traits() override;
protected:
void control(const cover::CoverCall &call) override;
void parse_response_();
void send_command_(const char *cmd);
void write_command_(const char *cmd, uint32_t now);
void stop_cmd_(uint32_t now);
void retry_pending_command_(uint32_t now);
void clear_pending_();
// The longest valid frame is "?s=XX"; anything longer is line noise
static constexpr size_t MAX_RESPONSE_LEN = 16;
// A movement command is acknowledged once the controller reports motion in the commanded
// direction; resend it this often, up to MAX_COMMAND_RETRIES times, since the serial link
// occasionally drops one. The stop toggle is never resent because a spurious toggle could start the gate.
static constexpr uint32_t COMMAND_ACK_MS = 2000;
static constexpr uint8_t MAX_COMMAND_RETRIES = 3;
// The controller echoes every command, sometimes without a terminator. A query sent too soon
// after a command collides with that echo, so polling pauses for this long after each command.
static constexpr uint32_t COMMAND_QUIET_MS = 250;
// Set only for a partial target; the gate is stopped when the reported position reaches it
optional<float> target_position_{};
// Last movement command sent and not yet acknowledged; lets a stop cancel it and drives the retries
const char *pending_command_{nullptr};
uint32_t pending_command_time_{0};
uint32_t last_command_tx_{0};
char rx_buffer_[MAX_RESPONSE_LEN + 1];
uint8_t rx_len_{0};
uint8_t command_retries_{0};
bool query_s_next_{true};
};
} // namespace esphome::apc_proteous
+33
View File
@@ -0,0 +1,33 @@
import esphome.codegen as cg
from esphome.components import cover, uart
import esphome.config_validation as cv
from esphome.types import ConfigType
from . import DOMAIN
apc_proteous_ns = cg.esphome_ns.namespace("apc_proteous")
APCProteousCover = apc_proteous_ns.class_(
"APCProteousCover", cover.Cover, cg.PollingComponent, uart.UARTDevice
)
CONFIG_SCHEMA = (
cover.cover_schema(APCProteousCover)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.polling_component_schema("500ms"))
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
DOMAIN,
baud_rate=9600,
require_tx=True,
require_rx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
async def to_code(config: ConfigType) -> None:
var = await cover.new_cover(config)
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
-33
View File
@@ -251,9 +251,6 @@ def _validate_supports_response(value: Any) -> str:
return cv.enum(SUPPORTS_RESPONSE_OPTIONS, lower=True)(value)
# ESP8266 copies every string of an action into a stack buffer sized by codegen; keep it small
ESP8266_ACTION_STRINGS_MAX_TOTAL = 384
VARIABLE_SCHEMA = cv.Schema(
{
cv.Required(CONF_TYPE): cv.one_of(*SERVICE_ARG_NATIVE_TYPES, lower=True),
@@ -484,29 +481,6 @@ def _action_strings(conf: ConfigType, has_metadata: bool) -> list[str | None]:
return strings
def _action_strings_size(strings: list[str | None]) -> int:
"""Bytes needed to copy every string out of flash, each with its terminator."""
return sum(
len(string.encode("utf-8")) + 1 for string in strings if string is not None
)
def _validate_esp8266_action_strings(config: ConfigType) -> ConfigType:
if not CORE.is_esp8266:
return config
actions = config.get(CONF_ACTIONS, [])
has_metadata = _has_action_metadata(actions)
for conf in actions:
size = _action_strings_size(_action_strings(conf, has_metadata))
if size > ESP8266_ACTION_STRINGS_MAX_TOTAL:
raise cv.Invalid(
f"Action '{conf[CONF_ACTION]}' has {size} bytes of name, variable name, "
f"description and example text; ESP8266 allows at most "
f"{ESP8266_ACTION_STRINGS_MAX_TOTAL} bytes per action"
)
return config
def _validate_outgoing_host_ipv6(config: ConfigType) -> ConfigType:
"""An IPv6 host can never be parsed, so never dialed, without IPv6."""
if (
@@ -531,7 +505,6 @@ def _validate_wizard(config: ConfigType) -> ConfigType:
FINAL_VALIDATE_SCHEMA = cv.All(
_validate_esp8266_action_strings,
_validate_outgoing_host_ipv6,
_validate_wizard,
)
@@ -587,7 +560,6 @@ async def to_code(config: ConfigType) -> None:
if config[CONF_HOMEASSISTANT_STATES]:
cg.add_define("USE_API_HOMEASSISTANT_STATES")
scratch_size = 0
if actions:
# Metadata is compiled in for every action once any action declares it, because the
# string table layout is fixed by the define rather than per action
@@ -639,8 +611,6 @@ async def to_code(config: ConfigType) -> None:
func_args.append((native, name))
strings = _action_strings(conf, has_metadata)
table = _add_action_strings(index, strings)
if CORE.is_esp8266:
scratch_size = max(scratch_size, _action_strings_size(strings))
# Template args: supports_response mode, then user service arg types
templ = cg.TemplateArguments(supports_response, *service_template_args)
# Key is hashed here because the name is not readable at runtime on ESP8266
@@ -667,9 +637,6 @@ async def to_code(config: ConfigType) -> None:
cg.add(auto.add_action(unregister_action))
# Register all services at once - single allocation, no reallocations
cg.add(var.initialize_user_services(triggers))
if CORE.is_esp8266 and has_user_actions:
# Stack buffer that list-entities copies PROGMEM strings into, sized for the largest action
cg.add_define("API_USER_ACTION_STRINGS_SCRATCH_SIZE", max(scratch_size, 1))
if (wizard_config := config.get(wizard.CONF_WIZARD)) is not None:
await wizard.to_code(wizard_config)
+16 -10
View File
@@ -403,6 +403,9 @@ message WizardCapabilities {
message SendspinCapabilities {
// Bitmask of the Sendspin features this device supports
uint32 feature_flags = 1;
// The Sendspin client_id: base64url of the client's static public key, 43 characters.
// Empty until the Sendspin client has started once since boot.
string client_id = 2 [(max_data_length) = 43];
}
message DeviceCapabilitiesResponse {
@@ -1026,8 +1029,8 @@ message SubscribeHomeassistantServicesRequest {
}
message HomeassistantServiceMap {
string key = 1;
string value = 2;
string key = 1 [(progmem) = true];
string value = 2 [(progmem) = true];
}
message HomeassistantActionRequest {
@@ -1036,7 +1039,7 @@ message HomeassistantActionRequest {
option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_SERVICES";
string service = 1;
string service = 1 [(progmem) = true];
repeated HomeassistantServiceMap data = 2 [(fixed_vector) = true];
repeated HomeassistantServiceMap data_template = 3 [(fixed_vector) = true];
repeated HomeassistantServiceMap variables = 4 [(fixed_vector) = true];
@@ -1157,21 +1160,21 @@ enum SupportsResponseType {
}
message ListEntitiesServicesArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
string name = 1 [(progmem) = true];
ServiceArgType type = 2;
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
string description = 3 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
string example = 4 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
}
message ListEntitiesServicesResponse {
option (id) = 41;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
string name = 1;
string name = 1 [(progmem) = true];
fixed32 key = 2 [(force) = true];
repeated ListEntitiesServicesArgument args = 3 [(fixed_vector) = true];
SupportsResponseType supports_response = 4;
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA"];
string description = 5 [(field_ifdef) = "USE_API_USER_DEFINED_ACTION_METADATA", (progmem) = true];
}
message ExecuteServiceArgument {
option (ifdef) = "USE_API_USER_DEFINED_ACTIONS";
@@ -3043,12 +3046,15 @@ enum SerialProxyIdentityFlag {
// IDs below are empty
}
// The descriptor fields of a USB device as seen by the host stack
// The descriptor fields of a USB device as seen by the host stack, and the port's index
// among those a vendor driver exposes on the interface
message UsbDeviceDescriptor {
uint32 vendor_id = 1;
uint32 product_id = 2;
uint32 bcd_device = 3;
uint32 interface_number = 4; // bInterfaceNumber the host driver binds to
uint32 interface_number = 4; // bInterfaceNumber the port is bound through
uint32 interface_class = 5; // bInterfaceClass of that interface
uint32 port_number = 6;
}
// The identity of the device behind a port. Identifies one physical device among others of
+13
View File
@@ -1,4 +1,7 @@
#include "api_connection.h"
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
#include "esphome/components/logger/logger.h"
#endif
#ifdef USE_API
#include "api_connection_buffer.h" // for the APIServer-dependent APIConnection inlines
#ifdef USE_API_NOISE
@@ -451,6 +454,15 @@ bool APIConnection::send_disconnect_response_() {
DisconnectResponse resp;
return this->send_message(resp);
}
void APIConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
this->flags_.log_subscription = msg.level;
if (msg.dump_config)
App.schedule_dump_config();
#if defined(USE_LOGGER) && defined(USE_CRASH_HANDLER)
logger::log_crash_to_client();
#endif
}
void APIConnection::on_disconnect_response() {
// Don't close socket here, let APIServer::loop() do it
// so getpeername() still works for the disconnect trigger
@@ -2119,6 +2131,7 @@ bool APIConnection::send_device_capabilities_response_() {
#endif
#if defined(USE_SENDSPIN) && defined(USE_ESP_IDF)
resp.sendspin.feature_flags = sendspin_::global_sendspin_hub->get_feature_flags();
resp.sendspin.client_id = sendspin_::global_sendspin_hub->get_client_id();
#endif
return this->send_message(resp);
}
+1 -24
View File
@@ -15,15 +15,6 @@
#include "subscribe_state.h"
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#ifdef USE_ESP32_CRASH_HANDLER
#include "esphome/components/esp32/crash_handler.h"
#endif
#ifdef USE_RP2_CRASH_HANDLER
#include "esphome/components/rp2/crash_handler.h"
#endif
#ifdef USE_ESP8266_CRASH_HANDLER
#include "esphome/components/esp8266/crash_handler.h"
#endif
#include "esphome/core/entity_base.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
@@ -298,21 +289,7 @@ class APIConnection final : public APIServerConnectionBase {
this->begin_iterator_(ActiveIterator::INITIAL_STATE);
}
}
void on_subscribe_logs_request(const SubscribeLogsRequest &msg) {
this->flags_.log_subscription = msg.level;
if (msg.dump_config)
App.schedule_dump_config();
#ifdef USE_ESP32_CRASH_HANDLER
esp32::crash_handler_log();
esp32::crash_handler_clear();
#endif
#ifdef USE_RP2_CRASH_HANDLER
rp2::crash_handler_log();
#endif
#ifdef USE_ESP8266_CRASH_HANDLER
esp8266::crash_handler_log();
#endif
}
void on_subscribe_logs_request(const SubscribeLogsRequest &msg);
#ifdef USE_API_HOMEASSISTANT_SERVICES
void on_subscribe_homeassistant_services_request() { this->flags_.service_call_subscription = true; }
#endif
+1 -1
View File
@@ -123,7 +123,7 @@ extend google.protobuf.FieldOptions {
// from an absent one (e.g. a UTC ParsedTimezone, which is all zeros).
optional bool track_presence = 50020 [default=false];
// progmem: The pointer_to_buffer bytes field points into flash (PROGMEM).
// progmem: The string field, or pointer_to_buffer bytes field, points into flash (PROGMEM).
// The generated encode and dump copy it with progmem_memcpy, as ESP8266
// cannot read flash with ordinary loads.
optional bool progmem = 50021 [default=false];
+14 -8
View File
@@ -310,12 +310,14 @@ uint32_t WizardCapabilities::calc_size_msg(const void *self) {
uint8_t *SendspinCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.feature_flags);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.client_id);
return pos;
}
uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const SendspinCapabilities *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.feature_flags);
size += !msg.client_id.empty() ? 2 + msg.client_id.size() : 0;
return size;
}
#endif
@@ -1140,8 +1142,8 @@ uint32_t NoiseEncryptionSetKeyResponse::calc_size_msg(const void *self) {
#ifdef USE_API_HOMEASSISTANT_SERVICES
uint8_t *HomeassistantServiceMap::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const HomeassistantServiceMap *>(self);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.key);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.value);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.key);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.value);
return pos;
}
uint32_t HomeassistantServiceMap::calc_size_msg(const void *self) {
@@ -1153,7 +1155,7 @@ uint32_t HomeassistantServiceMap::calc_size_msg(const void *self) {
}
uint8_t *HomeassistantActionRequest::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const HomeassistantActionRequest *>(self);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.service);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.service);
for (auto &it : msg.data) {
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 2, it);
}
@@ -1324,13 +1326,13 @@ void GetTimeResponse::decode_field(void *self, uint32_t tag, const uint8_t *data
uint8_t *ListEntitiesServicesArgument::encode_msg(const void *self,
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const ListEntitiesServicesArgument *>(self);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, static_cast<uint32_t>(msg.type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.description);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.example);
#endif
return pos;
}
@@ -1350,14 +1352,14 @@ uint32_t ListEntitiesServicesArgument::calc_size_msg(const void *self) {
uint8_t *ListEntitiesServicesResponse::encode_msg(const void *self,
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const ListEntitiesServicesResponse *>(self);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.name);
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, msg.key);
for (auto &it : msg.args) {
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 3, it);
}
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, static_cast<uint32_t>(msg.supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.description);
pos = ProtoEncode::encode_progmem_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.description);
#endif
return pos;
}
@@ -4175,6 +4177,8 @@ uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.product_id);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.bcd_device);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.interface_number);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.interface_class);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.port_number);
return pos;
}
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
@@ -4184,6 +4188,8 @@ uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.product_id);
size += ProtoSize::calc_uint32(1, msg.bcd_device);
size += ProtoSize::calc_uint32(1, msg.interface_number);
size += ProtoSize::calc_uint32(1, msg.interface_class);
size += ProtoSize::calc_uint32(1, msg.port_number);
return size;
}
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
+11 -8
View File
@@ -750,6 +750,7 @@ class WizardCapabilities final : public ProtoMessage {
class SendspinCapabilities final : public ProtoMessage {
public:
uint32_t feature_flags{0};
StringRef client_id{nullptr, 0}; // null until set, encode only
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -1388,8 +1389,8 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
#ifdef USE_API_HOMEASSISTANT_SERVICES
class HomeassistantServiceMap final : public ProtoMessage {
public:
StringRef key{nullptr, 0}; // null until set, encode only
StringRef value{nullptr, 0}; // null until set, encode only
ProgmemStringRef key{};
ProgmemStringRef value{};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
@@ -1409,7 +1410,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("homeassistant_action_request"); }
#endif
StringRef service{nullptr, 0}; // null until set, encode only
ProgmemStringRef service{};
FixedVector<HomeassistantServiceMap> data{};
FixedVector<HomeassistantServiceMap> data_template{};
FixedVector<HomeassistantServiceMap> variables{};
@@ -1576,13 +1577,13 @@ class GetTimeResponse final : public ProtoDecodableMessage {
#ifdef USE_API_USER_DEFINED_ACTIONS
class ListEntitiesServicesArgument final : public ProtoMessage {
public:
StringRef name{nullptr, 0}; // null until set, encode only
ProgmemStringRef name{};
enums::ServiceArgType type{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{nullptr, 0}; // null until set, encode only
ProgmemStringRef description{};
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef example{nullptr, 0}; // null until set, encode only
ProgmemStringRef example{};
#endif
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1603,12 +1604,12 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_services_response"); }
#endif
StringRef name{nullptr, 0}; // null until set, encode only
ProgmemStringRef name{};
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
StringRef description{nullptr, 0}; // null until set, encode only
ProgmemStringRef description{};
#endif
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -4083,6 +4084,8 @@ class UsbDeviceDescriptor final : public ProtoMessage {
uint32_t product_id{0};
uint32_t bcd_device{0};
uint32_t interface_number{0};
uint32_t interface_class{0};
uint32_t port_number{0};
static uint8_t *encode_msg(const void *self, uint8_t *pos PROTO_ENCODE_DEBUG_PARAM);
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
return encode_msg(this, buffer.get_pos() PROTO_ENCODE_DEBUG_ARG);
+21 -10
View File
@@ -12,8 +12,9 @@ namespace esphome::api {
#ifdef USE_ESP8266
// Out-of-line to avoid inlining strlen_P/memcpy_P at every call site
void DumpBuffer::append_p_esp8266(const char *str) {
size_t len = strlen_P(str);
void DumpBuffer::append_p_esp8266(const char *str) { this->append_p_esp8266(str, strlen_P(str)); }
void DumpBuffer::append_p_esp8266(const char *str, size_t len) {
size_t space = CAPACITY - 1 - pos_;
if (len > space)
len = space;
@@ -134,6 +135,13 @@ static void dump_progmem_bytes_field(DumpBuffer &out, const char *field_name, co
progmem_memcpy(data_buf, data, len);
dump_bytes_field(out, field_name, data_buf, len, indent);
}
// Helper for string fields in flash
static void dump_progmem_string_field(DumpBuffer &out, const char *field_name, const ProgmemStringRef &value,
int indent = 2) {
append_field_prefix(out, field_name, indent);
out.append("'").append_p(value.progmem_ptr(), value.size()).append("'\n");
}
#pragma GCC diagnostic pop
template<> const char *proto_enum_to_string<enums::DisconnectReason>(enums::DisconnectReason value) {
@@ -1119,6 +1127,7 @@ const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SendspinCapabilities"));
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
dump_field(out, ESPHOME_PSTR("client_id"), this->client_id);
return out.c_str();
}
#endif
@@ -1511,13 +1520,13 @@ const char *NoiseEncryptionSetKeyResponse::dump_to(DumpBuffer &out) const {
#ifdef USE_API_HOMEASSISTANT_SERVICES
const char *HomeassistantServiceMap::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantServiceMap"));
dump_field(out, ESPHOME_PSTR("key"), this->key);
dump_field(out, ESPHOME_PSTR("value"), this->value);
dump_progmem_string_field(out, ESPHOME_PSTR("key"), this->key);
dump_progmem_string_field(out, ESPHOME_PSTR("value"), this->value);
return out.c_str();
}
const char *HomeassistantActionRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("HomeassistantActionRequest"));
dump_field(out, ESPHOME_PSTR("service"), this->service);
dump_progmem_string_field(out, ESPHOME_PSTR("service"), this->service);
for (const auto &it : this->data) {
out.append(4, ' ').append_p(ESPHOME_PSTR("data")).append(": ");
it.dump_to(out);
@@ -1612,19 +1621,19 @@ const char *GetTimeResponse::dump_to(DumpBuffer &out) const {
#ifdef USE_API_USER_DEFINED_ACTIONS
const char *ListEntitiesServicesArgument::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesArgument"));
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_progmem_string_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("type"), static_cast<enums::ServiceArgType>(this->type));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("description"), this->description);
dump_progmem_string_field(out, ESPHOME_PSTR("description"), this->description);
#endif
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("example"), this->example);
dump_progmem_string_field(out, ESPHOME_PSTR("example"), this->example);
#endif
return out.c_str();
}
const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesServicesResponse"));
dump_field(out, ESPHOME_PSTR("name"), this->name);
dump_progmem_string_field(out, ESPHOME_PSTR("name"), this->name);
dump_field(out, ESPHOME_PSTR("key"), this->key);
for (const auto &it : this->args) {
out.append(4, ' ').append_p(ESPHOME_PSTR("args")).append(": ");
@@ -1633,7 +1642,7 @@ const char *ListEntitiesServicesResponse::dump_to(DumpBuffer &out) const {
}
dump_field(out, ESPHOME_PSTR("supports_response"), static_cast<enums::SupportsResponseType>(this->supports_response));
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
dump_field(out, ESPHOME_PSTR("description"), this->description);
dump_progmem_string_field(out, ESPHOME_PSTR("description"), this->description);
#endif
return out.c_str();
}
@@ -2942,6 +2951,8 @@ const char *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("product_id"), this->product_id);
dump_field(out, ESPHOME_PSTR("bcd_device"), this->bcd_device);
dump_field(out, ESPHOME_PSTR("interface_number"), this->interface_number);
dump_field(out, ESPHOME_PSTR("interface_class"), this->interface_class);
dump_field(out, ESPHOME_PSTR("port_number"), this->port_number);
return out.c_str();
}
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
+1 -1
View File
@@ -547,7 +547,7 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
// arrive before the subscription and are lost - warn instead of failing silently.
ESP_LOGW(TAG, "Home Assistant %s '%.*s' dropped; %s",
call.is_event ? LOG_STR_LITERAL("event") : LOG_STR_LITERAL("action"),
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.c_str(),
static_cast<int>(call.service.size()), call.service.empty() ? "" : call.service.progmem_ptr(),
this->is_connected() ? LOG_STR_LITERAL("client has not subscribed to actions (yet)")
: LOG_STR_LITERAL("no client connected"));
}
+8 -8
View File
@@ -247,7 +247,7 @@ class CustomAPIDevice {
*/
void call_homeassistant_service(const std::string &service_name) {
HomeassistantActionRequest resp;
resp.service = StringRef(service_name);
resp.service = ProgmemStringRef(StringRef(service_name));
global_api_server->send_homeassistant_action(resp);
}
@@ -267,12 +267,12 @@ class CustomAPIDevice {
*/
void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp;
resp.service = StringRef(service_name);
resp.service = ProgmemStringRef(StringRef(service_name));
resp.data.init(data.size());
for (auto &it : data) {
auto &kv = resp.data.emplace_back();
kv.key = StringRef(it.first);
kv.value = StringRef(it.second); // data map lives until send completes
kv.key = ProgmemStringRef(StringRef(it.first));
kv.value = ProgmemStringRef(StringRef(it.second)); // data map lives until send completes
}
global_api_server->send_homeassistant_action(resp);
}
@@ -289,7 +289,7 @@ class CustomAPIDevice {
*/
void fire_homeassistant_event(const std::string &event_name) {
HomeassistantActionRequest resp;
resp.service = StringRef(event_name);
resp.service = ProgmemStringRef(StringRef(event_name));
resp.is_event = true;
global_api_server->send_homeassistant_action(resp);
}
@@ -309,13 +309,13 @@ class CustomAPIDevice {
*/
void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
HomeassistantActionRequest resp;
resp.service = StringRef(service_name);
resp.service = ProgmemStringRef(StringRef(service_name));
resp.is_event = true;
resp.data.init(data.size());
for (auto &it : data) {
auto &kv = resp.data.emplace_back();
kv.key = StringRef(it.first);
kv.value = StringRef(it.second); // data map lives until send completes
kv.key = ProgmemStringRef(StringRef(it.first));
kv.value = ProgmemStringRef(StringRef(it.second)); // data map lives until send completes
}
global_api_server->send_homeassistant_action(resp);
}
+4 -28
View File
@@ -116,42 +116,18 @@ template<typename... Ts> class HomeAssistantServiceCallAction final : public Act
const Field *fields = this->fields_;
const size_t total = 1 + this->data_count_ + this->data_template_count_ + this->variables_count_;
// Lambda results, and on ESP8266 the RAM copies of the flash strings, must live until the send
// Lambda results must live until the send; the constant strings are encoded from flash where they are
size_t lambda_count = 0;
#ifdef USE_ESP8266
size_t flash_len = 0;
#endif
for (size_t i = 0; i < total; i++) {
for (size_t i = 0; i < total; i++)
lambda_count += fields[i].fn != nullptr;
#ifdef USE_ESP8266
if (fields[i].fn == nullptr)
flash_len += ESPHOME_strlen_P(fields[i].value);
if (fields[i].key != nullptr)
flash_len += ESPHOME_strlen_P(fields[i].key);
#endif
}
FixedVector<std::string> results;
results.init(lambda_count);
#ifdef USE_ESP8266
SmallBufferWithHeapFallback<128, char> flash_copy(flash_len);
char *cursor = flash_copy.get();
#endif
auto string_ref = [&](const char *str) {
#ifdef USE_ESP8266
size_t len = ESPHOME_strlen_P(str);
memcpy_P(cursor, str, len);
StringRef ref(cursor, len);
cursor += len;
return ref;
#else
return StringRef(str);
#endif
};
auto string_ref = [](const char *str) { return ProgmemStringRef(str, ESPHOME_strlen_P(str)); };
auto value_ref = [&](const Field &field) {
if (field.fn == nullptr)
return string_ref(field.value);
results.push_back(field.fn(x...));
return StringRef(results.back());
return ProgmemStringRef(StringRef(results.back()));
};
auto fill = [&](FixedVector<HomeassistantServiceMap> &dest, uint8_t count) {
dest.init(count);
+1 -2
View File
@@ -99,8 +99,7 @@ ListEntitiesIterator::ListEntitiesIterator(APIConnection *client) : client_(clie
static constexpr uint8_t SERVICE_YIELD_INTERVAL = 3;
bool ListEntitiesIterator::on_service(UserServiceDescriptor *service) {
UserActionScratch scratch;
auto resp = service->encode_list_service_response(scratch);
auto resp = service->encode_list_service_response();
if (!this->client_->send_message(resp))
return false;
// at_ is this service's index
+34
View File
@@ -418,6 +418,18 @@ class ProtoEncode {
std::memcpy(pos + 2, ref.c_str(), ref.size());
return pos + 2 + ref.size();
}
/// encode_short_string_force for a string that may be in flash (PROGMEM).
[[nodiscard]] static inline uint8_t *encode_short_progmem_string_force(
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, const ProgmemStringRef &ref) {
#ifdef ESPHOME_DEBUG_API
assert(ref.size() < 128 && "encode_short_progmem_string_force: string exceeds max_data_length < 128");
#endif
PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size());
pos[0] = tag;
pos[1] = static_cast<uint8_t>(ref.size());
progmem_memcpy(pos + 2, ref.progmem_ptr(), ref.size());
return pos + 2 + ref.size();
}
/// Write a precomputed tag byte + 32-bit value. Outlined on embedded: one copy beats inline stores per field.
[[nodiscard]] static PROTO_OUTLINE_FOR_SIZE uint8_t *write_tag_and_fixed32(
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag, uint32_t value) {
@@ -481,6 +493,17 @@ class ProtoEncode {
progmem_memcpy(pos, data, len);
return pos + len;
}
/// encode_progmem_bytes for a string field that may be in flash.
[[nodiscard]] static inline uint8_t *encode_progmem_string(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, const ProgmemStringRef &ref) {
return encode_progmem_bytes(pos PROTO_ENCODE_DEBUG_ARG, field_id,
reinterpret_cast<const uint8_t *>(ref.progmem_ptr()), ref.size());
}
[[nodiscard]] static inline uint8_t *encode_progmem_string_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, const ProgmemStringRef &ref) {
return encode_progmem_bytes_force(pos PROTO_ENCODE_DEBUG_ARG, field_id,
reinterpret_cast<const uint8_t *>(ref.progmem_ptr()), ref.size());
}
[[nodiscard]] static inline uint8_t *encode_uint32_force(uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t value) {
pos = encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
@@ -680,9 +703,20 @@ class DumpBuffer {
return *this;
}
/// Append len bytes of a PROGMEM string
DumpBuffer &append_p(const char *str, size_t len) {
#ifdef USE_ESP8266
append_p_esp8266(str, len);
#else
append_impl_(str, len);
#endif
return *this;
}
#ifdef USE_ESP8266
/// Out-of-line ESP8266 PROGMEM append to avoid inlining strlen_P/memcpy_P at every call site
void append_p_esp8266(const char *str);
void append_p_esp8266(const char *str, size_t len);
#endif
const char *c_str() const { return buf_; }
+9 -20
View File
@@ -1,47 +1,36 @@
#include "user_services.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
StringRef UserServiceStatic::str_(size_t idx, std::span<char> &scratch) const {
ProgmemStringRef UserServiceStatic::str_(size_t idx) const {
const char *s = progmem_read_ptr(&this->strings_[idx]);
if (s == nullptr)
return {};
#ifdef USE_ESP8266
// Codegen sizes the scratch buffer for the largest service; the bound only guards other callers
if (scratch.empty())
return {};
size_t len = strnlen_P(s, scratch.size() - 1);
progmem_memcpy(scratch.data(), s, len);
scratch[len] = '\0';
StringRef ref(scratch.data(), len);
scratch = scratch.subspan(len + 1);
return ref;
#else
return StringRef(s);
#endif
return ProgmemStringRef(s, ESPHOME_strlen_P(s));
}
ListEntitiesServicesResponse UserServiceStatic::encode_list_service_response_(
std::span<const enums::ServiceArgType> arg_types, std::span<char> scratch) const {
std::span<const enums::ServiceArgType> arg_types) const {
ListEntitiesServicesResponse msg;
msg.name = this->str_(0, scratch);
msg.name = this->str_(0);
msg.key = this->key_;
msg.supports_response = this->supports_response_;
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
msg.description = this->str_(1, scratch);
msg.description = this->str_(1);
#endif
msg.args.init(arg_types.size());
for (size_t i = 0; i < arg_types.size(); i++) {
size_t base = USER_ACTION_HEADER_STRINGS + i * USER_ACTION_ARG_STRINGS;
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = this->str_(base, scratch);
arg.name = this->str_(base);
#ifdef USE_API_USER_DEFINED_ACTION_METADATA
arg.description = this->str_(base + 1, scratch);
arg.example = this->str_(base + 2, scratch);
arg.description = this->str_(base + 1);
arg.example = this->str_(base + 2);
#endif
}
return msg;
+10 -21
View File
@@ -20,9 +20,8 @@ class APIServer;
class UserServiceDescriptor {
public:
/// Build the list-entities message. On ESP8266 the strings live in PROGMEM and are copied into
/// `scratch`, so the returned message is only valid while `scratch` is; other platforms ignore it.
virtual ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) = 0;
/// Build the list-entities message; its strings may point into flash (PROGMEM).
virtual ListEntitiesServicesResponse encode_list_service_response() = 0;
virtual bool execute_service(const ExecuteServiceRequest &req) = 0;
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -37,13 +36,6 @@ template<typename T> T get_execute_arg_value(const ExecuteServiceArgument &arg);
template<typename T> enums::ServiceArgType to_service_arg_type();
// Scratch buffer list-entities hands to encode_list_service_response(); only ESP8266 copies into it
#ifdef USE_ESP8266
using UserActionScratch = std::array<char, API_USER_ACTION_STRINGS_SCRATCH_SIZE>;
#else
using UserActionScratch = std::array<char, 0>;
#endif
// Non-template base for YAML-defined services so the list-entities encoder is compiled once.
// All strings live in one PROGMEM pointer table emitted by codegen (see _action_strings in
// __init__.py), so each service costs a single pointer of RAM. Layout: the action name, then
@@ -63,12 +55,9 @@ class UserServiceStatic : public UserServiceDescriptor {
: strings_(strings), key_(key), supports_response_(supports_response) {}
protected:
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types,
std::span<char> scratch) const;
/// Reference table entry `idx`; nullptr gives an empty StringRef.
/// On ESP8266 the bytes are copied out of PROGMEM into `scratch` with a terminator, and the span
/// is advanced past the copy.
StringRef str_(size_t idx, std::span<char> &scratch) const;
ListEntitiesServicesResponse encode_list_service_response_(std::span<const enums::ServiceArgType> arg_types) const;
/// Table entry `idx` as it sits in flash; nullptr gives an empty string.
ProgmemStringRef str_(size_t idx) const;
const char *const *strings_; // PROGMEM pointer table, read with progmem_read_ptr()
uint32_t key_;
@@ -79,9 +68,9 @@ template<typename... Ts> class UserServiceBase : public UserServiceStatic {
public:
using UserServiceStatic::UserServiceStatic;
ListEntitiesServicesResponse encode_list_service_response(std::span<char> scratch) override {
ListEntitiesServicesResponse encode_list_service_response() override {
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
return this->encode_list_service_response_(arg_types, scratch);
return this->encode_list_service_response_(arg_types);
}
bool execute_service(const ExecuteServiceRequest &req) override {
@@ -125,9 +114,9 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
this->key_ = fnv1_hash(this->name_.c_str());
}
ListEntitiesServicesResponse encode_list_service_response(std::span<char> /*scratch*/) override {
ListEntitiesServicesResponse encode_list_service_response() override {
ListEntitiesServicesResponse msg;
msg.name = StringRef(this->name_);
msg.name = ProgmemStringRef(StringRef(this->name_));
msg.key = this->key_;
msg.supports_response = enums::SUPPORTS_RESPONSE_NONE; // Dynamic services don't support responses yet
std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
@@ -135,7 +124,7 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
for (size_t i = 0; i < sizeof...(Ts); i++) {
auto &arg = msg.args.emplace_back();
arg.type = arg_types[i];
arg.name = StringRef(this->arg_names_[i]);
arg.name = ProgmemStringRef(StringRef(this->arg_names_[i]));
}
return msg;
}
+1 -1
View File
@@ -338,7 +338,7 @@ async def to_code(config: ConfigType) -> None:
# HTTPS streams verify the server against the root certificate bundle
require_certificate_bundle()
add_idf_component(name="esphome/esp-audio-libs", ref="4.0.1")
add_idf_component(name="esphome/esp-audio-libs", ref="4.1.0")
data = _get_data()
+2 -1
View File
@@ -1,10 +1,11 @@
#pragma once
#include <set>
#include "esphome/core/log.h"
namespace esphome::bedjet {
static const char *const TAG = "bedjet";
ESPHOME_LOG_TAG(TAG, "bedjet");
/// Converts a BedJet fan step to a speed percentage, in the range of 5% to 100%.
inline static uint8_t bedjet_fan_step_to_speed(const uint8_t fan) {
+1 -1
View File
@@ -4,7 +4,7 @@
namespace esphome::ble_client {
const char *const Automation::TAG = "ble_client.automation";
const char *const Automation::TAG = "ble_client.automation"; // NOLINT: ble_client never builds for ESP8266
} // namespace esphome::ble_client
@@ -382,7 +382,7 @@ void BluedroidGattClient::set_disconnecting_() {
esp_err_t BluedroidGattClient::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {{0}};
esp_ble_conn_update_params_t conn_params = {};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
+3 -2
View File
@@ -100,7 +100,8 @@ void BM8563::get_date_(ESPTime &time) {
this->read_register(DATE_FIRST_REG, buf, sizeof(buf));
time.day_of_month = this->bcd2_to_byte_(buf[0] & 0x3f);
time.day_of_week = this->bcd2_to_byte_(buf[1] & 0x07);
// The chip counts weekdays 0-6 from Sunday (not BCD), ESPTime counts 1-7
time.day_of_week = (buf[1] & 0x07) + 1;
time.month = this->bcd2_to_byte_(buf[2] & 0x1f);
uint8_t year_byte = this->bcd2_to_byte_(buf[3] & 0xff);
@@ -115,7 +116,7 @@ void BM8563::get_date_(ESPTime &time) {
void BM8563::set_date_(const ESPTime &time) {
uint8_t buf[4] = {
this->byte_to_bcd2_(time.day_of_month),
this->byte_to_bcd2_(time.day_of_week),
static_cast<uint8_t>(time.day_of_week - 1),
this->byte_to_bcd2_(time.month),
this->byte_to_bcd2_(time.year % 100),
};
+2 -1
View File
@@ -2,10 +2,11 @@
#include "esphome/components/bme280_base/bme280_base.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
namespace esphome::bme280_i2c {
static const char *const TAG = "bme280_i2c.sensor";
ESPHOME_LOG_TAG(TAG, "bme280_i2c.sensor");
class BME280I2CComponent final : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
bool read_byte(uint8_t a_register, uint8_t *data) override;
+11 -7
View File
@@ -2,6 +2,7 @@
#include "bmi270_config.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include "esphome/core/progmem.h"
namespace esphome::bmi270 {
@@ -11,6 +12,9 @@ using namespace bmm150; // NOLINT - sole consumer of the BMM150 aux-device defi
ESPHOME_LOG_TAG(TAG, "bmi270");
PROGMEM_STRING_TABLE(AccelRangeStrings, "±2g", "±4g", "±8g", "±16g");
PROGMEM_STRING_TABLE(GyroRangeStrings, "±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s");
#if defined(USE_ARDUINO) && !defined(USE_ESP32)
static constexpr size_t MAX_I2C_BUFFER_SIZE = 32;
#else
@@ -273,11 +277,10 @@ void BMI270Component::dump_config() {
return;
}
static constexpr const char *const ACCEL_RANGE_STRS[] = {"±2g", "±4g", "±8g", "±16g"};
static constexpr const char *const GYRO_RANGE_STRS[] = {"±2000°/s", "±1000°/s", "±500°/s", "±250°/s", "±125°/s"};
ESP_LOGCONFIG(TAG, " Accel range : %s", ACCEL_RANGE_STRS[accel_range_]);
ESP_LOGCONFIG(TAG, " Gyro range : %s", GYRO_RANGE_STRS[gyro_range_]);
ESP_LOGCONFIG(TAG, " Accel range : %s",
LOG_STR_ARG(AccelRangeStrings::get_log_str(this->accel_range_, AccelRangeStrings::LAST_INDEX)));
ESP_LOGCONFIG(TAG, " Gyro range : %s",
LOG_STR_ARG(GyroRangeStrings::get_log_str(this->gyro_range_, GyroRangeStrings::LAST_INDEX)));
#ifdef USE_BMI270_BMM150
if (this->aux_device_ == BMI270_AUX_DEVICE_BMM150) {
ESP_LOGCONFIG(TAG, " Aux device : BMM150 magnetometer (%s)",
@@ -315,7 +318,8 @@ bool BMI270Component::update_data(motion::MotionData &data) {
}
// Scale factor: LSB/g depends on range
// raw is a signed 16-bit value; full-scale = range_g * 2^15 lsb
static constexpr float ACCEL_SCALE[] = {
// In PROGMEM; each read is a 32-bit load, which is safe from flash on ESP8266
alignas(4) static constexpr float ACCEL_SCALE[] PROGMEM = {
2.0f / 32768.0f,
4.0f / 32768.0f,
8.0f / 32768.0f,
@@ -329,7 +333,7 @@ bool BMI270Component::update_data(motion::MotionData &data) {
// Gyroscope: registers 0x12–0x17 (6 bytes)
// Scale: full-scale range / 2^15
static constexpr float GYRO_SCALE[] = {
alignas(4) static constexpr float GYRO_SCALE[] PROGMEM = {
2000.0f / 32768.0f, 1000.0f / 32768.0f, 500.0f / 32768.0f, 250.0f / 32768.0f, 125.0f / 32768.0f,
};
static constexpr uint8_t GYR_OFFS = BMI270_REG_DATA_14 - BMI270_REG_DATA_8;
+2 -1
View File
@@ -2,10 +2,11 @@
#include "esphome/components/bmp280_base/bmp280_base.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
namespace esphome::bmp280_i2c {
static const char *const TAG = "bmp280_i2c.sensor";
ESPHOME_LOG_TAG(TAG, "bmp280_i2c.sensor");
/// This class implements support for the BMP280 Temperature+Pressure i2c sensor.
class BMP280I2CComponent final : public esphome::bmp280_base::BMP280Component, public i2c::I2CDevice {
+2 -1
View File
@@ -2,10 +2,11 @@
#include "esphome/components/bmp581_base/bmp581_base.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/log.h"
namespace esphome::bmp581_i2c {
static const char *const TAG = "bmp581_i2c.sensor";
ESPHOME_LOG_TAG(TAG, "bmp581_i2c.sensor");
/// This class implements support for the BMP581 Temperature+Pressure i2c sensor.
class BMP581I2CComponent final : public esphome::bmp581_base::BMP581Component, public i2c::I2CDevice {
@@ -80,6 +80,9 @@ void CaptivePortal::setup() {
this->disable_loop();
}
void CaptivePortal::start() {
// Each start takes a web server reference that end() returns once
if (this->active_)
return;
this->base_->init();
if (!this->initialized_) {
this->base_->add_handler_without_auth(this);
@@ -8,7 +8,7 @@
namespace esphome::chsc6x {
static const char *const TAG = "chsc6x.touchscreen";
ESPHOME_LOG_TAG(TAG, "chsc6x.touchscreen");
static const uint8_t CHSC6X_REG_STATUS = 0x00;
static const uint8_t CHSC6X_REG_STATUS_TOUCH = 0x00;
+1
View File
@@ -15,6 +15,7 @@ CONF_AQI = "aqi"
CONF_AUTOMATIC_BASELINE_CALIBRATION = "automatic_baseline_calibration"
CONF_B_CONSTANT = "b_constant"
CONF_BLUE_CHANNEL = "blue_channel"
CONF_BORDER_ROUTER = "border_router"
CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
CONF_BYTE_ORDER = "byte_order"
CONF_CHANNEL_COLORS = "channel_colors"
+22 -4
View File
@@ -8,6 +8,9 @@ namespace esphome::cse7766 {
ESPHOME_LOG_TAG(TAG, "cse7766");
// The chip only sends the time since the last pulse once a cycle is longer than 1 s (in us)
static constexpr uint32_t MIN_HELD_POWER_CYCLE = 1000000;
void CSE7766Component::loop() {
const uint32_t now = App.get_loop_component_start_time();
if (now - this->last_transmission_ >= 500) {
@@ -154,18 +157,33 @@ void CSE7766Component::parse_data_() {
if (power_cycle_exceeds_range) {
// Datasheet: power cycle exceeding range means active power is 0
have_power = true;
this->last_power_cycle_ = 0;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(0.0f);
}
} else if (have_power) {
power = power_coeff / float(power_cycle);
this->last_power_cycle_ = power_cycle;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
} else if (this->power_sensor_ != nullptr) {
// No valid power measurement from chip - publish 0W to avoid stale readings
// This typically happens when current is below the measurable threshold (~50mA)
this->power_sensor_->publish_state(0.0f);
} else if (this->last_power_cycle_ > MIN_HELD_POWER_CYCLE && power_cycle <= 2 * this->last_power_cycle_) {
// No new measurement in this frame. With cycles over 1 s the register holds the last
// cycle or the time elapsed since the last pulse. While that time is within twice the
// last cycle, the next pulse is not overdue, so the last measurement still applies.
// Shorter cycles never get here in normal operation and keep the previous behaviour.
have_power = true;
power = power_coeff / float(this->last_power_cycle_);
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(power);
}
} else {
// The next pulse is overdue: the load has dropped, report 0 W. A load that drops to less
// than half also reads 0 W here until its first new pulse arrives.
this->last_power_cycle_ = 0;
if (this->power_sensor_ != nullptr) {
this->power_sensor_->publish_state(0.0f);
}
}
float current = 0.0f;
+3 -1
View File
@@ -36,6 +36,7 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
uint8_t raw_data_[CSE7766_RAW_DATA_SIZE];
uint8_t raw_data_index_{0};
uint16_t cf_pulses_last_{0};
uint32_t last_transmission_{0};
sensor::Sensor *voltage_sensor_{nullptr};
sensor::Sensor *current_sensor_{nullptr};
@@ -45,7 +46,8 @@ class CSE7766Component final : public Component, public uart::UARTDevice {
sensor::Sensor *reactive_power_sensor_{nullptr};
sensor::Sensor *power_factor_sensor_{nullptr};
uint32_t cf_pulses_total_{0};
uint16_t cf_pulses_last_{0};
// Power cycle of the last frame that carried a new power measurement, 0 if none
uint32_t last_power_cycle_{0};
};
} // namespace esphome::cse7766
@@ -8,7 +8,7 @@
namespace esphome::cst816 {
static const char *const TAG = "cst816.touchscreen";
ESPHOME_LOG_TAG(TAG, "cst816.touchscreen");
static const uint8_t REG_STATUS = 0x00;
static const uint8_t REG_TOUCH_NUM = 0x02;
@@ -8,7 +8,7 @@
namespace esphome::cst9220 {
static const char *const TAG = "cst9220.touchscreen";
ESPHOME_LOG_TAG(TAG, "cst9220.touchscreen");
// The CST92xx family uses 16-bit (big-endian) register addresses.
static const uint16_t REG_TOUCH_DATA = 0xD000; // touch report
@@ -0,0 +1,76 @@
from collections.abc import Callable
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_MODE
from esphome.core import ID
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@remcom"]
DEPENDENCIES = ["uart"]
DOMAIN = "dfrobot_sen0609"
MULTI_CONF = True
dfrobot_sen0609_ns = cg.esphome_ns.namespace("dfrobot_sen0609")
DFRobotSEN0609Component = dfrobot_sen0609_ns.class_(
"DFRobotSEN0609Component", cg.Component, uart.UARTDevice
)
RunMode = dfrobot_sen0609_ns.enum("RunMode", is_class=True)
CONF_DFROBOT_SEN0609_ID = "dfrobot_sen0609_id"
MODE_PRESENCE = "presence"
MODE_SPEED_AND_DISTANCE = "speed_and_distance"
RUN_MODES = {
MODE_PRESENCE: RunMode.RUN_MODE_PRESENCE,
MODE_SPEED_AND_DISTANCE: RunMode.RUN_MODE_SPEED_AND_DISTANCE,
}
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(DFRobotSEN0609Component),
cv.Optional(CONF_MODE, default=MODE_PRESENCE): cv.one_of(
*RUN_MODES, lower=True
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
DOMAIN,
baud_rate=9600,
require_tx=True,
require_rx=True,
parity="NONE",
stop_bits=1,
)
def get_hub_config(root: ConfigType, hub_id: ID) -> ConfigType:
"""Return the configuration of the hub with the given id."""
return next(conf for conf in root[DOMAIN] if conf[CONF_ID] == hub_id)
def final_validate_mode(keys: dict[str, str]) -> Callable[[ConfigType], None]:
"""Reject entity keys that need a different hub mode; `keys` maps each key to the mode it needs."""
def validator(config: ConfigType) -> None:
hub = get_hub_config(fv.full_config.get(), config[CONF_DFROBOT_SEN0609_ID])
for key, mode in keys.items():
if key in config and hub[CONF_MODE] != mode:
raise cv.Invalid(
f"'{key}' needs the {DOMAIN} 'mode' to be '{mode}'", path=[key]
)
return validator
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID], RUN_MODES[config[CONF_MODE]])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
@@ -0,0 +1,24 @@
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_HAS_TARGET, CONF_ID, DEVICE_CLASS_OCCUPANCY, ICON_ACCOUNT
from esphome.types import ConfigType
from . import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
DEPENDENCIES = ["dfrobot_sen0609"]
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_OCCUPANCY,
icon=ICON_ACCOUNT,
),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
binary_sensors = binary_sensor.sub_binary_sensors(config)
await binary_sensors(CONF_HAS_TARGET, hub.set_has_target_binary_sensor)
@@ -0,0 +1,483 @@
#include "dfrobot_sen0609.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/progmem.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <cstring>
namespace esphome::dfrobot_sen0609 {
ESPHOME_LOG_TAG(TAG, "dfrobot_sen0609");
static constexpr uint8_t MAX_ATTEMPTS = 3;
// The module ignores a command that follows its last answer too closely
static constexpr uint32_t COMMAND_GAP_MS = 100;
static constexpr uint32_t DEFAULT_TIMEOUT_MS = 1000;
static constexpr uint32_t LONG_TIMEOUT_MS = 2000;
static constexpr uint32_t GIVE_UP_BACKOFF_MS = 30000;
// Presence frames come every second (setUartOutput period); this long without one means the module lost them
static constexpr uint32_t FRAME_TIMEOUT_MS = 10000;
static constexpr uint8_t FRAME_FIELDS = 5;
static constexpr uint8_t LINE_END[] = {'\r', '\n'};
// Indexed by Param; empty for parameters that cannot be read
PROGMEM_STRING_TABLE(ReadCommands, "", "", "getSWV", "getHWV");
static_assert(PARAM_COUNT <= 16, "pending_writes_ and pending_reads_ hold one bit per parameter");
static constexpr uint16_t param_bit(Param param) { return static_cast<uint16_t>(1U << param); }
static bool starts_with(const char *str, ESPHOME_PGM_P prefix) {
return ESPHOME_strncmp_P(str, prefix, ESPHOME_strlen_P(prefix)) == 0;
}
static bool contains(const char *str, ESPHOME_PGM_P needle) {
const size_t len = ESPHOME_strlen_P(needle);
for (; *str != '\0'; str++) {
if (ESPHOME_strncmp_P(str, needle, len) == 0)
return true;
}
return false;
}
// Splits "a,b,c*" in place; returns the number of fields stored in `fields`
static uint8_t split_fields(char *str, char **fields, uint8_t max_fields) {
uint8_t count = 0;
char *start = str;
for (;; str++) {
if (*str != ',' && *str != '*' && *str != '\0')
continue;
const bool last = *str != ',';
*str = '\0';
if (count < max_fields)
fields[count++] = start;
if (last)
return count;
start = str + 1;
}
}
// Answers that refuse a command whose effect is already in place
static bool answer_means_done(Operation operation, const char *line) {
switch (operation) {
case Operation::OPERATION_STOP:
return contains(line, ESPHOME_PSTR("stopped already"));
case Operation::OPERATION_START:
return contains(line, ESPHOME_PSTR("started already"));
default:
return false;
}
}
static bool parse_float(const char *str, float &value) {
char *end;
value = strtof(str, &end);
return end != str && *end == '\0';
}
static const LogString *operation_to_str(Operation operation) {
switch (operation) {
case Operation::OPERATION_STOP:
return LOG_STR("stop");
case Operation::OPERATION_START:
return LOG_STR("start");
case Operation::OPERATION_WRITE:
return LOG_STR("write");
case Operation::OPERATION_READ:
return LOG_STR("read");
default:
return LOG_STR("none");
}
}
DFRobotSEN0609Component::DFRobotSEN0609Component(RunMode mode) : mode_(mode) {}
void DFRobotSEN0609Component::dump_config() {
ESP_LOGCONFIG(
TAG,
"DFRobot SEN0609:\n"
" Mode: %s",
this->mode_ == RunMode::RUN_MODE_PRESENCE ? LOG_STR_LITERAL("presence") : LOG_STR_LITERAL("speed and distance"));
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "HasTarget", this->has_target_binary_sensor_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR(" ", "TargetDistance", this->target_distance_sensor_);
LOG_SENSOR(" ", "TargetSpeed", this->target_speed_sensor_);
LOG_SENSOR(" ", "TargetEnergy", this->target_energy_sensor_);
#endif
#ifdef USE_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Version", this->version_text_sensor_);
LOG_TEXT_SENSOR(" ", "HardwareVersion", this->hardware_version_text_sensor_);
#endif
}
void DFRobotSEN0609Component::setup() { this->queue_configuration_(); }
void DFRobotSEN0609Component::loop() {
uint8_t buf[MAX_LINE_LENGTH];
size_t avail = this->available();
while (avail > 0) {
const size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read)) {
if (!this->read_failed_) {
ESP_LOGW(TAG, "UART read failed");
}
this->read_failed_ = true;
break;
}
this->read_failed_ = false;
avail -= to_read;
for (size_t i = 0; i < to_read; i++)
this->readline_(buf[i]);
}
this->run_sequencer_();
}
void DFRobotSEN0609Component::readline_(uint8_t c) {
if (c == '\r')
return;
if (c == '\n') {
if (!this->line_overflow_) {
this->line_[this->line_pos_] = '\0';
this->handle_line_(this->line_);
}
this->line_overflow_ = false;
this->line_pos_ = 0;
return;
}
if (this->line_overflow_)
return;
if (this->line_pos_ >= MAX_LINE_LENGTH - 1) {
ESP_LOGW(TAG, "Line too long, dropped");
this->line_overflow_ = true;
return;
}
this->line_[this->line_pos_++] = static_cast<char>(c);
// The prompt has no line end, and the echo of the next command follows it on the same line
if (c == '>') {
this->line_[this->line_pos_] = '\0';
if (ESPHOME_strcmp_P(this->line_, ESPHOME_PSTR("DFRobot:/>")) == 0)
this->line_pos_ = 0;
}
}
void DFRobotSEN0609Component::handle_line_(char *line) {
if (*line == '\0')
return;
ESP_LOGV(TAG, "RX: %s", line);
if (line[0] == '$') {
this->handle_frame_(line);
return;
}
// Version lines name their value, so they are used even when they arrive late
const bool software = starts_with(line, ESPHOME_PSTR("SoftwareVersion:"));
if (software || starts_with(line, ESPHOME_PSTR("HardwareVersion:"))) {
if (this->operation_ == Operation::OPERATION_READ &&
this->param_ == (software ? PARAM_SW_VERSION : PARAM_HW_VERSION))
this->version_received_ = true;
#ifdef USE_TEXT_SENSOR
text_sensor::TextSensor *sens = software ? this->version_text_sensor_ : this->hardware_version_text_sensor_;
if (sens != nullptr)
sens->publish_state(line + 16);
#endif
return;
}
if (this->operation_ == Operation::OPERATION_NONE) {
ESP_LOGD(TAG, "Ignored line, no command pending: %s", line);
return;
}
if (strcmp(line, this->command_) == 0) {
this->echo_seen_ = true;
return;
}
// The module echoes every command first; lines before the echo belong to an earlier command
if (!this->echo_seen_) {
ESP_LOGD(TAG, "Ignored line from an earlier command: %s", line);
return;
}
if (starts_with(line, ESPHOME_PSTR("Done")) || answer_means_done(this->operation_, line)) {
this->finish_operation_();
} else if (contains(line, ESPHOME_PSTR("not stopped"))) {
// Stop the sensor before the next attempt
this->stopped_ = false;
this->fail_operation_();
} else if (starts_with(line, ESPHOME_PSTR("Error")) || contains(line, ESPHOME_PSTR("not recognized"))) {
ESP_LOGW(TAG, "Command '%s' rejected: %s", this->command_, line);
this->fail_operation_();
} else {
ESP_LOGD(TAG, "Unexpected answer to '%s': %s", this->command_, line);
}
}
void DFRobotSEN0609Component::handle_frame_(char *line) {
// $DFHPD,<presence>, , , *
if (starts_with(line, ESPHOME_PSTR("$DFHPD,"))) {
const char presence = line[7];
if (presence != '0' && presence != '1') {
this->log_bad_frame_(LOG_STR("Malformed presence frame"), line);
return;
}
this->last_frame_ms_ = App.get_loop_component_start_time();
#ifdef USE_BINARY_SENSOR
if (this->has_target_binary_sensor_ != nullptr)
this->has_target_binary_sensor_->publish_state(presence == '1');
#endif
return;
}
// $DFDMD,<targets>,<reserved>,<distance m>,<speed m/s>,<energy>,...*
if (!starts_with(line, ESPHOME_PSTR("$DFDMD,"))) {
this->log_bad_frame_(LOG_STR("Unknown frame"), line);
return;
}
char *fields[FRAME_FIELDS];
float distance;
float speed;
float energy;
if (split_fields(line + 7, fields, FRAME_FIELDS) < FRAME_FIELDS) {
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
return;
}
const optional<uint8_t> targets = parse_number<uint8_t>(fields[0]);
if (!targets.has_value()) {
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
return;
}
if (*targets == 0) {
#ifdef USE_BINARY_SENSOR
if (this->has_target_binary_sensor_ != nullptr)
this->has_target_binary_sensor_->publish_state(false);
#endif
#ifdef USE_SENSOR
this->publish_target_(NAN, NAN, NAN);
#endif
return;
}
if (!parse_float(fields[2], distance) || !parse_float(fields[3], speed) || !parse_float(fields[4], energy)) {
this->log_bad_frame_(LOG_STR("Malformed target frame"), line);
return;
}
#ifdef USE_BINARY_SENSOR
if (this->has_target_binary_sensor_ != nullptr)
this->has_target_binary_sensor_->publish_state(true);
#endif
#ifdef USE_SENSOR
this->publish_target_(distance, speed, energy);
#endif
}
void DFRobotSEN0609Component::log_bad_frame_(const LogString *problem, const char *line) {
// The module sends frames several times a second, so only the first bad one is logged
if (this->bad_frame_logged_)
return;
this->bad_frame_logged_ = true;
ESP_LOGW(TAG, "%s: %s", LOG_STR_ARG(problem), line);
}
#ifdef USE_SENSOR
static void publish_deduplicated(sensor::Sensor *sens, Deduplicator<float> &dedup, float value) {
if (sens == nullptr)
return;
if (std::isnan(value) ? dedup.next_unknown() : dedup.next(value))
sens->publish_state(value);
}
void DFRobotSEN0609Component::publish_target_(float distance, float speed, float energy) {
publish_deduplicated(this->target_distance_sensor_, this->distance_dedup_, distance);
publish_deduplicated(this->target_speed_sensor_, this->speed_dedup_, speed);
publish_deduplicated(this->target_energy_sensor_, this->energy_dedup_, energy);
}
#endif
void DFRobotSEN0609Component::queue_configuration_() {
// The mode and the UART output are applied at every boot instead of being saved
uint16_t writes = param_bit(PARAM_RUN_APP);
if (this->mode_ == RunMode::RUN_MODE_PRESENCE)
writes |= param_bit(PARAM_UART_OUTPUT);
this->pending_writes_ |= writes;
this->pending_reads_ |= this->wanted_reads_();
}
uint16_t DFRobotSEN0609Component::wanted_reads_() const {
uint16_t reads = 0;
#ifdef USE_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr)
reads |= param_bit(PARAM_SW_VERSION);
if (this->hardware_version_text_sensor_ != nullptr)
reads |= param_bit(PARAM_HW_VERSION);
#endif
return reads;
}
void DFRobotSEN0609Component::check_frames_(uint32_t now) {
// Speed mode frames are not watched: whether the module sends them without a target is not known
if (this->mode_ != RunMode::RUN_MODE_PRESENCE || now - this->last_frame_ms_ < FRAME_TIMEOUT_MS)
return;
// The module most likely restarted on its own and lost the configuration applied at boot
ESP_LOGW(TAG, "No presence frames, applying the configuration again");
this->status_set_warning();
#ifdef USE_BINARY_SENSOR
if (this->has_target_binary_sensor_ != nullptr)
this->has_target_binary_sensor_->invalidate_state();
#endif
this->last_frame_ms_ = now;
this->queue_configuration_();
}
void DFRobotSEN0609Component::hold_(uint32_t duration_ms) {
this->hold_start_ms_ = App.get_loop_component_start_time();
this->hold_ms_ = duration_ms;
}
void DFRobotSEN0609Component::run_sequencer_() {
const uint32_t now = App.get_loop_component_start_time();
if (this->operation_ != Operation::OPERATION_NONE) {
const bool slow = this->operation_ == Operation::OPERATION_STOP;
if (now - this->sent_ms_ < (slow ? LONG_TIMEOUT_MS : DEFAULT_TIMEOUT_MS))
return;
ESP_LOGD(TAG, "No answer to '%s'", this->command_);
this->fail_operation_();
return;
}
if (now - this->hold_start_ms_ < this->hold_ms_)
return;
const bool work_pending = this->pending_writes_ != 0 || this->pending_reads_ != 0;
Operation operation;
Param param = PARAM_COUNT;
if (!work_pending) {
if (!this->stopped_) {
this->check_frames_(now);
return;
}
operation = Operation::OPERATION_START;
} else if (!this->stopped_) {
operation = Operation::OPERATION_STOP;
} else if (this->pending_writes_ != 0) {
operation = Operation::OPERATION_WRITE;
param = static_cast<Param>(__builtin_ctz(this->pending_writes_));
} else {
operation = Operation::OPERATION_READ;
param = static_cast<Param>(__builtin_ctz(this->pending_reads_));
}
const uint16_t bit = param_bit(param);
uint16_t &pending = operation == Operation::OPERATION_WRITE ? this->pending_writes_ : this->pending_reads_;
if (!this->build_command_(operation, param)) {
ESP_LOGE(TAG, "No command for %s of parameter %u", LOG_STR_ARG(operation_to_str(operation)),
static_cast<unsigned>(param));
pending &= ~bit;
this->sequence_failed_ = true;
this->status_set_warning();
return;
}
if (operation == Operation::OPERATION_WRITE)
pending &= ~bit;
this->operation_ = operation;
this->param_ = param;
this->echo_seen_ = false;
this->version_received_ = false;
this->sent_ms_ = now;
ESP_LOGV(TAG, "TX: %s", this->command_);
this->write_str(this->command_);
this->write_array(LINE_END, sizeof(LINE_END));
}
bool DFRobotSEN0609Component::build_command_(Operation operation, Param param) {
char *buf = this->command_;
constexpr size_t len = MAX_COMMAND_LENGTH;
switch (operation) {
case Operation::OPERATION_STOP:
ESPHOME_strncpy_P(buf, ESPHOME_PSTR("sensorStop"), len);
return true;
case Operation::OPERATION_START:
ESPHOME_strncpy_P(buf, ESPHOME_PSTR("sensorStart"), len);
return true;
case Operation::OPERATION_READ:
ESPHOME_strncpy_P(buf, reinterpret_cast<ESPHOME_PGM_P>(ReadCommands::get_progmem_str(param, 0)), len);
return buf[0] != '\0';
default:
break;
}
switch (param) {
case PARAM_RUN_APP:
ESPHOME_snprintf_P(buf, len, ESPHOME_PSTR("setRunApp %u"), static_cast<unsigned>(this->mode_));
return true;
case PARAM_UART_OUTPUT:
ESPHOME_strncpy_P(buf, ESPHOME_PSTR("setUartOutput 1 1 1 1.0"), len);
return true;
default:
buf[0] = '\0';
return false;
}
}
void DFRobotSEN0609Component::finish_operation_() {
switch (this->operation_) {
case Operation::OPERATION_STOP:
this->stopped_ = true;
break;
case Operation::OPERATION_START:
this->stopped_ = false;
this->last_frame_ms_ = App.get_loop_component_start_time();
if (!this->sequence_failed_)
this->status_clear_warning();
this->sequence_failed_ = false;
break;
case Operation::OPERATION_WRITE:
break;
case Operation::OPERATION_READ:
// The versions are published as their lines arrive
if (!this->version_received_) {
ESP_LOGW(TAG, "No version in the answer to '%s'", this->command_);
this->fail_operation_();
return;
}
this->pending_reads_ &= ~param_bit(this->param_);
break;
default:
return;
}
if (this->is_aux_operation_()) {
this->aux_attempts_ = 0;
} else {
this->attempts_ = 0;
}
this->operation_ = Operation::OPERATION_NONE;
this->hold_(COMMAND_GAP_MS);
}
void DFRobotSEN0609Component::fail_operation_() {
const bool aux = this->is_aux_operation_();
uint8_t &attempts = aux ? this->aux_attempts_ : this->attempts_;
const Operation operation = this->operation_;
this->operation_ = Operation::OPERATION_NONE;
this->hold_(COMMAND_GAP_MS);
// The writes are the boot configuration, which the frames depend on: keep them until they succeed
if (operation == Operation::OPERATION_WRITE)
this->pending_writes_ |= param_bit(this->param_);
if (++attempts < MAX_ATTEMPTS)
return;
attempts = 0;
this->status_set_warning();
if (aux || operation == Operation::OPERATION_WRITE) {
// Without stop, start or the configuration the sensor cannot run as set up; try again later
ESP_LOGW(TAG, "Command '%s' failed, trying again later", this->command_);
this->hold_ms_ = GIVE_UP_BACKOFF_MS;
return;
}
ESP_LOGW(TAG, "Giving up on %s '%s'", LOG_STR_ARG(operation_to_str(operation)), this->command_);
this->sequence_failed_ = true;
if (operation == Operation::OPERATION_READ)
this->pending_reads_ &= ~param_bit(this->param_);
}
} // namespace esphome::dfrobot_sen0609
@@ -0,0 +1,113 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
#include "esphome/components/uart/uart.h"
namespace esphome::dfrobot_sen0609 {
static constexpr uint8_t MAX_LINE_LENGTH = 80;
static constexpr uint8_t MAX_COMMAND_LENGTH = 40;
enum class RunMode : uint8_t {
RUN_MODE_PRESENCE = 0,
RUN_MODE_SPEED_AND_DISTANCE = 1,
};
/// Module parameters; a write or read of each is one command. Pending work runs in this order.
enum Param : uint8_t {
PARAM_RUN_APP,
PARAM_UART_OUTPUT,
PARAM_SW_VERSION,
PARAM_HW_VERSION,
PARAM_COUNT,
};
enum class Operation : uint8_t {
OPERATION_NONE,
OPERATION_STOP,
OPERATION_START,
OPERATION_WRITE,
OPERATION_READ,
};
class DFRobotSEN0609Component final : public Component, public uart::UARTDevice {
#ifdef USE_BINARY_SENSOR
SUB_BINARY_SENSOR(has_target)
#endif
#ifdef USE_SENSOR
SUB_SENSOR(target_distance)
SUB_SENSOR(target_speed)
SUB_SENSOR(target_energy)
#endif
#ifdef USE_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
SUB_TEXT_SENSOR(hardware_version)
#endif
public:
explicit DFRobotSEN0609Component(RunMode mode);
void setup() override;
void loop() override;
void dump_config() override;
protected:
void readline_(uint8_t c);
void handle_line_(char *line);
void handle_frame_(char *line);
void log_bad_frame_(const LogString *problem, const char *line);
void queue_configuration_();
uint16_t wanted_reads_() const;
void run_sequencer_();
void check_frames_(uint32_t now);
bool build_command_(Operation operation, Param param);
void finish_operation_();
void fail_operation_();
void hold_(uint32_t duration_ms);
bool is_aux_operation_() const {
return this->operation_ == Operation::OPERATION_STOP || this->operation_ == Operation::OPERATION_START;
}
#ifdef USE_SENSOR
void publish_target_(float distance, float speed, float energy);
Deduplicator<float> distance_dedup_;
Deduplicator<float> speed_dedup_;
Deduplicator<float> energy_dedup_;
#endif
char line_[MAX_LINE_LENGTH];
char command_[MAX_COMMAND_LENGTH]{};
uint32_t sent_ms_{0};
uint32_t hold_start_ms_{0};
uint32_t hold_ms_{0};
uint32_t last_frame_ms_{0};
uint16_t pending_writes_{0};
uint16_t pending_reads_{0};
RunMode mode_;
Operation operation_{Operation::OPERATION_NONE};
Param param_{PARAM_COUNT};
uint8_t line_pos_{0};
uint8_t attempts_{0};
uint8_t aux_attempts_{0};
bool line_overflow_{false};
bool read_failed_{false};
bool bad_frame_logged_{false};
bool echo_seen_{false};
bool version_received_{false};
bool stopped_{false};
bool sequence_failed_{false};
};
} // namespace esphome::dfrobot_sen0609
@@ -0,0 +1,64 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_SPEED,
ICON_SIGNAL,
STATE_CLASS_MEASUREMENT,
UNIT_METER,
UNIT_METER_PER_SECOND,
)
from esphome.types import ConfigType
from . import (
CONF_DFROBOT_SEN0609_ID,
MODE_SPEED_AND_DISTANCE,
DFRobotSEN0609Component,
final_validate_mode,
)
DEPENDENCIES = ["dfrobot_sen0609"]
CONF_TARGET_DISTANCE = "target_distance"
CONF_TARGET_ENERGY = "target_energy"
CONF_TARGET_SPEED = "target_speed"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
cv.Optional(CONF_TARGET_DISTANCE): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_DISTANCE,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_METER,
),
cv.Optional(CONF_TARGET_SPEED): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_SPEED,
state_class=STATE_CLASS_MEASUREMENT,
unit_of_measurement=UNIT_METER_PER_SECOND,
),
cv.Optional(CONF_TARGET_ENERGY): sensor.sensor_schema(
accuracy_decimals=0,
icon=ICON_SIGNAL,
state_class=STATE_CLASS_MEASUREMENT,
),
}
FINAL_VALIDATE_SCHEMA = final_validate_mode(
{
CONF_TARGET_DISTANCE: MODE_SPEED_AND_DISTANCE,
CONF_TARGET_ENERGY: MODE_SPEED_AND_DISTANCE,
CONF_TARGET_SPEED: MODE_SPEED_AND_DISTANCE,
}
)
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
sensors = sensor.sub_sensors(config)
await sensors(CONF_TARGET_DISTANCE, hub.set_target_distance_sensor)
await sensors(CONF_TARGET_SPEED, hub.set_target_speed_sensor)
await sensors(CONF_TARGET_ENERGY, hub.set_target_energy_sensor)
@@ -0,0 +1,29 @@
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_VERSION, ENTITY_CATEGORY_DIAGNOSTIC, ICON_CHIP
from esphome.types import ConfigType
from . import CONF_DFROBOT_SEN0609_ID, DFRobotSEN0609Component
DEPENDENCIES = ["dfrobot_sen0609"]
CONF_HARDWARE_VERSION = "hardware_version"
CONFIG_SCHEMA = {
cv.GenerateID(CONF_ID): cv.declare_id(cg.EntityBase),
cv.GenerateID(CONF_DFROBOT_SEN0609_ID): cv.use_id(DFRobotSEN0609Component),
cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP
),
cv.Optional(CONF_HARDWARE_VERSION): text_sensor.text_sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC, icon=ICON_CHIP
),
}
async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_DFROBOT_SEN0609_ID])
text_sensors = text_sensor.sub_text_sensors(config)
await text_sensors(CONF_VERSION, hub.set_version_text_sensor)
await text_sensors(CONF_HARDWARE_VERSION, hub.set_hardware_version_text_sensor)
+36
View File
@@ -250,6 +250,42 @@ def add_metadata(
)
KEY_EXTERNAL_WRITERS = "external_writers"
def _get_external_writer_list() -> list[tuple[ID, str]]:
"""Get the raw external writer list. Each entry is (display id, owner)."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_EXTERNAL_WRITERS, [])
def register_external_writer(display_id: ID, owner: str) -> None:
"""Declare that ``owner`` draws into the display from outside its lambda or pages.
Call from the owner's CONFIG_SCHEMA. The ID may still be unresolved; the ID pass
resolves it in place before final validation.
"""
_get_external_writer_list().append((display_id, owner))
def get_external_writers(display_id: ID) -> list[str]:
"""Get the owners registered for a display. Only valid once IDs are resolved."""
return [
owner
for id_, owner in _get_external_writer_list()
if id_.id is not None and id_.id == display_id.id
]
def has_external_writer(display_id: ID) -> bool:
"""Tell a driver's final validation that something else draws into the display.
LVGL anywhere in the config also counts, as the drivers assumed before.
"""
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
return LVGL_DOMAIN in full_config.get() or bool(get_external_writers(display_id))
async def register_display(var, config):
await cg.register_component(var, config)
await setup_display_core_(var, config)
+53 -27
View File
@@ -5,6 +5,7 @@
#include "display_color_utils.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::display {
@@ -52,40 +53,65 @@ void Display::line_at_angle(int x, int y, int angle, int start_radius, int stop_
this->line(x1, y1, x2, y2, color);
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
uint32_t color_value;
// flatten keeps to_color() inlined in the pixel loop; in PSRAM builds GCC at
// -Os stops inlining it once there are two call sites, which costs a call and
// spills per pixel. Without PSRAM there is one call site and it changes
// nothing, and draw_pixel_at() stays a virtual call either way.
template<ColorBitness BITNESS>
static void __attribute__((flatten))
draw_packed_pixels(Display *display, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
bool big_endian, int x_offset, int y_offset, int x_pad) {
const size_t line_stride = x_offset + w + x_pad; // length of each source line in pixels
// BITNESS is a constant here, so to_color()'s three per pixel divisions
// become a multiply and shift and its bitness switch folds.
const auto draw_source_pixel = [&](int x, int y, size_t source_idx) ESPHOME_ALWAYS_INLINE {
display->draw_pixel_at(
x + x_start, y + y_start,
ColorUtil::to_color(ColorUtil::read_packed<BITNESS>(ptr, source_idx, big_endian), order, BITNESS));
};
// The source and frame buffers are row-major, so walking rows keeps the read
// and the write sequential. When the display swaps the axes before writing,
// walking columns keeps the write sequential instead; with the frame buffer
// in PSRAM a strided write costs more than a strided read.
#ifdef USE_PSRAM
if (display->pixel_axes_swapped()) {
size_t column_idx = y_offset * line_stride + x_offset;
for (int x = 0; x != w; x++, column_idx++) {
size_t source_idx = column_idx;
for (int y = 0; y != h; y++, source_idx += line_stride) {
draw_source_pixel(x, y, source_idx);
}
}
return;
}
#endif
// Row walk: builds without PSRAM, and PSRAM builds whose axes are not swapped.
for (int y = 0; y != h; y++) {
size_t source_idx = (y_offset + y) * line_stride + x_offset;
size_t source_idx_mod;
for (int x = 0; x != w; x++, source_idx++) {
switch (bitness) {
default:
color_value = ptr[source_idx];
break;
case COLOR_BITNESS_565:
source_idx_mod = source_idx * 2;
if (big_endian) {
color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
} else {
color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
}
break;
case COLOR_BITNESS_888:
source_idx_mod = source_idx * 3;
if (big_endian) {
color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
} else {
color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
}
break;
}
this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
draw_source_pixel(x, y, source_idx);
}
}
}
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
switch (bitness) {
case COLOR_BITNESS_565:
draw_packed_pixels<COLOR_BITNESS_565>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
case COLOR_BITNESS_888:
draw_packed_pixels<COLOR_BITNESS_888>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
default:
draw_packed_pixels<COLOR_BITNESS_332>(this, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset,
x_pad);
break;
}
}
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
// Future: Could be made more efficient by manipulating buffer directly in certain rotations.
for (int i = x; i < x + width; i++)
+13 -1
View File
@@ -139,6 +139,11 @@ enum DisplayRotation {
DISPLAY_ROTATION_270_DEGREES = 270,
};
/// Whether a rotation swaps the x and y axes.
constexpr bool rotation_swaps_axes(DisplayRotation rotation) {
return rotation == DISPLAY_ROTATION_90_DEGREES || rotation == DISPLAY_ROTATION_270_DEGREES;
}
const int EDGES_TRIGON = 3;
const int EDGES_TRIANGLE = 3;
const int EDGES_TETRAGON = 4;
@@ -352,7 +357,8 @@ class Display : public PollingComponent {
* \param ptr A pointer to the start of the data to be copied
* \param order The ordering of the colors
* \param bitness Defines the number of bits and their format for each pixel
* \param big_endian True if 16 bit values are stored big-endian
* \param big_endian True if 16 bit values are stored big-endian. Ignored for 888, which is always b,g,r
* in memory for RGB order, as LVGL and ESPHome images store it.
* \param x_offset The initial x-offset into the source buffer.
* \param y_offset The initial y-offset into the source buffer.
* \param x_pad How many pixels are in each line after the end of the pixels to be copied.
@@ -717,6 +723,12 @@ class Display : public PollingComponent {
virtual DisplayRotation get_rotation() const { return this->rotation_; }
/// True when draw_pixel_at() swaps x and y before writing, so pixels that are
/// neighbours along x land a row apart in the frame buffer. Not the same as
/// get_width()/get_height() swapping: a display that rotates in hardware
/// reports swapped dimensions but writes unswapped pixels.
virtual bool pixel_axes_swapped() const { return rotation_swaps_axes(this->rotation_); }
/** Get the type of display that the buffer corresponds to. In case of dynamically configurable displays,
* returns the type the display is currently configured to.
*/
@@ -1,5 +1,8 @@
#pragma once
#include <array>
#include <cstdint>
#include "esphome/core/color.h"
#include "esphome/core/helpers.h"
namespace esphome::display {
enum ColorOrder : uint8_t { COLOR_ORDER_RGB = 0, COLOR_ORDER_BGR = 1, COLOR_ORDER_GRB = 2 };
@@ -8,6 +11,26 @@ inline static uint8_t esp_scale(uint8_t i, uint8_t scale, uint8_t max_value = 25
class ColorUtil {
public:
/// Reads one packed pixel of the given bitness from a source buffer into the
/// value to_color() takes. BITNESS is a template argument so the byte layout
/// folds; pass it on to to_color() as a constant too, and its channel scaling
/// divides by a constant, which the compiler turns into a multiply and shift.
template<ColorBitness BITNESS>
static ESPHOME_ALWAYS_INLINE uint32_t read_packed(const uint8_t *ptr, size_t index, bool big_endian) {
if constexpr (BITNESS == COLOR_BITNESS_565) {
const size_t idx = index * 2;
if (big_endian)
return (ptr[idx] << 8) + ptr[idx + 1];
return ptr[idx] + (ptr[idx + 1] << 8);
} else if constexpr (BITNESS == COLOR_BITNESS_888) {
// Always the LVGL RGB888 layout, b,g,r in memory for RGB order; byte order applies to 565 only
const size_t idx = index * 3;
return ptr[idx + 0] + (ptr[idx + 1] << 8) + (ptr[idx + 2] << 16);
} else {
return ptr[index];
}
}
static Color to_color(uint32_t colorcode, ColorOrder color_order,
ColorBitness color_bitness = ColorBitness::COLOR_BITNESS_888, bool right_bit_aligned = true) {
uint8_t first_color, second_color, third_color;
@@ -154,4 +177,54 @@ class ColorUtil {
return color;
}
};
/** Rec.601 luma weights (0.299/0.587/0.114), scaled by 256 */
static constexpr uint16_t REC601_LUMA_R = 77;
static constexpr uint16_t REC601_LUMA_G = 150;
static constexpr uint16_t REC601_LUMA_B = 29;
/** Rec.601 luma of an RGB color, for optimum perceptual brightness */
constexpr uint8_t rec601_luma(Color color) {
return (uint8_t) ((REC601_LUMA_R * color.r + REC601_LUMA_G * color.g + REC601_LUMA_B * color.b + 128u) >> 8);
}
/** Map an RGB color to a single monochrome bit: 1 = white, 0 = black */
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
/** color_to_mono() of an RGB565 value as ColorUtil::to_color() reads it, from table lookups instead of a division
* per channel.
*/
class Rgb565ToMono {
public:
explicit Rgb565ToMono(ColorOrder order) {
// Rec.601 weight of the 5-bit, 6-bit and 5-bit fields, by the channel each one holds in this order
uint16_t high = REC601_LUMA_R, middle = REC601_LUMA_G, low = REC601_LUMA_B;
if (order == COLOR_ORDER_BGR) {
high = REC601_LUMA_B;
low = REC601_LUMA_R;
} else if (order == COLOR_ORDER_GRB) {
high = REC601_LUMA_G;
middle = REC601_LUMA_R;
}
// Each field value scaled to 0..255, then weighted
for (unsigned i = 0; i != 32; i++) {
this->high_[i] = high * esp_scale(i, 31);
this->low_[i] = low * esp_scale(i, 31);
}
for (unsigned i = 0; i != 64; i++)
this->middle_[i] = middle * esp_scale(i, 63);
}
uint8_t operator()(uint16_t value) const {
const uint32_t weighted = this->high_[value >> 11] + this->middle_[(value >> 5) & 0x3F] + this->low_[value & 0x1F];
return ((weighted + 128u) >> 8) >= 128 ? 1 : 0;
}
protected:
// Weighted contribution of each value of each field
std::array<uint16_t, 32> high_{};
std::array<uint16_t, 64> middle_{};
std::array<uint16_t, 32> low_{};
};
} // namespace esphome::display
@@ -118,13 +118,8 @@ void Ecocomfort2Fan::on_status() {
int new_speed = this->speed;
uint8_t base_speed = raw_speed & SPEED_MASK;
if (raw_speed & FLAG_BOOST) {
new_speed = SPEED_COUNT;
} else if (raw_speed & FLAG_NIGHT) {
new_speed = SPEED_SLEEP;
} else if (base_speed >= SPEED_SLEEP && base_speed <= SPEED_VEL3) {
if (base_speed >= SPEED_SLEEP && base_speed <= SPEED_VEL3)
new_speed = base_speed;
}
const Preset *preset = find_preset(mode, (raw_speed & FLAG_AUTO) != 0);
const char *new_preset = preset != nullptr ? preset->name : nullptr;
bool same_preset = new_preset == nullptr ? !this->has_preset_mode() : this->get_preset_mode().c_str() == new_preset;
@@ -205,6 +205,10 @@ void Ecocomfort2Hub::publish_unknown_() {
sensor->publish_state(NAN);
}
#endif
#ifdef USE_BINARY_SENSOR
if (this->boost_binary_sensor_ != nullptr)
this->boost_binary_sensor_->invalidate_state();
#endif
}
// C_OPER: mode byte, speed/flags byte
@@ -216,10 +220,6 @@ void Ecocomfort2Hub::on_oper_(const uint8_t *data, uint16_t len) {
this->mode_ = data[0];
this->speed_ = data[1];
ESP_LOGD(TAG, "[%s] C_OPER: mode=0x%02x, speed=0x%02x", this->parent_->address_str(), this->mode_, this->speed_);
#ifdef USE_BINARY_SENSOR
if (this->boost_binary_sensor_ != nullptr)
this->boost_binary_sensor_->publish_state((this->speed_ & FLAG_BOOST) != 0);
#endif
#ifdef ECOCOMFORT2_CHILD_COUNT
for (auto *child : this->children_) {
child->on_status();
@@ -227,7 +227,9 @@ void Ecocomfort2Hub::on_oper_(const uint8_t *data, uint16_t len) {
#endif
}
// C_STATE: bytes 3-4 temperature and 5-6 humidity in hundredths, 7-8 VOC in ppb, byte 2 the air flow direction
// C_STATE: bytes 0-1 the mode and speed the unit runs; byte 1 also has the boost flag (0x40) and the advanced
// control step (0x20). Byte 2 the air flow direction, bytes 3-4 temperature and 5-6 humidity in hundredths,
// 7-8 VOC in ppb
void Ecocomfort2Hub::on_state_(const uint8_t *data, uint16_t len) {
if (len < 9) {
ESP_LOGW(TAG, "[%s] C_STATE too short: %u bytes", this->parent_->address_str(), len);
@@ -236,8 +238,12 @@ void Ecocomfort2Hub::on_state_(const uint8_t *data, uint16_t len) {
const float temperature = static_cast<int16_t>(encode_uint16(data[3], data[4])) / 100.0f;
const float humidity = encode_uint16(data[5], data[6]) / 100.0f;
const uint16_t voc = encode_uint16(data[7], data[8]);
ESP_LOGD(TAG, "[%s] C_STATE: %.2f °C, %.2f %%, %u ppb, direction=%u", this->parent_->address_str(), temperature,
humidity, voc, data[2]);
ESP_LOGD(TAG, "[%s] C_STATE: mode=0x%02x, speed=0x%02x, %.2f °C, %.2f %%, %u ppb, direction=%u",
this->parent_->address_str(), data[0], data[1], temperature, humidity, voc, data[2]);
#ifdef USE_BINARY_SENSOR
if (this->boost_binary_sensor_ != nullptr)
this->boost_binary_sensor_->publish_state((data[1] & FLAG_BOOST) != 0);
#endif
#ifdef USE_SENSOR
if (this->temperature_sensor_ != nullptr)
this->temperature_sensor_->publish_state(temperature);
@@ -267,7 +273,7 @@ void Ecocomfort2Hub::on_info_(const uint8_t *data, uint16_t len) {
}
#ifdef USE_TIME
// C_CLOCK: hour, minute, second, weekday with Monday as 0, day, month, year as two bytes
// C_CLOCK: two-digit year, month, day, weekday with Monday as 0, hour, minute, second, automatic DST flag
void Ecocomfort2Hub::sync_clock_() {
if (!this->is_ready() || this->char_handle_clock_ == 0)
return;
@@ -276,15 +282,11 @@ void Ecocomfort2Hub::sync_clock_() {
return;
// ESPTime counts Sunday=1 .. Saturday=7, the unit Monday=0 .. Sunday=6
const uint8_t weekday = (now.day_of_week + 5) % 7;
uint8_t data[8]{now.hour,
now.minute,
now.second,
weekday,
now.day_of_month,
now.month,
static_cast<uint8_t>(now.year >> 8),
static_cast<uint8_t>(now.year)};
ESP_LOGD(TAG, "[%s] Syncing clock to %02u:%02u:%02u", this->parent_->address_str(), now.hour, now.minute, now.second);
// The time source already applies DST and every sync rewrites the clock, so the unit must not shift it again
uint8_t data[8]{
static_cast<uint8_t>(now.year % 100), now.month, now.day_of_month, weekday, now.hour, now.minute, now.second, 0};
ESP_LOGD(TAG, "[%s] Syncing clock to %04u-%02u-%02u %02u:%02u:%02u", this->parent_->address_str(), now.year,
now.month, now.day_of_month, now.hour, now.minute, now.second);
auto status =
esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->char_handle_clock_,
sizeof(data), data, ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
@@ -32,7 +32,8 @@ static constexpr uint8_t OPER_OUT = 0x02;
static constexpr uint8_t OPER_IN_OUT = 0x03;
static constexpr uint8_t OPER_SENSOR_OR_AUTO = 0x04;
// C_OPER byte 1: speed in the low nibble, flags in the high nibble
// Speed byte: speed in the low nibble, flags in the high nibble.
// C_OPER carries FLAG_AUTO; the unit reports FLAG_BOOST in byte 1 of C_STATE.
static constexpr uint8_t SPEED_OFF = 0x00;
static constexpr uint8_t SPEED_SLEEP = 0x01;
static constexpr uint8_t SPEED_VEL1 = 0x02;
@@ -41,7 +42,6 @@ static constexpr uint8_t SPEED_VEL3 = 0x04;
static constexpr uint8_t SPEED_MASK = 0x0F;
static constexpr uint8_t FLAG_AUTO = 0x10;
static constexpr uint8_t FLAG_BOOST = 0x40;
static constexpr uint8_t FLAG_NIGHT = 0x80;
/** Hub component for Ecocomfort 2.0 VMC via BLE. */
class Ecocomfort2Hub : public esphome::ble_client::BLEClientNode, public PollingComponent {
+15 -3
View File
@@ -23,11 +23,18 @@ DEPENDENCIES = ["uart"]
emontx_ns = cg.esphome_ns.namespace("emontx")
EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice)
LockOnReboot = emontx_ns.enum("LockOnReboot", is_class=True)
CONF_EMONTX_ID = "emontx_id"
CONF_TAG_NAME = "tag_name"
CONF_ON_JSON = "on_json"
CONF_LOCK_ON_REBOOT = "lock_on_reboot"
LOCK_ON_REBOOT_AUTO = "auto"
LOCK_ON_REBOOT_MODES = {
LOCK_ON_REBOOT_AUTO: LockOnReboot.LOCK_ON_REBOOT_AUTO,
True: LockOnReboot.LOCK_ON_REBOOT_ALWAYS,
False: LockOnReboot.LOCK_ON_REBOOT_NEVER,
}
DOMAIN = "emontx"
@@ -59,7 +66,9 @@ CONFIG_SCHEMA = (
cv.GenerateID(): cv.declare_id(EmonTx),
cv.Optional(CONF_ON_JSON): _synchronous_automation_schema(),
cv.Optional(CONF_ON_DATA): _synchronous_automation_schema(),
cv.Optional(CONF_LOCK_ON_REBOOT, default=False): cv.boolean,
cv.Optional(CONF_LOCK_ON_REBOOT, default=LOCK_ON_REBOOT_AUTO): cv.Any(
cv.boolean, cv.one_of(LOCK_ON_REBOOT_AUTO, lower=True)
),
}
)
.extend(cv.COMPONENT_SCHEMA)
@@ -135,8 +144,11 @@ async def to_code(config: ConfigType) -> None:
await uart.register_uart_device(var, config)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if config[CONF_LOCK_ON_REBOOT]:
cg.add(var.set_lock_on_reboot(True))
lock = config[CONF_LOCK_ON_REBOOT]
cg.add(var.set_lock_on_reboot(LOCK_ON_REBOOT_MODES[lock]))
if lock == LOCK_ON_REBOOT_AUTO:
# Locking depends on the firmware version the emonTx reports
cg.add_define("USE_EMONTX_VERSION")
# Action: emontx.send_command
+129 -27
View File
@@ -7,30 +7,85 @@ namespace esphome::emontx {
ESPHOME_LOG_TAG(TAG, "emontx");
static constexpr uint32_t UNLOCK_ID = 0;
// The emonTx joins bytes received while a command is pending into that command, so the next one waits
static constexpr uint32_t COMMAND_GAP_MS = 250;
#ifdef USE_EMONTX_VERSION
static constexpr uint32_t VERSION_REQUEST_ID = 0;
// The emonTx joins bytes received while a command is pending into that command, so 'v' waits
static constexpr uint32_t VERSION_REQUEST_DELAY_MS = 250;
static constexpr uint32_t VERSION_REQUEST_ID = 1;
// Without a reply, 'v' is sent again every VERSION_RETRY_FRAMES data lines, VERSION_MAX_ATTEMPTS times in all
static constexpr uint8_t VERSION_RETRY_FRAMES = 2;
static constexpr uint8_t VERSION_MAX_ATTEMPTS = 3;
// Line of the 'v' reply (and of the boot banner) carrying the release, e.g. " - Version: v1.1.1"
static const char VERSION_PREFIX[] PROGMEM = " - Version:";
// Every data line starts with the message counter, in JSON or text format
static const char MSG_JSON_PREFIX[] PROGMEM = "{\"MSG\":";
static const char MSG_TEXT_PREFIX[] PROGMEM = "MSG:";
// First firmware with 'emonlock' and 'emonunlock'; older firmware takes them for the bootloader command
static constexpr uint32_t LOCK_MIN_MAJOR = 1;
static constexpr uint32_t LOCK_MIN_MINOR = 2;
// True if line starts with the PROGMEM prefix and has more after it
static bool starts_with(const char *line, size_t len, const char *prefix, size_t prefix_len) {
return len > prefix_len && ESPHOME_strncmp_P(line, prefix, prefix_len) == 0;
}
// Reads the decimal number (up to 9 digits) at pos and moves pos past it; false if there is none
static bool read_number(const char *&pos, const char *end, uint32_t &value) {
const char *start = pos;
value = 0;
while (pos < end && pos - start < 9 && *pos >= '0' && *pos <= '9')
value = value * 10 + (*pos++ - '0');
return pos != start;
}
// True for a release such as "v1.2.0" or "v1.3.0-rc1" from LOCK_MIN_MAJOR.LOCK_MIN_MINOR on
static bool supports_lock(const char *version, const char *end) {
if (version < end && (*version == 'v' || *version == 'V'))
version++;
uint32_t major;
if (!read_number(version, end, major) || version == end || *version != '.')
return false;
version++;
uint32_t minor;
if (!read_number(version, end, minor))
return false;
return major > LOCK_MIN_MAJOR || (major == LOCK_MIN_MAJOR && minor >= LOCK_MIN_MINOR);
}
#endif
void EmonTx::setup() {
this->buffer_pos_ = 0;
if (this->lock_on_reboot_) {
// The line feed ends any boot noise first
ESP_LOGI(TAG, "Unlocking emonTx after boot");
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
// The line feed ends any boot noise
this->write_byte('\n');
this->send_command("emonunlock");
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
this->unlock_();
#ifdef USE_EMONTX_VERSION
if (this->wants_version_())
this->request_version_();
#endif
});
return;
}
#ifdef USE_EMONTX_VERSION
if (this->version_text_sensor_ != nullptr)
// With AUTO, the reply decides whether to unlock
if (this->wants_version_())
this->request_version_();
#endif
}
void EmonTx::unlock_() {
ESP_LOGI(TAG, "Unlocking emonTx");
this->send_command("emonunlock");
}
void EmonTx::on_shutdown() {
if (!this->lock_on_reboot_)
bool lock = this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS;
#ifdef USE_EMONTX_VERSION
lock |= this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && this->lock_supported_;
#endif
if (!lock)
return;
if (this->is_paused()) {
ESP_LOGW(TAG, "Not locking emonTx before shutdown, UART is paused");
@@ -63,7 +118,7 @@ void EmonTx::loop() {
this->discarding_line_ = dropped != '\n';
#ifdef USE_EMONTX_VERSION
// The emonTx may have been flashed, or a request dropped, during the pause
if (this->version_text_sensor_ != nullptr)
if (this->wants_version_())
this->request_version_();
#endif
}
@@ -96,8 +151,10 @@ void EmonTx::loop() {
this->parse_json_(this->buffer_.data(), len);
}
#ifdef USE_EMONTX_VERSION
if (this->version_text_sensor_ != nullptr)
if (this->wants_version_()) {
this->parse_msg_number_(this->buffer_.data(), len);
this->parse_version_line_(this->buffer_.data(), len);
}
#endif
}
} else if (received == '\r' || this->discarding_line_) {
@@ -124,16 +181,6 @@ void EmonTx::parse_json_(const char *data, size_t len) {
}
}
#endif
#ifdef USE_EMONTX_VERSION
auto msg = root["MSG"];
if (this->version_text_sensor_ != nullptr && msg.is<uint32_t>()) {
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
uint32_t msg_number = msg;
if (msg_number <= this->last_msg_number_)
this->request_version_();
this->last_msg_number_ = msg_number;
}
#endif
this->json_callbacks_.call(root, StringRef(data, len));
return true;
@@ -145,17 +192,50 @@ void EmonTx::parse_json_(const char *data, size_t len) {
}
#ifdef USE_EMONTX_VERSION
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
void EmonTx::request_version_() {
// The firmware may have changed; until the reply, an unknown version gets no lock commands
this->lock_supported_ = false;
this->version_frames_left_ = VERSION_RETRY_FRAMES * VERSION_MAX_ATTEMPTS;
this->send_version_request_();
}
// The line feed ends any pending input; the reply is picked up in loop() by parse_version_line_()
void EmonTx::send_version_request_() {
if (this->is_paused())
return;
this->write_byte('\n');
this->set_timeout(VERSION_REQUEST_ID, VERSION_REQUEST_DELAY_MS, [this]() { this->send_command("v"); });
this->set_timeout(VERSION_REQUEST_ID, COMMAND_GAP_MS, [this]() { this->send_command("v"); });
}
void EmonTx::parse_msg_number_(const char *line, size_t len) {
const char *pos;
if (starts_with(line, len, MSG_JSON_PREFIX, sizeof(MSG_JSON_PREFIX) - 1)) {
pos = line + sizeof(MSG_JSON_PREFIX) - 1;
} else if (starts_with(line, len, MSG_TEXT_PREFIX, sizeof(MSG_TEXT_PREFIX) - 1)) {
pos = line + sizeof(MSG_TEXT_PREFIX) - 1;
} else {
return;
}
uint32_t msg_number;
if (!read_number(pos, line + len, msg_number))
return;
// MSG restarts at 1 when the emonTx reboots, as it does after a firmware update
if (msg_number <= this->last_msg_number_) {
this->request_version_();
} else if (this->version_frames_left_ > 0) {
if (--this->version_frames_left_ == 0) {
ESP_LOGW(TAG, "No firmware version from the emonTx, it may still be locked");
} else if (this->version_frames_left_ % VERSION_RETRY_FRAMES == 0) {
this->send_version_request_();
}
}
this->last_msg_number_ = msg_number;
}
void EmonTx::parse_version_line_(const char *line, size_t len) {
constexpr size_t prefix_len = sizeof(VERSION_PREFIX) - 1;
if (len <= prefix_len || ESPHOME_strncmp_P(line, VERSION_PREFIX, prefix_len) != 0)
if (!starts_with(line, len, VERSION_PREFIX, prefix_len))
return;
const char *version = line + prefix_len;
const char *end = line + len;
@@ -164,7 +244,23 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
if (version == end)
return;
ESP_LOGI(TAG, "emonTx firmware version: %.*s", static_cast<int>(end - version), version);
this->version_text_sensor_->publish_state(version, end - version);
this->version_frames_left_ = 0;
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr)
this->version_text_sensor_->publish_state(version, end - version);
#endif
bool supported = supports_lock(version, end);
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO && supported && !this->unlock_sent_) {
// Once the rest of the reply has gone out; a pause drops it, and the version request on resume retries
this->set_timeout(UNLOCK_ID, COMMAND_GAP_MS, [this]() {
if (this->is_paused())
return;
this->unlock_();
this->unlock_sent_ = true;
});
}
this->lock_supported_ = supported;
}
#endif
@@ -173,7 +269,13 @@ void EmonTx::parse_version_line_(const char *line, size_t len) {
*/
void EmonTx::dump_config() {
ESP_LOGCONFIG(TAG, "EmonTx:");
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", YESNO(this->lock_on_reboot_));
const char *lock = LOG_STR_LITERAL("no");
if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_ALWAYS) {
lock = LOG_STR_LITERAL("yes");
} else if (this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO) {
lock = LOG_STR_LITERAL("auto");
}
ESP_LOGCONFIG(TAG, " Lock on reboot: %s", lock);
#ifdef USE_SENSOR
ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size());
@@ -183,7 +285,7 @@ void EmonTx::dump_config() {
#else
ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
#endif
#ifdef USE_EMONTX_VERSION
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
LOG_TEXT_SENSOR(" ", "Firmware version", this->version_text_sensor_);
#endif
}
+24 -5
View File
@@ -13,7 +13,7 @@
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#ifdef USE_EMONTX_VERSION
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#endif
@@ -22,6 +22,12 @@ namespace esphome::emontx {
/// Maximum line length in bytes (plus one byte reserved for null terminator)
static constexpr size_t MAX_LINE_LENGTH = 1024;
enum class LockOnReboot : uint8_t {
LOCK_ON_REBOOT_NEVER,
LOCK_ON_REBOOT_ALWAYS,
LOCK_ON_REBOOT_AUTO, // Only when the emonTx reports firmware v1.2.0 or later
};
/**
* @class EmonTx
* @brief Main class for the EmonTx component.
@@ -30,7 +36,7 @@ static constexpr size_t MAX_LINE_LENGTH = 1024;
* extracts tags and values, and publishes them to registered sensors.
*/
class EmonTx final : public Component, public uart::UARTDevice {
#ifdef USE_EMONTX_VERSION
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
SUB_TEXT_SENSOR(version)
#endif
public:
@@ -56,8 +62,8 @@ class EmonTx final : public Component, public uart::UARTDevice {
bool is_paused() const { return this->paused_.load(std::memory_order_relaxed); }
/// Lock the emonTx ('emonlock') before a reboot, so serial garbage during the reboot is not taken
/// as a command, and unlock it in setup(). Needs emonTx firmware v1.2.0 or later.
void set_lock_on_reboot(bool lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
/// as a command, and unlock it after boot. Needs emonTx firmware v1.2.0 or later, which AUTO checks.
void set_lock_on_reboot(LockOnReboot lock_on_reboot) { this->lock_on_reboot_ = lock_on_reboot; }
#ifdef USE_SENSOR
void init_sensors(size_t count) { this->sensors_.init(count); }
@@ -66,8 +72,18 @@ class EmonTx final : public Component, public uart::UARTDevice {
protected:
void parse_json_(const char *data, size_t len);
void unlock_();
#ifdef USE_EMONTX_VERSION
bool wants_version_() const {
#ifdef USE_EMONTX_VERSION_TEXT_SENSOR
if (this->version_text_sensor_ != nullptr)
return true;
#endif
return this->lock_on_reboot_ == LockOnReboot::LOCK_ON_REBOOT_AUTO;
}
void request_version_();
void send_version_request_();
void parse_msg_number_(const char *line, size_t len);
void parse_version_line_(const char *line, size_t len);
#endif
@@ -79,9 +95,12 @@ class EmonTx final : public Component, public uart::UARTDevice {
uint16_t buffer_pos_{0};
std::atomic<bool> paused_{false};
bool was_paused_{false};
bool lock_on_reboot_{false};
LockOnReboot lock_on_reboot_{LockOnReboot::LOCK_ON_REBOOT_NEVER};
bool discarding_line_{false}; // Set after an overflow or a pause until the end of that line
#ifdef USE_EMONTX_VERSION
bool lock_supported_{false}; // The last version reported is v1.2.0 or later
bool unlock_sent_{false}; // AUTO has sent 'emonunlock' since boot
uint8_t version_frames_left_{0}; // Data lines still to wait for a version reply; 0 when none is owed
uint32_t last_msg_number_{0};
#endif
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
@@ -21,3 +21,4 @@ async def to_code(config: ConfigType) -> None:
text_sensors = text_sensor.sub_text_sensors(config)
if await text_sensors(CONF_VERSION, hub.set_version_text_sensor) is not None:
cg.add_define("USE_EMONTX_VERSION")
cg.add_define("USE_EMONTX_VERSION_TEXT_SENSOR")
+7 -12
View File
@@ -2,6 +2,7 @@
#include <cstdint>
#include <algorithm>
#include "esphome/components/display/display_color_utils.h"
#include "esphome/core/color.h"
/* Utility for converting internal \a Color RGB representation to supported IC hardware color keys
@@ -13,21 +14,15 @@
namespace esphome::epaper_spi {
// The Rec.601 luma helpers and the RGB565 mono lookup live in the shared display utilities, so other
// drivers (e.g. it8951 in its 1 bit per pixel mode) can use them too.
using display::color_to_mono;
using display::rec601_luma;
using display::Rgb565ToMono;
/** Delta for when to regard as gray */
static constexpr uint8_t COLORCONV_GRAY_THRESHOLD = 50;
/** Rec.601 luma (0.299/0.587/0.114 weights, scaled by 256) for optimum perceptual brightness */
constexpr uint8_t rec601_luma(Color color) {
return (uint8_t) ((77u * color.r + 150u * color.g + 29u * color.b + 128u) >> 8);
}
/** Map RGB color to a single monochrome bit
*
* @param color RGB color to convert from
* @return 1 = white, 0 = black
*/
constexpr uint8_t color_to_mono(Color color) { return rec601_luma(color) >= 128 ? 1 : 0; }
/** Map RGB color to one of 4 discrete gray levels (2 bits per pixel)
*
* @param color RGB color to convert from
+2 -6
View File
@@ -40,7 +40,6 @@ from esphome.const import (
CONF_WIDTH,
)
from esphome.cpp_generator import RawExpression
from esphome.final_validate import full_config
from . import models
@@ -198,16 +197,13 @@ def _final_validate(config) -> None:
"epaper_spi", require_miso=False, require_mosi=True
)(config)
global_config = full_config.get()
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
if CONF_LAMBDA not in config and CONF_PAGES not in config:
if LVGL_DOMAIN in global_config:
if display.has_external_writer(config[CONF_ID]):
if CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("never")
else:
# If no drawing methods are configured, and LVGL is not enabled, show a test card
config[CONF_SHOW_TEST_CARD] = True
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
elif CONF_UPDATE_INTERVAL not in config:
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
+133 -2
View File
@@ -131,9 +131,7 @@ void EPaperBase::update() {
}
void EPaperBase::wait_for_idle_(bool should_wait) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
this->waiting_for_idle_start_ = millis();
#endif
this->waiting_for_idle_ = should_wait;
}
@@ -156,6 +154,10 @@ void EPaperBase::loop() {
ESP_LOGV(TAG, "Screen was busy for %u ms", (unsigned) (millis() - this->waiting_for_idle_start_));
#endif
} else {
if (now - this->waiting_for_idle_start_ > this->busy_timeout_seconds_ * 1000u) {
this->abandon_update_();
return;
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
if (now - this->waiting_for_idle_last_print_ >= 1000) {
ESP_LOGV(TAG, "Waiting for idle in state %s", this->epaper_state_to_string_());
@@ -168,6 +170,20 @@ void EPaperBase::loop() {
this->process_state_();
}
// The busy pin never cleared: give up on this update so later ones are not refused, and make the next one full
void EPaperBase::abandon_update_() {
ESP_LOGE(TAG, "Display stayed busy for %u s in state %s; check the busy pin", (unsigned) this->busy_timeout_seconds_,
this->epaper_state_to_string_());
this->status_set_warning(LOG_STR("Busy pin timeout"));
if (this->state_ == EPaperState::TRANSFER_DATA)
this->abort_transfer();
// Leave the panel powered down rather than with its high-voltage drivers enabled
this->power_off();
this->full_update_requested_ = true;
this->current_data_index_ = 0;
this->set_state_(EPaperState::IDLE);
}
/**
* Process the state machine.
* Typical state sequence:
@@ -242,6 +258,7 @@ void EPaperBase::process_state_() {
break;
case EPaperState::DEEP_SLEEP:
this->deep_sleep();
this->status_clear_warning();
this->set_state_(EPaperState::IDLE);
ESP_LOGD(TAG, "Display update took %" PRIu32 " ms", millis() - this->update_start_time_);
break;
@@ -345,6 +362,120 @@ void HOT EPaperBase::draw_pixel_at(int x, int y, Color color) {
}
}
namespace {
// One source row of an RGB565 block, written into a contiguous 1 bit per pixel buffer. Without SwapAxes the row lies
// along a buffer row and the bit moves by step; with it the row lies along a bit column and the byte moves by step
// rows.
template<bool SwapAxes, bool BigEndianSource>
void HOT write_rgb565_row(uint8_t *byte, uint8_t mask, int step, size_t row_width, const uint8_t *source, int count,
const Rgb565ToMono &to_mono) {
const ptrdiff_t byte_step = SwapAxes ? step * static_cast<ptrdiff_t>(row_width) : 0;
for (; count != 0; count--, source += 2) {
const uint16_t value = BigEndianSource ? (source[0] << 8) | source[1] : source[0] | (source[1] << 8);
if (to_mono(value)) {
*byte |= mask;
} else {
*byte &= ~mask;
}
if constexpr (SwapAxes) {
byte += byte_step;
} else if (step > 0) {
mask >>= 1;
if (mask == 0) {
mask = 0x80;
byte++;
}
} else {
mask <<= 1;
if (mask == 0) {
mask = 0x01;
byte--;
}
}
}
}
} // namespace
// Each source column maps to one buffer coordinate and each source row to the other, each moving by 1 per pixel, so
// the visible part of a block and its dirty area are worked out once for the whole block.
void HOT EPaperBase::draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr,
ColorOrder order, bool big_endian, int x_offset, int y_offset, int x_pad) {
const bool swap = this->effective_transform_ & SWAP_XY;
const bool mirror_x = this->effective_transform_ & MIRROR_X;
const bool mirror_y = this->effective_transform_ & MIRROR_Y;
// The buffer axis a source column moves along, and the one a source row moves along
const int column_limit = swap ? this->height_ : this->width_;
const int row_limit = swap ? this->width_ : this->height_;
const bool column_mirrored = swap ? mirror_y : mirror_x;
const bool row_mirrored = swap ? mirror_x : mirror_y;
const int first_column = std::max(0, -x_start);
const int end_column = std::min(w, column_limit - x_start);
const int first_row = std::max(0, -y_start);
const int end_row = std::min(h, row_limit - y_start);
if (first_column >= end_column || first_row >= end_row)
return;
auto column_to_buffer = [&](int column) {
const int u = x_start + column;
return column_mirrored ? column_limit - 1 - u : u;
};
auto row_to_buffer = [&](int row) {
const int v = y_start + row;
return row_mirrored ? row_limit - 1 - v : v;
};
const int column_low = std::min(column_to_buffer(first_column), column_to_buffer(end_column - 1));
const int row_low = std::min(row_to_buffer(first_row), row_to_buffer(end_row - 1));
const int columns = end_column - first_column;
const int rows = end_row - first_row;
const int x_low = swap ? row_low : column_low;
const int y_low = swap ? column_low : row_low;
const int x_high = x_low + (swap ? rows : columns);
const int y_high = y_low + (swap ? columns : rows);
const Rgb565ToMono to_mono(order);
const size_t line_stride = x_offset + w + x_pad;
const size_t row_width = this->row_width_;
const int step = column_mirrored ? -1 : 1;
for (int row = first_row; row != end_row; row++) {
const uint8_t *source = ptr + ((y_offset + row) * line_stride + x_offset + first_column) * 2;
const int along = column_to_buffer(first_column);
const int across = row_to_buffer(row);
const int x = swap ? across : along;
const int y = swap ? along : across;
uint8_t *byte = buffer + y * row_width + x / 8;
const uint8_t mask = 0x80u >> (x % 8);
if (swap) {
if (big_endian) {
write_rgb565_row<true, true>(byte, mask, step, row_width, source, columns, to_mono);
} else {
write_rgb565_row<true, false>(byte, mask, step, row_width, source, columns, to_mono);
}
} else if (big_endian) {
write_rgb565_row<false, true>(byte, mask, step, row_width, source, columns, to_mono);
} else {
write_rgb565_row<false, false>(byte, mask, step, row_width, source, columns, to_mono);
}
}
this->x_low_ = clamp_at_most(this->x_low_, x_low);
this->x_high_ = clamp_at_least(this->x_high_, x_high);
this->y_low_ = clamp_at_most(this->y_low_, y_low);
this->y_high_ = clamp_at_least(this->y_high_, y_high);
}
// Writes an RGB565 block of pixels, as LVGL sends them, straight into a 1 bit per pixel buffer that is one block (the
// usual case): the same result as draw_pixel_at() for each pixel. Anything else goes through draw_pixel_at().
void HOT EPaperBase::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
size_t contiguous_length;
uint8_t *contiguous = this->buffer_.get_span(0, contiguous_length);
if (!this->buffer_is_mono_() || this->get_clipping().is_set() || bitness != COLOR_BITNESS_565 ||
contiguous_length != this->buffer_length_) {
Display::draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, x_offset, y_offset, x_pad);
return;
}
this->draw_rgb565_block_(contiguous, x_start, y_start, w, h, ptr, order, big_endian, x_offset, y_offset, x_pad);
}
void EPaperBase::dump_config() {
LOG_DISPLAY("", "E-Paper SPI", this);
ESP_LOGCONFIG(TAG,
+25 -8
View File
@@ -38,15 +38,18 @@ class EPaperBase : public Display,
spi::DATA_RATE_2MHZ> {
public:
EPaperBase(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence = nullptr,
size_t init_sequence_length = 0, DisplayType display_type = DISPLAY_TYPE_BINARY)
size_t init_sequence_length = 0, DisplayType display_type = DISPLAY_TYPE_BINARY,
uint8_t bits_per_pixel = 1, uint8_t planes = 1)
: name_(name),
width_(width),
row_width_((width * bits_per_pixel + 7) / 8),
height_(height),
bits_per_pixel_(bits_per_pixel),
planes_(planes),
init_sequence_(init_sequence),
init_sequence_length_(init_sequence_length),
display_type_(display_type) {
this->row_width_ = (this->width_ + 7) / 8; // width of a row in bytes
}
display_type_(display_type),
buffer_length_((size_t) this->row_width_ * height * planes) {}
void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
float get_setup_priority() const override;
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
@@ -106,7 +109,10 @@ class EPaperBase : public Display,
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
bool pixel_axes_swapped() const override { return this->effective_transform_ & SWAP_XY; }
void draw_pixel_at(int x, int y, Color color) override;
void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) override;
// Make the next update a full one. Applied when that update starts, so one in progress is not affected.
void request_full_update() { this->full_update_requested_ = true; }
// True from the start of an update until the display has refreshed and gone back to sleep.
@@ -125,9 +131,14 @@ class EPaperBase : public Display,
virtual bool initialise(bool partial);
void send_init_sequence_(const uint8_t *sequence, size_t length);
void wait_for_idle_(bool should_wait);
void abandon_update_();
bool init_buffer_(size_t buffer_length);
void update_effective_transform_();
bool rotate_coordinates_(int &x, int &y);
void draw_rgb565_block_(uint8_t *buffer, int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
bool big_endian, int x_offset, int y_offset, int x_pad);
// Whether the buffer is a single plane of 1 bit per pixel, as draw_pixel_at() writes it
bool buffer_is_mono_() const { return this->bits_per_pixel_ == 1 && this->planes_ == 1; }
/**
* Methods that must be implemented by concrete classes to control the display
@@ -137,6 +148,8 @@ class EPaperBase : public Display,
* @return true if done, false if it should be called next loop
*/
virtual bool transfer_data() = 0;
// Called when an update is abandoned in the middle of transfer_data(), to release anything it left open
virtual void abort_transfer() {}
/**
* Refresh the screen after data transfer
*/
@@ -163,13 +176,15 @@ class EPaperBase : public Display,
// properties initialised in the constructor
const char *name_;
uint16_t width_;
uint16_t row_width_; // width of a row in bytes
uint16_t row_width_; // width of a row of one plane in bytes
uint16_t height_;
uint8_t bits_per_pixel_;
uint8_t planes_; // the buffer holds this many planes of row_width_ * height_ bytes, one after another
const uint8_t *init_sequence_;
size_t init_sequence_length_;
DisplayType display_type_;
size_t buffer_length_{};
size_t buffer_length_;
size_t current_data_index_{}; // used by data transfer to track progress
split_buffer::SplitBuffer buffer_{};
GPIOPin *dc_pin_{};
@@ -187,18 +202,20 @@ class EPaperBase : public Display,
// point to the pixel past the last one updated, i.e. may range up to width/height.
uint16_t x_low_{}, y_low_{}, x_high_{}, y_high_{};
uint32_t waiting_for_idle_start_{};
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
uint32_t waiting_for_idle_last_print_{};
uint32_t waiting_for_idle_start_{};
#endif
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
uint32_t update_start_time_{};
#endif
// properties with specific initialisers go last
EPaperState state_{EPaperState::IDLE};
uint32_t reset_duration_{10};
// Longer than any panel takes to refresh; past it the busy pin is assumed stuck
uint16_t busy_timeout_seconds_{120};
uint8_t full_update_every_{1};
EPaperState state_{EPaperState::IDLE};
};
} // namespace esphome::epaper_spi
@@ -15,9 +15,7 @@ class EPaper4bpp : public EPaperBase {
public:
EPaper4bpp(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
this->buffer_length_ = width * height / 2; // 2 pixels per byte
}
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR, 4) {}
void fill(Color color) override;
void clear() override;
@@ -9,10 +9,7 @@ class EPaperInkplate2 final : public EPaperBase {
public:
EPaperInkplate2(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
// Dual-plane buffer: black/white plane followed by red plane, 1 bit per pixel each.
this->buffer_length_ = this->row_width_ * this->height_ * 2;
}
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR, 1, 2) {}
void fill(Color color) override;
void clear() override;
@@ -27,12 +27,9 @@ class EPaperJD79660 final : public EPaperBase {
public:
EPaperJD79660(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length, const uint8_t *fast_update, uint16_t fast_update_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR),
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR, 2),
fast_update_(fast_update),
fast_update_length_(fast_update_length) {
this->row_width_ = (width + 3) / 4; // Fix base class calc (2bpp instead of 1bpp)
this->buffer_length_ = this->row_width_ * height;
}
fast_update_length_(fast_update_length) {}
void fill(Color color) override;
@@ -9,10 +9,8 @@ namespace esphome::epaper_spi {
class EPaperMono : public EPaperBase {
public:
EPaperMono(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, display_type) {
this->buffer_length_ = (width + 7) / 8 * height; // 8 pixels per byte, rounded up
}
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY, uint8_t bits_per_pixel = 1)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, display_type, bits_per_pixel) {}
protected:
void refresh_screen(bool partial) override;
@@ -58,7 +58,9 @@ bool HOT EPaperSSD1677::transfer_data() {
// A full update ignores 0x26, and the copy may not hold a real frame yet (first update after
// boot): send the new frame to both planes.
const bool send_copy = this->plane_ == 0 && this->update_count_ != 0;
const bool direct = send_copy || this->buffer_is_plane();
// A 1 bit per pixel buffer already holds the frame as a RAM plane does, so it can be sent as it is; a
// deeper buffer (the four-level model, 2 bits per pixel) packs the plane in plane_row() instead.
const bool direct = send_copy || this->buffer_is_mono_();
const auto &source = send_copy ? this->sent_ : this->buffer_;
const size_t row_length = this->plane_row_length_();
const size_t plane_length = row_length * this->height_;
@@ -23,8 +23,8 @@ namespace esphome::epaper_spi {
class EPaperSSD1677 : public EPaperMono {
public:
EPaperSSD1677(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY)
: EPaperMono(name, width, height, init_sequence, init_sequence_length, display_type) {}
size_t init_sequence_length, DisplayType display_type = DISPLAY_TYPE_BINARY, uint8_t bits_per_pixel = 1)
: EPaperMono(name, width, height, init_sequence, init_sequence_length, display_type, bits_per_pixel) {}
void setup() override;
@@ -34,9 +34,6 @@ class EPaperSSD1677 : public EPaperMono {
void init_comparison_frame_();
// Bytes in one row of a RAM plane: 8 pixels per byte, whatever the buffer holds.
size_t plane_row_length_() const { return (this->width_ + 7) / 8; }
// Whether the buffer already holds the frame as a RAM plane does, so it can be sent as it is.
// A subclass with a deeper buffer returns false and overrides plane_row().
virtual bool buffer_is_plane() const { return true; }
// Row y of the frame as a RAM plane holds it, 1 bit per pixel with 1 = white.
virtual void plane_row(size_t y, uint8_t *out);
bool reset() override;
@@ -29,10 +29,7 @@ class EPaperSSD1677Gray4 : public EPaperSSD1677 {
public:
EPaperSSD1677Gray4(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperSSD1677(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_GRAYSCALE) {
this->row_width_ = (width + 3) / 4; // 4 pixels per byte
this->buffer_length_ = (size_t) this->row_width_ * height;
}
: EPaperSSD1677(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_GRAYSCALE, 2) {}
void fill(Color color) override;
void draw_pixel_at(int x, int y, Color color) override;
@@ -40,7 +37,6 @@ class EPaperSSD1677Gray4 : public EPaperSSD1677 {
protected:
// A partial update if partial updates are enabled and this is not a full one.
bool is_partial_push_() const { return this->update_count_ != 0 && this->sent_.is_valid(); }
bool buffer_is_plane() const override { return false; }
void plane_row(size_t y, uint8_t *out) override;
void refresh_screen(bool partial) override;
bool transfer_data() override;
@@ -364,6 +364,15 @@ bool HOT EPaperT133A01::transfer_data() {
return true;
}
void EPaperT133A01::abort_transfer() {
// A data stream is open, with one of the chip selects held low, whenever transfer_data() has started sending rows
if (this->current_data_index_ == 0)
return;
this->disable();
this->cs_pin_->digital_write(true);
this->cs1_pin_->digital_write(true);
}
void EPaperT133A01::dump_config() {
EPaperBase::dump_config();
LOG_PIN(" CS Pin: ", this->cs_pin_);
@@ -22,9 +22,7 @@ class EPaperT133A01 : public EPaperBase {
public:
EPaperT133A01(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR) {
this->buffer_length_ = (size_t) width * height / 2; // 2 pixels per byte at 4bpp
}
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_COLOR, 4) {}
void set_cs_pins(GPIOPin *cs, GPIOPin *cs1) {
this->cs_pin_ = cs;
@@ -46,6 +44,7 @@ class EPaperT133A01 : public EPaperBase {
void deep_sleep() override;
bool transfer_data() override;
void abort_transfer() override;
/**
* Send a command (and optional data) selecting one or both controllers.
@@ -32,9 +32,7 @@ class EPaperUC8179 final : public EPaperBase {
public:
EPaperUC8179(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
this->buffer_length_ = this->row_width_ * height;
}
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {}
protected:
bool initialise(bool partial) override;
@@ -22,9 +22,7 @@ class EPaperWaveshareBWR : public EPaperBase {
public:
EPaperWaveshareBWR(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
this->buffer_length_ = this->row_width_ * height * 2;
}
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY, 1, 2) {}
void fill(Color color) override;
@@ -18,9 +18,7 @@ class EPaperWeAct3C : public EPaperBase {
public:
EPaperWeAct3C(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY) {
this->buffer_length_ = this->row_width_ * height * 2;
}
: EPaperBase(name, width, height, init_sequence, init_sequence_length, DISPLAY_TYPE_BINARY, 1, 2) {}
void fill(Color color) override;
void clear() override;
@@ -87,6 +87,38 @@ jd79660.extend(
),
)
# Waveshare 2.13-G (V2 hardware/init revision, PCB marked "rev 2.1" or later)
#
# Vendor init derived from vendor sample code
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g_V2.cpp>
# Compatible MIT license, see esphome/LICENSE file.
#
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
# Note: panel is 122px wide, but vendor code declares the TRES register as a fixed
# 124 on this revision. width=124 is declared here (rather than 122) purely so the
# transferred bytes-per-row matches what the vendor init tells the IC to expect; the
# extra 2 columns fall outside the visible glass. The model width must equal the
# TRES value in initsequence, so overriding dimensions: in YAML brings the shear back.
#
# fmt: off
jd79660.extend(
"Waveshare-2.13in-G",
width=124,
height=250,
initsequence=(
(0x61, 0x00, 0x7C, 0x00, 0xFA,), # TRES: 124x250 fixed (124 = TRES stride value)
(0xE9, 0x01,),
# Power On (0x04): Must be early part of init seq = Disabled later!
(0x04,),
),
fast_update=(
(0xE0, 0x02,),
(0xE6, 0x5A,),
(0xA5,),
),
)
# Waveshare 7.5-H
#
# Vendor init derived from vendor sample code
@@ -130,7 +162,7 @@ jd79660.extend(
# Note: busy pin uses LOW=busy, HIGH=idle.
#
# fmt: off
jd79660.extend(
gdey0213f51 = jd79660.extend(
"goodisplay-gdey0213f51-2.13",
width=128,
height=250,
@@ -154,3 +186,13 @@ jd79660.extend(
(0x04,),
),
)
# Waveshare 2.13-G (V1 hardware/init revision, older stock): the same panel and init as the Good
# Display entry above, byte for byte, so it only adds the name. Vendor sample:
# <https://github.com/waveshareteam/e-Paper/blob/master/E-paper_Separate_Program/2in13_e-Paper_G/ESP32/EPD_2in13g.cpp>
# Compatible MIT license, see esphome/LICENSE file.
#
# Note: busy pin uses LOW=busy, HIGH=idle. Configure with inverted: true in YAML.
# Note: width=128 is the TRES value the init sequence programs, not the 122 visible columns; overriding
# dimensions: in YAML shears the image. Not tested on V1 hardware.
gdey0213f51.extend("Waveshare-2.13in-G-V1")
@@ -24,3 +24,11 @@ WaveshareB(
data_rate="10MHz",
minimum_update_interval="1s",
)
WaveshareB(
"waveshare-2.66in-b",
width=152,
height=296,
data_rate="10MHz",
minimum_update_interval="15s",
)
+26 -14
View File
@@ -111,24 +111,36 @@ void ES8388::setup() {
// Power on ADC, Enable LIN&RIN, Power off MICBIAS, set int1lp to low power mode
ES8388_ERROR_FAILED(this->write_byte(ES8388_ADCPOWER, 0x09));
#ifdef USE_SELECT
#ifdef USE_ES8388_SELECT
if (this->dac_output_select_ != nullptr) {
auto dac_power = this->get_dac_power();
if (dac_power.has_value()) {
if (this->dac_output_select_->has_index(dac_power.value())) {
this->dac_output_select_->publish_state(dac_power.value());
} else {
ESP_LOGW(TAG, "Unknown DAC output power value: %d", dac_power.value());
if (this->dac_output_initial_.has_value()) {
const DacOutputLine line = this->dac_output_initial_.value();
ES8388_ERROR_FAILED(this->set_dac_output(line));
this->dac_output_select_->publish_state(static_cast<size_t>(line));
} else {
auto dac_power = this->get_dac_power();
if (dac_power.has_value()) {
if (this->dac_output_select_->has_index(dac_power.value())) {
this->dac_output_select_->publish_state(dac_power.value());
} else {
ESP_LOGW(TAG, "Unknown DAC output power value: %d", dac_power.value());
}
}
}
}
if (this->adc_input_mic_select_ != nullptr) {
auto mic_input = this->get_mic_input();
if (mic_input.has_value()) {
if (this->adc_input_mic_select_->has_index(mic_input.value())) {
this->adc_input_mic_select_->publish_state(mic_input.value());
} else {
ESP_LOGW(TAG, "Unknown ADC input mic value: %d", mic_input.value());
if (this->adc_input_mic_initial_.has_value()) {
const AdcInputMicLine line = this->adc_input_mic_initial_.value();
ES8388_ERROR_FAILED(this->set_adc_input_mic(line));
this->adc_input_mic_select_->publish_state(static_cast<size_t>(line));
} else {
auto mic_input = this->get_mic_input();
if (mic_input.has_value()) {
if (this->adc_input_mic_select_->has_index(mic_input.value())) {
this->adc_input_mic_select_->publish_state(mic_input.value());
} else {
ESP_LOGW(TAG, "Unknown ADC input mic value: %d", mic_input.value());
}
}
}
}
@@ -138,7 +150,7 @@ void ES8388::setup() {
void ES8388::dump_config() {
ESP_LOGCONFIG(TAG, "ES8388 Audio Codec:");
LOG_I2C_DEVICE(this);
#ifdef USE_SELECT
#ifdef USE_ES8388_SELECT
LOG_SELECT(" ", "DacOutputSelect", this->dac_output_select_);
LOG_SELECT(" ", "ADCInputMicSelect", this->adc_input_mic_select_);
#endif
+13 -2
View File
@@ -5,7 +5,7 @@
#include "esphome/components/audio_dac/audio_dac.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#ifdef USE_SELECT
#ifdef USE_ES8388_SELECT
#include "esphome/components/select/select.h"
#endif
@@ -26,7 +26,7 @@ enum AdcInputMicLine : uint8_t {
};
class ES8388 final : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
#ifdef USE_SELECT
#ifdef USE_ES8388_SELECT
SUB_SELECT(dac_output)
SUB_SELECT(adc_input_mic)
#endif
@@ -68,11 +68,22 @@ class ES8388 final : public audio_dac::AudioDac, public Component, public i2c::I
bool set_dac_output(DacOutputLine line);
bool set_adc_input_mic(AdcInputMicLine line);
#ifdef USE_ES8388_SELECT
/// Line to apply in setup() instead of keeping what the codec reports.
void set_dac_output_initial(DacOutputLine line) { this->dac_output_initial_ = line; }
void set_adc_input_mic_initial(AdcInputMicLine line) { this->adc_input_mic_initial_ = line; }
#endif
protected:
/// @brief Mutes or unmutes the DAC audio out
/// @param mute_state True to mute, false to unmute
/// @return True if successful and false otherwise
bool set_mute_state_(bool mute_state);
#ifdef USE_ES8388_SELECT
optional<DacOutputLine> dac_output_initial_{};
optional<AdcInputMicLine> adc_input_mic_initial_{};
#endif
};
} // namespace esphome::es8388
+43 -3
View File
@@ -1,7 +1,11 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import ENTITY_CATEGORY_CONFIG, ICON_CHIP # noqa: F401
from esphome.const import (
CONF_INITIAL_OPTION,
ENTITY_CATEGORY_CONFIG,
ICON_CHIP, # noqa: F401
)
from esphome.types import ConfigType
from ..audio_dac import CONF_ES8388_ID, ES8388, es8388_ns
@@ -9,6 +13,21 @@ from ..audio_dac import CONF_ES8388_ID, ES8388, es8388_ns
CONF_DAC_OUTPUT = "dac_output"
CONF_ADC_INPUT_MIC = "adc_input_mic"
DacOutputLine = es8388_ns.enum("DacOutputLine")
DAC_OUTPUT_OPTIONS = {
"LINE1": DacOutputLine.DAC_OUTPUT_LINE1,
"LINE2": DacOutputLine.DAC_OUTPUT_LINE2,
"BOTH": DacOutputLine.DAC_OUTPUT_BOTH,
}
DAC_OUTPUT_OPTION_NAMES = list(DAC_OUTPUT_OPTIONS)
AdcInputMicLine = es8388_ns.enum("AdcInputMicLine")
ADC_INPUT_MIC_OPTIONS = {
"LINE1": AdcInputMicLine.ADC_INPUT_MIC_LINE1,
"LINE2": AdcInputMicLine.ADC_INPUT_MIC_LINE2,
"DIFFERENCE": AdcInputMicLine.ADC_INPUT_MIC_DIFFERENCE,
}
ADC_INPUT_MIC_OPTION_NAMES = list(ADC_INPUT_MIC_OPTIONS)
DacOutputSelect = es8388_ns.class_("DacOutputSelect", select.Select)
ADCInputMicSelect = es8388_ns.class_("ADCInputMicSelect", select.Select)
@@ -19,24 +38,45 @@ CONFIG_SCHEMA = cv.All(
DacOutputSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP,
).extend(
{
cv.Optional(CONF_INITIAL_OPTION): cv.enum(
DAC_OUTPUT_OPTIONS, upper=True
),
}
),
cv.Optional(CONF_ADC_INPUT_MIC): select.select_schema(
ADCInputMicSelect,
entity_category=ENTITY_CATEGORY_CONFIG,
icon=ICON_CHIP,
).extend(
{
cv.Optional(CONF_INITIAL_OPTION): cv.enum(
ADC_INPUT_MIC_OPTIONS, upper=True
),
}
),
}
)
async def to_code(config: ConfigType) -> None:
cg.add_define("USE_ES8388_SELECT")
hub = await cg.get_variable(config[CONF_ES8388_ID])
selects = select.sub_selects(config, parent=hub)
await selects(
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=["LINE1", "LINE2", "BOTH"]
CONF_DAC_OUTPUT, hub.set_dac_output_select, options=DAC_OUTPUT_OPTION_NAMES
)
await selects(
CONF_ADC_INPUT_MIC,
hub.set_adc_input_mic_select,
options=["LINE1", "LINE2", "DIFFERENCE"],
options=ADC_INPUT_MIC_OPTION_NAMES,
)
if (
initial := config.get(CONF_DAC_OUTPUT, {}).get(CONF_INITIAL_OPTION)
) is not None:
cg.add(hub.set_dac_output_initial(initial))
if (
initial := config.get(CONF_ADC_INPUT_MIC, {}).get(CONF_INITIAL_OPTION)
) is not None:
cg.add(hub.set_adc_input_mic_initial(initial))
@@ -0,0 +1,3 @@
CODEOWNERS = ["@GioMez"]
DEPENDENCIES = ["i2c"]
DOMAIN = "es9038q2m_katana"
@@ -0,0 +1,92 @@
import esphome.codegen as cg
from esphome.components import audio_dac, i2c
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_ID, CONF_SAMPLE_RATE
from esphome.types import ConfigType
CONF_DEEMPHASIS = "deemphasis"
CONF_DOP = "dop"
CONF_FILTER_SHAPE = "filter_shape"
# Fields of the Katana format register: channel layout, word length and rate code.
FORMAT_STEREO = 0x80
BITS_PER_SAMPLE_CODES = {16: 0x10, 24: 0x20, 32: 0x30}
SAMPLE_RATE_CODES = {
44100: 0x04,
48000: 0x05,
88200: 0x06,
96000: 0x07,
176400: 0x08,
192000: 0x09,
352800: 0x0A,
384000: 0x0B,
}
es9038q2m_katana_ns = cg.esphome_ns.namespace("es9038q2m_katana")
ES9038Q2MKatana = es9038q2m_katana_ns.class_(
"ES9038Q2MKatana",
audio_dac.AudioDac,
cg.Component,
i2c.I2CDevice,
)
FilterShape = es9038q2m_katana_ns.enum("FilterShape")
FILTER_SHAPES = {
"linear_phase_fast": FilterShape.FILTER_SHAPE_LINEAR_PHASE_FAST,
"linear_phase_slow": FilterShape.FILTER_SHAPE_LINEAR_PHASE_SLOW,
"minimum_phase_fast": FilterShape.FILTER_SHAPE_MIN_PHASE_FAST,
"minimum_phase_slow": FilterShape.FILTER_SHAPE_MIN_PHASE_SLOW,
"apodizing": FilterShape.FILTER_SHAPE_APODIZING,
"hybrid": FilterShape.FILTER_SHAPE_HYBRID,
"brick_wall": FilterShape.FILTER_SHAPE_BRICK_WALL,
}
DeemphasisMode = es9038q2m_katana_ns.enum("DeemphasisMode")
DEEMPHASIS_MODES = {
"bypass": DeemphasisMode.DEEMPHASIS_BYPASS,
"32khz": DeemphasisMode.DEEMPHASIS_32KHZ,
"44_1khz": DeemphasisMode.DEEMPHASIS_44_1KHZ,
"48khz": DeemphasisMode.DEEMPHASIS_48KHZ,
}
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ES9038Q2MKatana),
cv.Optional(CONF_FILTER_SHAPE, default="apodizing"): cv.enum(
FILTER_SHAPES, lower=True
),
cv.Optional(CONF_BITS_PER_SAMPLE, default="16bit"): cv.All(
cv.float_with_unit("bits", "bit"),
cv.one_of(*BITS_PER_SAMPLE_CODES, int=True),
),
cv.Optional(CONF_SAMPLE_RATE, default=48000): cv.one_of(
*SAMPLE_RATE_CODES, int=True
),
cv.Optional(CONF_DEEMPHASIS, default="bypass"): cv.enum(
DEEMPHASIS_MODES, lower=True
),
# Only switches the control logic; the source must already send DoP frames.
cv.Optional(CONF_DOP, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(i2c.i2c_device_schema(0x30))
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await i2c.register_i2c_device(var, config)
cg.add(var.set_filter_shape(config[CONF_FILTER_SHAPE]))
cg.add(
var.set_format(
FORMAT_STEREO
| BITS_PER_SAMPLE_CODES[config[CONF_BITS_PER_SAMPLE]]
| SAMPLE_RATE_CODES[config[CONF_SAMPLE_RATE]]
)
)
cg.add(var.set_deemphasis_mode(config[CONF_DEEMPHASIS]))
cg.add(var.set_dop_enabled(config[CONF_DOP]))
@@ -0,0 +1,176 @@
#include "es9038q2m_katana.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include <cmath>
namespace esphome::es9038q2m_katana {
ESPHOME_LOG_TAG(TAG, "es9038q2m_katana");
namespace {
constexpr uint8_t KATANA_CHIP_ID = 0x30;
constexpr uint8_t KATANA_REG_CHIP_ID = 0x00;
constexpr uint8_t KATANA_REG_RESET = 0x01;
constexpr uint8_t KATANA_REG_VOLUME_1 = 0x02;
constexpr uint8_t KATANA_REG_VOLUME_2 = 0x03;
constexpr uint8_t KATANA_REG_MUTE = 0x04;
constexpr uint8_t KATANA_REG_DSP_PROGRAM = 0x05;
constexpr uint8_t KATANA_REG_DEEMPHASIS = 0x06;
constexpr uint8_t KATANA_REG_DOP = 0x07;
constexpr uint8_t KATANA_REG_FORMAT = 0x08;
constexpr uint8_t KATANA_REG_COMMAND = 0x09;
constexpr uint8_t KATANA_REG_MUTE_STREAM = 0x0A;
constexpr float KATANA_VOLUME_MIDPOINT = 0.5f;
constexpr float KATANA_VOLUME_MIDPOINT_STEPS = 50.0f; // -25 dB at 50% volume.
// Cubic amplitude gain: -20 * log10(volume^3) / 0.5 attenuation steps.
constexpr float KATANA_VOLUME_CUBIC_SCALE = 120.0f;
constexpr uint8_t KATANA_VOLUME_FULL_ATTENUATION = 255;
// The control logic ignores writes for this long after a reset.
constexpr uint32_t RESET_SETTLE_DELAY_MS = 10;
const LogString *filter_shape_to_str(FilterShape shape) {
switch (shape) {
case FILTER_SHAPE_LINEAR_PHASE_FAST:
return LOG_STR("linear_phase_fast");
case FILTER_SHAPE_LINEAR_PHASE_SLOW:
return LOG_STR("linear_phase_slow");
case FILTER_SHAPE_MIN_PHASE_FAST:
return LOG_STR("minimum_phase_fast");
case FILTER_SHAPE_MIN_PHASE_SLOW:
return LOG_STR("minimum_phase_slow");
case FILTER_SHAPE_APODIZING:
return LOG_STR("apodizing");
case FILTER_SHAPE_HYBRID:
return LOG_STR("hybrid");
default:
return LOG_STR("brick_wall");
}
}
const LogString *deemphasis_to_str(DeemphasisMode mode) {
switch (mode) {
case DEEMPHASIS_32KHZ:
return LOG_STR("32khz");
case DEEMPHASIS_44_1KHZ:
return LOG_STR("44_1khz");
case DEEMPHASIS_48KHZ:
return LOG_STR("48khz");
default:
return LOG_STR("bypass");
}
}
} // namespace
void ES9038Q2MKatana::setup() {
uint8_t chip_id = 0;
if (!this->read_byte(KATANA_REG_CHIP_ID, &chip_id)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
if (chip_id != KATANA_CHIP_ID) {
ESP_LOGE(TAG, "Unexpected chip ID 0x%02X", chip_id);
this->mark_failed(LOG_STR("Unexpected chip ID"));
return;
}
if (!this->write_byte(KATANA_REG_RESET, 0x01)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
delay(RESET_SETTLE_DELAY_MS);
// MUTE_STREAM is the gate the Linux driver closes between streams; ESPHome has no such
// notion, so it stays open and mute is only ever driven through MUTE.
if (!this->write_byte(KATANA_REG_FORMAT, this->format_) || !this->write_byte(KATANA_REG_COMMAND, 0x00) ||
!this->write_byte(KATANA_REG_DSP_PROGRAM, this->filter_shape_) ||
!this->write_byte(KATANA_REG_DEEMPHASIS, this->deemphasis_mode_) ||
!this->write_byte(KATANA_REG_DOP, this->dop_enabled_ ? 1 : 0) || !this->write_volume_() ||
!this->write_byte(KATANA_REG_MUTE, this->is_muted_ ? 1 : 0) || !this->write_byte(KATANA_REG_MUTE_STREAM, 0x00)) {
this->mark_failed(LOG_STR("Failed to configure the control logic"));
}
}
void ES9038Q2MKatana::dump_config() {
ESP_LOGCONFIG(TAG,
"ES9038Q2M Katana:\n"
" Filter shape: %s\n"
" De-emphasis: %s\n"
" Format register: 0x%02X\n"
" DoP: %s",
LOG_STR_ARG(filter_shape_to_str(this->filter_shape_)),
LOG_STR_ARG(deemphasis_to_str(this->deemphasis_mode_)), this->format_, ONOFF(this->dop_enabled_));
LOG_I2C_DEVICE(this);
if (this->is_failed())
return;
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
for (uint8_t reg = KATANA_REG_CHIP_ID; reg <= KATANA_REG_MUTE_STREAM; reg++) {
uint8_t value = 0;
if (this->read_byte(reg, &value)) {
ESP_LOGV(TAG, " Register 0x%02X: 0x%02X", reg, value);
} else {
ESP_LOGV(TAG, " Register 0x%02X: read failed", reg);
}
}
#endif
}
// Both channel registers follow ESPHome's single master volume.
bool ES9038Q2MKatana::write_volume_() {
return this->write_byte(KATANA_REG_VOLUME_1, this->volume_reg_) &&
this->write_byte(KATANA_REG_VOLUME_2, this->volume_reg_);
}
// Records the outcome of a runtime write; the cached state only changes on success.
bool ES9038Q2MKatana::check_write_(bool ok) {
if (ok) {
this->status_clear_warning();
} else {
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
}
return ok;
}
bool ES9038Q2MKatana::set_volume(float volume) {
if (this->is_failed())
return false;
volume = clamp(volume, 0.0f, 1.0f);
const uint8_t previous = this->volume_reg_;
float attenuation = KATANA_VOLUME_FULL_ATTENUATION;
if (volume >= KATANA_VOLUME_MIDPOINT) {
attenuation = KATANA_VOLUME_MIDPOINT_STEPS * (1.0f - volume) / (1.0f - KATANA_VOLUME_MIDPOINT);
} else if (volume > 0.0f) {
attenuation = KATANA_VOLUME_MIDPOINT_STEPS - KATANA_VOLUME_CUBIC_SCALE * log10f(volume / KATANA_VOLUME_MIDPOINT);
}
this->volume_reg_ =
static_cast<uint8_t>(lroundf(clamp(attenuation, 0.0f, static_cast<float>(KATANA_VOLUME_FULL_ATTENUATION))));
if (this->check_write_(this->write_volume_()))
return true;
this->volume_reg_ = previous;
return false;
}
float ES9038Q2MKatana::volume() {
if (this->volume_reg_ == KATANA_VOLUME_FULL_ATTENUATION)
return 0.0f;
if (this->volume_reg_ <= KATANA_VOLUME_MIDPOINT_STEPS)
return 1.0f - this->volume_reg_ * (1.0f - KATANA_VOLUME_MIDPOINT) / KATANA_VOLUME_MIDPOINT_STEPS;
return KATANA_VOLUME_MIDPOINT *
powf(10.0f, (KATANA_VOLUME_MIDPOINT_STEPS - this->volume_reg_) / KATANA_VOLUME_CUBIC_SCALE);
}
bool ES9038Q2MKatana::set_mute_state_(bool mute_state) {
if (this->is_failed())
return false;
if (!this->check_write_(this->write_byte(KATANA_REG_MUTE, mute_state ? 1 : 0)))
return false;
this->is_muted_ = mute_state;
return true;
}
} // namespace esphome::es9038q2m_katana
@@ -0,0 +1,62 @@
#pragma once
#include "esphome/components/audio_dac/audio_dac.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::es9038q2m_katana {
// DSP program numbers of the Katana control interface; there is no program 5.
enum FilterShape : uint8_t {
FILTER_SHAPE_LINEAR_PHASE_FAST = 0,
FILTER_SHAPE_LINEAR_PHASE_SLOW = 1,
FILTER_SHAPE_MIN_PHASE_FAST = 2,
FILTER_SHAPE_MIN_PHASE_SLOW = 3,
FILTER_SHAPE_APODIZING = 4,
FILTER_SHAPE_HYBRID = 6,
FILTER_SHAPE_BRICK_WALL = 7,
};
enum DeemphasisMode : uint8_t {
DEEMPHASIS_BYPASS = 0,
DEEMPHASIS_32KHZ = 1,
DEEMPHASIS_44_1KHZ = 2,
DEEMPHASIS_48KHZ = 3,
};
// Boards that expose an ES9038Q2M through the Allo Katana control logic at I2C address 0x30.
// Ported from the Linux driver sound/soc/bcm/allo-katana-codec.c; the DAC's own registers
// are never written, the control logic applies the configuration itself.
class ES9038Q2MKatana : public audio_dac::AudioDac, public Component, public i2c::I2CDevice {
public:
void setup() override;
void dump_config() override;
// Cubic below 50%, then 0.5 dB per 1% above, meeting at -25 dB.
// Zero selects full attenuation (-127.5 dB).
bool set_volume(float volume) override;
float volume() override;
bool set_mute_off() override { return this->set_mute_state_(false); }
bool set_mute_on() override { return this->set_mute_state_(true); }
bool is_muted() override { return this->is_muted_; }
void set_filter_shape(FilterShape shape) { this->filter_shape_ = shape; }
void set_format(uint8_t format) { this->format_ = format; }
void set_deemphasis_mode(DeemphasisMode mode) { this->deemphasis_mode_ = mode; }
void set_dop_enabled(bool enabled) { this->dop_enabled_ = enabled; }
protected:
bool set_mute_state_(bool mute_state);
bool write_volume_();
bool check_write_(bool ok);
FilterShape filter_shape_{FILTER_SHAPE_APODIZING};
DeemphasisMode deemphasis_mode_{DEEMPHASIS_BYPASS};
uint8_t format_{0};
// Attenuation in 0.5 dB steps: 0 is loudest, 255 quietest. Starts 20 dB down.
uint8_t volume_reg_{0x28};
bool dop_enabled_{false};
};
} // namespace esphome::es9038q2m_katana
+2 -1
View File
@@ -243,7 +243,7 @@ DEFAULT_EXCLUDED_IDF_COMPONENTS = (
"esp_coex", # WiFi/BT coexistence - re-included by esp32_ble_tracker, zigbee; esp_wifi/bt pull it back
"esp_driver_cam", # Camera driver - the esp32-camera managed component pulls it back
"esp_driver_dac", # DAC driver - only needed by esp32_dac component
"esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, opentherm, Arduino BLE libs
"esp_driver_gptimer", # General purpose timer - re-included by ac_dimmer, hdmi_cec, opentherm, Arduino BLE libs
"esp_driver_i2c", # I2C driver - re-included by i2c; esp32-camera pulls it back itself
"esp_driver_i2s", # I2S driver - only needed by i2s_audio component
"esp_driver_ledc", # LEDC PWM driver - re-included by ledc; esp32-camera pulls it back itself
@@ -2920,6 +2920,7 @@ async def _finalize_arduino_aware_flags():
if not arduino_linked:
cg.add_build_flag("-Wl,--wrap=esp_panic_handler")
cg.add_define("USE_ESP32_CRASH_HANDLER")
cg.add_define("USE_CRASH_HANDLER")
elif not CORE.using_arduino:
cg.add_build_flag("-DUSE_ARDUINO")
+3 -2
View File
@@ -161,7 +161,7 @@ static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-c
namespace esphome::esp32 {
ESPHOME_LOG_TAG(TAG, "esp32.crash");
ESPHOME_LOG_TAG(TAG, CRASH_LOG_TAG);
// RAM copy of the build timestamp. The generated constant lives in flash,
// which the panic handler must not read (cache may be disabled during
@@ -175,7 +175,8 @@ static uint32_t s_current_build_time = static_cast<uint32_t>(ESPHOME_BUILD_TIME)
// Validate the NOINIT record. Runs on every has_data() call; re-running is
// harmless and the magic is left alone so the record survives an OTA
// rollback reboot, crash_handler_clear() drops it once an API client has it.
// rollback reboot, crash_handler_clear() drops it once a log client has it
// (logger::log_crash_to_client(), for the API and MQTT).
static void read_crash_data() {
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
s_crash_data_valid = true;
+3
View File
@@ -4,6 +4,9 @@
namespace esphome::esp32 {
/// The tag the crash record is logged under, so a client can check the level in force for it
inline constexpr const char *CRASH_LOG_TAG = "esp32.crash";
/// Log crash data if a crash was detected on previous boot.
void crash_handler_log();
+1
View File
@@ -131,6 +131,7 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
void ESP32Preferences::open() {
// Runs from app_main() before the logger is initialized; any logging here
// must be deferred. See s_open_err and make_preference() below.
// Components that need NVS check nvs_handle instead of initializing it again.
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err == 0)
+4 -12
View File
@@ -5,6 +5,7 @@
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#ifndef CONFIG_BT_CONTROLLER_DISABLED
#include <esp_bt.h>
@@ -28,7 +29,6 @@ extern "C" {
#include <freertos/FreeRTOS.h>
#include <freertos/FreeRTOSConfig.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#ifdef USE_ARDUINO
// Prevent Arduino from releasing BT memory at startup (esp32-hal-misc.c).
@@ -77,8 +77,9 @@ static constexpr uint32_t HOSTED_BT_WDT_TIMEOUT_MS = 60000;
void ESP32BLE::setup() {
global_ble = this;
if (!ble_pre_setup_()) {
ESP_LOGE(TAG, "BLE could not be prepared for configuration");
// Preferences opens NVS in app_main(); a zero handle means it could not be opened, even after an erase
if (global_preferences->nvs_handle == 0) {
ESP_LOGE(TAG, "NVS unavailable");
this->mark_failed();
return;
}
@@ -185,15 +186,6 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
}
#endif
bool ESP32BLE::ble_pre_setup_() {
esp_err_t err = nvs_flash_init();
if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_flash_init failed: %d", err);
return false;
}
return true;
}
#ifdef USE_ESP32_BLE_ADVERTISING
void ESP32BLE::advertising_init_() {
if (this->advertising_ != nullptr)
-1
View File
@@ -185,7 +185,6 @@ class ESP32BLE final : public Component {
}
this->ble_events_.get_and_reset_dropped_count();
}
bool ble_pre_setup_();
#ifdef USE_ESP32_BLE_ADVERTISING
void advertising_init_();
#endif
@@ -10,7 +10,6 @@
#include <esp_gap_ble_api.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cstring>
#include "esphome/core/hal.h"
@@ -300,7 +300,7 @@ void BLEClientBase::log_warning_(const char *message) {
esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency,
uint16_t timeout, const char *param_type) {
esp_ble_conn_update_params_t conn_params = {{0}};
esp_ble_conn_update_params_t conn_params = {};
memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
conn_params.min_int = min_interval;
conn_params.max_int = max_interval;
@@ -7,7 +7,6 @@
#ifdef USE_ESP32
#include <nvs_flash.h>
#include <freertos/FreeRTOSConfig.h>
#include <esp_bt_main.h>
#ifndef CONFIG_BT_CONTROLLER_DISABLED
@@ -48,6 +48,7 @@ class BLEService final {
bool is_created() { return this->state_ == CREATED; }
bool is_running() { return this->state_ == RUNNING; }
bool is_starting() { return this->state_ == STARTING; }
bool is_stopped() { return this->state_ == STOPPED; }
bool is_deleted() { return this->state_ == DELETED; }
protected:
@@ -16,7 +16,6 @@
#include <freertos/FreeRTOS.h>
#include <freertos/FreeRTOSConfig.h>
#include <freertos/task.h>
#include <nvs_flash.h>
#include <cinttypes>
#ifdef USE_OTA
+23
View File
@@ -6,6 +6,8 @@ import esphome.codegen as cg
from esphome.components import canbus
from esphome.components.canbus import CONF_BIT_RATE, CanbusComponent, CanSpeed
from esphome.components.esp32 import (
CONF_MINIMUM_CHIP_REVISION,
KEY_ESP32,
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
@@ -20,6 +22,8 @@ from esphome.components.esp32 import (
)
import esphome.config_validation as cv
from esphome.const import (
CONF_ADVANCED,
CONF_FRAMEWORK,
CONF_ID,
CONF_MODE,
CONF_RX_PIN,
@@ -27,6 +31,7 @@ from esphome.const import (
CONF_TX_PIN,
CONF_TX_QUEUE_LEN,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@Sympatron"]
@@ -42,6 +47,7 @@ CanMode = esp32_can_ns.enum("CanMode")
CAN_MODES = {
"NORMAL": CanMode.CAN_MODE_NORMAL,
"LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY,
"NO_ACK": CanMode.CAN_MODE_NO_ACK,
}
# Currently the driver only supports a subset of the bit rates defined in canbus
@@ -49,6 +55,7 @@ CAN_MODES = {
# See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI)
CAN_SPEEDS_ESP32 = {
"20KBPS": CanSpeed.CAN_20KBPS,
"25KBPS": CanSpeed.CAN_25KBPS,
"50KBPS": CanSpeed.CAN_50KBPS,
"100KBPS": CanSpeed.CAN_100KBPS,
@@ -114,6 +121,22 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend(
)
def _final_validate(config: ConfigType) -> None:
if get_esp32_variant() == VARIANT_ESP32 and config[CONF_BIT_RATE] == "20KBPS":
esp32_config = fv.full_config.get()[KEY_ESP32]
min_revision = esp32_config[CONF_FRAMEWORK][CONF_ADVANCED].get(
CONF_MINIMUM_CHIP_REVISION
)
if min_revision is None or min_revision < "2.0":
raise cv.Invalid(
"20KBPS on ESP32 requires esp32.framework.advanced.minimum_chip_revision: '2.0' or higher",
path=[CONF_BIT_RATE],
)
FINAL_VALIDATE_SCHEMA = _final_validate
def get_default_tx_enqueue_timeout(bit_rate: str) -> int:
bit_rate_numeric = canbus.get_rate(bit_rate)
bits_per_packet = 140 # ~max CAN message length
+9 -2
View File
@@ -33,6 +33,10 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
case canbus::CAN_16KBPS:
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_16KBITS();
return true;
#endif
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C6) || defined(USE_ESP32_VARIANT_ESP32C61) || \
defined(USE_ESP32_VARIANT_ESP32H2) || defined(USE_ESP32_VARIANT_ESP32P4) || defined(USE_ESP32_VARIANT_ESP32S2) || \
defined(USE_ESP32_VARIANT_ESP32S3) || (defined(USE_ESP32_VARIANT_ESP32) && CONFIG_ESP32_REV_MIN_FULL >= 200)
case canbus::CAN_20KBPS:
*t_config = (twai_timing_config_t) TWAI_TIMING_CONFIG_20KBITS();
return true;
@@ -75,10 +79,13 @@ bool ESP32Can::setup_internal() {
}
// Select TWAI mode based on configuration
twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL;
twai_mode_t twai_mode = TWAI_MODE_NORMAL;
if (this->mode_ == CAN_MODE_LISTEN_ONLY) {
twai_mode = TWAI_MODE_LISTEN_ONLY;
ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)");
} else if (this->mode_ == CAN_MODE_NO_ACK) {
twai_mode = TWAI_MODE_NO_ACK;
ESP_LOGI(TAG, "CAN bus configured in NO_ACK mode (transmit without requiring ACK)");
}
twai_general_config_t g_config =

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