Compare commits

...

166 Commits

Author SHA1 Message Date
J. Nick Koston 5f460b9c7c Fix remaining components using Python int instead of cg.int32 2026-04-08 08:18:36 -10:00
J. Nick Koston adeabb4178 [rotary_encoder] Fix templatable value type to use cg.int32 instead of Python int 2026-04-08 08:03:58 -10:00
J. Nick Koston a72609e640 [yaml] Resolve top-level IncludeFile in load_yaml (#15557) 2026-04-08 08:39:14 -04:00
J. Nick Koston a8b7c7a4ac [core] Add TemplatableFn for 4-byte function-pointer templatable storage (#15545)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-08 08:38:00 -04:00
Jonathan Swoboda 9bf53e0ab8 [esp32_hosted] Add SPI transport and SDIO 1-bit bus width support (#15551) 2026-04-08 03:17:58 +00:00
dependabot[bot] 51f3f5c774 Bump esphome-dashboard from 20260210.0 to 20260408.1 (#15552)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 03:08:28 +00:00
Szewcson 313b9fd5bf [gdk101] Retry reset on interval for slow-booting sensor MCU (#11750)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-07 17:05:18 -10:00
J. Nick Koston e658a8559e [ethernet] Add W6100 and W6300 support for RP2040 (#15543) 2026-04-07 16:57:05 -10:00
J. Nick Koston 4db82877af [yaml] Add IncludeFile representer to ESPHomeDumper (#15549) 2026-04-07 16:27:11 -10:00
dependabot[bot] 2e3ff4e215 Bump cryptography from 46.0.6 to 46.0.7 (#15550)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-08 02:11:51 +00:00
Jonathan Swoboda 8ffe0f5e31 [core] Fix ANSI codes for secret text hiding (#15521) 2026-04-07 22:02:36 -04:00
J. Nick Koston c7513b9262 [ci] Add lint check for test package key matching bus directory (#15547) 2026-04-07 16:01:18 -10:00
J. Nick Koston de7f081799 [emontx] Fix uart package name in tests (#15546) 2026-04-07 21:52:37 -04:00
Clyde Stubbs 88f4067dd6 [lvgl] Implement rotation with PPA (#15453) 2026-04-08 13:19:29 +12:00
Javier Peletier d20d613c1d [substitutions] !include ${filename}, Substitutions in include filename paths (package refactor part 5) (#12213)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-07 15:12:55 -10:00
Clyde Stubbs 801f3fadaa [epaper_spi] Fix deep sleep command (#15544) 2026-04-08 13:00:39 +12:00
Jesse Hills b307c7c74c [config_validation] Add unbounded percentage validators (#15500) 2026-04-08 11:44:52 +12:00
Jonathan Swoboda aad898503d [multiple] Fix channel/pin range validation and widen channel types (#15529) 2026-04-07 18:37:17 -04:00
Frédéric Metrich 14bcdfe700 [emontx] emonTx component (#9027)
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-07 22:29:55 +00:00
Jonathan Swoboda 0d7f2f05b9 [libretiny] Fix board pin alias resolution TypeError (#15527) 2026-04-07 18:16:37 -04:00
Edward Firmo ee7b38504b [nextion] Expose custom protocol frames as automation triggers (#13248)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-07 22:13:58 +00:00
J. Nick Koston 5d31f4aeba [light] Use function-pointer fields in LightControlAction (#15132) 2026-04-07 12:00:17 -10:00
Jonathan Swoboda 9fe4d5c63d [rp2040_pio_led_strip][rp2040_pio] Fix CUSTOM chipset crash and improve error message (#15537) 2026-04-07 11:56:50 -10:00
Jonathan Swoboda 97ad5ab35f [udp] Fix on_receive only processing first automation (#15538) 2026-04-07 11:56:01 -10:00
Jonathan Swoboda e7ddc6f6d3 [multiple] Fix validation ranges (batch 2) (#15533) 2026-04-07 17:54:57 -04:00
Jonathan Swoboda cbcf80081b [pcf8563] Fix default I2C address from 8-bit (0xA3) to 7-bit (0x51) (#15526) 2026-04-07 17:54:12 -04:00
Jonathan Swoboda 3073f3ec5c [haier] Fix control_method schema incorrectly using ensure_list (#15523) 2026-04-07 17:53:16 -04:00
Jonathan Swoboda 5a52936f72 [graph] Fix legend config incorrectly accepting a list (#15522) 2026-04-07 17:52:33 -04:00
Jonathan Swoboda 3ca3cdc5e2 [multiple] Fix missing entity base classes in Python class declarations (#15534) 2026-04-07 11:44:28 -10:00
Jonathan Swoboda 4ebfe71b8f [seeed_mr24hpc1] Move baud rate validation to FINAL_VALIDATE_SCHEMA (#15536) 2026-04-07 11:42:33 -10:00
Jonathan Swoboda 2fe6cb392b [rotary_encoder] Fix set_value action accepting any sensor ID (#15535) 2026-04-07 11:40:43 -10:00
Edward Firmo d354747da0 [nextion] Fix format specifiers and error message typos in command handlers (#15542) 2026-04-07 21:10:56 +00:00
Jonathan Swoboda 17ec5389d8 [mcp4461] Fix terminal disable passing string where C++ expects char (#15528) 2026-04-07 11:07:28 -10:00
Jonathan Swoboda 687753b0be [lightwaverf] Fix write pin using input schema instead of output (#15525) 2026-04-07 11:03:55 -10:00
Jonathan Swoboda 186525e77d [ld2420] Fix select options wrapped in extra list (#15524) 2026-04-07 10:57:26 -10:00
Jonathan Swoboda 9d396cea5a [grove_tb6612fng] Move direction logic from Python to C++ to fix lambda crash (#15513) 2026-04-07 10:56:25 -10:00
dependabot[bot] ac14b9e558 Bump pypa/gh-action-pypi-publish from 1.13.0 to 1.14.0 (#15541)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 10:40:21 -10:00
J. Nick Koston ef6c65c7ec [cli] Add config bundle CLI command for remote compilation (#13791) 2026-04-07 10:37:19 -10:00
dependabot[bot] c6c743e2bb Bump pytest from 9.0.2 to 9.0.3 (#15540)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 10:26:11 -10:00
J. Nick Koston 6460f3a757 [api] Add max_data_length and force to DeviceInfoResponse/HelloResponse proto fields (#15514) 2026-04-07 10:24:36 -10:00
J. Nick Koston 0d809a7481 [automation] Add CallbackAutomation dataclass and build_callback_automations helper (#15246) 2026-04-07 10:09:27 -10:00
J. Nick Koston 674d030cbb [core] Reschedule fired intervals directly into heap (#15516) 2026-04-07 07:36:55 -10:00
Diorcet Yann 7ab7538220 [hdc2080] Fix tests (#15518) 2026-04-06 21:59:05 -10:00
dependabot[bot] 488a6a1c40 Bump aioesphomeapi from 44.11.1 to 44.12.0 (#15515)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 05:15:03 +00:00
J. Nick Koston f94e1dfab6 [core] Move ControllerRegistry notify methods inline into header (#15505) 2026-04-07 16:12:01 +12:00
Jonathan Swoboda e49384cd57 [dfrobot_sen0395] Fix list.index() on mutated list in range validator (#15511) 2026-04-06 23:42:39 -04:00
J. Nick Koston 10b38e1588 [api] Add max_data_length proto option and optimize entity name/object_id (#15426) 2026-04-07 03:31:01 +00:00
Jonathan Swoboda b6ef1a58fb [multiple] Fix validation ranges and error messages (#15508) 2026-04-06 23:17:35 -04:00
Jonathan Swoboda 9894bdc0f1 [multiple] Fix misc low-priority bugs (batch 3) (#15506) 2026-04-06 23:03:57 -04:00
Jonathan Swoboda 99ee405f4e [esp32_ble][esp32_ble_server][esp32_ble_beacon] Fix UUID regex, IndexError, and unused inheritance (#15504) 2026-04-06 22:17:34 -04:00
Jonathan Swoboda 517d0390d0 [ota] Fix check_error skipping validation for RESPONSE_OK (#15501) 2026-04-06 22:17:25 -04:00
J. Nick Koston 96c3986481 [core] Replace std::vector in CallbackManager with trivial-copy container (#15272) 2026-04-07 01:58:17 +00:00
Jonathan Swoboda e62c78ad46 [multiple] Fix misc cosmetic bugs (error messages, types, defaults) (#15499) 2026-04-07 01:41:57 +00:00
Jonathan Swoboda e428cb5092 [multiple] Fix misc cosmetic bugs (batch 2) (#15502) 2026-04-06 21:33:22 -04:00
Jonathan Swoboda b8b8d1bb15 [core] Replace deprecated datetime.utcfromtimestamp() (#15503) 2026-04-06 21:31:57 -04:00
J. Nick Koston 82dc80a413 [scheduler] Skip cancel for anonymous items, add empty-container fast path (#15397) 2026-04-07 01:26:40 +00:00
J. Nick Koston d15fa84f4f [api] Auto-derive max_value for enum fields in protobuf codegen (#15469) 2026-04-06 14:39:55 -10:00
Jonathan Swoboda 4fa3e48d33 [remote_base] Fix misc protocol schema and codegen bugs (#15497) 2026-04-07 00:34:07 +00:00
Jonathan Swoboda 094e0440c6 [config] Fix unfilled placeholder in dimensions() error message (#15498) 2026-04-06 14:30:36 -10:00
J. Nick Koston b155c13117 [api] Use integer comparison for float zero checks in protobuf encoding (#15490) 2026-04-07 12:25:53 +12:00
Jonathan Swoboda 0816579fa9 [prometheus] Fix relabel validation not checking for required keys (#15496) 2026-04-06 14:20:46 -10:00
Jonathan Swoboda c6e683cc33 [pmsx003] Connect model-specific sensor validation to schema (#15495) 2026-04-06 14:19:53 -10:00
Jonathan Swoboda 14bcd9db59 [neopixelbus] Fix SPI pin validation accepting one wrong pin on ESP8266 (#15494) 2026-04-06 14:18:59 -10:00
Jonathan Swoboda d9da91efbe [bl0940] Fix restore_value reading from wrong config dict (#15492) 2026-04-06 14:14:17 -10:00
Jesse Hills 017af24c22 Merge branch 'release' into dev 2026-04-07 12:06:30 +12:00
Jesse Hills 496c395f1a Merge pull request #15489 from esphome/bump-2026.3.3
2026.3.3
2026-04-07 12:05:46 +12:00
Jonathan Swoboda 29ca7bc8f9 [espnow] Fix string data generating invalid C++ char literals (#15493) 2026-04-06 19:57:16 -04:00
Jesse Hills 62d0c25a2b [CI] Add branches-ignore for release and beta in PR title check (#15491) 2026-04-07 11:14:59 +12:00
Jesse Hills 1c67e4ce4c Bump version to 2026.3.3 2026-04-07 10:50:41 +12:00
Clyde Stubbs 162c8810db [esp32] Clean build when sdkconfig options change (#15439) 2026-04-07 10:50:41 +12:00
Clyde Stubbs 9036c29c8a [online_image] Clear LVGL dsc when image size changes. (#15360) 2026-04-07 10:50:41 +12:00
Edward Firmo 9bd936112d [nextion] Fix queue age check using inconsistent time sources (#15317) 2026-04-07 10:50:41 +12:00
Clyde Stubbs c98bb9060f [lvgl] Fix setting triggers on display (#15364) 2026-04-07 10:48:14 +12:00
Clyde Stubbs ce0d360790 [lvgl] Implement rotation directly (#14955) 2026-04-07 10:46:42 +12:00
J. Nick Koston 2b5ee69eb2 [api] Speed up protobuf encode 17-20% with register-optimized write path (#15290)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-04-06 12:42:18 -10:00
Jonathan Swoboda 5a14d6a4ad [multiple] Add missing device_class to sensor schemas (batch 2) (#15487)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-06 22:38:47 +00:00
Jonathan Swoboda 6f62b2f18c [thermostat] Remove non-functional cv.templatable from preset fields (#15481) 2026-04-06 12:20:38 -10:00
Jonathan Swoboda c78fb964a2 [multiple] Add missing state_class to remaining sensor schemas (#15486) 2026-04-06 12:15:42 -10:00
Jonathan Swoboda 8650c5b013 [multiple] Add missing state_class to sensor schemas (#15478) 2026-04-06 17:19:20 -04:00
Jonathan Swoboda 5051891813 [esp32] Fix ESP32-C6 pin validator rejecting GPIO 24-30 with wrong error (#15477) 2026-04-06 17:02:28 -04:00
Jonathan Swoboda 95e2b0a8b0 [multiple] Add missing device_class to sensor schemas (#15479) 2026-04-06 17:02:20 -04:00
J. Nick Koston ab45591507 [core] Move wake_loop out of socket component into core (#15446)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-04-06 21:01:03 +00:00
Jonathan Swoboda 62b4b250c7 [opentherm] Fix step=0 default overriding entity step (#15484) 2026-04-07 08:35:50 +12:00
Jonathan Swoboda a7963bee98 [gcja5][cd74hc4067][openthread_info] Fix PollingComponent mismatches (#15476) 2026-04-06 16:31:40 -04:00
Jonathan Swoboda e86978f0da [rpi_dpi_rgb][st7701s][ags10] Fix Optional config keys accessed unconditionally (#15474) 2026-04-06 16:30:46 -04:00
Jonathan Swoboda 6044f41db5 [multiple] Add missing cv.COMPONENT_SCHEMA to CONFIG_SCHEMA (#15475) 2026-04-06 16:30:15 -04:00
Jonathan Swoboda a64f09a43f [sprinkler][dfplayer][max6956][rf_bridge] Fix cg.templatable type mismatches (#15480) 2026-04-06 16:29:59 -04:00
Jonathan Swoboda dbd4e77d61 [pylontech] Remove unnecessary Component inheritance from sensor/text_sensor (#15482) 2026-04-06 16:23:10 -04:00
Boris Krivonog 02185fb4f4 [mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 5) (#15483) 2026-04-06 09:59:18 -10:00
dependabot[bot] 2f2b7e42ba Bump aioesphomeapi from 44.9.1 to 44.11.1 (#15471) 2026-04-05 21:15:02 -10:00
dependabot[bot] 1c97954b47 Bump aioesphomeapi from 44.9.0 to 44.9.1 (#15470)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-05 18:42:17 -10:00
Boris Krivonog 859ea23bde [mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 4) (#15462) 2026-04-05 18:33:02 -10:00
Jonathan Swoboda 7644f17cf6 [at581x] Fix codegen crash when using lambdas for frequency/time/power (#15468) 2026-04-06 00:05:04 -04:00
J. Nick Koston 1de94c1a84 [api] Add max_value proto option for constant-size varint codegen (#15424) 2026-04-05 18:02:06 -10:00
J. Nick Koston 10f08e0802 [esp8266] Add crash handler for post-mortem diagnostics (#15465) 2026-04-06 03:30:56 +00:00
Jonathan Swoboda aac74f4c94 [ags10] Fix wrong type passed to cg.templatable for set_zero_point mode (#15467) 2026-04-05 22:48:00 -04:00
Keith Burzinski 07f6be679f [esp32] Add signed app verification without hardware secure boot (#15357)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 21:20:48 -05:00
J. Nick Koston ea0ce710a8 [api] Split Noise handshake state_action_ to reduce stack pressure (#15464) 2026-04-05 13:55:06 -10:00
J. Nick Koston 155657f1cc [mcp23xxx][pi4ioe5v6408] Disable loop when all pins are outputs (#15460) 2026-04-05 13:26:55 -10:00
J. Nick Koston 0f2d8656ad [esp32_ble] Skip dropped count memw when queue is empty (#15422) 2026-04-05 13:26:40 -10:00
J. Nick Koston 30d1230a17 [button] Downgrade press logging from DEBUG to VERBOSE (#15408) 2026-04-05 13:26:21 -10:00
J. Nick Koston 83a4edbea1 [select] [switch] Downgrade control path logging from DEBUG to VERBOSE (#15406) 2026-04-05 13:26:08 -10:00
J. Nick Koston f193bab60b [api] Add ListEntities benchmarks for sensor, binary_sensor, and light (#15427) 2026-04-05 13:25:50 -10:00
Tomer27cz f01762ea44 [ci] move import to function (#15440) 2026-04-05 19:17:52 -04:00
Andrew Rankin f23843130e [lvgl] option to enable LVGL's built-in dark theme (#15389) 2026-04-06 09:07:42 +10:00
Ross Tyler c7a163441e [ethernet] Add interface configuration variable for esp-idf (#10285)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
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>
2026-04-05 20:57:41 +00:00
Edvard Filistovič ae9068a4c4 [internal_temperature] Add support for LN882X (Lightning LN882H) (#15370)
Co-authored-by: Bl00d-B0b <Bl00d-B0b@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-05 09:17:12 -10:00
J. Nick Koston dae8ea1b04 [mcp23xxx][pi4ioe5v6408] Add optional interrupt pin to eliminate polling (#15445) 2026-04-05 08:26:39 -10:00
Javier Peletier 2d7eb116f2 [spi] Enable host-platform builds for unit testing (#15188) 2026-04-05 20:11:49 +10:00
J. Nick Koston 9ea27e68ee [pcf8574][pca9554] Disable loop when all pins are outputs (#15455) 2026-04-04 22:52:40 -10:00
Clyde Stubbs 4d2062282e [mipi_spi] Run spi final validation (#15418)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-04-04 21:11:49 -04:00
J. Nick Koston 2d9a42e4ba [pcf8574][pca9554] Add optional interrupt pin to eliminate polling (#15444) 2026-04-04 13:56:21 -10:00
Boris Krivonog 830517a98f [mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 3) (#15437)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-04-04 12:40:05 -10:00
Clyde Stubbs 1a1725f958 [esp32] Clean build when sdkconfig options change (#15439) 2026-04-04 09:11:29 -04:00
J. Nick Koston 297f9c134f [time] Use set_interval for CronTrigger instead of loop() (#15433) 2026-04-04 01:07:16 -10:00
J. Nick Koston f51871fa6b [total_daily_energy] Replace loop() with timeout-based midnight reset (#15432) 2026-04-04 00:37:50 -10:00
J. Nick Koston 9ee5089891 [time] Support */N syntax in cron expressions (#15434) 2026-04-04 00:30:41 -10:00
J. Nick Koston b0d39aedd3 [hlw8012] Change periodic sensor reading logs to LOGV (#15431) 2026-04-04 00:30:29 -10:00
Clyde Stubbs 89de00e7ce [online_image] Clear LVGL dsc when image size changes. (#15360) 2026-04-04 17:04:01 +11:00
alorente 53b6528cc5 [epaper_spi] Allow runtime rotation change (#15419)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-04-04 16:02:15 +10:00
Boris Krivonog 16ae753317 [mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 2) (#15358) 2026-04-03 19:44:04 -10:00
J. Nick Koston 2337767c38 [modbus_controller] Fix format specifier warnings (#15429) 2026-04-03 16:37:31 -10:00
J. Nick Koston 4f2290d548 [web_server] Disable loop when no SSE clients are connected (#15428) 2026-04-03 16:37:20 -10:00
Clyde Stubbs 7ab26a4fe0 [ili9xxx][st7735] Add deprecation warnings (#15416) 2026-04-04 13:21:58 +11:00
dependabot[bot] 533eeabf1d Bump aioesphomeapi from 44.8.1 to 44.9.0 (#15425)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 23:17:49 +00:00
Bonne Eggleston c6bb1fe141 [modbus] Add integration tests for server and server via controller (#14845)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-03 20:24:02 +00:00
dependabot[bot] f8f65c1a7b Bump click from 8.3.1 to 8.3.2 (#15421)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-03 09:42:37 -10:00
J. Nick Koston d90e2a6a9a [core] Use __builtin_ctz for FiniteSetMask bit scanning (#15400) 2026-04-03 08:28:54 -10:00
J. Nick Koston 4969fd6e99 [light] Use reciprocal multiply in normalize_color (#15401) 2026-04-03 08:28:41 -10:00
J. Nick Koston 95683b7416 [light] Pass LightTraits to avoid redundant virtual get_traits() calls (#15403) 2026-04-03 08:28:29 -10:00
J. Nick Koston 38f4dc3217 [uptime] Pass known length to publish_state to avoid redundant strlen (#15410) 2026-04-03 08:28:07 -10:00
J. Nick Koston f2a0d9943d [benchmarks] Add host platform benchmarks for text_sensor and button (#15407) 2026-04-03 08:27:55 -10:00
J. Nick Koston ea0227a206 [benchmarks] Add host platform benchmarks for number, select, and switch (#15405) 2026-04-03 08:27:44 -10:00
J. Nick Koston 5a23669747 [scheduler] Fix unrealistic scheduler benchmarks missing periodic drain (#15396) 2026-04-03 08:27:29 -10:00
J. Nick Koston 2a5933e4f7 [host] Add graceful shutdown on SIGINT/SIGTERM (#15387) 2026-04-03 08:27:13 -10:00
Jonathan Swoboda 6fecd72049 [ezo_pmp] Fix change_i2c_address action using wrong template type (#15393) 2026-04-03 08:35:16 -04:00
Clyde Stubbs 8360502a94 [ci] Fix deprecated-component matcher (#15417) 2026-04-03 08:01:04 -04:00
Jonathan Swoboda 5548a32771 [ili9xxx] Fix SPI MOSI pin validation never executing (#15399) 2026-04-03 21:15:51 +11:00
Clyde Stubbs 6f05e3d204 [ci] Run ci-custom.py as a pre-commit check (#15411) 2026-04-03 12:54:44 +11:00
Jonathan Swoboda bcd8ddeabe [lvgl] Fix ext_click_area property application (#15394)
Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com>
2026-04-03 12:44:54 +11:00
Clyde Stubbs af662da90d [mipi_spi] Rotation and buffer size changes (#15047) 2026-04-03 12:28:45 +11:00
Keith Burzinski 710186998b [ota] Use modernized namespace syntax (#15398) 2026-04-02 19:12:05 -04:00
J. Nick Koston be3e0c27bf [core] Inline fast path for enable_loop (#15392) 2026-04-02 21:28:12 +00:00
Jonathan Swoboda 4d0d3cc271 [sen5x] Remove dead voc_baseline config option (#15391) 2026-04-02 10:53:53 -10:00
Jonathan Swoboda 4134763f34 [at581x][canbus] Fix walrus operator skipping falsy config values (#15390) 2026-04-02 20:32:10 +00:00
Edward Firmo 1e72f0ee5a [nextion] Gate waveform code behind USE_NEXTION_WAVEFORM, use StaticRingBuffer (#15273)
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+github@koston.org>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-02 20:17:20 +00:00
J. Nick Koston 63710a4cb7 [spi] Add spi0 and spi1 to reserved IDs for RP2040 compatibility (#15388) 2026-04-02 16:10:16 -04:00
Thom Wiggers c82166e5f3 [dsmr] Allow setting MBUS id for thermal sensors in DSMR component (#7519)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-02 10:06:49 -10:00
Jonathan Swoboda 90624e6eca [deep_sleep] Fix wakeup_pin_mode rejecting lowercase on ESP32/BK72XX (#15384) 2026-04-02 09:34:27 -10:00
Jonathan Swoboda 6b89998b60 [template] Fix cover position_action overridden by has_position default (#15379) 2026-04-02 09:29:33 -10:00
Jonathan Swoboda dde472b0cf [pipsolar] Fix set_level action passing string to cv.use_id (#15380) 2026-04-02 09:28:44 -10:00
dependabot[bot] f7222a0e6c Bump ruff from 0.15.8 to 0.15.9 (#15385)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-02 19:28:30 +00:00
Jonathan Swoboda 0262d20bbe [mlx90393] Remove call to non-existent set_drdy_pin method (#15381) 2026-04-02 09:26:47 -10:00
Jonathan Swoboda 37b33f62de [htu21d] Fix set_heater action reading wrong config key (#15378) 2026-04-02 09:25:54 -10:00
Jonathan Swoboda 2f405fd96f [espnow] Fix enable_on_boot config option not passed to C++ (#15377) 2026-04-02 09:25:15 -10:00
dependabot[bot] 67ee727e38 Bump docker/login-action from 4.0.0 to 4.1.0 in the docker-actions group (#15386)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-02 09:24:26 -10:00
Jonathan Swoboda 12a0f5959f [bl0940] Fix reference_voltage config ignored in non-legacy mode (#15375) 2026-04-02 09:23:04 -10:00
Jonathan Swoboda 5dcae1a133 [climate] Fix MQTT target_temperature_low_state_topic calling wrong setter (#15376) 2026-04-02 09:22:07 -10:00
Jonathan Swoboda 0343121e9b [ble_client] Fix descriptor_uuid ignored for text sensors (#15374) 2026-04-02 09:21:18 -10:00
J. Nick Koston da09e1e1ce [time] Use O(1) closed-form leap year math for epoch-to-year conversion (#15368) 2026-04-02 09:19:47 -10:00
Jonathan Swoboda e7e590b36f [thermostat] Fix on_boot_restore_from DEFAULT_PRESET validation bypass (#15383) 2026-04-02 19:08:43 +00:00
Kevin Ahrendt da8d9d9c2d [audio] use microFLAC library for decoding (#15372) 2026-04-02 11:37:14 -04:00
Kevin Ahrendt b8a9d327f0 [media_player] Add enqueue action (#14775) 2026-04-02 10:40:19 -04:00
tomaszduda23 a359ecaaf4 [zigbee] print logs after reporting info update (#13916)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-04-02 14:12:20 +00:00
J. Nick Koston c21c7dd292 [mitsubishi_cn105] Fix test grouping conflict with uart package (#15366) 2026-04-02 09:12:38 -04:00
Edward Firmo 34295fbd69 [nextion] Collapse nested namespace to esphome::nextion (#15367) 2026-04-02 00:25:54 -10:00
559 changed files with 15564 additions and 4549 deletions
+5 -4
View File
@@ -235,19 +235,20 @@ async function detectDeprecatedComponents(github, context, changedFiles) {
}
}
// Get PR head to fetch files from the PR branch
const prNumber = context.payload.pull_request.number;
// Get base branch ref to check if deprecation already exists for the component
// This prevents flagging a PR that simply adds deprecation
const baseRef = context.payload.pull_request.base.ref;
// Check each component's __init__.py for DEPRECATED_COMPONENT constant
for (const component of components) {
const initFile = `esphome/components/${component}/__init__.py`;
try {
// Fetch file content from PR head using GitHub API
// Fetch file content from base branch using GitHub API
const { data: fileData } = await github.rest.repos.getContent({
owner,
repo,
path: initFile,
ref: `refs/pull/${prNumber}/head`
ref: baseRef
});
// Decode base64 content
+1 -1
View File
@@ -723,7 +723,7 @@ jobs:
cache-key: ${{ needs.common.outputs.cache-key }}
- uses: esphome/pre-commit-action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
env:
SKIP: pylint,clang-tidy-hash
SKIP: pylint,clang-tidy-hash,ci-custom
- uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
if: always()
+3
View File
@@ -3,6 +3,9 @@ name: PR Title Check
on:
pull_request:
types: [opened, edited, synchronize, reopened]
branches-ignore:
- release
- beta
permissions:
contents: read
+5 -5
View File
@@ -70,7 +70,7 @@ jobs:
pip3 install build
python3 -m build
- name: Publish
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
with:
skip-existing: true
@@ -102,12 +102,12 @@ jobs:
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Log in to docker hub
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
username: ${{ secrets.DOCKER_USER }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to the GitHub container registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -182,13 +182,13 @@ jobs:
- name: Log in to docker hub
if: matrix.registry == 'dockerhub'
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.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@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+5 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.8
rev: v0.15.9
hooks:
# Run the linter.
- id: ruff
@@ -65,3 +65,7 @@ repos:
files: ^(\.clang-tidy|platformio\.ini|requirements_dev\.txt)$
pass_filenames: false
additional_dependencies: []
- id: ci-custom
name: ci-custom
entry: python3 script/run-in-env.py script/ci-custom.py
language: system
+2 -1
View File
@@ -142,12 +142,13 @@ esphome/components/dlms_meter/* @SimonFischer04
esphome/components/dps310/* @kbx81
esphome/components/ds1307/* @badbadc0ffee
esphome/components/ds2484/* @mrk-its
esphome/components/dsmr/* @glmnet @PolarGoose @zuidwijk
esphome/components/dsmr/* @glmnet @PolarGoose
esphome/components/duty_time/* @dudanov
esphome/components/ee895/* @Stock-M
esphome/components/ektf2232/touchscreen/* @jesserockz
esphome/components/emc2101/* @ellull
esphome/components/emmeti/* @E440QF
esphome/components/emontx/* @FredM67 @glynhudson @TrystanLea
esphome/components/ens160/* @latonita
esphome/components/ens160_base/* @latonita @vincentscode
esphome/components/ens160_i2c/* @latonita
+62 -1
View File
@@ -1083,7 +1083,7 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
# add the console decoration so the front-end can hide the secrets
if not args.show_secrets:
output = re.sub(
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[5m\2\\033[6m", output
r"(password|key|psk|ssid)\: (.+)", r"\1: \\033[8m\2\\033[28m", output
)
if not CORE.quiet:
safe_print(output)
@@ -1242,6 +1242,38 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
return 0
def command_bundle(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome.bundle import BUNDLE_EXTENSION, ConfigBundleCreator
creator = ConfigBundleCreator(config)
if args.list_only:
files = creator.discover_files()
for bf in sorted(files, key=lambda f: f.path):
safe_print(f" {bf.path}")
_LOGGER.info("Found %d files", len(files))
return 0
result = creator.create_bundle()
if args.output:
output_path = Path(args.output)
else:
stem = CORE.config_path.stem
output_path = CORE.config_dir / f"{stem}{BUNDLE_EXTENSION}"
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_bytes(result.data)
_LOGGER.info(
"Bundle created: %s (%d files, %.1f KB)",
output_path,
len(result.files),
len(result.data) / 1024,
)
return 0
def command_dashboard(args: ArgsProtocol) -> int | None:
from esphome.dashboard import dashboard
@@ -1517,6 +1549,7 @@ POST_CONFIG_ACTIONS = {
"rename": command_rename,
"discover": command_discover,
"analyze-memory": command_analyze_memory,
"bundle": command_bundle,
}
SIMPLE_CONFIG_ACTIONS = [
@@ -1818,6 +1851,24 @@ def parse_args(argv):
"configuration", help="Your YAML configuration file(s).", nargs="+"
)
parser_bundle = subparsers.add_parser(
"bundle",
help="Create a self-contained config bundle for remote compilation.",
)
parser_bundle.add_argument(
"configuration", help="Your YAML configuration file(s).", nargs="+"
)
parser_bundle.add_argument(
"-o",
"--output",
help="Output path for the bundle archive.",
)
parser_bundle.add_argument(
"--list-only",
help="List discovered files without creating the archive.",
action="store_true",
)
# Keep backward compatibility with the old command line format of
# esphome <config> <command>.
#
@@ -1896,6 +1947,16 @@ def run_esphome(argv):
_LOGGER.warning("Skipping secrets file %s", conf_path)
return 0
# Bundle support: if the configuration is a .esphomebundle, extract it
# and rewrite conf_path to the extracted YAML config.
from esphome.bundle import is_bundle_path, prepare_bundle_for_compile
if is_bundle_path(conf_path):
_LOGGER.info("Extracting config bundle %s...", conf_path)
conf_path = prepare_bundle_for_compile(conf_path)
# Update the argument so downstream code sees the extracted path
args.configuration[0] = str(conf_path)
CORE.config_path = conf_path
CORE.dashboard = args.dashboard
+33
View File
@@ -1,3 +1,4 @@
from dataclasses import dataclass, field
import logging
import esphome.codegen as cg
@@ -715,3 +716,35 @@ async def build_callback_automation(
# MockObjs (not user input), and there's no Expression type for positional
# aggregate initialization (StructInitializer uses named fields).
cg.add(getattr(parent, callback_method)(cg.RawExpression(f"{forwarder}{{{obj}}}")))
@dataclass(frozen=True, slots=True)
class CallbackAutomation:
"""A single callback automation entry for build_callback_automations."""
conf_key: str
callback_method: str
args: TemplateArgsType = field(default_factory=list)
forwarder: MockObj | MockObjClass | None = None
async def build_callback_automations(
parent: MockObj,
config: ConfigType,
entries: tuple[CallbackAutomation, ...],
) -> None:
"""Build multiple callback automations from a tuple of entries.
:param parent: The component object (e.g., button, sensor).
:param config: The full component config dict.
:param entries: Tuple of CallbackAutomation entries to process.
"""
for entry in entries:
for conf in config.get(entry.conf_key, []):
await build_callback_automation(
parent,
entry.callback_method,
entry.args,
conf,
forwarder=entry.forwarder,
)
+699
View File
@@ -0,0 +1,699 @@
"""Config bundle creator and extractor for ESPHome.
A bundle is a self-contained .tar.gz archive containing a YAML config
and every local file it depends on. Bundles can be created from a config
and compiled directly: ``esphome compile my_device.esphomebundle.tar.gz``
"""
from __future__ import annotations
from dataclasses import dataclass
from enum import StrEnum
import io
import json
import logging
from pathlib import Path
import re
import shutil
import tarfile
from typing import Any
from esphome import const, yaml_util
from esphome.const import (
CONF_ESPHOME,
CONF_EXTERNAL_COMPONENTS,
CONF_INCLUDES,
CONF_INCLUDES_C,
CONF_PATH,
CONF_SOURCE,
CONF_TYPE,
)
from esphome.core import CORE, EsphomeError
_LOGGER = logging.getLogger(__name__)
BUNDLE_EXTENSION = ".esphomebundle.tar.gz"
MANIFEST_FILENAME = "manifest.json"
CURRENT_MANIFEST_VERSION = 1
MAX_DECOMPRESSED_SIZE = 500 * 1024 * 1024 # 500 MB
MAX_MANIFEST_SIZE = 1024 * 1024 # 1 MB
# Directories preserved across bundle extractions (build caches)
_PRESERVE_DIRS = (".esphome", ".pioenvs", ".pio")
_BUNDLE_STAGING_DIR = ".bundle_staging"
class ManifestKey(StrEnum):
"""Keys used in bundle manifest.json."""
MANIFEST_VERSION = "manifest_version"
ESPHOME_VERSION = "esphome_version"
CONFIG_FILENAME = "config_filename"
FILES = "files"
HAS_SECRETS = "has_secrets"
# String prefixes that are never local file paths
_NON_PATH_PREFIXES = ("http://", "https://", "ftp://", "mdi:", "<")
# File extensions recognized when resolving relative path strings.
# A relative string with one of these extensions is resolved against the
# config directory and included if the file exists.
_KNOWN_FILE_EXTENSIONS = frozenset(
{
# Fonts
".ttf",
".otf",
".woff",
".woff2",
".pcf",
".bdf",
# Images
".png",
".jpg",
".jpeg",
".bmp",
".gif",
".svg",
".ico",
".webp",
# Certificates
".pem",
".crt",
".key",
".der",
".p12",
".pfx",
# C/C++ includes
".h",
".hpp",
".c",
".cpp",
".ino",
# Web assets
".css",
".js",
".html",
}
)
# Matches !secret references in YAML text. This is intentionally a simple
# regex scan rather than a YAML parse — it may match inside comments or
# multi-line strings, which is the conservative direction (include more
# secrets rather than fewer).
_SECRET_RE = re.compile(r"!secret\s+(\S+)")
def _find_used_secret_keys(yaml_files: list[Path]) -> set[str]:
"""Scan YAML files for ``!secret <key>`` references."""
keys: set[str] = set()
for fpath in yaml_files:
try:
text = fpath.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
continue
for match in _SECRET_RE.finditer(text):
keys.add(match.group(1))
return keys
@dataclass
class BundleFile:
"""A file to include in the bundle."""
path: str # Relative path inside the archive
source: Path # Absolute path on disk
@dataclass
class BundleResult:
"""Result of creating a bundle."""
data: bytes
manifest: dict[str, Any]
files: list[BundleFile]
@dataclass
class BundleManifest:
"""Parsed and validated bundle manifest."""
manifest_version: int
esphome_version: str
config_filename: str
files: list[str]
has_secrets: bool
class ConfigBundleCreator:
"""Creates a self-contained bundle from an ESPHome config."""
def __init__(self, config: dict[str, Any]) -> None:
self._config = config
self._config_dir = CORE.config_dir
self._config_path = CORE.config_path
self._files: list[BundleFile] = []
self._seen_paths: set[Path] = set()
self._secrets_paths: set[Path] = set()
def discover_files(self) -> list[BundleFile]:
"""Discover all files needed for the bundle."""
self._files = []
self._seen_paths = set()
self._secrets_paths = set()
# The main config file
self._add_file(self._config_path)
# Phase 1: YAML includes (tracked during config loading)
self._discover_yaml_includes()
# Phase 2: Component-referenced files from validated config
self._discover_component_files()
return list(self._files)
def create_bundle(self) -> BundleResult:
"""Create the bundle archive."""
files = self.discover_files()
# Determine which secret keys are actually referenced by the
# bundled YAML files so we only ship those, not the entire
# secrets.yaml which may contain secrets for other devices.
yaml_sources = [
bf.source for bf in files if bf.source.suffix in (".yaml", ".yml")
]
used_secret_keys = _find_used_secret_keys(yaml_sources)
filtered_secrets = self._build_filtered_secrets(used_secret_keys)
has_secrets = bool(filtered_secrets)
if has_secrets:
_LOGGER.warning(
"Bundle contains secrets (e.g. Wi-Fi passwords). "
"Do not share it with untrusted parties."
)
manifest = self._build_manifest(files, has_secrets=has_secrets)
buf = io.BytesIO()
with tarfile.open(fileobj=buf, mode="w:gz") as tar:
# Add manifest first
manifest_data = json.dumps(manifest, indent=2).encode("utf-8")
_add_bytes_to_tar(tar, MANIFEST_FILENAME, manifest_data)
# Add filtered secrets files
for rel_path, data in sorted(filtered_secrets.items()):
_add_bytes_to_tar(tar, rel_path, data)
# Add files in sorted order for determinism, skipping secrets
# files which were already added above with filtered content
for bf in sorted(files, key=lambda f: f.path):
if bf.source in self._secrets_paths:
continue
self._add_to_tar(tar, bf)
return BundleResult(data=buf.getvalue(), manifest=manifest, files=files)
def _add_file(self, abs_path: Path) -> bool:
"""Add a file to the bundle. Returns False if already added."""
abs_path = abs_path.resolve()
if abs_path in self._seen_paths:
return False
if not abs_path.is_file():
_LOGGER.warning("Bundle: skipping missing file %s", abs_path)
return False
rel_path = self._relative_to_config_dir(abs_path)
if rel_path is None:
_LOGGER.warning(
"Bundle: skipping file outside config directory: %s", abs_path
)
return False
self._seen_paths.add(abs_path)
self._files.append(BundleFile(path=rel_path, source=abs_path))
return True
def _add_directory(self, abs_path: Path) -> None:
"""Recursively add all files in a directory."""
abs_path = abs_path.resolve()
if not abs_path.is_dir():
_LOGGER.warning("Bundle: skipping missing directory %s", abs_path)
return
for child in sorted(abs_path.rglob("*")):
if child.is_file() and "__pycache__" not in child.parts:
self._add_file(child)
def _relative_to_config_dir(self, abs_path: Path) -> str | None:
"""Get a path relative to the config directory. Returns None if outside.
Always uses forward slashes for consistency in tar archives.
"""
try:
return abs_path.relative_to(self._config_dir).as_posix()
except ValueError:
return None
def _discover_yaml_includes(self) -> None:
"""Discover YAML files loaded during config parsing.
We track files by wrapping _load_yaml_internal. The config has already
been loaded at this point (bundle is a POST_CONFIG_ACTION), so we
re-load just to discover the file list.
Secrets files are tracked separately so we can filter them to
only include the keys this config actually references.
"""
with yaml_util.track_yaml_loads() as loaded_files:
try:
yaml_util.load_yaml(self._config_path)
except EsphomeError:
_LOGGER.debug(
"Bundle: re-loading YAML for include discovery failed, "
"proceeding with partial file list"
)
for fpath in loaded_files:
if fpath == self._config_path.resolve():
continue # Already added as config
if fpath.name in const.SECRETS_FILES:
self._secrets_paths.add(fpath)
self._add_file(fpath)
def _discover_component_files(self) -> None:
"""Walk the validated config for file references.
Uses a generic recursive walk to find file paths instead of
hardcoding per-component knowledge about config dict formats.
After validation, components typically resolve paths to absolute
using CORE.relative_config_path() or cv.file_(). Relative paths
with known file extensions are also resolved and checked.
Core ESPHome concepts that use relative paths or directories
are handled explicitly.
"""
config = self._config
# Generic walk: find all file paths in the validated config
self._walk_config_for_files(config)
# --- Core ESPHome concepts needing explicit handling ---
# esphome.includes / includes_c - can be relative paths and directories
esphome_conf = config.get(CONF_ESPHOME, {})
for include_path in esphome_conf.get(CONF_INCLUDES, []):
resolved = _resolve_include_path(include_path)
if resolved is None:
continue
if resolved.is_dir():
self._add_directory(resolved)
else:
self._add_file(resolved)
for include_path in esphome_conf.get(CONF_INCLUDES_C, []):
resolved = _resolve_include_path(include_path)
if resolved is not None:
self._add_file(resolved)
# external_components with source: local - directories
for ext_conf in config.get(CONF_EXTERNAL_COMPONENTS, []):
source = ext_conf.get(CONF_SOURCE, {})
if not isinstance(source, dict):
continue
if source.get(CONF_TYPE) != "local":
continue
path = source.get(CONF_PATH)
if not path:
continue
p = Path(path)
if not p.is_absolute():
p = CORE.relative_config_path(p)
self._add_directory(p)
def _walk_config_for_files(self, obj: Any) -> None:
"""Recursively walk the config dict looking for file path references."""
if isinstance(obj, dict):
for value in obj.values():
self._walk_config_for_files(value)
elif isinstance(obj, (list, tuple)):
for item in obj:
self._walk_config_for_files(item)
elif isinstance(obj, Path):
if obj.is_absolute() and obj.is_file():
self._add_file(obj)
elif isinstance(obj, str):
self._check_string_path(obj)
def _check_string_path(self, value: str) -> None:
"""Check if a string value is a local file reference."""
# Fast exits for strings that cannot be file paths
if len(value) < 2 or "\n" in value:
return
if value.startswith(_NON_PATH_PREFIXES):
return
# File paths must contain a path separator or a dot (for extension)
if "/" not in value and "\\" not in value and "." not in value:
return
p = Path(value)
# Absolute path - check if it points to an existing file
if p.is_absolute():
if p.is_file():
self._add_file(p)
return
# Relative path with a known file extension - likely a component
# validator that forgot to resolve to absolute via cv.file_() or
# CORE.relative_config_path(). Warn and try to resolve.
if p.suffix.lower() in _KNOWN_FILE_EXTENSIONS:
_LOGGER.warning(
"Bundle: non-absolute path in validated config: %s "
"(component validator should return absolute paths)",
value,
)
resolved = CORE.relative_config_path(p)
if resolved.is_file():
self._add_file(resolved)
def _build_filtered_secrets(self, used_keys: set[str]) -> dict[str, bytes]:
"""Build filtered secrets files containing only the referenced keys.
Returns a dict mapping relative archive path to YAML bytes.
"""
if not used_keys or not self._secrets_paths:
return {}
result: dict[str, bytes] = {}
for secrets_path in self._secrets_paths:
rel_path = self._relative_to_config_dir(secrets_path)
if rel_path is None:
continue
try:
all_secrets = yaml_util.load_yaml(secrets_path, clear_secrets=False)
except EsphomeError:
_LOGGER.warning("Bundle: failed to load secrets file %s", secrets_path)
continue
if not isinstance(all_secrets, dict):
continue
filtered = {k: v for k, v in all_secrets.items() if k in used_keys}
if filtered:
data = yaml_util.dump(filtered, show_secrets=True).encode("utf-8")
result[rel_path] = data
return result
def _build_manifest(
self, files: list[BundleFile], *, has_secrets: bool
) -> dict[str, Any]:
"""Build the manifest.json content."""
return {
ManifestKey.MANIFEST_VERSION: CURRENT_MANIFEST_VERSION,
ManifestKey.ESPHOME_VERSION: const.__version__,
ManifestKey.CONFIG_FILENAME: self._config_path.name,
ManifestKey.FILES: [f.path for f in files],
ManifestKey.HAS_SECRETS: has_secrets,
}
@staticmethod
def _add_to_tar(tar: tarfile.TarFile, bf: BundleFile) -> None:
"""Add a BundleFile to the tar archive with deterministic metadata."""
with open(bf.source, "rb") as f:
_add_bytes_to_tar(tar, bf.path, f.read())
def extract_bundle(
bundle_path: Path,
target_dir: Path | None = None,
) -> Path:
"""Extract a bundle archive and return the path to the config YAML.
Sanity checks reject path traversal, symlinks, absolute paths, and
oversized archives to prevent accidental file overwrites or extraction
outside the target directory. These are **not** a security boundary —
bundles are assumed to come from the user's own machine or a trusted
build pipeline.
Args:
bundle_path: Path to the .tar.gz bundle file.
target_dir: Directory to extract into. If None, extracts next to
the bundle file in a directory named after it.
Returns:
Absolute path to the extracted config YAML file.
Raises:
EsphomeError: If the bundle is invalid or extraction fails.
"""
bundle_path = bundle_path.resolve()
if not bundle_path.is_file():
raise EsphomeError(f"Bundle file not found: {bundle_path}")
if target_dir is None:
target_dir = _default_target_dir(bundle_path)
target_dir = target_dir.resolve()
target_dir.mkdir(parents=True, exist_ok=True)
# Read and validate the archive
try:
with tarfile.open(bundle_path, "r:gz") as tar:
manifest = _read_manifest_from_tar(tar)
_validate_tar_members(tar, target_dir)
tar.extractall(path=target_dir, filter="data")
except tarfile.TarError as err:
raise EsphomeError(f"Failed to extract bundle: {err}") from err
config_filename = manifest[ManifestKey.CONFIG_FILENAME]
config_path = target_dir / config_filename
if not config_path.is_file():
raise EsphomeError(
f"Bundle manifest references config '{config_filename}' "
f"but it was not found in the archive"
)
return config_path
def read_bundle_manifest(bundle_path: Path) -> BundleManifest:
"""Read and validate the manifest from a bundle without full extraction.
Args:
bundle_path: Path to the .tar.gz bundle file.
Returns:
Parsed BundleManifest.
Raises:
EsphomeError: If the manifest is missing, invalid, or version unsupported.
"""
try:
with tarfile.open(bundle_path, "r:gz") as tar:
manifest = _read_manifest_from_tar(tar)
except tarfile.TarError as err:
raise EsphomeError(f"Failed to read bundle: {err}") from err
return BundleManifest(
manifest_version=manifest[ManifestKey.MANIFEST_VERSION],
esphome_version=manifest.get(ManifestKey.ESPHOME_VERSION, "unknown"),
config_filename=manifest[ManifestKey.CONFIG_FILENAME],
files=manifest.get(ManifestKey.FILES, []),
has_secrets=manifest.get(ManifestKey.HAS_SECRETS, False),
)
def _read_manifest_from_tar(tar: tarfile.TarFile) -> dict[str, Any]:
"""Read and validate manifest.json from an open tar archive."""
try:
member = tar.getmember(MANIFEST_FILENAME)
except KeyError:
raise EsphomeError("Invalid bundle: missing manifest.json") from None
f = tar.extractfile(member)
if f is None:
raise EsphomeError("Invalid bundle: manifest.json is not a regular file")
if member.size > MAX_MANIFEST_SIZE:
raise EsphomeError(
f"Invalid bundle: manifest.json too large "
f"({member.size} bytes, max {MAX_MANIFEST_SIZE})"
)
try:
manifest = json.loads(f.read())
except (json.JSONDecodeError, UnicodeDecodeError) as err:
raise EsphomeError(f"Invalid bundle: malformed manifest.json: {err}") from err
# Version check
version = manifest.get(ManifestKey.MANIFEST_VERSION)
if version is None:
raise EsphomeError("Invalid bundle: manifest.json missing 'manifest_version'")
if not isinstance(version, int) or version < 1:
raise EsphomeError(
f"Invalid bundle: manifest_version must be a positive integer, got {version!r}"
)
if version > CURRENT_MANIFEST_VERSION:
raise EsphomeError(
f"Bundle manifest version {version} is newer than this ESPHome "
f"version supports (max {CURRENT_MANIFEST_VERSION}). "
f"Please upgrade ESPHome to compile this bundle."
)
# Required fields
if ManifestKey.CONFIG_FILENAME not in manifest:
raise EsphomeError("Invalid bundle: manifest.json missing 'config_filename'")
return manifest
def _validate_tar_members(tar: tarfile.TarFile, target_dir: Path) -> None:
"""Sanity-check tar members to prevent mistakes and accidental overwrites.
This is not a security boundary — bundles are created locally or come
from a trusted build pipeline. The checks catch malformed archives
and common mistakes (stray absolute paths, ``..`` components) that
could silently overwrite unrelated files.
"""
total_size = 0
for member in tar.getmembers():
# Reject absolute paths (Unix and Windows)
if member.name.startswith(("/", "\\")):
raise EsphomeError(
f"Invalid bundle: absolute path in archive: {member.name}"
)
# Reject path traversal (split on both / and \ for cross-platform)
parts = re.split(r"[/\\]", member.name)
if ".." in parts:
raise EsphomeError(
f"Invalid bundle: path traversal in archive: {member.name}"
)
# Reject symlinks
if member.issym() or member.islnk():
raise EsphomeError(f"Invalid bundle: symlink in archive: {member.name}")
# Ensure extraction stays within target_dir
target_path = (target_dir / member.name).resolve()
if not target_path.is_relative_to(target_dir):
raise EsphomeError(
f"Invalid bundle: file would extract outside target: {member.name}"
)
# Track total decompressed size
total_size += member.size
if total_size > MAX_DECOMPRESSED_SIZE:
raise EsphomeError(
f"Invalid bundle: decompressed size exceeds "
f"{MAX_DECOMPRESSED_SIZE // (1024 * 1024)}MB limit"
)
def is_bundle_path(path: Path) -> bool:
"""Check if a path looks like a bundle file."""
return path.name.lower().endswith(BUNDLE_EXTENSION)
def _add_bytes_to_tar(tar: tarfile.TarFile, name: str, data: bytes) -> None:
"""Add in-memory bytes to a tar archive with deterministic metadata."""
info = tarfile.TarInfo(name=name)
info.size = len(data)
info.mtime = 0
info.uid = 0
info.gid = 0
info.mode = 0o644
tar.addfile(info, io.BytesIO(data))
def _resolve_include_path(include_path: Any) -> Path | None:
"""Resolve an include path to absolute, skipping system includes."""
if isinstance(include_path, str) and include_path.startswith("<"):
return None # System include, not a local file
p = Path(include_path)
if not p.is_absolute():
p = CORE.relative_config_path(p)
return p
def _default_target_dir(bundle_path: Path) -> Path:
"""Compute the default extraction directory for a bundle."""
name = bundle_path.name
if name.lower().endswith(BUNDLE_EXTENSION):
name = name[: -len(BUNDLE_EXTENSION)]
return bundle_path.parent / name
def _restore_preserved_dirs(preserved: dict[str, Path], target_dir: Path) -> None:
"""Move preserved build cache directories back into target_dir.
If the bundle contained entries under a preserved directory name,
the extracted copy is removed so the original cache always wins.
"""
for dirname, src in preserved.items():
dst = target_dir / dirname
if dst.exists():
shutil.rmtree(dst)
shutil.move(str(src), str(dst))
def prepare_bundle_for_compile(
bundle_path: Path,
target_dir: Path | None = None,
) -> Path:
"""Extract a bundle for compilation, preserving build caches.
Unlike extract_bundle(), this preserves .esphome/ and .pioenvs/
directories in the target if they already exist (for incremental builds).
Args:
bundle_path: Path to the .tar.gz bundle file.
target_dir: Directory to extract into. Must be specified for
build server use.
Returns:
Absolute path to the extracted config YAML file.
"""
bundle_path = bundle_path.resolve()
if not bundle_path.is_file():
raise EsphomeError(f"Bundle file not found: {bundle_path}")
if target_dir is None:
target_dir = _default_target_dir(bundle_path)
target_dir = target_dir.resolve()
target_dir.mkdir(parents=True, exist_ok=True)
preserved: dict[str, Path] = {}
# Temporarily move preserved dirs out of the way
staging = target_dir / _BUNDLE_STAGING_DIR
for dirname in _PRESERVE_DIRS:
src = target_dir / dirname
if src.is_dir():
dst = staging / dirname
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.move(str(src), str(dst))
preserved[dirname] = dst
try:
# Clean non-preserved content and extract fresh
for item in target_dir.iterdir():
if item.name == _BUNDLE_STAGING_DIR:
continue
if item.is_dir():
shutil.rmtree(item)
else:
item.unlink()
config_path = extract_bundle(bundle_path, target_dir)
finally:
# Restore preserved dirs (idempotent) and clean staging
_restore_preserved_dirs(preserved, target_dir)
if staging.is_dir():
shutil.rmtree(staging)
return config_path
+9 -5
View File
@@ -12,11 +12,15 @@ CONF_ADS1118_ID = "ads1118_id"
ads1118_ns = cg.esphome_ns.namespace("ads1118")
ADS1118 = ads1118_ns.class_("ADS1118", cg.Component, spi.SPIDevice)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ADS1118),
}
).extend(spi.spi_device_schema(cs_pin_required=True))
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ADS1118),
}
)
.extend(spi.spi_device_schema(cs_pin_required=True))
.extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
+9 -5
View File
@@ -35,7 +35,7 @@ CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(AGS10Component),
cv.Optional(CONF_TVOC): sensor.sensor_schema(
cv.Required(CONF_TVOC): sensor.sensor_schema(
unit_of_measurement=UNIT_PARTS_PER_BILLION,
icon=ICON_RADIATOR,
accuracy_decimals=0,
@@ -97,7 +97,7 @@ AGS10_NEW_I2C_ADDRESS_SCHEMA = cv.maybe_simple_value(
async def ags10newi2caddress_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
address = await cg.templatable(config[CONF_ADDRESS], args, int)
address = await cg.templatable(config[CONF_ADDRESS], args, cg.int32)
cg.add(var.set_new_address(address))
return var
@@ -112,7 +112,9 @@ AGS10_SET_ZERO_POINT_ACTION_MODE = {
AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(AGS10Component),
cv.Required(CONF_MODE): cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True),
cv.Required(CONF_MODE): cv.templatable(
cv.enum(AGS10_SET_ZERO_POINT_ACTION_MODE, upper=True)
),
cv.Optional(CONF_VALUE, default=0xFFFF): cv.templatable(cv.uint16_t),
},
)
@@ -127,8 +129,10 @@ AGS10_SET_ZERO_POINT_SCHEMA = cv.Schema(
async def ags10setzeropoint_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
mode = await cg.templatable(config.get(CONF_MODE), args, enumerate)
mode = await cg.templatable(
config.get(CONF_MODE), args, AGS10SetZeroPointActionMode
)
cg.add(var.set_mode(mode))
value = await cg.templatable(config[CONF_VALUE], args, int)
value = await cg.templatable(config[CONF_VALUE], args, cg.uint16)
cg.add(var.set_value(value))
return var
+1 -1
View File
@@ -43,7 +43,7 @@ async def aic3204_set_volume_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config.get(CONF_MODE), args, int)
template_ = await cg.templatable(config.get(CONF_MODE), args, cg.int32)
cg.add(var.set_auto_mute_mode(template_))
return var
@@ -111,42 +111,66 @@ ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_STATE, "add_on_state_callback", forwarder=StateAnyForwarder
),
automation.CallbackAutomation(
CONF_ON_TRIGGERED,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_TRIGGERED
),
),
automation.CallbackAutomation(
CONF_ON_ARMING,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(AlarmControlPanelState.ACP_STATE_ARMING),
),
automation.CallbackAutomation(
CONF_ON_PENDING,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_PENDING
),
),
automation.CallbackAutomation(
CONF_ON_ARMED_HOME,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_ARMED_HOME
),
),
automation.CallbackAutomation(
CONF_ON_ARMED_NIGHT,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_ARMED_NIGHT
),
),
automation.CallbackAutomation(
CONF_ON_ARMED_AWAY,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_ARMED_AWAY
),
),
automation.CallbackAutomation(
CONF_ON_DISARMED,
"add_on_state_callback",
forwarder=StateEnterForwarder.template(
AlarmControlPanelState.ACP_STATE_DISARMED
),
),
automation.CallbackAutomation(CONF_ON_CLEARED, "add_on_cleared_callback"),
automation.CallbackAutomation(CONF_ON_CHIME, "add_on_chime_callback"),
automation.CallbackAutomation(CONF_ON_READY, "add_on_ready_callback"),
)
@setup_entity("alarm_control_panel")
async def setup_alarm_control_panel_core_(var, config):
for conf in config.get(CONF_ON_STATE, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=StateAnyForwarder
)
_STATE_ENTER_MAP = {
CONF_ON_TRIGGERED: AlarmControlPanelState.ACP_STATE_TRIGGERED,
CONF_ON_ARMING: AlarmControlPanelState.ACP_STATE_ARMING,
CONF_ON_PENDING: AlarmControlPanelState.ACP_STATE_PENDING,
CONF_ON_ARMED_HOME: AlarmControlPanelState.ACP_STATE_ARMED_HOME,
CONF_ON_ARMED_NIGHT: AlarmControlPanelState.ACP_STATE_ARMED_NIGHT,
CONF_ON_ARMED_AWAY: AlarmControlPanelState.ACP_STATE_ARMED_AWAY,
CONF_ON_DISARMED: AlarmControlPanelState.ACP_STATE_DISARMED,
}
for conf_key, state_enum in _STATE_ENTER_MAP.items():
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var,
"add_on_state_callback",
[],
conf,
forwarder=StateEnterForwarder.template(state_enum),
)
for conf in config.get(CONF_ON_CLEARED, []):
await automation.build_callback_automation(
var, "add_on_cleared_callback", [], conf
)
for conf in config.get(CONF_ON_CHIME, []):
await automation.build_callback_automation(
var, "add_on_chime_callback", [], conf
)
for conf in config.get(CONF_ON_READY, []):
await automation.build_callback_automation(
var, "add_on_ready_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
if web_server_config := config.get(CONF_WEB_SERVER):
await web_server.add_entity_config(var, web_server_config)
if mqtt_id := config.get(CONF_MQTT_ID):
+13
View File
@@ -9,6 +9,10 @@ from esphome.const import (
CONF_POWER,
CONF_SPEED,
CONF_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
UNIT_AMPERE,
UNIT_CUBIC_METER_PER_HOUR,
UNIT_METER,
@@ -27,26 +31,35 @@ CONFIG_SCHEMA = (
cv.Optional(CONF_FLOW): sensor.sensor_schema(
unit_of_measurement=UNIT_CUBIC_METER_PER_HOUR,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HEAD): sensor.sensor_schema(
unit_of_measurement=UNIT_METER,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_SPEED): sensor.sensor_schema(
unit_of_measurement=UNIT_REVOLUTIONS_PER_MINUTE,
accuracy_decimals=2,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=2,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
@@ -8,6 +8,7 @@ from esphome.const import (
DEVICE_CLASS_BATTERY,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_BRIGHTNESS_5,
STATE_CLASS_MEASUREMENT,
UNIT_PERCENT,
)
@@ -26,11 +27,13 @@ CONFIG_SCHEMA = (
device_class=DEVICE_CLASS_BATTERY,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ILLUMINANCE): sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
icon=ICON_BRIGHTNESS_5,
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
@@ -19,8 +19,8 @@ class AnalogThresholdBinarySensor : public Component, public binary_sensor::Bina
protected:
sensor::Sensor *sensor_{nullptr};
TemplatableValue<float> upper_threshold_{};
TemplatableValue<float> lower_threshold_{};
TemplatableFn<float> upper_threshold_{};
TemplatableFn<float> lower_threshold_{};
bool raw_state_{false}; // Pre-filter state for hysteresis logic
};
+127 -106
View File
@@ -129,11 +129,12 @@ message HelloResponse {
// A string identifying the server (ESP); like client info this may be empty
// and only exists for debugging/logging purposes.
// For example "ESPHome v1.10.0 on ESP8266"
string server_info = 3;
// Currently set to ESPHOME_VERSION string literal.
string server_info = 3 [(max_data_length) = 32, (force) = true];
// The name of the server (App.get_name())
string name = 4;
// The name of the server (App.get_name() - device hostname)
// max_data_length matches ESPHOME_DEVICE_NAME_MAX_LEN (validated by validate_hostname)
string name = 4 [(max_data_length) = 31, (force) = true];
}
// DEPRECATED in ESPHome 2026.1.0 - Password authentication is no longer supported.
@@ -196,12 +197,14 @@ message DeviceInfoRequest {
message AreaInfo {
uint32 area_id = 1;
string name = 2;
// max_data_length matches core/config.FRIENDLY_NAME_MAX_LEN via AREA_SCHEMA
string name = 2 [(max_data_length) = 120, (force) = true];
}
message DeviceInfo {
uint32 device_id = 1;
string name = 2;
// max_data_length matches core/config.FRIENDLY_NAME_MAX_LEN via DEVICE_SCHEMA
string name = 2 [(max_data_length) = 120, (force) = true];
uint32 area_id = 3;
}
@@ -216,6 +219,16 @@ message SerialProxyInfo {
SerialProxyPortType port_type = 2; // Port type (RS232, RS485)
}
// DeviceInfoResponse max_data_length values:
// name = 31 (ESPHOME_DEVICE_NAME_MAX_LEN, validated by validate_hostname)
// friendly_name = 120 (core/config.FRIENDLY_NAME_MAX_LEN)
// mac_address/bluetooth_mac_address = 17 (MAC_ADDRESS_PRETTY_BUFFER_SIZE - 1, constexpr)
// esphome_version = 32 (ESPHOME_VERSION string literal)
// compilation_time = 25 (Application::BUILD_TIME_STR_SIZE - 1, constexpr)
// manufacturer = 20 (longest hardcoded literal: "Nordic Semiconductor")
// model = 127 (core/config.BOARD_MAX_LENGTH, validated in platform schemas)
// project_name/project_version = 127 (core/config.PROJECT_MAX_LENGTH)
// suggested_area = 120 (core/config.FRIENDLY_NAME_MAX_LEN via AREA_SCHEMA)
message DeviceInfoResponse {
option (id) = 10;
option (source) = SOURCE_SERVER;
@@ -224,28 +237,30 @@ message DeviceInfoResponse {
// with older ESPHome versions that still send this field.
bool uses_password = 1 [deprecated = true];
// The name of the node, given by "App.set_name()"
string name = 2;
// The name of the node, given by "App.set_name()" - device hostname
string name = 2 [(max_data_length) = 31, (force) = true];
// The mac address of the device. For example "AC:BC:32:89:0E:A9"
string mac_address = 3;
string mac_address = 3 [(max_data_length) = 17, (force) = true];
// A string describing the ESPHome version. For example "1.10.0"
string esphome_version = 4;
string esphome_version = 4 [(max_data_length) = 32, (force) = true];
// A string describing the date of compilation, this is generated by the compiler
// and therefore may not be in the same format all the time.
// If the user isn't using ESPHome, this will also not be set.
string compilation_time = 5;
string compilation_time = 5 [(max_data_length) = 25, (force) = true];
// The model of the board. For example NodeMCU
string model = 6;
// max_data_length matches core/config.BOARD_MAX_LENGTH (validated in platform schemas)
string model = 6 [(max_data_length) = 127, (force) = true];
bool has_deep_sleep = 7 [(field_ifdef) = "USE_DEEP_SLEEP"];
// The esphome project details if set
string project_name = 8 [(field_ifdef) = "ESPHOME_PROJECT_NAME"];
string project_version = 9 [(field_ifdef) = "ESPHOME_PROJECT_NAME"];
// max_data_length matches core/config.PROJECT_MAX_LENGTH
string project_name = 8 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME"];
string project_version = 9 [(max_data_length) = 127, (force) = true, (field_ifdef) = "ESPHOME_PROJECT_NAME"];
uint32 webserver_port = 10 [(field_ifdef) = "USE_WEBSERVER"];
@@ -253,18 +268,18 @@ message DeviceInfoResponse {
uint32 legacy_bluetooth_proxy_version = 11 [deprecated=true, (field_ifdef) = "USE_BLUETOOTH_PROXY"];
uint32 bluetooth_proxy_feature_flags = 15 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
string manufacturer = 12;
string manufacturer = 12 [(max_data_length) = 20, (force) = true];
string friendly_name = 13;
string friendly_name = 13 [(max_data_length) = 120, (force) = true];
// Deprecated in API version 1.10
uint32 legacy_voice_assistant_version = 14 [deprecated=true, (field_ifdef) = "USE_VOICE_ASSISTANT"];
uint32 voice_assistant_feature_flags = 17 [(field_ifdef) = "USE_VOICE_ASSISTANT"];
string suggested_area = 16 [(field_ifdef) = "USE_AREAS"];
string suggested_area = 16 [(max_data_length) = 120, (force) = true, (field_ifdef) = "USE_AREAS"];
// The Bluetooth mac address of the device. For example "AC:BC:32:89:0E:AA"
string bluetooth_mac_address = 18 [(field_ifdef) = "USE_BLUETOOTH_PROXY"];
string bluetooth_mac_address = 18 [(max_data_length) = 17, (force) = true, (field_ifdef) = "USE_BLUETOOTH_PROXY"];
// Supports receiving and saving api encryption key
bool api_encryption_supported = 19 [(field_ifdef) = "USE_API_NOISE"];
@@ -308,6 +323,12 @@ enum EntityCategory {
ENTITY_CATEGORY_DIAGNOSTIC = 2;
}
// Entity field max_data_length values match Python validation constants:
// name/object_id = 120 (config_validation.NAME_MAX_LENGTH)
// icon = 63 (core/config.ICON_MAX_LENGTH)
// device_class = 47 (core/config.DEVICE_CLASS_MAX_LENGTH)
// unit_of_measurement = 63 (core/config.UNIT_OF_MEASUREMENT_MAX_LENGTH)
// ==================== BINARY SENSOR ====================
message ListEntitiesBinarySensorResponse {
option (id) = 12;
@@ -315,15 +336,15 @@ message ListEntitiesBinarySensorResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BINARY_SENSOR";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string device_class = 5;
string device_class = 5 [(max_data_length) = 47];
bool is_status_binary_sensor = 6;
bool disabled_by_default = 7;
string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 8 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 9;
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
@@ -349,17 +370,17 @@ message ListEntitiesCoverResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_COVER";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
bool assumed_state = 5;
bool supports_position = 6;
bool supports_tilt = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 11;
bool supports_stop = 12;
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
@@ -433,9 +454,9 @@ message ListEntitiesFanResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_FAN";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
bool supports_oscillation = 5;
@@ -443,7 +464,7 @@ message ListEntitiesFanResponse {
bool supports_direction = 7;
int32 supported_speed_count = 8;
bool disabled_by_default = 9;
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 11;
repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"];
uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
@@ -521,9 +542,9 @@ message ListEntitiesLightResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LIGHT";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
@@ -540,7 +561,7 @@ message ListEntitiesLightResponse {
float max_mireds = 10;
repeated string effects = 11 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool disabled_by_default = 13;
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 14 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 15;
uint32 device_id = 16 [(field_ifdef) = "USE_DEVICES"];
}
@@ -626,16 +647,16 @@ message ListEntitiesSensorResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SENSOR";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string unit_of_measurement = 6;
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
string unit_of_measurement = 6 [(max_data_length) = 63];
int32 accuracy_decimals = 7;
bool force_update = 8;
string device_class = 9;
string device_class = 9 [(max_data_length) = 47];
SensorStateClass state_class = 10;
// Last reset type removed in 2021.9.0
// Deprecated in API version 1.5
@@ -666,16 +687,16 @@ message ListEntitiesSwitchResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SWITCH";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool assumed_state = 6;
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
string device_class = 9;
string device_class = 9 [(max_data_length) = 47];
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
}
message SwitchStateResponse {
@@ -708,15 +729,15 @@ message ListEntitiesTextSensorResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT_SENSOR";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
message TextSensorStateResponse {
@@ -971,12 +992,12 @@ message ListEntitiesCameraResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CAMERA";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
bool disabled_by_default = 5;
string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 6 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 7;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
}
@@ -1056,9 +1077,9 @@ message ListEntitiesClimateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CLIMATE";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
bool supports_current_temperature = 5; // Deprecated: use feature_flags
@@ -1078,7 +1099,7 @@ message ListEntitiesClimateResponse {
repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"];
bool disabled_by_default = 18;
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
EntityCategory entity_category = 20;
float visual_current_temperature_step = 21;
bool supports_current_humidity = 22; // Deprecated: use feature_flags
@@ -1167,10 +1188,10 @@ message ListEntitiesWaterHeaterResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_WATER_HEATER";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
string name = 3 [(max_data_length) = 120, (force) = true];
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
@@ -1243,20 +1264,20 @@ message ListEntitiesNumberResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_NUMBER";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
float min_value = 6;
float max_value = 7;
float step = 8;
bool disabled_by_default = 9;
EntityCategory entity_category = 10;
string unit_of_measurement = 11;
string unit_of_measurement = 11 [(max_data_length) = 63];
NumberMode mode = 12;
string device_class = 13;
string device_class = 13 [(max_data_length) = 47];
uint32 device_id = 14 [(field_ifdef) = "USE_DEVICES"];
}
message NumberStateResponse {
@@ -1292,12 +1313,12 @@ message ListEntitiesSelectResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SELECT";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool disabled_by_default = 7;
EntityCategory entity_category = 8;
@@ -1336,12 +1357,12 @@ message ListEntitiesSirenResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
repeated string tones = 7 [(container_pointer_no_template) = "FixedVector<const char *>"];
bool supports_duration = 8;
@@ -1399,12 +1420,12 @@ message ListEntitiesLockResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
bool assumed_state = 8;
@@ -1448,15 +1469,15 @@ message ListEntitiesButtonResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BUTTON";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
message ButtonCommandRequest {
@@ -1515,12 +1536,12 @@ message ListEntitiesMediaPlayerResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
@@ -1606,7 +1627,7 @@ message BluetoothLEAdvertisementResponse {
message BluetoothLERawAdvertisement {
uint64 address = 1 [(force) = true];
sint32 rssi = 2 [(force) = true];
uint32 address_type = 3;
uint32 address_type = 3 [(max_value) = 4];
bytes data = 4 [(fixed_array_size) = 62, (force) = true];
}
@@ -2103,11 +2124,11 @@ message ListEntitiesAlarmControlPanelResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 supported_features = 8;
@@ -2150,11 +2171,11 @@ message ListEntitiesTextResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
@@ -2198,12 +2219,12 @@ message ListEntitiesDateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATE";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
@@ -2245,12 +2266,12 @@ message ListEntitiesTimeResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
@@ -2292,15 +2313,15 @@ message ListEntitiesEventResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
repeated string event_types = 9 [(container_pointer_no_template) = "FixedVector<const char *>"];
uint32 device_id = 10 [(field_ifdef) = "USE_DEVICES"];
@@ -2323,15 +2344,15 @@ message ListEntitiesValveResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VALVE";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
bool assumed_state = 9;
bool supports_position = 10;
@@ -2378,12 +2399,12 @@ message ListEntitiesDateTimeResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
uint32 device_id = 8 [(field_ifdef) = "USE_DEVICES"];
@@ -2421,15 +2442,15 @@ message ListEntitiesUpdateResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string name = 3 [(max_data_length) = 120, (force) = true];
reserved 4; // Deprecated: was string unique_id
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 6;
EntityCategory entity_category = 7;
string device_class = 8;
string device_class = 8 [(max_data_length) = 47];
uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
}
message UpdateStateResponse {
@@ -2504,10 +2525,10 @@ message ListEntitiesInfraredResponse {
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_INFRARED";
string object_id = 1;
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3;
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"];
string name = 3 [(max_data_length) = 120, (force) = true];
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
+11 -2
View File
@@ -72,6 +72,14 @@ static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000;
static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
// Cross-validate C++ constants against proto max_data_length annotations in api.proto
static_assert(MAC_ADDRESS_PRETTY_BUFFER_SIZE - 1 == 17,
"Update max_data_length for mac_address/bluetooth_mac_address in api.proto");
static_assert(Application::BUILD_TIME_STR_SIZE - 1 == 25, "Update max_data_length for compilation_time in api.proto");
static_assert(sizeof(ESPHOME_VERSION) - 1 <= 32, "Update max_data_length for esphome_version in api.proto");
static_assert(ESPHOME_DEVICE_NAME_MAX_LEN <= 31, "Update max_data_length for name in api.proto");
static_assert(ESPHOME_FRIENDLY_NAME_MAX_LEN <= 120, "Update max_data_length for friendly_name in api.proto");
static const char *const TAG = "api.connection";
#ifdef USE_CAMERA
static const int CAMERA_STOP_STREAM = 5000;
@@ -1716,6 +1724,7 @@ bool APIConnection::send_device_info_response_() {
static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
resp.manufacturer = MANUFACTURER;
#endif
static_assert(sizeof(ESPHOME_MANUFACTURER) - 1 <= 20, "Update max_data_length for manufacturer in api.proto");
#undef ESPHOME_MANUFACTURER
#ifdef USE_ESP8266
@@ -1993,7 +2002,7 @@ bool APIConnection::send_message_(uint32_t payload_size, uint8_t message_type, M
size_t write_start = shared_buf.size();
shared_buf.resize(write_start + payload_size);
ProtoWriteBuffer buffer{&shared_buf, write_start};
encode_fn(msg, buffer);
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
}
// Encodes a message to the buffer and returns the total number of bytes used,
@@ -2034,7 +2043,7 @@ uint16_t APIConnection::encode_to_buffer(uint32_t calculated_size, MessageEncode
shared_buf.resize(shared_buf.size() + to_add);
ProtoWriteBuffer buffer{&shared_buf, shared_buf.size() - calculated_size};
encode_fn(msg, buffer);
encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
// Return total size (header + payload + footer)
return static_cast<uint16_t>(total_calculated_size);
+10 -2
View File
@@ -20,6 +20,9 @@
#ifdef USE_RP2040_CRASH_HANDLER
#include "esphome/components/rp2040/crash_handler.h"
#endif
#ifdef USE_ESP8266_CRASH_HANDLER
#include "esphome/components/esp8266/crash_handler.h"
#endif
#include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
@@ -276,6 +279,9 @@ class APIConnection final : public APIServerConnectionBase {
#endif
#ifdef USE_RP2040_CRASH_HANDLER
rp2040::crash_handler_log();
#endif
#ifdef USE_ESP8266_CRASH_HANDLER
esp8266::crash_handler_log();
#endif
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
@@ -318,7 +324,7 @@ class APIConnection final : public APIServerConnectionBase {
void on_no_setup_connection();
// Function pointer type for type-erased message encoding
using MessageEncodeFn = void (*)(const void *, ProtoWriteBuffer &);
using MessageEncodeFn = uint8_t *(*) (const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM);
// Function pointer type for type-erased size calculation
using CalculateSizeFn = uint32_t (*)(const void *);
@@ -397,7 +403,9 @@ class APIConnection final : public APIServerConnectionBase {
}
// Shared no-op encode thunk for empty messages (ESTIMATED_SIZE == 0)
static void encode_msg_noop(const void *, ProtoWriteBuffer &) {}
static uint8_t *encode_msg_noop(const void *, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
return buf.get_pos();
}
// Non-template buffer management for send_message
bool send_message_(uint32_t payload_size, uint8_t message_type, MessageEncodeFn encode_fn, const void *msg);
+131 -119
View File
@@ -244,132 +244,144 @@ APIError APINoiseFrameHelper::try_read_frame_() {
* If an error occurred, returns that error. Only returns OK if the transport is ready for data
* traffic.
*/
// Split into per-state methods so the compiler doesn't allocate stack space
// for all branches simultaneously. On RP2040 the core0 stack lives in a 4KB
// scratch RAM bank; the Noise crypto path (curve25519) needs ~2KB+ of stack,
// so every byte saved in the caller matters.
APIError APINoiseFrameHelper::state_action_() {
int err;
APIError aerr;
if (state_ == State::INITIALIZE) {
HELPER_LOG("Bad state for method: %d", (int) state_);
return APIError::BAD_STATE;
switch (this->state_) {
case State::INITIALIZE:
HELPER_LOG("Bad state for method: %d", (int) this->state_);
return APIError::BAD_STATE;
case State::CLIENT_HELLO:
return this->state_action_client_hello_();
case State::SERVER_HELLO:
return this->state_action_server_hello_();
case State::HANDSHAKE:
return this->state_action_handshake_();
case State::CLOSED:
case State::FAILED:
return APIError::BAD_STATE;
default:
return APIError::OK;
}
if (state_ == State::CLIENT_HELLO) {
// waiting for client hello
aerr = this->try_read_frame_();
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size();
size_t rx_size = this->rx_buf_.size();
this->prologue_.resize(old_size + 2 + rx_size);
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
this->prologue_[old_size + 1] = (uint8_t) rx_size;
if (rx_size > 0) {
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
}
state_ = State::SERVER_HELLO;
}
APIError APINoiseFrameHelper::state_action_client_hello_() {
// waiting for client hello
APIError aerr = this->try_read_frame_();
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
if (state_ == State::SERVER_HELLO) {
// send server hello
const auto &name = App.get_name();
char mac[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac);
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t name_offset = 1;
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE;
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
uint8_t msg[max_msg_size];
// chosen proto
msg[0] = 0x01;
// node name, terminated by null byte
std::memcpy(msg + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
aerr = write_frame_(msg, total_size);
if (aerr != APIError::OK)
return aerr;
// start handshake
aerr = init_handshake_();
if (aerr != APIError::OK)
return aerr;
state_ = State::HANDSHAKE;
// ignore contents, may be used in future for flags
// Resize for: existing prologue + 2 size bytes + frame data
size_t old_size = this->prologue_.size();
size_t rx_size = this->rx_buf_.size();
this->prologue_.resize(old_size + 2 + rx_size);
this->prologue_[old_size] = (uint8_t) (rx_size >> 8);
this->prologue_[old_size + 1] = (uint8_t) rx_size;
if (rx_size > 0) {
std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), rx_size);
}
if (state_ == State::HANDSHAKE) {
int action = noise_handshakestate_get_action(handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
// waiting for handshake msg
aerr = this->try_read_frame_();
if (aerr != APIError::OK) {
return handle_handshake_frame_error_(aerr);
}
if (this->rx_buf_.empty()) {
send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (this->rx_buf_[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
if (err != 0) {
// Special handling for MAC failure
send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
: LOG_STR("Handshake error"));
return handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
APIError::HANDSHAKESTATE_READ_FAILED);
}
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
uint8_t buffer[65];
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
err = noise_handshakestate_write_message(handshake_, &mbuf, nullptr);
APIError aerr_write = handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
APIError::HANDSHAKESTATE_WRITE_FAILED);
if (aerr_write != APIError::OK)
return aerr_write;
buffer[0] = 0x00; // success
aerr = write_frame_(buffer, mbuf.size + 1);
if (aerr != APIError::OK)
return aerr;
aerr = check_handshake_finished_();
if (aerr != APIError::OK)
return aerr;
} else {
// bad state for action
state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
}
if (state_ == State::CLOSED || state_ == State::FAILED) {
return APIError::BAD_STATE;
}
state_ = State::SERVER_HELLO;
return APIError::OK;
}
APIError APINoiseFrameHelper::state_action_server_hello_() {
// send server hello
const auto &name = App.get_name();
char mac[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac);
// Calculate positions and sizes
size_t name_len = name.size() + 1; // including null terminator
size_t name_offset = 1;
size_t mac_offset = name_offset + name_len;
size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE;
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
uint8_t msg[max_msg_size];
// chosen proto
msg[0] = 0x01;
// node name, terminated by null byte
std::memcpy(msg + name_offset, name.c_str(), name_len);
// node mac, terminated by null byte
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
APIError aerr = write_frame_(msg, total_size);
if (aerr != APIError::OK)
return aerr;
// start handshake
aerr = init_handshake_();
if (aerr != APIError::OK)
return aerr;
state_ = State::HANDSHAKE;
return APIError::OK;
}
APIError APINoiseFrameHelper::state_action_handshake_() {
int action = noise_handshakestate_get_action(this->handshake_);
if (action == NOISE_ACTION_READ_MESSAGE) {
return this->state_action_handshake_read_();
} else if (action == NOISE_ACTION_WRITE_MESSAGE) {
return this->state_action_handshake_write_();
}
// bad state for action
this->state_ = State::FAILED;
HELPER_LOG("Bad action for handshake: %d", action);
return APIError::HANDSHAKESTATE_BAD_STATE;
}
APIError APINoiseFrameHelper::state_action_handshake_read_() {
APIError aerr = this->try_read_frame_();
if (aerr != APIError::OK) {
return this->handle_handshake_frame_error_(aerr);
}
if (this->rx_buf_.empty()) {
this->send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
} else if (this->rx_buf_[0] != 0x00) {
HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
this->send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
return APIError::BAD_HANDSHAKE_ERROR_BYTE;
}
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
int err = noise_handshakestate_read_message(this->handshake_, &mbuf, nullptr);
if (err != 0) {
// Special handling for MAC failure
this->send_explicit_handshake_reject_(err == NOISE_ERROR_MAC_FAILURE ? LOG_STR("Handshake MAC failure")
: LOG_STR("Handshake error"));
return this->handle_noise_error_(err, LOG_STR("noise_handshakestate_read_message"),
APIError::HANDSHAKESTATE_READ_FAILED);
}
return this->check_handshake_finished_();
}
APIError APINoiseFrameHelper::state_action_handshake_write_() {
uint8_t buffer[65];
NoiseBuffer mbuf;
noise_buffer_init(mbuf);
noise_buffer_set_output(mbuf, buffer + 1, sizeof(buffer) - 1);
int err = noise_handshakestate_write_message(this->handshake_, &mbuf, nullptr);
APIError aerr = this->handle_noise_error_(err, LOG_STR("noise_handshakestate_write_message"),
APIError::HANDSHAKESTATE_WRITE_FAILED);
if (aerr != APIError::OK)
return aerr;
buffer[0] = 0x00; // success
aerr = this->write_frame_(buffer, mbuf.size + 1);
if (aerr != APIError::OK)
return aerr;
return this->check_handshake_finished_();
}
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
uint8_t data[32];
@@ -26,6 +26,11 @@ class APINoiseFrameHelper final : public APIFrameHelper {
protected:
APIError state_action_();
APIError state_action_client_hello_();
APIError state_action_server_hello_();
APIError state_action_handshake_();
APIError state_action_handshake_read_();
APIError state_action_handshake_write_();
APIError try_read_frame_();
APIError write_frame_(const uint8_t *data, uint16_t len);
APIError init_handshake_();
+12
View File
@@ -96,4 +96,16 @@ extend google.protobuf.FieldOptions {
// variant of the calc_ method. Use on fields that are almost always non-default
// to eliminate dead branches on hot paths.
optional bool force = 50016 [default=false];
// max_value: Maximum value a field can have.
// When max_value < 128, the code generator emits constant-size calculations
// and direct byte writes instead of varint branching, since the encoded varint
// is guaranteed to be 1 byte.
optional uint32 max_value = 50017;
// max_data_length: Maximum length of a string or bytes field.
// When max_data_length < 128, the code generator emits constant-size
// length varint calculations and direct byte writes, since the length
// varint is guaranteed to be 1 byte.
optional uint32 max_data_length = 50018;
}
File diff suppressed because it is too large Load Diff
+93 -93
View File
@@ -412,7 +412,7 @@ class HelloResponse final : public ProtoMessage {
uint32_t api_version_minor{0};
StringRef server_info{};
StringRef name{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -477,7 +477,7 @@ class AreaInfo final : public ProtoMessage {
public:
uint32_t area_id{0};
StringRef name{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -492,7 +492,7 @@ class DeviceInfo final : public ProtoMessage {
uint32_t device_id{0};
StringRef name{};
uint32_t area_id{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -506,7 +506,7 @@ class SerialProxyInfo final : public ProtoMessage {
public:
StringRef name{};
enums::SerialProxyPortType port_type{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -574,7 +574,7 @@ class DeviceInfoResponse final : public ProtoMessage {
#ifdef USE_SERIAL_PROXY
std::array<SerialProxyInfo, SERIAL_PROXY_COUNT> serial_proxies{};
#endif
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -605,7 +605,7 @@ class ListEntitiesBinarySensorResponse final : public InfoResponseProtoMessage {
#endif
StringRef device_class{};
bool is_status_binary_sensor{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -622,7 +622,7 @@ class BinarySensorStateResponse final : public StateResponseProtoMessage {
#endif
bool state{false};
bool missing_state{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -644,7 +644,7 @@ class ListEntitiesCoverResponse final : public InfoResponseProtoMessage {
bool supports_tilt{false};
StringRef device_class{};
bool supports_stop{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -662,7 +662,7 @@ class CoverStateResponse final : public StateResponseProtoMessage {
float position{0.0f};
float tilt{0.0f};
enums::CoverOperation current_operation{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -704,7 +704,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
bool supports_direction{false};
int32_t supported_speed_count{0};
const std::vector<const char *> *supported_preset_modes{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -724,7 +724,7 @@ class FanStateResponse final : public StateResponseProtoMessage {
enums::FanDirection direction{};
int32_t speed_level{0};
StringRef preset_mode{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -771,7 +771,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
float min_mireds{0.0f};
float max_mireds{0.0f};
const FixedVector<const char *> *effects{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -798,7 +798,7 @@ class LightStateResponse final : public StateResponseProtoMessage {
float cold_white{0.0f};
float warm_white{0.0f};
StringRef effect{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -862,7 +862,7 @@ class ListEntitiesSensorResponse final : public InfoResponseProtoMessage {
bool force_update{false};
StringRef device_class{};
enums::SensorStateClass state_class{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -879,7 +879,7 @@ class SensorStateResponse final : public StateResponseProtoMessage {
#endif
float state{0.0f};
bool missing_state{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -898,7 +898,7 @@ class ListEntitiesSwitchResponse final : public InfoResponseProtoMessage {
#endif
bool assumed_state{false};
StringRef device_class{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -914,7 +914,7 @@ class SwitchStateResponse final : public StateResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("switch_state_response"); }
#endif
bool state{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -948,7 +948,7 @@ class ListEntitiesTextSensorResponse final : public InfoResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("list_entities_text_sensor_response"); }
#endif
StringRef device_class{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -965,7 +965,7 @@ class TextSensorStateResponse final : public StateResponseProtoMessage {
#endif
StringRef state{};
bool missing_state{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1004,7 +1004,7 @@ class SubscribeLogsResponse final : public ProtoMessage {
this->message_ptr_ = data;
this->message_len_ = len;
}
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1037,7 +1037,7 @@ class NoiseEncryptionSetKeyResponse final : public ProtoMessage {
const LogString *message_name() const override { return LOG_STR("noise_encryption_set_key_response"); }
#endif
bool success{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1051,7 +1051,7 @@ class HomeassistantServiceMap final : public ProtoMessage {
public:
StringRef key{};
StringRef value{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1080,7 +1080,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
StringRef response_template{};
#endif
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1124,7 +1124,7 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
StringRef entity_id{};
StringRef attribute{};
bool once{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1215,7 +1215,7 @@ class ListEntitiesServicesArgument final : public ProtoMessage {
public:
StringRef name{};
enums::ServiceArgType type{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1234,7 +1234,7 @@ class ListEntitiesServicesResponse final : public ProtoMessage {
uint32_t key{0};
FixedVector<ListEntitiesServicesArgument> args{};
enums::SupportsResponseType supports_response{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1304,7 +1304,7 @@ class ExecuteServiceResponse final : public ProtoMessage {
const uint8_t *response_data{nullptr};
uint16_t response_data_len{0};
#endif
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1321,7 +1321,7 @@ class ListEntitiesCameraResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_camera_response"); }
#endif
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1343,7 +1343,7 @@ class CameraImageResponse final : public StateResponseProtoMessage {
this->data_len_ = len;
}
bool done{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1394,7 +1394,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
uint32_t feature_flags{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1422,7 +1422,7 @@ class ClimateStateResponse final : public StateResponseProtoMessage {
StringRef custom_preset{};
float current_humidity{0.0f};
float target_humidity{0.0f};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1480,7 +1480,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
float target_temperature_step{0.0f};
const water_heater::WaterHeaterModeMask *supported_modes{};
uint32_t supported_features{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1501,7 +1501,7 @@ class WaterHeaterStateResponse final : public StateResponseProtoMessage {
uint32_t state{0};
float target_temperature_low{0.0f};
float target_temperature_high{0.0f};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1545,7 +1545,7 @@ class ListEntitiesNumberResponse final : public InfoResponseProtoMessage {
StringRef unit_of_measurement{};
enums::NumberMode mode{};
StringRef device_class{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1562,7 +1562,7 @@ class NumberStateResponse final : public StateResponseProtoMessage {
#endif
float state{0.0f};
bool missing_state{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1596,7 +1596,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("list_entities_select_response"); }
#endif
const FixedVector<const char *> *options{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1613,7 +1613,7 @@ class SelectStateResponse final : public StateResponseProtoMessage {
#endif
StringRef state{};
bool missing_state{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1650,7 +1650,7 @@ class ListEntitiesSirenResponse final : public InfoResponseProtoMessage {
const FixedVector<const char *> *tones{};
bool supports_duration{false};
bool supports_volume{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1666,7 +1666,7 @@ class SirenStateResponse final : public StateResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("siren_state_response"); }
#endif
bool state{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1711,7 +1711,7 @@ class ListEntitiesLockResponse final : public InfoResponseProtoMessage {
bool supports_open{false};
bool requires_code{false};
StringRef code_format{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1727,7 +1727,7 @@ class LockStateResponse final : public StateResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("lock_state_response"); }
#endif
enums::LockState state{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1764,7 +1764,7 @@ class ListEntitiesButtonResponse final : public InfoResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("list_entities_button_response"); }
#endif
StringRef device_class{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1796,7 +1796,7 @@ class MediaPlayerSupportedFormat final : public ProtoMessage {
uint32_t num_channels{0};
enums::MediaPlayerFormatPurpose purpose{};
uint32_t sample_bytes{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1814,7 +1814,7 @@ class ListEntitiesMediaPlayerResponse final : public InfoResponseProtoMessage {
bool supports_pause{false};
std::vector<MediaPlayerSupportedFormat> supported_formats{};
uint32_t feature_flags{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1832,7 +1832,7 @@ class MediaPlayerStateResponse final : public StateResponseProtoMessage {
enums::MediaPlayerState state{};
float volume{0.0f};
bool muted{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1888,7 +1888,7 @@ class BluetoothLERawAdvertisement final : public ProtoMessage {
uint32_t address_type{0};
uint8_t data[62]{};
uint8_t data_len{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1905,7 +1905,7 @@ class BluetoothLERawAdvertisementsResponse final : public ProtoMessage {
#endif
std::array<BluetoothLERawAdvertisement, BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE> advertisements{};
uint16_t advertisements_len{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1942,7 +1942,7 @@ class BluetoothDeviceConnectionResponse final : public ProtoMessage {
bool connected{false};
uint32_t mtu{0};
int32_t error{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1970,7 +1970,7 @@ class BluetoothGATTDescriptor final : public ProtoMessage {
std::array<uint64_t, 2> uuid{};
uint32_t handle{0};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1985,7 +1985,7 @@ class BluetoothGATTCharacteristic final : public ProtoMessage {
uint32_t properties{0};
FixedVector<BluetoothGATTDescriptor> descriptors{};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -1999,7 +1999,7 @@ class BluetoothGATTService final : public ProtoMessage {
uint32_t handle{0};
FixedVector<BluetoothGATTCharacteristic> characteristics{};
uint32_t short_uuid{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2016,7 +2016,7 @@ class BluetoothGATTGetServicesResponse final : public ProtoMessage {
#endif
uint64_t address{0};
std::vector<BluetoothGATTService> services{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2032,7 +2032,7 @@ class BluetoothGATTGetServicesDoneResponse final : public ProtoMessage {
const LogString *message_name() const override { return LOG_STR("bluetooth_gatt_get_services_done_response"); }
#endif
uint64_t address{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2071,7 +2071,7 @@ class BluetoothGATTReadResponse final : public ProtoMessage {
this->data_ptr_ = data;
this->data_len_ = len;
}
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2166,7 +2166,7 @@ class BluetoothGATTNotifyDataResponse final : public ProtoMessage {
this->data_ptr_ = data;
this->data_len_ = len;
}
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2184,7 +2184,7 @@ class BluetoothConnectionsFreeResponse final : public ProtoMessage {
uint32_t free{0};
uint32_t limit{0};
std::array<uint64_t, BLUETOOTH_PROXY_MAX_CONNECTIONS> allocated{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2202,7 +2202,7 @@ class BluetoothGATTErrorResponse final : public ProtoMessage {
uint64_t address{0};
uint32_t handle{0};
int32_t error{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2219,7 +2219,7 @@ class BluetoothGATTWriteResponse final : public ProtoMessage {
#endif
uint64_t address{0};
uint32_t handle{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2236,7 +2236,7 @@ class BluetoothGATTNotifyResponse final : public ProtoMessage {
#endif
uint64_t address{0};
uint32_t handle{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2254,7 +2254,7 @@ class BluetoothDevicePairingResponse final : public ProtoMessage {
uint64_t address{0};
bool paired{false};
int32_t error{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2272,7 +2272,7 @@ class BluetoothDeviceUnpairingResponse final : public ProtoMessage {
uint64_t address{0};
bool success{false};
int32_t error{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2290,7 +2290,7 @@ class BluetoothDeviceClearCacheResponse final : public ProtoMessage {
uint64_t address{0};
bool success{false};
int32_t error{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2308,7 +2308,7 @@ class BluetoothScannerStateResponse final : public ProtoMessage {
enums::BluetoothScannerState state{};
enums::BluetoothScannerMode mode{};
enums::BluetoothScannerMode configured_mode{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2354,7 +2354,7 @@ class VoiceAssistantAudioSettings final : public ProtoMessage {
uint32_t noise_suppression_level{0};
uint32_t auto_gain{0};
float volume_multiplier{0.0f};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2374,7 +2374,7 @@ class VoiceAssistantRequest final : public ProtoMessage {
uint32_t flags{0};
VoiceAssistantAudioSettings audio_settings{};
StringRef wake_word_phrase{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2436,7 +2436,7 @@ class VoiceAssistantAudio final : public ProtoDecodableMessage {
const uint8_t *data{nullptr};
uint16_t data_len{0};
bool end{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2494,7 +2494,7 @@ class VoiceAssistantAnnounceFinished final : public ProtoMessage {
const LogString *message_name() const override { return LOG_STR("voice_assistant_announce_finished"); }
#endif
bool success{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2507,7 +2507,7 @@ class VoiceAssistantWakeWord final : public ProtoMessage {
StringRef id{};
StringRef wake_word{};
std::vector<std::string> trained_languages{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2557,7 +2557,7 @@ class VoiceAssistantConfigurationResponse final : public ProtoMessage {
std::vector<VoiceAssistantWakeWord> available_wake_words{};
const std::vector<std::string> *active_wake_words{};
uint32_t max_active_wake_words{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2592,7 +2592,7 @@ class ListEntitiesAlarmControlPanelResponse final : public InfoResponseProtoMess
uint32_t supported_features{0};
bool requires_code{false};
bool requires_code_to_arm{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2608,7 +2608,7 @@ class AlarmControlPanelStateResponse final : public StateResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("alarm_control_panel_state_response"); }
#endif
enums::AlarmControlPanelState state{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2647,7 +2647,7 @@ class ListEntitiesTextResponse final : public InfoResponseProtoMessage {
uint32_t max_length{0};
StringRef pattern{};
enums::TextMode mode{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2664,7 +2664,7 @@ class TextStateResponse final : public StateResponseProtoMessage {
#endif
StringRef state{};
bool missing_state{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2698,7 +2698,7 @@ class ListEntitiesDateResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_date_response"); }
#endif
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2717,7 +2717,7 @@ class DateStateResponse final : public StateResponseProtoMessage {
uint32_t year{0};
uint32_t month{0};
uint32_t day{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2752,7 +2752,7 @@ class ListEntitiesTimeResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_time_response"); }
#endif
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2771,7 +2771,7 @@ class TimeStateResponse final : public StateResponseProtoMessage {
uint32_t hour{0};
uint32_t minute{0};
uint32_t second{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2808,7 +2808,7 @@ class ListEntitiesEventResponse final : public InfoResponseProtoMessage {
#endif
StringRef device_class{};
const FixedVector<const char *> *event_types{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2824,7 +2824,7 @@ class EventResponse final : public StateResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("event_response"); }
#endif
StringRef event_type{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2845,7 +2845,7 @@ class ListEntitiesValveResponse final : public InfoResponseProtoMessage {
bool assumed_state{false};
bool supports_position{false};
bool supports_stop{false};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2862,7 +2862,7 @@ class ValveStateResponse final : public StateResponseProtoMessage {
#endif
float position{0.0f};
enums::ValveOperation current_operation{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2897,7 +2897,7 @@ class ListEntitiesDateTimeResponse final : public InfoResponseProtoMessage {
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_date_time_response"); }
#endif
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2914,7 +2914,7 @@ class DateTimeStateResponse final : public StateResponseProtoMessage {
#endif
bool missing_state{false};
uint32_t epoch_seconds{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2948,7 +2948,7 @@ class ListEntitiesUpdateResponse final : public InfoResponseProtoMessage {
const LogString *message_name() const override { return LOG_STR("list_entities_update_response"); }
#endif
StringRef device_class{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -2972,7 +2972,7 @@ class UpdateStateResponse final : public StateResponseProtoMessage {
StringRef title{};
StringRef release_summary{};
StringRef release_url{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -3007,7 +3007,7 @@ class ZWaveProxyFrame final : public ProtoDecodableMessage {
#endif
const uint8_t *data{nullptr};
uint16_t data_len{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -3026,7 +3026,7 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage {
enums::ZWaveProxyRequestType type{};
const uint8_t *data{nullptr};
uint16_t data_len{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -3047,7 +3047,7 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage {
#endif
uint32_t capabilities{0};
uint32_t receiver_frequency{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -3094,7 +3094,7 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
#endif
uint32_t key{0};
const std::vector<int32_t> *timings{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -3138,7 +3138,7 @@ class SerialProxyDataReceived final : public ProtoMessage {
this->data_ptr_ = data;
this->data_len_ = len;
}
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -3204,7 +3204,7 @@ class SerialProxyGetModemPinsResponse final : public ProtoMessage {
#endif
uint32_t instance{0};
uint32_t line_states{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -3239,7 +3239,7 @@ class SerialProxyRequestResponse final : public ProtoMessage {
enums::SerialProxyRequestType type{};
enums::SerialProxyStatus status{};
StringRef error_message{};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
@@ -3277,7 +3277,7 @@ class BluetoothSetConnectionParamsResponse final : public ProtoMessage {
#endif
uint64_t address{0};
int32_t error{0};
void encode(ProtoWriteBuffer &buffer) const;
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
+12 -5
View File
@@ -145,14 +145,15 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size
// [tag][v1][v2][body ..... body]
// ^-- pos_ = element end, within buffer
void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
uint8_t *(*encode_fn)(const void *,
ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)) {
this->encode_field_raw(field_id, 2);
// Reserve 1 byte for length varint (optimistic: submessage < 128 bytes)
uint8_t *len_pos = this->pos_;
this->debug_check_bounds_(1);
this->pos_++;
uint8_t *body_start = this->pos_;
encode_fn(value, *this);
this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_));
uint32_t body_size = static_cast<uint32_t>(this->pos_ - body_start);
if (body_size < 128) [[likely]] {
// Common case: 1-byte varint, just backpatch
@@ -173,22 +174,27 @@ void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value,
// Non-template core for encode_optional_sub_message.
void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &)) {
uint8_t *(*encode_fn)(const void *,
ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM)) {
if (nested_size == 0)
return;
this->encode_field_raw(field_id, 2);
this->encode_varint_raw(nested_size);
#ifdef ESPHOME_DEBUG_API
uint8_t *start = this->pos_;
encode_fn(value, *this);
this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_));
if (static_cast<uint32_t>(this->pos_ - start) != nested_size)
this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start);
#else
encode_fn(value, *this);
this->pos_ = encode_fn(value, *this PROTO_ENCODE_DEBUG_INIT(this->buffer_));
#endif
}
#ifdef ESPHOME_DEBUG_API
void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller) {
ESP_LOGE(TAG, "Proto encode bounds check failed in %s: need %zu bytes, %td available", caller, bytes, end - pos);
abort();
}
void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) {
if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) {
ESP_LOGE(TAG, "ProtoWriteBuffer bounds check failed in %s: bytes=%zu offset=%td buf_size=%zu", caller, bytes,
@@ -201,6 +207,7 @@ void ProtoWriteBuffer::debug_check_encode_size_(uint32_t field_id, uint32_t expe
expected, actual);
abort();
}
#endif
void ProtoDecodableMessage::decode(const uint8_t *buffer, size_t length) {
+276 -157
View File
@@ -25,6 +25,19 @@ constexpr uint8_t WIRE_TYPE_LENGTH_DELIMITED = 2; // string, bytes, embedded me
constexpr uint8_t WIRE_TYPE_FIXED32 = 5; // fixed32, sfixed32, float
constexpr uint8_t WIRE_TYPE_MASK = 0b111; // Mask to extract wire type from tag
// Reinterpret float bits as uint32_t without floating-point comparison.
// Used by both encode_float() and calc_float() to ensure identical zero checks.
// Uses union type-punning which is a GCC/Clang extension (not standard C++),
// but bit_cast/memcpy don't optimize to a no-op on xtensa-gcc (ESP8266).
inline uint32_t float_to_raw(float value) {
union {
float f;
uint32_t u;
} v;
v.f = value;
return v.u;
}
// Helper functions for ZigZag encoding/decoding
inline constexpr uint32_t encode_zigzag32(int32_t value) {
return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
@@ -195,6 +208,26 @@ class Proto32Bit {
// NOTE: Proto64Bit class removed - wire type 1 (64-bit fixed) not supported
// Debug bounds checking for proto encode functions.
// In debug mode (ESPHOME_DEBUG_API), an extra end-of-buffer pointer is threaded
// through the entire encode chain. In production, these expand to nothing.
#ifdef ESPHOME_DEBUG_API
#define PROTO_ENCODE_DEBUG_PARAM , uint8_t *proto_debug_end_
#define PROTO_ENCODE_DEBUG_ARG , proto_debug_end_
#define PROTO_ENCODE_DEBUG_INIT(buf) , (buf)->data() + (buf)->size()
#define PROTO_ENCODE_CHECK_BOUNDS(pos, n) \
do { \
if ((pos) + (n) > proto_debug_end_) \
proto_check_bounds_failed(pos, n, proto_debug_end_, __builtin_FUNCTION()); \
} while (0)
void proto_check_bounds_failed(const uint8_t *pos, size_t bytes, const uint8_t *end, const char *caller);
#else
#define PROTO_ENCODE_DEBUG_PARAM
#define PROTO_ENCODE_DEBUG_ARG
#define PROTO_ENCODE_DEBUG_INIT(buf)
#define PROTO_ENCODE_CHECK_BOUNDS(pos, n)
#endif
class ProtoWriteBuffer {
public:
ProtoWriteBuffer(APIBuffer *buffer) : buffer_(buffer), pos_(buffer->data() + buffer->size()) {}
@@ -207,15 +240,6 @@ class ProtoWriteBuffer {
}
this->encode_varint_raw_slow_(value);
}
void encode_varint_raw_64(uint64_t value) {
while (value > 0x7F) {
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
}
this->debug_check_bounds_(1);
*this->pos_++ = static_cast<uint8_t>(value);
}
/**
* Encode a field key (tag/wire type combination).
*
@@ -229,123 +253,6 @@ class ProtoWriteBuffer {
* Following https://protobuf.dev/programming-guides/encoding/#structure
*/
void encode_field_raw(uint32_t field_id, uint32_t type) { this->encode_varint_raw((field_id << 3) | type); }
/// Write a single precomputed tag byte. Tag must be < 128.
inline void write_raw_byte(uint8_t b) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(1);
*this->pos_++ = b;
}
/// Write raw bytes to the buffer (no tag, no length prefix).
inline void encode_raw(const void *data, size_t len) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(len);
std::memcpy(this->pos_, data, len);
this->pos_ += len;
}
/// Write a precomputed tag byte + 32-bit value in one operation.
/// Tag must be a single-byte varint (< 128). No zero check.
inline void write_tag_and_fixed32(uint8_t tag, uint32_t value) ESPHOME_ALWAYS_INLINE {
this->debug_check_bounds_(5);
this->pos_[0] = tag;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
std::memcpy(this->pos_ + 1, &value, 4);
#else
this->pos_[1] = static_cast<uint8_t>(value & 0xFF);
this->pos_[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
this->pos_[3] = static_cast<uint8_t>((value >> 16) & 0xFF);
this->pos_[4] = static_cast<uint8_t>((value >> 24) & 0xFF);
#endif
this->pos_ += 5;
}
void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
if (len == 0 && !force)
return;
this->encode_field_raw(field_id, 2); // type 2: Length-delimited string
this->encode_varint_raw(len);
// Direct memcpy into pre-sized buffer — avoids push_back() per-byte capacity checks
// and vector::insert() iterator overhead. ~10-11x faster for 16-32 byte strings.
this->debug_check_bounds_(len);
std::memcpy(this->pos_, string, len);
this->pos_ += len;
}
void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size(), force);
}
void encode_string(uint32_t field_id, const StringRef &ref, bool force = false) {
this->encode_string(field_id, ref.c_str(), ref.size(), force);
}
void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) {
this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);
}
void encode_uint32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - uint32
this->encode_varint_raw(value);
}
void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
if (value == 0 && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - uint64
this->encode_varint_raw_64(value);
}
void encode_bool(uint32_t field_id, bool value, bool force = false) {
if (!value && !force)
return;
this->encode_field_raw(field_id, 0); // type 0: Varint - bool
this->debug_check_bounds_(1);
*this->pos_++ = value ? 0x01 : 0x00;
}
void encode_fixed32(uint32_t field_id, uint32_t value, bool force = false) {
if (value == 0 && !force)
return;
this->encode_field_raw(field_id, 5); // type 5: 32-bit fixed32
this->debug_check_bounds_(4);
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
// Protobuf fixed32 is little-endian, so direct copy works
std::memcpy(this->pos_, &value, 4);
this->pos_ += 4;
#else
*this->pos_++ = (value >> 0) & 0xFF;
*this->pos_++ = (value >> 8) & 0xFF;
*this->pos_++ = (value >> 16) & 0xFF;
*this->pos_++ = (value >> 24) & 0xFF;
#endif
}
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
// not supported to reduce overhead on embedded systems. All ESPHome devices are
// 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support
// is needed in the future, the necessary encoding/decoding functions must be added.
void encode_float(uint32_t field_id, float value, bool force = false) {
if (value == 0.0f && !force)
return;
union {
float value;
uint32_t raw;
} val{};
val.value = value;
this->encode_fixed32(field_id, val.raw);
}
void encode_int32(uint32_t field_id, int32_t value, bool force = false) {
if (value < 0) {
// negative int32 is always 10 byte long
this->encode_int64(field_id, value, force);
return;
}
this->encode_uint32(field_id, static_cast<uint32_t>(value), force);
}
void encode_int64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, static_cast<uint64_t>(value), force);
}
void encode_sint32(uint32_t field_id, int32_t value, bool force = false) {
this->encode_uint32(field_id, encode_zigzag32(value), force);
}
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, encode_zigzag64(value), force);
}
/// Encode a packed repeated sint32 field (zero-copy from vector)
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
/// Single-pass encode for repeated submessage elements.
/// Thin template wrapper; all buffer work is in the non-template core.
template<typename T> void encode_sub_message(uint32_t field_id, const T &value);
@@ -353,12 +260,17 @@ class ProtoWriteBuffer {
/// Thin template wrapper; all buffer work is in the non-template core.
template<typename T> void encode_optional_sub_message(uint32_t field_id, const T &value);
// NOLINTBEGIN(readability-identifier-naming)
// Non-template core for encode_sub_message — backpatch approach.
void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &));
void encode_sub_message(uint32_t field_id, const void *value,
uint8_t *(*encode_fn)(const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM));
// Non-template core for encode_optional_sub_message.
void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value,
void (*encode_fn)(const void *, ProtoWriteBuffer &));
uint8_t *(*encode_fn)(const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM));
// NOLINTEND(readability-identifier-naming)
APIBuffer *get_buffer() const { return buffer_; }
uint8_t *get_pos() const { return pos_; }
void set_pos(uint8_t *pos) { pos_ = pos; }
protected:
// Slow path for encode_varint_raw values >= 128, outlined to keep fast path small
@@ -375,6 +287,220 @@ class ProtoWriteBuffer {
uint8_t *pos_;
};
// Varint encoding thresholds — used by both proto_encode_* free functions and ProtoSize.
constexpr uint32_t VARINT_MAX_1_BYTE = 1 << 7; // 128
constexpr uint32_t VARINT_MAX_2_BYTE = 1 << 14; // 16384
/// Static encode helpers for generated encode() functions.
/// Generated code hoists buffer.pos_ into a local uint8_t *__restrict__ pos,
/// then calls these methods which take pos by reference. No struct, no overhead.
/// For sub-messages, pos is synced back to buffer before the call and reloaded after.
class ProtoEncode {
public:
/// Write a multi-byte varint directly through a pos pointer.
template<typename T>
static inline void encode_varint_raw_loop(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, T value) {
do {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value | 0x80);
value >>= 7;
} while (value > 0x7F);
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value);
}
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t value) {
if (value < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value);
return;
}
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
}
/// Encode a varint that is expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_short(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t value) {
if (value < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value);
return;
}
if (value < VARINT_MAX_2_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 2);
*pos++ = static_cast<uint8_t>(value | 0x80);
*pos++ = static_cast<uint8_t>(value >> 7);
return;
}
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
}
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint64_t value) {
if (value < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = static_cast<uint8_t>(value);
return;
}
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
}
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t type) {
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type);
}
/// Write a single precomputed tag byte. Tag must be < 128.
static inline void ESPHOME_ALWAYS_INLINE write_raw_byte(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint8_t b) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = b;
}
/// Write raw bytes to the buffer (no tag, no length prefix).
static inline void ESPHOME_ALWAYS_INLINE encode_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
const void *data, size_t len) {
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
std::memcpy(pos, data, len);
pos += len;
}
/// Encode tag + 1-byte length + raw string data. For strings with max_data_length < 128.
/// Tag must be a single-byte varint (< 128). Always encodes (no zero check).
static inline void encode_short_string_force(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint8_t tag,
const StringRef &ref) {
#ifdef ESPHOME_DEBUG_API
assert(ref.size() < 128 && "encode_short_string_force: string exceeds max_data_length < 128");
#endif
PROTO_ENCODE_CHECK_BOUNDS(pos, 2 + ref.size());
pos[0] = tag;
pos[1] = static_cast<uint8_t>(ref.size());
std::memcpy(pos + 2, ref.c_str(), ref.size());
pos += 2 + ref.size();
}
/// Write a precomputed tag byte + 32-bit value in one operation.
static inline void ESPHOME_ALWAYS_INLINE write_tag_and_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint8_t tag, uint32_t value) {
PROTO_ENCODE_CHECK_BOUNDS(pos, 5);
pos[0] = tag;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
std::memcpy(pos + 1, &value, 4);
#else
pos[1] = static_cast<uint8_t>(value & 0xFF);
pos[2] = static_cast<uint8_t>((value >> 8) & 0xFF);
pos[3] = static_cast<uint8_t>((value >> 16) & 0xFF);
pos[4] = static_cast<uint8_t>((value >> 24) & 0xFF);
#endif
pos += 5;
}
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
const char *string, size_t len, bool force = false) {
if (len == 0 && !force)
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
if (len < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
*pos++ = static_cast<uint8_t>(len);
} else {
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, len);
PROTO_ENCODE_CHECK_BOUNDS(pos, len);
}
std::memcpy(pos, string, len);
pos += len;
}
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
const std::string &value, bool force = false) {
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, value.data(), value.size(), force);
}
static inline void encode_string(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
const StringRef &ref, bool force = false) {
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, ref.c_str(), ref.size(), force);
}
static inline void encode_bytes(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
const uint8_t *data, size_t len, bool force = false) {
encode_string(pos PROTO_ENCODE_DEBUG_ARG, field_id, reinterpret_cast<const char *>(data), len, force);
}
static inline void encode_uint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
uint32_t value, bool force = false) {
if (value == 0 && !force)
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, value);
}
static inline void encode_uint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
uint64_t value, bool force = false) {
if (value == 0 && !force)
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
}
static inline void encode_bool(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, bool value,
bool force = false) {
if (!value && !force)
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 0);
PROTO_ENCODE_CHECK_BOUNDS(pos, 1);
*pos++ = value ? 0x01 : 0x00;
}
static inline void encode_fixed32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
uint32_t value, bool force = false) {
if (value == 0 && !force)
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 5);
PROTO_ENCODE_CHECK_BOUNDS(pos, 4);
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
std::memcpy(pos, &value, 4);
pos += 4;
#else
*pos++ = (value >> 0) & 0xFF;
*pos++ = (value >> 8) & 0xFF;
*pos++ = (value >> 16) & 0xFF;
*pos++ = (value >> 24) & 0xFF;
#endif
}
// NOTE: Wire type 1 (64-bit fixed: double, fixed64, sfixed64) is intentionally
// not supported to reduce overhead on embedded systems. All ESPHome devices are
// 32-bit microcontrollers where 64-bit operations are expensive. If 64-bit support
// is needed in the future, the necessary encoding/decoding functions must be added.
static inline void encode_float(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, float value,
bool force = false) {
uint32_t raw = float_to_raw(value);
if (raw == 0 && !force)
return;
encode_fixed32(pos PROTO_ENCODE_DEBUG_ARG, field_id, raw);
}
static inline void encode_int32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int32_t value,
bool force = false) {
if (value < 0) {
// negative int32 is always 10 byte long
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force);
return;
}
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint32_t>(value), force);
}
static inline void encode_int64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id, int64_t value,
bool force = false) {
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, static_cast<uint64_t>(value), force);
}
static inline void encode_sint32(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
int32_t value, bool force = false) {
encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag32(value), force);
}
static inline void encode_sint64(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, uint32_t field_id,
int64_t value, bool force = false) {
encode_uint64(pos PROTO_ENCODE_DEBUG_ARG, field_id, encode_zigzag64(value), force);
}
/// Sub-message encoding: sync pos to buffer, delegate, get pos from return value.
template<typename T>
static inline void encode_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM, ProtoWriteBuffer &buffer,
uint32_t field_id, const T &value) {
buffer.set_pos(pos);
buffer.encode_sub_message(field_id, value);
pos = buffer.get_pos();
}
template<typename T>
static inline void encode_optional_sub_message(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
ProtoWriteBuffer &buffer, uint32_t field_id, const T &value) {
buffer.set_pos(pos);
buffer.encode_optional_sub_message(field_id, value);
pos = buffer.get_pos();
}
};
#ifdef HAS_PROTO_MESSAGE_DUMP
/**
* Fixed-size buffer for message dumps - avoids heap allocation.
@@ -470,7 +596,7 @@ class ProtoMessage {
// All call sites use templates to preserve the concrete type, so virtual
// dispatch is not needed. This eliminates per-message vtable entries for
// encode/calculate_size, saving ~1.3 KB of flash across all message types.
void encode(ProtoWriteBuffer &buffer) const {}
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const { return buffer.get_pos(); }
uint32_t calculate_size() const { return 0; }
#ifdef HAS_PROTO_MESSAGE_DUMP
virtual const char *dump_to(DumpBuffer &out) const = 0;
@@ -512,9 +638,10 @@ class ProtoDecodableMessage : public ProtoMessage {
class ProtoSize {
public:
// Varint encoding thresholds: values below each threshold fit in N bytes
static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128
static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384
// Varint encoding thresholds — use namespace-level constants for 1/2 byte,
// class-level for 3/4 byte (only used within ProtoSize).
static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = VARINT_MAX_1_BYTE;
static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = VARINT_MAX_2_BYTE;
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
@@ -531,6 +658,17 @@ class ProtoSize {
return varint_wide(value);
return varint_slow(value);
}
/// Size of a varint expected to be 1-2 bytes (e.g. zigzag RSSI, small lengths).
/// Inlines both checks; falls back to slow path for 3+ bytes.
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_short(uint32_t value) {
if (value < VARINT_THRESHOLD_1_BYTE) [[likely]]
return 1;
if (value < VARINT_THRESHOLD_2_BYTE) [[likely]]
return 2;
if (__builtin_is_constant_evaluated())
return varint_wide(value);
return varint_slow(value);
}
private:
// Slow path for varint >= 128, outlined to keep fast path small
@@ -635,8 +773,8 @@ class ProtoSize {
}
static constexpr uint32_t calc_bool(uint32_t field_id_size, bool value) { return value ? field_id_size + 1 : 0; }
static constexpr uint32_t calc_bool_force(uint32_t field_id_size) { return field_id_size + 1; }
static constexpr uint32_t calc_float(uint32_t field_id_size, float value) {
return value != 0.0f ? field_id_size + 4 : 0;
static uint32_t calc_float(uint32_t field_id_size, float value) {
return float_to_raw(value) != 0 ? field_id_size + 4 : 0;
}
static constexpr uint32_t calc_fixed32(uint32_t field_id_size, uint32_t value) {
return value ? field_id_size + 4 : 0;
@@ -645,10 +783,10 @@ class ProtoSize {
return value ? field_id_size + 4 : 0;
}
static constexpr uint32_t calc_sint32(uint32_t field_id_size, int32_t value) {
return value ? field_id_size + varint(encode_zigzag32(value)) : 0;
return value ? field_id_size + varint_short(encode_zigzag32(value)) : 0;
}
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_sint32_force(uint32_t field_id_size, int32_t value) {
return field_id_size + varint(encode_zigzag32(value));
return field_id_size + varint_short(encode_zigzag32(value));
}
static constexpr uint32_t calc_int64(uint32_t field_id_size, int64_t value) {
return value ? field_id_size + varint(value) : 0;
@@ -691,28 +829,9 @@ class ProtoSize {
// Implementation of methods that depend on ProtoSize being fully defined
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
if (values.empty())
return;
// Calculate packed size
size_t packed_size = 0;
for (int value : values) {
packed_size += ProtoSize::varint(encode_zigzag32(value));
}
// Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values
this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED);
this->encode_varint_raw(packed_size);
for (int value : values) {
this->encode_varint_raw(encode_zigzag32(value));
}
}
// Encode thunk — converts void* back to concrete type for direct encode() call
template<typename T> void proto_encode_msg(const void *msg, ProtoWriteBuffer &buf) {
static_cast<const T *>(msg)->encode(buf);
template<typename T> uint8_t *proto_encode_msg(const void *msg, ProtoWriteBuffer &buf PROTO_ENCODE_DEBUG_PARAM) {
return static_cast<const T *>(msg)->encode(buf PROTO_ENCODE_DEBUG_ARG);
}
// Thin template wrapper; delegates to non-template core in proto.cpp.
+1 -1
View File
@@ -275,7 +275,7 @@ template<typename... Ts> class APIRespondAction : public Action<Ts...> {
protected:
APIServer *parent_;
TemplatableValue<bool, Ts...> success_{true};
TemplatableFn<bool, Ts...> success_{[](Ts...) -> bool { return true; }};
TemplatableValue<std::string, Ts...> error_message_{""};
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
std::function<void(Ts..., JsonObject)> json_builder_;
+3 -1
View File
@@ -6,6 +6,7 @@ from esphome.const import (
CONF_LIGHTNING_ENERGY,
ICON_FLASH,
ICON_SIGNAL_DISTANCE_VARIANT,
STATE_CLASS_MEASUREMENT,
UNIT_KILOMETER,
)
@@ -20,13 +21,14 @@ CONFIG_SCHEMA = cv.Schema(
unit_of_measurement=UNIT_KILOMETER,
icon=ICON_SIGNAL_DISTANCE_VARIANT,
accuracy_decimals=1,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_LIGHTNING_ENERGY): sensor.sensor_schema(
icon=ICON_FLASH,
accuracy_decimals=1,
),
}
).extend(cv.COMPONENT_SCHEMA)
)
async def to_code(config):
+17 -25
View File
@@ -169,53 +169,45 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
# Radar configuration
if frontend_reset := config.get(CONF_HW_FRONTEND_RESET):
template_ = await cg.templatable(frontend_reset, args, int)
template_ = await cg.templatable(frontend_reset, args, cg.int32)
cg.add(var.set_hw_frontend_reset(template_))
if freq := config.get(CONF_FREQUENCY):
template_ = await cg.templatable(freq, args, float)
template_ = int(template_ / 1000000)
if cg.is_template(freq):
template_ = await cg.templatable(freq, args, cg.int32)
else:
template_ = int(freq / 1000000)
cg.add(var.set_frequency(template_))
if sens_dist := config.get(CONF_SENSING_DISTANCE):
template_ = await cg.templatable(sens_dist, args, int)
if (sens_dist := config.get(CONF_SENSING_DISTANCE)) is not None:
template_ = await cg.templatable(sens_dist, args, cg.int32)
cg.add(var.set_sensing_distance(template_))
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
template_ = await cg.templatable(selfcheck, args, float)
if isinstance(template_, cv.TimePeriod):
template_ = template_.total_milliseconds
template_ = int(template_)
template_ = await cg.templatable(selfcheck, args, cg.int32)
cg.add(var.set_poweron_selfcheck_time(template_))
if protect := config.get(CONF_PROTECT_TIME):
template_ = await cg.templatable(protect, args, float)
if isinstance(template_, cv.TimePeriod):
template_ = template_.total_milliseconds
template_ = int(template_)
template_ = await cg.templatable(protect, args, cg.int32)
cg.add(var.set_protect_time(template_))
if trig_base := config.get(CONF_TRIGGER_BASE):
template_ = await cg.templatable(trig_base, args, float)
if isinstance(template_, cv.TimePeriod):
template_ = template_.total_milliseconds
template_ = int(template_)
template_ = await cg.templatable(trig_base, args, cg.int32)
cg.add(var.set_trigger_base(template_))
if trig_keep := config.get(CONF_TRIGGER_KEEP):
template_ = await cg.templatable(trig_keep, args, float)
if isinstance(template_, cv.TimePeriod):
template_ = template_.total_milliseconds
template_ = int(template_)
template_ = await cg.templatable(trig_keep, args, cg.int32)
cg.add(var.set_trigger_keep(template_))
if stage_gain := config.get(CONF_STAGE_GAIN):
template_ = await cg.templatable(stage_gain, args, int)
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
template_ = await cg.templatable(stage_gain, args, cg.int32)
cg.add(var.set_stage_gain(template_))
if power := config.get(CONF_POWER_CONSUMPTION):
template_ = await cg.templatable(power, args, float)
template_ = int(template_ * 1000000)
if cg.is_template(power):
template_ = await cg.templatable(power, args, cg.int32)
else:
template_ = int(power * 1000000)
cg.add(var.set_power_consumption(template_))
return var
+2
View File
@@ -14,6 +14,7 @@ from esphome.const import (
CONF_VOLTAGE,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_REACTIVE_POWER,
@@ -103,6 +104,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_HERTZ,
icon=ICON_CURRENT_AC,
accuracy_decimals=1,
device_class=DEVICE_CLASS_FREQUENCY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Required(CONF_LINE_FREQUENCY): cv.enum(LINE_FREQS, upper=True),
+2 -1
View File
@@ -20,6 +20,7 @@ from esphome.const import (
DEVICE_CLASS_APPARENT_POWER,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_REACTIVE_POWER,
@@ -131,7 +132,6 @@ ATM90E32_PHASE_SCHEMA = cv.Schema(
cv.Optional(CONF_PHASE_ANGLE): sensor.sensor_schema(
unit_of_measurement=UNIT_DEGREES,
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_HARMONIC_POWER): sensor.sensor_schema(
@@ -166,6 +166,7 @@ CONFIG_SCHEMA = (
unit_of_measurement=UNIT_HERTZ,
icon=ICON_CURRENT_AC,
accuracy_decimals=1,
device_class=DEVICE_CLASS_FREQUENCY,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CHIP_TEMPERATURE): sensor.sensor_schema(
+1
View File
@@ -210,6 +210,7 @@ async def to_code(config):
data = _get_data()
if data.flac_support:
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
add_idf_component(name="esphome/micro-flac", ref="0.1.1")
if data.mp3_support:
cg.add_define("USE_AUDIO_MP3_SUPPORT")
if data.opus_support:
+33 -50
View File
@@ -84,13 +84,10 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
switch (this->audio_file_type_) {
#ifdef USE_AUDIO_FLAC_SUPPORT
case AudioFileType::FLAC:
this->flac_decoder_ = make_unique<esp_audio_libs::flac::FLACDecoder>();
// CRC check slows down decoding by 15-20% on an ESP32-S3. FLAC sources in ESPHome are either from an http source
// or built into the firmware, so the data integrity is already verified by the time it gets to the decoder,
// making the CRC check unnecessary.
this->flac_decoder_->set_crc_check_enabled(false);
this->flac_decoder_ = make_unique<micro_flac::FLACDecoder>();
this->free_buffer_required_ =
this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header
this->decoder_buffers_internally_ = true;
break;
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
@@ -268,59 +265,45 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
#ifdef USE_AUDIO_FLAC_SUPPORT
FileDecoderState AudioDecoder::decode_flac_() {
if (!this->audio_stream_info_.has_value()) {
// Header hasn't been read
auto result = this->flac_decoder_->read_header(this->input_buffer_->data(), this->input_buffer_->available());
size_t bytes_consumed, samples_decoded;
if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
// Serrious error reading FLAC header, there is no recovery
return FileDecoderState::FAILED;
micro_flac::FLACDecoderResult result = this->flac_decoder_->decode(
this->input_buffer_->data(), this->input_buffer_->available(), this->output_transfer_buffer_->get_buffer_end(),
this->output_transfer_buffer_->free(), bytes_consumed, samples_decoded);
if (result == micro_flac::FLAC_DECODER_SUCCESS) {
if (samples_decoded > 0 && this->audio_stream_info_.has_value()) {
this->output_transfer_buffer_->increase_buffer_length(
this->audio_stream_info_.value().samples_to_bytes(samples_decoded));
}
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_buffer_->consume(bytes_consumed);
} else if (result == micro_flac::FLAC_DECODER_HEADER_READY) {
// Header just parsed, stream info now available
const auto &info = this->flac_decoder_->get_stream_info();
this->audio_stream_info_ = audio::AudioStreamInfo(info.bits_per_sample(), info.num_channels(), info.sample_rate());
if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
return FileDecoderState::MORE_TO_PROCESS;
}
// Reallocate the output transfer buffer to the smallest necessary size
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
// Reallocate the output transfer buffer to the required size
this->free_buffer_required_ = this->flac_decoder_->get_output_buffer_size_samples() * info.bytes_per_sample();
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
// Couldn't reallocate output buffer
return FileDecoderState::FAILED;
}
this->audio_stream_info_ =
audio::AudioStreamInfo(this->flac_decoder_->get_sample_depth(), this->flac_decoder_->get_num_channels(),
this->flac_decoder_->get_sample_rate());
return FileDecoderState::MORE_TO_PROCESS;
}
uint32_t output_samples = 0;
auto result = this->flac_decoder_->decode_frame(this->input_buffer_->data(), this->input_buffer_->available(),
this->output_transfer_buffer_->get_buffer_end(), &output_samples);
if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
// Not an issue, just needs more data that we'll get next time.
return FileDecoderState::POTENTIALLY_FAILED;
}
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_buffer_->consume(bytes_consumed);
if (result > esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
// Corrupted frame, don't retry with current buffer content, wait for new sync
return FileDecoderState::POTENTIALLY_FAILED;
}
// We have successfully decoded some input data and have new output data
this->output_transfer_buffer_->increase_buffer_length(
this->audio_stream_info_.value().samples_to_bytes(output_samples));
if (result == esp_audio_libs::flac::FLAC_DECODER_NO_MORE_FRAMES) {
this->input_buffer_->consume(bytes_consumed);
} else if (result == micro_flac::FLAC_DECODER_END_OF_STREAM) {
this->input_buffer_->consume(bytes_consumed);
return FileDecoderState::END_OF_FILE;
} else if (result == micro_flac::FLAC_DECODER_NEED_MORE_DATA) {
this->input_buffer_->consume(bytes_consumed);
return FileDecoderState::MORE_TO_PROCESS;
} else if (result == micro_flac::FLAC_DECODER_ERROR_OUTPUT_TOO_SMALL) {
// Reallocate to decode the frame on the next call
const auto &info = this->flac_decoder_->get_stream_info();
this->free_buffer_required_ = this->flac_decoder_->get_output_buffer_size_samples() * info.bytes_per_sample();
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
return FileDecoderState::FAILED;
}
} else {
ESP_LOGE(TAG, "FLAC decoder failed: %d", static_cast<int>(result));
return FileDecoderState::POTENTIALLY_FAILED;
}
return FileDecoderState::MORE_TO_PROCESS;
+6 -4
View File
@@ -16,14 +16,16 @@
#include "esp_err.h"
// esp-audio-libs
#ifdef USE_AUDIO_FLAC_SUPPORT
#include <flac_decoder.h>
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
#include <mp3_decoder.h>
#endif
#include <wav_decoder.h>
// micro-flac
#ifdef USE_AUDIO_FLAC_SUPPORT
#include <micro_flac/flac_decoder.h>
#endif
// micro-opus
#ifdef USE_AUDIO_OPUS_SUPPORT
#include <micro_opus/ogg_opus_decoder.h>
@@ -119,7 +121,7 @@ class AudioDecoder {
std::unique_ptr<esp_audio_libs::wav_decoder::WAVDecoder> wav_decoder_;
#ifdef USE_AUDIO_FLAC_SUPPORT
FileDecoderState decode_flac_();
std::unique_ptr<esp_audio_libs::flac::FLACDecoder> flac_decoder_;
std::unique_ptr<micro_flac::FLACDecoder> flac_decoder_;
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
FileDecoderState decode_mp3_();
+1 -1
View File
@@ -12,7 +12,7 @@ CODEOWNERS = ["@B48D81EFCC"]
sensor_ns = cg.esphome_ns.namespace("bh1900nux")
BH1900NUXSensor = sensor_ns.class_(
"BH1900NUXSensor", cg.PollingComponent, i2c.I2CDevice
"BH1900NUXSensor", sensor.Sensor, cg.PollingComponent, i2c.I2CDevice
)
CONFIG_SCHEMA = (
+24 -25
View File
@@ -390,7 +390,7 @@ def validate_multi_click_timing(value):
new_state = v_.get(CONF_STATE, not state)
if new_state == state:
raise cv.Invalid(
f"Timings must have alternating state. Indices {i} and {i + 1} have the same state {state}"
f"Timings must have alternating state. Indices {i - 1} and {i} have the same state {state}"
)
if max_length is not None and max_length < min_length:
raise cv.Invalid(
@@ -531,16 +531,31 @@ def binary_sensor_schema(
return _BINARY_SENSOR_SCHEMA.extend(schema)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_PRESS,
"add_on_state_callback",
forwarder=automation.TriggerOnTrueForwarder,
),
automation.CallbackAutomation(
CONF_ON_RELEASE,
"add_on_state_callback",
forwarder=automation.TriggerOnFalseForwarder,
),
automation.CallbackAutomation(
CONF_ON_STATE, "add_on_state_callback", [(bool, "x")]
),
automation.CallbackAutomation(
CONF_ON_STATE_CHANGE,
"add_full_state_callback",
[(cg.optional.template(bool), "x_previous"), (cg.optional.template(bool), "x")],
),
)
@coroutine_with_priority(CoroPriority.AUTOMATION)
async def _build_binary_sensor_automations(var, config):
for conf_key, forwarder in (
(CONF_ON_PRESS, automation.TriggerOnTrueForwarder),
(CONF_ON_RELEASE, automation.TriggerOnFalseForwarder),
):
for conf in config.get(conf_key, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [], conf, forwarder=forwarder
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
for conf in config.get(CONF_ON_CLICK, []):
trigger = cg.new_Pvariable(
@@ -572,22 +587,6 @@ async def _build_binary_sensor_automations(var, config):
await cg.register_component(trigger, conf)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_STATE, []):
await automation.build_callback_automation(
var, "add_on_state_callback", [(bool, "x")], conf
)
for conf in config.get(CONF_ON_STATE_CHANGE, []):
await automation.build_callback_automation(
var,
"add_full_state_callback",
[
(cg.optional.template(bool), "x_previous"),
(cg.optional.template(bool), "x"),
],
conf,
)
@setup_entity("binary_sensor")
async def setup_binary_sensor_core_(var, config):
+6 -6
View File
@@ -36,7 +36,7 @@ class TimeoutFilter : public Filter, public Component {
template<typename T> void set_timeout_value(T timeout) { this->timeout_delay_ = timeout; }
protected:
TemplatableValue<uint32_t> timeout_delay_{};
TemplatableFn<uint32_t> timeout_delay_{};
};
class DelayedOnOffFilter final : public Filter, public Component {
@@ -49,8 +49,8 @@ class DelayedOnOffFilter final : public Filter, public Component {
template<typename T> void set_off_delay(T delay) { this->off_delay_ = delay; }
protected:
TemplatableValue<uint32_t> on_delay_{};
TemplatableValue<uint32_t> off_delay_{};
TemplatableFn<uint32_t> on_delay_{};
TemplatableFn<uint32_t> off_delay_{};
};
class DelayedOnFilter : public Filter, public Component {
@@ -62,7 +62,7 @@ class DelayedOnFilter : public Filter, public Component {
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected:
TemplatableValue<uint32_t> delay_{};
TemplatableFn<uint32_t> delay_{};
};
class DelayedOffFilter : public Filter, public Component {
@@ -74,7 +74,7 @@ class DelayedOffFilter : public Filter, public Component {
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected:
TemplatableValue<uint32_t> delay_{};
TemplatableFn<uint32_t> delay_{};
};
class InvertFilter : public Filter {
@@ -155,7 +155,7 @@ class SettleFilter : public Filter, public Component {
template<typename T> void set_delay(T delay) { this->delay_ = delay; }
protected:
TemplatableValue<uint32_t> delay_{};
TemplatableFn<uint32_t> delay_{};
bool steady_{true};
};
+1 -1
View File
@@ -89,6 +89,6 @@ async def to_code(config):
)
await cg.register_component(var, conf)
if restore_value := config.get(CONF_RESTORE_VALUE):
if restore_value := conf.get(CONF_RESTORE_VALUE):
cg.add(var.set_restore_value(restore_value))
cg.add(getattr(bl0940, setter_method)(var))
+1 -1
View File
@@ -126,7 +126,7 @@ def set_reference_values(config):
config.setdefault(CONF_POWER_REFERENCE, DEFAULT_BL0940_LEGACY_PREF)
config.setdefault(CONF_ENERGY_REFERENCE, DEFAULT_BL0940_LEGACY_EREF)
else:
vref = config.get(CONF_VOLTAGE_REFERENCE, DEFAULT_BL0940_VREF)
vref = config.get(CONF_REFERENCE_VOLTAGE, DEFAULT_BL0940_VREF)
r_one = config.get(CONF_RESISTOR_ONE, DEFAULT_BL0940_R1)
r_two = config.get(CONF_RESISTOR_TWO, DEFAULT_BL0940_R2)
r_shunt = config.get(CONF_RESISTOR_SHUNT, DEFAULT_BL0940_RL)
@@ -88,7 +88,7 @@ async def to_code(config):
)
cg.add(var.set_char_uuid128(uuid128))
if descriptor_uuid := config:
if descriptor_uuid := config.get(CONF_DESCRIPTOR_UUID):
if len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid16_format):
cg.add(var.set_descr_uuid16(esp32_ble_tracker.as_hex(descriptor_uuid)))
elif len(descriptor_uuid) == len(esp32_ble_tracker.bt_uuid32_format):
+1 -1
View File
@@ -31,7 +31,7 @@ BMP581SPIComponent = bmp581_ns.class_(
def check_spi_mode(config):
spi_mode = config.get(CONF_SPI_MODE)
if spi_mode not in VALID_SPI_MODES:
raise cv.Invalid("BMP581 only supports SPI mode 3")
raise cv.Invalid("BMP581 only supports SPI mode 0 or mode 3")
return config
+1 -1
View File
@@ -14,7 +14,7 @@ CONFIG_SCHEMA = output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.GenerateID(CONF_BP1658CJ_ID): cv.use_id(BP1658CJ),
cv.Required(CONF_ID): cv.declare_id(Channel),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=65535),
cv.Required(CONF_CHANNEL): cv.int_range(min=0, max=4),
}
).extend(cv.COMPONENT_SCHEMA)
+6 -4
View File
@@ -79,12 +79,14 @@ def button_schema(
return _BUTTON_SCHEMA.extend(schema)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(CONF_ON_PRESS, "add_on_press_callback"),
)
@setup_entity("button")
async def setup_button_core_(var, config):
for conf in config.get(CONF_ON_PRESS, []):
await automation.build_callback_automation(
var, "add_on_press_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
setup_device_class(config)
+1 -1
View File
@@ -16,7 +16,7 @@ void log_button(const char *tag, const char *prefix, const char *type, Button *o
}
void Button::press() {
ESP_LOGD(TAG, "'%s' Pressed.", this->get_name().c_str());
ESP_LOGV(TAG, "'%s' Pressed.", this->get_name().c_str());
this->press_action();
this->press_callback_.call();
}
+1 -1
View File
@@ -161,7 +161,7 @@ async def canbus_action_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_CANBUS_ID])
if can_id := config.get(CONF_CAN_ID):
if (can_id := config.get(CONF_CAN_ID)) is not None:
can_id = await cg.templatable(can_id, args, cg.uint32)
cg.add(var.set_can_id(can_id))
cg.add(var.set_use_extended_id(config[CONF_USE_EXTENDED_ID]))
+4 -5
View File
@@ -423,11 +423,10 @@ def _register_setter_actions():
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
data = config[CONF_VALUE]
if cg.is_template(data):
templ_ = await cg.templatable(data, args, _type)
cg.add(getattr(var, _setter)(templ_))
else:
cg.add(getattr(var, _setter)(_map[data] if _map else data))
if _map and not cg.is_template(data):
data = _map[data]
templ_ = await cg.templatable(data, args, _type)
cg.add(getattr(var, _setter)(templ_))
return var
automation.register_action(
+1 -3
View File
@@ -9,9 +9,7 @@ MULTI_CONF = True
cd74hc4067_ns = cg.esphome_ns.namespace("cd74hc4067")
CD74HC4067Component = cd74hc4067_ns.class_(
"CD74HC4067Component", cg.Component, cg.PollingComponent
)
CD74HC4067Component = cd74hc4067_ns.class_("CD74HC4067Component", cg.Component)
CONF_PIN_S0 = "pin_s0"
CONF_PIN_S1 = "pin_s1"
+1 -1
View File
@@ -400,7 +400,7 @@ async def setup_climate_core_(var, config):
)
) is not None:
cg.add(
mqtt_.set_custom_target_temperature_state_topic(
mqtt_.set_custom_target_temperature_low_state_topic(
target_temperature_low_state_topic
)
)
+2
View File
@@ -32,4 +32,6 @@ ICON_CURRENT_DC = "mdi:current-dc"
ICON_SOLAR_PANEL = "mdi:solar-panel"
ICON_SOLAR_POWER = "mdi:solar-power"
KEY_METADATA = "metadata"
UNIT_AMPERE_HOUR = "Ah"
+4
View File
@@ -12,6 +12,7 @@ from esphome.const import (
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_POWER,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
UNIT_AMPERE,
UNIT_VOLT,
UNIT_WATT,
@@ -82,16 +83,19 @@ CONFIG_SCHEMA = cv.All(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=1,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=0,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
+50 -46
View File
@@ -32,52 +32,56 @@ DEPENDENCIES = ["uart"]
cse7766_ns = cg.esphome_ns.namespace("cse7766")
CSE7766Component = cse7766_ns.class_("CSE7766Component", cg.Component, uart.UARTDevice)
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(CSE7766Component),
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_APPARENT_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
accuracy_decimals=1,
device_class=DEVICE_CLASS_REACTIVE_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend(uart.UART_DEVICE_SCHEMA)
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(CSE7766Component),
cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_VOLTAGE,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_CURRENT): sensor.sensor_schema(
unit_of_measurement=UNIT_AMPERE,
accuracy_decimals=2,
device_class=DEVICE_CLASS_CURRENT,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT,
accuracy_decimals=1,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_ENERGY): sensor.sensor_schema(
unit_of_measurement=UNIT_WATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional(CONF_APPARENT_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS,
accuracy_decimals=1,
device_class=DEVICE_CLASS_APPARENT_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_REACTIVE_POWER): sensor.sensor_schema(
unit_of_measurement=UNIT_VOLT_AMPS_REACTIVE,
accuracy_decimals=1,
device_class=DEVICE_CLASS_REACTIVE_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_POWER_FACTOR): sensor.sensor_schema(
accuracy_decimals=2,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
}
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
"cse7766", baud_rate=4800, parity="EVEN", require_rx=True
)
@@ -15,10 +15,14 @@ CST226Button = cst226_ns.class_(
cg.Parented.template(CST226Touchscreen),
)
CONFIG_SCHEMA = binary_sensor.binary_sensor_schema(CST226Button).extend(
{
cv.GenerateID(CONF_CST226_ID): cv.use_id(CST226Touchscreen),
}
CONFIG_SCHEMA = (
binary_sensor.binary_sensor_schema(CST226Button)
.extend(
{
cv.GenerateID(CONF_CST226_ID): cv.use_id(CST226Touchscreen),
}
)
.extend(cv.COMPONENT_SCHEMA)
)
+6 -3
View File
@@ -204,7 +204,8 @@ async def datetime_date_set_to_code(config, action_id, template_arg, args):
("month", date_config[CONF_MONTH]),
("year", date_config[CONF_YEAR]),
)
cg.add(action_var.set_date(date_struct))
template_ = await cg.templatable(date_struct, args, cg.ESPTime)
cg.add(action_var.set_date(template_))
return action_var
@@ -236,7 +237,8 @@ async def datetime_time_set_to_code(config, action_id, template_arg, args):
("minute", time_config[CONF_MINUTE]),
("hour", time_config[CONF_HOUR]),
)
cg.add(action_var.set_time(time_struct))
template_ = await cg.templatable(time_struct, args, cg.ESPTime)
cg.add(action_var.set_time(template_))
return action_var
@@ -271,5 +273,6 @@ async def datetime_datetime_set_to_code(config, action_id, template_arg, args):
("month", datetime_config[CONF_MONTH]),
("year", datetime_config[CONF_YEAR]),
)
cg.add(action_var.set_datetime(datetime_struct))
template_ = await cg.templatable(datetime_struct, args, cg.ESPTime)
cg.add(action_var.set_datetime(template_))
return action_var
+10
View File
@@ -8,12 +8,14 @@ from esphome.const import (
CONF_FRAGMENTATION,
CONF_FREE,
CONF_LOOP_TIME,
DEVICE_CLASS_FREQUENCY,
ENTITY_CATEGORY_DIAGNOSTIC,
ICON_COUNTER,
ICON_TIMER,
PLATFORM_BK72XX,
PLATFORM_LN882X,
PLATFORM_RTL87XX,
STATE_CLASS_MEASUREMENT,
UNIT_BYTES,
UNIT_HERTZ,
UNIT_MILLISECOND,
@@ -38,12 +40,14 @@ CONFIG_SCHEMA = {
icon=ICON_COUNTER,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_BLOCK): sensor.sensor_schema(
unit_of_measurement=UNIT_BYTES,
icon=ICON_COUNTER,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_FRAGMENTATION): cv.All(
cv.Any(
@@ -59,6 +63,7 @@ CONFIG_SCHEMA = {
icon=ICON_COUNTER,
accuracy_decimals=1,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
state_class=STATE_CLASS_MEASUREMENT,
),
),
cv.Optional(CONF_MIN_FREE): cv.All(
@@ -72,6 +77,7 @@ CONFIG_SCHEMA = {
icon=ICON_COUNTER,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
state_class=STATE_CLASS_MEASUREMENT,
),
),
cv.Optional(CONF_LOOP_TIME): sensor.sensor_schema(
@@ -79,6 +85,7 @@ CONFIG_SCHEMA = {
icon=ICON_TIMER,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional(CONF_PSRAM): cv.All(
cv.only_on_esp32,
@@ -88,6 +95,7 @@ CONFIG_SCHEMA = {
icon=ICON_COUNTER,
accuracy_decimals=0,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
state_class=STATE_CLASS_MEASUREMENT,
),
),
cv.Optional(CONF_CPU_FREQUENCY): cv.All(
@@ -95,7 +103,9 @@ CONFIG_SCHEMA = {
unit_of_measurement=UNIT_HERTZ,
icon="mdi:speedometer",
accuracy_decimals=0,
device_class=DEVICE_CLASS_FREQUENCY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
state_class=STATE_CLASS_MEASUREMENT,
),
),
}
+1 -2
View File
@@ -266,8 +266,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_WAKEUP_PIN): validate_wakeup_pin,
cv.Optional(CONF_WAKEUP_PIN_MODE): cv.All(
cv.only_on([PLATFORM_ESP32, PLATFORM_BK72XX]),
cv.enum(WAKEUP_PIN_MODES),
upper=True,
cv.enum(WAKEUP_PIN_MODES, upper=True),
),
cv.Optional(CONF_ESP32_EXT1_WAKEUP): cv.All(
cv.only_on_esp32,
+15 -11
View File
@@ -64,15 +64,19 @@ FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_FINISHED_PLAYBACK, "add_on_finished_playback_callback"
),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
for conf in config.get(CONF_ON_FINISHED_PLAYBACK, []):
await automation.build_callback_automation(
var, "add_on_finished_playback_callback", [], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@automation.register_action(
@@ -122,7 +126,7 @@ async def dfplayer_previous_to_code(config, action_id, template_arg, args):
async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_FILE], args, float)
template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16)
cg.add(var.set_file(template_))
return var
@@ -143,10 +147,10 @@ async def dfplayer_play_mp3_to_code(config, action_id, template_arg, args):
async def dfplayer_play_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_FILE], args, float)
template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16)
cg.add(var.set_file(template_))
if CONF_LOOP in config:
template_ = await cg.templatable(config[CONF_LOOP], args, float)
template_ = await cg.templatable(config[CONF_LOOP], args, cg.bool_)
cg.add(var.set_loop(template_))
return var
@@ -167,13 +171,13 @@ async def dfplayer_play_to_code(config, action_id, template_arg, args):
async def dfplayer_play_folder_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_FOLDER], args, float)
template_ = await cg.templatable(config[CONF_FOLDER], args, cg.uint16)
cg.add(var.set_folder(template_))
if CONF_FILE in config:
template_ = await cg.templatable(config[CONF_FILE], args, float)
template_ = await cg.templatable(config[CONF_FILE], args, cg.uint16)
cg.add(var.set_file(template_))
if CONF_LOOP in config:
template_ = await cg.templatable(config[CONF_LOOP], args, float)
template_ = await cg.templatable(config[CONF_LOOP], args, cg.bool_)
cg.add(var.set_loop(template_))
return var
@@ -213,7 +217,7 @@ async def dfplayer_set_device_to_code(config, action_id, template_arg, args):
async def dfplayer_set_volume_to_code(config, action_id, template_arg, args):
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_VOLUME], args, float)
template_ = await cg.templatable(config[CONF_VOLUME], args, cg.uint8)
cg.add(var.set_volume(template_))
return var
@@ -97,7 +97,7 @@ def range_segment_list(input):
)
largest_distance = -1
for distance in input:
for i, distance in enumerate(input):
if isinstance(distance, cv.Lambda):
continue
m = cv.distance(distance)
@@ -112,7 +112,7 @@ def range_segment_list(input):
)
largest_distance = m
# Replace distance object with meters float
input[input.index(distance)] = m
input[i] = m
return input
@@ -159,7 +159,7 @@ async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args
await cg.register_parented(var, config[CONF_ID])
if factory_reset_config := config.get(CONF_FACTORY_RESET):
template_ = await cg.templatable(factory_reset_config, args, int)
template_ = await cg.templatable(factory_reset_config, args, cg.int32)
cg.add(var.set_factory_reset(template_))
if CONF_DETECTION_SEGMENTS in config:
@@ -200,7 +200,7 @@ async def dfrobot_sen0395_settings_to_code(config, action_id, template_arg, args
template_ = template_.total_milliseconds / 1000
cg.add(var.set_delay_after_disappear(template_))
if CONF_SENSITIVITY in config:
template_ = await cg.templatable(config[CONF_SENSITIVITY], args, int)
template_ = await cg.templatable(config[CONF_SENSITIVITY], args, cg.int32)
cg.add(var.set_sensitivity(template_))
return var
+43 -3
View File
@@ -1,6 +1,9 @@
from dataclasses import dataclass
from esphome import automation, core
from esphome.automation import maybe_simple_id
import esphome.codegen as cg
from esphome.components.const import KEY_METADATA
import esphome.config_validation as cv
from esphome.const import (
CONF_AUTO_CLEAR_ENABLED,
@@ -16,7 +19,9 @@ from esphome.const import (
SCHEDULER_DONT_RUN,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.cpp_generator import MockObj
DOMAIN = "display"
IS_PLATFORM_COMPONENT = True
display_ns = cg.esphome_ns.namespace("display")
@@ -112,8 +117,9 @@ FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
async def setup_display_core_(var, config):
if CONF_ROTATION in config:
cg.add(var.set_rotation(DISPLAY_ROTATIONS[config[CONF_ROTATION]]))
if rotation := config.get(CONF_ROTATION, 0):
# Default initialised value for rotation is 0
cg.add(var.set_rotation(DISPLAY_ROTATIONS[rotation]))
if (auto_clear := config.get(CONF_AUTO_CLEAR_ENABLED)) is not None:
# Default to true if pages or lambda is specified. Ideally this would be done during validation, but
@@ -146,6 +152,39 @@ async def setup_display_core_(var, config):
cg.add(var.show_test_card())
# Storage of display metadata in a central location, accessible via the id
@dataclass(frozen=True)
class DisplayMetaData:
width: int = 0
height: int = 0
has_writer: bool = False
has_hardware_rotation: bool = False
def get_all_display_metadata() -> dict[str, DisplayMetaData]:
"""Get all display metadata."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
def get_display_metadata(display_id: str) -> DisplayMetaData | None:
"""Get display metadata by ID for use by other components."""
return get_all_display_metadata().get(display_id, DisplayMetaData())
def add_metadata(
id: str | MockObj,
width: int,
height: int,
has_writer: bool,
has_hardware_rotation: bool = False,
):
get_all_display_metadata()[str(id)] = DisplayMetaData(
width, height, has_writer, has_hardware_rotation
)
async def register_display(var, config):
await cg.register_component(var, config)
await setup_display_core_(var, config)
@@ -168,7 +207,8 @@ async def display_page_show_to_code(config, action_id, template_arg, args):
cg.add(var.set_page(template_))
else:
paren = await cg.get_variable(config[CONF_ID])
cg.add(var.set_page(paren))
template_ = await cg.templatable(paren, args, DisplayPagePtr)
cg.add(var.set_page(template_))
return var
+1 -1
View File
@@ -704,7 +704,7 @@ class Display : public PollingComponent {
void add_on_page_change_trigger(DisplayOnPageChangeTrigger *t) { this->on_page_change_triggers_.push_back(t); }
/// Internal method to set the display rotation with.
void set_rotation(DisplayRotation rotation);
virtual void set_rotation(DisplayRotation rotation);
// Internal method to set display auto clearing.
void set_auto_clear(bool auto_clear_enabled) { this->auto_clear_enabled_ = auto_clear_enabled; }
@@ -119,7 +119,7 @@ DisplayMenuOnPrevTrigger = display_menu_base_ns.class_(
def validate_format(format):
if re.search(r"^%([+-])*(\d+)*(\.\d+)*[fg]$", format) is None:
if re.search(r"^%[+-]*(\d+)?(\.\d+)?[fg]$", format) is None:
raise cv.Invalid(
f"{CONF_FORMAT}: has to specify a printf-like format string specifying exactly one f or g type conversion, '{format}' provided"
)
+8 -3
View File
@@ -4,7 +4,7 @@ from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_UART_ID
CODEOWNERS = ["@glmnet", "@zuidwijk", "@PolarGoose"]
CODEOWNERS = ["@glmnet", "@PolarGoose"]
MULTI_CONF = True
@@ -16,6 +16,7 @@ CONF_DECRYPTION_KEY = "decryption_key"
CONF_DSMR_ID = "dsmr_id"
CONF_GAS_MBUS_ID = "gas_mbus_id"
CONF_WATER_MBUS_ID = "water_mbus_id"
CONF_THERMAL_MBUS_ID = "thermal_mbus_id"
CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
CONF_REQUEST_INTERVAL = "request_interval"
CONF_REQUEST_PIN = "request_pin"
@@ -35,7 +36,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_CRC_CHECK, default=True): cv.boolean,
cv.Optional(CONF_GAS_MBUS_ID, default=1): cv.int_,
cv.Optional(CONF_WATER_MBUS_ID, default=2): cv.int_,
cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_,
cv.Optional(CONF_THERMAL_MBUS_ID, default=3): cv.int_,
cv.Optional(CONF_MAX_TELEGRAM_LENGTH, default=1500): cv.int_range(min=1),
cv.Optional(CONF_REQUEST_PIN): pins.gpio_output_pin_schema,
cv.Optional(
CONF_REQUEST_INTERVAL, default="0ms"
@@ -44,7 +46,9 @@ CONFIG_SCHEMA = cv.All(
CONF_RECEIVE_TIMEOUT, default="200ms"
): cv.positive_time_period_milliseconds,
}
).extend(uart.UART_DEVICE_SCHEMA),
)
.extend(uart.UART_DEVICE_SCHEMA)
.extend(cv.COMPONENT_SCHEMA),
)
@@ -64,6 +68,7 @@ async def to_code(config):
cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID]))
# DSMR Parser
cg.add_library("esphome/dsmr_parser", "1.1.0")
+10
View File
@@ -122,42 +122,52 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional("total_imported_energy"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_delivered_tariff1"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_delivered_tariff2"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_delivered_tariff3"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_delivered_tariff4"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("total_exported_energy"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_returned_tariff1"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_returned_tariff2"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_returned_tariff3"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("reactive_energy_returned_tariff4"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("power_delivered"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT,
+1 -1
View File
@@ -29,7 +29,7 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(): cv.declare_id(E131Component),
cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True),
}
)
).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
+152
View File
@@ -0,0 +1,152 @@
from dataclasses import dataclass, field
from esphome import automation
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import (
CONF_COMMAND,
CONF_ID,
CONF_ON_DATA,
CONF_RX_BUFFER_SIZE,
CONF_UART_ID,
)
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.types import ConfigType
AUTO_LOAD = ["json"]
CODEOWNERS = ["@FredM67", "@TrystanLea", "@glynhudson"]
DEPENDENCIES = ["uart"]
emontx_ns = cg.esphome_ns.namespace("emontx")
EmonTx = emontx_ns.class_("EmonTx", cg.Component, uart.UARTDevice)
# Action to send command to emonTx
EmonTxSendCommandAction = emontx_ns.class_("EmonTxSendCommandAction", automation.Action)
CONF_EMONTX_ID = "emontx_id"
CONF_TAG_NAME = "tag_name"
CONF_ON_JSON = "on_json"
DOMAIN = "emontx"
MINIMUM_RX_BUFFER_SIZE = 2048
@dataclass
class EmonTxData:
sensor_counts: dict[str, int] = field(default_factory=dict)
def _get_data() -> EmonTxData:
if DOMAIN not in CORE.data:
CORE.data[DOMAIN] = EmonTxData()
return CORE.data[DOMAIN]
# Main configuration schema
CONFIG_SCHEMA = (
cv.Schema(
{
cv.GenerateID(): cv.declare_id(EmonTx),
cv.Optional(CONF_ON_JSON): automation.validate_automation({}),
cv.Optional(CONF_ON_DATA): automation.validate_automation({}),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA)
)
def final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
# Count sensors registered to this hub (IDs are resolved at final_validate stage)
hub_id = str(config[CONF_ID])
sensor_count = sum(
1
for s in full_config.get("sensor", [])
if s.get("platform") == "emontx" and str(s.get(CONF_EMONTX_ID)) == hub_id
)
_get_data().sensor_counts[hub_id] = sensor_count
# Ensure UART RX buffer size is large enough to handle data bursts from firmware
for uart_conf in full_config["uart"]:
if uart_conf[CONF_ID] == config[CONF_UART_ID]:
current_buffer_size = uart_conf[CONF_RX_BUFFER_SIZE]
if current_buffer_size < MINIMUM_RX_BUFFER_SIZE:
raise cv.Invalid(
f"Component emontx requires UART '{config[CONF_UART_ID]}' to have "
f"rx_buffer_size of at least {MINIMUM_RX_BUFFER_SIZE} bytes "
f"(currently set to {current_buffer_size} bytes). "
f"Please add 'rx_buffer_size: {MINIMUM_RX_BUFFER_SIZE}' to your uart configuration.",
path=[CONF_UART_ID],
)
break
# Validate UART settings
schema = uart.final_validate_device_schema(
"emontx",
baud_rate=115200,
require_tx=False,
require_rx=True,
data_bits=8,
parity="NONE",
stop_bits=1,
)
return schema(config)
FINAL_VALIDATE_SCHEMA = final_validate
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_JSON,
"add_on_json_callback",
[(cg.JsonObject, "json"), (cg.std_string, "raw_json")],
),
automation.CallbackAutomation(
CONF_ON_DATA, "add_on_data_callback", [(cg.std_string, "data")]
),
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
# Initialize sensor storage with count from final_validate
sensor_count = _get_data().sensor_counts.get(str(config[CONF_ID]), 0)
if sensor_count > 0:
cg.add(var.init_sensors(sensor_count))
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
# Action: emontx.send_command
EMONTX_SEND_COMMAND_ACTION_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.use_id(EmonTx),
cv.Required(CONF_COMMAND): cv.templatable(cv.string),
}
)
@automation.register_action(
"emontx.send_command",
EmonTxSendCommandAction,
EMONTX_SEND_COMMAND_ACTION_SCHEMA,
synchronous=True,
)
async def emontx_send_command_action_to_code(
config: ConfigType, action_id, template_arg, args
) -> None:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
template_ = await cg.templatable(config[CONF_COMMAND], args, cg.std_string)
cg.add(var.set_command(template_))
return var
+116
View File
@@ -0,0 +1,116 @@
#include "emontx.h"
#include "esphome/core/log.h"
#include "esphome/components/json/json_util.h"
namespace esphome::emontx {
static const char *const TAG = "emontx";
void EmonTx::setup() { this->buffer_pos_ = 0; }
/**
* @brief Implements the main loop for parsing data from the serial port.
*
* @details Continuously processes incoming UART data line-by-line:
* 1. Fire on_data callbacks for all received lines
* 2. If line starts with '{', parse as JSON and update sensors/callbacks
*/
void EmonTx::loop() {
// Read all available data to prevent UART buffer overflow
while (this->available() > 0) {
uint8_t received = this->read();
if (received == '\r') {
continue; // Ignore CR
} else if (received == '\n') {
// End of line - process the buffer
if (this->buffer_pos_ > 0) {
// Null-terminate for safe logging and c_str() use
size_t len = this->buffer_pos_;
this->buffer_[len] = '\0';
this->buffer_pos_ = 0;
StringRef line(this->buffer_.data(), len);
ESP_LOGD(TAG, "Received line: %s", line.c_str());
// Fire data callbacks for all received lines
this->data_callbacks_.call(line);
// Check if this line is JSON (starts with '{')
if (this->buffer_[0] == '{') {
ESP_LOGV(TAG, "Line is JSON, parsing...");
this->parse_json_(this->buffer_.data(), len);
}
}
} else if (this->buffer_pos_ >= MAX_LINE_LENGTH) {
ESP_LOGW(TAG, "Buffer overflow (>%zu bytes), discarding buffer", MAX_LINE_LENGTH);
this->buffer_pos_ = 0;
} else {
this->buffer_[this->buffer_pos_++] = static_cast<char>(received);
}
}
}
void EmonTx::parse_json_(const char *data, size_t len) {
bool success = json::parse_json(reinterpret_cast<const uint8_t *>(data), len, [this, data, len](JsonObject root) {
#ifdef USE_SENSOR
for (auto &sensor_pair : this->sensors_) {
auto val = root[sensor_pair.first];
if (val.is<JsonVariant>()) {
float value = val;
ESP_LOGV(TAG, "Updating sensor '%s' with value: %.2f", sensor_pair.first, value);
sensor_pair.second->publish_state(value);
}
}
#endif
this->json_callbacks_.call(root, StringRef(data, len));
return true;
});
if (!success) {
ESP_LOGW(TAG, "Failed to parse JSON");
}
}
/**
* @brief Logs the EmonTx component configuration details.
*/
void EmonTx::dump_config() {
ESP_LOGCONFIG(TAG, "EmonTx:");
#ifdef USE_SENSOR
ESP_LOGCONFIG(TAG, " Registered sensors: %zu", this->sensors_.size());
for (const auto &sensor_pair : this->sensors_) {
ESP_LOGCONFIG(TAG, " Sensor: %s", sensor_pair.first);
}
#else
ESP_LOGCONFIG(TAG, " Sensor support: DISABLED");
#endif
}
/**
* @brief Sends a command string to the emonTx device via UART.
*
* @param command The command string to send (LF will be appended automatically).
*/
void EmonTx::send_command(const std::string &command) {
ESP_LOGD(TAG, "Sending command to emonTx: %s", command.c_str());
this->write_str(command.c_str());
this->write_byte('\n');
}
#ifdef USE_SENSOR
/**
* @brief Registers a sensor to receive updates for a specific JSON tag.
*
* @param tag_name The JSON key to monitor for this sensor (must be a string literal).
* @param sensor Pointer to the sensor that will receive value updates.
*/
void EmonTx::register_sensor(const char *tag_name, sensor::Sensor *sensor) {
ESP_LOGCONFIG(TAG, "Registering sensor for tag: %s", tag_name);
this->sensors_.emplace_back(tag_name, sensor);
}
#endif
} // namespace esphome::emontx
+69
View File
@@ -0,0 +1,69 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/automation.h"
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
#include "esphome/components/uart/uart.h"
#include "esphome/components/json/json_util.h"
#include <array>
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
namespace esphome::emontx {
/// Maximum line length in bytes (plus one byte reserved for null terminator)
static constexpr size_t MAX_LINE_LENGTH = 1024;
/**
* @class EmonTx
* @brief Main class for the EmonTx component.
*
* The EmonTx processes incoming data frames via UART,
* extracts tags and values, and publishes them to registered sensors.
*/
class EmonTx : public Component, public uart::UARTDevice {
public:
EmonTx() = default;
void loop() override;
void setup() override;
void dump_config() override;
template<typename F> void add_on_json_callback(F &&callback) { this->json_callbacks_.add(std::forward<F>(callback)); }
template<typename F> void add_on_data_callback(F &&callback) { this->data_callbacks_.add(std::forward<F>(callback)); }
// Send command to emonTx via UART
void send_command(const std::string &command);
#ifdef USE_SENSOR
void init_sensors(size_t count) { this->sensors_.init(count); }
void register_sensor(const char *tag_name, sensor::Sensor *sensor);
#endif
protected:
void parse_json_(const char *data, size_t len);
#ifdef USE_SENSOR
FixedVector<std::pair<const char *, sensor::Sensor *>> sensors_{};
#endif
LazyCallbackManager<void(JsonObject, StringRef)> json_callbacks_;
LazyCallbackManager<void(StringRef)> data_callbacks_;
uint16_t buffer_pos_{0};
std::array<char, MAX_LINE_LENGTH + 1> buffer_{};
};
// Action to send command to emonTx
template<typename... Ts> class EmonTxSendCommandAction : public Action<Ts...>, public Parented<EmonTx> {
public:
TEMPLATABLE_VALUE(std::string, command)
void play(const Ts &...x) override { this->parent_->send_command(this->command_.value(x...)); }
};
} // namespace esphome::emontx
@@ -0,0 +1,133 @@
import esphome.codegen as cg
from esphome.components import sensor
import esphome.config_validation as cv
from esphome.const import (
CONF_ACCURACY_DECIMALS,
CONF_DEVICE_CLASS,
CONF_ID,
CONF_STATE_CLASS,
CONF_UNIT_OF_MEASUREMENT,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLTAGE,
STATE_CLASS_MEASUREMENT,
STATE_CLASS_TOTAL_INCREASING,
UNIT_AMPERE,
UNIT_CELSIUS,
UNIT_EMPTY,
UNIT_PULSES,
UNIT_VOLT,
UNIT_WATT,
UNIT_WATT_HOURS,
)
from esphome.types import ConfigType
from .. import CONF_EMONTX_ID, CONF_TAG_NAME, EmonTx, emontx_ns
EmonTxSensor = emontx_ns.class_("EmonTxSensor", sensor.Sensor, cg.Component)
# Define sensor type configurations by prefix
SENSOR_CONFIGS = {
"P": {
CONF_UNIT_OF_MEASUREMENT: UNIT_WATT,
CONF_DEVICE_CLASS: DEVICE_CLASS_POWER,
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
CONF_ACCURACY_DECIMALS: 0,
},
"E": {
CONF_UNIT_OF_MEASUREMENT: UNIT_WATT_HOURS,
CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
CONF_STATE_CLASS: STATE_CLASS_TOTAL_INCREASING,
CONF_ACCURACY_DECIMALS: 0,
},
"V": {
CONF_UNIT_OF_MEASUREMENT: UNIT_VOLT,
CONF_DEVICE_CLASS: DEVICE_CLASS_VOLTAGE,
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
CONF_ACCURACY_DECIMALS: 2,
},
"I": {
CONF_UNIT_OF_MEASUREMENT: UNIT_AMPERE,
CONF_DEVICE_CLASS: DEVICE_CLASS_CURRENT,
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
CONF_ACCURACY_DECIMALS: 2,
},
"T": {
CONF_UNIT_OF_MEASUREMENT: UNIT_CELSIUS,
CONF_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE,
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
CONF_ACCURACY_DECIMALS: 2,
},
}
# Pattern-based configurations
PATTERN_CONFIGS = {
"PULSE": {
CONF_UNIT_OF_MEASUREMENT: UNIT_PULSES,
CONF_DEVICE_CLASS: DEVICE_CLASS_ENERGY,
CONF_ACCURACY_DECIMALS: 0,
},
"PF": {
CONF_UNIT_OF_MEASUREMENT: UNIT_EMPTY,
CONF_DEVICE_CLASS: DEVICE_CLASS_POWER_FACTOR,
CONF_STATE_CLASS: STATE_CLASS_MEASUREMENT,
CONF_ACCURACY_DECIMALS: 2,
},
}
# Create a base schema that's flexible for any tag
BASE_SCHEMA = sensor.sensor_schema(
EmonTxSensor,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=0,
).extend(
{
cv.GenerateID(CONF_EMONTX_ID): cv.use_id(EmonTx),
cv.Required(CONF_TAG_NAME): cv.string,
}
)
def apply_tag_defaults(config: ConfigType) -> ConfigType:
"""Apply defaults based on tag prefix if applicable, but don't restrict any tags."""
tag = config[CONF_TAG_NAME]
# Skip if tag is too short
if len(tag) < 2:
return config
# Check if this tag starts with a known prefix
tag_upper = tag.upper()
for pattern, pattern_config in PATTERN_CONFIGS.items():
if tag_upper.startswith(pattern):
# Apply pattern defaults if not overridden by user
for key, value in pattern_config.items():
if key not in config:
config[key] = value
return config
# Only apply defaults for known prefixes with numeric indices
prefix = tag_upper[0]
if prefix in SENSOR_CONFIGS and len(tag) > 1 and tag[1:].isdigit():
# Apply defaults for known tag types, but only if not overridden by user
defaults = SENSOR_CONFIGS[prefix]
for key, value in defaults.items():
if key not in config:
config[key] = value
return config
CONFIG_SCHEMA = cv.All(BASE_SCHEMA, apply_tag_defaults)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
hub = await cg.get_variable(config[CONF_EMONTX_ID])
cg.add(hub.register_sensor(config[CONF_TAG_NAME], var))
@@ -0,0 +1,10 @@
#include "emontx_sensor.h"
#include "esphome/core/log.h"
namespace esphome::emontx {
static const char *const TAG = "emontx_sensor";
void EmonTxSensor::dump_config() { LOG_SENSOR(" ", "EmonTx Sensor", this); }
} // namespace esphome::emontx
@@ -0,0 +1,13 @@
#pragma once
#include "esphome/components/sensor/sensor.h"
#include "esphome/core/component.h"
namespace esphome::emontx {
class EmonTxSensor : public sensor::Sensor, public Component {
public:
void dump_config() override;
};
} // namespace esphome::emontx
@@ -24,7 +24,6 @@ CODEOWNERS = ["@vincentscode", "@latonita"]
ens160_ns = cg.esphome_ns.namespace("ens160_base")
CONF_AQI = "aqi"
UNIT_INDEX = "index"
CONFIG_SCHEMA_BASE = cv.Schema(
{
+1 -13
View File
@@ -175,9 +175,7 @@ async def to_code(config):
*model.get_constructor_args(config),
)
# Rotation is handled by setting the transform
display_config = {k: v for k, v in config.items() if k != CONF_ROTATION}
await display.register_display(var, display_config)
await display.register_display(var, config)
await spi.register_spi_device(var, config, write_only=True)
dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
@@ -201,16 +199,6 @@ async def to_code(config):
transform[CONF_SWAP_XY] = False
else:
transform = {x: model.get_default(x, False) for x in TRANSFORM_OPTIONS}
rotation = config[CONF_ROTATION]
if rotation == 180:
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
elif rotation == 90:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_X] = not transform[CONF_MIRROR_X]
elif rotation == 270:
transform[CONF_SWAP_XY] = not transform[CONF_SWAP_XY]
transform[CONF_MIRROR_Y] = not transform[CONF_MIRROR_Y]
transform_str = "|".join(
{
str(getattr(Transform, x.upper()))
+20 -3
View File
@@ -97,6 +97,23 @@ bool EPaperBase::reset() {
return true;
}
void EPaperBase::update_effective_transform_() {
switch (this->rotation_) {
case DISPLAY_ROTATION_90_DEGREES:
this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_X);
break;
case DISPLAY_ROTATION_180_DEGREES:
this->effective_transform_ = this->transform_ ^ (MIRROR_Y | MIRROR_X);
break;
case DISPLAY_ROTATION_270_DEGREES:
this->effective_transform_ = this->transform_ ^ (SWAP_XY | MIRROR_Y);
break;
default:
this->effective_transform_ = this->transform_;
break;
}
}
void EPaperBase::update() {
if (this->state_ != EPaperState::IDLE) {
ESP_LOGE(TAG, "Display already in state %s", epaper_state_to_string_());
@@ -280,11 +297,11 @@ bool EPaperBase::initialise(bool partial) {
bool EPaperBase::rotate_coordinates_(int &x, int &y) {
if (!this->get_clipping().inside(x, y))
return false;
if (this->transform_ & SWAP_XY)
if (this->effective_transform_ & SWAP_XY)
std::swap(x, y);
if (this->transform_ & MIRROR_X)
if (this->effective_transform_ & MIRROR_X)
x = this->width_ - x - 1;
if (this->transform_ & MIRROR_Y)
if (this->effective_transform_ & MIRROR_Y)
y = this->height_ - y - 1;
if (x >= this->width_ || y >= this->height_ || x < 0 || y < 0)
return false;
+16 -5
View File
@@ -1,6 +1,6 @@
#pragma once
#include "esphome/components/display/display_buffer.h"
#include "esphome/components/display/display.h"
#include "esphome/components/spi/spi.h"
#include "esphome/components/split_buffer/split_buffer.h"
#include "esphome/core/component.h"
@@ -51,7 +51,14 @@ class EPaperBase : public Display,
void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
void set_transform(uint8_t transform) { this->transform_ = transform; }
void set_transform(uint8_t transform) {
this->transform_ = transform;
this->update_effective_transform_();
}
void set_rotation(DisplayRotation rotation) override {
Display::set_rotation(rotation);
this->update_effective_transform_();
}
void set_full_update_every(uint8_t full_update_every) { this->full_update_every_ = full_update_every; }
void dump_config() override;
@@ -103,12 +110,14 @@ class EPaperBase : public Display,
this->fill(COLOR_ON);
}
int get_width() override { return this->effective_transform_ & SWAP_XY ? this->height_ : this->width_; }
int get_height() override { return this->effective_transform_ & SWAP_XY ? this->width_ : this->height_; }
void draw_pixel_at(int x, int y, Color color) override;
protected:
int get_height_internal() override { return this->height_; };
int get_width_internal() override { return this->width_; };
int get_width() override { return this->transform_ & SWAP_XY ? this->height_ : this->width_; }
int get_height() override { return this->transform_ & SWAP_XY ? this->width_ : this->height_; }
void draw_pixel_at(int x, int y, Color color) override;
bool is_using_partial_update_() const { return this->full_update_every_ > 1; }
void process_state_();
const char *epaper_state_to_string_();
@@ -119,6 +128,7 @@ class EPaperBase : public Display,
void send_init_sequence_(const uint8_t *sequence, size_t length);
void wait_for_idle_(bool should_wait);
bool init_buffer_(size_t buffer_length);
void update_effective_transform_();
bool rotate_coordinates_(int &x, int &y);
/**
@@ -171,6 +181,7 @@ class EPaperBase : public Display,
uint32_t delay_until_{}; // timestamp until which to delay processing
uint16_t next_delay_{}; // milliseconds to delay before next state
uint8_t transform_{};
uint8_t effective_transform_{};
uint8_t update_count_{};
// these values represent the bounds of the updated buffer. Note that x_high and y_high
// point to the pixel past the last one updated, i.e. may range up to width/height.
@@ -15,7 +15,11 @@ void EPaperMono::refresh_screen(bool partial) {
void EPaperMono::deep_sleep() {
ESP_LOGV(TAG, "Deep sleep");
this->command(0x10);
if (this->is_using_partial_update_()) {
this->cmd_data(0x10, {0x00}); // sleep in power on mode
} else {
this->cmd_data(0x10, {0x03}); // deep sleep
}
}
bool EPaperMono::reset() {
@@ -27,6 +31,14 @@ bool EPaperMono::reset() {
}
void EPaperMono::set_window() {
// if not using partial update, the display will go into deep sleep, so must rewrite entire
// buffer since the display RAM will not retain contents
if (!this->is_using_partial_update_()) {
this->x_low_ = 0;
this->x_high_ = this->width_;
this->y_low_ = 0;
this->y_high_ = this->height_;
}
// round x-coordinates to byte boundaries
this->x_low_ &= ~7;
this->x_high_ += 7;
+110 -2
View File
@@ -44,11 +44,12 @@ from esphome.const import (
__version__,
)
from esphome.core import CORE, HexInt
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.coroutine import CoroPriority, coroutine_with_priority
import esphome.final_validate as fv
from esphome.helpers import copy_file_if_changed, rmtree, write_file_if_changed
from esphome.types import ConfigType
from esphome.writer import clean_cmake_cache
from esphome.writer import clean_build, clean_cmake_cache
from .boards import BOARDS, STANDARD_BOARDS
from .const import ( # noqa
@@ -97,8 +98,12 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
CONF_MINIMUM_CHIP_REVISION = "minimum_chip_revision"
CONF_RELEASE = "release"
CONF_SIGNED_OTA_VERIFICATION = "signed_ota_verification"
CONF_SIGNING_KEY = "signing_key"
CONF_SIGNING_SCHEME = "signing_scheme"
CONF_SRAM1_AS_IRAM = "sram1_as_iram"
CONF_SUBTYPE = "subtype"
CONF_VERIFICATION_KEY = "verification_key"
ARDUINO_FRAMEWORK_NAME = "framework-arduinoespressif32"
ARDUINO_FRAMEWORK_PKG = f"pioarduino/{ARDUINO_FRAMEWORK_NAME}"
@@ -120,6 +125,27 @@ ASSERTION_LEVELS = {
"SILENT": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_SILENT",
}
SIGNING_SCHEMES = {
"rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME",
"ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME",
}
# Chip variants that only support one signing scheme for Secure Boot V2.
# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h.
# Variants not listed in either set support both RSA and ECDSA
# (e.g. C5, C6, H2, P4). New variants should be added to the
# appropriate set if they only support one scheme.
SIGNED_OTA_RSA_ONLY_VARIANTS = {
VARIANT_ESP32,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C3,
}
SIGNED_OTA_ECC_ONLY_VARIANTS = {
VARIANT_ESP32C2,
VARIANT_ESP32C61,
}
COMPILER_OPTIMIZATIONS = {
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
"NONE": "CONFIG_COMPILER_OPTIMIZATION_NONE",
@@ -962,6 +988,47 @@ def final_validate(config):
)
# disable the rollback feature anyway since it can't be used.
advanced[CONF_ENABLE_OTA_ROLLBACK] = False
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
scheme = signed_ota[CONF_SIGNING_SCHEME]
variant = config[CONF_VARIANT]
scheme_variant_conflicts = {
"ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"),
"rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"),
}
if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[
0
]:
errs.append(
cv.Invalid(
f"Signing scheme '{scheme}' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
path=[
CONF_FRAMEWORK,
CONF_ADVANCED,
CONF_SIGNED_OTA_VERIFICATION,
CONF_SIGNING_SCHEME,
],
)
)
if CONF_OTA not in full_config:
_LOGGER.warning(
"Signed OTA verification is enabled but no OTA component is configured. "
"The initial firmware will be signed but OTA updates won't be possible "
"until an OTA component is added."
)
if CONF_SIGNING_KEY in signed_ota:
_LOGGER.info(
"Signed OTA verification is enabled. Keep your signing key safe! "
"If you lose the signing key, you will NOT be able to OTA update "
"devices running firmware signed with this key. "
"Without the key, you'll need to reflash via serial."
)
else:
_LOGGER.info(
"Signed OTA verification is configured with a public verification key. "
"Binaries will NOT be signed automatically during build. "
"You must sign them externally before flashing."
)
if errs:
raise cv.MultipleInvalid(errs)
@@ -1173,6 +1240,18 @@ FRAMEWORK_SCHEMA = cv.Schema(
min=8192, max=32768
),
cv.Optional(CONF_ENABLE_OTA_ROLLBACK, default=True): cv.boolean,
cv.Optional(CONF_SIGNED_OTA_VERIFICATION): cv.All(
cv.Schema(
{
cv.Optional(CONF_SIGNING_KEY): cv.file_,
cv.Optional(CONF_VERIFICATION_KEY): cv.file_,
cv.Optional(
CONF_SIGNING_SCHEME, default="rsa3072"
): cv.one_of(*SIGNING_SCHEMES, lower=True),
}
),
cv.has_exactly_one_key(CONF_SIGNING_KEY, CONF_VERIFICATION_KEY),
),
cv.Optional(
CONF_USE_FULL_CERTIFICATE_BUNDLE, default=False
): cv.boolean,
@@ -1325,7 +1404,9 @@ CONF_PARTITIONS = "partitions"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_BOARD): cv.string_strict,
cv.Optional(CONF_BOARD): cv.All(
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
),
cv.Optional(CONF_CPU_FREQUENCY): cv.one_of(
*FULL_CPU_FREQUENCIES, upper=True
),
@@ -1878,6 +1959,32 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE", True)
cg.add_define("USE_OTA_ROLLBACK")
# Enable signed app verification without hardware secure boot
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT", True)
add_idf_sdkconfig_option("CONFIG_SECURE_SIGNED_ON_UPDATE_NO_SECURE_BOOT", True)
scheme = signed_ota[CONF_SIGNING_SCHEME]
for key, flag in SIGNING_SCHEMES.items():
add_idf_sdkconfig_option(flag, scheme == key)
if CONF_SIGNING_KEY in signed_ota:
# Private key mode — auto-sign binaries during build
add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", True)
add_idf_sdkconfig_option(
"CONFIG_SECURE_BOOT_SIGNING_KEY",
str(signed_ota[CONF_SIGNING_KEY].resolve()),
)
else:
# Public key mode — verification only, external signing required
add_idf_sdkconfig_option("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES", False)
add_idf_sdkconfig_option(
"CONFIG_SECURE_BOOT_VERIFICATION_KEY",
str(signed_ota[CONF_VERIFICATION_KEY].resolve()),
)
cg.add_define("USE_OTA_SIGNED_VERIFICATION")
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
cg.add_define(
@@ -2195,6 +2302,7 @@ def _write_sdkconfig():
if write_file_if_changed(internal_path, contents):
# internal changed, update real one
write_file_if_changed(sdk_path, contents)
clean_build(clear_pio_cache=False)
def _write_idf_component_yml():
+4 -4
View File
@@ -26,8 +26,8 @@ _LOGGER = logging.getLogger(__name__)
def esp32_c6_validate_gpio_pin(value: int) -> int:
if value < 0 or value > 23:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-23)")
if value < 0 or value > 30:
raise cv.Invalid(f"Invalid pin number: {value} (must be 0-30)")
if value in _ESP32C6_SPI_PSRAM_PINS:
raise cv.Invalid(
f"This pin cannot be used on ESP32-C6s and is already used by the SPI/PSRAM interface (function: {_ESP32C6_SPI_PSRAM_PINS[value]})"
@@ -47,8 +47,8 @@ def esp32_c6_validate_supports(value: dict[str, Any]) -> dict[str, Any]:
mode = value[CONF_MODE]
is_input = mode[CONF_INPUT]
if num < 0 or num > 23:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-23)")
if num < 0 or num > 30:
raise cv.Invalid(f"Invalid pin number: {num} (must be 0-30)")
if is_input:
# All ESP32 pins support input mode
pass
+95 -1
View File
@@ -8,6 +8,99 @@ import shutil # noqa: E402
from glob import glob # noqa: E402
def _parse_sdkconfig(sdkconfig_path):
"""Parse sdkconfig file and return a dict of CONFIG_ options."""
options = {}
try:
for line in sdkconfig_path.read_text().splitlines():
line = line.strip()
if line and not line.startswith("#") and "=" in line:
key, _, value = line.partition("=")
# Strip surrounding quotes from string values
if value.startswith('"') and value.endswith('"'):
value = value[1:-1]
options[key] = value
except FileNotFoundError:
pass
return options
def sign_firmware(source, target, env):
"""
Sign the firmware binary using espsecure.py if signed OTA verification is enabled.
Reads signing configuration from sdkconfig.
"""
build_dir = pathlib.Path(env.subst("$BUILD_DIR"))
project_dir = pathlib.Path(env.subst("$PROJECT_DIR"))
pioenv = env.subst("$PIOENV")
sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}")
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_NO_SECURE_BOOT") != "y":
return
if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") != "y":
print("Signed OTA verification enabled but build-time signing disabled.")
print("You must sign the firmware externally before flashing.")
return
signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY")
if not signing_key:
print("Error: CONFIG_SECURE_BOOT_SIGNING_KEY not set in sdkconfig")
env.Exit(1)
return
signing_key_path = pathlib.Path(signing_key)
if not signing_key_path.exists():
print(f"Error: Signing key not found: {signing_key_path}")
env.Exit(1)
return
# ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes),
# so the espsecure signature version is always 2.
sign_version = "2"
firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
firmware_path = build_dir / firmware_name
if not firmware_path.exists():
print(f"Error: Firmware binary not found: {firmware_path}")
env.Exit(1)
return
python_exe = f'"{env.subst("$PYTHONEXE")}"'
unsigned_path = firmware_path.with_suffix(".unsigned.bin")
# Keep a copy of the unsigned binary
shutil.copyfile(str(firmware_path), str(unsigned_path))
cmd = [
python_exe,
"-m",
"espsecure",
"sign-data",
"--version",
sign_version,
"--keyfile",
str(signing_key_path),
"--output",
str(firmware_path),
str(unsigned_path),
]
print(f"Signing firmware with key: {signing_key_path.name}")
result = env.Execute(
env.VerboseAction(" ".join(cmd), "Signing firmware with espsecure")
)
if result == 0:
print("Successfully signed firmware")
else:
print(f"Error: espsecure sign_data failed with code {result}")
# Restore unsigned binary on failure
shutil.copyfile(str(unsigned_path), str(firmware_path))
env.Exit(1)
def merge_factory_bin(source, target, env):
"""
Merges all flash sections into a single .factory.bin using esptool.
@@ -124,7 +217,8 @@ def esp32_copy_ota_bin(source, target, env):
print(f"Copied firmware to {new_file_name}")
# Run merge first, then ota copy second
# Run signing first, then merge, then ota copy
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", esp32_copy_ota_bin) # noqa: F821
+3 -9
View File
@@ -7,7 +7,6 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components import socket
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
from esphome.components.esp32.const import VARIANT_ESP32C2
import esphome.config_validation as cv
@@ -374,14 +373,14 @@ def bt_uuid(value):
value = in_value.upper()
if len(value) == len(bt_uuid16_format):
pattern = re.compile("^[A-F|0-9]{4,}$")
pattern = re.compile("^[A-F0-9]{4,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 16 bit UUID format: '{in_value}'"
)
return value
if len(value) == len(bt_uuid32_format):
pattern = re.compile("^[A-F|0-9]{8,}$")
pattern = re.compile("^[A-F0-9]{8,}$")
if not pattern.match(value):
raise cv.Invalid(
f"Invalid hexadecimal value for 32 bit UUID format: '{in_value}'"
@@ -389,7 +388,7 @@ def bt_uuid(value):
return value
if len(value) == len(bt_uuid128_format):
pattern = re.compile(
"^[A-F|0-9]{8,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{4,}-[A-F|0-9]{12,}$"
"^[A-F0-9]{8,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{4,}-[A-F0-9]{12,}$"
)
if not pattern.match(value):
raise cv.Invalid(
@@ -592,11 +591,6 @@ async def to_code(config):
cg.add(var.set_name(name))
await cg.register_component(var, config)
# BLE uses the socket wake_loop_threadsafe() mechanism to wake the main loop from BLE tasks
# This enables low-latency (~12μs) BLE event processing instead of waiting for
# select() timeout (0-16ms). The wake socket is shared across all components.
socket.require_wake_loop_threadsafe()
# Define max connections for use in C++ code (e.g., ble_server.h)
max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
+14 -15
View File
@@ -399,8 +399,17 @@ void ESP32BLE::loop() {
return;
}
#ifdef USE_ESP32_BLE_ADVERTISING
if (this->advertising_ != nullptr) {
this->advertising_->loop();
}
#endif
BLEEvent *ble_event = this->ble_events_.pop();
while (ble_event != nullptr) {
if (ble_event == nullptr)
return;
do {
switch (ble_event->type_) {
#if defined(USE_ESP32_BLE_SERVER) && defined(ESPHOME_ESP32_BLE_GATTS_EVENT_HANDLER_COUNT)
case BLEEvent::GATTS: {
@@ -488,15 +497,11 @@ void ESP32BLE::loop() {
}
// Return the event to the pool
this->ble_event_pool_.release(ble_event);
ble_event = this->ble_events_.pop();
}
#ifdef USE_ESP32_BLE_ADVERTISING
if (this->advertising_ != nullptr) {
this->advertising_->loop();
}
#endif
} while ((ble_event = this->ble_events_.pop()) != nullptr);
// Log dropped events periodically
// Log dropped events - only reachable when events were processed.
// Drops only occur when the queue is full, and only this loop drains it,
// so if pop() returned nullptr above we can skip this check (saves a memw).
uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped);
@@ -594,9 +599,7 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
GAP_SECURITY_EVENTS:
enqueue_ble_event(event, param);
// Wake up main loop to process security event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
return;
// Ignore these GAP events as they are not relevant for our use case
@@ -617,9 +620,7 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
esp_ble_gatts_cb_param_t *param) {
enqueue_ble_event(event, gatts_if, param);
// Wake up main loop to process GATT event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
}
#endif
@@ -628,9 +629,7 @@ void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gat
esp_ble_gattc_cb_param_t *param) {
enqueue_ble_event(event, gattc_if, param);
// Wake up main loop to process GATT event immediately
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
App.wake_loop_threadsafe();
#endif
}
#endif
@@ -10,11 +10,7 @@ AUTO_LOAD = ["esp32_ble"]
DEPENDENCIES = ["esp32"]
esp32_ble_beacon_ns = cg.esphome_ns.namespace("esp32_ble_beacon")
ESP32BLEBeacon = esp32_ble_beacon_ns.class_(
"ESP32BLEBeacon",
cg.Component,
cg.Parented.template(esp32_ble.ESP32BLE),
)
ESP32BLEBeacon = esp32_ble_beacon_ns.class_("ESP32BLEBeacon", cg.Component)
CONF_MAJOR = "major"
CONF_MINOR = "minor"
CONF_MIN_INTERVAL = "min_interval"
@@ -35,7 +35,7 @@ using esp_ble_ibeacon_t = struct {
using namespace esp32_ble;
class ESP32BLEBeacon : public Component, public Parented<ESP32BLE> {
class ESP32BLEBeacon : public Component {
public:
explicit ESP32BLEBeacon(const std::array<uint8_t, 16> &uuid) : uuid_(uuid) {}
@@ -307,24 +307,30 @@ def final_validate_config(config):
# Check if all characteristics that require notifications have the notify property set
for char_id in CORE.data.get(DOMAIN, {}).get(KEY_NOTIFY_REQUIRED, set()):
# Look for the characteristic in the configuration
char_config = [
matches = [
char_conf
for service_conf in config[CONF_SERVICES]
for char_conf in service_conf[CONF_CHARACTERISTICS]
if char_conf[CONF_ID] == char_id
][0]
]
if not matches:
continue
char_config = matches[0]
if not char_config[CONF_NOTIFY]:
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has notify actions and the {CONF_NOTIFY} property is not set"
)
for char_id in CORE.data.get(DOMAIN, {}).get(KEY_SET_VALUE, set()):
# Look for the characteristic in the configuration
char_config = [
matches = [
char_conf
for service_conf in config[CONF_SERVICES]
for char_conf in service_conf[CONF_CHARACTERISTICS]
if char_conf[CONF_ID] == char_id
][0]
]
if not matches:
continue
char_config = matches[0]
if isinstance(char_config.get(CONF_VALUE, {}).get(CONF_DATA), cv.Lambda):
raise cv.Invalid(
f"Characteristic {char_config[CONF_UUID]} has both a set_value action and a templated value"
@@ -378,7 +378,8 @@ async def esp32_ble_tracker_start_scan_action_to_code(
):
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
cg.add(var.set_continuous(config[CONF_CONTINUOUS]))
template_ = await cg.templatable(config[CONF_CONTINUOUS], args, cg.bool_)
cg.add(var.set_continuous(template_))
return var
+2 -3
View File
@@ -2,7 +2,7 @@ import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components import i2c, socket
from esphome.components import i2c
from esphome.components.esp32 import add_idf_component, add_idf_sdkconfig_option
from esphome.components.psram import DOMAIN as psram_domain
import esphome.config_validation as cv
@@ -29,7 +29,7 @@ from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
AUTO_LOAD = ["camera", "socket"]
AUTO_LOAD = ["camera"]
DEPENDENCIES = ["esp32"]
esp32_camera_ns = cg.esphome_ns.namespace("esp32_camera")
@@ -370,7 +370,6 @@ SETTERS = {
async def to_code(config):
cg.add_define("USE_CAMERA")
socket.require_wake_loop_threadsafe()
var = cg.new_Pvariable(config[CONF_ID])
await setup_entity(var, config, "camera")
await cg.register_component(var, config)
@@ -521,11 +521,9 @@ void ESP32Camera::framebuffer_task(void *pv) {
camera_fb_t *framebuffer = esp_camera_fb_get();
xQueueSend(that->framebuffer_get_queue_, &framebuffer, portMAX_DELAY);
// Only wake the main loop if there's a pending request to consume the frame
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
if (that->has_requested_image_()) {
App.wake_loop_threadsafe();
}
#endif
// return is no-op for config with 1 fb
xQueueReceive(that->framebuffer_return_queue_, &framebuffer, portMAX_DELAY);
esp_camera_fb_return(framebuffer);
+207 -57
View File
@@ -4,95 +4,245 @@ from pathlib import Path
from esphome import pins
from esphome.components import esp32
import esphome.config_validation as cv
from esphome.const import CONF_CLK_PIN, CONF_RESET_PIN, CONF_VARIANT
from esphome.const import (
CONF_CLK_PIN,
CONF_CS_PIN,
CONF_FREQUENCY,
CONF_MISO_PIN,
CONF_MOSI_PIN,
CONF_RESET_PIN,
CONF_TYPE,
CONF_VARIANT,
)
from esphome.cpp_generator import add_define
CODEOWNERS = ["@swoboda1337"]
CONF_ACTIVE_HIGH = "active_high"
CONF_BUS_WIDTH = "bus_width"
CONF_CMD_PIN = "cmd_pin"
CONF_D0_PIN = "d0_pin"
CONF_D1_PIN = "d1_pin"
CONF_D2_PIN = "d2_pin"
CONF_D3_PIN = "d3_pin"
CONF_SLOT = "slot"
CONF_DATA_READY_ACTIVE_HIGH = "data_ready_active_high"
CONF_DATA_READY_PIN = "data_ready_pin"
CONF_HANDSHAKE_ACTIVE_HIGH = "handshake_active_high"
CONF_HANDSHAKE_PIN = "handshake_pin"
CONF_SDIO_FREQUENCY = "sdio_frequency"
CONF_SLOT = "slot"
CONF_SPI_MODE = "spi_mode"
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True),
cv.Required(CONF_ACTIVE_HIGH): cv.boolean,
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_D1_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_D2_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_D3_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1),
cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All(
cv.frequency, cv.Range(min=400e3, max=50e6)
),
}
),
# Shared fields for both transport modes
BASE_SCHEMA = cv.Schema(
{
cv.Required(CONF_VARIANT): cv.one_of(*esp32.VARIANTS, upper=True),
cv.Required(CONF_ACTIVE_HIGH): cv.boolean,
cv.Required(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
}
)
SDIO_SCHEMA = BASE_SCHEMA.extend(
{
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_CMD_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_D0_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_D1_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_D2_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_D3_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_BUS_WIDTH, default=4): cv.one_of(1, 4, int=True),
cv.Optional(CONF_SLOT, default=1): cv.int_range(min=0, max=1),
cv.Optional(CONF_SDIO_FREQUENCY, default="40MHz"): cv.All(
cv.frequency, cv.Range(min=400e3, max=50e6)
),
}
)
async def to_code(config):
add_define("USE_ESP32_HOSTED")
if config[CONF_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_HIGH",
True,
def _validate_sdio(config):
if config[CONF_BUS_WIDTH] == 4:
for pin in (CONF_D1_PIN, CONF_D2_PIN, CONF_D3_PIN):
if pin not in config:
raise cv.Invalid(
f"{pin} is required when bus_width is 4",
path=[pin],
)
return config
# SPI variant-dependent defaults and limits
_SPI_VARIANT_DEFAULTS = {
"ESP32": {"spi_mode": 2, "frequency": 10, "max_frequency": 10},
"ESP32C6": {"spi_mode": 3, "frequency": 26, "max_frequency": 40},
}
_SPI_DEFAULT = {"spi_mode": 3, "frequency": 40, "max_frequency": 40}
SPI_SCHEMA = BASE_SCHEMA.extend(
{
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_HANDSHAKE_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_DATA_READY_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_SPI_MODE): cv.int_range(min=0, max=3),
cv.Optional(CONF_FREQUENCY): cv.All(cv.frequency, cv.Range(min=1e6, max=40e6)),
cv.Optional(CONF_HANDSHAKE_ACTIVE_HIGH, default=True): cv.boolean,
cv.Optional(CONF_DATA_READY_ACTIVE_HIGH, default=True): cv.boolean,
}
)
def _validate_spi(config):
variant = config[CONF_VARIANT]
defaults = _SPI_VARIANT_DEFAULTS.get(variant, _SPI_DEFAULT)
if CONF_SPI_MODE not in config:
config[CONF_SPI_MODE] = defaults["spi_mode"]
if CONF_FREQUENCY not in config:
config[CONF_FREQUENCY] = float(defaults["frequency"] * 1e6)
freq_mhz = int(config[CONF_FREQUENCY] // 1e6)
if freq_mhz > defaults["max_frequency"]:
raise cv.Invalid(
f"SPI frequency {freq_mhz}MHz exceeds maximum {defaults['max_frequency']}MHz for {variant}",
path=[CONF_FREQUENCY],
)
return config
CONFIG_SCHEMA = cv.typed_schema(
{
"sdio": cv.All(SDIO_SCHEMA, _validate_sdio),
"spi": cv.All(SPI_SCHEMA, _validate_spi),
},
default_type="sdio",
)
def _configure_sdio(config):
slot = config[CONF_SLOT]
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_SDIO_SLOT_{slot}",
True,
)
if config[CONF_BUS_WIDTH] == 1:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SDIO_1_BIT_BUS", True)
else:
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_RESET_ACTIVE_LOW",
True,
)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SDIO_4_BIT_BUS", True)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_GPIO_RESET_SLAVE", # NOLINT
config[CONF_RESET_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT
True,
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_SDIO_SLOT_{config[CONF_SLOT]}",
True,
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{config[CONF_SLOT]}",
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CLK_SLOT_{slot}",
config[CONF_CLK_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{config[CONF_SLOT]}",
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_CMD_SLOT_{slot}",
config[CONF_CMD_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{config[CONF_SLOT]}",
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D0_SLOT_{slot}",
config[CONF_D0_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{config[CONF_SLOT]}",
config[CONF_D1_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{config[CONF_SLOT]}",
config[CONF_D2_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{config[CONF_SLOT]}",
config[CONF_D3_PIN],
)
if config[CONF_BUS_WIDTH] == 4:
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D1_4BIT_BUS_SLOT_{slot}",
config[CONF_D1_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D2_4BIT_BUS_SLOT_{slot}",
config[CONF_D2_PIN],
)
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_PRIV_SDIO_PIN_D3_4BIT_BUS_SLOT_{slot}",
config[CONF_D3_PIN],
)
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_CUSTOM_SDIO_PINS", True)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SDIO_CLOCK_FREQ_KHZ",
int(config[CONF_SDIO_FREQUENCY] // 1000),
)
def _configure_spi(config):
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_HOST_INTERFACE", True)
# SPI mode is set via per-variant choice options
variant = config[CONF_VARIANT]
mode = config[CONF_SPI_MODE]
suffix = "ESP32" if variant == "ESP32" else "ESP32XX"
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_SPI_PRIV_MODE_{mode}_{suffix}",
True,
)
# Frequency is set via per-variant options
freq_mhz = int(config[CONF_FREQUENCY] // 1e6)
if variant == "ESP32":
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32", freq_mhz)
elif variant == "ESP32C6":
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32C6", freq_mhz)
else:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_SPI_FREQ_ESP32XX", freq_mhz)
# Pin configuration (use HSPI variant as P4/H2 hosts don't have VSPI)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_MOSI", config[CONF_MOSI_PIN]
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_MISO", config[CONF_MISO_PIN]
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_CLK", config[CONF_CLK_PIN]
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_HSPI_GPIO_CS", config[CONF_CS_PIN]
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_GPIO_HANDSHAKE", config[CONF_HANDSHAKE_PIN]
)
esp32.add_idf_sdkconfig_option(
"CONFIG_ESP_HOSTED_SPI_GPIO_DATA_READY", config[CONF_DATA_READY_PIN]
)
# Handshake and data_ready polarity
if config[CONF_HANDSHAKE_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_HS_ACTIVE_HIGH", True)
else:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_HS_ACTIVE_LOW", True)
if config[CONF_DATA_READY_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DR_ACTIVE_HIGH", True)
else:
esp32.add_idf_sdkconfig_option("CONFIG_ESP_HOSTED_DR_ACTIVE_LOW", True)
async def to_code(config):
add_define("USE_ESP32_HOSTED")
transport = config[CONF_TYPE]
transport_prefix = "SDIO" if transport == "sdio" else "SPI"
# Reset polarity
if config[CONF_ACTIVE_HIGH]:
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_{transport_prefix}_RESET_ACTIVE_HIGH", True
)
else:
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_{transport_prefix}_RESET_ACTIVE_LOW", True
)
# Reset GPIO
esp32.add_idf_sdkconfig_option(
f"CONFIG_ESP_HOSTED_{transport_prefix}_GPIO_RESET_SLAVE", # NOLINT
config[CONF_RESET_PIN],
)
# Slave variant # NOLINT
esp32.add_idf_sdkconfig_option(
f"CONFIG_SLAVE_IDF_TARGET_{config[CONF_VARIANT]}", # NOLINT
True,
)
# Transport-specific configuration
if transport == "sdio":
_configure_sdio(config)
else:
_configure_spi(config)
# Library versions
idf_ver = esp32.idf_version()
os.environ["ESP_IDF_VERSION"] = f"{idf_ver.major}.{idf_ver.minor}"
if idf_ver >= cv.Version(5, 5, 0):
+5 -1
View File
@@ -20,6 +20,7 @@ from esphome.const import (
ThreadModel,
)
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
from esphome.core.config import BOARD_MAX_LENGTH
from esphome.helpers import copy_file_if_changed
from esphome.types import ConfigType
@@ -203,7 +204,9 @@ BUILD_FLASH_MODES = ["qio", "qout", "dio", "dout"]
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_BOARD): cv.string_strict,
cv.Required(CONF_BOARD): cv.All(
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
),
cv.Optional(CONF_FRAMEWORK, default={}): ARDUINO_FRAMEWORK_SCHEMA,
cv.Optional(CONF_RESTORE_FROM_FLASH, default=False): cv.boolean,
cv.Optional(CONF_EARLY_PIN_INIT, default=True): cv.boolean,
@@ -233,6 +236,7 @@ async def to_code(config):
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
cg.add_define("ESPHOME_VARIANT", "ESP8266")
cg.add_define(ThreadModel.SINGLE)
cg.add_define("USE_ESP8266_CRASH_HANDLER")
enable_scanf_float = config.get(CONF_ENABLE_SCANF_FLOAT)
if enable_scanf_float is None and lambdas_use_scanf_float(CORE.config):
@@ -0,0 +1,235 @@
#ifdef USE_ESP8266
#include "esphome/core/defines.h"
#ifdef USE_ESP8266_CRASH_HANDLER
#include "crash_handler.h"
#include "esphome/core/log.h"
#include <cinttypes>
extern "C" {
#include <user_interface.h>
// Global reset info struct populated by SDK/Arduino core at boot
extern struct rst_info resetInfo;
}
// Xtensa windowed-ABI: bits[31:30] encode call type (CALL0=00, CALL4=01,
// CALL8=10, CALL12=11). Mask and force bit 30 to recover the real address.
static constexpr uint32_t XTENSA_ADDR_MASK = 0x3FFFFFFF;
static constexpr uint32_t XTENSA_CODE_BASE = 0x40000000;
// ESP8266 memory map boundaries for code regions
static constexpr uint32_t IRAM_START = 0x40100000;
static constexpr uint32_t IRAM_END = 0x40108000; // 32KB
// Linker symbols for the actual firmware IROM section.
// Using these instead of a conservative upper bound (0x40400000) prevents
// false positives from stale stack values beyond the actual flash mapping.
extern "C" {
// NOLINTBEGIN(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration)
extern void _irom0_text_start(void);
extern void _irom0_text_end(void);
// NOLINTEND(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration)
}
// Check if a value looks like a code address in IRAM or flash-mapped IROM.
// IRAM_ATTR as safety net — normally inlined into custom_crash_callback, but
// ensures correctness if the compiler ever chooses not to inline.
static inline bool IRAM_ATTR is_code_addr(uint32_t val) {
uint32_t addr = (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE;
return (addr >= IRAM_START && addr < IRAM_END) ||
(addr >= (uint32_t) _irom0_text_start && addr < (uint32_t) _irom0_text_end);
}
// Recover the actual code address from a windowed-ABI return address on the stack.
static inline uint32_t IRAM_ATTR recover_code_addr(uint32_t val) { return (val & XTENSA_ADDR_MASK) | XTENSA_CODE_BASE; }
// RTC user memory layout for crash backtrace data.
// User-accessible RTC memory: blocks 64-191 (each block = 4 bytes).
// We use blocks 174-191 (last 18 blocks, 72 bytes) to minimize conflicts.
// Store 16 raw candidates, filter to real return addresses at log time.
static constexpr uint8_t RTC_CRASH_BASE = 174;
static constexpr size_t MAX_BACKTRACE = 16;
// Magic word packs sentinel, version, and count into one uint32_t:
// bits[31:16] = sentinel
// bits[15:8] = version
// bits[7:0] = backtrace count
static constexpr uint8_t CRASH_SENTINEL_BITS = 16;
static constexpr uint8_t CRASH_VERSION_BITS = 8;
static constexpr uint16_t CRASH_SENTINEL_VALUE = 0xDEAD;
static constexpr uint8_t CRASH_VERSION_VALUE = 1;
static constexpr uint32_t CRASH_SENTINEL = static_cast<uint32_t>(CRASH_SENTINEL_VALUE) << CRASH_SENTINEL_BITS;
static constexpr uint32_t CRASH_VERSION = static_cast<uint32_t>(CRASH_VERSION_VALUE) << CRASH_VERSION_BITS;
static constexpr uint32_t CRASH_SENTINEL_MASK = static_cast<uint32_t>(0xFFFF) << CRASH_SENTINEL_BITS;
static constexpr uint32_t CRASH_VERSION_MASK = static_cast<uint32_t>(0xFF) << CRASH_VERSION_BITS;
static constexpr uint32_t CRASH_COUNT_MASK = 0xFF;
// Struct layout: 18 RTC blocks (72 bytes):
// [0] = magic (sentinel | version | count)
// [1..16] = up to 16 code addresses from stack scanning
// [17] = epc1 at crash time (to skip duplicates at log time)
struct RtcCrashData {
uint32_t magic;
uint32_t backtrace[MAX_BACKTRACE];
uint32_t epc1; // Fault PC, used to filter duplicates
};
static_assert(sizeof(RtcCrashData) == 72, "RtcCrashData must fit in 18 RTC blocks");
namespace esphome::esp8266 {
static const char *const TAG = "esp8266";
static inline bool is_crash_reason(uint32_t reason) {
return reason == REASON_WDT_RST || reason == REASON_EXCEPTION_RST || reason == REASON_SOFT_WDT_RST;
}
bool crash_handler_has_data() { return is_crash_reason(resetInfo.reason); }
// Xtensa exception cause names for the LX106 core (ESP8266).
// Only includes causes that can actually occur on the LX106 — it has no MMU,
// no TLB, no PIF, and no privilege levels, so causes 12-18 and 24-26 are
// impossible and omitted. The numeric cause is always logged as fallback.
// Uses if-else with LOG_STR to avoid CSWTCH jump tables (RAM on ESP8266).
static const LogString *get_exception_cause(uint32_t cause) {
if (cause == 0)
return LOG_STR("IllegalInst");
if (cause == 2)
return LOG_STR("InstFetchErr");
if (cause == 3)
return LOG_STR("LoadStoreErr");
if (cause == 4)
return LOG_STR("Level1Int");
if (cause == 6)
return LOG_STR("DivByZero");
if (cause == 9)
return LOG_STR("Alignment");
if (cause == 20)
return LOG_STR("InstFetchProhibit");
if (cause == 28)
return LOG_STR("LoadProhibit");
if (cause == 29)
return LOG_STR("StoreProhibit");
return nullptr;
}
static const LogString *get_reset_reason(uint32_t reason) {
if (reason == REASON_WDT_RST)
return LOG_STR("Hardware WDT");
if (reason == REASON_EXCEPTION_RST)
return LOG_STR("Exception");
if (reason == REASON_SOFT_WDT_RST)
return LOG_STR("Soft WDT");
return LOG_STR("Unknown");
}
// Read backtrace from RTC user memory into caller-provided buffer.
// Returns the number of valid backtrace entries (0 if no data found).
static uint8_t read_rtc_backtrace(uint32_t *backtrace, size_t max_entries) {
RtcCrashData rtc_data;
if (!system_rtc_mem_read(RTC_CRASH_BASE, &rtc_data, sizeof(rtc_data)))
return 0;
uint32_t magic = rtc_data.magic;
if ((magic & CRASH_SENTINEL_MASK) != CRASH_SENTINEL || (magic & CRASH_VERSION_MASK) != CRASH_VERSION)
return 0;
uint8_t raw_count = magic & CRASH_COUNT_MASK;
if (raw_count > MAX_BACKTRACE)
raw_count = MAX_BACKTRACE;
// Skip any that match epc1 (already reported as the fault PC).
// Note: we cannot verify CALL instructions at addr-3 on ESP8266 because
// reading from IROM causes LoadStoreError due to flash cache conflicts
// (the reading code and target can share a direct-mapped cache line).
// The linker-symbol IROM bounds already eliminate most false positives.
uint8_t out = 0;
for (uint8_t i = 0; i < raw_count && out < max_entries; i++) {
uint32_t addr = rtc_data.backtrace[i];
if (addr != rtc_data.epc1)
backtrace[out++] = addr;
}
return out;
}
// Intentionally uses separate ESP_LOGE calls per line instead of combining into
// one multi-line log message. This ensures each address appears as its own line
// on the serial console, making it possible to see partial output if the device
// crashes again during boot, and allowing the CLI's process_stacktrace to match
// and decode each address individually.
void crash_handler_log() {
if (!is_crash_reason(resetInfo.reason))
return;
// Read and filter backtrace from RTC into stack-local buffer (no persistent RAM cost).
// Both resetInfo and RTC data survive until the next reset, so this can be
// called multiple times (logger init + API subscribe) with the same result.
uint32_t backtrace[MAX_BACKTRACE];
uint8_t bt_count = read_rtc_backtrace(backtrace, MAX_BACKTRACE);
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
// GCC's ROM divide routine triggers IllegalInstruction (exccause=0) at specific
// ROM addresses instead of IntegerDivideByZero (exccause=6). Patch to match
// the Arduino core's postmortem handler behavior.
static constexpr uint32_t EXCCAUSE_ILLEGAL_INSTRUCTION = 0;
static constexpr uint32_t EXCCAUSE_INTEGER_DIVIDE_BY_ZERO = 6;
static constexpr uint32_t ROM_DIV_ZERO_ADDR_1 = 0x4000dce5;
static constexpr uint32_t ROM_DIV_ZERO_ADDR_2 = 0x4000dd3d;
uint32_t exccause = resetInfo.exccause;
if (exccause == EXCCAUSE_ILLEGAL_INSTRUCTION &&
(resetInfo.epc1 == ROM_DIV_ZERO_ADDR_1 || resetInfo.epc1 == ROM_DIV_ZERO_ADDR_2)) {
exccause = EXCCAUSE_INTEGER_DIVIDE_BY_ZERO;
}
const LogString *cause = get_exception_cause(exccause);
if (cause != nullptr) {
ESP_LOGE(TAG, " Reason: %s - %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)),
LOG_STR_ARG(cause), exccause);
} else {
ESP_LOGE(TAG, " Reason: %s (exccause=%" PRIu32 ")", LOG_STR_ARG(get_reset_reason(resetInfo.reason)), exccause);
}
ESP_LOGE(TAG, " PC: 0x%08" PRIX32, resetInfo.epc1);
if (resetInfo.reason == REASON_EXCEPTION_RST) {
ESP_LOGE(TAG, " EXCVADDR: 0x%08" PRIX32, resetInfo.excvaddr);
}
for (uint8_t i = 0; i < bt_count; i++) {
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32, i, backtrace[i]);
}
}
} // namespace esphome::esp8266
// --- Custom crash callback ---
// Overrides the weak custom_crash_callback() from Arduino core's
// core_esp8266_postmortem.cpp. Called during exception handling before
// the device restarts. We scan the full stack for code addresses and store
// them in RTC user memory (which survives software reset).
extern "C" void IRAM_ATTR custom_crash_callback(struct rst_info *rst_info, uint32_t stack, uint32_t stack_end) {
// No zero-init — only magic, epc1, and backtrace[0..count-1] are read.
// Saves the IRAM cost of a 72-byte zero-init loop.
RtcCrashData data; // NOLINT(cppcoreguidelines-pro-type-member-init)
uint8_t count = 0;
// Stack pointer from the Xtensa exception frame is always 4-byte aligned.
auto *scan = (uint32_t *) stack; // NOLINT(performance-no-int-to-ptr)
auto *end = (uint32_t *) stack_end; // NOLINT(performance-no-int-to-ptr)
uint32_t epc1 = rst_info->epc1;
for (; scan < end && count < MAX_BACKTRACE; scan++) {
uint32_t val = *scan;
if (is_code_addr(val)) {
uint32_t addr = recover_code_addr(val);
// Skip epc1 — already reported as the fault PC
if (addr != epc1)
data.backtrace[count++] = addr;
}
}
data.epc1 = epc1;
data.magic = CRASH_SENTINEL | CRASH_VERSION | count;
system_rtc_mem_write(RTC_CRASH_BASE, &data, sizeof(data));
}
#endif // USE_ESP8266_CRASH_HANDLER
#endif // USE_ESP8266
@@ -0,0 +1,20 @@
#pragma once
#ifdef USE_ESP8266
#include "esphome/core/defines.h"
#ifdef USE_ESP8266_CRASH_HANDLER
namespace esphome::esp8266 {
/// Log crash data if a crash was detected on previous boot.
void crash_handler_log();
/// Returns true if the previous boot was a crash (exception, WDT, or soft WDT).
bool crash_handler_has_data();
} // namespace esphome::esp8266
#endif // USE_ESP8266_CRASH_HANDLER
#endif // USE_ESP8266
+1 -1
View File
@@ -155,7 +155,7 @@ ESP8266_PIN_SCHEMA = cv.All(
@dataclass
class PinInitialState:
mode = 255
mode: int = 255
level: int = 255
+6 -5
View File
@@ -19,12 +19,13 @@ static constexpr uint32_t ESP_RTC_USER_MEM_START = 0x60001200;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_WORDS = 128;
static constexpr uint32_t ESP_RTC_USER_MEM_SIZE_BYTES = ESP_RTC_USER_MEM_SIZE_WORDS * 4;
// RTC memory layout for preferences:
// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 96-127)
// - Normal region: RTC words 32-127 (mapped from preference offset 0-95)
// RTC memory layout:
// - Eboot region: RTC words 0-31 (reserved, mapped from preference offset 78-109)
// - Normal region: RTC words 32-109 (mapped from preference offset 0-77)
// - Crash handler: RTC words 110-127 (reserved for crash_handler.cpp backtrace data)
static constexpr uint32_t RTC_EBOOT_REGION_WORDS = 32; // Words 0-31 reserved for eboot
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 96; // Words 32-127 for normal prefs
static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 128
static constexpr uint32_t RTC_NORMAL_REGION_WORDS = 78; // Words 32-109 for normal prefs
static constexpr uint32_t PREF_TOTAL_WORDS = RTC_EBOOT_REGION_WORDS + RTC_NORMAL_REGION_WORDS; // 110
// Maximum preference size in words (limited by uint8_t length_words field)
static constexpr uint32_t MAX_PREFERENCE_WORDS = 255;
@@ -262,7 +262,7 @@ void ESPHomeOTAComponent::handle_data_() {
/// BSD sockets (ESP32): setblocking(true) makes read/write block
/// lwip sockets (LT): setblocking(true) makes read/write block
/// Raw TCP (8266, RP2040): setblocking is no-op; SO_RCVTIMEO uses
/// socket_delay()/socket_wake() in read();
/// wakeable_delay() in read();
/// write() always returns immediately
ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
bool update_started = false;
+7 -10
View File
@@ -1,6 +1,6 @@
from esphome import automation, core
import esphome.codegen as cg
from esphome.components import socket, wifi
from esphome.components import wifi
from esphome.components.udp import CONF_ON_RECEIVE
import esphome.config_validation as cv
from esphome.const import (
@@ -17,7 +17,7 @@ from esphome.core import HexInt
from esphome.types import ConfigType
CODEOWNERS = ["@jesserockz"]
AUTO_LOAD = ["socket"]
byte_vector = cg.std_vector.template(cg.uint8)
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
@@ -124,14 +124,11 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# ESP-NOW uses wake_loop_threadsafe() to wake the main loop from ESP-NOW callbacks
# This enables low-latency event processing instead of waiting for select() timeout
socket.require_wake_loop_threadsafe()
cg.add_define("USE_ESPNOW")
if wifi_channel := config.get(CONF_CHANNEL):
cg.add(var.set_wifi_channel(wifi_channel))
cg.add(var.set_enable_on_boot(config[CONF_ENABLE_ON_BOOT]))
cg.add(var.set_auto_add_peer(config[CONF_AUTO_ADD_PEER]))
for peer in config.get(CONF_PEERS, []):
@@ -161,15 +158,15 @@ def validate_peer(value):
def _validate_raw_data(value):
if isinstance(value, str):
if len(value) >= MAX_ESPNOW_PACKET_SIZE:
if len(value) > MAX_ESPNOW_PACKET_SIZE:
raise cv.Invalid(
f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}"
f"'{CONF_DATA}' must be at most {MAX_ESPNOW_PACKET_SIZE} characters long, got {len(value)}"
)
return value
if isinstance(value, list):
if len(value) > MAX_ESPNOW_PACKET_SIZE:
raise cv.Invalid(
f"'{CONF_DATA}' must be less than {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}"
f"'{CONF_DATA}' must be at most {MAX_ESPNOW_PACKET_SIZE} bytes long, got {len(value)}"
)
return cv.Schema([cv.hex_uint8_t])(value)
raise cv.Invalid(
@@ -248,7 +245,7 @@ async def send_action(
data = config.get(CONF_DATA, [])
if isinstance(data, str):
data = [cg.RawExpression(f"'{c}'") for c in data]
data = list(data.encode())
templ = await cg.templatable(data, args, byte_vector, byte_vector)
cg.add(var.set_data(templ))
@@ -92,10 +92,8 @@ void on_send_report(const uint8_t *mac_addr, esp_now_send_status_t status)
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
// Wake main loop immediately to process ESP-NOW send event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
// Wake main loop immediately to process ESP-NOW send event
App.wake_loop_threadsafe();
#endif
}
void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size) {
@@ -115,10 +113,8 @@ void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int
// Push always succeeds: pool is sized to queue capacity (SIZE-1), so if
// allocate() returned non-null, the queue cannot be full.
// Wake main loop immediately to process ESP-NOW receive event instead of waiting for select() timeout
#if defined(USE_SOCKET_SELECT_SUPPORT) && defined(USE_WAKE_LOOP_THREADSAFE)
// Wake main loop immediately to process ESP-NOW receive event
App.wake_loop_threadsafe();
#endif
}
ESPNowComponent::ESPNowComponent() { global_esp_now = this; }
+50 -27
View File
@@ -104,6 +104,8 @@ CONF_CLK_MODE = "clk_mode"
CONF_POWER_PIN = "power_pin"
CONF_PHY_REGISTERS = "phy_registers"
CONF_INTERFACE = "interface"
CONF_CLOCK_SPEED = "clock_speed"
EthernetType = ethernet_ns.enum("EthernetType")
@@ -121,6 +123,8 @@ ETHERNET_TYPES = {
"DM9051": EthernetType.ETHERNET_TYPE_DM9051,
"LAN8670": EthernetType.ETHERNET_TYPE_LAN8670,
"ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60,
"W6100": EthernetType.ETHERNET_TYPE_W6100,
"W6300": EthernetType.ETHERNET_TYPE_W6300,
}
# PHY types that need compile-time defines for conditional compilation
@@ -138,6 +142,8 @@ _PHY_TYPE_TO_DEFINE = {
"DM9051": "USE_ETHERNET_DM9051",
"LAN8670": "USE_ETHERNET_LAN8670",
"ENC28J60": "USE_ETHERNET_ENC28J60",
"W6100": "USE_ETHERNET_W6100",
"W6300": "USE_ETHERNET_W6300",
}
@@ -168,12 +174,14 @@ _ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"}
# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver)
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"}
# RP2040-supported SPI ethernet types
RP2040_SPI_ETHERNET_TYPES = {"W5100", "W5500", "ENC28J60"}
# RP2040-supported ethernet types (SPI and PIO QSPI)
RP2040_ETHERNET_TYPES = {"W5100", "W5500", "W6100", "W6300", "ENC28J60"}
_RP2040_SPI_LIBRARIES = {
"W5100": "lwIP_w5100",
"W5500": "lwIP_w5500",
"ENC28J60": "lwIP_enc28j60",
"W6100": "lwIP_w6100",
"W6300": "lwIP_w6300",
}
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
@@ -191,6 +199,13 @@ CLK_MODES_DEPRECATED = {
"GPIO17_OUT": ("CLK_OUT", 17),
}
spi_host_device_t = cg.global_ns.enum("spi_host_device_t")
SPI_INTERFACE_MAP = {
"spi2": spi_host_device_t.SPI2_HOST,
"spi3": spi_host_device_t.SPI3_HOST,
}
MANUAL_IP_SCHEMA = cv.Schema(
{
cv.Required(CONF_STATIC_IP): cv.ipv4address,
@@ -225,6 +240,24 @@ def _is_framework_spi_polling_mode_supported() -> bool:
return False
def _validate_spi_interface(config: ConfigType) -> ConfigType:
"""Set default SPI interface or validate user choice against the variant."""
if not CORE.is_esp32:
return config
from esphome.components.esp32 import VARIANT_ESP32, get_esp32_variant
from esphome.components.spi import get_hw_interface_list
has_spi3 = "spi3" in sum(get_hw_interface_list(), [])
if CONF_INTERFACE not in config:
# Only classic ESP32 defaults to spi3; all others default to spi2
config[CONF_INTERFACE] = (
"spi3" if get_esp32_variant() == VARIANT_ESP32 else "spi2"
)
elif config[CONF_INTERFACE] == "spi3" and not has_spi3:
raise cv.Invalid("Interface 'spi3' is not available on this variant.")
return config
def _validate(config):
if CONF_USE_ADDRESS not in config:
if CONF_MANUAL_IP in config:
@@ -301,9 +334,9 @@ def _validate(config):
f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported "
f"on ESP32 classic and ESP32-P4, not {variant}"
)
elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_SPI_ETHERNET_TYPES:
elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_ETHERNET_TYPES:
raise cv.Invalid(
f"Only {', '.join(sorted(RP2040_SPI_ETHERNET_TYPES))} are supported on RP2040, "
f"Only {', '.join(sorted(RP2040_ETHERNET_TYPES))} are supported on RP2040, "
f"not {config[CONF_TYPE]}"
)
return config
@@ -368,6 +401,10 @@ SPI_SCHEMA = cv.All(
cv.frequency,
cv.int_range(int(8e6), int(80e6)),
),
cv.Optional(CONF_INTERFACE): cv.All(
cv.only_on_esp32,
cv.one_of(*SPI_INTERFACE_MAP.keys(), lower=True),
),
# Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate()
cv.Optional(CONF_POLLING_INTERVAL): cv.All(
cv.only_on_esp32,
@@ -378,6 +415,7 @@ SPI_SCHEMA = cv.All(
),
),
cv.only_on([Platform.ESP32, Platform.RP2040]),
_validate_spi_interface,
)
CONFIG_SCHEMA = cv.All(
@@ -395,6 +433,8 @@ CONFIG_SCHEMA = cv.All(
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
"DM9051": SPI_SCHEMA,
"ENC28J60": SPI_SCHEMA,
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"LAN8670": RMII_SCHEMA,
},
upper=True,
@@ -408,37 +448,18 @@ def _final_validate_spi(config):
return # SPI interface validation is ESP32-only
if config[CONF_TYPE] not in SPI_ETHERNET_TYPES:
return
from esphome.components.esp32 import (
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
)
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
if spi_configs := fv.full_config.get().get(CONF_SPI):
variant = get_esp32_variant()
if variant in (
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
):
spi_host = "SPI2_HOST"
else:
spi_host = "SPI3_HOST"
# get_spi_interface() returns strings like "SPI2_HOST"
spi_host = f"{config[CONF_INTERFACE].upper()}_HOST"
for spi_conf in spi_configs:
if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None:
interface = get_spi_interface(index)
if interface == spi_host:
raise cv.Invalid(
f"`spi` component is using interface '{interface}'. "
f"To use {config[CONF_TYPE]}, you must change the `interface` on the `spi` component.",
f"The `ethernet` and `spi` components are both using interface '{interface}'. "
f"To use {config[CONF_TYPE]}, change the `interface` on either `ethernet:` or `spi:`."
)
@@ -528,6 +549,8 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED]))
cg.add_define("USE_ETHERNET_SPI")
cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]]))
add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True)
# CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0
# ENC28J60 was never built-in to IDF, so it has no Kconfig option
@@ -11,6 +11,9 @@
#ifdef USE_ESP32
#include "esp_eth.h"
#ifdef USE_ETHERNET_SPI
#include "hal/spi_types.h"
#endif
#include "esp_eth_mac.h"
#include "esp_eth_mac_esp.h"
#include "esp_netif.h"
@@ -27,6 +30,20 @@ extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
#include <W5500lwIP.h>
#elif defined(USE_ETHERNET_W5100)
#include <W5100lwIP.h>
#elif defined(USE_ETHERNET_W6100)
#include <W6100lwIP.h>
#elif defined(USE_ETHERNET_W6300)
#include <W6300lwIP.h>
// W6300 uses PIO QSPI, not Arduino SPI. The upstream Wiznet6300 class
// incorrectly returns needsSPI()=true, causing LwipIntfDev::begin() to
// call SPI.begin() which claims GPIOs that PIO QSPI needs.
// This wrapper hides needsSPI() with a version returning false.
class Wiznet6300NoSPI : public Wiznet6300 {
public:
using Wiznet6300::Wiznet6300;
constexpr bool needsSPI() const { return false; }
};
using Wiznet6300lwIPFixed = LwipIntfDev<Wiznet6300NoSPI>;
#elif defined(USE_ETHERNET_ENC28J60)
#include <ENC28J60lwIP.h>
#else
@@ -67,6 +84,8 @@ enum EthernetType : uint8_t {
ETHERNET_TYPE_DM9051,
ETHERNET_TYPE_LAN8670,
ETHERNET_TYPE_ENC28J60,
ETHERNET_TYPE_W6100,
ETHERNET_TYPE_W6300,
};
struct ManualIP {
@@ -135,6 +154,7 @@ class EthernetComponent final : public Component {
void set_interrupt_pin(uint8_t interrupt_pin);
void set_reset_pin(uint8_t reset_pin);
void set_clock_speed(int clock_speed);
void set_interface(spi_host_device_t interface);
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
void set_polling_interval(uint32_t polling_interval);
#endif
@@ -201,6 +221,7 @@ class EthernetComponent final : public Component {
int reset_pin_{-1};
int phy_addr_spi_{-1};
int clock_speed_;
spi_host_device_t interface_{SPI3_HOST};
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
uint32_t polling_interval_{0};
#endif
@@ -227,6 +248,8 @@ class EthernetComponent final : public Component {
static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls
#if defined(USE_ETHERNET_W5100)
static constexpr uint32_t RESET_DELAY_MS = 150; // W5100S PLL lock time
#elif defined(USE_ETHERNET_W6300)
static constexpr uint32_t RESET_DELAY_MS = 100; // W6300 needs 100ms after hardware reset
#else
static constexpr uint32_t RESET_DELAY_MS = 10;
#endif
@@ -234,6 +257,10 @@ class EthernetComponent final : public Component {
Wiznet5500lwIP *eth_{nullptr};
#elif defined(USE_ETHERNET_W5100)
Wiznet5100lwIP *eth_{nullptr};
#elif defined(USE_ETHERNET_W6100)
Wiznet6100lwIP *eth_{nullptr};
#elif defined(USE_ETHERNET_W6300)
Wiznet6300lwIPFixed *eth_{nullptr};
#elif defined(USE_ETHERNET_ENC28J60)
ENC28J60lwIP *eth_{nullptr};
#else
@@ -158,12 +158,7 @@ void EthernetComponent::setup() {
.intr_flags = 0,
};
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
auto host = SPI2_HOST;
#else
auto host = SPI3_HOST;
#endif
auto host = this->interface_;
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
@@ -458,6 +453,11 @@ void EthernetComponent::dump_config() {
" MOSI Pin: %u\n"
" CS Pin: %u",
this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_);
const char *spi_interface = "spi3";
if (this->interface_ == SPI2_HOST) {
spi_interface = "spi2";
}
ESP_LOGCONFIG(TAG, " Interface: %s", spi_interface);
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
if (this->polling_interval_ != 0) {
ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_);
@@ -760,6 +760,7 @@ void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; }
void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; }
void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; }
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
#endif
@@ -18,9 +18,14 @@ static const char *const TAG = "ethernet";
void EthernetComponent::setup() {
// Configure SPI pins
#if !defined(USE_ETHERNET_W6300)
SPI.setRX(this->miso_pin_);
SPI.setTX(this->mosi_pin_);
SPI.setSCK(this->clk_pin_);
#endif
// W6300 uses PIO QSPI with hardcoded pins, not Arduino SPI.
// SPI pin config is skipped; Wiznet6300lwIPFixed (needsSPI()=false)
// prevents LwipIntfDev::begin() from calling SPI.begin().
// Toggle reset pin if configured
if (this->reset_pin_ >= 0) {
@@ -40,6 +45,10 @@ void EthernetComponent::setup() {
this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_W5100)
this->eth_ = new Wiznet5100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_W6100)
this->eth_ = new Wiznet6100lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_W6300)
this->eth_ = new Wiznet6300lwIPFixed(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#elif defined(USE_ETHERNET_ENC28J60)
this->eth_ = new ENC28J60lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
#endif
@@ -183,9 +192,23 @@ void EthernetComponent::dump_config() {
type_str = "W5500";
#elif defined(USE_ETHERNET_W5100)
type_str = "W5100";
#elif defined(USE_ETHERNET_W6100)
type_str = "W6100";
#elif defined(USE_ETHERNET_W6300)
type_str = "W6300";
#elif defined(USE_ETHERNET_ENC28J60)
type_str = "ENC28J60";
#endif
#if defined(USE_ETHERNET_W6300)
// W6300 uses PIO QSPI with hardcoded pins — SPI pin fields are not used
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: %s (PIO QSPI)\n"
" Connected: %s\n"
" IRQ Pin: %d\n"
" Reset Pin: %d",
type_str, YESNO(this->is_connected()), this->interrupt_pin_, this->reset_pin_);
#else
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: %s\n"
@@ -198,6 +221,7 @@ void EthernetComponent::dump_config() {
" Reset Pin: %d",
type_str, YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
this->interrupt_pin_, this->reset_pin_);
#endif
this->dump_connect_params_();
}
+8 -4
View File
@@ -82,12 +82,16 @@ def event_schema(
return _EVENT_SCHEMA.extend(schema)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_EVENT, "add_on_event_callback", [(cg.StringRef, "event_type")]
),
)
@setup_entity("event")
async def setup_event_core_(var, config, *, event_types: list[str]):
for conf in config.get(CONF_ON_EVENT, []):
await automation.build_callback_automation(
var, "add_on_event_callback", [(cg.StringRef, "event_type")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
cg.add(var.set_event_types(event_types))
+21 -24
View File
@@ -38,33 +38,30 @@ CONFIG_SCHEMA = (
)
_CALLBACK_AUTOMATIONS = (
automation.CallbackAutomation(
CONF_ON_CUSTOM, "add_custom_callback", [(cg.std_string, "x")]
),
automation.CallbackAutomation(CONF_ON_LED, "add_led_state_callback", [(bool, "x")]),
automation.CallbackAutomation(
CONF_ON_DEVICE_INFORMATION,
"add_device_infomation_callback",
[(cg.std_string, "x")],
),
automation.CallbackAutomation(
CONF_ON_SLOPE, "add_slope_callback", [(cg.std_string, "x")]
),
automation.CallbackAutomation(
CONF_ON_CALIBRATION, "add_calibration_callback", [(cg.std_string, "x")]
),
automation.CallbackAutomation(CONF_ON_T, "add_t_callback", [(cg.std_string, "x")]),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await sensor.register_sensor(var, config)
await i2c.register_i2c_device(var, config)
for conf in config.get(CONF_ON_CUSTOM, []):
await automation.build_callback_automation(
var, "add_custom_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_LED, []):
await automation.build_callback_automation(
var, "add_led_state_callback", [(bool, "x")], conf
)
for conf in config.get(CONF_ON_DEVICE_INFORMATION, []):
await automation.build_callback_automation(
var, "add_device_infomation_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_SLOPE, []):
await automation.build_callback_automation(
var, "add_slope_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_CALIBRATION, []):
await automation.build_callback_automation(
var, "add_calibration_callback", [(cg.std_string, "x")], conf
)
for conf in config.get(CONF_ON_T, []):
await automation.build_callback_automation(
var, "add_t_callback", [(cg.std_string, "x")], conf
)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)

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