Compare commits

...
Author SHA1 Message Date
J. Nick Koston 55b020eaf8 Merge remote-tracking branch 'origin/dev' into sx1509-max6956-apply-outputs
# Conflicts:
#	esphome/core/defines.h
2026-11-04 20:51:14 -06:00
J. Nick Koston 2499ecaa51 [remote_base] Generate transmit_raw and rc_switch transmits as inline actions (#22005) 2026-11-04 20:47:10 -06:00
J. Nick Koston 82824861ae [thermopro_ble] Simplify the parser and pass the MAC address to the constructor (#22052) 2026-11-04 21:39:56 -05:00
J. Nick Koston ce35de6468 [components] Use the gpio_expander input store in every cached expander (#22057) 2026-11-04 20:29:01 -06:00
J. Nick Koston fc106564ed [bme680_bsec] Publish pending readings from fixed storage instead of a heap queue (#22193) 2026-11-04 20:16:06 -06:00
J. Nick Koston 7b380674ee [gpio_expander] Add an input store to the cached expander bases (#22056) 2026-11-04 20:14:38 -06:00
J. Nick Koston e4011bd96a [components] Publish sy6970, tca8418 and tm1638 children through cg.HubFunction (#22104) 2026-11-04 20:11:22 -06:00
J. Nick Koston 1fa3fe26bb [ezo] Replace the heap command queue with a fixed ring buffer (#21840) 2026-11-04 20:05:52 -06:00
Keith Burzinski 3de1503ec0 [api] Let clients fetch the crash recorded on the previous boot (#21596) 2026-11-05 02:04:01 +00:00
Jesse Hills c248a7d3c4 [epaper_spi] Add Good Display GDEY0583T81 5.83" model (#22143) 2026-11-04 20:58:45 -05:00
esphome[bot] 59d507197d Bump aioesphomeapi from 46.27.0 to 46.28.0 (#22290) 2026-11-05 01:38:40 +00:00
J. Nick Koston 469a459b9a [wifi] Retry at once when a scan leaves no hidden network to try (#21747) 2026-11-04 19:36:43 -06:00
J. Nick Koston f06cba700f [web_server_base] Apply the web server's allowed origins check to every handler (#21972) 2026-11-04 19:36:18 -06:00
J. Nick Koston 5b077a0a9a [wifi] Treat open networks as open in RTL87xx scan results (#21750) 2026-11-04 19:35:13 -06:00
J. Nick Koston 51b88cee9b [wifi] Ignore the scan inside a join on RTL87xx (#21749) 2026-11-04 20:20:34 -05:00
J. Nick Koston da5aba76c7 [wifi] Log why a join failed on RTL87xx (#21751) 2026-11-04 20:19:37 -05:00
J. Nick Koston 765ca4a085 [schema] Keep sibling extends when shrink inlines one schema (#22287) 2026-11-04 19:19:26 -06:00
J. Nick Koston 9b24d1afda [libretiny] Record abort() as a crash and reboot (#22288) 2026-11-04 18:55:07 -06:00
esphome[bot]andJ. Nick Koston 549b56197f Bump aioesphomeapi from 46.26.0 to 46.27.0 (#22279)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-04 18:38:46 -06:00
J. Nick Koston c390a46f1c [core] Keep a trigger's only extra key in the language schema (#21801) 2026-11-04 17:40:01 -06:00
JoeandJ. Nick Koston cdc0d69e4c [canbus] Add support for CAN FD frames (#21950)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-04 17:27:20 -06:00
Michael Hansen 92723c905c [voice_assistant] Stream buffered wake word audio for verification (#21885) 2026-11-04 16:47:29 -06:00
J. Nick Koston ca8be3e953 [bme68x_bsec2] Publish pending readings from fixed storage instead of a heap queue (#22191) 2026-11-04 17:27:31 -05:00
J. Nick Koston 3f330fcdb2 [mqtt] Cap incoming message size with max_payload_size (#22278) 2026-11-04 17:24:38 -05:00
J. Nick Koston 95278ea4d8 [pipsolar] Fix flaky switch integration test (#22282) 2026-11-04 17:23:26 -05:00
Jonathan Swoboda a6b2d20038 [docker] Bump base image to 2026.11.0 (#22275) 2026-11-04 17:15:04 -05:00
Keith BurzinskiandClaude Fable 5.1 a37e3bba26 [nfc] Fix out-of-bounds read in NDEF record parsing (#22276)
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-04 20:31:55 +00:00
puddlyandJ. Nick Koston e83e892514 [usb_host] Read iInterface for connected interfaces (#21781)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-04 14:04:52 -06:00
esphome[bot] e1c781871b Bump aioesphomeapi from 46.25.1 to 46.26.0 (#22274) 2026-11-04 19:54:30 +00:00
dependabot[bot] f926fabbd8 Bump pytest-asyncio from 1.4.1 to 1.4.1.post1 (#22272)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-04 13:46:21 -06:00
124969267d [bthome_mithermometer] Add opt-in replay protection (#19957)
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-11-04 13:45:33 -06:00
dependabot[bot] de24754b4d Bump zeroconf from 0.152.0 to 0.153.0 (#22273)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-04 19:32:33 +00:00
dependabot[bot] 9b138d17de Bump platformdirs from 4.14.2 to 4.14.3 (#22271)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-04 19:32:10 +00:00
J. Nick Koston c64d5f149f [ai] Prefer generated decoding for fixed register fields in the codegen skill (#22093) 2026-11-04 12:52:42 -06:00
J. Nick Koston d29268cc00 [remote_base] Dispatch received frames from a span (#22269) 2026-11-04 12:47:35 -06:00
J. Nick Koston 9969cb3de1 [bq25186] Generate the status entity decoding in code generation (#22091) 2026-11-04 12:43:38 -06:00
J. Nick Koston c9ca5ba910 [canbus] Pass received frames to callbacks without a heap vector per frame (#22008) 2026-11-04 12:41:24 -06:00
J. Nick Kostonandpre-commit-ci-lite[bot] 6d3239bf43 [store_yaml] Embed user YAML in firmware for recovery (#16445)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-11-04 12:34:44 -06:00
295de90303 [sm2135] Fix light turning off when switching color modes (#19438)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-04 12:17:55 -06:00
J. Nick Koston 2473ddcf19 [logger] Report the RP2 crash record when a host opens the USB port (#22266) 2026-11-04 12:17:40 -06:00
J. Nick Koston 26303448d7 [bq25186] Add tests for the status binary and text sensors (#22090) 2026-11-04 12:12:47 -06:00
J. Nick Koston 3755a7c970 [core] Add cg.HubFunction to generate one function per hub from child statements (#22102) 2026-11-04 12:12:46 -06:00
J. Nick Koston 76a0c75d27 [mdns] Bump espressif/mdns to 1.15.0 (#22267) 2026-11-04 13:11:22 -05:00
J. Nick Koston 55cb8ea03d [core] Let a byte payload lambda return a fixed-size buffer (#22043) 2026-11-04 12:06:39 -06:00
J. Nick Koston 58419f2207 [canbus] Register on_frame automations as callbacks instead of trigger components (#21795) 2026-11-04 11:58:57 -06:00
J. Nick Koston 1854bd12bb [rp2040_pio_led_strip] Compute the PIO clock divider with its fraction (#22265) 2026-11-04 16:45:04 +00:00
J. Nick Koston d927597771 [rp2040_pio_led_strip] Size the DMA channel arrays for RP2350 (#22153) 2026-11-04 16:44:16 +00:00
J. Nick Koston 20bd5f5316 [rp2040_pio_led_strip] Fail setup instead of panicking when no state machine or DMA channel is free (#22152) 2026-11-04 10:32:03 -06:00
J. Nick KostonandJonathan Swoboda 3b862f03f1 [rp2040_pio_led_strip] Load each PIO program once per PIO without a heap map (#22151)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-11-04 10:31:12 -06:00
J. Nick Koston c94cb79f65 [it8951] Use StaticRingBuffer for the op queue (#22263) 2026-11-04 10:31:04 -06:00
J. Nick Koston bffe06211a [core] Add push_front to StaticRingBuffer (#22262) 2026-11-04 10:31:04 -06:00
J. Nick Koston e07be6bef5 [tfluna] Drop signal_strength device class from the signal strength sensor (#22260) 2026-11-04 10:09:31 -06:00
J. Nick Koston c4100d5b35 [components] Use setup_check() in the remaining setup failure paths (#22255) 2026-11-04 10:09:00 -06:00
J. Nick Koston 781f4330ba [core] Dump typed_schema types that have no keys of their own (#22258) 2026-11-04 10:08:42 -06:00
J. Nick Koston d20e3b3bb5 [api] Make the event order backpressure test wait until the pipe is full (#22112) 2026-11-04 11:08:18 -05:00
J. Nick Koston b764b18733 [ld2410][ld2412] Drop signal_strength device class from the gate threshold numbers (#22259) 2026-11-04 11:03:21 -05:00
J. Nick Koston 657416e14b Merge remote-tracking branch 'origin/dev' into sx1509-max6956-apply-outputs
# Conflicts:
#	esphome/components/sx1509/sx1509.h
2026-11-04 09:33:03 -06:00
2d74ef7b29 [sx1509] Add configurable led_driver_frequency option (#20285)
Co-authored-by: Claude <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: J. Nick Koston <nick@home-assistant.io>
2026-11-04 09:30:35 -06:00
J. Nick Koston 562614d290 [esp32_ble] Fix build on Windows when Bluedroid's private headers are not reachable (#22257) 2026-11-04 09:28:41 -06:00
8a5c5a51f4 [remote_base] Switch Toto to little endian interpretation (#22254)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-04 15:22:59 +00:00
J. Nick Koston b656f2f9cb [bl0942][d01][daly_bms][dht][dht12][micronova][rc522][rp2][shelly_dimmer][radon_eye_rd200] Use sum_bytes and xor_bytes for checksums (#22209) 2026-11-04 08:49:23 -06:00
Reinout Heeck af3d283825 [remote_base] update Toto to measured timings (#22253) 2026-11-04 08:33:26 -06:00
eeefe3eb14 [deep_sleep] Fix ESP32-P4 deep sleep (#22246)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-11-04 09:18:06 -05:00
J. Nick Koston 1085d1bf3a [components] Use put_be and put_le for integer splits (pca9554 to toshiba) (#22216) 2026-11-04 09:17:49 -05:00
J. Nick Koston af34c1d009 [components] Use put_be and put_le for integer splits (ecocomfort2 to modbus_controller) (#22215) 2026-11-04 09:17:26 -05:00
J. Nick Koston 6536d88837 [bl0942][daly_bms][d01][dht12] Add checksum unit tests (#22208) 2026-11-04 09:15:59 -05:00
Francesco PaceandJ. Nick Koston d7c9cc6b63 [wifi_info] Add channel sensor (#22142)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-04 08:12:21 -06:00
J. Nick Koston 6dd111b1de [components] Use put_be and put_le for integer splits (ade7953_i2c to ezsp_proxy_tap) (#22214) 2026-11-04 08:03:09 -06:00
J. Nick Koston 4969f643cd [components] Use setup_check() in setup failure paths (#22182) 2026-11-04 08:02:30 -06:00
Jesse Hills ae31989bd8 Merge branch 'beta' into dev 2026-11-04 22:33:16 +13:00
Jesse Hills 49f3133c4f Merge pull request #22247 from esphome/bump-2026.11.0b1
2026.11.0b1
2026-11-04 22:32:37 +13:00
Clyde Stubbs d07311b8f1 [core] Move min/max volume consts to components/const (#22250) 2026-11-04 04:39:20 +00:00
J. Nick Koston 39966b6182 Merge remote-tracking branch 'origin/dev' into sx1509-max6956-apply-outputs
# Conflicts:
#	esphome/components/sx1509/sx1509.h
2026-11-03 21:55:39 -06:00
J. Nick Koston 75d2cd10bf [qmp6988] Use update_register for the CTRLMEAS register (#22218) 2026-11-03 22:43:19 -05:00
J. Nick Koston 5f7fd7692d [xgzp68xx] Use update_register for the oversampling config (#22221) 2026-11-03 22:43:01 -05:00
J. Nick Koston f960d99d66 [sx1509] Add a 16-bit update_register helper for the register pairs (#22223) 2026-11-03 22:42:37 -05:00
J. Nick Koston ab4e3b42f5 [opt3001] Avoid heap allocation in the read path (#22238) 2026-11-03 22:41:40 -05:00
J. Nick Koston 5c6a7a89c8 [rx8111] Keep boot retry callback in std::function inline buffer (#22239) 2026-11-03 22:39:38 -05:00
J. Nick Koston be25249209 [ai] Keep scheduler lambda captures within the std::function small buffer (#22240) 2026-11-03 22:37:53 -05:00
J. Nick Koston ffab9a81e7 [ld6002b] Keep the wake pulse frame in members so its timeout does not allocate (#22242) 2026-11-03 22:36:45 -05:00
Jonathan Swoboda bea7994c9f [bme280_base][bmp280_base] Compensate raw 0x80000 when the channel is enabled (#22236) 2026-11-03 22:33:26 -05:00
J. Nick Koston 6c8c105449 [bme68x_bsec2] Share BSEC test double types with bme680_bsec to fix the all-components C++ unit test build (#22245) 2026-11-04 16:08:58 +13:00
J. Nick Koston ca7401750d [ld6002b] Add unit tests for the wake pulse deferred write (#22241) 2026-11-03 22:02:59 -05:00
Jesse Hills cfb98dc580 Bump version to 2026.12.0-dev 2026-11-04 15:54:17 +13:00
psewar 1065312345 [voice_assistant] Do not treat "playback has not started" as "playback finished" (#19408) 2026-11-03 20:44:21 -06:00
Bascht74andJ. Nick Koston ca291a9724 [uart] Check devices on a forwarding UART against its source's settings (#20200)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-04 01:26:42 +00:00
J. Nick Koston 38ed6919f8 [bme68x_bsec2] Share BSEC test double types with bme680_bsec to fix the all-components C++ unit test build (#22245) 2026-11-03 19:25:55 -06:00
Jesse Hills 9a5a8f9822 Bump version to 2026.11.0b1 2026-11-04 14:20:07 +13:00
J. Nick Koston 9dea935499 [web_server_base] Count the HTTP server's sockets in one place (#21971) 2026-11-04 13:50:59 +13:00
J. Nick Koston 2d73971fcc [web_server_idf] Send a default header once when a response sets it (#21974) 2026-11-03 18:28:29 -06:00
J. Nick Koston 5d511a1bd1 [sim800l] Pass callback strings by const reference (#22203) 2026-11-03 18:17:08 -06:00
14b190ab8f [esp32_rmt] Check that RMT components fit the chip's RMT channels (#22172)
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-11-03 18:11:19 -06:00
J. Nick Koston c50a5a0879 [core] Give apply action lambdas their arguments by value again (#22011) 2026-11-04 13:02:32 +13:00
J. Nick Koston 75b9f8de1c [esp32_ble_client] Ignore a disconnect event from the previous link during a new connection (#21918) 2026-11-04 12:59:16 +13:00
J. Nick Koston 7637cdc162 [web_server] Keep a custom js_url/css_url on AP only WiFi (#21879) 2026-11-04 12:58:34 +13:00
J. Nick Koston 891ebdaaaa [espota2] Make the probe deadline test deterministic (#22234) 2026-11-03 17:50:38 -05:00
J. Nick Koston a01b213ab4 [bme68x_bsec2] Add unit tests for publishing readings (#22189) 2026-11-03 17:49:42 -05:00
J. Nick Koston a21268f641 [bme680_bsec] Add unit tests for publishing readings (#22190) 2026-11-03 17:49:13 -05:00
J. Nick Koston 7813ea1c7f [sim800l] Add integration test for the on_* automation arguments (#22202) 2026-11-03 17:48:31 -05:00
J. Nick Koston 82a375907d [xl9535] Use update_register and status_comm_result for pin access (#22217) 2026-11-03 17:47:13 -05:00
J. Nick Koston 9591e34722 [lps22] Use update_register for the one-shot trigger (#22219) 2026-11-03 17:46:18 -05:00
J. Nick Koston 408bd8820a [cap1188] Use update_register and check the touch status read (#22220) 2026-11-03 17:45:32 -05:00
J. Nick Koston 07a175692a [components] Replace local byte-order write helpers with put_be and put_le (#22212) 2026-11-03 22:14:32 +00:00
J. Nick Koston 94d0113f66 [core] Add put_be and put_le helpers to write integers into byte buffers (#22211) 2026-11-03 16:02:02 -06:00
09d3b645a3 [i2s_audio] Fix sample-rate-dependent microphone DC correction (#22130)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-03 21:45:41 +00:00
Adam Matthews 940f87056d [cse7766] Log the clamped power factor and reactive power at verbose level (#22224) 2026-11-03 14:00:28 -06:00
dependabot[bot] 6f284d171a Bump docker/setup-buildx-action from 4.4.0 to 4.4.1 in the docker-actions group (#22229)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-03 13:48:16 -06:00
dependabot[bot] a7a4473b32 Bump filelock from 4.2.4 to 4.2.6 (#22228)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-03 13:48:06 -06:00
dependabot[bot] 8092a23553 Bump pytest-codspeed from 5.0.3 to 5.0.4 (#22227)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-03 13:47:57 -06:00
dependabot[bot] 1a260e4c57 Bump zeroconf from 0.151.5 to 0.152.0 (#22226)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-03 13:47:48 -06:00
dependabot[bot] 8a30af4ad1 Bump platformdirs from 4.14.0 to 4.14.2 (#22225)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-03 13:47:41 -06:00
cad297866f [speaker] Reduce startup buffering for announcements (#21091)
Co-authored-by: andytrofimov <26643796+andytrofimov@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-03 14:04:36 -05:00
Petapton ff254ae9d8 [daikin_madoka] Add new Daikin Madoka integration (#5951) 2026-11-03 16:00:39 +00:00
J. Nick Koston f5ab5febb4 [wifi] Keep manual IP settings in flash (#21708) 2026-11-02 16:39:40 -10:00
J. Nick Koston 3d95a0ba33 [libretiny] Report the previous boot's crash on RTL8710B (#21752) 2026-11-02 16:39:17 -10:00
J. Nick Koston d8bfae306f [core] Add AutomationList for hubs that trigger their own automations (#21788) 2026-11-02 16:38:22 -10:00
J. Nick Koston 78d2fdbfc0 [wifi] Keep scans working after a join with a BSSID on RTL87xx (#21754) 2026-11-02 16:37:33 -10:00
J. Nick Koston 4af8e50dca [ai] Point reviewers at existing helpers (#22178) 2026-11-02 21:24:24 -05:00
J. Nick Koston 93d00b5920 [rx8111] Keep the scheduled event poll out of the unit tests (#22179) 2026-11-02 21:24:03 -05:00
J. Nick Koston 3d4e20f574 [components] Build enum options with options(prefix=) in ld600x and rx8111 (#22180) 2026-11-02 21:23:22 -05:00
J. Nick Koston be3113e7d5 [m5stack_unit_bldc] Declare child entities with the SUB_* macros (#22181) 2026-11-02 21:23:02 -05:00
J. Nick Koston e236c741db [core] Add setup_check() (#22177) 2026-11-02 21:22:44 -05:00
iskyguy ed4db4b20e [sht4x] Expose serial number as a text sensor (#22176) 2026-11-02 14:20:42 -10:00
iskyguy 51db039f59 [sht4x] Increase serial number response wait to 10 ms (#22183) 2026-11-02 13:45:30 -10:00
dependabot[bot] a24b994a5e Bump esphome/workflows/.github/workflows/lock.yml from 2026.10.2 to 2026.10.3 (#22168)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 16:05:58 -06:00
dependabot[bot] 912e9dedae Bump esphome/workflows/.github/workflows/stale.yml from 2026.10.2 to 2026.10.3 (#22169)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 16:05:44 -06:00
dependabot[bot] 47a526d35c Bump astral-sh/setup-uv from 10.3.2 to 10.3.3 in /.github/actions/restore-python (#22167)
Signed-off-by: dependabot[bot] <support@github.com>
2026-11-02 15:40:35 -06:00
Jesse Hills 389e782e78 Merge branch 'release' into dev 2026-11-03 10:30:47 +13:00
Jesse Hills 9a78022ded Merge pull request #22174 from esphome/bump-2026.10.2
2026.10.2
2026-11-03 10:30:22 +13:00
Jesse Hills c11c788d25 Revert "[lvgl] Make a scrollable container automatically clickable (#22094)"
This reverts commit 8e87cfc17e.
2026-11-03 10:11:19 +13:00
Clyde Stubbs d516fd5c92 [http_request] Fix macOS host build with the precompiled header (#21705) 2026-11-03 10:00:14 +13:00
Clyde StubbsandClaude 8e87cfc17e [lvgl] Make a scrollable container automatically clickable (#22094)
Co-authored-by: Claude <noreply@anthropic.com>
2026-11-03 09:59:38 +13:00
Jesse Hills f0c4dc67e7 Bump version to 2026.10.2 2026-11-03 09:51:12 +13:00
J. Nick Koston 863cb54982 [ota] Work around the LibreTiny OTA issue that bricks RTL8710B on a compressed update (#22111)
Squash of #22111 for the release branch, with the conflicts resolved:
release has no UpdateLock in the LibreTiny OTA backend, so abort() has no
lock check and a successful end() clears wrote_; release negotiates inline
in perform_ota(), so the RTL8710B check is made there.
2026-11-03 09:50:22 +13:00
J. Nick Koston 64b621f8f1 [bluetooth_proxy] Release held connections when a new client replaces the subscriber (#22077) 2026-11-03 09:43:51 +13:00
Jonathan Swoboda 7d29bdf177 [host] Offer the built program as a download in the Device Builder (#22114) 2026-11-03 09:43:51 +13:00
J. Nick Koston 86a0cdfb7a [core] Start the Apple Silicon cc1plus wrapper through sh when the Python path has a space (#21967) 2026-11-03 09:43:51 +13:00
cf5519615a [esp32][audio_http] Keep TLS 1.3 working (#21786)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-03 09:43:51 +13:00
dependabot[bot] 741310ef30 Bump prek from 0.5.7 to 0.5.9 (#22165) 2026-11-02 10:43:23 -10:00
dependabot[bot] f96c0c7d0e Bump CodSpeedHQ/action from 5.6.0 to 5.6.1 (#22166) 2026-11-02 10:40:54 -10:00
dependabot[bot] b18b7fe7a1 Bump astral-sh/setup-uv from 10.3.2 to 10.3.3 (#22170) 2026-11-02 10:40:34 -10:00
Jesse HillsandJ. Nick Koston 6ea4ed0868 [CI] Adopt gh actions-lock for workflow action pinning (#22138)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-11-03 08:50:25 +13:00
Jesse Hills a78a80c0d1 [epaper_spi] Fix SSD1681 partial updates alternating between two images (#22145) 2026-11-03 07:01:17 +13:00
Jakub SztanderaandJ. Nick Koston 4206de5ed0 [ledc] Correct PWM duty scaling (#22160)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-11-02 11:04:48 -06:00
J. Nick Koston ae54e8b696 [esp32_ble] Parse Bluedroid hex strings without sscanf (#22081) 2026-11-02 09:23:36 -06:00
J. Nick Kostonandpre-commit-ci-lite[bot] b252c90a5b [api] Add tests for Home Assistant state subscriptions (#22155)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-11-02 09:39:21 -05:00
J. Nick Koston d68674597e [const] Move shared charger config keys to components/const (#22161) 2026-11-02 09:34:38 -05:00
J. Nick Koston 57a5e54118 [uptime] Keep the text sensor format in flash (#22147) 2026-11-02 09:34:00 -05:00
J. Nick Koston 69d96a4707 [ota] Work around the LibreTiny OTA issue that bricks RTL8710B on a compressed update (#22111) 2026-11-02 04:33:18 -10:00
J. Nick Koston 52fedcbe97 [components] Log byte dumps with format_hex_pretty_to (#22149) 2026-11-02 09:01:31 -05:00
J. Nick Koston d2b635cd92 [uptime] Add unit tests for the uptime text sensor format (#22146) 2026-11-02 09:00:36 -05:00
J. Nick Koston 6d5305f1fe [ci] Show why a C++ unit test build raised (#22140) 2026-11-02 09:00:16 -05:00
esphome[bot] 354660cafc [ci] Refresh integration test durations (#22150) 2026-11-01 19:54:11 -10:00
chunckus-aurelius e5f6dbe6bf [motion] Add shared tap and double tap support (#22144) 2026-11-02 16:35:11 +11:00
Giorgio Aresu ab95d504fa [mipi_rgb] Update pins for ZX3D95CE01S-TR-4848 (#22134) 2026-11-02 11:55:40 +11:00
J. Nick Koston 605e7dc475 Merge branch 'dev' into sx1509-max6956-apply-outputs 2026-10-30 17:49:55 -10:00
J. Nick Koston e8dc7471a0 Merge remote-tracking branch 'origin/dev' into sx1509-max6956-apply-outputs
# Conflicts:
#	tests/component_tests/output/test_output.py
2026-10-30 16:04:53 -10:00
J. Nick Koston e2c7d1e29c Merge output-channel-key into sx1509-max6956-apply-outputs 2026-10-29 20:28:04 -10:00
J. Nick Koston d4e1c12d76 Merge dev into output-channel-key 2026-10-29 20:28:01 -10:00
J. Nick Koston 502c93df84 [sx1509] Expect the pin wording in the duplicate-pin test 2026-10-29 18:39:33 -10:00
J. Nick Koston 37cff473d0 Merge output-channel-key into sx1509-max6956-apply-outputs 2026-10-29 18:39:18 -10:00
J. Nick Koston 443c2caa0c [output] Name the channel key in the duplicate-channel error 2026-10-29 18:39:14 -10:00
J. Nick Koston 79d6752c97 Merge output-channel-key into sx1509-max6956-apply-outputs 2026-10-29 18:30:29 -10:00
J. Nick Koston c3146d8f6a [output] Let final_validate_unique_channel take the channel key 2026-10-29 18:28:57 -10:00
J. Nick Koston 45d565db01 [sx1509] Use Apply outputs for the LED channels
The LED driver outputs become output::ApplyFloatOutput sharing one apply
function per hub. Codegen registers each pin with the duty the old channel's
setup() wrote through turn_off(), and the hub sets the channels up at the end
of its own setup() in config order. Duplicate pins on one hub are rejected.
2026-10-29 18:19:01 -10:00
664 changed files with 19929 additions and 4921 deletions

No files matched your search

+2
View File
@@ -116,6 +116,8 @@ void add_on_state_callback(std::function<void(bool)> &&callback) {
|------|----------------------|-------------|
| `CallbackManager<void(Ts...)>` | 12 bytes (empty `std::vector`) | Callbacks are always or almost always registered |
| `LazyCallbackManager<void(Ts...)>` | 4 bytes (`nullptr`) | Callbacks are often not registered (common case) |
| `AutomationList<N, Ts...>` + `build_automation_lists` | 4 bytes per automation + count, none when unused | A hub fires its own `on_*` automations with no filter (4 B per automation vs 8 B per `Callback`) |
| `automation.new_automation(args, conf)` + the parent's own fixed table | the table entry (pointer plus its own data) | Like `AutomationList`, but each automation needs a stored value of its own, e.g. a threshold the parent compares against |
Both live in `esphome/core/helpers.h` and accept `std::function`, lambdas and forwarder structs through
their templatized `add()` method. `LazyCallbackManager` is a drop-in replacement for `CallbackManager` that defers allocation until the first
+27
View File
@@ -41,6 +41,12 @@ performance nit, because it fragments a small shared heap. Flag:
literal already lives in flash).
- `std::string` / `std::to_string` / string-returning helpers on hot paths where
a buffer or view API exists.
- A `set_timeout` / `set_interval` / `defer` lambda whose captures exceed 8 bytes
on 32-bit or are not trivially copyable (`this` plus two 4-byte values, `this`
plus a member function pointer, a `std::string`, a `std::function`):
`std::function` then heap-allocates each time it is scheduled. Flag it on
retries, re-armed timeouts and hot-path `defer`; a `set_interval` set up once
in `setup()` is a single allocation. `[this]` plus one 4-byte value fits.
## C++ conventions
@@ -69,6 +75,27 @@ performance nit, because it fragments a small shared heap. Flag:
typed as `std::function`, so lightweight forwarders avoid a heap allocation.
- Two-space indent, `using` over `typedef`, wrap at 120 columns.
## Use the existing helper
New components often hand-write code a helper already covers. Ask for the
helper; `AGENTS.md` section 4 and the `component-codegen` skill have the detail.
| Seen in the PR | Ask for |
| --- | --- |
| `if (this->x_sensor_ != nullptr) this->x_sensor_->publish_state(v);` | `PUBLISH_STATE(this->x_sensor_, v);` (`esphome/core/entity_base.h`) |
| set warning / `return` / clear warning around a bus result | `if (!this->status_comm_result(ok)) return;` |
| hand-written `set_x_sensor()` plus `x_sensor_` member | `SUB_SENSOR(x)`, `SUB_BINARY_SENSOR`, `SUB_TEXT_SENSOR`, `SUB_SWITCH`, ... |
| `if CONF_X in config: s = await sensor.new_sensor(config[CONF_X])` plus setter | `sensors = sensor.sub_sensors(config)` then `await sensors(CONF_X, hub.set_x_sensor)` |
| hand-written `{"a": MyEnum.MY_ENUM_A, ...}` dict | `MyEnum.options("a", "b", prefix="MY_ENUM_")` |
| loops on `available()` / `read_byte()` | UART `read_available(on_byte)`, `discard_available()`, `LineReader<N>`, `FrameReader<N, HEADER...>` |
| hand-rolled CRC, checksum or bit loop | `crc8`, `crc16`, `xor_bytes`, `sum16_bytes`, `encode_uint16`, ... (`esphome/core/helpers.h`) |
| integer split into buffer bytes by hand (`buf[0] = v >> 8; buf[1] = v;`) | `put_be16`/`24`/`32`, `put_le16`, `put_le32` (`esphome/core/helpers.h`) |
| bare `mark_failed()`, or `ESP_LOGE` then `mark_failed()` | `this->mark_failed(LOG_STR("reason"))` or `LOG_STR(ESP_LOG_MSG_COMM_FAIL)` |
| button/switch/number/select C++ class that only calls the hub | `ApplyButton`, `ApplySwitch`, `ApplyNumber`, `ApplySelect` with `apply=` |
| action or condition class that only forwards to the parent | `register_apply_action` / `register_apply_condition` |
| enum-to-string `switch` returning literals | `PROGMEM_STRING_TABLE` with `get_log_str` / `get_progmem_str` |
| `if (x := config.get(CONF_X)) is not None: cg.add(var.set_x(x))` | `cg.add_optional(var.set_x, config.get(CONF_X))`, when the setter takes the value unchanged |
## Python conventions
- Type-annotate every new function signature (params and return), new dataclass
+31 -3
View File
@@ -188,9 +188,37 @@ Restore an owned or standalone switch in `setup()` with `sw->restore()`, or `res
drive a state when the restore mode is disabled. A hub that applies its switches' restored states itself
reads `sw->get_initial_write_state()`, which already applies `inverted`.
**Entities that each watch a register field:** one class taking register, shift, mask and match from a
Python tuple table, in typed `StaticVector`s sized by `cg.slot_counter`; not a template per bit, not a
virtual listener base (a second vptr per entity).
**A hub that forwards data to its children:** when what each child does with the data is known at build time
(decode a register field, match a key or tag, publish a value), declare a `cg.HubFunction` once in the hub's
`__init__.py` and have each platform add one item per entity, built from codegen expressions. Code generation writes
one function per hub. Pass `define=` and put the hub's `HUB_PUBLISHER(<name>, args...)` (in a public section; it
ends on `public:`) and its call under that `#ifdef`; call the pointer only when it is not null. The children stay
plain entities, with no listener class or list. A child that lost a `Component` base keeps its dump lines through
a second hub function (`"dump"`, a `const char *` tag parameter, no define of its own) that the hub's
`dump_config()` calls inside the value function's `#ifdef`; every child adds to both.
**Entities that each watch a register field:** when every field (register, shift, mask, match) is known at
build time, use the rule above (see `bq25186`). A binary sensor tests its field value against an on-values
bitmap (`match`); a one-bit field reads the bit (simplified from bq25186's `_state_expression`):
```python
# hub __init__.py
STATUS_PUBLISHER = cg.HubFunction(
"status", # esphome__<hub>__status_fn, passed to HUB_PUBLISHER(status, ...)'s setter
cg.void,
[(BQ25186StatusRegisters.const_ref(), "status")],
define="USE_BQ25186_STATUS_PUBLISHER",
)
STATUS = STATUS_PUBLISHER.param("status")
# platform to_code, per configured entity
value = (STATUS[reg] >> shift) & mask
if mask != 0x01 or match != 0b10: # 0b10: a one-bit field that is on when set
value = (cg.MockObj(f"{match:#06b}") >> value) & 1
STATUS_PUBLISHER.add(hub, parent_ref(var).publish_state(value))
```
Store a field description per entity (a struct in a `StaticVector` sized by `cg.slot_counter`) only when the
field is chosen at run time, as in `sen6x`, where the status bit depends on the variant read in `setup()`.
Never a template per bit or a virtual listener base (a second vptr per entity).
## Other helpers
+5 -2
View File
@@ -58,8 +58,11 @@ APIs.
Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration.
**Listener and child-entity registration lists are the most common case, and the most commonly
missed.** A `register_*()` method called once per child at code generation time has a count that is
known at compile time, so it should never be a `std::vector`. Use `cg.slot_counter()`: it returns a
missed.** When the hub only forwards data to its children and each child's handling is known at build
time, keep no list at all: use `cg.HubFunction` (see the component-codegen skill). When the hub must
store the children (it iterates them for other reasons, or picks fields at run time), a `register_*()`
method called once per child at code generation time has a count that is known at compile time, so it
should never be a `std::vector`. Use `cg.slot_counter()`: it returns a
function that each consumer calls once per slot it will occupy, and after every `to_code` has run it
emits the define with the final count. When nothing registers, no define is emitted and the storage plus
its registration method compile out entirely.
+1 -1
View File
@@ -1,4 +1,4 @@
ARG BUILD_BASE_VERSION=2026.06.1
ARG BUILD_BASE_VERSION=2026.11.0
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base
+1 -1
View File
@@ -45,7 +45,7 @@ runs:
id: apt
# Pinned on purpose and not watched by Dependabot: the step above relies on how this
# release calls apt-fast and where it writes install.log.
uses: awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # v1.6.3
uses: awalsh128/cache-apt-pkgs-action@v1.6.3
with:
packages: libsdl2-dev libmbedtls-dev ccache
# 1.3 drops the empty caches saved before the step above existed
+2 -2
View File
@@ -42,7 +42,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
uses: docker/build-push-action@v7.4.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -67,7 +67,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
uses: docker/build-push-action@v7.4.0
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
+2 -2
View File
@@ -27,13 +27,13 @@ runs:
echo "key=$key" >> "$GITHUB_OUTPUT"
- name: Cache the native toolchain (write on dev)
if: github.ref == 'refs/heads/dev'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
- name: Restore the native toolchain (off dev)
if: github.ref != 'refs/heads/dev'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.ESPHOME_ARDUINO8266_PREFIX }}
key: ${{ runner.os }}-esp8266-native-${{ steps.version.outputs.key }}
@@ -38,13 +38,13 @@ runs:
# Extend the extension list if a component ships extensionless headers.
- name: Cache clang-tidy idedata (write on dev)
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && steps.key.outputs.skip != 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: ${{ steps.key.outputs.path }}
key: ${{ steps.key.outputs.key }}
- name: Cache clang-tidy idedata (restore-only off dev)
if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') && steps.key.outputs.skip != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ steps.key.outputs.path }}
key: ${{ steps.key.outputs.key }}
+3 -3
View File
@@ -49,13 +49,13 @@ runs:
# -slim: bump when prune-esp-idf changes what it removes; a key is never overwritten.
- name: Cache ESP-IDF install (write on dev)
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && inputs.restore-only != 'true'
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache@v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim
- name: Cache ESP-IDF install (restore-only off dev)
if: github.ref != 'refs/heads/dev' && !contains(github.event.pull_request.labels.*.name, 'ci-cache-write') || inputs.restore-only == 'true'
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
uses: actions/cache/restore@v5.0.5
with:
path: ~/.esphome-idf
key: ${{ runner.os }}-esphome-idf-${{ steps.version.outputs.version }}-py${{ steps.version.outputs.python-version }}-slim
@@ -67,4 +67,4 @@ runs:
. venv/bin/activate
python -c 'from esphome.espidf.framework import check_esp_idf_install; check_esp_idf_install("${{ steps.version.outputs.version }}")'
- name: Prune ESP-IDF install
uses: ./.github/actions/prune-esp-idf
uses: $/.github/actions/prune-esp-idf
+2 -2
View File
@@ -35,14 +35,14 @@ runs:
# restore-only and never push multi-GB artifacts into their own scope.
- name: Cache nRF Connect SDK install (write on dev)
if: github.ref == 'refs/heads/dev' && inputs.restore-only != 'true'
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: ~/.esphome-sdk-nrf
# yamllint disable-line rule:line-length
key: ${{ runner.os }}-esphome-sdk-nrf-${{ steps.version.outputs.version }}-${{ hashFiles('esphome/components/nrf52/requirements.txt') }}
- name: Cache nRF Connect SDK install (restore-only off dev)
if: github.ref != 'refs/heads/dev' || inputs.restore-only == 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ~/.esphome-sdk-nrf
# yamllint disable-line rule:line-length
+3 -3
View File
@@ -17,12 +17,12 @@ runs:
steps:
- name: Set up Python ${{ inputs.python-version }}
id: python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: ${{ inputs.python-version }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: venv
# yamllint disable-line rule:line-length
@@ -32,7 +32,7 @@ runs:
# detects the activated venv via ``VIRTUAL_ENV`` so the venv layout
# downstream jobs rely on is preserved.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
+232
View File
@@ -0,0 +1,232 @@
# This file is machine-generated by `gh actions-lock`.
# Do not edit by hand; run `gh actions-lock` to update.
# Docs: https://gh.io/actions-lockfile
version: 'v0.0.3'
workflows:
'.github/workflows/auto-label-pr.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/github-script@v9.0.0'
'.github/workflows/ci-api-proto.yml':
- 'actions/checkout@v7.0.1'
- 'actions/github-script@v9.0.0'
- 'actions/setup-python@v7.1.0'
- 'actions/upload-artifact@v7.0.2'
- 'astral-sh/setup-uv@v10.3.3'
'.github/workflows/ci-docker.yml':
- 'actions/checkout@v7.0.1'
- 'actions/download-artifact@v8.0.2'
- 'actions/setup-python@v7.1.0'
- 'actions/upload-artifact@v7.0.2'
- 'docker/login-action@v4.7.0'
- 'docker/setup-buildx-action@v4.4.1'
'.github/workflows/ci-github-scripts.yml':
- 'actions/checkout@v7.0.1'
'.github/workflows/ci-memory-impact-comment.yml':
- 'actions/cache@v6.1.0'
- 'actions/checkout@v7.0.1'
- 'actions/setup-python@v7.1.0'
- 'astral-sh/setup-uv@v10.3.3'
'.github/workflows/ci.yml':
- 'actions/cache@v5.0.5'
- 'actions/cache@v6.1.0'
- 'actions/checkout@v7.0.1'
- 'actions/download-artifact@v8.0.2'
- 'actions/setup-python@v7.1.0'
- 'actions/upload-artifact@v7.0.2'
- 'astral-sh/setup-uv@v10.3.3'
- 'awalsh128/cache-apt-pkgs-action@v1.6.3'
- 'codecov/codecov-action@v7.1.1'
- 'codspeedhq/action@v5.6.1'
- 'j178/prek-action@v3.1.0'
- 'pre-commit-ci/lite-action@v1.2.0'
'.github/workflows/close-pr-from-fork-default-branch.yml':
- 'actions/github-script@v9.0.0'
'.github/workflows/codeowner-approved-label-update.yml':
- 'actions/checkout@v7.0.1'
- 'actions/github-script@v9.0.0'
'.github/workflows/codeowner-review-request.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/github-script@v9.0.0'
'.github/workflows/codeql.yml':
- 'actions/checkout@v7.0.1'
- 'github/codeql-action@v4.39.1'
'.github/workflows/external-component-bot.yml':
- 'actions/create-github-app-token@v3.3.0'
- 'actions/github-script@v9.0.0'
'.github/workflows/issue-codeowner-notify.yml':
- 'actions/github-script@v9.0.0'
'.github/workflows/lock.yml': []
'.github/workflows/pr-title-check.yml':
- 'actions/checkout@v7.0.1'
- 'actions/github-script@v9.0.0'
'.github/workflows/release-nightly.yml':
- 'actions/checkout@v7.0.1'
'.github/workflows/release.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/download-artifact@v8.0.2'
- 'actions/github-script@v9.0.0'
- 'actions/setup-python@v7.1.0'
- 'actions/upload-artifact@v7.0.2'
- 'docker/build-push-action@v7.4.0'
- 'docker/login-action@v4.7.0'
- 'docker/setup-buildx-action@v4.4.1'
- 'pypa/gh-action-pypi-publish@v1.14.3'
'.github/workflows/stale.yml': []
'.github/workflows/status-check-labels.yml':
- 'actions/github-script@v9.0.0'
'.github/workflows/sync-dependency-versions.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/setup-python@v7.1.0'
'.github/workflows/sync-device-classes.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/setup-python@v7.1.0'
- 'astral-sh/setup-uv@v10.3.3'
- 'peter-evans/create-pull-request@v8.1.1'
'.github/workflows/sync-integration-durations.yml':
- 'actions/checkout@v7.0.1'
- 'actions/create-github-app-token@v3.3.0'
- 'actions/setup-python@v7.1.0'
- 'peter-evans/create-pull-request@v8.1.1'
'.github/workflows/verify-lockfile.yml':
- 'actions/checkout@v7.0.1'
dependencies:
'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9':
ref: 'v6.1.0'
commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
owner_id: 44036562
repo_id: 215566462
'actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7':
ref: 'v5.0.4'
commit: 'sha1-668228422ae6a00e4ad889ee87cd7109ec5666a7'
owner_id: 44036562
repo_id: 215566462
'actions/cache@v5.0.5':
ref: 'v5.0.5'
commit: 'sha1-27d5ce7f107fe9357f9df03efb73ab90386fccae'
owner_id: 44036562
repo_id: 215566462
'actions/cache@v6.1.0':
ref: 'v6.1.0'
commit: 'sha1-55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
owner_id: 44036562
repo_id: 215566462
'actions/checkout@v7.0.1':
ref: 'v7.0.1'
commit: 'sha1-3d3c42e5aac5ba805825da76410c181273ba90b1'
owner_id: 44036562
repo_id: 197814629
'actions/create-github-app-token@v3.3.0':
ref: 'v3.3.0'
commit: 'sha1-64ab729a6294000f5aa4a4bc0b73d1f32bc45510'
owner_id: 44036562
repo_id: 642580244
'actions/download-artifact@v8.0.2':
ref: 'v8.0.2'
commit: 'sha1-9000827ccba6bdab643e8b6fd33ac0654aef8333'
owner_id: 44036562
repo_id: 192626254
'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd':
ref: 'v8.0.0'
commit: 'sha1-ed597411d8f924073f98dfc5c65a23a2325f34cd'
owner_id: 44036562
repo_id: 205262760
'actions/github-script@v9.0.0':
ref: 'v9.0.0'
commit: 'sha1-3a2844b7e9c422d3c10d287c895573f7108da1b3'
owner_id: 44036562
repo_id: 205262760
'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405':
ref: 'v6.2.0'
commit: 'sha1-a309ff8b426b58ec0e2a45f0f869d46889d02405'
owner_id: 44036562
repo_id: 192625525
'actions/setup-python@v7.1.0':
ref: 'v7.1.0'
commit: 'sha1-6d4fd22b20465ccc0f7354476588e8df7c9ca0e0'
owner_id: 44036562
repo_id: 192625525
'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f':
ref: 'v7.0.0'
commit: 'sha1-bbbca2ddaa5d8feaa63e36b76fdaad77386f024f'
owner_id: 44036562
repo_id: 192625955
'actions/upload-artifact@v7.0.2':
ref: 'v7.0.2'
commit: 'sha1-cf430e030ddbb5b0abf93d22962f4752f3646cd9'
owner_id: 44036562
repo_id: 192625955
'astral-sh/setup-uv@v10.3.3':
ref: 'v10.3.3'
commit: 'sha1-68559e99cc807c1eb7e9238fd43a15bba460eec2'
owner_id: 115962839
repo_id: 846733598
'awalsh128/cache-apt-pkgs-action@v1.6.3':
ref: 'v1.6.3'
commit: 'sha1-553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9'
owner_id: 2087466
repo_id: 408212054
uses:
- 'actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9'
- 'actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f'
'codecov/codecov-action@v7.1.1':
ref: 'v7.1.1'
commit: 'sha1-303a32d7a59b442fa8d48b6a1cc6825c09c847a5'
owner_id: 8226205
repo_id: 200299178
uses:
- 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd'
'codspeedhq/action@v5.6.1':
ref: 'v5.6.1'
commit: 'sha1-61efe07e57e55bedeab750c434504de272a56e8c'
owner_id: 116658140
repo_id: 561699778
uses:
- 'actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7'
- 'actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd'
'docker/build-push-action@v7.4.0':
ref: 'v7.4.0'
commit: 'sha1-c3c9e263c25d99ce0380d002d59b67737d91b0dc'
owner_id: 5429470
repo_id: 241092383
'docker/login-action@v4.7.0':
ref: 'v4.7.0'
commit: 'sha1-81c6f93d4bc991e510b342a00b419b3bb4efc5c6'
owner_id: 5429470
repo_id: 287743349
'docker/setup-buildx-action@v4.4.1':
ref: 'v4.4.1'
commit: 'sha1-f87e5991a6d7451dcb8d9637bfbc97413f497069'
owner_id: 5429470
repo_id: 288485773
'github/codeql-action@v4.39.1':
ref: 'v4.39.1'
commit: 'sha1-9b3254085494b55a0f7c4c2b3d2d52cc712ce370'
owner_id: 9919
repo_id: 259445878
'j178/prek-action@v3.1.0':
ref: 'v3.1.0'
commit: 'sha1-44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d'
owner_id: 10510431
repo_id: 1052387486
'peter-evans/create-pull-request@v8.1.1':
ref: 'v8.1.1'
commit: 'sha1-5f6978faf089d4d20b00c7766989d076bb2fc7f1'
owner_id: 18365890
repo_id: 197176365
'pre-commit-ci/lite-action@v1.2.0':
ref: 'v1.2.0'
commit: 'sha1-062bca0919bc9d6e66755cc05074b70c77e111fc'
owner_id: 64617429
repo_id: 560608915
'pypa/gh-action-pypi-publish@v1.14.3':
ref: 'v1.14.3'
commit: 'sha1-7245906fb61fa33a13554f9d5fb856fe61758086'
owner_id: 647025
repo_id: 178055147
uses:
- 'actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405'
+3 -3
View File
@@ -24,11 +24,11 @@ jobs:
if: github.event.pull_request.state == 'open' && (github.event.action != 'labeled' || github.event.sender.type != 'Bot')
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -38,7 +38,7 @@ jobs:
permission-pull-requests: write # pulls.get, pulls.listFiles, list/create/update/dismissReview
- name: Auto Label PR
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
+6 -6
View File
@@ -21,15 +21,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.12"
- name: Set up uv
# ``--system`` (below) installs into the setup-python interpreter;
# no venv is created or restored by this workflow.
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pull-request-only workflow: a save could never be shared and
@@ -82,7 +82,7 @@ jobs:
fi
- if: failure()
name: Review PR
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
script: |
await github.rest.pulls.createReview({
@@ -97,7 +97,7 @@ jobs:
run: git diff
- if: failure()
name: Archive artifacts
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: generated-proto-files
path: |
@@ -105,7 +105,7 @@ jobs:
esphome/components/api/api_pb2_service.*
- if: success()
name: Dismiss review
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
script: |
let reviews = await github.rest.pulls.listReviews({
+11 -11
View File
@@ -60,13 +60,13 @@ jobs:
tag: ${{ steps.tag.outputs.tag }}
push: ${{ steps.tag.outputs.push }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@v4.4.1
- name: Determine tag and whether to push
id: tag
@@ -97,7 +97,7 @@ jobs:
- name: Log in to the GitHub container registry
if: steps.tag.outputs.push == 'true'
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -128,7 +128,7 @@ jobs:
- name: Upload compile-test image artifact
if: matrix.os == 'ubuntu-24.04' && matrix.build_type == 'docker'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
# The tar is already compressed, so upload it as-is. archive: false
# skips the redundant zip and makes the file name the artifact name
@@ -152,16 +152,16 @@ jobs:
- "ha-addon"
- "docker"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@v4.4.1
- name: Log in to the GitHub container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -208,9 +208,9 @@ jobs:
- nrf52
- host
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Download image artifact
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
uses: actions/download-artifact@v8.0.2
with:
name: compile-test-image.tar.zst
- name: Load image
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Run tests
working-directory: .github/scripts/auto-label-pr
@@ -49,7 +49,7 @@ jobs:
- name: Check out code from base repository
if: steps.pr.outputs.skip != 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Always check out from the base repository (esphome/esphome), never from forks
# Use the PR's target branch to ensure we run trusted code from the main repo
@@ -58,7 +58,7 @@ jobs:
- name: Restore Python
if: steps.pr.outputs.skip != 'true'
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: "3.12"
cache-key: ${{ hashFiles('.cache-key') }}
+104 -104
View File
@@ -28,18 +28,18 @@ jobs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: venv
# yamllint disable-line rule:line-length
@@ -49,7 +49,7 @@ jobs:
# detects the activated venv via ``VIRTUAL_ENV`` so downstream jobs
# that ``. venv/bin/activate`` see an identical layout.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -79,11 +79,11 @@ jobs:
timeout-minutes: 10
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
sparse-checkout: .github
- name: Install apt packages (cached)
uses: ./.github/actions/apt-packages
uses: $/.github/actions/apt-packages
determine-jobs:
name: Determine which jobs to run
@@ -125,17 +125,17 @@ jobs:
release-pr: ${{ github.base_ref == 'beta' || github.base_ref == 'release' }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Fetch enough history to find the merge base
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Restore components graph cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -185,7 +185,7 @@ jobs:
echo "benchmarks=$(echo "$output" | jq -r '.benchmarks')" >> $GITHUB_OUTPUT
- name: Save components graph cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@v6.1.0
with:
path: .temp/components_graph.json
key: components-graph-${{ hashFiles('esphome/components/**/*.py') }}
@@ -204,14 +204,14 @@ jobs:
timeout-minutes: 15
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native toolchain
uses: ./.github/actions/cache-arduino8266
uses: $/.github/actions/cache-arduino8266
- name: Install the native toolchain
run: |
. venv/bin/activate
@@ -226,9 +226,9 @@ jobs:
if: needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -255,9 +255,9 @@ jobs:
if: needs.determine-jobs.outputs.python-linters == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -277,7 +277,7 @@ jobs:
if: github.event_name == 'pull_request' && needs.determine-jobs.outputs.release-pr == 'false' && needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Read prek version from requirements_test.txt
id: prek
# requirements_test.txt is the only place the version is pinned, so a
@@ -289,7 +289,7 @@ jobs:
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Run prek
uses: j178/prek-action@44924fe9ef4a6f88253c4b6b1bf11c689d3ce53d # v3.1.0
uses: j178/prek-action@v3.1.0
with:
prek-version: ${{ steps.prek.outputs.version }}
# This job only runs on pull requests, so nothing ever populates
@@ -302,7 +302,7 @@ jobs:
# Pushes any fixes the hooks made back to the pull request. This step
# must keep its default name: the GitHub App that performs the push
# locates the workflow run by that name.
- uses: pre-commit-ci/lite-action@062bca0919bc9d6e66755cc05074b70c77e111fc # v1.2.0
- uses: pre-commit-ci/lite-action@v1.2.0
if: always()
with:
msg: apply automatic formatting fixes
@@ -336,10 +336,10 @@ jobs:
if: needs.determine-jobs.outputs.core-ci == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ matrix.python-version }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -356,10 +356,10 @@ jobs:
. venv/bin/activate
pytest -vv --cov-report=xml --tb=native --durations=30 -n auto tests --ignore=tests/integration/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
uses: codecov/codecov-action@v7.1.1
- name: Save Python virtual environment cache
if: github.ref == 'refs/heads/dev'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@v6.1.0
with:
path: venv
key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
@@ -383,9 +383,9 @@ jobs:
if: needs.determine-jobs.outputs.core-ci == 'false'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Report empty upload to Codecov
uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1
uses: codecov/codecov-action@v7.1.1
with:
run_command: empty-upload
force: true
@@ -416,23 +416,23 @@ jobs:
INTEGRATION_CCACHE_SAVE: ${{ needs.determine-jobs.outputs.integration-run-all == 'true' && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Install apt packages (cached)
# ccache speeds up the host compiles. A cache hit never touches apt
# (mirror outages cannot hang the job); the timeout bounds the cold
# path. libsdl2-dev is needed by the headless display tests, which
# capture screenshots.
timeout-minutes: 10
uses: ./.github/actions/apt-packages
uses: $/.github/actions/apt-packages
- name: Set up Python 3.13
id: python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.13"
- name: Restore integration library cache
# A miss or a changed pin self-heals with a download, so no restore-keys
id: library-cache
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_LIBRARY_CACHE_PATH }}
key: integration-libraries-v1-${{ runner.os }}-${{ hashFiles('esphome/components/json/__init__.py', 'esphome/components/noise/__init__.py', 'esphome/components/lvgl/__init__.py', 'esphome/components/improv_base/__init__.py') }}
@@ -441,35 +441,35 @@ jobs:
# per slice of INTEGRATION_TESTS_SPLIT_BUCKETS in script/determine-jobs.py.
- name: Restore integration ccache (slice 1/5)
if: needs.determine-jobs.outputs.integration-run-all != 'true' || strategy.job-index == 0
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-0-${{ github.run_id }}
restore-keys: integration-ccache-v1-0-
- name: Restore integration ccache (slice 2/5)
if: needs.determine-jobs.outputs.integration-run-all != 'true' || strategy.job-index == 1
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-1-${{ github.run_id }}
restore-keys: integration-ccache-v1-1-
- name: Restore integration ccache (slice 3/5)
if: needs.determine-jobs.outputs.integration-run-all != 'true' || strategy.job-index == 2
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-2-${{ github.run_id }}
restore-keys: integration-ccache-v1-2-
- name: Restore integration ccache (slice 4/5)
if: needs.determine-jobs.outputs.integration-run-all != 'true' || strategy.job-index == 3
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-3-${{ github.run_id }}
restore-keys: integration-ccache-v1-3-
- name: Restore integration ccache (slice 5/5)
if: needs.determine-jobs.outputs.integration-run-all != 'true' || strategy.job-index == 4
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-4-${{ github.run_id }}
@@ -486,14 +486,14 @@ jobs:
CCACHE_DIR="$dir" ccache -z
- name: Restore Python virtual environment
id: cache-venv
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: venv
key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
- name: Set up uv
# Only needed on cache miss to populate the venv.
if: steps.cache-venv.outputs.cache-hit != 'true'
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -560,7 +560,7 @@ jobs:
# Tests that compare rendered output write the image they actually got here, so a
# failure can be looked at without reproducing the whole build locally.
if: failure()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: integration-test-artifacts-${{ matrix.bucket.name }}
path: test_artifacts/
@@ -571,7 +571,7 @@ jobs:
# script/update_integration_test_durations.py; only full matrix dev
# runs produce usable data.
if: github.ref == 'refs/heads/dev' && needs.determine-jobs.outputs.integration-run-all == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: junit-integration-${{ strategy.job-index }}
path: junit-integration.xml
@@ -587,14 +587,14 @@ jobs:
run: ccache -s
- name: Save integration ccache slice
if: success() && env.INTEGRATION_CCACHE_SAVE == 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@v6.1.0
with:
path: ${{ env.INTEGRATION_CCACHE_PATH }}
key: integration-ccache-v1-${{ strategy.job-index }}-${{ github.run_id }}
- name: Save integration library cache
# Bucket 0 only; the others would race the same immutable key.
if: success() && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write')) && strategy.job-index == 0 && steps.library-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@v6.1.0
with:
path: ${{ env.INTEGRATION_LIBRARY_CACHE_PATH }}
key: ${{ steps.library-cache.outputs.cache-primary-key }}
@@ -643,9 +643,9 @@ jobs:
if: needs.determine-jobs.outputs.import-time == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -655,7 +655,7 @@ jobs:
script/check_import_time.py --check --har importtime.har
- name: Upload waterfall HAR
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: import-time-waterfall
path: importtime.har
@@ -689,10 +689,10 @@ jobs:
# have already been benchmarked on their original dev pull requests.
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -753,7 +753,7 @@ jobs:
apt-get install -y libc6-dbg
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@78102a468f96d8e0ccb92d0ec49425753fc42062 # v5.6.0
uses: CodSpeedHQ/action@v5.6.1
with:
run: |
. venv/bin/activate
@@ -770,10 +770,10 @@ jobs:
if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -834,7 +834,7 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
@@ -843,11 +843,11 @@ jobs:
# libsdl2-dev is needed by the sdl component on the host, libmbedtls-dev
# by dsmr and dlms_meter.
timeout-minutes: 10
uses: ./.github/actions/apt-packages
uses: $/.github/actions/apt-packages
- name: Restore Python
id: restore-python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -858,33 +858,33 @@ jobs:
# a new image ships a newer patch release and drops the old interpreter.
- name: Cache platformio
if: github.ref == 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@v6.1.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ steps.restore-python.outputs.python-version }}-${{ hashFiles('platformio.ini') }}
- name: Cache platformio
if: github.ref != 'refs/heads/dev' && matrix.pio_cache_key
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: ~/.platformio
key: platformio-${{ matrix.pio_cache_key }}-${{ steps.restore-python.outputs.python-version }}-${{ hashFiles('platformio.ini') }}
- name: Cache ESP-IDF install
if: matrix.cache_idf
uses: ./.github/actions/cache-esp-idf
uses: $/.github/actions/cache-esp-idf
with:
framework: arduino
- name: Cache clang-tidy idedata
if: matrix.cache_idf
uses: ./.github/actions/cache-clang-tidy-idedata
uses: $/.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-arduino-tidy
- name: Cache nRF Connect SDK install
if: matrix.cache_sdk_nrf
uses: ./.github/actions/cache-sdk-nrf
uses: $/.github/actions/cache-sdk-nrf
- name: Register problem matchers
run: |
@@ -933,7 +933,7 @@ jobs:
- name: Prune ESP-IDF install before cache save
if: matrix.cache_idf && (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
uses: $/.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes ${{ matrix.ignore_errors && '|| true' || '' }}
@@ -953,22 +953,22 @@ jobs:
ESPHOME_ESP_IDF_PREFIX: ~/.esphome-idf
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
uses: $/.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
uses: $/.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-idf-tidy
@@ -1008,7 +1008,7 @@ jobs:
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
uses: $/.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
@@ -1047,22 +1047,22 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
uses: $/.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
uses: $/.github/actions/cache-clang-tidy-idedata
with:
environment: esp32-idf-tidy
@@ -1102,7 +1102,7 @@ jobs:
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
uses: $/.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
@@ -1143,22 +1143,22 @@ jobs:
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
# Need history for HEAD~1 to work for checking changed files
fetch-depth: 2
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache ESP-IDF install
uses: ./.github/actions/cache-esp-idf
uses: $/.github/actions/cache-esp-idf
- name: Cache clang-tidy idedata
uses: ./.github/actions/cache-clang-tidy-idedata
uses: $/.github/actions/cache-clang-tidy-idedata
with:
environment: ${{ matrix.tidy_environment }}
@@ -1197,7 +1197,7 @@ jobs:
- name: Prune ESP-IDF install before cache save
if: (github.ref == 'refs/heads/dev' || contains(github.event.pull_request.labels.*.name, 'ci-cache-write'))
uses: ./.github/actions/prune-esp-idf
uses: $/.github/actions/prune-esp-idf
- name: Suggested changes
run: script/ci-suggest-changes
@@ -1231,7 +1231,7 @@ jobs:
run: echo ${{ matrix.batch.components }}
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Install apt packages (cached)
# A cache hit (seeded on dev by seed-apt-cache) never touches apt,
@@ -1239,9 +1239,9 @@ jobs:
# the cold path. The action has no --no-install-recommends; same
# package set this job used before #17463.
timeout-minutes: 10
uses: ./.github/actions/apt-packages
uses: $/.github/actions/apt-packages
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -1250,7 +1250,7 @@ jobs:
# ESP-IDF install; never save -- just reuse the shared install the
# dev tidy jobs already cached when present.
if: matrix.batch.needs_idf
uses: ./.github/actions/cache-esp-idf
uses: $/.github/actions/cache-esp-idf
with:
restore-only: true
- name: Cache nRF Connect SDK install (restore-only)
@@ -1258,14 +1258,14 @@ jobs:
# sdk-nrf install; never save -- just reuse the shared install the
# dev nrf52 tidy job cached when present.
if: matrix.batch.needs_nrf
uses: ./.github/actions/cache-sdk-nrf
uses: $/.github/actions/cache-sdk-nrf
with:
restore-only: true
- name: Cache the native ESP8266 toolchain
# Only batches whose test platforms include esp8266; never saves
# here, it reuses the install the dev seed job cached.
if: matrix.batch.needs_arduino8266
uses: ./.github/actions/cache-arduino8266
uses: $/.github/actions/cache-arduino8266
- name: Validate and compile components with intelligent grouping
run: |
. venv/bin/activate
@@ -1408,10 +1408,10 @@ jobs:
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp32-platformio-components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -1446,16 +1446,16 @@ jobs:
TEST_COMPONENTS: ${{ needs.determine-jobs.outputs.esp8266-native-components }}
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native toolchain
uses: ./.github/actions/cache-arduino8266
uses: $/.github/actions/cache-arduino8266
- name: Run native toolchain compile test
run: |
@@ -1495,10 +1495,10 @@ jobs:
CCACHE_SHA256: 2568347a697e103ca1b073981c704ad76fb2507d066c38dba038dd73399d968f
steps:
- name: Check out code from GitHub
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
@@ -1622,17 +1622,17 @@ jobs:
if: needs.determine-jobs.outputs.device-builder == 'true'
steps:
- name: Check out esphome (this PR)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
path: esphome
- name: Check out esphome/device-builder
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
repository: esphome/device-builder
ref: main
path: device-builder
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.13"
- name: Set up uv
@@ -1641,7 +1641,7 @@ jobs:
# install step (order-of-magnitude faster on cold boots,
# with its own wheel cache). actions/setup-python still
# provides the interpreter.
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pull request saves land in per-PR scopes nothing else can
@@ -1684,7 +1684,7 @@ jobs:
skip: ${{ steps.check-script.outputs.skip || steps.check-tests.outputs.skip }}
steps:
- name: Check out target branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.base_ref }}
@@ -1756,7 +1756,7 @@ jobs:
- name: Restore cached memory analysis
id: cache-memory-analysis
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true'
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/restore@v6.1.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -1773,14 +1773,14 @@ jobs:
- name: Restore Python
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native ESP8266 toolchain
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266
uses: ./.github/actions/cache-arduino8266
uses: $/.github/actions/cache-arduino8266
- name: Build, compile, and analyze memory
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
@@ -1819,7 +1819,7 @@ jobs:
- name: Save memory analysis to cache
if: steps.check-script.outputs.skip != 'true' && steps.check-tests.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache/save@v6.1.0
with:
path: memory-analysis-target.json
key: ${{ steps.cache-key.outputs.cache-key }}
@@ -1840,7 +1840,7 @@ jobs:
fi
- name: Upload memory analysis JSON
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: memory-analysis-target
path: memory-analysis-target.json
@@ -1859,15 +1859,15 @@ jobs:
flash_usage: ${{ steps.extract.outputs.flash_usage }}
steps:
- name: Check out PR branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Cache the native ESP8266 toolchain
if: fromJSON(needs.determine-jobs.outputs.memory_impact).needs_arduino8266
uses: ./.github/actions/cache-arduino8266
uses: $/.github/actions/cache-arduino8266
- name: Build, compile, and analyze memory
id: extract
run: |
@@ -1903,7 +1903,7 @@ jobs:
--platform "$platform"
- name: Upload memory analysis JSON
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: memory-analysis-pr
path: memory-analysis-pr.json
@@ -1926,20 +1926,20 @@ jobs:
GH_TOKEN: ${{ github.token }}
steps:
- name: Check out code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Restore Python
uses: ./.github/actions/restore-python
uses: $/.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Download target analysis JSON
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
uses: actions/download-artifact@v8.0.2
with:
name: memory-analysis-target
path: ./memory-analysis
continue-on-error: true
- name: Download PR analysis JSON
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
uses: actions/download-artifact@v8.0.2
with:
name: memory-analysis-pr
path: ./memory-analysis
@@ -17,7 +17,7 @@ jobs:
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
&& github.event.pull_request.head.ref == github.event.repository.default_branch
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- uses: actions/github-script@v9.0.0
with:
script: |
const { owner, repo } = context.repo;
@@ -26,7 +26,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
sparse-checkout: |
@@ -34,7 +34,7 @@ jobs:
CODEOWNERS
- name: Check codeowner approval and update label
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
with:
@@ -29,13 +29,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout base branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -44,7 +44,7 @@ jobs:
permission-issues: write # issues.listComments and issues.createComment (PR comments use the issues API)
- name: Request reviews from component codeowners
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
+3 -3
View File
@@ -52,11 +52,11 @@ jobs:
# your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/init@v4.39.1
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -84,6 +84,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1
uses: github/codeql-action/analyze@v4.39.1
with:
category: "/language:${{matrix.language}}"
+2 -2
View File
@@ -15,7 +15,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -24,7 +24,7 @@ jobs:
permission-pull-requests: write
- name: Add external component comment
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Notify codeowners for component issues
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
script: |
const owner = context.repo.owner;
+1 -1
View File
@@ -14,4 +14,4 @@ jobs:
permissions:
issues: write # issues.lock on closed issues
pull-requests: write # issues.lock on closed pull requests
uses: esphome/workflows/.github/workflows/lock.yml@31d160e5f0ffa0ee65d5b27cb55c153d415f2abe # 2026.10.2
uses: esphome/workflows/.github/workflows/lock.yml@e1432c7b31fec7d24a13dc192953ebac836ed98a # 2026.10.3
+2 -2
View File
@@ -16,9 +16,9 @@ jobs:
name: Validate PR title
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
- uses: actions/github-script@v9.0.0
with:
script: |
const {
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
actions: write # gh workflow run starts release.yml
steps:
- name: Check out the repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Start the release workflow
env:
+19 -19
View File
@@ -31,7 +31,7 @@ jobs:
branch_build: ${{ steps.tag.outputs.branch_build }}
deploy_env: ${{ steps.tag.outputs.deploy_env }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Get tag
id: tag
env:
@@ -84,9 +84,9 @@ jobs:
contents: read # actions/checkout to build the sdist/wheel
id-token: write # OIDC token for PyPI Trusted Publishing (pypa/gh-action-pypi-publish)
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.x"
- name: Build
@@ -94,7 +94,7 @@ jobs:
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
uses: pypa/gh-action-pypi-publish@v1.14.3
with:
skip-existing: true
@@ -116,29 +116,29 @@ jobs:
os: "ubuntu-24.04-arm"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.12"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@v4.4.1
- name: Log in to docker hub
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build docker
uses: ./.github/actions/build-image
uses: $/.github/actions/build-image
with:
target: final
build_type: docker
@@ -146,7 +146,7 @@ jobs:
version: ${{ needs.init.outputs.tag }}
- name: Build ha-addon
uses: ./.github/actions/build-image
uses: $/.github/actions/build-image
with:
target: final
build_type: ha-addon
@@ -162,7 +162,7 @@ jobs:
# version: ${{ needs.init.outputs.tag }}
- name: Upload digests
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
uses: actions/upload-artifact@v7.0.2
with:
name: digests-${{ matrix.platform.arch }}
path: /tmp/digests
@@ -192,27 +192,27 @@ jobs:
- ghcr
- dockerhub
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@v7.0.1
- name: Download digests
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
uses: actions/download-artifact@v8.0.2
with:
pattern: digests-*
path: /tmp/digests
merge-multiple: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
uses: docker/setup-buildx-action@v4.4.1
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
if: matrix.registry == 'ghcr'
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
uses: docker/login-action@v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -245,7 +245,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -254,7 +254,7 @@ jobs:
permission-actions: write # actions.createWorkflowDispatch on the target repo (only API call made with this token)
- name: Trigger Workflow
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
github-token: ${{ steps.generate-token.outputs.token }}
script: |
+1 -1
View File
@@ -16,7 +16,7 @@ jobs:
# No GITHUB_TOKEN permissions: the reusable workflow mints an ESPHome
# GitHub App token so the labels, comments and closures come from
# esphome[bot] instead of github-actions[bot].
uses: esphome/workflows/.github/workflows/stale.yml@31d160e5f0ffa0ee65d5b27cb55c153d415f2abe # main
uses: esphome/workflows/.github/workflows/stale.yml@e1432c7b31fec7d24a13dc192953ebac836ed98a # main
secrets:
ESPHOME_GITHUB_APP_PRIVATE_KEY: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
with:
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check for blocking labels
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
uses: actions/github-script@v9.0.0
with:
script: |
const blockingLabels = ['needs-docs', 'needs-developer-docs', 'merge-after-release', 'chained-pr'];
@@ -43,7 +43,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -54,7 +54,7 @@ jobs:
- name: Check out base branch
# Provides the script that runs below. Deliberately the base branch
# so the pull request cannot change what executes here.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.event.pull_request.base.sha }}
persist-credentials: false
@@ -64,14 +64,14 @@ jobs:
# refuses heads that live in a different repository, and the job
# condition above already limits runs to same-repository branches.
# Leaving it off keeps that refusal as a backstop for fork heads.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
ref: ${{ github.event.pull_request.head.ref }}
path: pull-request
token: ${{ steps.generate-token.outputs.token }}
- name: Set up Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.12"
+6 -6
View File
@@ -19,7 +19,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -28,16 +28,16 @@ jobs:
permission-pull-requests: write # pulls.create / pulls.update to open or refresh the sync PR
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Checkout Home Assistant
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
with:
repository: home-assistant/core
path: lib/home-assistant
- name: Setup Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.14"
@@ -47,7 +47,7 @@ jobs:
# setup-python interpreter so subsequent ``prek`` /
# ``script/run-in-env.py`` steps find the deps without a
# ``uv run`` prefix.
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
uses: astral-sh/setup-uv@v10.3.3
with:
enable-cache: true
# Pin uv version so the action does not have to fetch the
@@ -93,7 +93,7 @@ jobs:
run: python script/run-in-env.py prek run --all-files
- name: Commit changes
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
uses: peter-evans/create-pull-request@v8.1.1
with:
commit-message: "Synchronise Device Classes from Home Assistant"
committer: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
@@ -20,7 +20,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
uses: actions/create-github-app-token@v3.3.0
with:
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -28,10 +28,10 @@ jobs:
permission-pull-requests: write # open or refresh the sync PR
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
uses: actions/checkout@v7.0.1
- name: Setup Python
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
uses: actions/setup-python@v7.1.0
with:
python-version: "3.13"
@@ -86,7 +86,7 @@ jobs:
exit 1
- name: Commit changes
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
uses: peter-evans/create-pull-request@v8.1.1
with:
commit-message: "[ci] Refresh integration test durations"
committer: esphome[bot] <115708604+esphome[bot]@users.noreply.github.com>
+26
View File
@@ -0,0 +1,26 @@
name: verify-lockfile
on:
pull_request:
push:
branches:
- dev
permissions:
contents: read
jobs:
verify:
name: Verify actions lockfile
runs-on: ubuntu-latest
env:
GH_TOKEN: ${{ github.token }}
steps:
- name: Checkout
uses: actions/checkout@v7.0.1
with:
persist-credentials: false
- name: Install gh-actions-lock
run: gh extension install github/gh-actions-lock --pin v0.4.0
- name: Verify lockfile
run: gh actions-lock --verify
+19 -3
View File
@@ -205,6 +205,18 @@ file does, and it is the authority when they disagree. The most useful starting
static constexpr uint32_t READ_TIMEOUT_ID = 0;
this->set_timeout(READ_TIMEOUT_ID, 50, [this]() { this->read_(); });
```
* **Scheduler lambdas capture at most 8 bytes:** `set_timeout`, `set_interval` and `defer` store the lambda
in a `std::function`, which keeps it inline only when it is at most 8 bytes on 32-bit targets and trivially
copyable; anything else is a heap allocation each time it is scheduled, which matters for retries, re-armed
timeouts and `defer` on a hot path. `[this]` plus one 4-byte value fits. `[this]` plus two 4-byte values,
`[this]` plus a member function pointer (8 bytes), a `std::string` or a `std::function` do not: keep that
state in members and capture `[this]`, or capture a small index instead.
```cpp
// Bad - 12 bytes on 32-bit, allocates on every retry
this->set_timeout(RETRY_ID, 10, [this, configure = config.configure]() { (this->*configure)(); });
// Good - 8 bytes, the pointer is looked up when the timeout runs
this->set_timeout(RETRY_ID, 10, [this, feature]() { this->run_configure_(feature); });
```
* **Don't override a default with the same value:** if a base class method already returns what you
want, do not override it. `Component::get_setup_priority()` returns `setup_priority::DATA`, so a
component that wants `DATA` should simply leave it alone.
@@ -227,16 +239,20 @@ file does, and it is the authority when they disagree. The most useful starting
A `dump_config` helper that picks a name returns `const LogString *` from `LOG_STR(...)`.
* **CRCs and bit tricks** come from `esphome/core/helpers.h` (`crc8`, `crc16`, `xor_bytes`, `sum16_bytes`,
`reverse_bits`, `encode_uint16`, `encode_value_le`, `sign_extend<BITS>`, `bcd_to_uint8`, `assign_bits`); pick
the helper's arguments instead of writing a loop. 7-byte BCD RTC calendars use `time::decode_bcd_calendar<LAYOUT>`
and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`, `DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
the helper's arguments instead of writing a loop. `put_be16`/`24`/`32`, `put_le16` and `put_le32` write an
integer into a byte buffer and return the pointer past it. 7-byte BCD RTC calendars use
`time::decode_bcd_calendar<LAYOUT>` and `encode_bcd_calendar<LAYOUT>` with `EPSON_BCD_CALENDAR`,
`DS_BCD_CALENDAR` or `PCF_BCD_CALENDAR`.
* **Bus helpers:** UART drains with `read_available(on_byte)` and `discard_available()`, text lines with
`LineReader<N>`, fixed-length binary frames with `FrameReader<N, HEADER...>`; I2C has `update_register`,
`write_registers` (PROGMEM `I2CRegValue` table), `check_chip_id` and `read_bytes_16_le`.
`if (!this->status_comm_result(ok)) return;` sets or clears the warning.
`if (!this->status_comm_result(ok)) return;` sets or clears the warning; in `setup()`,
`if (!this->setup_check(ok)) return;` marks the component failed on error.
* **A hub's pointer to a child entity** comes from `SUB_SENSOR(name)`, `SUB_BINARY_SENSOR`, `SUB_TEXT_SENSOR`,
`SUB_SWITCH`, `SUB_NUMBER`, `SUB_SELECT` or `SUB_BUTTON`, which declare the `set_<name>_<type>()` setter and
the `<name>_<type>_` member (`SUB_SWITCH(turbo)` gives `set_turbo_switch()`); do not write the pair by hand.
A child sensor that skips repeated values is `SUB_SENSOR_WITH_DEDUP(name, type)`.
Publish to an optional child with `PUBLISH_STATE(this->x_sensor_, value);`, not a hand-written null check.
* **An unset float is `NAN`**, not `optional<float>`; the optional costs 4 to 8 bytes of padding per member.
* **`PollingComponent` first runs `update()`** after a random share of the interval; call `this->update()` at the
end of `setup()` when the entities need a state at boot.
+2
View File
@@ -154,6 +154,7 @@ esphome/components/d01/* @ch604
esphome/components/dac7678/* @NickB1
esphome/components/daikin_arc/* @MagicBear
esphome/components/daikin_brc/* @hagak
esphome/components/daikin_madoka/* @Petapton
esphome/components/dallas_temp/* @ssieb
esphome/components/daly_bms/* @s1lvi0
esphome/components/dashboard_import/* @esphome/core
@@ -632,6 +633,7 @@ esphome/components/st7789v/* @kbx81
esphome/components/st7920/* @marsjan155
esphome/components/statsd/* @Links2004
esphome/components/stcc4/* @j9brown
esphome/components/store_yaml/* @bdraco
esphome/components/stts22h/* @B48D81EFCC
esphome/components/substitutions/* @esphome/core
esphome/components/sun/* @OttoWinter
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.11.0-dev
PROJECT_NUMBER = 2026.12.0-dev
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+9 -8
View File
@@ -94,23 +94,24 @@ without a password leaves OTA open. The API is documented and is meant to be
called by other devices, scripts, and pages.
As defense-in-depth, the web server checks the `Origin` header on browser requests
to its entity control and state endpoints: a request whose `Origin` does not match
the address the device is served on is rejected, and the `allowed_origins` option
to every endpoint served through it, including the web OTA `/update` endpoint
(the captive portal pages are open by design): a request whose `Origin` does not match
the `Host` header of the same request is rejected, and the `allowed_origins` option
widens that list. This blocks the common "confused deputy" (CSRF) case where a page
the operator visits drives the device through their browser. It is **not** an
authentication boundary: it only constrains browsers. Any client that omits the
`Origin` header — `curl`, scripts, or other non-browser callers on the same
network — reaches every endpoint exactly as before. The check also does not cover
the web OTA `/update` endpoint. The device performs no CSRF-token or `Referer`
network — reaches every endpoint exactly as before. The device performs no CSRF-token or `Referer`
validation. The following are therefore **not** vulnerabilities in this repository:
- Requests without an `Origin` header (for example `curl`) reaching the control
endpoints, whether or not `web_server` `auth:` is set.
- Requests from an origin the operator added to `allowed_origins`.
- Cross-origin or CSRF firmware upload through the web OTA endpoint (`/update`) when
web OTA is enabled without `web_server` `auth:`. The `/update` endpoint is not
covered by the `Origin` check; this is the same exposure as running OTA without a
password.
- DNS rebinding: a page whose host name resolves to the device sends a matching
`Origin` and `Host`, so the check lets it through.
- Firmware upload through the web OTA endpoint (`/update`) by a client that sends no
`Origin` header when web OTA is enabled without `web_server` `auth:`; this is the
same exposure as running OTA without a password.
The supported defenses are `web_server` `auth:`, protecting OTA (a web password or
a native OTA password), and keeping devices on a trusted, segmented network. See
+1 -1
View File
@@ -1,5 +1,5 @@
ARG BUILD_VERSION=dev
ARG BUILD_BASE_VERSION=2026.06.1
ARG BUILD_BASE_VERSION=2026.11.0
ARG BUILD_TYPE=docker
FROM ghcr.io/esphome/docker-base:debian-${BUILD_BASE_VERSION} AS base-source-docker
+135 -74
View File
@@ -3,6 +3,7 @@ from contextvars import ContextVar
from dataclasses import dataclass, field
import logging
import math
import re
import string
from typing import Any
@@ -44,6 +45,9 @@ CONF_CHOOSE = "choose"
CONF_CHOICE_ID = "choice_id"
_BYTE_VECTOR = cg.std_vector.template(cg.uint8)
def progmem_bytes(name: str, data: bytes | list[int]) -> MockObj:
"""Shared PROGMEM table for constant bytes; equal payloads share one, empty is nullptr."""
if not data:
@@ -59,10 +63,11 @@ async def templatable_bytes(
set_template: MockObj,
set_static: MockObj,
table_name: str,
return_type: MockObj = _BYTE_VECTOR,
) -> None:
"""Set a TemplatableBytes: a lambda via set_template, constant bytes via set_static."""
if cg.is_template(value):
fn = await cg.templatable(value, args, cg.std_vector.template(cg.uint8))
fn = await cg.templatable(value, args, return_type)
cg.add(set_template(fn))
else:
cg.add(set_static(*_static_bytes(table_name, value)))
@@ -623,34 +628,34 @@ async def _apply_parent(config: ConfigType, id_key: str = CONF_ID) -> str:
return str(parent_ref(await cg.get_variable(config[id_key])))
def _apply_lambda_args(args: TemplateArgsType) -> TemplateArgsType:
# The generated function's parameters: a reference keeps its spelling, so a mutable
# one stays mutable; a value is taken by const reference, so a std::string is never copied.
return [
(
(
t
if str(cg.safe_exp(t)).rstrip().endswith("&")
else cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(t)}> &")
),
arg,
)
for t, arg in args
]
def _is_reference(type_: SafeExpType) -> bool:
return str(cg.safe_exp(type_)).rstrip().endswith("&")
def _apply_param_type(type_: SafeExpType) -> SafeExpType:
# A reference keeps its spelling; a value is taken by const reference, so it is never copied.
if _is_reference(type_):
return type_
return cg.RawExpression(f"const std::remove_cvref_t<{cg.safe_exp(type_)}> &")
def _apply_params(args: TemplateArgsType) -> TemplateArgsType:
"""The generated function's parameters."""
return [(_apply_param_type(t), arg) for t, arg in args]
def _apply_function(
id_: ID,
return_type: SafeExpType,
template_arg: cg.TemplateArguments,
lambda_args: TemplateArgsType,
args: TemplateArgsType,
statements: list[str],
) -> MockObj:
"""Emit the generated function and declare ``id_`` as the ``ApplyAction`` or
``ApplyCondition`` templated on it, so ``play()`` calls it directly."""
def emit() -> MockObj:
fn = apply_static_function(id_, return_type, lambda_args, statements)
fn = apply_static_function(id_, return_type, _apply_params(args), statements)
return cg.new_Pvariable(id_, cg.TemplateArguments(fn, *template_arg))
return _COLLECTED_ACTION if _collect_action(id_, statements, emit) else emit()
@@ -667,9 +672,7 @@ def apply_statements_action(
Use it when the statements do not fit ``register_apply_action``. Generated names should
start with ``esphome__`` so a trigger argument cannot shadow them.
"""
return _apply_function(
action_id, cg.void, template_arg, _apply_lambda_args(args), statements
)
return _apply_function(action_id, cg.void, template_arg, args, statements)
def apply_static_function(
@@ -682,19 +685,78 @@ def apply_static_function(
return cg.static_function(f"esphome__{id_.id}__fn", return_type, params, statements)
async def _render_values(
# Comments, raw strings, strings and char literals; a ' after a digit is a digit separator.
_CPP_NOISE = re.compile(
r"//[^\n]*|/\*.*?\*/"
r'|\b(?:u8|u|U|L)?R"([^()\\\s]{0,16})\(.*?\)\1"'
r'|"(?:\\.|[^"\\\n])*"'
r"|(?<!\w)'(?:\\.[^'\\\n]{0,8}|[^'\\\n])'",
re.DOTALL,
)
def _cpp_code_only(text: str) -> str:
return _CPP_NOISE.sub(" ", text)
def _names_arg(code: str, name: str) -> bool:
"""Whether ``code`` (with noise removed) names ``name`` other than as a member."""
return re.search(rf"(?<!\.)(?<!->)(?<!::)\b{re.escape(name)}\b", code) is not None
def _has_top_level_comma(code: str) -> bool:
depth = 0
for ch in code:
if ch in "([{":
depth += 1
elif ch in ")]}":
depth -= 1
elif ch == "," and depth == 0:
return True
return False
async def _call_user_lambda(
value: Lambda, args: TemplateArgsType, return_type: SafeExpType
) -> Expression:
"""Call a config lambda in place; a value arg its body names is a copy it may modify.
The other args are the generated function's const references, so a body that names
none of them is reduced to its expression, as ``call_lambda`` does. When unsure the
name check counts a use: a needless copy is only a cost, a missed one fails to compile.
"""
code = _cpp_code_only(value.value)
by_value = {
name for t, name in args if not _is_reference(t) and _names_arg(code, name)
}
params = [
(t if name in by_value else _apply_param_type(t), name) for t, name in args
]
inner = await cg.process_lambda(value, params, return_type=return_type)
if by_value:
return immediate_lambda_call(inner)
expr = call_lambda(inner)
# An uncast `a, b` would split into two arguments or end an initializer early
if isinstance(expr, cg.RawExpression) and _has_top_level_comma(
_cpp_code_only(str(expr))
):
return cg.RawExpression(f"({expr})")
return expr
async def _render_call(
name: str,
target: str,
members: list[tuple[Any, Any, Any]],
values: list[Any],
config: ConfigType,
parent: str | None,
lambda_args: TemplateArgsType,
args: TemplateArgsType,
compare: bool = False,
) -> list[str]:
"""Render the argument text of one statement; every key must be present.
) -> str:
"""Render ``target`` with its config values filled in; every key must be present.
``compare``: an inlined lambda expression is parenthesized so it binds as a whole
``compare``: a reduced lambda expression is parenthesized so it binds as a whole
beside an operator.
"""
if any(value is None for value in values):
@@ -705,8 +767,7 @@ async def _render_values(
if isinstance(value, Lambda):
if isinstance(type_, str):
type_ = cg.RawExpression(type_.format(parent=parent))
inner = await cg.process_lambda(value, lambda_args, return_type=type_)
expr = call_lambda(inner)
expr = await _call_user_lambda(value, args, type_)
bare = compare and isinstance(expr, cg.RawExpression)
exprs.append(f"({expr})" if bare else str(expr))
elif isinstance(value, ID):
@@ -716,7 +777,7 @@ async def _render_values(
exprs.append(const_fn(config, value))
else:
exprs.append(str(cg.safe_exp(value)))
return exprs
return target.format(*exprs, parent=parent)
def _apply_values(config: ConfigType, members: list[tuple[Any, Any, Any]]) -> list[Any]:
@@ -780,7 +841,6 @@ def apply_action_builder(
args: TemplateArgsType,
) -> MockObj:
parent = await _apply_parent(config, id_key)
lambda_args = _apply_lambda_args(args)
receiver = "apply_call." if call else f"{parent}->"
statements: list[str] = []
for spec in statements_spec:
@@ -795,19 +855,17 @@ def apply_action_builder(
values = _apply_values(config, members)
if not _apply_call_active(members, values):
continue
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args
rendered = await _render_call(
name, target, members, values, config, parent, args
)
statements.append(f"{receiver}{target.format(*exprs, parent=parent)};")
statements.append(f"{receiver}{rendered};")
if call:
statements = [
f"auto apply_call = {parent}->{call}();",
*statements,
"apply_call.perform();",
]
return _apply_function(
action_id, cg.void, template_arg, lambda_args, statements
)
return _apply_function(action_id, cg.void, template_arg, args, statements)
return builder
@@ -820,11 +878,7 @@ async def _render_bytes(
args: TemplateArgsType,
table_name: str,
) -> list[str]:
"""The statements for one ``ApplyBytes``: what ``TemplatableBytes::visit`` does, inlined.
The payload lambda keeps the automation's own argument types, as ``TemplatableBytes``
gave it, and is always called, so a body that modifies an argument still compiles.
"""
"""The statements for one ``ApplyBytes``: what ``TemplatableBytes::visit`` does, inlined."""
value = _config_lookup(config, spec.conf_key)
if value is None:
return []
@@ -834,11 +888,7 @@ async def _render_bytes(
return f"{receiver}{target.format(data, parent=parent)};"
if isinstance(value, Lambda):
inner = await cg.process_lambda(
value, args, return_type=cg.std_vector.template(cg.uint8)
)
# Never reduced into the outer function, whose own parameters are const
call = CallExpression(inner, *[cg.MockObj(name) for _, name in args])
call = await _call_user_lambda(value, args, _BYTE_VECTOR)
return [
"{",
f" const std::vector<uint8_t> esphome__bytes = {call};",
@@ -873,22 +923,6 @@ def _apply_call_active(members: list[tuple[Any, Any, Any]], values: list[Any]) -
return not members or any(value is not None for value in values)
async def _render_check(
name: str,
target: str,
members: list[tuple[Any, Any, Any]],
values: list[Any],
config: ConfigType,
parent: str | None,
lambda_args: TemplateArgsType,
) -> str:
"""Render a boolean check with ``values`` compared against config."""
exprs = await _render_values(
name, target, members, values, config, parent, lambda_args, compare=True
)
return target.format(*exprs, parent=parent)
def register_apply_condition(
name: str,
schema: cv.Schema,
@@ -921,16 +955,15 @@ def register_apply_condition(
if defaults:
config = {**defaults, **config}
parent = await _apply_parent(config, id_key)
lambda_args = _apply_lambda_args(args)
values = _apply_values(config, members)
check = await _render_check(
name, call.target, members, values, config, parent, lambda_args
check = await _render_call(
name, call.target, members, values, config, parent, args, compare=True
)
return _apply_function(
condition_id,
cg.bool_,
template_arg,
lambda_args,
args,
[f"return {parent}->{check};"],
)
@@ -1718,7 +1751,7 @@ def _emit_inline_run(
cg.void,
[
(cg.RawExpression("const int &"), _RUNNING_PARAM),
*_apply_lambda_args(args),
*_apply_params(args),
],
statements,
)
@@ -1873,10 +1906,13 @@ def validate_synchronous_automation(
)
async def _new_automation(
async def new_automation(
args: TemplateArgsType, config: ConfigType, *ctor_args: MockObj
) -> MockObj:
"""Create the Automation for ``config`` with its actions."""
"""Create the ``Automation<args>`` for an ``on_*`` config with its actions.
For a parent that stores and fires the automation itself; ``build_automation`` attaches it to a trigger.
"""
plan = await _build_automation_plan(args, config)
return _automation_from_plan(plan, args, config, *ctor_args)
@@ -1923,7 +1959,7 @@ def _direct_function(
return None
statements = [line for line in _run_statements(plan[0]) if line != _STOP_CHECK]
return apply_static_function(
plan[0][0].id_, cg.void, _apply_lambda_args(args), statements
plan[0][0].id_, cg.void, _apply_params(args), statements
)
@@ -1941,7 +1977,7 @@ def _when_call(
async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj:
return await _new_automation(args, config, trigger)
return await new_automation(args, config, trigger)
async def build_trigger_callback(
@@ -1979,27 +2015,27 @@ async def _trigger_callback(
) -> LambdaExpression:
if (target := _direct_function(plan, args, config)) is None:
target = f"{parent_ref(_automation_from_plan(plan, args, config))}->trigger"
lambda_args = _apply_lambda_args(params)
statements: list[str] = []
if call is not None:
members = call.members
values = _apply_values(config, members)
if _apply_call_active(members, values):
check = await _render_check(
check = await _render_call(
"trigger filter",
call.target,
members,
values,
config,
None if parent is None else str(parent_ref(parent)),
lambda_args,
params,
compare=True,
)
statements.append(f"if (!({check}))\n return;")
if forward is None:
forward = [name for _, name in params]
statements.append(f"{target}({', '.join(map(str, forward))});")
return LambdaExpression(
["\n".join(statements)], lambda_args, capture="", return_type=cg.void
["\n".join(statements)], _apply_params(params), capture="", return_type=cg.void
)
@@ -2102,6 +2138,31 @@ async def build_trigger_automations(
await build_automation(trigger, args, conf)
@dataclass(frozen=True, slots=True)
class AutomationList:
"""Automations a parent keeps in an ``AutomationList<count_define, args...>`` and triggers itself.
The parent adds each one with ``add_<conf_key>_automation()``. ``count_define`` is the most
automations any one parent gets; it is absent when none do.
"""
conf_key: str
count_define: str
args: TemplateArgsType
async def build_automation_lists(
parent: MockObj, config: ConfigType, entries: tuple[AutomationList, ...]
) -> None:
"""Build each entry's automations and add them to the parent's slot-counted list."""
for entry in entries:
request_slot = cg.slot_counter(entry.count_define)
for conf in config.get(entry.conf_key, []):
request_slot(str(parent))
obj = await new_automation(entry.args, conf)
cg.add(getattr(parent, f"add_{entry.conf_key}_automation")(obj))
@dataclass(frozen=True, slots=True)
class CallbackAutomation:
"""A single callback automation entry for build_callback_automations."""
+1
View File
@@ -54,6 +54,7 @@ from esphome.cpp_generator import ( # noqa: F401
with_local_variable,
)
from esphome.cpp_helpers import ( # noqa: F401
HubFunction,
add_optional,
add_pin,
add_variable,
+8 -18
View File
@@ -13,33 +13,25 @@ void AdE7953I2c::dump_config() {
}
bool AdE7953I2c::ade_write_8(uint16_t reg, uint8_t value) {
uint8_t data[3];
data[0] = reg >> 8;
data[1] = reg >> 0;
put_be16(data, reg);
data[2] = value;
return this->write(data, 3) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_write_16(uint16_t reg, uint16_t value) {
uint8_t data[4];
data[0] = reg >> 8;
data[1] = reg >> 0;
data[2] = value >> 8;
data[3] = value >> 0;
put_be16(data, reg);
put_be16(data + 2, value);
return this->write(data, 4) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_write_32(uint16_t reg, uint32_t value) {
uint8_t data[6];
data[0] = reg >> 8;
data[1] = reg >> 0;
data[2] = value >> 24;
data[3] = value >> 16;
data[4] = value >> 8;
data[5] = value >> 0;
put_be16(data, reg);
put_be32(data + 2, value);
return this->write(data, 6) != i2c::ERROR_OK;
}
bool AdE7953I2c::ade_read_8(uint16_t reg, uint8_t *value) {
uint8_t reg_data[2];
reg_data[0] = reg >> 8;
reg_data[1] = reg >> 0;
put_be16(reg_data, reg);
i2c::ErrorCode err = this->write(reg_data, 2);
if (err != i2c::ERROR_OK)
return true;
@@ -48,8 +40,7 @@ bool AdE7953I2c::ade_read_8(uint16_t reg, uint8_t *value) {
}
bool AdE7953I2c::ade_read_16(uint16_t reg, uint16_t *value) {
uint8_t reg_data[2];
reg_data[0] = reg >> 8;
reg_data[1] = reg >> 0;
put_be16(reg_data, reg);
i2c::ErrorCode err = this->write(reg_data, 2);
if (err != i2c::ERROR_OK)
return true;
@@ -62,8 +53,7 @@ bool AdE7953I2c::ade_read_16(uint16_t reg, uint16_t *value) {
}
bool AdE7953I2c::ade_read_32(uint16_t reg, uint32_t *value) {
uint8_t reg_data[2];
reg_data[0] = reg >> 8;
reg_data[1] = reg >> 0;
put_be16(reg_data, reg);
i2c::ErrorCode err = this->write(reg_data, 2);
if (err != i2c::ERROR_OK)
return true;
+2 -6
View File
@@ -10,10 +10,8 @@ static const uint8_t ADS1115_REGISTER_CONFIG = 0x01;
void ADS1115Component::setup() {
uint16_t value;
if (!this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_byte_16(ADS1115_REGISTER_CONVERSION, &value)))
return;
}
uint16_t config = 0;
// Clear single-shot bit
@@ -57,10 +55,8 @@ void ADS1115Component::setup() {
// 0bxxxxxxxxxxxxxx11
config |= 0b0000000000000011;
if (!this->write_byte_16(ADS1115_REGISTER_CONFIG, config)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte_16(ADS1115_REGISTER_CONFIG, config)))
return;
}
this->prev_config_ = config;
}
void ADS1115Component::dump_config() {
+4 -9
View File
@@ -51,28 +51,23 @@ void AHT10Component::setup() {
error_code = this->write(AHT10_INITIALIZE_CMD, sizeof(AHT10_INITIALIZE_CMD));
break;
}
if (error_code != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(error_code == i2c::ERROR_OK))
return;
}
uint8_t cal_attempts = 0;
uint8_t data = AHT10_STATUS_BUSY;
while (data & AHT10_STATUS_BUSY) {
delay(AHT10_DEFAULT_DELAY);
if (this->read(&data, 1) != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read(&data, 1) == i2c::ERROR_OK))
return;
}
++cal_attempts;
if (cal_attempts > AHT10_INIT_ATTEMPTS) {
this->mark_failed(LOG_STR("Initialization timed out"));
return;
}
}
if ((data & 0x68) != 0x08) { // Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
this->mark_failed(LOG_STR("Initialization failed"));
// Bit[6:5] = 0b00, NORMAL mode and Bit[3] = 0b1, CALIBRATED
if (!this->setup_check((data & 0x68) == 0x08, LOG_STR("Initialization failed")))
return;
}
}
void AHT10Component::restart_read_() {
@@ -373,9 +373,7 @@ void AirtonSerialClimate::send_ab_() {
}
void AirtonSerialClimate::write_frame_(uint8_t *frame, size_t len) {
const uint16_t crc = crc16(frame, len - 2);
frame[len - 2] = static_cast<uint8_t>(crc >> 8);
frame[len - 1] = static_cast<uint8_t>(crc);
put_be16(frame + len - 2, crc16(frame, len - 2));
this->write_array(frame, len);
}
+1 -3
View File
@@ -72,10 +72,8 @@ bool AM2315C::convert_(uint8_t *data, float &humidity, float &temperature) {
void AM2315C::setup() {
// get status
uint8_t status = 0;
if (this->read(&status, 1) != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read(&status, 1) == i2c::ERROR_OK))
return;
}
// reset registers if required, according to the datasheet
// this can be required after power on, although this was
+1 -3
View File
@@ -34,10 +34,8 @@ void AM2320Component::setup() {
uint8_t data[8];
data[0] = 0;
data[1] = 4;
if (!this->read_data_(data)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_data_(data)))
return;
}
}
void AM2320Component::dump_config() {
ESP_LOGD(TAG, "AM2320:");
+1 -2
View File
@@ -26,8 +26,7 @@ Am43Packet Am43Encoder::get_position_request() {
Am43Packet Am43Encoder::get_send_pin_request(uint16_t pin) {
uint8_t data[2];
data[0] = (pin & 0xFF00) >> 8;
data[1] = pin & 0xFF;
put_be16(data, pin);
return encode(CMD_SEND_PIN, data, 2);
}
+1 -3
View File
@@ -304,9 +304,7 @@ void APC1Component::send_command_(APC1Command cmd, uint16_t data) {
0,
0,
};
uint16_t checksum = sum16_bytes(buf.data(), 5);
buf[5] = static_cast<uint8_t>((checksum >> 8) & 0xFF);
buf[6] = static_cast<uint8_t>(checksum & 0xFF);
put_be16(buf.data() + 5, sum16_bytes(buf.data(), 5));
this->write_array(buf);
}
+1 -3
View File
@@ -60,10 +60,8 @@ static constexpr i2c::I2CRegValue APDS9960_GESTURE_OFFSETS[] PROGMEM = {
void APDS9960::setup() {
uint8_t id;
if (!this->read_byte(0x92, &id)) { // ID register
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_byte(0x92, &id))) // ID register
return;
}
if (id != 0xAB && id != 0x9C && id != 0xA8 && id != 0x9E) { // APDS9960 all should have one of these IDs
this->mark_failed(LOG_STR("APDS9960 has invalid id"));
+77 -1
View File
@@ -22,6 +22,7 @@ service APIConnection {
rpc device_capabilities (DeviceCapabilitiesRequest) returns (DeviceCapabilitiesResponse) {}
rpc device_wizard (DeviceWizardRequest) returns (DeviceWizardResponse) {}
rpc wizard_input_set (WizardInputSetRequest) returns (void) {}
rpc crash_report (CrashReportRequest) returns (CrashReportResponse) {}
rpc list_entities (ListEntitiesRequest) returns (void) {}
rpc subscribe_states (SubscribeStatesRequest) returns (void) {}
rpc subscribe_logs (SubscribeLogsRequest) returns (void) {}
@@ -83,6 +84,7 @@ service APIConnection {
rpc serial_proxy_set_mode(SerialProxySetModeRequest) returns (void) {}
rpc sendspin_pairing_token(SendspinPairingTokenRequest) returns (SendspinPairingTokenResponse) {}
rpc get_yaml(GetYamlRequest) returns (void) {}
}
@@ -401,6 +403,11 @@ message ZWaveProxyCapabilities {
uint32 home_id = 2;
}
message StoreYamlCapabilities {
// Firmware embeds its YAML, so clients can request it with get_yaml
bool supported = 1;
}
message WizardCapabilities {
// True when the device has a wizard configured, so DeviceWizardRequest will be answered
bool configured = 1;
@@ -414,6 +421,12 @@ message SendspinCapabilities {
string client_id = 2 [(max_data_length) = 43];
}
message CrashReportCapabilities {
// Bitmask of CrashReportFlag. ESP32 checks BUILD_MATCHES against the build time stored with the record;
// ESP8266, RP2 and LibreTiny store none and assume it.
uint32 flags = 1;
}
message DeviceCapabilitiesResponse {
option (id) = 150;
option (source) = SOURCE_SERVER;
@@ -425,6 +438,8 @@ message DeviceCapabilitiesResponse {
[(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"];
WizardCapabilities wizard = 5 [(field_ifdef) = "USE_API_WIZARD"];
SendspinCapabilities sendspin = 6 [(field_ifdef) = "USE_SENDSPIN"];
StoreYamlCapabilities store_yaml = 7 [(field_ifdef) = "USE_STORE_YAML"];
CrashReportCapabilities crash_report = 8 [(field_ifdef) = "USE_CRASH_REPORT"];
}
// ==================== DEVICE WIZARD ====================
@@ -538,6 +553,36 @@ message SubscribeStatesRequest {
// Empty
}
// ==================== CRASH REPORT ====================
// Send only when crash_report.flags has PRESENT. The report is dropped once sent.
message CrashReportRequest {
option (id) = 163;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_CRASH_REPORT";
// Empty
}
message CrashReportCore {
uint32 core = 1;
repeated uint32 backtrace = 2 [(fixed_array_with_length_define) = "CRASH_REPORT_MAX_BACKTRACE"];
}
message CrashReportResponse {
option (id) = 164;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CRASH_REPORT";
uint32 flags = 1; // Bitmask of CrashReportFlag
uint32 reason = 2; // CrashReason
uint32 pc = 3;
uint32 cause = 4;
uint32 fault_address = 5;
uint32 build_time = 6;
uint32 crashed_core = 7;
repeated CrashReportCore cores = 8 [(fixed_array_with_length_define) = "CRASH_REPORT_MAX_CORES"];
}
// ==================== COMMON =====================
enum EntityCategory {
@@ -2192,6 +2237,8 @@ message BluetoothScannerSetModeRequest {
enum VoiceAssistantSubscribeFlag {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0;
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1;
// Not 2 or 4: aioesphomeapi sends API audio as 1 << 2
VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO = 8;
}
message SubscribeVoiceAssistantRequest {
@@ -2225,6 +2272,11 @@ message VoiceAssistantRequest {
uint32 flags = 3;
VoiceAssistantAudioSettings audio_settings = 4;
string wake_word_phrase = 5;
string wake_word_id = 6;
uint32 wake_word_audio_bytes = 7;
float wake_word_probability = 8;
float wake_word_probability_cutoff = 9;
uint32 wake_word_audio_bytes2 = 10;
}
message VoiceAssistantResponse {
@@ -2250,6 +2302,7 @@ enum VoiceAssistantEvent {
VOICE_ASSISTANT_WAKE_WORD_END = 10;
VOICE_ASSISTANT_STT_VAD_START = 11;
VOICE_ASSISTANT_STT_VAD_END = 12;
VOICE_ASSISTANT_WAKE_WORD_REJECTED = 13;
VOICE_ASSISTANT_TTS_STREAM_START = 98;
VOICE_ASSISTANT_TTS_STREAM_END = 99;
VOICE_ASSISTANT_INTENT_PROGRESS = 100;
@@ -2843,7 +2896,7 @@ message InfraredRFReceiveEvent {
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::span<const int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}
// Sent only to the client that issued an InfraredRFTransmitRawTimingsRequest, once the
@@ -3071,6 +3124,7 @@ message UsbDeviceDescriptor {
uint32 interface_number = 4; // bInterfaceNumber the port is bound through
uint32 interface_class = 5; // bInterfaceClass of that interface
uint32 port_number = 6;
string interface_description = 7; // iInterface string of that interface, empty when it has none
}
// The identity of the device behind a port. Identifies one physical device among others of
@@ -3143,3 +3197,25 @@ message SendspinPairingTokenResponse {
// The pairing token (Sendspin pairing.md "Pairing Token"). Set only when status is OK.
string token = 2 [(max_data_length) = 127];
}
// ==================== STORE YAML ====================
// Streams the compressed user YAML embedded in firmware; the client decompresses.
// A GetYamlRequest during an active transfer is ignored; reconnect to restart.
message GetYamlRequest {
option (id) = 161;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_STORE_YAML";
option (no_delay) = true;
}
message GetYamlResponse {
option (id) = 162;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_STORE_YAML";
// A zstd frame of the EHY1 file envelope, sent from flash
bytes data = 1 [(force) = true, (pointer_to_buffer) = true, (progmem) = true];
bool done = 2 [(force) = true];
// Set on the first chunk only
uint32 total_size = 3;
}
+80 -3
View File
@@ -366,6 +366,13 @@ void APIConnection::loop() {
// (missing a frame is fine, missing a state update is not)
this->try_send_camera_image_();
#endif
#ifdef USE_STORE_YAML
// Guard inline so the idle hot path pays a compare, not a function call.
if (this->store_yaml_pos_ != STORE_YAML_IDLE) {
this->try_send_store_yaml_();
}
#endif
}
void APIConnection::check_keepalive_(uint32_t now) {
@@ -1222,6 +1229,36 @@ void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
}
#endif
#ifdef USE_STORE_YAML
void APIConnection::on_get_yaml_request() {
// A re-request while a transfer is in flight is ignored (see api.proto)
if (this->store_yaml_pos_ != STORE_YAML_IDLE)
return;
this->store_yaml_pos_ = 0;
this->try_send_store_yaml_();
}
void APIConnection::try_send_store_yaml_() {
// Advance only after a successful send, so a full TX buffer retries the same chunk from loop()
while (this->helper_->can_write_without_blocking()) {
const size_t to_send = std::min(STORE_YAML_DATA_SIZE - this->store_yaml_pos_, MAX_BATCH_PACKET_SIZE);
GetYamlResponse resp;
resp.data = STORE_YAML_DATA + this->store_yaml_pos_;
resp.data_len = to_send;
if (this->store_yaml_pos_ == 0)
resp.total_size = STORE_YAML_DATA_SIZE;
resp.done = this->store_yaml_pos_ + to_send == STORE_YAML_DATA_SIZE;
if (!this->send_message(resp))
return;
if (resp.done) {
this->store_yaml_pos_ = STORE_YAML_IDLE;
return;
}
this->store_yaml_pos_ += to_send;
}
}
#endif
#ifdef USE_HOMEASSISTANT_TIME
void APIConnection::on_get_time_response(const GetTimeResponse &value) {
if (homeassistant::global_homeassistant_time != nullptr) {
@@ -1316,7 +1353,7 @@ bool APIConnection::check_voice_assistant_api_connection_() const {
void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
if (voice_assistant::global_voice_assistant != nullptr) {
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe);
voice_assistant::global_voice_assistant->client_subscription(this, msg.subscribe, msg.flags);
}
}
void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
@@ -1932,7 +1969,7 @@ bool APIConnection::send_hello_response_(const HelloRequest &msg) {
HelloResponse resp;
resp.api_version_major = 1;
resp.api_version_minor = 19;
resp.api_version_minor = 20;
// Send only the version string - the client only logs this for debugging and doesn't use it otherwise
resp.server_info = esphome_version_ref();
resp.name = StringRef(App.get_name());
@@ -2123,6 +2160,14 @@ bool APIConnection::send_device_capabilities_response_() {
#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
#ifdef USE_STORE_YAML
resp.store_yaml.supported = true;
#endif
#ifdef USE_CRASH_REPORT
CrashReport report;
if (crash_report_get(report))
resp.crash_report.flags = report.flags;
#endif
return this->send_message(resp);
}
@@ -2152,6 +2197,34 @@ void APIConnection::on_device_capabilities_request() {
this->on_fatal_error();
}
}
#ifdef USE_CRASH_REPORT
void APIConnection::on_crash_report_request() {
CrashReport report;
CrashReportResponse resp;
if (crash_report_get(report)) {
resp.flags = report.flags;
resp.reason = static_cast<uint32_t>(report.reason);
resp.pc = report.pc;
resp.cause = report.cause;
resp.fault_address = report.fault_address;
resp.build_time = report.build_time;
resp.crashed_core = report.crashed_core;
for (uint8_t i = 0; i < report.core_count; i++) {
auto &core = resp.cores[i];
core.core = report.cores[i].core;
core.backtrace = report.cores[i].addresses;
core.backtrace_len = report.cores[i].count;
}
resp.cores_len = report.core_count;
}
if (!this->send_message(resp)) {
this->on_fatal_error();
return;
}
if (resp.flags & CRASH_REPORT_FLAG_PRESENT)
crash_report_clear();
}
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
@@ -2365,10 +2438,14 @@ bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) {
bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn,
const void *msg) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise)
// Skip dump for log messages (recursive logging risk), camera frames (high-frequency noise)
// and YAML chunks (may contain secrets)
if (message_type != SubscribeLogsResponse::MESSAGE_TYPE
#ifdef USE_CAMERA
&& message_type != CameraImageResponse::MESSAGE_TYPE
#endif
#ifdef USE_STORE_YAML
&& message_type != GetYamlResponse::MESSAGE_TYPE
#endif
) {
auto *proto_msg = static_cast<const ProtoMessage *>(msg);
+18
View File
@@ -32,6 +32,11 @@ namespace esphome::api {
// Forward-declared to break the api_server.h cycle; full-type inlines are in api_connection_buffer.h.
class APIServer;
#ifdef USE_STORE_YAML
/// The zstd YAML envelope generated by store_yaml, STORE_YAML_DATA_SIZE bytes in flash.
extern const uint8_t STORE_YAML_DATA[] PROGMEM;
#endif
// One shared flash string for every refused-frame warning: send_message()
// fails as soon as the TCP buffer is full, and each caller only pays for its
// short name. The guard drops the helper and its arguments below WARN.
@@ -123,6 +128,9 @@ class APIConnection final : public APIServerConnectionBase {
void set_camera_state(std::shared_ptr<camera::CameraImage> image);
void on_camera_image_request(const CameraImageRequest &msg);
#endif
#ifdef USE_STORE_YAML
void on_get_yaml_request();
#endif
#ifdef USE_CLIMATE
bool send_climate_state(climate::Climate *climate);
void on_climate_command_request(const ClimateCommandRequest &msg);
@@ -277,6 +285,9 @@ class APIConnection final : public APIServerConnectionBase {
#ifdef USE_API_WIZARD
void on_device_wizard_request();
#endif
#ifdef USE_CRASH_REPORT
void on_crash_report_request();
#endif
#ifdef USE_API_WIZARD_INPUTS
void on_wizard_input_set_request(const WizardInputSetRequest &msg);
#endif
@@ -421,6 +432,13 @@ class APIConnection final : public APIServerConnectionBase {
void try_send_camera_image_();
#endif
#ifdef USE_STORE_YAML
void try_send_store_yaml_();
static constexpr size_t STORE_YAML_IDLE = std::numeric_limits<size_t>::max();
// Streaming offset into STORE_YAML_DATA, or STORE_YAML_IDLE
size_t store_yaml_pos_{STORE_YAML_IDLE};
#endif
#ifdef USE_API_HOMEASSISTANT_STATES
void process_state_subscriptions_();
#endif
+120
View File
@@ -299,6 +299,19 @@ uint32_t ZWaveProxyCapabilities::calc_size_msg(const void *self) {
return size;
}
#endif
#ifdef USE_STORE_YAML
uint8_t *StoreYamlCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const StoreYamlCapabilities *>(self);
pos = ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.supported);
return pos;
}
uint32_t StoreYamlCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const StoreYamlCapabilities *>(self);
uint32_t size = 0;
size += ProtoSize::calc_bool(1, msg.supported);
return size;
}
#endif
#ifdef USE_API_WIZARD
uint8_t *WizardCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const WizardCapabilities *>(self);
@@ -327,6 +340,19 @@ uint32_t SendspinCapabilities::calc_size_msg(const void *self) {
return size;
}
#endif
#ifdef USE_CRASH_REPORT
uint8_t *CrashReportCapabilities::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const CrashReportCapabilities *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.flags);
return pos;
}
uint32_t CrashReportCapabilities::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const CrashReportCapabilities *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.flags);
return size;
}
#endif
uint8_t *DeviceCapabilitiesResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const DeviceCapabilitiesResponse *>(self);
#ifdef USE_BLUETOOTH_PROXY
@@ -348,6 +374,12 @@ uint8_t *DeviceCapabilitiesResponse::encode_msg(const void *self, uint8_t *__res
#endif
#ifdef USE_SENDSPIN
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.sendspin);
#endif
#ifdef USE_STORE_YAML
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.store_yaml);
#endif
#ifdef USE_CRASH_REPORT
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 8, msg.crash_report);
#endif
return pos;
}
@@ -373,6 +405,12 @@ uint32_t DeviceCapabilitiesResponse::calc_size_msg(const void *self) {
#endif
#ifdef USE_SENDSPIN
size += ProtoSize::calc_message(1, msg.sendspin.calculate_size());
#endif
#ifdef USE_STORE_YAML
size += ProtoSize::calc_message(1, msg.store_yaml.calculate_size());
#endif
#ifdef USE_CRASH_REPORT
size += ProtoSize::calc_message(1, msg.crash_report.calculate_size());
#endif
return size;
}
@@ -405,6 +443,54 @@ void WizardInputSetRequest::decode_field(void *self, uint32_t tag, const uint8_t
}
}
#endif
#ifdef USE_CRASH_REPORT
uint8_t *CrashReportCore::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const CrashReportCore *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.core);
for (uint16_t i = 0; i < msg.backtrace_len; i++) {
pos = ProtoEncode::encode_uint32_force(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.backtrace[i]);
}
return pos;
}
uint32_t CrashReportCore::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const CrashReportCore *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.core);
for (uint16_t i = 0; i < msg.backtrace_len; i++) {
size += ProtoSize::calc_uint32_force(1, msg.backtrace[i]);
}
return size;
}
uint8_t *CrashReportResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const CrashReportResponse *>(self);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.flags);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 2, msg.reason);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.pc);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.cause);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.fault_address);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.build_time);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.crashed_core);
for (uint16_t i = 0; i < msg.cores_len; i++) {
pos = ProtoEncode::encode_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 8, msg.cores[i]);
}
return pos;
}
uint32_t CrashReportResponse::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const CrashReportResponse *>(self);
uint32_t size = 0;
size += ProtoSize::calc_uint32(1, msg.flags);
size += ProtoSize::calc_uint32(1, msg.reason);
size += ProtoSize::calc_uint32(1, msg.pc);
size += ProtoSize::calc_uint32(1, msg.cause);
size += ProtoSize::calc_uint32(1, msg.fault_address);
size += ProtoSize::calc_uint32(1, msg.build_time);
size += ProtoSize::calc_uint32(1, msg.crashed_core);
for (uint16_t i = 0; i < msg.cores_len; i++) {
size += ProtoSize::calc_message_force(1, msg.cores[i].calculate_size());
}
return size;
}
#endif
#ifdef USE_BINARY_SENSOR
uint8_t *ListEntitiesBinarySensorResponse::encode_msg(const void *self,
uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -3028,6 +3114,15 @@ uint8_t *VoiceAssistantRequest::encode_msg(const void *self, uint8_t *__restrict
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.flags);
pos = ProtoEncode::encode_optional_sub_message(pos PROTO_ENCODE_DEBUG_ARG, 4, msg.audio_settings);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 5, msg.wake_word_phrase);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 6, msg.wake_word_id);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.wake_word_audio_bytes);
if (uint32_t raw = float_to_raw(msg.wake_word_probability); raw != 0) [[likely]] {
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 69, raw);
}
if (uint32_t raw = float_to_raw(msg.wake_word_probability_cutoff); raw != 0) [[likely]] {
pos = ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 77, raw);
}
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, msg.wake_word_audio_bytes2);
return pos;
}
uint32_t VoiceAssistantRequest::calc_size_msg(const void *self) {
@@ -3038,6 +3133,11 @@ uint32_t VoiceAssistantRequest::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.flags);
size += ProtoSize::calc_message(1, msg.audio_settings.calculate_size());
size += ProtoSize::calc_length(1, msg.wake_word_phrase.size());
size += ProtoSize::calc_length(1, msg.wake_word_id.size());
size += ProtoSize::calc_uint32(1, msg.wake_word_audio_bytes);
size += ProtoSize::calc_float(1, msg.wake_word_probability);
size += ProtoSize::calc_float(1, msg.wake_word_probability_cutoff);
size += ProtoSize::calc_uint32(1, msg.wake_word_audio_bytes2);
return size;
}
void VoiceAssistantResponse::decode_field(void *self, uint32_t tag, const uint8_t *data, proto_varint_value_t scalar) {
@@ -4314,6 +4414,7 @@ uint8_t *UsbDeviceDescriptor::encode_msg(const void *self, uint8_t *__restrict__
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);
pos = ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 7, msg.interface_description);
return pos;
}
uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
@@ -4325,6 +4426,7 @@ uint32_t UsbDeviceDescriptor::calc_size_msg(const void *self) {
size += ProtoSize::calc_uint32(1, msg.interface_number);
size += ProtoSize::calc_uint32(1, msg.interface_class);
size += ProtoSize::calc_uint32(1, msg.port_number);
size += ProtoSize::calc_length(1, msg.interface_description.size());
return size;
}
uint8_t *SerialProxyIdentity::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
@@ -4407,6 +4509,24 @@ uint32_t SendspinPairingTokenResponse::calc_size_msg(const void *self) {
return size;
}
#endif
#ifdef USE_STORE_YAML
uint8_t *GetYamlResponse::encode_msg(const void *self, uint8_t *__restrict__ pos PROTO_ENCODE_DEBUG_PARAM) {
const auto &msg = *static_cast<const GetYamlResponse *>(self);
pos = ProtoEncode::encode_progmem_bytes_force(pos PROTO_ENCODE_DEBUG_ARG, 1, msg.data, msg.data_len);
pos = ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
pos = ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, msg.done ? 0x01 : 0x00);
pos = ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 3, msg.total_size);
return pos;
}
uint32_t GetYamlResponse::calc_size_msg(const void *self) {
const auto &msg = *static_cast<const GetYamlResponse *>(self);
uint32_t size = 0;
size += ProtoSize::calc_length_force(1, msg.data_len);
size += ProtoSize::calc_bool_force(1);
size += ProtoSize::calc_uint32(1, msg.total_size);
return size;
}
#endif
#ifndef HAS_PROTO_MESSAGE_DUMP
static_assert(!std::is_polymorphic_v<HelloRequest>, "decodable messages carry no vtable");
static_assert(!std::is_polymorphic_v<DisconnectRequest>, "decodable messages carry no vtable");
+123 -3
View File
@@ -262,6 +262,7 @@ enum BluetoothScannerMode : uint32_t {
enum VoiceAssistantSubscribeFlag : uint32_t {
VOICE_ASSISTANT_SUBSCRIBE_NONE = 0,
VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO = 1,
VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO = 8,
};
enum VoiceAssistantRequestFlag : uint32_t {
VOICE_ASSISTANT_REQUEST_NONE = 0,
@@ -283,6 +284,7 @@ enum VoiceAssistantEvent : uint32_t {
VOICE_ASSISTANT_WAKE_WORD_END = 10,
VOICE_ASSISTANT_STT_VAD_START = 11,
VOICE_ASSISTANT_STT_VAD_END = 12,
VOICE_ASSISTANT_WAKE_WORD_REJECTED = 13,
VOICE_ASSISTANT_TTS_STREAM_START = 98,
VOICE_ASSISTANT_TTS_STREAM_END = 99,
VOICE_ASSISTANT_INTENT_PROGRESS = 100,
@@ -734,6 +736,23 @@ class ZWaveProxyCapabilities final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_STORE_YAML
class StoreYamlCapabilities final : public ProtoMessage {
public:
bool supported{false};
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);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_API_WIZARD
class WizardCapabilities final : public ProtoMessage {
public:
@@ -769,10 +788,27 @@ class SendspinCapabilities final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_CRASH_REPORT
class CrashReportCapabilities final : public ProtoMessage {
public:
uint32_t flags{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);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
class DeviceCapabilitiesResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 150;
static constexpr uint8_t ESTIMATED_SIZE = 136;
static constexpr uint8_t ESTIMATED_SIZE = 170;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("device_capabilities_response"); }
#endif
@@ -793,6 +829,12 @@ class DeviceCapabilitiesResponse final : public ProtoMessage {
#endif
#ifdef USE_SENDSPIN
SendspinCapabilities sendspin{};
#endif
#ifdef USE_STORE_YAML
StoreYamlCapabilities store_yaml{};
#endif
#ifdef USE_CRASH_REPORT
CrashReportCapabilities crash_report{};
#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 {
@@ -863,6 +905,53 @@ class ListEntitiesDoneResponse final : public ProtoMessage {
protected:
};
#ifdef USE_CRASH_REPORT
class CrashReportCore final : public ProtoMessage {
public:
uint32_t core{0};
std::array<uint32_t, CRASH_REPORT_MAX_BACKTRACE> backtrace{};
uint16_t backtrace_len{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);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
class CrashReportResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 164;
static constexpr uint8_t ESTIMATED_SIZE = 164;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("crash_report_response"); }
#endif
uint32_t flags{0};
uint32_t reason{0};
uint32_t pc{0};
uint32_t cause{0};
uint32_t fault_address{0};
uint32_t build_time{0};
uint32_t crashed_core{0};
std::array<CrashReportCore, CRASH_REPORT_MAX_CORES> cores{};
uint16_t cores_len{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);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_BINARY_SENSOR
class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
public:
@@ -2953,7 +3042,7 @@ class VoiceAssistantAudioSettings final : public ProtoMessage {
class VoiceAssistantRequest final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 90;
static constexpr uint8_t ESTIMATED_SIZE = 41;
static constexpr uint8_t ESTIMATED_SIZE = 68;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("voice_assistant_request"); }
#endif
@@ -2962,6 +3051,11 @@ class VoiceAssistantRequest final : public ProtoMessage {
uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{};
StringRef wake_word_phrase{nullptr, 0}; // null until set, encode only
StringRef wake_word_id{nullptr, 0}; // null until set, encode only
uint32_t wake_word_audio_bytes{0};
float wake_word_probability{0.0f};
float wake_word_probability_cutoff{0.0f};
uint32_t wake_word_audio_bytes2{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);
@@ -3841,7 +3935,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
uint32_t device_id{0};
#endif
uint32_t key{0};
const std::vector<int32_t> *timings{};
const std::span<const int32_t> *timings{};
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);
@@ -4102,6 +4196,7 @@ class UsbDeviceDescriptor final : public ProtoMessage {
uint32_t interface_number{0};
uint32_t interface_class{0};
uint32_t port_number{0};
StringRef interface_description{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);
@@ -4209,5 +4304,30 @@ class SendspinPairingTokenResponse final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_STORE_YAML
class GetYamlResponse final : public ProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 162;
static constexpr uint8_t ESTIMATED_SIZE = 25;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("get_yaml_response"); }
#endif
const uint8_t *data{nullptr};
uint16_t data_len{0};
bool done{false};
uint32_t total_size{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);
}
static uint32_t calc_size_msg(const void *self);
uint32_t calculate_size() const { return calc_size_msg(this); }
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
} // namespace esphome::api
+69
View File
@@ -675,6 +675,8 @@ const char *proto_enum_to_string<enums::VoiceAssistantSubscribeFlag>(enums::Voic
return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_NONE");
case enums::VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO:
return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_API_AUDIO");
case enums::VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO:
return ESPHOME_PSTR("VOICE_ASSISTANT_SUBSCRIBE_WAKE_WORD_AUDIO");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -720,6 +722,8 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V
return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_START");
case enums::VOICE_ASSISTANT_STT_VAD_END:
return ESPHOME_PSTR("VOICE_ASSISTANT_STT_VAD_END");
case enums::VOICE_ASSISTANT_WAKE_WORD_REJECTED:
return ESPHOME_PSTR("VOICE_ASSISTANT_WAKE_WORD_REJECTED");
case enums::VOICE_ASSISTANT_TTS_STREAM_START:
return ESPHOME_PSTR("VOICE_ASSISTANT_TTS_STREAM_START");
case enums::VOICE_ASSISTANT_TTS_STREAM_END:
@@ -1127,6 +1131,13 @@ const char *ZWaveProxyCapabilities::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_STORE_YAML
const char *StoreYamlCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("StoreYamlCapabilities"));
dump_field(out, ESPHOME_PSTR("supported"), this->supported);
return out.c_str();
}
#endif
#ifdef USE_API_WIZARD
const char *WizardCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("WizardCapabilities"));
@@ -1142,6 +1153,13 @@ const char *SendspinCapabilities::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_CRASH_REPORT
const char *CrashReportCapabilities::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("CrashReportCapabilities"));
dump_field(out, ESPHOME_PSTR("flags"), this->flags);
return out.c_str();
}
#endif
const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("DeviceCapabilitiesResponse"));
#ifdef USE_BLUETOOTH_PROXY
@@ -1175,6 +1193,16 @@ const char *DeviceCapabilitiesResponse::dump_to(DumpBuffer &out) const {
out.append(2, ' ').append_p(ESPHOME_PSTR("sendspin")).append(": ");
this->sendspin.dump_to(out);
out.append("\n");
#endif
#ifdef USE_STORE_YAML
out.append(2, ' ').append_p(ESPHOME_PSTR("store_yaml")).append(": ");
this->store_yaml.dump_to(out);
out.append("\n");
#endif
#ifdef USE_CRASH_REPORT
out.append(2, ' ').append_p(ESPHOME_PSTR("crash_report")).append(": ");
this->crash_report.dump_to(out);
out.append("\n");
#endif
return out.c_str();
}
@@ -1197,6 +1225,32 @@ const char *ListEntitiesDoneResponse::dump_to(DumpBuffer &out) const {
out.append_p(ESPHOME_PSTR("ListEntitiesDoneResponse {}"));
return out.c_str();
}
#ifdef USE_CRASH_REPORT
const char *CrashReportCore::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("CrashReportCore"));
dump_field(out, ESPHOME_PSTR("core"), this->core);
for (uint16_t i = 0; i < this->backtrace_len; i++) {
dump_field(out, ESPHOME_PSTR("backtrace"), this->backtrace[i], 4);
}
return out.c_str();
}
const char *CrashReportResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("CrashReportResponse"));
dump_field(out, ESPHOME_PSTR("flags"), this->flags);
dump_field(out, ESPHOME_PSTR("reason"), this->reason);
dump_field(out, ESPHOME_PSTR("pc"), this->pc);
dump_field(out, ESPHOME_PSTR("cause"), this->cause);
dump_field(out, ESPHOME_PSTR("fault_address"), this->fault_address);
dump_field(out, ESPHOME_PSTR("build_time"), this->build_time);
dump_field(out, ESPHOME_PSTR("crashed_core"), this->crashed_core);
for (uint16_t i = 0; i < this->cores_len; i++) {
out.append(4, ' ').append_p(ESPHOME_PSTR("cores")).append(": ");
this->cores[i].dump_to(out);
out.append("\n");
}
return out.c_str();
}
#endif
#ifdef USE_BINARY_SENSOR
const char *ListEntitiesBinarySensorResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesBinarySensorResponse"));
@@ -2391,6 +2445,11 @@ const char *VoiceAssistantRequest::dump_to(DumpBuffer &out) const {
this->audio_settings.dump_to(out);
out.append("\n");
dump_field(out, ESPHOME_PSTR("wake_word_phrase"), this->wake_word_phrase);
dump_field(out, ESPHOME_PSTR("wake_word_id"), this->wake_word_id);
dump_field(out, ESPHOME_PSTR("wake_word_audio_bytes"), this->wake_word_audio_bytes);
dump_field(out, ESPHOME_PSTR("wake_word_probability"), this->wake_word_probability);
dump_field(out, ESPHOME_PSTR("wake_word_probability_cutoff"), this->wake_word_probability_cutoff);
dump_field(out, ESPHOME_PSTR("wake_word_audio_bytes2"), this->wake_word_audio_bytes2);
return out.c_str();
}
const char *VoiceAssistantResponse::dump_to(DumpBuffer &out) const {
@@ -2975,6 +3034,7 @@ const char *UsbDeviceDescriptor::dump_to(DumpBuffer &out) const {
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);
dump_field(out, ESPHOME_PSTR("interface_description"), this->interface_description);
return out.c_str();
}
const char *SerialProxyIdentity::dump_to(DumpBuffer &out) const {
@@ -3016,6 +3076,15 @@ const char *SendspinPairingTokenResponse::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_STORE_YAML
const char *GetYamlResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("GetYamlResponse"));
dump_progmem_bytes_field(out, ESPHOME_PSTR("data"), this->data, this->data_len);
dump_field(out, ESPHOME_PSTR("done"), this->done);
dump_field(out, ESPHOME_PSTR("total_size"), this->total_size);
return out.c_str();
}
#endif
} // namespace esphome::api
@@ -27,6 +27,10 @@
#include "esphome/components/select/select_traits.h"
#endif
#ifdef USE_CRASH_REPORT
#include "esphome/core/crash_report.h"
#endif
// Standard library includes that might be needed
#include <set>
#include <span>
@@ -760,6 +760,24 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
this->on_sendspin_pairing_token_request();
break;
}
#endif
#ifdef USE_STORE_YAML
case 161 /* GetYamlRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_get_yaml_request"));
#endif
this->on_get_yaml_request();
break;
}
#endif
#ifdef USE_CRASH_REPORT
case 163 /* CrashReportRequest is empty */: {
#ifdef HAS_PROTO_MESSAGE_DUMP
this->log_receive_message_(LOG_STR("on_crash_report_request"));
#endif
this->on_crash_report_request();
break;
}
#endif
default:
break;
+7
View File
@@ -39,6 +39,9 @@ class APIServerConnectionBase {
void on_list_entities_request() {};
void on_subscribe_states_request() {};
#ifdef USE_CRASH_REPORT
void on_crash_report_request() {};
#endif
#ifdef USE_COVER
void on_cover_command_request(const CoverCommandRequest &value) {};
@@ -256,6 +259,10 @@ class APIServerConnectionBase {
#ifdef USE_SENDSPIN
void on_sendspin_pairing_token_request() {};
#endif
#ifdef USE_STORE_YAML
void on_get_yaml_request() {};
#endif
};
} // namespace esphome::api
+2 -2
View File
@@ -520,13 +520,13 @@ void APIServer::send_serial_proxy_identity(const SerialProxyIdentity &msg) {
#ifdef USE_IR_RF
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
const std::vector<int32_t> *timings) {
std::span<const int32_t> timings) {
InfraredRFReceiveEvent resp{};
#ifdef USE_DEVICES
resp.device_id = device_id;
#endif
resp.key = key;
resp.timings = timings;
resp.timings = &timings;
for (auto &c : this->active_clients())
c->send_infrared_rf_receive_event(resp);
+2 -1
View File
@@ -30,6 +30,7 @@
#include <array>
#include <memory>
#include <span>
#include <vector>
namespace esphome::api {
@@ -208,7 +209,7 @@ class APIServer final : public Component
void send_serial_proxy_identity(const SerialProxyIdentity &msg);
#endif
#ifdef USE_IR_RF
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, std::span<const int32_t> timings);
#endif
bool is_connected() const { return this->api_connection_count_ != 0; }
+2 -6
View File
@@ -22,10 +22,8 @@ static const uint8_t REGISTER_AGC = 0x1A; // 8 bytes / R
static const uint8_t REGISTER_MAGNITUDE = 0x1B; // 16 bytes / R
void AS5600Component::setup() {
if (!this->read_byte(REGISTER_STATUS).has_value()) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_byte(REGISTER_STATUS).has_value()))
return;
}
// configuration direction pin, if given
// the dir pin on the chip should be low for clockwise
@@ -53,10 +51,8 @@ void AS5600Component::setup() {
// clang-format on
// write config to config register
if (!this->write_byte_16(REGISTER_CONF, config)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte_16(REGISTER_CONF, config)))
return;
}
// configure the start position
this->write_byte_16(REGISTER_ZPOS, this->start_position_);
+1 -3
View File
@@ -315,10 +315,8 @@ void AS734XComponent::setup() {
this->write_byte(AS7343_CHAIN_CMD, 0x46 + (i / len) * 0x10);
}
}
if (!ok) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(ok))
return;
}
this->disable_loop();
}
+3 -6
View File
@@ -1,4 +1,5 @@
#include "ballu.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::ballu {
@@ -90,9 +91,7 @@ void BalluClimate::transmit_state() {
for (uint8_t i = 0; i < BALLU_STATE_LENGTH - 1; i++)
remote_state[12] += remote_state[i];
ESP_LOGV(TAG, "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0],
remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6],
remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12]);
ESP_LOGV_HEX(TAG, remote_state, BALLU_STATE_LENGTH, BALLU_STATE_LENGTH, "Sending: %s");
// Send code
auto transmit = this->transmitter_->transmit();
@@ -136,9 +135,7 @@ bool BalluClimate::on_receive(remote_base::RemoteReceiveData data) {
return false;
}
ESP_LOGV(TAG, "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X", remote_state[0],
remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6],
remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11], remote_state[12]);
ESP_LOGV_HEX(TAG, remote_state, BALLU_STATE_LENGTH, BALLU_STATE_LENGTH, "Received: %s");
// verify header remote code
if (remote_state[0] != 0xc3)
@@ -160,17 +160,14 @@ void spi_dma_tx_finish_callback(unsigned int param) {
void BekenSPILEDStripLightOutput::setup() {
RAMAllocator<uint8_t> allocator;
if (!this->internal_accessor_.allocate(allocator, this->num_leds_)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(this->internal_accessor_.allocate(allocator, this->num_leds_),
LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
const size_t dma_buffer_size = (this->get_buffer_size_() * 8) + (2 * 64);
this->dma_buf_ = allocator.allocate(dma_buffer_size);
if (this->dma_buf_ == nullptr) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(this->dma_buf_ != nullptr, LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
memset(this->dma_buf_, 0, dma_buffer_size);
@@ -180,22 +177,16 @@ void BekenSPILEDStripLightOutput::setup() {
value = PWD_SPI_CLK_BIT;
sddev_control(ICU_DEV_NAME, CMD_CLK_PWR_UP, &value);
if (spi_data != nullptr) {
this->mark_failed(LOG_STR("SPI already initialized"));
if (!this->setup_check(spi_data == nullptr, LOG_STR("SPI already initialized")))
return;
}
spi_data = (SpiData *) calloc(1, sizeof(SpiData)); // NOLINT(cppcoreguidelines-no-malloc)
if (spi_data == nullptr) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_ALLOC_FAIL));
if (!this->setup_check(spi_data != nullptr, LOG_STR(ESP_LOG_MSG_ALLOC_FAIL)))
return;
}
spi_data->dma_tx_semaphore = xSemaphoreCreateBinary();
if (spi_data->dma_tx_semaphore == nullptr) {
this->mark_failed(LOG_STR("TX semaphore init failed"));
if (!this->setup_check(spi_data->dma_tx_semaphore != nullptr, LOG_STR("TX semaphore init failed")))
return;
}
spi_data->first_run = true;
+2 -6
View File
@@ -71,10 +71,8 @@ void BH1745Component::setup() {
this->mark_failed(LOG_STR("Wrong part ID"));
return;
}
if (!this->write_byte(REG_SYSTEM_CONTROL, SYSTEM_CONTROL_SW_RESET)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(REG_SYSTEM_CONTROL, SYSTEM_CONTROL_SW_RESET)))
return;
}
// Light lost behind glass is scaled back up, as the other light sensors do
this->lux_scale_ = 160.0f * this->glass_attenuation_factor_ /
@@ -85,10 +83,8 @@ void BH1745Component::setup() {
// High threshold 0 and low threshold 0xFFFF (low byte first) make the interrupt condition always
// true, so the INT pin simply follows the enable bit, which drives the breakout's LEDs.
static constexpr uint8_t THRESHOLDS[4] = {0x00, 0x00, 0xFF, 0xFF};
if (!this->write_bytes(REG_TH_LSB, THRESHOLDS, sizeof(THRESHOLDS))) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_bytes(REG_TH_LSB, THRESHOLDS, sizeof(THRESHOLDS))))
return;
}
}
// RGBC_EN makes the chip measure continuously; update() reads the latest result.
if (!this->write_byte(REG_MODE_CONTROL1, static_cast<uint8_t>(this->measurement_time_)) ||
+1 -3
View File
@@ -64,10 +64,8 @@ MTreg:
void BH1750Sensor::setup() {
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write(&turn_on, 1) == i2c::ERROR_OK))
return;
}
this->state_ = IDLE;
}
+1 -3
View File
@@ -22,10 +22,8 @@ void BH1900NUXSensor::setup() {
// Initialize I2C device
i2c::ErrorCode result_code =
this->write_register(SOFT_RESET_REG, &SOFT_RESET_PAYLOAD, 1); // Software Reset to check communication
if (result_code != i2c::ERROR_OK) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(result_code == i2c::ERROR_OK))
return;
}
}
void BH1900NUXSensor::update() {
+5 -11
View File
@@ -1,4 +1,5 @@
#include "bl0942.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <cinttypes>
@@ -89,26 +90,19 @@ void BL0942::write_reg_(uint8_t reg, uint32_t val) {
pkt[2] = (val & 0xff);
pkt[3] = (val >> 8) & 0xff;
pkt[4] = (val >> 16) & 0xff;
pkt[5] = (pkt[0] + pkt[1] + pkt[2] + pkt[3] + pkt[4]) ^ 0xff;
pkt[5] = sum_bytes(pkt, 5) ^ 0xFF;
this->write_array(pkt, 6);
delay(1);
}
int BL0942::read_reg_(uint8_t reg) {
union {
uint8_t b[4];
uint32_le_t le32;
} resp;
uint8_t resp[4];
this->write_byte(BL0942_READ_COMMAND | this->address_);
this->write_byte(reg);
this->flush();
if (this->read_array(resp.b, 4) &&
resp.b[3] ==
(uint8_t) ((BL0942_READ_COMMAND + this->address_ + reg + resp.b[0] + resp.b[1] + resp.b[2]) ^ 0xff)) {
resp.b[3] = 0;
return resp.le32;
}
if (this->read_array(resp, 4) && resp[3] == (sum_bytes(resp, 3, BL0942_READ_COMMAND + this->address_ + reg) ^ 0xFF))
return encode_uint24(resp[2], resp[1], resp[0]);
return -1;
}
+1 -3
View File
@@ -21,10 +21,8 @@ static constexpr uint8_t CLOCK_1_60_HZ = 0x83;
static constexpr uint32_t MAX_TIMER_DURATION_S = 255 * 60;
void BM8563::setup() {
if (!this->write_byte_16(CONTROL_STATUS_1_REG, 0)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte_16(CONTROL_STATUS_1_REG, 0)))
return;
}
}
void BM8563::update() { this->read_time(); }
+23 -36
View File
@@ -75,29 +75,21 @@ void BME280Component::setup() {
this->reset_to_construction_state();
}
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_byte(BME280_REGISTER_CHIPID, &chip_id)))
return;
}
if (chip_id != 0x60) {
this->mark_failed(LOG_STR("Wrong chip ID or no response"));
if (!this->setup_check(chip_id == 0x60, LOG_STR("Wrong chip ID or no response")))
return;
}
// Send a soft reset.
if (!this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET)) {
this->mark_failed(LOG_STR("Reset failed"));
if (!this->setup_check(this->write_byte(BME280_REGISTER_RESET, BME280_SOFT_RESET), LOG_STR("Reset failed")))
return;
}
// Wait until the NVM data has finished loading.
uint8_t status;
uint8_t retry = 5;
do { // NOLINT
delay(2);
if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
this->mark_failed(LOG_STR("Error reading status register"));
if (!this->setup_check(this->read_byte(BME280_REGISTER_STATUS, &status), LOG_STR("Error reading status register")))
return;
}
} while ((status & BME280_STATUS_IM_UPDATE) && (--retry));
if (status & BME280_STATUS_IM_UPDATE) {
this->mark_failed(LOG_STR("Timeout loading NVM"));
@@ -129,29 +121,23 @@ void BME280Component::setup() {
this->calibration_.h6 = read_u8_(BME280_REGISTER_DIG_H6);
uint8_t humid_control_val = 0;
if (!this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val)) {
this->mark_failed(LOG_STR("Read humidity control"));
if (!this->setup_check(this->read_byte(BME280_REGISTER_CONTROLHUMID, &humid_control_val),
LOG_STR("Read humidity control")))
return;
}
humid_control_val &= ~0b00000111;
humid_control_val |= this->humidity_oversampling_ & 0b111;
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val)) {
this->mark_failed(LOG_STR("Write humidity control"));
if (!this->setup_check(this->write_byte(BME280_REGISTER_CONTROLHUMID, humid_control_val),
LOG_STR("Write humidity control")))
return;
}
uint8_t config_register = 0;
if (!this->read_byte(BME280_REGISTER_CONFIG, &config_register)) {
this->mark_failed(LOG_STR("Read config"));
if (!this->setup_check(this->read_byte(BME280_REGISTER_CONFIG, &config_register), LOG_STR("Read config")))
return;
}
config_register &= ~0b11111100;
config_register |= 0b101 << 5; // 1000 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->write_byte(BME280_REGISTER_CONFIG, config_register)) {
this->mark_failed(LOG_STR("Write config"));
if (!this->setup_check(this->write_byte(BME280_REGISTER_CONFIG, config_register), LOG_STR("Write config")))
return;
}
}
void BME280Component::dump_config() {
ESP_LOGCONFIG(TAG, "BME280:");
@@ -171,6 +157,10 @@ inline uint8_t oversampling_to_time(BME280Oversampling over_sampling) { return (
void BME280Component::update() {
// Enable sensor
ESP_LOGV(TAG, "Sending conversion request");
if (!this->write_byte(BME280_REGISTER_CONTROLHUMID, this->humidity_oversampling_ & 0b111)) {
this->status_set_warning();
return;
}
uint8_t meas_value = 0;
meas_value |= (this->temperature_oversampling_ & 0b111) << 5;
meas_value |= (this->pressure_oversampling_ & 0b111) << 2;
@@ -210,12 +200,11 @@ void BME280Component::update() {
});
}
float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) {
int32_t adc = encode_uint24(data[3], data[4], data[5]);
adc >>= 4;
if (adc == 0x80000) {
// temperature was disabled
if (this->temperature_oversampling_ == BME280_OVERSAMPLING_NONE) {
return NAN;
}
int32_t adc = encode_uint24(data[3], data[4], data[5]);
adc >>= 4;
const int32_t t1 = this->calibration_.t1;
const int32_t t2 = this->calibration_.t2;
@@ -230,12 +219,11 @@ float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) {
}
float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
int32_t adc = encode_uint24(data[0], data[1], data[2]);
adc >>= 4;
if (adc == 0x80000) {
// pressure was disabled
if (this->pressure_oversampling_ == BME280_OVERSAMPLING_NONE) {
return NAN;
}
int32_t adc = encode_uint24(data[0], data[1], data[2]);
adc >>= 4;
const int64_t p1 = this->calibration_.p1;
const int64_t p2 = this->calibration_.p2;
const int64_t p3 = this->calibration_.p3;
@@ -267,11 +255,10 @@ float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
}
float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
uint16_t const raw_adc = encode_uint16(data[6], data[7]);
if (raw_adc == 0x8000)
if (this->humidity_oversampling_ == BME280_OVERSAMPLING_NONE) {
return NAN;
int32_t const adc = raw_adc;
}
int32_t const adc = encode_uint16(data[6], data[7]);
const int32_t h1 = this->calibration_.h1;
const int32_t h2 = this->calibration_.h2;
+61 -23
View File
@@ -193,12 +193,54 @@ void BME680BSECComponent::loop() {
}
this->status_comm_result(this->bsec_status_ <= BSEC_OK && this->bme680_status_ <= BME680_OK, nullptr);
// Process a single action from the queue. These are primarily sensor state publishes
// that in totality take too long to send in a single call.
if (this->queue_.size()) {
auto action = std::move(this->queue_.front());
this->queue_.pop();
action();
// Publish a single waiting state. All of them in one call take too long.
if (this->pending_.empty())
return;
const PublishSlot slot = this->pending_.front();
this->pending_.pop();
this->pending_mask_.erase(slot);
switch (slot) {
case PUBLISH_SLOT_IAQ:
this->publish_sensor_(this->iaq_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_IAQ_ACCURACY_TEXT:
this->publish_sensor_(
this->iaq_accuracy_text_sensor_,
IaqAccuracyStrings::get_progmem_str(this->pending_accuracy_, IaqAccuracyStrings::LAST_INDEX));
break;
case PUBLISH_SLOT_IAQ_ACCURACY:
this->publish_sensor_(this->iaq_accuracy_sensor_, this->pending_accuracy_, true);
break;
case PUBLISH_SLOT_SAVE_STATE:
this->save_state_(this->pending_accuracy_);
break;
case PUBLISH_SLOT_CO2_EQUIVALENT:
this->publish_sensor_(this->co2_equivalent_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_BREATH_VOC_EQUIVALENT:
this->publish_sensor_(this->breath_voc_equivalent_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_PRESSURE:
this->publish_sensor_(this->pressure_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_GAS_RESISTANCE:
this->publish_sensor_(this->gas_resistance_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_TEMPERATURE:
this->publish_sensor_(this->temperature_sensor_, this->pending_values_[slot]);
break;
case PUBLISH_SLOT_HUMIDITY:
this->publish_sensor_(this->humidity_sensor_, this->pending_values_[slot]);
break;
default:
break;
}
}
void BME680BSECComponent::queue_publish_(PublishSlot slot) {
if (!this->pending_mask_.count(slot)) {
this->pending_mask_.insert(slot);
this->pending_.push(slot);
}
}
@@ -385,35 +427,31 @@ void BME680BSECComponent::publish_(const bsec_output_t *outputs, uint8_t num_out
float signal = outputs[i].signal;
switch (outputs[i].sensor_id) {
case BSEC_OUTPUT_IAQ:
case BSEC_OUTPUT_STATIC_IAQ: {
uint8_t accuracy = std::min<uint8_t>(outputs[i].accuracy, IaqAccuracyStrings::LAST_INDEX);
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_push_([this, accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_,
IaqAccuracyStrings::get_progmem_str(accuracy, IaqAccuracyStrings::LAST_INDEX));
});
this->queue_push_([this, accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, accuracy, true); });
case BSEC_OUTPUT_STATIC_IAQ:
this->pending_accuracy_ = std::min<uint8_t>(outputs[i].accuracy, IaqAccuracyStrings::LAST_INDEX);
this->queue_publish_<PUBLISH_SLOT_IAQ>(signal);
this->queue_publish_(PUBLISH_SLOT_IAQ_ACCURACY_TEXT);
this->queue_publish_(PUBLISH_SLOT_IAQ_ACCURACY);
// Queue up an opportunity to save state
this->queue_push_([this, accuracy]() { this->save_state_(accuracy); });
} break;
this->queue_publish_(PUBLISH_SLOT_SAVE_STATE);
break;
case BSEC_OUTPUT_CO2_EQUIVALENT:
this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); });
this->queue_publish_<PUBLISH_SLOT_CO2_EQUIVALENT>(signal);
break;
case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); });
this->queue_publish_<PUBLISH_SLOT_BREATH_VOC_EQUIVALENT>(signal);
break;
case BSEC_OUTPUT_RAW_PRESSURE:
this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); });
this->queue_publish_<PUBLISH_SLOT_PRESSURE>(signal / 100.0f);
break;
case BSEC_OUTPUT_RAW_GAS:
this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); });
this->queue_publish_<PUBLISH_SLOT_GAS_RESISTANCE>(signal);
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); });
this->queue_publish_<PUBLISH_SLOT_TEMPERATURE>(signal);
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); });
this->queue_publish_<PUBLISH_SLOT_HUMIDITY>(signal);
break;
}
}
+29 -3
View File
@@ -6,8 +6,9 @@
#include "esphome/components/i2c/i2c.h"
#include "esphome/core/preferences.h"
#include "esphome/core/defines.h"
#include "esphome/core/finite_set_mask.h"
#include "esphome/core/helpers.h"
#include <map>
#include <queue>
#ifdef USE_BSEC
#include <bsec.h>
@@ -67,6 +68,22 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
void loop() override;
protected:
// States waiting for loop() to publish them, one per pass
enum PublishSlot : uint8_t {
PUBLISH_SLOT_IAQ,
PUBLISH_SLOT_CO2_EQUIVALENT,
PUBLISH_SLOT_BREATH_VOC_EQUIVALENT,
PUBLISH_SLOT_PRESSURE,
PUBLISH_SLOT_GAS_RESISTANCE,
PUBLISH_SLOT_TEMPERATURE,
PUBLISH_SLOT_HUMIDITY,
// These take pending_accuracy_ instead of a value
PUBLISH_SLOT_IAQ_ACCURACY_TEXT,
PUBLISH_SLOT_IAQ_ACCURACY,
PUBLISH_SLOT_SAVE_STATE,
PUBLISH_SLOT_COUNT,
};
void set_config_();
float calc_sensor_sample_rate_(SampleRate sample_rate);
void update_subscription_();
@@ -91,7 +108,12 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
void save_state_(
uint8_t accuracy); // Save the bsec_state_data_ member (volatile memory) to the ESP preferences (storage)
void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
void queue_publish_(PublishSlot slot);
template<PublishSlot S> void queue_publish_(float value) {
static_assert(S < PUBLISH_SLOT_IAQ_ACCURACY_TEXT, "slot has no value");
this->pending_values_[S] = value;
this->queue_publish_(S);
}
static uint8_t work_buffer_[BSEC_MAX_WORKBUFFER_SIZE];
struct bme680_dev bme680_;
@@ -102,7 +124,11 @@ class BME680BSECComponent final : public Component, public i2c::I2CDevice {
uint32_t millis_overflow_counter_{0};
int64_t next_call_ns_{0};
std::queue<std::function<void()>> queue_;
// Arrival order; a slot already waiting keeps its place and takes the newer value
StaticRingBuffer<PublishSlot, PUBLISH_SLOT_COUNT> pending_;
FiniteSetMask<PublishSlot, DefaultBitPolicy<PublishSlot, PUBLISH_SLOT_COUNT>> pending_mask_;
float pending_values_[PUBLISH_SLOT_IAQ_ACCURACY_TEXT];
uint8_t pending_accuracy_{0};
bool bsec_state_data_valid_;
uint8_t bsec_state_data_[BSEC_MAX_STATE_BLOB_SIZE]; // This is the current snapshot of the BSEC library state
@@ -130,14 +130,62 @@ void BME68xBSEC2Component::check_status_() {
}
void BME68xBSEC2Component::loop() {
// Process a single action from the queue. These are primarily sensor state publishes
// that in totality take too long to send in a single call.
if (this->queue_.size()) {
auto action = std::move(this->queue_.front());
this->queue_.pop();
action();
} else {
// Publish a single waiting state. All of them in one call take too long.
if (this->pending_.empty()) {
this->disable_loop();
return;
}
const PendingPublish pending = this->pending_.front();
this->pending_.pop();
const float value = pending.value;
switch (pending.slot) {
#ifdef USE_SENSOR
case PUBLISH_SLOT_IAQ:
this->publish_sensor_(this->iaq_sensor_, value);
break;
case PUBLISH_SLOT_IAQ_STATIC:
this->publish_sensor_(this->iaq_static_sensor_, value);
break;
case PUBLISH_SLOT_CO2_EQUIVALENT:
this->publish_sensor_(this->co2_equivalent_sensor_, value);
break;
case PUBLISH_SLOT_BREATH_VOC_EQUIVALENT:
this->publish_sensor_(this->breath_voc_equivalent_sensor_, value);
break;
case PUBLISH_SLOT_PRESSURE:
this->publish_sensor_(this->pressure_sensor_, value);
break;
case PUBLISH_SLOT_GAS_RESISTANCE:
this->publish_sensor_(this->gas_resistance_sensor_, value);
break;
case PUBLISH_SLOT_TEMPERATURE:
this->publish_sensor_(this->temperature_sensor_, value);
break;
case PUBLISH_SLOT_HUMIDITY:
this->publish_sensor_(this->humidity_sensor_, value);
break;
case PUBLISH_SLOT_IAQ_ACCURACY:
this->publish_sensor_(this->iaq_accuracy_sensor_, value, true);
break;
#endif
#ifdef USE_TEXT_SENSOR
case PUBLISH_SLOT_IAQ_ACCURACY_TEXT:
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[static_cast<uint8_t>(value)]);
break;
#endif
case PUBLISH_SLOT_SAVE_STATE:
this->save_state_(static_cast<uint8_t>(value));
break;
default:
break;
}
}
void BME68xBSEC2Component::queue_publish_(PublishSlot slot, float value) {
// Only full when the loop stalled for seconds; drop the oldest so the newest reading still goes out
if (!this->pending_.push({value, slot})) {
this->pending_.pop();
this->pending_.push({value, slot});
}
}
@@ -414,44 +462,44 @@ void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_ou
max_accuracy = std::max(outputs[i].accuracy, max_accuracy);
update_accuracy = true;
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_IAQ, signal);
#endif
break;
case BSEC_OUTPUT_STATIC_IAQ:
max_accuracy = std::max(outputs[i].accuracy, max_accuracy);
update_accuracy = true;
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->iaq_static_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_IAQ_STATIC, signal);
#endif
break;
case BSEC_OUTPUT_CO2_EQUIVALENT:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->co2_equivalent_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_CO2_EQUIVALENT, signal);
#endif
break;
case BSEC_OUTPUT_BREATH_VOC_EQUIVALENT:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->breath_voc_equivalent_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_BREATH_VOC_EQUIVALENT, signal);
#endif
break;
case BSEC_OUTPUT_RAW_PRESSURE:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->pressure_sensor_, signal / 100.0f); });
this->queue_publish_(PUBLISH_SLOT_PRESSURE, signal / 100.0f);
#endif
break;
case BSEC_OUTPUT_RAW_GAS:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->gas_resistance_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_GAS_RESISTANCE, signal);
#endif
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_TEMPERATURE:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->temperature_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_TEMPERATURE, signal);
#endif
break;
case BSEC_OUTPUT_SENSOR_HEAT_COMPENSATED_HUMIDITY:
#ifdef USE_SENSOR
this->queue_push_([this, signal]() { this->publish_sensor_(this->humidity_sensor_, signal); });
this->queue_publish_(PUBLISH_SLOT_HUMIDITY, signal);
#endif
break;
}
@@ -459,16 +507,13 @@ void BME68xBSEC2Component::publish_(const bsec_output_t *outputs, uint8_t num_ou
if (update_accuracy) {
max_accuracy = std::min<uint8_t>(max_accuracy, std::size(IAQ_ACCURACY_STATES) - 1);
#ifdef USE_SENSOR
this->queue_push_(
[this, max_accuracy]() { this->publish_sensor_(this->iaq_accuracy_sensor_, max_accuracy, true); });
this->queue_publish_(PUBLISH_SLOT_IAQ_ACCURACY, max_accuracy);
#endif
#ifdef USE_TEXT_SENSOR
this->queue_push_([this, max_accuracy]() {
this->publish_sensor_(this->iaq_accuracy_text_sensor_, IAQ_ACCURACY_STATES[max_accuracy]);
});
this->queue_publish_(PUBLISH_SLOT_IAQ_ACCURACY_TEXT, max_accuracy);
#endif
// Queue up an opportunity to save state
this->queue_push_([this, max_accuracy]() { this->save_state_(max_accuracy); });
this->queue_publish_(PUBLISH_SLOT_SAVE_STATE, max_accuracy);
}
this->enable_loop();
}
+25 -3
View File
@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#ifdef USE_BSEC2
@@ -15,7 +16,6 @@
#endif
#include <cinttypes>
#include <queue>
#include <bsec2.h>
@@ -83,6 +83,28 @@ class BME68xBSEC2Component : public Component {
virtual uint32_t get_hash() = 0;
protected:
// States waiting for loop() to publish them, one per pass
enum PublishSlot : uint8_t {
PUBLISH_SLOT_IAQ,
PUBLISH_SLOT_IAQ_STATIC,
PUBLISH_SLOT_CO2_EQUIVALENT,
PUBLISH_SLOT_BREATH_VOC_EQUIVALENT,
PUBLISH_SLOT_PRESSURE,
PUBLISH_SLOT_GAS_RESISTANCE,
PUBLISH_SLOT_TEMPERATURE,
PUBLISH_SLOT_HUMIDITY,
PUBLISH_SLOT_IAQ_ACCURACY,
PUBLISH_SLOT_IAQ_ACCURACY_TEXT,
PUBLISH_SLOT_SAVE_STATE,
PUBLISH_SLOT_COUNT,
};
struct __attribute__((packed)) PendingPublish {
float value; // the accuracy for the accuracy and save slots
PublishSlot slot;
};
// A read returns up to 3 fields in parallel mode, each published in full
static constexpr uint8_t PUBLISH_QUEUE_SIZE = 3 * PUBLISH_SLOT_COUNT;
void set_config_(const uint8_t *config, u_int32_t len);
float calc_sensor_sample_rate_(SampleRate sample_rate);
void update_subscription_();
@@ -103,14 +125,14 @@ class BME68xBSEC2Component : public Component {
void load_state_();
void save_state_(uint8_t accuracy);
void queue_push_(std::function<void()> &&f) { this->queue_.push(std::move(f)); }
void queue_publish_(PublishSlot slot, float value);
struct bme68x_dev bme68x_;
bsec_bme_settings_t bsec_settings_;
bsec_version_t version_;
struct bme68x_heatr_conf bme68x_heatr_conf_;
std::queue<std::function<void()>> queue_;
StaticRingBuffer<PendingPublish, PUBLISH_QUEUE_SIZE> pending_;
ESPPreferenceObject bsec_state_;
uint8_t const *bsec2_configuration_{nullptr};
+9 -24
View File
@@ -82,59 +82,44 @@ void BMI270Component::setup() {
return;
// 2. Soft-reset via CMD register (0x7E = 0xB6)
if (!this->write_byte(BMI270_REG_CMD, BMI270_CMD_SOFT_RESET)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_CMD, BMI270_CMD_SOFT_RESET)))
return;
}
delay(20);
// 4. Upload the configuration blob
if (!load_config_file_()) {
this->mark_failed(LOG_STR("Config file upload failed"));
if (!this->setup_check(load_config_file_(), LOG_STR("Config file upload failed")))
return;
}
ESP_LOGD(TAG, "Config blob uploaded ✓");
// 5. Configure accelerometer
// ACC_CONF: ODR | BWP(0x2 = normal avg4) | perf_mode(1)
uint8_t acc_conf =
(uint8_t) (accel_odr_) | (BMI270_CONF_BWP_NORMAL << BMI270_CONF_BWP_SHIFT) | BMI270_ACC_CONF_PERF_MODE;
if (!this->write_byte(BMI270_REG_ACC_CONF, acc_conf)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_ACC_CONF, acc_conf)))
return;
}
if (!this->write_byte(BMI270_REG_ACC_RANGE, (uint8_t) accel_range_)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_ACC_RANGE, (uint8_t) accel_range_)))
return;
}
// 6. Configure gyroscope
// GYR_CONF: ODR | BWP(0x2 = normal) | noise_perf(1) | filter_perf(1)
uint8_t gyr_conf = (uint8_t) (gyro_odr_) | (BMI270_CONF_BWP_NORMAL << BMI270_CONF_BWP_SHIFT) |
BMI270_GYR_CONF_NOISE_PERF | BMI270_GYR_CONF_FILTER_PERF;
if (!this->write_byte(BMI270_REG_GYR_CONF, gyr_conf)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_GYR_CONF, gyr_conf)))
return;
}
if (!this->write_byte(BMI270_REG_GYR_RANGE, (uint8_t) gyro_range_)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_GYR_RANGE, (uint8_t) gyro_range_)))
return;
}
// 7. Enable accelerometer, gyroscope, and temperature sensor
// PWR_CTRL bits: temp_en[3] | gyr_en[2] | acc_en[1]
if (!this->write_byte(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_IMU)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->write_byte(BMI270_REG_PWR_CTRL, BMI270_PWR_CTRL_IMU)))
return;
}
delay(5);
// 8. Re-enable advanced power save (optional; keeps current low between reads)
// Disabled here for simplicity – leave in performance mode
if (!this->write_byte(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_FIFO_SELF_WAKEUP)) { // bit1 = fifo_self_wakeup
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
// bit1 = fifo_self_wakeup
if (!this->setup_check(this->write_byte(BMI270_REG_PWR_CONF, BMI270_PWR_CONF_FIFO_SELF_WAKEUP)))
return;
}
// 9. Optionally bring up whatever is wired to the aux interface.
// This is entirely additive: it is off by default, and failure only disables
+1 -3
View File
@@ -24,10 +24,8 @@ void BMP085Component::update() {
}
void BMP085Component::setup() {
uint8_t data[22];
if (!this->read_bytes(BMP085_REGISTER_AC1_H, data, 22)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->read_bytes(BMP085_REGISTER_AC1_H, data, 22)))
return;
}
// Load calibration
this->calibration_.ac1 = encode_uint16(data[0], data[1]);
+14 -29
View File
@@ -40,33 +40,24 @@ void BMP280Component::setup() {
// Read the chip id twice, to work around a bug where the first read is 0.
// https://community.st.com/t5/stm32-mcus-products/issue-with-reading-bmp280-chip-id-using-spi/td-p/691855
if (!this->bmp_read_byte(0xD0, &chip_id)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->bmp_read_byte(0xD0, &chip_id)))
return;
}
if (!this->bmp_read_byte(0xD0, &chip_id)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(this->bmp_read_byte(0xD0, &chip_id)))
return;
}
if (chip_id != 0x58) {
this->mark_failed(LOG_STR("Wrong chip ID or no response"));
if (!this->setup_check(chip_id == 0x58, LOG_STR("Wrong chip ID or no response")))
return;
}
// Send a soft reset.
if (!this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET)) {
this->mark_failed(LOG_STR("Reset failed"));
if (!this->setup_check(this->bmp_write_byte(BMP280_REGISTER_RESET, BMP280_SOFT_RESET), LOG_STR("Reset failed")))
return;
}
// Wait until the NVM data has finished loading.
uint8_t status;
uint8_t retry = 5;
do {
delay(2);
if (!this->bmp_read_byte(BMP280_REGISTER_STATUS, &status)) {
this->mark_failed(LOG_STR("Error reading status register"));
if (!this->setup_check(this->bmp_read_byte(BMP280_REGISTER_STATUS, &status),
LOG_STR("Error reading status register")))
return;
}
} while ((status & BMP280_STATUS_IM_UPDATE) && (--retry));
if (status & BMP280_STATUS_IM_UPDATE) {
this->mark_failed(LOG_STR("Timeout loading NVM"));
@@ -89,17 +80,13 @@ void BMP280Component::setup() {
this->calibration_.p9 = this->read_s16_le_(0x9E);
uint8_t config_register = 0;
if (!this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register)) {
this->mark_failed(LOG_STR("Read config"));
if (!this->setup_check(this->bmp_read_byte(BMP280_REGISTER_CONFIG, &config_register), LOG_STR("Read config")))
return;
}
config_register &= ~0b11111100;
config_register |= 0b000 << 5; // 0.5 ms standby time
config_register |= (this->iir_filter_ & 0b111) << 2;
if (!this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register)) {
this->mark_failed(LOG_STR("Write config"));
if (!this->setup_check(this->bmp_write_byte(BMP280_REGISTER_CONFIG, config_register), LOG_STR("Write config")))
return;
}
}
void BMP280Component::dump_config() {
ESP_LOGCONFIG(TAG, "BMP280:");
@@ -148,16 +135,15 @@ void BMP280Component::update() {
}
float BMP280Component::read_temperature_(int32_t *t_fine) {
if (this->temperature_oversampling_ == BMP280_OVERSAMPLING_NONE) {
return NAN;
}
uint8_t data[3]{};
if (!this->bmp_read_bytes(BMP280_REGISTER_TEMPDATA, data, 3))
return NAN;
ESP_LOGV(TAG, "Read temperature data, raw: %02X %02X %02X", data[0], data[1], data[2]);
int32_t adc = encode_uint24(data[0], data[1], data[2]);
adc >>= 4;
if (adc == 0x80000) {
// temperature was disabled
return NAN;
}
const int32_t t1 = this->calibration_.t1;
const int32_t t2 = this->calibration_.t2;
@@ -172,15 +158,14 @@ float BMP280Component::read_temperature_(int32_t *t_fine) {
}
float BMP280Component::read_pressure_(int32_t t_fine) {
if (this->pressure_oversampling_ == BMP280_OVERSAMPLING_NONE) {
return NAN;
}
uint8_t data[3];
if (!this->bmp_read_bytes(BMP280_REGISTER_PRESSUREDATA, data, 3))
return NAN;
int32_t adc = encode_uint24(data[0], data[1], data[2]);
adc >>= 4;
if (adc == 0x80000) {
// pressure was disabled
return NAN;
}
const int64_t p1 = this->calibration_.p1;
const int64_t p2 = this->calibration_.p2;
const int64_t p3 = this->calibration_.p3;
+8 -16
View File
@@ -45,15 +45,11 @@ static const LogString *iir_filter_to_str(IIRFilter filter) {
void BMP3XXComponent::setup() {
// Call the Device base class "initialise" function
if (!reset()) {
this->mark_failed(LOG_STR("Failed to reset"));
if (!this->setup_check(this->reset(), LOG_STR("Failed to reset")))
return;
}
if (!read_byte(BMP388_CHIP_ID, &this->chip_id_.reg)) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(read_byte(BMP388_CHIP_ID, &this->chip_id_.reg)))
return;
}
ESP_LOGCONFIG(TAG, "Chip %s Id 0x%X", LOG_STR_ARG(chip_type_to_str(this->chip_id_.reg)), this->chip_id_.reg);
if (chip_id_.reg != BMP388_ID && chip_id_.reg != BMP390_ID) {
@@ -63,10 +59,10 @@ void BMP3XXComponent::setup() {
// set sensor in sleep mode
stop_conversion();
// Read the calibration parameters into the params structure
if (!read_bytes(BMP388_TRIM_PARAMS, (uint8_t *) &compensation_params_, sizeof(compensation_params_))) {
this->mark_failed(LOG_STR("Can't read calibration data"));
if (!this->setup_check(
read_bytes(BMP388_TRIM_PARAMS, (uint8_t *) &compensation_params_, sizeof(compensation_params_)),
LOG_STR("Can't read calibration data")))
return;
}
compensation_float_params_.param_T1 =
(float) compensation_params_.param_T1 / powf(2.0f, -8.0f); // Calculate the floating point trim parameters
compensation_float_params_.param_T2 = (float) compensation_params_.param_T2 / powf(2.0f, 30.0f);
@@ -84,10 +80,8 @@ void BMP3XXComponent::setup() {
compensation_float_params_.param_P11 = (float) compensation_params_.param_P11 / powf(2.0f, 65.0f);
// Initialise the BMP388 IIR filter register
if (!set_iir_filter(this->iir_filter_)) {
this->mark_failed(LOG_STR("Failed to set IIR filter"));
if (!this->setup_check(this->set_iir_filter(this->iir_filter_), LOG_STR("Failed to set IIR filter")))
return;
}
// Set power control registers
pwr_ctrl_.bit.press_en = 1;
@@ -103,10 +97,8 @@ void BMP3XXComponent::setup() {
this->temperature_oversampling_ = OVERSAMPLING_NONE;
}
// Initialise the BMP388 oversampling register
if (!set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_)) {
this->mark_failed(LOG_STR("Failed to set oversampling register"));
return;
}
this->setup_check(this->set_oversampling_register(this->pressure_oversampling_, this->temperature_oversampling_),
LOG_STR("Failed to set oversampling register"));
}
void BMP3XXComponent::dump_config() {
+9 -15
View File
@@ -4,6 +4,7 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import i2c
from esphome.components.const import CONF_CHARGE_ENABLED, CONF_INPUT_CURRENT_LIMIT
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_UPDATE_INTERVAL, CONF_WATCHDOG
from esphome.core import CORE, ID, EsphomeError, Lambda, TimePeriod
@@ -19,7 +20,6 @@ CONF_BQ25186_ID = "bq25186_id"
CONF_CONFIGURE_ON_BOOT = "configure_on_boot"
CONF_BATTERY_REGULATION_VOLTAGE = "battery_regulation_voltage"
CONF_FAST_CHARGE_CURRENT = "fast_charge_current"
CONF_CHARGE_ENABLED = "charge_enabled"
CONF_FLASH_CHARGING_MODE = "flash_charging_mode"
CONF_PRECHARGE_IS_ITERM = "precharge_is_iterm"
CONF_TERMINATION_PERCENT = "termination_percent"
@@ -38,7 +38,6 @@ CONF_SAFETY_TIMER = "safety_timer"
CONF_LONG_PRESS_DURATION = "long_press_duration"
CONF_HW_RESET_REQUIRES_VIN = "hw_reset_requires_vin"
CONF_AUTOWAKE = "autowake"
CONF_INPUT_CURRENT_LIMIT = "input_current_limit"
CONF_PUSH_BUTTON_LONG_PRESS_ACTION = "push_button_long_press_action"
CONF_WAKE1_TIMER = "wake1_timer"
CONF_WAKE2_TIMER = "wake2_timer"
@@ -66,19 +65,14 @@ BQ25186Component = bq25186_ns.class_(
BQ25186_CHILD_SCHEMA = cv.Schema(
{cv.GenerateID(CONF_BQ25186_ID): cv.use_id(BQ25186Component)}
)
_request_binary_sensor_slot = cg.slot_counter("BQ25186_BINARY_SENSOR_COUNT")
_request_text_sensor_slot = cg.slot_counter("BQ25186_TEXT_SENSOR_COUNT")
def register_binary_sensor(hub: cg.MockObj, var: cg.MockObj) -> cg.Expression:
_request_binary_sensor_slot(str(hub))
return hub.register_binary_sensor(var)
def register_text_sensor(hub: cg.MockObj, var: cg.MockObj) -> cg.Expression:
_request_text_sensor_slot(str(hub))
return hub.register_text_sensor(var)
BQ25186StatusRegisters = bq25186_ns.class_("BQ25186StatusRegisters")
STATUS_PUBLISHER = cg.HubFunction(
"status",
cg.void,
[(BQ25186StatusRegisters.const_ref(), "status")],
define="USE_BQ25186_STATUS_PUBLISHER",
)
STATUS = STATUS_PUBLISHER.param("status")
REG_VBAT_CTRL = 0x03
REG_ICHG_CTRL = 0x04
+15 -11
View File
@@ -1,5 +1,4 @@
from functools import partial
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
@@ -9,16 +8,13 @@ from esphome.const import (
DEVICE_CLASS_PLUG,
DEVICE_CLASS_RUNNING,
)
from esphome.cpp_generator import HexIntLiteral
from esphome.types import ConfigType
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns, register_binary_sensor
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, STATUS, STATUS_PUBLISHER
DEPENDENCIES = ["bq25186"]
BQ25186BinarySensor = bq25186_ns.class_(
"BQ25186BinarySensor", binary_sensor.BinarySensor
)
REG_STAT0 = 0
REG_STAT1 = 1
REG_FLAG0 = 2
@@ -51,11 +47,19 @@ _FIELDS = (
)
def _state_expression(reg: int, shift: int, mask: int, match: int) -> cg.MockObj:
"""Whether the field (status[reg] >> shift) & mask reads as on, as a bit of match."""
value = (STATUS[reg] >> shift) & HexIntLiteral(mask)
if mask == 0x01 and match == _BIT_SET:
return value
return (cg.MockObj(f"{match:#06b}") >> value) & 1
CONFIG_SCHEMA = cv.All(
BQ25186_CHILD_SCHEMA.extend(
{
cv.Optional(key): binary_sensor.binary_sensor_schema(
BQ25186BinarySensor, device_class=device_class
device_class=device_class
)
for key, *_, device_class in _FIELDS
}
@@ -68,6 +72,6 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_BQ25186_ID])
sensors = binary_sensor.sub_binary_sensors(config)
for key, reg, shift, mask, match, _ in _FIELDS:
await sensors(
key, partial(register_binary_sensor, hub), reg, shift, mask, match
)
if (var := await sensors(key, None)) is not None:
state = _state_expression(reg, shift, mask, match)
STATUS_PUBLISHER.add(hub, parent_ref(var).publish_state(state))
+3 -13
View File
@@ -1,10 +1,4 @@
#include "bq25186.h"
#ifdef USE_BINARY_SENSOR
#include "bq25186_binary_sensor.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "bq25186_text_sensor.h"
#endif
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
@@ -144,13 +138,9 @@ void BQ25186Component::update() {
#endif
if (!pending)
this->status_clear_warning();
#ifdef BQ25186_BINARY_SENSOR_COUNT
for (auto *sensor : this->binary_sensors_)
sensor->on_status(status);
#endif
#ifdef BQ25186_TEXT_SENSOR_COUNT
for (auto *sensor : this->text_sensors_)
sensor->on_status(status);
#ifdef USE_BQ25186_STATUS_PUBLISHER
if (this->status_publisher_ != nullptr)
this->status_publisher_(status);
#endif
}
+21 -13
View File
@@ -7,6 +7,10 @@
#ifdef USE_BQ25186_PG_GPO
#include "esphome/components/switch/switch.h"
#endif
#ifdef USE_TEXT_SENSOR
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/core/progmem.h"
#endif
#include <array>
@@ -38,8 +42,21 @@ static constexpr uint8_t BQ25186_SYS_REG_PG_GPO_LEVEL = 0x10;
using BQ25186ConfigRegisters = std::array<uint8_t, BQ25186_CONFIG_REGISTER_COUNT>;
using BQ25186StatusRegisters = std::array<uint8_t, BQ25186_STATUS_REGISTER_COUNT>;
class BQ25186BinarySensor;
class BQ25186TextSensor;
#ifdef USE_TEXT_SENSOR
// STAT0[6:5] charge status, then STAT1[4:3] TS status
PROGMEM_STRING_TABLE(BQ25186StatusStrings, "Not Charging", "Constant Current", "Constant Voltage",
"Charge Done/Disabled", "Normal", "Hot/Cold (Charging Suspended)", "Cool (Current Reduced)",
"Warm (Voltage Reduced)");
/// Publish the two-bit field in the low bits of `value` as string `first + field`, only when it differs from `last`
inline void publish_status_text(text_sensor::TextSensor *sensor, uint8_t &last, uint8_t value, uint8_t first) {
value &= 0x03;
if (value == last)
return;
last = value;
sensor->publish_state(BQ25186StatusStrings::get_progmem_str(first + value, BQ25186StatusStrings::LAST_INDEX));
}
#endif
class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
public:
@@ -52,11 +69,8 @@ class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
this->configure_on_boot_ = true;
}
bool apply_config(const BQ25186ConfigRegisters &regs);
#ifdef BQ25186_BINARY_SENSOR_COUNT
void register_binary_sensor(BQ25186BinarySensor *sensor) { this->binary_sensors_.push_back(sensor); }
#endif
#ifdef BQ25186_TEXT_SENSOR_COUNT
void register_text_sensor(BQ25186TextSensor *sensor) { this->text_sensors_.push_back(sensor); }
#ifdef USE_BQ25186_STATUS_PUBLISHER
HUB_PUBLISHER(status, const BQ25186StatusRegisters &)
#endif
#ifdef USE_BQ25186_PG_GPO
/// The /PG/GPO pin is put in general purpose output mode when a switch drives it
@@ -78,12 +92,6 @@ class BQ25186Component : public PollingComponent, public i2c::I2CDevice {
void check_pg_gpo_(const BQ25186StatusRegisters &status, bool &pending);
#endif
#ifdef BQ25186_BINARY_SENSOR_COUNT
StaticVector<BQ25186BinarySensor *, BQ25186_BINARY_SENSOR_COUNT> binary_sensors_;
#endif
#ifdef BQ25186_TEXT_SENSOR_COUNT
StaticVector<BQ25186TextSensor *, BQ25186_TEXT_SENSOR_COUNT> text_sensors_;
#endif
BQ25186ConfigRegisters regs_{}; // the configuration last applied, written again after a chip reset
bool configure_on_boot_{false};
bool configured_{false}; // regs_ went to the chip at least once
@@ -1,31 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_BINARY_SENSOR
#include "bq25186.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::bq25186 {
/// One field of a status register; `match` has a bit set for each field value that reads as on
class BQ25186BinarySensor final : public binary_sensor::BinarySensor {
public:
BQ25186BinarySensor(uint8_t reg, uint8_t shift, uint8_t mask, uint8_t match)
: reg_(reg), shift_(shift), mask_(mask), match_(match) {}
void on_status(const BQ25186StatusRegisters &status) {
const uint8_t value = (status[this->reg_] >> this->shift_) & this->mask_;
this->publish_state((this->match_ >> value) & 1);
}
protected:
const uint8_t reg_;
const uint8_t shift_;
const uint8_t mask_;
const uint8_t match_;
};
} // namespace esphome::bq25186
#endif // USE_BINARY_SENSOR
@@ -1,22 +0,0 @@
#include "bq25186_text_sensor.h"
#ifdef USE_TEXT_SENSOR
#include "esphome/core/progmem.h"
namespace esphome::bq25186 {
// STAT0[6:5] charge status, then STAT1[4:3] TS status
PROGMEM_STRING_TABLE(StatusStrings, "Not Charging", "Constant Current", "Constant Voltage", "Charge Done/Disabled",
"Normal", "Hot/Cold (Charging Suspended)", "Cool (Current Reduced)", "Warm (Voltage Reduced)");
void BQ25186TextSensor::on_status(const BQ25186StatusRegisters &status) {
const uint8_t value = (status[this->reg_] >> this->shift_) & 0x03;
if (value == this->last_)
return;
this->last_ = value;
this->publish_state(StatusStrings::get_progmem_str(this->first_ + value, StatusStrings::LAST_INDEX));
}
} // namespace esphome::bq25186
#endif // USE_TEXT_SENSOR
@@ -1,26 +0,0 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_TEXT_SENSOR
#include "bq25186.h"
#include "esphome/components/text_sensor/text_sensor.h"
namespace esphome::bq25186 {
/// A two-bit status field; `first` is the string table index of the field's value 0
class BQ25186TextSensor final : public text_sensor::TextSensor {
public:
BQ25186TextSensor(uint8_t reg, uint8_t shift, uint8_t first) : reg_(reg), shift_(shift), first_(first) {}
void on_status(const BQ25186StatusRegisters &status);
protected:
const uint8_t reg_;
const uint8_t shift_;
const uint8_t first_;
uint8_t last_{0xFF}; // the field value last published
};
} // namespace esphome::bq25186
#endif // USE_TEXT_SENSOR
+35 -10
View File
@@ -1,16 +1,24 @@
from functools import partial
from esphome.automation import parent_ref
import esphome.codegen as cg
from esphome.components import text_sensor
import esphome.config_validation as cv
from esphome.cpp_generator import (
AssignmentExpression,
HexIntLiteral,
VariableDeclarationExpression,
)
from esphome.types import ConfigType
from . import BQ25186_CHILD_SCHEMA, CONF_BQ25186_ID, bq25186_ns, register_text_sensor
from . import (
BQ25186_CHILD_SCHEMA,
CONF_BQ25186_ID,
STATUS,
STATUS_PUBLISHER,
bq25186_ns,
)
DEPENDENCIES = ["bq25186"]
BQ25186TextSensor = bq25186_ns.class_("BQ25186TextSensor", text_sensor.TextSensor)
REG_STAT0 = 0
REG_STAT1 = 1
@@ -20,12 +28,28 @@ _FIELDS = (
("ts_status", REG_STAT1, 3, 4),
)
def _add_text_status(
hub: cg.MockObj, var: cg.MockObj, reg: int, shift: int, first: int
) -> None:
"""Publish status string first + the two-bit field at status[reg] >> shift, on change."""
last = cg.MockObj(f"esphome__{var}__last")
# 0xFF so the first poll publishes
declaration = VariableDeclarationExpression(cg.uint8, "", last, static=True)
STATUS_PUBLISHER.add(
hub, AssignmentExpression(None, None, declaration, HexIntLiteral(0xFF))
)
STATUS_PUBLISHER.add(
hub,
bq25186_ns.publish_status_text(
parent_ref(var), last, STATUS[reg] >> shift, first
),
)
CONFIG_SCHEMA = cv.All(
BQ25186_CHILD_SCHEMA.extend(
{
cv.Optional(key): text_sensor.text_sensor_schema(BQ25186TextSensor)
for key, *_ in _FIELDS
}
{cv.Optional(key): text_sensor.text_sensor_schema() for key, *_ in _FIELDS}
),
cv.has_at_least_one_key(*(key for key, *_ in _FIELDS)),
)
@@ -35,4 +59,5 @@ async def to_code(config: ConfigType) -> None:
hub = await cg.get_variable(config[CONF_BQ25186_ID])
sensors = text_sensor.sub_text_sensors(config)
for key, reg, shift, first in _FIELDS:
await sensors(key, partial(register_text_sensor, hub), reg, shift, first)
if (var := await sensors(key, None)) is not None:
_add_text_status(hub, var, reg, shift, first)
+4 -6
View File
@@ -27,16 +27,14 @@ static constexpr uint16_t TIME_TO_EMPTY_NOT_APPLICABLE = 0xFFFF;
void BQ27220Component::setup() {
const uint8_t device_number_cmd[2] = {0x01, 0x00}; // DEVICE_NUMBER subcommand, little-endian
if (this->write_register(REG_CONTROL, device_number_cmd, 2) != i2c::ERROR_OK) {
this->mark_failed(LOG_STR("Failed to communicate with BQ27220"));
if (!this->setup_check(this->write_register(REG_CONTROL, device_number_cmd, 2) == i2c::ERROR_OK,
LOG_STR("Failed to communicate with BQ27220")))
return;
}
delay(2); // SLUUBD4A §5.3: the subcommand result needs a moment before it can be read
uint16_t device_number = 0;
if (!this->read_byte_16_le(REG_MAC_DATA, &device_number)) {
this->mark_failed(LOG_STR("Failed to read device number from BQ27220"));
if (!this->setup_check(this->read_byte_16_le(REG_MAC_DATA, &device_number),
LOG_STR("Failed to read device number from BQ27220")))
return;
}
if (device_number != DEVICE_NUMBER) {
ESP_LOGE(TAG, "Unexpected device number 0x%04X (expected 0x%04X)", device_number, DEVICE_NUMBER);
this->mark_failed(LOG_STR("Unexpected device number"));
@@ -10,6 +10,8 @@ CODEOWNERS = ["@nagyrobi"]
AUTO_LOAD = ["ble_device_base"]
DOMAIN = "bthome_mithermometer"
CONF_REPLAY_PROTECTION = "replay_protection"
bthome_mithermometer_ns = cg.esphome_ns.namespace("bthome_mithermometer")
BTHomeMiThermometer = bthome_mithermometer_ns.class_(
@@ -29,14 +31,25 @@ def bthome_mithermometer_base_schema(
cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer),
cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
cv.Optional(CONF_BINDKEY): cv.bind_key,
cv.Optional(CONF_REPLAY_PROTECTION, default=False): cv.boolean,
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(extra_schema)
.extend(ble_device_base.BLE_DEVICE_SCHEMA),
_validate_replay_protection,
)
def _validate_replay_protection(config: ConfigType) -> ConfigType:
if config[CONF_REPLAY_PROTECTION] and CONF_BINDKEY not in config:
raise cv.Invalid(
f"'{CONF_REPLAY_PROTECTION}' requires '{CONF_BINDKEY}'",
path=[CONF_REPLAY_PROTECTION],
)
return config
async def setup_bthome_mithermometer(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
await ble_device_base.register_ble_device(var, config)
@@ -47,3 +60,6 @@ async def setup_bthome_mithermometer(var: MockObj, config: ConfigType) -> None:
for index in range(0, len(bindkey), 2)
]
cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes)))
if config[CONF_REPLAY_PROTECTION]:
cg.add_define("USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION")
cg.add(var.set_replay_protection(True))
@@ -1,10 +1,12 @@
#include "bthome_ble.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <algorithm>
#include <array>
#include <cinttypes>
#include <cstring>
#include <span>
@@ -31,6 +33,9 @@ static constexpr size_t BTHOME_BINDKEY_SIZE = 16;
static constexpr size_t BTHOME_NONCE_SIZE = 13;
static constexpr size_t BTHOME_MIC_SIZE = 4;
static constexpr size_t BTHOME_COUNTER_SIZE = 4;
// If no frame with a higher counter has been accepted for this long, a lower
// counter is accepted again (the sensor rebooted, e.g. after a battery swap).
static constexpr uint32_t BTHOME_REPLAY_RESYNC_MS = 10 * 60 * 1000;
// Both callers are log macros (LOGCONFIG / LOGVV); below CONFIG level they
// compile away and an ungated helper trips -Wunused-function.
@@ -156,6 +161,9 @@ void BTHomeMiThermometer::dump_config() {
if (this->has_bindkey_) {
char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)];
ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.'));
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
ESP_LOGCONFIG(TAG, " Replay Protection: %s", YESNO(this->replay_protection_));
#endif
}
LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
LOG_SENSOR(" ", "Humidity", this->humidity_sensor_);
@@ -253,6 +261,40 @@ bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector<uint8_t> &da
return true;
}
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
// PVVX sends new data under an unchanged counter, so only an exact repeat (counter and MIC) is a duplicate and
// earlier frames under the current counter can be replayed; the lower-counter rule is the defence. It holds only
// while the sensor keeps being heard: after the resync window one old frame becomes the baseline and recorded
// frames above it can follow in order, the price of recovering from a counter restart. RAM only.
bool BTHomeMiThermometer::check_replay_counter_(uint32_t counter, uint32_t mic) {
const uint32_t now = App.get_loop_component_start_time();
if (counter == this->last_counter_ && mic == this->last_mic_) {
ESP_LOGVV(TAG, "Duplicate BTHome frame (counter %" PRIu32 ")", counter);
return false;
}
if (counter < this->last_counter_) {
if (now - this->last_accepted_ms_ < BTHOME_REPLAY_RESYNC_MS) {
// One warning per run of rejections
if (this->replay_rejected_) {
ESP_LOGVV(TAG, "BTHome frame rejected: counter %" PRIu32 " lower than last %" PRIu32, counter,
this->last_counter_);
} else {
ESP_LOGW(TAG, "BTHome frame rejected: counter %" PRIu32 " lower than last %" PRIu32 " (replay?)", counter,
this->last_counter_);
this->replay_rejected_ = true;
}
return false;
}
ESP_LOGW(TAG, "BTHome counter restarted (%" PRIu32 " -> %" PRIu32 "), resync", this->last_counter_, counter);
}
this->last_counter_ = counter;
this->last_mic_ = mic;
this->last_accepted_ms_ = now;
this->replay_rejected_ = false;
return true;
}
#endif
bool BTHomeMiThermometer::handle_service_data_(const ble_device_base::ServiceData &service_data,
const ble_device_base::ESPBTDevice &device) {
if (!service_data.uuid.contains(0xD2, 0xFC)) {
@@ -339,6 +381,17 @@ bool BTHomeMiThermometer::handle_service_data_(const ble_device_base::ServiceDat
return false;
}
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
// After the MIC and address checks, so other devices never touch the state
if (is_encrypted && this->replay_protection_) {
const uint8_t *tail = data.data() + data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE;
if (!this->check_replay_counter_(encode_value_le<uint32_t>(tail),
encode_value_le<uint32_t>(tail + BTHOME_COUNTER_SIZE))) {
return false;
}
}
#endif
if (payload_size == 0) {
ESP_LOGVV(TAG, "BTHome payload empty after header");
return false;
@@ -13,6 +13,10 @@
// decryption availability is selected per platform in the .cpp.
namespace esphome::bthome_mithermometer {
namespace testing {
class ReplayClockAccess;
} // namespace testing
class BTHomeMiThermometer final : public ble_device_base::ESPBTDeviceListener, public Component {
SUB_SENSOR(temperature)
SUB_SENSOR(humidity)
@@ -23,18 +27,36 @@ class BTHomeMiThermometer final : public ble_device_base::ESPBTDeviceListener, p
public:
void set_address(uint64_t address) { this->address_ = address; }
void set_bindkey(std::initializer_list<uint8_t> bindkey);
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
void set_replay_protection(bool replay_protection) { this->replay_protection_ = replay_protection; }
#endif
void dump_config() override;
bool parse_device(const ble_device_base::ESPBTDevice &device) override;
protected:
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
friend class testing::ReplayClockAccess;
#endif
bool handle_service_data_(const ble_device_base::ServiceData &service_data,
const ble_device_base::ESPBTDevice &device);
bool decrypt_bthome_payload_(const std::vector<uint8_t> &data, uint64_t source_address,
std::vector<uint8_t> &payload) const;
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
bool check_replay_counter_(uint32_t counter, uint32_t mic);
#endif
uint64_t address_{0};
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
uint32_t last_counter_{0};
uint32_t last_mic_{0};
uint32_t last_accepted_ms_{0};
#endif
optional<uint8_t> last_packet_id_{};
#ifdef USE_BTHOME_MITHERMOMETER_REPLAY_PROTECTION
bool replay_protection_{false};
bool replay_rejected_{false};
#endif
bool has_bindkey_{false};
uint8_t bindkey_[16];
};
+147 -30
View File
@@ -4,10 +4,12 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.config import iter_ids
import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_PLATFORM, CONF_TRIGGER_ID
from esphome.const import CONF_DATA, CONF_ID, CONF_PLATFORM
from esphome.core import CORE, ID
from esphome.cpp_generator import MockObj, TemplateArgsType
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -24,8 +26,25 @@ CONF_CANBUS_ID = "canbus_id"
CONF_BIT_RATE = "bit_rate"
CONF_ON_FRAME = "on_frame"
CONF_MAX_FRAMES_PER_LOOP = "max_frames_per_loop"
CONF_FD = "fd"
CONF_BIT_RATE_SWITCH = "bit_rate_switch"
CONF_PAD_DATA = "pad_data"
CONF_DEFAULT_FD = "default_fd"
CONF_DEFAULT_BIT_RATE_SWITCH = "default_bit_rate_switch"
CONF_DEFAULT_PAD_DATA = "default_pad_data"
DEFAULT_MAX_FRAMES_PER_LOOP = 50 # Keep in sync with max_frames_per_loop_ in canbus.h
DEFAULT_BIT_RATE = "125KBPS" # Keep in sync with bit_rate_ in canbus.h
# All 29 id bits; received ids never use more, so on_frame skips the & for this mask
CAN_ID_MASK_ALL = 0x1FFFFFFF
CAN_MAX_DATA_LENGTH = 8
# Payload length of each CAN FD DLC, keep in sync with CAN_FD_DLC_LENGTHS in canbus.h
CAN_FD_DATA_LENGTHS = (0, 1, 2, 3, 4, 5, 6, 7, 8, 12, 16, 20, 24, 32, 48, 64)
# canbus.send options that need a platform built on CANBUS_FD_SCHEMA
_FD_SEND_KEYS = (CONF_FD, CONF_BIT_RATE_SWITCH, CONF_PAD_DATA)
def _flag(name: str, value: bool) -> str:
return name if value else f"!{name}"
def validate_id(config: ConfigType) -> ConfigType:
@@ -49,11 +68,8 @@ def validate_raw_data(value: Any) -> bytes | list:
canbus_ns = cg.esphome_ns.namespace("canbus")
CanbusComponent = canbus_ns.class_("CanbusComponent", cg.Component)
CanbusTrigger = canbus_ns.class_(
"CanbusTrigger",
automation.Trigger.template(cg.std_vector.template(cg.uint8), cg.uint32),
cg.Component,
)
CanFrame = canbus_ns.struct("CanFrame")
CanPayload = canbus_ns.class_("CanPayload")
CanSpeed = canbus_ns.enum("CAN_SPEED")
CAN_SPEEDS = CanSpeed.options(
@@ -81,6 +97,21 @@ CAN_SPEEDS = CanSpeed.options(
prefix="CAN_",
)
# Data phase rates for a CAN FD platform's own schema
CAN_DATA_SPEEDS = {
**CAN_SPEEDS,
**CanSpeed.options(
"2000KBPS",
"3000KBPS",
"4000KBPS",
"5000KBPS",
"6000KBPS",
"7000KBPS",
"8000KBPS",
prefix="CAN_",
),
}
def get_rate(value: str) -> int:
match = re.match(r"(\d+)(?:K(\d+)?)?BPS", value, re.IGNORECASE)
@@ -93,7 +124,7 @@ def get_rate(value: str) -> int:
CANBUS_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CanbusComponent),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=CAN_ID_MASK_ALL),
cv.Optional(CONF_BIT_RATE, default=DEFAULT_BIT_RATE): cv.enum(
CAN_SPEEDS, upper=True
),
@@ -103,10 +134,9 @@ CANBUS_SCHEMA = cv.Schema(
): cv.positive_not_null_int,
cv.Optional(CONF_ON_FRAME): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(CanbusTrigger),
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_CAN_ID_MASK, default=0x1FFFFFFF): cv.int_range(
min=0, max=0x1FFFFFFF
cv.Required(CONF_CAN_ID): cv.int_range(min=0, max=CAN_ID_MASK_ALL),
cv.Optional(CONF_CAN_ID_MASK, default=CAN_ID_MASK_ALL): cv.int_range(
min=0, max=CAN_ID_MASK_ALL
),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST): cv.boolean,
@@ -118,39 +148,67 @@ CANBUS_SCHEMA = cv.Schema(
CANBUS_SCHEMA.add_extra(validate_id)
# A platform whose controller supports CAN FD extends this instead of CANBUS_SCHEMA. Its buses then accept the
# CAN FD options of canbus.send, and register_canbus() turns on USE_CANBUS_FD, which makes every CanFrame
# 64 bytes long.
CANBUS_FD_SCHEMA = CANBUS_SCHEMA.extend(
{
cv.Optional(CONF_DEFAULT_FD, default=False): cv.boolean,
cv.Optional(CONF_DEFAULT_BIT_RATE_SWITCH, default=False): cv.boolean,
cv.Optional(CONF_DEFAULT_PAD_DATA, default=False): cv.boolean,
}
)
def _is_fd_bus(config: ConfigType) -> bool:
return CONF_DEFAULT_FD in config
async def setup_canbus_core_(var: MockObj, config: ConfigType) -> None:
await cg.register_component(var, config)
cg.add(var.set_can_id(config[CONF_CAN_ID]))
if config[CONF_USE_EXTENDED_ID]:
cg.add(var.set_use_extended_id(True))
if _is_fd_bus(config):
cg.add_define("USE_CANBUS_FD")
cg.add(var.set_supports_fd(True))
for key, setter in (
(CONF_DEFAULT_FD, var.set_default_fd),
(CONF_DEFAULT_BIT_RATE_SWITCH, var.set_default_bit_rate_switch),
(CONF_DEFAULT_PAD_DATA, var.set_default_pad_data),
):
if config.get(key):
cg.add(setter(True))
if (bit_rate := config[CONF_BIT_RATE]) != DEFAULT_BIT_RATE:
cg.add(var.set_bitrate(CAN_SPEEDS[bit_rate]))
if (max_frames := config[CONF_MAX_FRAMES_PER_LOOP]) != DEFAULT_MAX_FRAMES_PER_LOOP:
cg.add(var.set_max_frames_per_loop(max_frames))
for conf in config.get(CONF_ON_FRAME, []):
can_id = conf[CONF_CAN_ID]
can_id_mask = conf[CONF_CAN_ID_MASK]
ext_id = conf[CONF_USE_EXTENDED_ID]
trigger = cg.new_Pvariable(
conf[CONF_TRIGGER_ID], var, can_id, can_id_mask, ext_id
)
if CONF_REMOTE_TRANSMISSION_REQUEST in conf:
cg.add(
trigger.set_remote_transmission_request(
conf[CONF_REMOTE_TRANSMISSION_REQUEST]
)
)
await cg.register_component(trigger, conf)
await automation.build_automation(
trigger,
can_id = f"0x{conf[CONF_CAN_ID]:X}"
if (mask := conf[CONF_CAN_ID_MASK]) != CAN_ID_MASK_ALL:
checks = [f"(frame.can_id & 0x{mask:X}) == {can_id}"]
else:
checks = [f"frame.can_id == {can_id}"]
checks.append(_flag("frame.use_extended_id", conf[CONF_USE_EXTENDED_ID]))
if (rtr := conf.get(CONF_REMOTE_TRANSMISSION_REQUEST)) is not None:
checks.append(_flag("frame.remote_transmission_request", rtr))
await automation.build_callback_automation(
var,
"add_frame_callback",
[
(cg.std_vector.template(cg.uint8), "x"),
(CanPayload, "x"),
(cg.uint32, "can_id"),
(cg.bool_, "remote_transmission_request"),
],
conf,
params=[(CanFrame.operator("const").operator("ref"), "frame")],
forward=[
"frame.payload()",
"frame.can_id",
"frame.remote_transmission_request",
],
when=" && ".join(checks),
)
@@ -170,16 +228,66 @@ async def register_canbus(
await setup_canbus_core_(var, config)
def validate_send(action: ConfigType, bus: ConfigType) -> None:
"""Reject a canbus.send that its bus cannot send, as far as the values are known.
Lambda values are checked when the action runs.
"""
data = action[CONF_DATA]
length = None if cg.is_template(data) else len(data)
if not _is_fd_bus(bus):
for key in _FD_SEND_KEYS:
if key in action:
raise cv.Invalid(
f"{key} needs a canbus platform that supports CAN FD", path=[key]
)
fd = False
else:
fd = action.get(CONF_FD, bus[CONF_DEFAULT_FD])
if fd is False and action.get(CONF_BIT_RATE_SWITCH) is True:
raise cv.Invalid("bit_rate_switch requires fd: true")
if fd is True and action[CONF_REMOTE_TRANSMISSION_REQUEST]:
raise cv.Invalid("remote_transmission_request cannot be used with fd: true")
if length is None:
return
if fd is False and length > CAN_MAX_DATA_LENGTH:
raise cv.Invalid("Classic CAN data must be at most 8 bytes", path=[CONF_DATA])
if (
fd is True
and length not in CAN_FD_DATA_LENGTHS
and action.get(CONF_PAD_DATA, bus[CONF_DEFAULT_PAD_DATA]) is False
):
raise cv.Invalid("CAN FD data length requires pad_data: true", path=[CONF_DATA])
def final_validate(config: list[ConfigType]) -> None:
full_config = fv.full_config.get()
for bus_id, path in iter_ids(full_config):
if len(path) < 2 or path[-2:] != ["canbus.send", CONF_CANBUS_ID]:
continue
action_path = path[:-1]
action = full_config.get_config_for_path(action_path)
bus = fv.get_declaring_config(bus_id, full_config)
with cv.prepend_path([cv.ROOT_CONFIG_PATH, *action_path]):
validate_send(action, bus)
FINAL_VALIDATE_SCHEMA = final_validate
# Actions
CANBUS_SEND_ACTION_SCHEMA = cv.maybe_simple_value(
{
cv.GenerateID(CONF_CANBUS_ID): cv.use_id(CanbusComponent),
cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=0x1FFFFFFF),
cv.Optional(CONF_CAN_ID): cv.int_range(min=0, max=CAN_ID_MASK_ALL),
cv.Optional(CONF_USE_EXTENDED_ID, default=False): cv.boolean,
cv.Optional(CONF_REMOTE_TRANSMISSION_REQUEST, default=False): cv.boolean,
# One classic CAN frame
cv.Optional(CONF_FD): cv.templatable(cv.boolean),
cv.Optional(CONF_BIT_RATE_SWITCH): cv.templatable(cv.boolean),
cv.Optional(CONF_PAD_DATA): cv.templatable(cv.boolean),
# One CAN FD frame; final_validate holds a classic bus to 8 bytes
cv.Required(CONF_DATA): cv.templatable(
cv.All(validate_raw_data, cv.Length(max=8))
cv.All(validate_raw_data, cv.Length(max=CAN_FD_DATA_LENGTHS[-1]))
),
},
validate_id,
@@ -209,6 +317,14 @@ async def canbus_action_to_code(
cg.add(
var.set_remote_transmission_request(config[CONF_REMOTE_TRANSMISSION_REQUEST])
)
# Only set on a CAN FD bus, which final_validate checks
for key, setter in (
(CONF_FD, var.set_fd),
(CONF_BIT_RATE_SWITCH, var.set_bit_rate_switch),
(CONF_PAD_DATA, var.set_pad_data),
):
if (value := config.get(key)) is not None:
cg.add(setter(await cg.templatable(value, args, cg.bool_)))
await automation.templatable_bytes(
config[CONF_DATA],
@@ -216,5 +332,6 @@ async def canbus_action_to_code(
var.set_data_template,
var.set_data_static,
"canbus_data",
return_type=CanPayload,
)
return var
+104 -52
View File
@@ -22,49 +22,112 @@ void Canbus::dump_config() {
}
}
canbus::Error Canbus::send_data(const CanFrame &frame) {
if (frame.can_id > (frame.use_extended_id ? CAN_EXTENDED_ID_MAX : CAN_STANDARD_ID_MAX)) {
ESP_LOGE(TAG, "CAN ID 0x%08" PRIx32 " exceeds frame identifier width", frame.can_id);
return ERROR_FAILTX;
}
#ifdef USE_CANBUS_FD
if (frame.fd && !this->supports_fd_) {
ESP_LOGE(TAG, "CAN FD frame on a bus without CAN FD");
return ERROR_FAILTX;
}
if (frame.fd ? (frame.remote_transmission_request || can_dlc_to_len(frame.can_data_length_code) == CAN_INVALID_DLC)
: frame.bit_rate_switch) {
ESP_LOGE(TAG, "Invalid CAN frame: fd=%d rtr=%d brs=%d dlc=%" PRIu8, frame.fd, frame.remote_transmission_request,
frame.bit_rate_switch, frame.can_data_length_code);
return ERROR_FAILTX;
}
#endif
return this->send_frame_(frame);
}
#ifdef USE_CANBUS_FD
canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const uint8_t *data, size_t len, bool fd, bool bit_rate_switch, bool pad_data) {
#else
canbus::Error Canbus::send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const uint8_t *data, size_t len) {
struct CanFrame can_message;
#endif
CanFrame frame;
frame.use_extended_id = use_extended_id;
frame.remote_transmission_request = remote_transmission_request;
frame.can_id = can_id;
#ifdef USE_CANBUS_FD
frame.fd = fd;
frame.bit_rate_switch = bit_rate_switch;
frame.error_state_indicator = false;
if (fd) {
frame.can_data_length_code = can_len_to_dlc(len, pad_data);
if (frame.can_data_length_code == CAN_INVALID_DLC) {
ESP_LOGE(TAG, "CAN FD cannot carry %zu bytes without pad_data", len);
return ERROR_FAILTX;
}
if (!remote_transmission_request && len != 0) {
std::memcpy(frame.data, data, len);
std::memset(frame.data + len, 0, can_dlc_to_len(frame.can_data_length_code) - len);
}
return this->send_data(frame);
}
#endif
if (len > CAN_MAX_DATA_LENGTH) {
ESP_LOGW(TAG, "Classic CAN carries 8 bytes, %zu given, sent 8", len);
}
// Clamp before narrowing, so a lambda payload of 256 bytes or more cannot wrap to a short length
const uint8_t size = static_cast<uint8_t>(std::min<size_t>(len, CAN_MAX_DATA_LENGTH));
if (use_extended_id) {
ESP_LOGD(TAG, "send extended id=0x%08" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request),
size);
frame.can_data_length_code = static_cast<uint8_t>(std::min<size_t>(len, CAN_MAX_DATA_LENGTH));
// A remote request carries no data, so its bytes are zeroed rather than left from the stack. An empty vector may
// hand over a null pointer, which memcpy must not get even for 0 bytes.
if (remote_transmission_request) {
std::memset(frame.data, 0, frame.can_data_length_code);
} else if (frame.can_data_length_code != 0) {
std::memcpy(frame.data, data, frame.can_data_length_code);
}
#ifdef USE_CANBUS_FD
// The caller's flags can still contradict each other
return this->send_data(frame);
#else
return this->send_frame_(frame);
#endif
}
bool Canbus::lambda_payload_fits_(const CanPayload &payload, [[maybe_unused]] bool fd) {
if (!payload.truncated())
return true;
#ifdef USE_CANBUS_FD
if (fd) {
ESP_LOGE(TAG, "CAN FD carries at most %u bytes, frame not sent", CAN_FRAME_DATA_LENGTH);
return false;
}
// The payload still holds 64 bytes, so send_data() warns as it cuts a classic frame to 8
#else
ESP_LOGW(TAG, "Classic CAN carries 8 bytes, the rest is cut");
#endif
return true;
}
canbus::Error Canbus::send_frame_(const CanFrame &frame) {
const uint8_t size = frame.payload_length();
if (frame.use_extended_id) {
ESP_LOGD(TAG, "send extended id=0x%08" PRIx32 " rtr=%s size=%d", frame.can_id,
TRUEFALSE(frame.remote_transmission_request), size);
} else {
ESP_LOGD(TAG, "send standard id=0x%03" PRIx32 " rtr=%s size=%d", can_id, TRUEFALSE(remote_transmission_request),
size);
ESP_LOGD(TAG, "send standard id=0x%03" PRIx32 " rtr=%s size=%d", frame.can_id,
TRUEFALSE(frame.remote_transmission_request), size);
}
can_message.can_data_length_code = size;
can_message.can_id = can_id;
can_message.use_extended_id = use_extended_id;
can_message.remote_transmission_request = remote_transmission_request;
ESP_LOGV_HEX(TAG, frame.data, size, CAN_FRAME_DATA_LENGTH, " data=%s");
for (int i = 0; i < size; i++) {
can_message.data[i] = data[i];
ESP_LOGVV(TAG, " data[%d]=%02x", i, can_message.data[i]);
}
canbus::Error error = this->send_message(&can_message);
// Drivers take a mutable pointer but do not change the frame
canbus::Error error = this->send_message(const_cast<CanFrame *>(&frame));
if (error != canbus::ERROR_OK) {
if (use_extended_id) {
ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed with error %d!", can_id, error);
if (frame.use_extended_id) {
ESP_LOGW(TAG, "send to extended id=0x%08" PRIx32 " failed with error %d!", frame.can_id, error);
} else {
ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed with error %d!", can_id, error);
ESP_LOGW(TAG, "send to standard id=0x%03" PRIx32 " failed with error %d!", frame.can_id, error);
}
}
return error;
}
void Canbus::add_trigger(CanbusTrigger *trigger) {
if (trigger->use_extended_id_) {
ESP_LOGVV(TAG, "add trigger for extended canid=0x%08" PRIx32, trigger->can_id_);
} else {
ESP_LOGVV(TAG, "add trigger for std canid=0x%03" PRIx32, trigger->can_id_);
}
this->triggers_.push_back(trigger);
};
void Canbus::loop() {
uint32_t now = App.get_loop_component_start_time();
if ((now - this->last_event_check_time_) >= EVENT_CHECK_INTERVAL_MS) {
@@ -75,38 +138,27 @@ void Canbus::loop() {
struct CanFrame can_message;
uint32_t message_counter = 0;
for (; message_counter < this->max_frames_per_loop_; message_counter++) {
#ifdef USE_CANBUS_FD
// Classic drivers leave the CAN FD flags alone
can_message.fd = false;
can_message.bit_rate_switch = false;
can_message.error_state_indicator = false;
#endif
if (this->read_message(&can_message) != canbus::ERROR_OK) {
break;
}
const uint8_t size = can_message.payload_length();
if (can_message.use_extended_id) {
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") extended can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
can_message.can_id, size);
} else {
ESP_LOGD(TAG, "received can message (#%" PRIu32 ") std can_id=0x%" PRIx32 " size=%d", message_counter + 1,
can_message.can_id, can_message.can_data_length_code);
can_message.can_id, size);
}
std::vector<uint8_t> data;
// show data received
for (int i = 0; i < std::min(can_message.can_data_length_code, CAN_MAX_DATA_LENGTH); i++) {
ESP_LOGV(TAG, " can_message.data[%d]=%02x", i, can_message.data[i]);
data.push_back(can_message.data[i]);
}
this->callback_manager_(can_message.can_id, can_message.use_extended_id, can_message.remote_transmission_request,
data);
// fire all triggers
for (auto *trigger : this->triggers_) {
if ((trigger->can_id_ == (can_message.can_id & trigger->can_id_mask_)) &&
(trigger->use_extended_id_ == can_message.use_extended_id) &&
(!trigger->remote_transmission_request_.has_value() ||
trigger->remote_transmission_request_.value() == can_message.remote_transmission_request)) {
trigger->trigger(data, can_message.can_id, can_message.remote_transmission_request);
}
}
ESP_LOGV_HEX(TAG, can_message.data, size, CAN_FRAME_DATA_LENGTH, " data=%s");
this->frame_callbacks_.call(can_message);
}
if (message_counter == this->max_frames_per_loop_) {
+198 -45
View File
@@ -2,9 +2,16 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/optional.h"
#include <algorithm>
#include <cinttypes>
#include <cstddef>
#include <cstring>
#include <initializer_list>
#include <type_traits>
#include <vector>
namespace esphome::canbus {
@@ -39,28 +46,128 @@ enum CanSpeed : uint8_t {
CAN_250KBPS,
CAN_500KBPS,
CAN_800KBPS,
CAN_1000KBPS
CAN_1000KBPS,
#ifdef USE_CANBUS_FD
// Data phase rates of a CAN FD controller
CAN_2000KBPS,
CAN_3000KBPS,
CAN_4000KBPS,
CAN_5000KBPS,
CAN_6000KBPS,
CAN_7000KBPS,
CAN_8000KBPS,
#endif
};
class CanbusTrigger;
template<typename... Ts> class CanbusSendAction;
/* CAN payload length definitions according to ISO 11898-1 */
static const uint8_t CAN_MAX_DATA_LENGTH = 8;
static constexpr uint8_t CAN_MAX_DATA_LENGTH = 8;
#ifdef USE_CANBUS_FD
static constexpr uint8_t CAN_FD_MAX_DATA_LENGTH = 64;
/// Payload bytes a CanFrame holds: one CAN FD frame when a platform that supports it is configured
static constexpr uint8_t CAN_FRAME_DATA_LENGTH = CAN_FD_MAX_DATA_LENGTH;
#else
/// Payload bytes a CanFrame holds: one classic frame
static constexpr uint8_t CAN_FRAME_DATA_LENGTH = CAN_MAX_DATA_LENGTH;
#endif
static const uint8_t CAN_ERROR_PASSIVE_THRESHOLD = 128;
static constexpr uint32_t CAN_STANDARD_ID_MAX = 0x7FF;
static constexpr uint32_t CAN_EXTENDED_ID_MAX = 0x1FFFFFFF;
/*
Can Frame describes a normative CAN Frame
The RTR = Remote Transmission Request is implemented in every CAN controller but rarely used
So currently the flag is passed to and from the hardware but currently ignored to the user application.
*/
static constexpr uint8_t CAN_ERROR_PASSIVE_THRESHOLD = 128;
#ifdef USE_CANBUS_FD
/// Returned by the length conversions for a value that cannot be represented
static constexpr uint8_t CAN_INVALID_DLC = UINT8_MAX;
/// Payload length of each CAN FD DLC; a classic frame carries min(DLC, 8) bytes
static constexpr uint8_t CAN_FD_DLC_COUNT = 16;
static constexpr uint8_t CAN_FD_DLC_LENGTHS[CAN_FD_DLC_COUNT] PROGMEM = {0, 1, 2, 3, 4, 5, 6, 7,
8, 12, 16, 20, 24, 32, 48, 64};
/// CAN_FD_DLC_LENGTHS[dlc] for dlc below CAN_FD_DLC_COUNT; read from flash at run time
constexpr uint8_t can_fd_dlc_length(uint8_t dlc) {
return std::is_constant_evaluated() ? CAN_FD_DLC_LENGTHS[dlc] : progmem_read_byte(&CAN_FD_DLC_LENGTHS[dlc]);
}
/// Payload length of a CAN FD DLC, or CAN_INVALID_DLC above 15
constexpr uint8_t can_dlc_to_len(uint8_t dlc) {
return dlc < CAN_FD_DLC_COUNT ? can_fd_dlc_length(dlc) : CAN_INVALID_DLC;
}
/// CAN FD DLC for a payload length. With pad, the next longer length is used and the caller zero fills
/// the extra bytes; otherwise a length CAN FD cannot carry returns CAN_INVALID_DLC.
constexpr uint8_t can_len_to_dlc(size_t len, bool pad) {
for (uint8_t dlc = 0; dlc < CAN_FD_DLC_COUNT; dlc++) {
const uint8_t length = can_fd_dlc_length(dlc);
if (length >= len)
return (length == len || pad) ? dlc : CAN_INVALID_DLC;
}
return CAN_INVALID_DLC;
}
#else
/// Payload length of a classic DLC; 9 to 15 still carry 8 bytes
constexpr uint8_t can_dlc_to_len(uint8_t dlc) { return std::min(dlc, CAN_MAX_DATA_LENGTH); }
#endif
/// Frame payload without heap; converts to and from std::vector<uint8_t> for older lambdas.
class CanPayload : public StaticVector<uint8_t, CAN_FRAME_DATA_LENGTH> {
public:
using StaticVector<uint8_t, CAN_FRAME_DATA_LENGTH>::StaticVector;
// Not = default, so value-init does not zero the buffer
CanPayload() {}
// Every constructor that can be given more than one frame records that it cut the rest
template<typename InputIt> CanPayload(InputIt first, InputIt last) {
for (; first != last; ++first) {
if (this->size() == CAN_FRAME_DATA_LENGTH) {
this->truncated_ = true;
break;
}
this->push_back(*first);
}
}
CanPayload(std::initializer_list<uint8_t> data) : CanPayload(data.begin(), data.end()) {}
CanPayload(const std::vector<uint8_t> &data) // NOLINT(google-explicit-constructor)
: CanPayload(data.begin(), data.end()) {}
operator std::vector<uint8_t>() const { return {this->begin(), this->end()}; } // NOLINT(google-explicit-constructor)
/// Whether it was built from more bytes than one frame can carry
bool truncated() const { return this->truncated_; }
protected:
bool truncated_{false};
};
/// One CAN frame as passed between the bus and its driver. The CAN FD flags come after data, so code that
/// brace-initializes a classic frame keeps working.
struct CanFrame {
bool use_extended_id = false;
bool remote_transmission_request = false;
uint32_t can_id; /* 29 or 11 bit CAN_ID */
uint8_t can_data_length_code; /* frame payload length in byte (0 .. CAN_MAX_DATA_LENGTH) */
uint8_t data[CAN_MAX_DATA_LENGTH] __attribute__((aligned(8)));
uint32_t can_id; /* 29 or 11 bit CAN_ID; drivers clear bits 29-31 (on_frame relies on it) */
uint8_t can_data_length_code; /* DLC as on the wire, not a byte count (see payload_length()) */
uint8_t data[CAN_FRAME_DATA_LENGTH] __attribute__((aligned(8)));
#ifdef USE_CANBUS_FD
bool fd = false; /* CAN FD frame; it cannot be a remote transmission request */
bool bit_rate_switch = false; /* CAN FD only: data phase sent at the data bit rate */
bool error_state_indicator = false; /* CAN FD only: received transmitter was error passive */
#endif
/// Payload bytes for the DLC: at most 8 for a classic frame, 0 for an invalid CAN FD DLC
uint8_t payload_length() const {
#ifdef USE_CANBUS_FD
if (this->fd) {
const uint8_t len = can_dlc_to_len(this->can_data_length_code);
return len == CAN_INVALID_DLC ? 0 : len;
}
#endif
return std::min(this->can_data_length_code, CAN_MAX_DATA_LENGTH);
}
CanPayload payload() const {
CanPayload out;
std::memcpy(out.data(), this->data, CAN_FRAME_DATA_LENGTH);
out.resize(this->payload_length());
return out;
}
};
enum CanEventFlags : uint8_t {
@@ -91,8 +198,18 @@ class Canbus : public Component {
float get_setup_priority() const override { return setup_priority::HARDWARE; }
void loop() override;
/// Validates the frame and passes it to the driver.
canbus::Error send_data(const CanFrame &frame);
#ifdef USE_CANBUS_FD
/// Sends up to 8 bytes as a classic frame, longer data is cut. An fd frame carries up to 64 bytes; pad_data
/// zero fills a length CAN FD cannot carry up to the next one.
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, const uint8_t *data,
size_t len, bool fd = false, bool bit_rate_switch = false, bool pad_data = false);
#else
/// Sends up to 8 bytes as a classic frame, longer data is cut.
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request, const uint8_t *data,
size_t len);
#endif
canbus::Error send_data(uint32_t can_id, bool use_extended_id, bool remote_transmission_request,
const std::vector<uint8_t> &data) {
return this->send_data(can_id, use_extended_id, remote_transmission_request, data.data(), data.size());
@@ -105,9 +222,28 @@ class Canbus : public Component {
void set_use_extended_id(bool use_extended_id) { this->use_extended_id_ = use_extended_id; }
void set_bitrate(CanSpeed bit_rate) { this->bit_rate_ = bit_rate; }
void set_max_frames_per_loop(uint32_t max_frames_per_loop) { this->max_frames_per_loop_ = max_frames_per_loop; }
#ifdef USE_CANBUS_FD
// Defaults for canbus.send actions only, not for send_data()
void set_default_fd(bool default_fd) { this->default_fd_ = default_fd; }
void set_default_bit_rate_switch(bool default_bit_rate_switch) {
this->default_bit_rate_switch_ = default_bit_rate_switch;
}
void set_default_pad_data(bool default_pad_data) { this->default_pad_data_ = default_pad_data; }
void set_supports_fd(bool supports_fd) { this->supports_fd_ = supports_fd; }
#endif
/// Whether this bus sends and receives CAN FD frames; a classic bus can share a build with a CAN FD one
bool supports_fd() const {
#ifdef USE_CANBUS_FD
return this->supports_fd_;
#else
return false;
#endif
}
virtual CanStatus get_status() { return {}; }
void add_trigger(CanbusTrigger *trigger);
/// The frame is only valid during the call.
template<typename F> void add_frame_callback(F &&callback) { this->frame_callbacks_.add(std::forward<F>(callback)); }
/**
* Add a callback to be called when a CAN message is received. All received messages
* are passed to the callback without filtering.
@@ -118,19 +254,30 @@ class Canbus : public Component {
* - rtr If this is a remote transmission request
* - data The message data
*/
template<typename F> void add_callback(F &&callback) { this->callback_manager_.add(std::forward<F>(callback)); }
template<typename F>
ESPDEPRECATED("Use add_frame_callback(), which does not copy the payload. Removed in 2027.5.0.", "2026.11.0")
void add_callback(F &&callback) {
this->frame_callbacks_.add([cb = std::forward<F>(callback)](const CanFrame &frame) mutable {
cb(frame.can_id, frame.use_extended_id, frame.remote_transmission_request,
std::vector<uint8_t>(frame.data, frame.data + frame.payload_length()));
});
}
protected:
template<typename... Ts> friend class CanbusSendAction;
std::vector<CanbusTrigger *> triggers_{};
uint32_t can_id_;
uint32_t max_frames_per_loop_{50}; // Keep in sync with DEFAULT_MAX_FRAMES_PER_LOOP in __init__.py
CanSpeed bit_rate_{CAN_125KBPS}; // Keep in sync with DEFAULT_BIT_RATE in __init__.py
bool use_extended_id_{false};
#ifdef USE_CANBUS_FD
bool default_fd_{false};
bool default_bit_rate_switch_{false};
bool default_pad_data_{false};
bool supports_fd_{false};
#endif
uint8_t events_to_log_{0};
bool bus_off_{false};
CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
callback_manager_{};
CallbackManager<void(const CanFrame &frame)> frame_callbacks_{};
// interval in which to check for new can bus events (bus-off, passive, active)
static constexpr uint32_t EVENT_CHECK_INTERVAL_MS = 100;
@@ -150,6 +297,10 @@ class Canbus : public Component {
#endif
void log_events_(CanEventFlags events, uint32_t now);
/// Logs and sends a frame that is already valid.
canbus::Error send_frame_(const CanFrame &frame);
/// For a canbus.send lambda: a CAN FD frame that did not fit is refused, a classic one is cut to 8 bytes.
bool lambda_payload_fits_(const CanPayload &payload, [[maybe_unused]] bool fd);
virtual bool setup_internal() = 0;
virtual Error send_message(struct CanFrame *frame) = 0;
virtual Error read_message(struct CanFrame *frame) = 0;
@@ -157,7 +308,12 @@ class Canbus : public Component {
};
template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, public Parented<Canbus> {
TEMPLATABLE_BYTES(data)
TEMPLATABLE_BUFFER(data, CanPayload)
#ifdef USE_CANBUS_FD
TEMPLATABLE_VALUE(bool, fd)
TEMPLATABLE_VALUE(bool, bit_rate_switch)
TEMPLATABLE_VALUE(bool, pad_data)
#endif
void set_can_id(uint32_t can_id) { this->can_id_ = can_id; }
@@ -171,11 +327,30 @@ template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, pu
auto can_id = this->can_id_.has_value() ? *this->can_id_ : this->parent_->can_id_;
auto use_extended_id =
this->use_extended_id_.has_value() ? *this->use_extended_id_ : this->parent_->use_extended_id_;
this->data_.template visit<CAN_MAX_DATA_LENGTH>(
[&](const uint8_t *data, size_t len) {
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data, len);
},
x...);
#ifdef USE_CANBUS_FD
const bool fd = this->fd_.value_or(x..., this->parent_->default_fd_);
const bool bit_rate_switch = this->bit_rate_switch_.value_or(x..., fd && this->parent_->default_bit_rate_switch_);
const bool pad_data = this->pad_data_.value_or(x..., this->parent_->default_pad_data_);
#endif
#ifndef USE_CANBUS_FD
const bool fd = false;
#endif
auto send = [&](const uint8_t *data, size_t len) {
#ifdef USE_CANBUS_FD
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data, len, fd,
bit_rate_switch, pad_data);
#else
this->parent_->send_data(can_id, use_extended_id, this->remote_transmission_request_, data, len);
#endif
};
if (this->data_.is_static()) {
this->data_.template visit<CAN_FRAME_DATA_LENGTH>(send, x...);
return;
}
// The conversion from a lambda's vector cuts it to one frame, so the length it had is checked here
const CanPayload payload = this->data_.value(x...);
if (this->parent_->lambda_payload_fits_(payload, fd))
send(payload.data(), payload.size());
}
protected:
@@ -184,26 +359,4 @@ template<typename... Ts> class CanbusSendAction final : public Action<Ts...>, pu
bool remote_transmission_request_{false};
};
class CanbusTrigger final : public Trigger<std::vector<uint8_t>, uint32_t, bool>, public Component {
friend class Canbus;
public:
explicit CanbusTrigger(Canbus *parent, const std::uint32_t can_id, const std::uint32_t can_id_mask,
const bool use_extended_id)
: parent_(parent), can_id_(can_id), can_id_mask_(can_id_mask), use_extended_id_(use_extended_id) {};
void set_remote_transmission_request(bool remote_transmission_request) {
this->remote_transmission_request_ = remote_transmission_request;
}
void setup() override { this->parent_->add_trigger(this); }
protected:
Canbus *parent_;
uint32_t can_id_;
uint32_t can_id_mask_;
bool use_extended_id_;
optional<bool> remote_transmission_request_{};
};
} // namespace esphome::canbus
+5 -8
View File
@@ -72,15 +72,12 @@ void CAP1188Component::dump_config() {
void CAP1188Component::loop() {
uint8_t touched = 0;
this->read_register(CAP1188_SENSOR_INPUT_STATUS, &touched, 1);
// On a failed read the channels keep their state instead of reporting a release
if (!this->status_comm_result(this->read_byte(CAP1188_SENSOR_INPUT_STATUS, &touched)))
return;
if (touched) {
uint8_t data = 0;
this->read_register(CAP1188_MAIN, &data, 1);
data = data & ~CAP1188_MAIN_INT;
this->write_register(CAP1188_MAIN, &data, 1);
}
if (touched)
this->update_register(CAP1188_MAIN, CAP1188_MAIN_INT, 0);
for (auto *channel : this->channels_) {
channel->process(touched);
+1 -3
View File
@@ -24,10 +24,8 @@ void CH423Component::setup() {
if (success && !(this->sys_params_ & CH423_SYS_IO_OE)) {
success = this->read_inputs_();
}
if (!success) {
this->mark_failed(LOG_STR("CH423 not detected"));
if (!this->setup_check(success, LOG_STR("CH423 not detected")))
return;
}
ESP_LOGCONFIG(TAG, "Initialization complete. Warning: %d, Error: %d", this->status_has_warning(),
this->status_has_error());
+12 -9
View File
@@ -23,10 +23,9 @@ uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
void CM1106Component::setup() {
uint8_t response[8] = {0};
if (!this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))) {
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
if (!this->setup_check(
this->cm1106_write_command_(C_M1106_CMD_GET_CO2, sizeof(C_M1106_CMD_GET_CO2), response, sizeof(response))))
return;
}
if (this->abc_boot_logic_ != CM1106_ABC_NONE) {
this->abc_set_(this->abc_boot_logic_ == CM1106_ABC_ENABLED);
@@ -42,7 +41,10 @@ void CM1106Component::update() {
}
if (response[0] != 0x16 || response[1] != 0x05 || response[2] != 0x01) {
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
char hex_buf[format_hex_pretty_size(4)];
ESP_LOGW(TAG, "Unexpected response: %s", format_hex_pretty_to(hex_buf, response, 4, ' '));
#endif
this->status_set_warning();
return;
}
@@ -73,8 +75,7 @@ void CM1106Component::update() {
void CM1106Component::calibrate_zero(uint16_t ppm) {
uint8_t cmd[6];
memcpy(cmd, C_M1106_CMD_SET_CO2_CALIB, sizeof(cmd));
cmd[3] = ppm >> 8;
cmd[4] = ppm & 0xFF;
put_be16(cmd + 3, ppm);
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_CO2_CALIB_RESPONSE)) {
ESP_LOGD(TAG, "Successfully calibrated sensor to %uppm", ppm);
}
@@ -86,8 +87,7 @@ void CM1106Component::abc_set_(bool enabled) {
memcpy(cmd, C_M1106_CMD_SET_ABC_STATUS, sizeof(cmd));
cmd[4] = enabled ? CM1106_ABC_FLAG_ENABLE : CM1106_ABC_FLAG_DISABLE;
cmd[5] = this->abc_cycle_;
cmd[6] = this->abc_baseline_ >> 8;
cmd[7] = this->abc_baseline_ & 0xFF;
put_be16(cmd + 6, this->abc_baseline_);
if (this->send_command_expect_ack_(cmd, sizeof(cmd), C_M1106_CMD_SET_ABC_STATUS_RESPONSE)) {
ESP_LOGD(TAG, "Successfully set ABC status");
}
@@ -101,7 +101,10 @@ bool CM1106Component::send_command_expect_ack_(const uint8_t *command, size_t co
return false;
}
if (!this->status_comm_result(memcmp(response, ack, sizeof(response)) == 0)) {
ESP_LOGW(TAG, "Unexpected response: %02X %02X %02X %02X", response[0], response[1], response[2], response[3]);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
char hex_buf[format_hex_pretty_size(4)];
ESP_LOGW(TAG, "Unexpected response: %s", format_hex_pretty_to(hex_buf, response, 4, ' '));
#endif
return false;
}
return true;
+6
View File
@@ -24,6 +24,8 @@ CONF_BREATH_VOC_EQUIVALENT = "breath_voc_equivalent"
CONF_BUTTONS = "buttons"
CONF_BYTE_ORDER = "byte_order"
CONF_CHANNEL_COLORS = "channel_colors"
CONF_CHARGE_CURRENT = "charge_current"
CONF_CHARGE_ENABLED = "charge_enabled"
CONF_CLEAR_CHANNEL = "clear_channel"
CONF_CLIMATE_ID = "climate_id"
CONF_CO2_EQUIVALENT = "co2_equivalent"
@@ -50,6 +52,7 @@ CONF_HOST = "host"
CONF_HUMIDITY_SOURCE = "humidity_source"
CONF_IAQ = "iaq"
CONF_IGNORE_NOT_FOUND = "ignore_not_found"
CONF_INPUT_CURRENT_LIMIT = "input_current_limit"
CONF_IS_WRGB = "is_wrgb"
CONF_KEYS = "keys"
CONF_LABEL = "label"
@@ -74,6 +77,7 @@ CONF_OUT_PIN = "out_pin"
CONF_OUT_PIN_LEVEL = "out_pin_level"
CONF_PARITY = "parity"
CONF_PASSKEY = "passkey"
CONF_PRECHARGE_CURRENT = "precharge_current"
CONF_PROVIDER = "provider"
CONF_PWM_FREQUENCY = "pwm_frequency"
CONF_QUERY_PARAMS = "query_params"
@@ -104,7 +108,9 @@ CONF_VOC_INDEX = "voc_index"
CONF_VOLUME_INCREMENT = "volume_increment"
CONF_VOLUME_INITIAL = "volume_initial"
CONF_VOLUME_MAX = "volume_max"
CONF_VOLUME_MAX_DB = "volume_max_db"
CONF_VOLUME_MIN = "volume_min"
CONF_VOLUME_MIN_DB = "volume_min_db"
CONF_WINDOW = "window"
ICON_CURRENT_DC = "mdi:current-dc"
+5 -3
View File
@@ -189,7 +189,8 @@ void CSE7766Component::parse_data_() {
if (have_power && this->reactive_power_sensor_ != nullptr) {
const float q_squared = apparent_power * apparent_power - power * power;
if (q_squared < 0.0f) {
ESP_LOGD(TAG, "Impossible reactive power: S^2-P^2 is negative (%.4f)", q_squared);
// From the same noise as a power factor above 1 (below)
ESP_LOGV(TAG, "Impossible reactive power: S^2-P^2 is negative (%.4f)", q_squared);
this->reactive_power_sensor_->publish_state(0.0f);
} else {
this->reactive_power_sensor_->publish_state(std::sqrt(q_squared));
@@ -200,8 +201,9 @@ void CSE7766Component::parse_data_() {
if (apparent_power > 0) {
pf = power / apparent_power;
if (pf > 1) {
// Noise near the chip's resolution limit, seen during inrush, pushes the ratio past unity
ESP_LOGD(TAG, "Power factor %.4f above 1, clamped", pf);
// Noise near the chip's resolution limit pushes the ratio past unity: during inrush, and on
// nearly every frame of a steady resistive load, so it is logged only at verbose level
ESP_LOGV(TAG, "Power factor %.4f above 1, clamped", pf);
pf = 1.0f;
}
} else if (apparent_power == 0 && power == 0) {
Loaded 100 of 664 files, more files were not shown because too many files have changed in this diff. Show more