Compare commits

...
Author SHA1 Message Date
J. Nick Koston 02f9a78a86 [bk72xx] Address Copilot review: fix UB in scan_backtrace + snprintf truncation
- scan_backtrace: do bounds math in uintptr_t space and only cast to
  pointer at dereference. Comparing pointers from unrelated objects
  (the SP value vs the linker RAM-end symbol) is technically UB in C++
  even though it works on flat-address embedded targets.
- crash_handler_log: clamp the snprintf return value before using it
  as an offset. snprintf returns < 0 on error or >= size on truncation,
  and feeding that straight back into pointer arithmetic is UB.
2026-04-30 13:15:02 -05:00
J. Nick Koston b0c618ed27 [bk72xx] Emit USE_BK72XX_CRASH_HANDLER via cg.add_define
The previous gate lived only in core/defines.h, which is loaded for
static analysis / IDE support but not for actual builds. As a result
the wrap symbols (__wrap_bk_trap_udef/pabt/dabt) were preprocessed out
of crash_handler.cpp at compile time, leaving the linker without a
target for the SDK's wrapped trap calls. Set the define from
libretiny/__init__.py (where the platform-specific codegen lives —
bk72xx/__init__.py is auto-generated and not the right home).
2026-04-30 13:03:25 -05:00
J. Nick Koston 1e3c104cbc [bk72xx] Read RAM/flash bounds from linker symbols, not constants
Addresses review feedback ("unsafe assumptions about memory layout",
"its a heuristic"). The patch_bk72xx_noinit.py extra_script now also
emits PROVIDE(_esphome_{ram,flash}_{start,end}) tied directly to the
linker's MEMORY definition. crash_handler.cpp consumes those symbols
instead of hardcoding bounds, so it tracks the actual variant + board
layout (BK7231N=192KB vs others=256KB, board-specific BKOFFSET_APP /
BKRBL_SIZE_APP) without any chip-aware constants of its own.

Mirrors the linker-symbol pattern already used in
components/esp8266/crash_handler.cpp for the IROM bounds.
2026-04-30 12:57:18 -05:00
J. Nick Koston 5bd5cf1dab [bk72xx] Use USE_LIBRETINY_VARIANT_BK7231N for variant-specific RAM size
Mirrors the split in LibreTiny's lt_mem.c: BK7231N has 192KB RAM, every
other BK72XX variant has 256KB. The previous hardcoded 0x00440000 was
correct for everything except BK7231N, where the stack-scan upper bound
sat 64KB past the chip's physical RAM end.
2026-04-30 12:55:38 -05:00
J. Nick Koston 8fc2e1e542 [bk72xx] Add crash-on-previous-boot detection
Brings BK72XX to parity with esp32/esp8266/rp2040: register snapshot +
stack-scanned backtrace are captured during the SDK's exception traps
(undefined instruction / prefetch abort / data abort), persisted across
the watchdog reset in a .noinit RAM region, then logged on the next
boot via the logger and API log subscription paths.

The capture path uses linker --wrap on bk_trap_udef/pabt/dabt — the
trap symbols are defined in LibreTiny's intc.c fixup but called from
the closed-source Beken SDK across translation units, so --wrap takes
effect cleanly. The original SDK behavior (UART register dump +
bk_cpu_shutdown → watchdog reset) is preserved by tail-calling the
real handlers after capture.

A small extra_script (patch_bk72xx_noinit.py) injects a .noinit
section between .bss and _empty_ram in the generated linker script
so the crash record is not zeroed by the C runtime startup and the
heap shrinks to fit rather than overlapping it.
2026-04-30 12:50:16 -05:00
J. Nick KostonandGitHub 61261b4a59 [libretiny] Move HAL bodies into components/libretiny/hal.cpp + inline trivial dispatches (#16113) 2026-04-30 12:33:22 -05:00
J. Nick KostonandGitHub d48aad8c4d [esp32] Replace 512B stack buffer in printf wraps with picolibc cookie FILE (#16170) 2026-04-30 13:27:54 -04:00
Kevin AhrendtandGitHub f1d3be4bda [core] Simplify RAMAllocator and add internal fallback to external mode (#16171) 2026-04-30 12:03:40 -04:00
Kevin AhrendtandGitHub 2758aa5517 [audio] bump microOpus to v0.4.0 to use fixed-point by default on ESP32 (#16168) 2026-04-30 09:12:39 -04:00
Kevin AhrendtandGitHub a8b0133ec1 [audio] Enable specific codecs and configure advanced features (#16166) 2026-04-30 08:49:28 -04:00
Clyde StubbsandGitHub 1398dcebb4 [st7789v] Add deprecation warnings (#16162) 2026-04-30 00:53:37 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
096d0c4279 Bump aioesphomeapi from 44.22.0 to 44.23.0 (#16161)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 04:45:19 +00:00
Jesse HillsandGitHub e127268dac [core] Strip \\?\ prefix from sys.executable for PlatformIO subprocess (#16158) 2026-04-30 16:04:52 +12:00
J. Nick KostonandGitHub f0bffed3c0 [esp8266] Move HAL bodies into components/esp8266/hal.cpp + inline arch_init (#16112) 2026-04-30 15:42:17 +12:00
Jesse HillsandGitHub 1a871e231d [ci] Use client-id for GitHub App token generation (#16155) 2026-04-30 13:09:37 +12:00
Jesse HillsandGitHub 47765bd2d0 [ci] Correct version comment on create-github-app-token pin (#16156) 2026-04-30 13:08:56 +12:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8066325e0b Bump esphome/workflows/.github/workflows/lock.yml from 2026.4.0 to 2026.4.1 (#16143)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 12:52:25 +12:00
J. Nick KostonandGitHub b8d24c9e49 [mcp23xxx_base] Reject unsupported interrupt_pin options (inverted, allow_other_uses) (#16149) 2026-04-30 11:14:07 +12:00
J. Nick KostonandGitHub 9b1f5c59bb [core] Fix null deref in WarnIfComponentBlockingGuard for self-keyed scheduler timers (#16150) 2026-04-29 23:05:38 +00:00
Jonathan SwobodaandGitHub e4b33fddf5 [esp32] Add ESP-IDF 6.0.1 platform entry (#16146) 2026-04-29 18:43:15 -04:00
Jonathan SwobodaandGitHub 77da64a367 [sx126x] Add cold sleep option and drop unused RTC wakeup bit (#16144) 2026-04-29 17:05:51 -04:00
J. Nick KostonandGitHub cecccebc64 [core] DelayAction: drop Component inheritance, use self-keyed scheduler (#16129) 2026-04-29 20:35:04 +00:00
Jonathan SwobodaGitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
53b682e48f [ci] Bump clang-tidy from 18.1.8 to 22.1.0.1 (#16078)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-29 20:19:33 +00:00
14910e65d9 [ota] Use WatchdogManager for OTA on ESP32 (#16138)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-29 20:15:21 +00:00
J. Nick KostonandGitHub 813964714c [esp32] Move HAL bodies into components/esp32/hal.cpp + inline trivial dispatches (#16111) 2026-04-29 20:09:08 +00:00
J. Nick KostonandGitHub 5a146ab6b7 [valve] Fold ControlAction fields into a single stateless lambda (#16123) 2026-04-29 19:20:15 +00:00
J. Nick KostonandGitHub 61a41402df [fan] Fold TurnOnAction fields into a single stateless lambda (#16122) 2026-04-29 19:16:05 +00:00
Mat931andGitHub 59b4cfd07c [watchdog] Use default CHECK_IDLE_TASK and PANIC when configuring the watchdog (#16142) 2026-04-29 18:41:12 +00:00
J. Nick KostonandGitHub c41f38e16d [scheduler] Add self-keyed timer API for callers without a Component (#16127) 2026-04-29 13:24:37 -05:00
Clyde StubbsandGitHub 0ad8a071a7 [espnow] Cleanup method visibility and naming (#16109) 2026-04-29 14:18:21 -04:00
J. Nick KostonandGitHub 985dba9332 [core] Defer heavy module-scope imports in __main__, loader, and config (#15955) 2026-04-29 13:17:59 -05:00
ca3f7251d4 [ens160] Fix sensor initialization timing (#16024)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-29 14:07:28 -04:00
J. Nick KostonandGitHub 44cabc191d [core] Catch body-read errors in download_content (#16023) 2026-04-29 14:06:41 -04:00
J. Nick KostonandGitHub e5b1991cf7 [fan] Add tests for fan.turn_on action field combinations (#16125) 2026-04-29 12:46:06 -05:00
J. Nick KostonandGitHub 7fba57ce51 [valve] Add tests for valve.control action field combinations (#16126) 2026-04-29 12:45:30 -05:00
J. Nick KostonandGitHub 69a33d8ac0 [core] Inline HAL clock wrappers and split hal.h into per-platform headers (#15977) 2026-04-29 12:31:55 -05:00
Jonathan SwobodaandGitHub ce61dcf387 [remote_base][core] Drop redundant typename in dependent type contexts (#16137) 2026-04-29 16:54:17 +00:00
Jonathan SwobodaandGitHub bae6b51652 [kamstrup_kmp][toshiba] Fix signed/unsigned comparisons against sizeof (#16135) 2026-04-29 11:33:57 -04:00
Jonathan SwobodaandGitHub 557c3d4436 [aqi] Use std::max initializer-list for non-negative AQI clamp (#16134) 2026-04-29 11:33:29 -04:00
Jonathan SwobodaandGitHub bacee89bca [mixer_speaker] NOLINT bugprone-unchecked-optional-access in audio_mixer_task (#16130) 2026-04-29 10:56:13 -04:00
Jonathan SwobodaandGitHub 2157d11913 [haier] Fix bugprone-unchecked-optional-access; switch HardwareInfo to char[9] (#16124) 2026-04-29 14:26:53 +00:00
Jonathan SwobodaandGitHub 42b8597719 [api] Extend NOLINT to cover bugprone-random-generator-seed in MAC varint test (#16120) 2026-04-29 13:58:19 +00:00
Jonathan SwobodaandGitHub 2bd28eee9d [tormatic] Use .value() for checked optional access in read_gate_status_ (#16121) 2026-04-29 09:51:31 -04:00
J. Nick KostonandGitHub 0a497d3c22 [light] Fold LightControlAction fields into a single stateless lambda (#16118) 2026-04-29 08:35:17 -05:00
Jonathan SwobodaandGitHub 79da2b9704 [time] Fix bugprone-unchecked-optional-access in CronTrigger::check_time_ (#16107) 2026-04-29 08:30:46 -04:00
Jonathan SwobodaandGitHub ae5b211c89 [api] Avoid JsonDocument copy-and-swap operator= in ActionResponse ctor (#16106) 2026-04-29 08:30:35 -04:00
J. Nick KostonandGitHub 8ceada8d04 [core] Download external_files in parallel (#16021) 2026-04-29 14:32:30 +12:00
J. Nick KostonandGitHub 49c7a6928e [script] Fix cpp_unit_test crash for non-MULTI_CONF platform components (#16104) 2026-04-29 14:32:13 +12:00
J. Nick KostonandGitHub 2fce71e0d4 [wifi] Add phy_mode option for ESP8266 (#16055) 2026-04-29 14:31:07 +12:00
J. Nick KostonandGitHub 80251c54be [climate] Add climate.control coverage to component tests via thermostat (#16052) 2026-04-29 14:27:56 +12:00
J. Nick KostonandGitHub 0d51a122d0 [cover] Add cover.control / cover.template.publish coverage to template tests (#16051) 2026-04-29 14:27:40 +12:00
J. Nick KostonandGitHub 5a33c50015 [light] Use constexpr template for DimRelativeAction transition_length (#16038) 2026-04-29 14:26:38 +12:00
J. Nick KostonandGitHub 0d150dc57e [light] Use constexpr template for ToggleAction transition_length (#16037) 2026-04-29 14:25:18 +12:00
J. Nick KostonandGitHub d287876d8d [light] Use bitmask template for LightControlAction unused fields (#16039) 2026-04-29 14:20:37 +12:00
J. Nick KostonandGitHub 592486ae9a [analyze_memory] Attribute main.cpp setup()/loop() to esphome core (#16033) 2026-04-29 14:06:54 +12:00
Jonathan SwobodaandGitHub c3bd38af77 [feedback] Fix bugprone-unchecked-optional-access in start_direction_ (#16103) 2026-04-28 21:54:15 -04:00
J. Nick KostonandGitHub eec770d622 [core] Use ETag in external_files cache to fix re-downloads from raw.githubusercontent.com (#16020) 2026-04-29 13:52:09 +12:00
J. Nick KostonandGitHub d7b21a84a3 [git] Make ref fetches and submodule updates shallow (#16014) 2026-04-29 13:49:51 +12:00
J. Nick KostonandGitHub f05243bd9d [api] Add 48-bit MAC address varint fast path for BLE advertisements (#15988) 2026-04-29 13:48:35 +12:00
J. Nick KostonandGitHub 35cb28edfe [output] Gate FloatOutput power scaling fields behind USE_OUTPUT_FLOAT_POWER_SCALING (#15998) 2026-04-29 13:27:22 +12:00
J. Nick KostonandGitHub 1363f661e6 [core] Inline ContinuationAction in If/While/RepeatAction (#16040) 2026-04-28 21:26:25 -04:00
J. Nick KostonandGitHub 8af499b591 [api] Use custom deleter to fix incomplete-type error on macOS libc++ (#16050) 2026-04-28 21:26:21 -04:00
Jonathan SwobodaandGitHub 1a57d9bc2f [sprinkler][pn532] Fix bugprone-unchecked-optional-access (#16102) 2026-04-29 01:04:19 +00:00
J. Nick KostonandGitHub 9768380856 [api] Hoist memw out of socket ready check to once per main-loop iter (#15996) 2026-04-29 13:04:10 +12:00
J. Nick KostonandGitHub 676f26919e [mdns] Drive MDNS.update() polling from IP state events on ESP8266/RP2040 (#15961) 2026-04-29 13:02:21 +12:00
J. Nick KostonandGitHub 29d3a3a498 [esp8266] Replace millis() with fast accumulator, wrap Arduino callers (#15662) 2026-04-29 12:58:00 +12:00
Jonathan SwobodaandGitHub 77b76ac48a [inkbird_ibsth1_mini][speaker][speaker_source] Fix performance-unnecessary-copy-initialization (#16101) 2026-04-29 00:56:03 +00:00
Clyde StubbsandGitHub 0b5835284a [lvgl] Additional layout features (#16041) 2026-04-29 12:35:24 +12:00
15df477472 [core] Reduce copies in Callback/CallbackManager call paths (#16093)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-04-28 19:41:28 -04:00
Jonathan SwobodaandGitHub be0ee73847 [i2c] NOLINT readability-identifier-naming on Zephyr struct forward-decl (#16099) 2026-04-28 19:22:42 -04:00
Jonathan SwobodaandGitHub a241c9e622 [online_image][sim800l] Use std::string::starts_with for prefix checks (#16097) 2026-04-28 19:02:39 -04:00
Jonathan SwobodaandGitHub 2f433c78bd [haier] Brace single-statement else-if in smartair2_climate (#16098) 2026-04-28 18:56:36 -04:00
Jonathan SwobodaandGitHub e39c474577 [binary_sensor] Bind at_index_ once in MultiClick on_state_ (#16095) 2026-04-28 22:13:35 +00:00
Jonathan SwobodaandGitHub a62e3fe4fc [json] NOLINT StackAddressEscape false positive in parse_json (#16091) 2026-04-28 21:35:40 +00:00
Jonathan SwobodaandGitHub 7d6b9bee19 [wifi] Avoid copying EAP config in three connect handlers (#16094) 2026-04-28 21:22:29 +00:00
Jonathan SwobodaandGitHub ab6bda50e4 [esp32_ble] Widen loop variable in as_128bit() to match uuid_.len type (#16088) 2026-04-28 20:58:40 +00:00
Jonathan SwobodaandGitHub 3d195d748c [ezo] Use make_unique to construct EzoCommand (#16092) 2026-04-28 20:50:15 +00:00
Jonathan SwobodaandGitHub 16cf4fb5e8 [nextion] Use std::string::starts_with for HTTPS URL check (#16090) 2026-04-28 20:47:20 +00:00
Jonathan SwobodaandGitHub 70503442f4 [dfrobot_sen0395] Brace single-statement else-if in enqueue() (#16089) 2026-04-28 20:37:29 +00:00
Jonathan SwobodaandGitHub 594b269dba [bme680] Rename cal1/cal2 to coeff1/coeff2 (#16087) 2026-04-28 20:33:57 +00:00
Clyde StubbsandGitHub 8157c721a5 [mapping] Implement default value (#15861) 2026-04-29 06:31:37 +10:00
Clyde StubbsandGitHub 9af557de6d [lvgl] Add utility gradient function (#16048) 2026-04-29 06:29:38 +10:00
Jonathan SwobodaandGitHub 1f4136e76f [pipsolar] Guard handle_qmod_ against empty message (#16085) 2026-04-28 16:29:09 -04:00
Jonathan SwobodaandGitHub c8dffcc9b8 [tlc5971] Remove dead bit-banging delay code (#16086) 2026-04-28 15:28:33 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
44fbb7f5a9 Bump CodSpeedHQ/action from 4.14.0 to 4.15.0 (#16084)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 15:10:21 -05:00
Jonathan SwobodaandGitHub eb01d43feb [spi][http_request][demo] Fix latent clang-tidy issues in headers (#16080) 2026-04-28 16:09:35 -04:00
Jonathan SwobodaandGitHub 7891fd5cf1 Add dependencies.lock to .gitignore (#16081) 2026-04-29 07:38:31 +12:00
Jonathan SwobodaandGitHub 4ee9cc432b [ci] Install requirements_dev.txt in the cached venv (#16082) 2026-04-29 07:37:46 +12:00
Mat931andGitHub 42ff10afe5 [watchdog] Fix WatchdogManager on single core apps (#16074) 2026-04-28 17:32:44 +00:00
6b3df66bdc [nrf52] make reset pin optional (#11684)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-28 12:20:38 -05:00
968878a62d [nrf52] implement wake_loop_threadsafe/wakeable_delay (#16032)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-28 16:35:12 +00:00
J. Nick KostonandGitHub daf3f4d2f1 [core] wakeable_delay: yield on already-woken fast path (ESP8266, RP2040) (#16045) 2026-04-28 10:41:17 -05:00
52e8c50f45 [modbus] Split modbus_server from modbus_controller (#15509)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-04-28 10:21:25 -05:00
J. Nick KostonandGitHub 0a4d9b430f [ci] Add import-time regression check for esphome.__main__ (#15954) 2026-04-28 14:05:12 +00:00
J. Nick KostonandGitHub 0759a3c681 [core] Split wake.{h,cpp} into per-platform files (#15978) 2026-04-28 08:48:13 -05:00
Egor VorontsovGitHubpre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>J. Nick Koston
8921e3bb3f [api] add open states for lock to api.proto (#15901)
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-28 07:49:16 -05:00
52f80618d4 [lvgl] Allow a binary sensor to report checked or pressed state (#16073)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-04-28 22:00:29 +10:00
876c8c4c2a [ci-custom] Lint imports of esphome.components.const outside components (#16068)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 05:59:02 -05:00
Darafei PraliaskouskiandGitHub 41458d72e0 [esp32] Make Arduino app metadata reproducible (#16053) 2026-04-28 05:58:34 -05:00
49d3df2698 [automation] Fix codegen type for component.resume update_interval (#16069)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 05:27:20 -05:00
J. Nick KostonandGitHub 792f2e8363 [ota] Add wall-clock timeout to OTA data transfer loop (#16047) 2026-04-28 17:29:42 +12:00
J. Nick KostonandGitHub 42c9fdc87e [feedback] Use App.get_loop_component_start_time() and constexpr timeout id (#16063) 2026-04-28 16:39:08 +12:00
Jesse Hills 5f6bbb98ce Merge branch 'release' into dev 2026-04-28 15:40:19 +12:00
Jesse HillsandGitHub 4e0509435a Merge pull request #16067 from esphome/bump-2026.4.3
2026.4.3
2026-04-28 15:39:24 +12:00
J. Nick KostonandGitHub a03de7cea2 [core] Freshen loop_component_start_time_ before scheduler dispatch (#16064) 2026-04-28 13:23:08 +12:00
Jesse Hills 95b5ab7e78 Bump version to 2026.4.3 2026-04-28 12:58:29 +12:00
3ac0939f55 [image] Fix RGB565+alpha rendering for multi-frame animations (#16017)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-28 12:58:29 +12:00
Jesse Hills 191d3bc7e4 [esp32_touch] Feed wdt (#16066) 2026-04-28 12:58:29 +12:00
Edward FirmoandJesse Hills a186f6fea9 [nextion] Unify TFT upload ack timeout to 5000ms (#15960) 2026-04-28 12:58:29 +12:00
aea88aef5e [esp32][wifi] Fix bootloop and WiFi connection issue if nvs partition is missing or has non-default label (#16025)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-28 12:58:29 +12:00
J. Nick KostonandJesse Hills 433bbdb016 [rotary_encoder][at581x] Fix templatable int field types (#16015) 2026-04-28 12:58:29 +12:00
J. Nick KostonandJesse Hills 4137d93cbf [wifi] Fix stale wifi.connected after state transition (#15966) 2026-04-28 12:58:29 +12:00
J. Nick KostonandJesse Hills 6a5919ee87 [deep_sleep] Fix sleep_duration codegen type to uint32_t (#15965) 2026-04-28 12:58:29 +12:00
Jesse Hills b753ee4e94 [time] Handle Windows EINVAL when validating POSIX TZ strings (#15934) 2026-04-28 12:58:29 +12:00
Clyde StubbsandJesse Hills c26ea52620 [lvgl] Triggers on tabview tabs fix (#15935) 2026-04-28 12:58:29 +12:00
39a69385fb [image] Fix RGB565+alpha rendering for multi-frame animations (#16017)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-28 12:57:42 +12:00
Jesse HillsandGitHub a34836c290 [esp32_touch] Feed wdt (#16066) 2026-04-28 11:27:08 +12:00
Edward FirmoandGitHub 01ac223913 [nextion] Unify TFT upload ack timeout to 5000ms (#15960) 2026-04-28 08:30:40 +12:00
7198c912c7 [esp32][wifi] Fix bootloop and WiFi connection issue if nvs partition is missing or has non-default label (#16025)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-27 12:41:28 -05:00
Kevin AhrendtandGitHub 24c6a0d711 [audio] Bump microDecoder library to v0.2.0 (#16054) 2026-04-27 12:17:02 +00:00
plazarreGitHubPaul Lazarrepre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
dec5d0449b [esp32_ble_tracker] Hold COEX_PREFER_BT for the lifetime of any active connection (#16036)
Co-authored-by: Paul Lazarre <plazarre@gmail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-04-27 06:51:54 -05:00
J. Nick KostonandGitHub 79b741b8dc [core] Combine entity register + configure_entity_ into one call (#16030) 2026-04-27 03:03:39 +00:00
tomaszduda23andGitHub 112646a9c4 [zigbee] add router for nrf52 (#16034) 2026-04-26 23:02:09 -04:00
J. Nick KostonandGitHub 2e096bb036 [core] Combine set_component_source_ + register_component_ into one call (#16029) 2026-04-26 21:54:15 -05:00
e87e78c544 [api] Expose TemperatureUnit in water heater and climate api (#15815)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-04-26 12:58:14 +00:00
J. Nick KostonandGitHub 0f25d91e68 [core] Unify skip_external_update and honor it in external_files for faster esphome logs (#16016) 2026-04-26 07:24:33 -05:00
J. Nick KostonandGitHub 8dbdcfc128 [bk72xx] Prepare for BK7238 support (#16018) 2026-04-26 07:24:07 -05:00
J. Nick KostonandGitHub 8950afc3c4 [bluetooth_proxy] Drop redundant remote_bda_ write in connect handler (#16000) 2026-04-26 07:23:53 -05:00
J. Nick KostonandGitHub 04d067196d [rotary_encoder][at581x] Fix templatable int field types (#16015) 2026-04-26 07:23:41 -05:00
J. Nick KostonandGitHub 502c010465 [bh1750] Downgrade per-reading Illuminance log to verbose (#16005) 2026-04-26 07:23:24 -05:00
J. Nick KostonandGitHub 180105bb4b [bluetooth_proxy] Partial revert of loop() → set_interval migration (#15992) 2026-04-26 07:23:08 -05:00
J. Nick KostonandGitHub 4c0dfb0e0d [core] Raise ESP32 WDT feed interval to 1/5 of configured timeout (#15984) 2026-04-26 07:22:50 -05:00
J. Nick KostonandGitHub df987a7ffb [ci-custom] Suggest uint32_to_str/int8_to_str for integer formatting (#15970) 2026-04-26 07:22:34 -05:00
Boris KrivonogandGitHub c8d4420408 [mitsubishi_cn105] add support for half-degree temperature setpoint (#15919) 2026-04-26 07:19:49 -05:00
b084fa4490 [esp32] Make ESP-IDF builds reproducible (#16008)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-26 06:31:32 -05:00
68625a1b76 [core] Isolate generated build metadata (#16007)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-26 09:11:09 +00:00
J. Nick KostonandGitHub dc57969afd [host] Use integer math in millis()/micros() (#15994) 2026-04-26 08:39:24 +00:00
J. Nick KostonandGitHub f092e619d8 [rtttl] Gate on_finished_playback callback storage behind define (#16003) 2026-04-26 00:03:59 -05:00
J. Nick KostonandGitHub 58f6ad2d0c [safe_mode] Use StaticCallbackManager for on_safe_mode (#16002) 2026-04-26 00:01:21 -05:00
bc33260c61 [ir_rf_proxy] Extend for RF (#15744)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-25 22:33:02 -05:00
J. Nick KostonandGitHub 4cab262ef8 [ci] Trigger CodSpeed benchmarks on host platform changes (#15995) 2026-04-25 17:18:21 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9ad820c921 Bump esphome-dashboard from 20260408.1 to 20260425.0 (#16006)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-25 20:59:01 +00:00
J. Nick KostonandGitHub 4f8feb86f0 [dashboard] Add --no-states support to logs WebSocket handler (#15993) 2026-04-25 15:43:05 -05:00
b5ccd55f4e [packages] Fix premature substitution of vars in remote package files (#15997)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-25 17:06:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a437b3086b Bump cryptography from 46.0.7 to 47.0.0 (#15990)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-25 02:30:10 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c27f9e512b Bump aioesphomeapi from 44.21.0 to 44.22.0 (#15989)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-25 02:28:04 +00:00
325 changed files with 9374 additions and 2271 deletions
+29 -4
View File
@@ -5,24 +5,30 @@ Checks: >-
-altera-*,
-android-*,
-boost-*,
-bugprone-derived-method-shadowing-base-method,
-bugprone-easily-swappable-parameters,
-bugprone-implicit-widening-of-multiplication-result,
-bugprone-invalid-enum-default-initialization,
-bugprone-multi-level-implicit-pointer-conversion,
-bugprone-narrowing-conversions,
-bugprone-tagged-union-member-count,
-bugprone-signed-char-misuse,
-bugprone-switch-missing-default-case,
-cert-dcl50-cpp,
-cert-err33-c,
-cert-err58-cpp,
-cert-int09-c,
-cert-oop57-cpp,
-cert-str34-c,
-clang-analyzer-optin.core.EnumCastOutOfRange,
-clang-analyzer-optin.cplusplus.UninitializedObject,
-clang-analyzer-osx.*,
-clang-analyzer-security.ArrayBound,
-clang-diagnostic-delete-abstract-non-virtual-dtor,
-clang-diagnostic-delete-non-abstract-non-virtual-dtor,
-clang-diagnostic-deprecated-declarations,
-clang-diagnostic-ignored-optimization-argument,
-clang-diagnostic-missing-designated-field-initializers,
-clang-diagnostic-missing-field-initializers,
-clang-diagnostic-shadow-field,
-clang-diagnostic-unused-const-variable,
@@ -42,6 +48,7 @@ Checks: >-
-cppcoreguidelines-owning-memory,
-cppcoreguidelines-prefer-member-initializer,
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
-cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
-cppcoreguidelines-pro-bounds-constant-array-index,
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
-cppcoreguidelines-pro-type-const-cast,
@@ -54,12 +61,13 @@ Checks: >-
-cppcoreguidelines-rvalue-reference-param-not-moved,
-cppcoreguidelines-special-member-functions,
-cppcoreguidelines-use-default-member-init,
-cppcoreguidelines-use-enum-class,
-cppcoreguidelines-virtual-class-destructor,
-fuchsia-default-arguments-calls,
-fuchsia-default-arguments-declarations,
-fuchsia-multiple-inheritance,
-fuchsia-overloaded-operator,
-fuchsia-statically-constructed-objects,
-fuchsia-default-arguments-declarations,
-fuchsia-default-arguments-calls,
-google-build-using-namespace,
-google-explicit-constructor,
-google-readability-braces-around-statements,
@@ -71,16 +79,23 @@ Checks: >-
-llvm-else-after-return,
-llvm-header-guard,
-llvm-include-order,
-llvm-prefer-static-over-anonymous-namespace,
-llvm-qualified-auto,
-llvm-use-ranges,
-llvmlibc-*,
-misc-const-correctness,
-misc-include-cleaner,
-misc-multiple-inheritance,
-misc-no-recursion,
-misc-non-private-member-variables-in-classes,
-misc-override-with-different-visibility,
-misc-unused-parameters,
-misc-use-anonymous-namespace,
-misc-use-internal-linkage,
-modernize-avoid-bind,
-modernize-avoid-variadic-functions,
-modernize-avoid-c-arrays,
-modernize-avoid-c-style-cast,
-modernize-concat-nested-namespaces,
-modernize-macro-to-enum,
-modernize-return-braced-init-list,
@@ -88,32 +103,42 @@ Checks: >-
-modernize-use-auto,
-modernize-use-constraints,
-modernize-use-default-member-init,
-modernize-use-designated-initializers,
-modernize-use-equals-default,
-modernize-use-integer-sign-comparison,
-modernize-use-nodiscard,
-modernize-use-nullptr,
-modernize-use-nodiscard,
-modernize-use-nullptr,
-modernize-use-ranges,
-modernize-use-trailing-return-type,
-mpi-*,
-objc-*,
-performance-enum-size,
-portability-avoid-pragma-once,
-portability-template-virtual-member-function,
-readability-ambiguous-smartptr-reset-call,
-readability-avoid-nested-conditional-operator,
-readability-container-contains,
-readability-container-data-pointer,
-readability-convert-member-functions-to-static,
-readability-else-after-return,
-readability-enum-initial-value,
-readability-function-cognitive-complexity,
-readability-implicit-bool-conversion,
-readability-isolate-declaration,
-readability-magic-numbers,
-readability-make-member-function-const,
-readability-math-missing-parentheses,
-readability-named-parameter,
-readability-redundant-casting,
-readability-redundant-inline-specifier,
-readability-redundant-member-init,
-readability-redundant-parentheses,
-readability-redundant-string-init,
-readability-redundant-typename,
-readability-uppercase-literal-suffix,
-readability-use-anyofallof,
-readability-use-std-min-max,
-readability-use-concise-preprocessor-directives,
WarningsAsErrors: '*'
FormatStyle: google
CheckOptions:
+1 -1
View File
@@ -1 +1 @@
1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324
0c7f309d70eca8e3efd510092ddb23c530f3934c49371717efa124b788d761f8
+2 -2
View File
@@ -27,9 +27,9 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v2
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
- name: Auto Label PR
+33 -3
View File
@@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -58,7 +58,7 @@ jobs:
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_test.txt pre-commit
pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
pip install -e .
pylint:
@@ -108,6 +108,34 @@ jobs:
script/generate-esp32-boards.py --check
script/generate-rp2040-boards.py --check
import-time:
name: Check import esphome.__main__ time
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.import-time == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Check import time against budget and write waterfall HAR
run: |
. venv/bin/activate
script/check_import_time.py --check --har importtime.har
- name: Upload waterfall HAR
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: import-time-waterfall
path: importtime.har
if-no-files-found: ignore
retention-days: 14
pytest:
name: Run pytest
strategy:
@@ -176,6 +204,7 @@ jobs:
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
python-linters: ${{ steps.determine.outputs.python-linters }}
import-time: ${{ steps.determine.outputs.import-time }}
changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
@@ -219,6 +248,7 @@ jobs:
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
@@ -339,7 +369,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation
+1 -1
View File
@@ -8,4 +8,4 @@ on:
jobs:
lock:
uses: esphome/workflows/.github/workflows/lock.yml@3c4e8446aa1029f1c346a482034b3ee1489077ca # 2026.4.0
uses: esphome/workflows/.github/workflows/lock.yml@025a1e6255610c498ed590403b7e510b69e474df # 2026.4.1
+3 -3
View File
@@ -223,7 +223,7 @@ jobs:
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: home-assistant-addon
@@ -258,7 +258,7 @@ jobs:
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: esphome-schema
@@ -289,7 +289,7 @@ jobs:
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
client-id: ${{ vars.ESPHOME_GITHUB_APP_CLIENT_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
owner: esphome
repositories: version-notifier
+1
View File
@@ -146,5 +146,6 @@ sdkconfig.*
/components
/managed_components
/dependencies.lock
api-docs/
+1
View File
@@ -347,6 +347,7 @@ esphome/components/modbus_controller/select/* @martgras @stegm
esphome/components/modbus_controller/sensor/* @martgras
esphome/components/modbus_controller/switch/* @martgras
esphome/components/modbus_controller/text_sensor/* @martgras
esphome/components/modbus_server/* @exciton
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
esphome/components/mopeka_pro_check/* @spbrogan
esphome/components/mopeka_std_check/* @Fabian-Schmidt
+25 -4
View File
@@ -21,7 +21,7 @@ import argcomplete
# Note: Do not import modules from esphome.components here, as this would
# cause them to be loaded before external components are processed, resulting
# in the built-in version being used instead of the external component one.
from esphome import const, writer, yaml_util
from esphome import const
import esphome.codegen as cg
from esphome.config import iter_component_configs, read_config, strip_default_ids
from esphome.const import (
@@ -72,7 +72,12 @@ from esphome.util import (
run_external_process,
safe_print,
)
from esphome.zeroconf import discover_mdns_devices
# Keep expensive imports (zeroconf, writer, yaml_util, etc.) out of this
# module's top level. Every `esphome` invocation — including fast paths
# like `esphome version` — pays the cost of what's imported here before
# any command runs. Import inside the function that needs it instead.
# `script/check_import_time.py` enforces a budget in CI.
_LOGGER = logging.getLogger(__name__)
@@ -241,6 +246,8 @@ def _discover_mac_suffix_devices() -> list[str] | None:
"""
if not (has_name_add_mac_suffix() and has_mdns() and has_non_ip_address()):
return None
from esphome.zeroconf import discover_mdns_devices
_LOGGER.info("Discovering devices...")
if not (discovered := discover_mdns_devices(CORE.name)):
_LOGGER.warning(
@@ -660,7 +667,7 @@ def run_miniterm(config: ConfigType, port: str, args) -> int:
return 0
def wrap_to_code(name, comp):
def _wrap_to_code(name, comp, yaml_util):
coro = coroutine(comp.to_code)
@functools.wraps(comp.to_code)
@@ -680,6 +687,8 @@ def wrap_to_code(name, comp):
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
from esphome import writer
if not get_bool_env(ENV_NOGITIGNORE):
writer.write_gitignore()
@@ -691,17 +700,21 @@ def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
def generate_cpp_contents(config: ConfigType) -> None:
from esphome import yaml_util
_LOGGER.info("Generating C++ source...")
for name, component, conf in iter_component_configs(CORE.config):
if component.to_code is not None:
coro = wrap_to_code(name, component)
coro = _wrap_to_code(name, component, yaml_util)
CORE.add_job(coro, conf)
CORE.flush_tasks()
def write_cpp_file(native_idf: bool = False) -> int:
from esphome import writer
code_s = indent(CORE.cpp_main_section)
writer.write_cpp(code_s)
@@ -1180,6 +1193,8 @@ def command_wizard(args: ArgsProtocol) -> int | None:
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import yaml_util
if not CORE.verbose:
config = strip_default_ids(config)
output = yaml_util.dump(config, args.show_secrets)
@@ -1321,6 +1336,8 @@ def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
def command_clean_all(args: ArgsProtocol) -> int | None:
from esphome import writer
try:
writer.clean_all(args.configuration)
except OSError as err:
@@ -1336,6 +1353,8 @@ def command_version(args: ArgsProtocol) -> int | None:
def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import writer
try:
writer.clean_build()
except OSError as err:
@@ -1538,6 +1557,8 @@ def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
from esphome import yaml_util
new_name = args.name
for c in new_name:
if c not in ALLOWED_NAME_CHARS:
+13 -6
View File
@@ -793,8 +793,11 @@ class MemoryAnalyzer:
"""Scan ESPHome source object files to map extern "C" symbols to components.
When no linker map file is available, this uses ``nm`` to scan ``.o`` files
under ``src/esphome/`` and build a symbol-to-component mapping. This catches
``extern "C"`` functions and other symbols that lack C++ namespace prefixes.
under ``src/`` (including ``src/main.cpp.o`` and everything beneath
``src/esphome/``) and build a symbol-to-component mapping. This catches
``extern "C"`` functions, the ESPHome-generated ``setup()``/``loop()``
entry points in ``main.cpp``, and other symbols that lack C++ namespace
prefixes.
Skips scanning if ``_source_symbol_map`` was already populated by
``_parse_map_file()``.
@@ -806,12 +809,12 @@ class MemoryAnalyzer:
if obj_dir is None:
return
# Find ESPHome source object files
esphome_src_dir = obj_dir / "src" / "esphome"
if not esphome_src_dir.is_dir():
# Scan all ESPHome-owned source object files: src/main.cpp.o and src/esphome/...
src_dir = obj_dir / "src"
if not src_dir.is_dir():
return
obj_files = sorted(esphome_src_dir.rglob("*.o"))
obj_files = sorted(src_dir.rglob("*.o"))
if not obj_files:
return
@@ -1064,6 +1067,10 @@ class MemoryAnalyzer:
if component_name in self.external_components:
return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
# ESPHome-generated entry point: src/main.cpp.o (contains setup()/loop())
if len(parts) >= 2 and parts[-2:] == ("src", "main.cpp.o"):
return _COMPONENT_CORE
# ESPHome core: src/esphome/core/... or src/esphome/...
if "core" in parts and "esphome" in parts:
return _COMPONENT_CORE
+2 -3
View File
@@ -127,7 +127,7 @@ def validate_potentially_or_condition(value):
return validate_condition(value)
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
DelayAction = cg.esphome_ns.class_("DelayAction", Action)
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
IfAction = cg.esphome_ns.class_("IfAction", Action)
@@ -396,7 +396,6 @@ async def delay_action_to_code(
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_component(var, {})
template_ = await cg.templatable(config, args, cg.uint32)
cg.add(var.set_delay(template_))
return var
@@ -597,7 +596,7 @@ async def component_resume_action_to_code(
comp = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, comp)
if CONF_UPDATE_INTERVAL in config:
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int)
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
cg.add(var.set_update_interval(template_))
return var
@@ -129,7 +129,7 @@ AdalightLightEffect::Frame AdalightLightEffect::parse_frame_(light::AddressableL
uint8_t *led_data = &frame_[6];
for (int led = 0; led < accepted_led_count; led++, led_data += 3) {
auto white = std::min(std::min(led_data[0], led_data[1]), led_data[2]);
auto white = std::min({led_data[0], led_data[1], led_data[2]});
it[led].set(Color(led_data[0], led_data[1], led_data[2], white));
}
@@ -13,7 +13,11 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@grahambrown11", "@hwstar"]
@@ -181,7 +185,7 @@ async def setup_alarm_control_panel_core_(var, config):
async def register_alarm_control_panel(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_alarm_control_panel(var))
queue_entity_register("alarm_control_panel", config)
CORE.register_platform_component("alarm_control_panel", var)
await setup_alarm_control_panel_core_(var, config)
+6 -1
View File
@@ -62,7 +62,12 @@ void Animation::set_frame(int frame) {
}
void Animation::update_data_start_() {
const uint32_t image_size = this->get_width_stride() * this->height_;
uint32_t image_size = this->get_width_stride() * this->height_;
// RGB565 with an alpha channel stores the alpha plane immediately after the RGB
// plane within each frame, so the per-frame stride includes the alpha bytes.
if (this->type_ == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
image_size += static_cast<uint32_t>(this->width_) * this->height_;
}
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
}
+12 -1
View File
@@ -1025,6 +1025,13 @@ message CameraImageRequest {
bool stream = 2;
}
// ==================== TEMPERATURE UNIT ====================
enum TemperatureUnit {
TEMPERATURE_UNIT_CELSIUS = 0;
TEMPERATURE_UNIT_FAHRENHEIT = 1;
TEMPERATURE_UNIT_KELVIN = 2;
}
// ==================== CLIMATE ====================
enum ClimateMode {
CLIMATE_MODE_OFF = 0;
@@ -1110,6 +1117,7 @@ message ListEntitiesClimateResponse {
float visual_max_humidity = 25;
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
uint32 feature_flags = 27;
TemperatureUnit temperature_unit = 28;
}
message ClimateStateResponse {
option (id) = 47;
@@ -1203,6 +1211,7 @@ message ListEntitiesWaterHeaterResponse {
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
// Bitmask of WaterHeaterFeature flags
uint32 supported_features = 12;
TemperatureUnit temperature_unit = 13;
}
message WaterHeaterStateResponse {
@@ -1410,6 +1419,8 @@ enum LockState {
LOCK_STATE_JAMMED = 3;
LOCK_STATE_LOCKING = 4;
LOCK_STATE_UNLOCKING = 5;
LOCK_STATE_OPENING = 6;
LOCK_STATE_OPEN = 7;
}
enum LockCommand {
LOCK_UNLOCK = 0;
@@ -1628,7 +1639,7 @@ message BluetoothLEAdvertisementResponse {
message BluetoothLERawAdvertisement {
option (inline_encode) = true;
uint64 address = 1 [(force) = true];
uint64 address = 1 [(force) = true, (mac_address) = true];
sint32 rssi = 2 [(force) = true];
uint32 address_type = 3 [(max_value) = 4];
+6
View File
@@ -23,6 +23,9 @@
#ifdef USE_ESP8266_CRASH_HANDLER
#include "esphome/components/esp8266/crash_handler.h"
#endif
#ifdef USE_BK72XX_CRASH_HANDLER
#include "esphome/components/bk72xx/crash_handler.h"
#endif
#include "esphome/core/entity_base.h"
#include "esphome/core/string_ref.h"
@@ -283,6 +286,9 @@ class APIConnection final : public APIServerConnectionBase {
#endif
#ifdef USE_ESP8266_CRASH_HANDLER
esp8266::crash_handler_log();
#endif
#ifdef USE_BK72XX_CRASH_HANDLER
bk72xx::crash_handler_log();
#endif
}
#ifdef USE_API_HOMEASSISTANT_SERVICES
+6
View File
@@ -110,4 +110,10 @@ extend google.protobuf.FieldOptions {
// length varint calculations and direct byte writes, since the length
// varint is guaranteed to be 1 byte.
optional uint32 max_data_length = 50018;
// mac_address: Field is a 48-bit MAC address stored in a uint64.
// Emits encode_varint_raw_48bit which has a 7-byte fast path that avoids
// the per-byte loop when the upper bits are non-zero (the common case
// for real MAC addresses, since OUIs occupy the top 24 bits).
optional bool mac_address = 50019 [default=false];
}
+6 -2
View File
@@ -1439,6 +1439,7 @@ uint8_t *ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 26, this->device_id);
#endif
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 27, this->feature_flags);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 28, static_cast<uint32_t>(this->temperature_unit));
return pos;
}
uint32_t ListEntitiesClimateResponse::calculate_size() const {
@@ -1488,6 +1489,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const {
size += ProtoSize::calc_uint32(2, this->device_id);
#endif
size += ProtoSize::calc_uint32(2, this->feature_flags);
size += this->temperature_unit ? 3 : 0;
return size;
}
uint8_t *ClimateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1645,6 +1647,7 @@ uint8_t *ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer PROTO_
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(it), true);
}
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supported_features);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, static_cast<uint32_t>(this->temperature_unit));
return pos;
}
uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const {
@@ -1667,6 +1670,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const {
size += this->supported_modes->size() * 2;
}
size += ProtoSize::calc_uint32(1, this->supported_features);
size += this->temperature_unit ? 2 : 0;
return size;
}
uint8_t *WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -2348,7 +2352,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
uint8_t *len_pos = pos;
ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi));
if (sub_msg.address_type) {
@@ -2369,7 +2373,7 @@ BluetoothLERawAdvertisementsResponse::calculate_size() const {
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];
size += 2;
size += ProtoSize::calc_uint64_force(1, sub_msg.address);
size += ProtoSize::calc_uint64_48bit_force(1, sub_msg.address);
size += ProtoSize::calc_sint32_force(1, sub_msg.rssi);
size += sub_msg.address_type ? 2 : 0;
size += 2 + sub_msg.data_len;
+11 -2
View File
@@ -92,6 +92,11 @@ enum SupportsResponseType : uint32_t {
SUPPORTS_RESPONSE_STATUS = 100,
};
#endif
enum TemperatureUnit : uint32_t {
TEMPERATURE_UNIT_CELSIUS = 0,
TEMPERATURE_UNIT_FAHRENHEIT = 1,
TEMPERATURE_UNIT_KELVIN = 2,
};
#ifdef USE_CLIMATE
enum ClimateMode : uint32_t {
CLIMATE_MODE_OFF = 0,
@@ -176,6 +181,8 @@ enum LockState : uint32_t {
LOCK_STATE_JAMMED = 3,
LOCK_STATE_LOCKING = 4,
LOCK_STATE_UNLOCKING = 5,
LOCK_STATE_OPENING = 6,
LOCK_STATE_OPEN = 7,
};
enum LockCommand : uint32_t {
LOCK_UNLOCK = 0,
@@ -1372,7 +1379,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 46;
static constexpr uint8_t ESTIMATED_SIZE = 150;
static constexpr uint8_t ESTIMATED_SIZE = 153;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_climate_response"); }
#endif
@@ -1394,6 +1401,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
uint32_t feature_flags{0};
enums::TemperatureUnit temperature_unit{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1471,7 +1479,7 @@ class ClimateCommandRequest final : public CommandProtoMessage {
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 132;
static constexpr uint8_t ESTIMATED_SIZE = 63;
static constexpr uint8_t ESTIMATED_SIZE = 65;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_water_heater_response"); }
#endif
@@ -1480,6 +1488,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
float target_temperature_step{0.0f};
const water_heater::WaterHeaterModeMask *supported_modes{};
uint32_t supported_features{0};
enums::TemperatureUnit temperature_unit{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
+18
View File
@@ -297,6 +297,18 @@ template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::
}
}
#endif
template<> const char *proto_enum_to_string<enums::TemperatureUnit>(enums::TemperatureUnit value) {
switch (value) {
case enums::TEMPERATURE_UNIT_CELSIUS:
return ESPHOME_PSTR("TEMPERATURE_UNIT_CELSIUS");
case enums::TEMPERATURE_UNIT_FAHRENHEIT:
return ESPHOME_PSTR("TEMPERATURE_UNIT_FAHRENHEIT");
case enums::TEMPERATURE_UNIT_KELVIN:
return ESPHOME_PSTR("TEMPERATURE_UNIT_KELVIN");
default:
return ESPHOME_PSTR("UNKNOWN");
}
}
#ifdef USE_CLIMATE
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
switch (value) {
@@ -475,6 +487,10 @@ template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState v
return ESPHOME_PSTR("LOCK_STATE_LOCKING");
case enums::LOCK_STATE_UNLOCKING:
return ESPHOME_PSTR("LOCK_STATE_UNLOCKING");
case enums::LOCK_STATE_OPENING:
return ESPHOME_PSTR("LOCK_STATE_OPENING");
case enums::LOCK_STATE_OPEN:
return ESPHOME_PSTR("LOCK_STATE_OPEN");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -1539,6 +1555,7 @@ const char *ListEntitiesClimateResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
#endif
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit));
return out.c_str();
}
const char *ClimateStateResponse::dump_to(DumpBuffer &out) const {
@@ -1612,6 +1629,7 @@ const char *ListEntitiesWaterHeaterResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast<enums::WaterHeaterMode>(it), 4);
}
dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features);
dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit));
return out.c_str();
}
const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const {
@@ -21,6 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
}
#endif
#ifdef USE_API
void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements
switch (msg_type) {
@@ -706,5 +707,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
break;
}
}
#endif // USE_API
} // namespace esphome::api
+5
View File
@@ -30,6 +30,11 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c
APIServer::APIServer() { global_api_server = this; }
// Custom deleter defined here so `delete` sees the complete APIConnection type.
// This prevents libc++ from emitting an "incomplete type" error when other
// translation units only have the forward declaration of APIConnection.
void APIServer::APIConnectionDeleter::operator()(APIConnection *p) const { delete p; }
void APIServer::socket_failed_(const LogString *msg) {
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
this->destroy_socket_();
+8 -2
View File
@@ -193,7 +193,13 @@ class APIServer final : public Component,
// Range-for view over the populated slice [0, api_connection_count_). Read-only with respect
// to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the
// APIConnection but cannot reset/move the slot and break the count invariant.
using APIConnectionPtr = std::unique_ptr<APIConnection>;
// Custom deleter is defined out-of-line in api_server.cpp so libc++ does not
// eagerly instantiate `delete static_cast<APIConnection *>(p)` here, where
// only the forward declaration of APIConnection is visible (incomplete type).
struct APIConnectionDeleter {
void operator()(APIConnection *p) const;
};
using APIConnectionPtr = std::unique_ptr<APIConnection, APIConnectionDeleter>;
class ActiveClientsView {
const APIConnectionPtr *begin_;
const APIConnectionPtr *end_;
@@ -292,7 +298,7 @@ class APIServer final : public Component,
uint32_t last_connected_{0};
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
std::array<std::unique_ptr<APIConnection>, MAX_API_CONNECTIONS> clients_{};
std::array<APIConnectionPtr, MAX_API_CONNECTIONS> clients_{};
// Vectors and strings (12 bytes each on 32-bit)
// Shared proto write buffer for all connections.
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
@@ -78,7 +78,8 @@ class ActionResponse {
: success_(success), error_message_(error_message) {
if (data == nullptr || data_len == 0)
return;
this->json_document_ = json::parse_json(data, data_len);
JsonDocument tmp = json::parse_json(data, data_len);
swap(this->json_document_, tmp);
}
#endif
+35
View File
@@ -342,6 +342,32 @@ class ProtoEncode {
}
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
}
/// Encode a 48-bit MAC address (stored in a uint64) as varint.
/// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the
/// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes
/// with no per-byte branch. Falls back to the general loop otherwise.
/// Caller must guarantee value fits in 48 bits (checked in debug builds).
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint64_t value) {
#ifdef ESPHOME_DEBUG_API
assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits");
#endif
// 7-byte varint holds 49 bits (7 * 7), so a 48-bit value needs all 7 bytes
// whenever bit 42 or higher is set (i.e. value >= 1 << (48 - 6)).
if (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6))) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 7);
pos[0] = static_cast<uint8_t>(value | 0x80);
pos[1] = static_cast<uint8_t>((value >> 7) | 0x80);
pos[2] = static_cast<uint8_t>((value >> 14) | 0x80);
pos[3] = static_cast<uint8_t>((value >> 21) | 0x80);
pos[4] = static_cast<uint8_t>((value >> 28) | 0x80);
pos[5] = static_cast<uint8_t>((value >> 35) | 0x80);
pos[6] = static_cast<uint8_t>(value >> 42);
pos += 7;
return;
}
encode_varint_raw_64(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);
@@ -398,6 +424,7 @@ class ProtoEncode {
if (len == 0 && !force)
return;
encode_field_raw(pos PROTO_ENCODE_DEBUG_ARG, field_id, 2); // type 2: Length-delimited string
// NOLINTNEXTLINE(readability-inconsistent-ifelse-braces) -- false positive on [[likely]] attribute
if (len < VARINT_MAX_1_BYTE) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 1 + len);
*pos++ = static_cast<uint8_t>(len);
@@ -817,6 +844,14 @@ class ProtoSize {
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
return field_id_size + varint(value);
}
/// 48-bit MAC address variant: matches encode_varint_raw_48bit's fast path.
/// When any of the top 6 of 48 bits is set the encoded varint is 7 bytes;
/// otherwise fall back to the general size calculation.
/// Caller must guarantee value fits in 48 bits (encoder asserts in debug).
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_48bit_force(uint32_t field_id_size,
uint64_t value) {
return field_id_size + (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6)) ? 7 : varint(value));
}
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
}
+1 -5
View File
@@ -14,11 +14,7 @@ class AQICalculator : public AbstractAQICalculator {
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
float aqi = std::max(pm2_5_index, pm10_0_index);
if (aqi < 0.0f) {
aqi = 0.0f;
}
float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f});
return static_cast<uint16_t>(std::lround(aqi));
}
+1 -5
View File
@@ -12,11 +12,7 @@ class CAQICalculator : public AbstractAQICalculator {
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
float aqi = std::max(pm2_5_index, pm10_0_index);
if (aqi < 0.0f) {
aqi = 0.0f;
}
float aqi = std::max({pm2_5_index, pm10_0_index, 0.0f});
return static_cast<uint16_t>(std::lround(aqi));
}
+4 -4
View File
@@ -183,19 +183,19 @@ async def at581x_settings_to_code(config, action_id, template_arg, args):
cg.add(var.set_sensing_distance(template_))
if selfcheck := config.get(CONF_POWERON_SELFCHECK_TIME):
template_ = await cg.templatable(selfcheck, args, cg.int32)
template_ = await cg.templatable(selfcheck, args, cg.int_)
cg.add(var.set_poweron_selfcheck_time(template_))
if protect := config.get(CONF_PROTECT_TIME):
template_ = await cg.templatable(protect, args, cg.int32)
template_ = await cg.templatable(protect, args, cg.int_)
cg.add(var.set_protect_time(template_))
if trig_base := config.get(CONF_TRIGGER_BASE):
template_ = await cg.templatable(trig_base, args, cg.int32)
template_ = await cg.templatable(trig_base, args, cg.int_)
cg.add(var.set_trigger_base(template_))
if trig_keep := config.get(CONF_TRIGGER_KEEP):
template_ = await cg.templatable(trig_keep, args, cg.int32)
template_ = await cg.templatable(trig_keep, args, cg.int_)
cg.add(var.set_trigger_keep(template_))
if (stage_gain := config.get(CONF_STAGE_GAIN)) is not None:
+194 -6
View File
@@ -1,4 +1,4 @@
from dataclasses import dataclass
from dataclasses import dataclass, field
import esphome.codegen as cg
from esphome.components.esp32 import (
@@ -7,7 +7,12 @@ from esphome.components.esp32 import (
include_builtin_idf_component,
)
import esphome.config_validation as cv
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_NUM_CHANNELS, CONF_SAMPLE_RATE
from esphome.const import (
CONF_BITS_PER_SAMPLE,
CONF_NUM_CHANNELS,
CONF_SAMPLE_RATE,
CONF_SIZE,
)
from esphome.core import CORE
import esphome.final_validate as fv
@@ -25,13 +30,46 @@ AUDIO_FILE_TYPE_ENUM = {
"OPUS": AudioFileType.OPUS,
}
MEMORY_PSRAM = "psram"
MEMORY_INTERNAL = "internal"
MEMORY_LOCATIONS = [MEMORY_PSRAM, MEMORY_INTERNAL]
@dataclass
class FlacOptions:
buffer_memory: str | None = None
@dataclass
class Mp3Options:
buffer_memory: str | None = None
@dataclass
class OpusPseudostackOptions:
threadsafe: bool | None = None
buffer_memory: str | None = None
size: int | None = None
@dataclass
class OpusOptions:
floating_point: bool | None = None
state_memory: str | None = None
pseudostack: OpusPseudostackOptions = field(default_factory=OpusPseudostackOptions)
@dataclass
class AudioData:
flac_support: bool = False
mp3_support: bool = False
opus_support: bool = False
# WAV defaults to True for backward compatibility; will become opt-in in a future release
wav_support: bool = True
micro_decoder_support: bool = False
flac: FlacOptions = field(default_factory=FlacOptions)
mp3: Mp3Options = field(default_factory=Mp3Options)
opus: OpusOptions = field(default_factory=OpusOptions)
def _get_data() -> AudioData:
@@ -55,6 +93,11 @@ def request_opus_support() -> None:
_get_data().opus_support = True
def request_wav_support() -> None:
"""Request WAV codec support for audio decoding."""
_get_data().wav_support = True
def request_micro_decoder_support() -> None:
"""Request micro-decoder library support for audio decoding."""
_get_data().micro_decoder_support = True
@@ -67,9 +110,78 @@ CONF_MAX_CHANNELS = "max_channels"
CONF_MIN_SAMPLE_RATE = "min_sample_rate"
CONF_MAX_SAMPLE_RATE = "max_sample_rate"
CONF_CODECS = "codecs"
CONF_WAV = "wav"
CONF_FLAC = "flac"
CONF_MP3 = "mp3"
CONF_OPUS = "opus"
CONF_BUFFER_MEMORY = "buffer_memory"
CONF_FLOATING_POINT = "floating_point"
CONF_STATE_MEMORY = "state_memory"
CONF_PSEUDOSTACK = "pseudostack"
CONF_THREADSAFE = "threadsafe"
_MEMORY_LOCATION_VALIDATOR = cv.one_of(*MEMORY_LOCATIONS, lower=True)
def _maybe_empty_codec(schema):
"""Wrap a codec dict schema so that a bare key (None value) is treated as an empty dict."""
def validator(value):
if value is None:
value = {}
return schema(value)
return validator
CODEC_FLAC_SCHEMA = cv.Schema(
{
cv.Optional(CONF_BUFFER_MEMORY): _MEMORY_LOCATION_VALIDATOR,
}
)
CODEC_MP3_SCHEMA = cv.Schema(
{
cv.Optional(CONF_BUFFER_MEMORY): _MEMORY_LOCATION_VALIDATOR,
}
)
OPUS_PSEUDOSTACK_SCHEMA = cv.Schema(
{
cv.Optional(CONF_THREADSAFE): cv.boolean,
cv.Optional(CONF_BUFFER_MEMORY): _MEMORY_LOCATION_VALIDATOR,
cv.Optional(CONF_SIZE): cv.int_range(60000, 240000),
}
)
CODEC_OPUS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_FLOATING_POINT): cv.boolean,
cv.Optional(CONF_STATE_MEMORY): _MEMORY_LOCATION_VALIDATOR,
cv.Optional(CONF_PSEUDOSTACK): _maybe_empty_codec(OPUS_PSEUDOSTACK_SCHEMA),
}
)
CODEC_WAV_SCHEMA = cv.Schema({})
CODECS_SCHEMA = cv.Schema(
{
cv.Optional(CONF_FLAC): _maybe_empty_codec(CODEC_FLAC_SCHEMA),
cv.Optional(CONF_MP3): _maybe_empty_codec(CODEC_MP3_SCHEMA),
cv.Optional(CONF_OPUS): _maybe_empty_codec(CODEC_OPUS_SCHEMA),
cv.Optional(CONF_WAV): _maybe_empty_codec(CODEC_WAV_SCHEMA),
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema({}),
cv.Schema(
{
cv.Optional(CONF_CODECS): _maybe_empty_codec(CODECS_SCHEMA),
}
),
cv.only_on_esp32,
)
AUDIO_COMPONENT_SCHEMA = cv.Schema(
@@ -208,6 +320,15 @@ def final_validate_audio_schema(
)
def _emit_memory_pair(value: str | None, psram_key: str, internal_key: str) -> None:
if value == MEMORY_PSRAM:
add_idf_sdkconfig_option(psram_key, True)
add_idf_sdkconfig_option(internal_key, False)
elif value == MEMORY_INTERNAL:
add_idf_sdkconfig_option(psram_key, False)
add_idf_sdkconfig_option(internal_key, True)
async def to_code(config):
# Re-enable ESP-IDF's HTTP client (excluded by default to save compile time)
include_builtin_idf_component("esp_http_client")
@@ -219,8 +340,38 @@ async def to_code(config):
data = _get_data()
# Merge user-supplied codec configuration (additive: presence enables the codec)
if codecs_config := config.get(CONF_CODECS):
if (flac_config := codecs_config.get(CONF_FLAC)) is not None:
data.flac_support = True
if (buffer_memory := flac_config.get(CONF_BUFFER_MEMORY)) is not None:
data.flac.buffer_memory = buffer_memory
if (mp3_config := codecs_config.get(CONF_MP3)) is not None:
data.mp3_support = True
if (buffer_memory := mp3_config.get(CONF_BUFFER_MEMORY)) is not None:
data.mp3.buffer_memory = buffer_memory
if (opus_config := codecs_config.get(CONF_OPUS)) is not None:
data.opus_support = True
floating_point = opus_config.get(CONF_FLOATING_POINT)
if floating_point is not None:
data.opus.floating_point = floating_point
if (state_memory := opus_config.get(CONF_STATE_MEMORY)) is not None:
data.opus.state_memory = state_memory
if (pseudostack_config := opus_config.get(CONF_PSEUDOSTACK)) is not None:
threadsafe = pseudostack_config.get(CONF_THREADSAFE)
if threadsafe is not None:
data.opus.pseudostack.threadsafe = threadsafe
if (
buffer_memory := pseudostack_config.get(CONF_BUFFER_MEMORY)
) is not None:
data.opus.pseudostack.buffer_memory = buffer_memory
if (size := pseudostack_config.get(CONF_SIZE)) is not None:
data.opus.pseudostack.size = size
if CONF_WAV in codecs_config:
data.wav_support = True
if data.micro_decoder_support:
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
add_idf_component(name="esphome/micro-decoder", ref="0.2.0")
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
if not data.flac_support:
@@ -229,13 +380,50 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_MP3", False)
if not data.opus_support:
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_OPUS", False)
if not data.wav_support:
add_idf_sdkconfig_option("CONFIG_MICRO_DECODER_CODEC_WAV", False)
# Legacy audio_decoder.cpp support defines and components
# Configure each codec library.
# Adds a define and IDF component for legacy `audio_decoder.cpp`.
if data.flac_support:
cg.add_define("USE_AUDIO_FLAC_SUPPORT")
add_idf_component(name="esphome/micro-flac", ref="0.1.1")
_emit_memory_pair(
data.flac.buffer_memory,
"CONFIG_MICRO_FLAC_PREFER_PSRAM",
"CONFIG_MICRO_FLAC_PREFER_INTERNAL",
)
if data.mp3_support:
cg.add_define("USE_AUDIO_MP3_SUPPORT")
_emit_memory_pair(
data.mp3.buffer_memory,
"CONFIG_MP3_DECODER_PREFER_PSRAM",
"CONFIG_MP3_DECODER_PREFER_INTERNAL",
)
if data.opus_support:
cg.add_define("USE_AUDIO_OPUS_SUPPORT")
add_idf_component(name="esphome/micro-opus", ref="0.3.6")
add_idf_component(name="esphome/micro-opus", ref="0.4.0")
if data.opus.floating_point is not None:
add_idf_sdkconfig_option(
"CONFIG_OPUS_FLOATING_POINT", data.opus.floating_point
)
_emit_memory_pair(
data.opus.state_memory,
"CONFIG_OPUS_STATE_PREFER_PSRAM",
"CONFIG_OPUS_STATE_PREFER_INTERNAL",
)
if data.opus.pseudostack.threadsafe is True:
add_idf_sdkconfig_option("CONFIG_OPUS_THREADSAFE_PSEUDOSTACK", True)
add_idf_sdkconfig_option("CONFIG_OPUS_NONTHREADSAFE_PSEUDOSTACK", False)
elif data.opus.pseudostack.threadsafe is False:
add_idf_sdkconfig_option("CONFIG_OPUS_THREADSAFE_PSEUDOSTACK", False)
add_idf_sdkconfig_option("CONFIG_OPUS_NONTHREADSAFE_PSEUDOSTACK", True)
_emit_memory_pair(
data.opus.pseudostack.buffer_memory,
"CONFIG_OPUS_PSEUDOSTACK_PREFER_PSRAM",
"CONFIG_OPUS_PSEUDOSTACK_PREFER_INTERNAL",
)
if data.opus.pseudostack.size is not None:
add_idf_sdkconfig_option(
"CONFIG_OPUS_PSEUDOSTACK_SIZE", data.opus.pseudostack.size
)
+5 -13
View File
@@ -1,4 +1,5 @@
from dataclasses import dataclass, field
from functools import partial
import hashlib
import logging
from pathlib import Path
@@ -19,7 +20,7 @@ from esphome.const import (
)
from esphome.core import CORE, ID, HexInt
from esphome.cpp_generator import MockObj
from esphome.external_files import download_content
from esphome.external_files import download_web_files_in_config
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -63,15 +64,6 @@ def _compute_local_file_path(value: ConfigType) -> Path:
return base_dir / key
def _download_web_file(value: ConfigType) -> ConfigType:
url = value[CONF_URL]
path = _compute_local_file_path(value)
download_content(url, path)
_LOGGER.debug("download_web_file: path=%s", path)
return value
def _file_schema(value: ConfigType | str) -> ConfigType:
if isinstance(value, str):
return _validate_file_shorthand(value)
@@ -142,11 +134,10 @@ LOCAL_SCHEMA = cv.Schema(
}
)
WEB_SCHEMA = cv.All(
WEB_SCHEMA = cv.Schema(
{
cv.Required(CONF_URL): cv.url,
},
_download_web_file,
}
)
@@ -209,6 +200,7 @@ def _validate_supported_local_file(config: list[ConfigType]) -> list[ConfigType]
CONFIG_SCHEMA = cv.All(
cv.only_on_esp32,
cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
partial(download_web_files_in_config, path_for=_compute_local_file_path),
_validate_supported_local_file,
)
+1 -1
View File
@@ -154,7 +154,7 @@ void BH1750Sensor::loop() {
break;
}
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
ESP_LOGV(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
this->status_clear_warning();
this->publish_state(lx);
this->state_ = IDLE;
+2 -1
View File
@@ -62,6 +62,7 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -624,7 +625,7 @@ async def setup_binary_sensor_core_(var, config):
async def register_binary_sensor(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_binary_sensor(var))
queue_entity_register("binary_sensor", config)
CORE.register_platform_component("binary_sensor", var)
await setup_binary_sensor_core_(var, config)
@@ -50,29 +50,31 @@ void MultiClickTriggerBase::on_state_(bool state) {
return;
}
if (*this->at_index_ == this->timing_count_) {
// at_index_ has a value here (the !has_value() branch above returns).
size_t at_index = *this->at_index_;
if (at_index == this->timing_count_) {
this->trigger_();
return;
}
MultiClickTriggerEvent evt = this->timing_[*this->at_index_];
MultiClickTriggerEvent evt = this->timing_[at_index];
if (evt.max_length != 4294967294UL) {
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, at_index, evt.min_length, evt.max_length); // NOLINT
this->schedule_is_valid_(evt.min_length);
this->schedule_is_not_valid_(evt.max_length);
} else if (*this->at_index_ + 1 != this->timing_count_) {
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
} else if (at_index + 1 != this->timing_count_) {
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->schedule_is_valid_(evt.min_length);
} else {
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
this->is_valid_ = false;
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
}
*this->at_index_ = *this->at_index_ + 1;
this->at_index_ = at_index + 1;
}
void MultiClickTriggerBase::schedule_cooldown_() {
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
+498 -2
View File
@@ -16,6 +16,7 @@ from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
FAMILY_BK7231T,
FAMILY_BK7238,
FAMILY_BK7251,
)
@@ -24,16 +25,32 @@ BK72XX_BOARDS = {
"name": "WB2L_M1 Wi-Fi Module",
"family": FAMILY_BK7231N,
},
"xh-wb3s": {
"name": "NiceMCU XH-WB3S",
"family": FAMILY_BK7238,
},
"cbu": {
"name": "CBU Wi-Fi Module",
"family": FAMILY_BK7231N,
},
"t1-u": {
"name": "T1-U Wi-Fi Module",
"family": FAMILY_BK7238,
},
"generic-bk7238-tuya": {
"name": "Generic - BK7238 (Tuya T1)",
"family": FAMILY_BK7238,
},
"t1-m": {
"name": "T1-M Wi-Fi Module",
"family": FAMILY_BK7238,
},
"generic-bk7231t-qfn32-tuya": {
"name": "Generic - BK7231T (Tuya QFN32)",
"name": "Generic - BK7231T (Tuya)",
"family": FAMILY_BK7231T,
},
"generic-bk7231n-qfn32-tuya": {
"name": "Generic - BK7231N (Tuya QFN32)",
"name": "Generic - BK7231N (Tuya)",
"family": FAMILY_BK7231N,
},
"cb1s": {
@@ -64,6 +81,10 @@ BK72XX_BOARDS = {
"name": "Generic - BK7252",
"family": FAMILY_BK7251,
},
"t1-3s": {
"name": "T1-3S Wi-Fi Module",
"family": FAMILY_BK7238,
},
"wb2l": {
"name": "WB2L Wi-Fi Module",
"family": FAMILY_BK7231T,
@@ -80,6 +101,10 @@ BK72XX_BOARDS = {
"name": "CB2S Wi-Fi Module",
"family": FAMILY_BK7231N,
},
"generic-bk7238": {
"name": "Generic - BK7238",
"family": FAMILY_BK7238,
},
"wa2": {
"name": "WA2 Wi-Fi Module",
"family": FAMILY_BK7231Q,
@@ -100,6 +125,10 @@ BK72XX_BOARDS = {
"name": "WB3L Wi-Fi Module",
"family": FAMILY_BK7231T,
},
"t1-2s": {
"name": "T1-2S Wi-Fi Module",
"family": FAMILY_BK7238,
},
"wb2s": {
"name": "WB2S Wi-Fi Module",
"family": FAMILY_BK7231T,
@@ -158,6 +187,83 @@ BK72XX_BOARD_PINS = {
"D12": 22,
"A0": 23,
},
"xh-wb3s": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P7": 7,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM1": 7,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 7,
"D1": 23,
"D2": 14,
"D3": 26,
"D4": 24,
"D5": 6,
"D6": 9,
"D7": 0,
"D8": 1,
"D9": 8,
"D10": 10,
"D11": 11,
"D12": 16,
"D13": 20,
"D14": 21,
"D15": 22,
"D16": 15,
"D17": 17,
"A0": 28,
"A1": 26,
"A2": 24,
"A3": 1,
"A4": 10,
"A5": 20,
},
"cbu": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
@@ -230,6 +336,204 @@ BK72XX_BOARD_PINS = {
"D18": 21,
"A0": 23,
},
"t1-u": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 14,
"D1": 16,
"D2": 23,
"D3": 22,
"D4": 20,
"D5": 1,
"D6": 0,
"D7": 24,
"D8": 9,
"D9": 26,
"D10": 6,
"D11": 8,
"D12": 11,
"D13": 10,
"D14": 28,
"D15": 21,
"D16": 17,
"D17": 15,
"A0": 20,
"A1": 1,
"A2": 24,
"A3": 26,
"A4": 10,
"A5": 28,
},
"generic-bk7238-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P7": 7,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM1": 7,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 0,
"D1": 1,
"D2": 6,
"D3": 7,
"D4": 8,
"D5": 9,
"D6": 10,
"D7": 11,
"D8": 14,
"D9": 15,
"D10": 16,
"D11": 17,
"D12": 20,
"D13": 21,
"D14": 22,
"D15": 23,
"D16": 24,
"D17": 26,
"D18": 28,
"A0": 1,
"A1": 10,
"A2": 20,
"A3": 24,
"A4": 26,
"A5": 28,
},
"t1-m": {
"WIRE2_SCL": 24,
"WIRE2_SDA": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC5": 1,
"ADC6": 10,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P24": 24,
"P26": 26,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCL2": 24,
"SDA2": 26,
"TX1": 11,
"TX2": 0,
"D0": 26,
"D1": 6,
"D2": 8,
"D3": 1,
"D4": 10,
"D5": 11,
"D6": 9,
"D7": 24,
"D11": 0,
"A0": 26,
"A1": 10,
"A2": 1,
"A3": 24,
},
"generic-bk7231t-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
@@ -781,6 +1085,75 @@ BK72XX_BOARD_PINS = {
"A6": 12,
"A7": 13,
},
"t1-3s": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 20,
"D1": 22,
"D2": 6,
"D3": 8,
"D4": 9,
"D5": 23,
"D6": 0,
"D7": 1,
"D8": 24,
"D9": 26,
"D10": 10,
"D11": 11,
"D12": 17,
"D13": 16,
"D14": 15,
"D15": 14,
"A0": 20,
"A1": 1,
"A2": 24,
"A3": 26,
"A4": 10,
},
"wb2l": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
@@ -965,6 +1338,84 @@ BK72XX_BOARD_PINS = {
"D10": 21,
"A0": 23,
},
"generic-bk7238": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P7": 7,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM1": 7,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 0,
"D1": 1,
"D2": 6,
"D3": 7,
"D4": 8,
"D5": 9,
"D6": 10,
"D7": 11,
"D8": 14,
"D9": 15,
"D10": 16,
"D11": 17,
"D12": 20,
"D13": 21,
"D14": 22,
"D15": 23,
"D16": 24,
"D17": 26,
"D18": 28,
"A0": 1,
"A1": 10,
"A2": 20,
"A3": 24,
"A4": 26,
"A5": 28,
},
"wa2": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
@@ -1235,6 +1686,51 @@ BK72XX_BOARD_PINS = {
"D15": 1,
"A0": 23,
},
"t1-2s": {
"WIRE2_SCL": 24,
"WIRE2_SDA": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC5": 1,
"ADC6": 10,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P24": 24,
"P26": 26,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCL2": 24,
"SDA2": 26,
"TX1": 11,
"TX2": 0,
"D0": 26,
"D1": 6,
"D2": 8,
"D3": 1,
"D4": 10,
"D5": 11,
"D6": 9,
"D7": 24,
"D11": 0,
"A0": 26,
"A1": 10,
"A2": 1,
"A3": 24,
},
"wb2s": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
+235
View File
@@ -0,0 +1,235 @@
#ifdef USE_BK72XX
#include "esphome/core/defines.h"
#ifdef USE_BK72XX_CRASH_HANDLER
#include "crash_handler.h"
#include "esphome/core/log.h"
#include <cinttypes>
#include <cstdint>
#include <cstring>
// ARM968E-S register snapshot passed to the SDK trap handlers (matches the
// layout used by bk_show_register() in LibreTiny's intc.c fixup).
extern "C" struct arm_registers {
uint32_t r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10;
uint32_t fp;
uint32_t ip;
uint32_t sp;
uint32_t lr;
uint32_t pc;
uint32_t spsr;
uint32_t cpsr;
};
static constexpr uint32_t CRASH_MAGIC = 0xDEADBEEF;
static constexpr uint32_t CRASH_DATA_VERSION = 1;
static constexpr size_t MAX_BACKTRACE = 16;
// BK72XX exception types (matches the order in which we wrap the SDK traps).
enum CrashException : uint8_t {
CRASH_EXC_NONE = 0,
CRASH_EXC_UNDEF = 1, // Undefined instruction
CRASH_EXC_PABT = 2, // Prefetch abort
CRASH_EXC_DABT = 3, // Data abort
};
// Persistent crash record. Lives in .noinit so it survives the SDK's watchdog
// reset that follows bk_cpu_shutdown(). Validated by magic + version on the
// next boot.
struct CrashData {
uint32_t magic;
uint32_t version;
uint32_t pc;
uint32_t lr;
uint32_t sp;
uint32_t cpsr;
uint8_t exception;
uint8_t backtrace_count;
uint16_t reserved;
uint32_t backtrace[MAX_BACKTRACE];
};
// Placed in .noinit so it survives the watchdog reset that follows
// bk_cpu_shutdown(). The libretiny linker fragment (patch_bk72xx_noinit.py)
// inserts a .noinit section between .bss and _empty_ram so this region is
// not zeroed by the C runtime startup.
static CrashData s_raw_crash_data __attribute__((section(".noinit"), used));
// Whether crash data was found and validated this boot. Lives in .bss
// (zero-initialized at startup) — set by crash_handler_read_and_clear().
static bool s_crash_data_valid = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// RAM and flash bounds come from linker symbols injected by
// libretiny/patch_bk72xx_noinit.py.script (PROVIDE assignments tied to the
// linker's own MEMORY definition). This avoids hardcoding chip-variant
// (BK7231N has 192KB RAM, others 256KB) or board (BKOFFSET_APP /
// BKRBL_SIZE_APP) layout — the linker is the source of truth.
extern "C" {
// NOLINTBEGIN(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration)
extern char _esphome_ram_start[];
extern char _esphome_ram_end[];
extern char _esphome_flash_start[];
extern char _esphome_flash_end[];
// NOLINTEND(bugprone-reserved-identifier,readability-identifier-naming,readability-redundant-declaration)
}
static inline bool is_code_addr(uint32_t addr) {
// ARM968 instructions are 4-byte aligned; reject obviously bogus values.
if ((addr & 0x3) != 0)
return false;
return addr >= reinterpret_cast<uintptr_t>(_esphome_flash_start) &&
addr < reinterpret_cast<uintptr_t>(_esphome_flash_end);
}
static inline bool is_valid_stack_ptr(uint32_t sp) {
return (sp & 0x3) == 0 && sp >= reinterpret_cast<uintptr_t>(_esphome_ram_start) &&
sp < reinterpret_cast<uintptr_t>(_esphome_ram_end);
}
// Walk the stack starting at `sp` and capture up to `max` code-looking
// 32-bit values into `out`. Skips entries equal to the fault PC so it isn't
// reported twice. Returns the number of entries captured.
//
// Bounds math is done in uintptr_t space; comparing pointers from unrelated
// objects (sp vs the linker-symbol RAM-end) is technically UB in C++ even
// though it works on flat-address embedded targets.
static uint8_t scan_backtrace(uint32_t sp, uint32_t pc, uint32_t *out, uint8_t max) {
if (!is_valid_stack_ptr(sp))
return 0;
// Limit the scan to 256 words (1KB) — covers typical nested call frames
// without dredging up too many stale stack values.
static constexpr uint32_t SCAN_WORDS = 256;
uintptr_t scan_addr = sp;
uintptr_t end_addr = reinterpret_cast<uintptr_t>(_esphome_ram_end);
uintptr_t limit_addr = scan_addr + SCAN_WORDS * sizeof(uint32_t);
if (limit_addr > end_addr)
limit_addr = end_addr;
uint8_t count = 0;
for (; scan_addr < limit_addr && count < max; scan_addr += sizeof(uint32_t)) {
uint32_t val = *reinterpret_cast<const uint32_t *>(scan_addr);
if (is_code_addr(val) && val != pc) {
out[count++] = val;
}
}
return count;
}
namespace esphome::bk72xx {
static const char *const TAG = "bk72xx.crash";
void crash_handler_read_and_clear() {
if (s_raw_crash_data.magic == CRASH_MAGIC && s_raw_crash_data.version == CRASH_DATA_VERSION) {
s_crash_data_valid = true;
if (s_raw_crash_data.backtrace_count > MAX_BACKTRACE)
s_raw_crash_data.backtrace_count = MAX_BACKTRACE;
if (s_raw_crash_data.exception > CRASH_EXC_DABT)
s_raw_crash_data.exception = CRASH_EXC_NONE;
}
// Clear magic so the next boot doesn't replay this crash. s_crash_data_valid
// remains true so additional API clients connecting in this boot session
// still see the log via crash_handler_log().
s_raw_crash_data.magic = 0;
}
bool crash_handler_has_data() { return s_crash_data_valid; }
static const char *get_exception_name(uint8_t exc) {
switch (exc) {
case CRASH_EXC_UNDEF:
return "Undefined instruction";
case CRASH_EXC_PABT:
return "Prefetch abort";
case CRASH_EXC_DABT:
return "Data abort";
default:
return "Unknown";
}
}
// 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 (!s_crash_data_valid)
return;
ESP_LOGE(TAG, "*** CRASH DETECTED ON PREVIOUS BOOT ***");
ESP_LOGE(TAG, " Reason: %s", get_exception_name(s_raw_crash_data.exception));
ESP_LOGE(TAG, " PC: 0x%08" PRIX32 " (fault location)", s_raw_crash_data.pc);
ESP_LOGE(TAG, " LR: 0x%08" PRIX32 " (return address)", s_raw_crash_data.lr);
ESP_LOGE(TAG, " SP: 0x%08" PRIX32, s_raw_crash_data.sp);
ESP_LOGE(TAG, " CPSR:0x%08" PRIX32, s_raw_crash_data.cpsr);
for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count; i++) {
ESP_LOGE(TAG, " BT%d: 0x%08" PRIX32 " (stack scan)", i, s_raw_crash_data.backtrace[i]);
}
// Build addr2line hint with all captured addresses for easy copy-paste.
// snprintf can return < 0 on error or >= size on truncation — clamp pos
// to a valid in-buffer offset before any further append, since we add
// `hint + pos` and OOB pointer arithmetic is UB.
char hint[160];
int written = snprintf(hint, sizeof(hint), "Use: addr2line -pfiaC -e firmware.elf 0x%08" PRIX32 " 0x%08" PRIX32,
s_raw_crash_data.pc, s_raw_crash_data.lr);
size_t pos = (written < 0) ? 0 : (static_cast<size_t>(written) >= sizeof(hint) ? sizeof(hint) - 1 : written);
for (uint8_t i = 0; i < s_raw_crash_data.backtrace_count && pos + 12 < sizeof(hint); i++) {
int n = snprintf(hint + pos, sizeof(hint) - pos, " 0x%08" PRIX32, s_raw_crash_data.backtrace[i]);
if (n < 0)
break;
if (static_cast<size_t>(n) >= sizeof(hint) - pos) {
// Truncated — buffer is full.
break;
}
pos += n;
}
ESP_LOGE(TAG, "%s", hint);
}
} // namespace esphome::bk72xx
// --- BK72XX trap-handler wrappers ---
// LibreTiny's fixup intc.c defines bk_trap_udef/pabt/dabt; the closed-source
// Beken SDK calls them from do_undefined/pabort/dabort after pushing a full
// arm_registers frame. We intercept the call via -Wl,--wrap so the original
// behavior (register dump on UART + bk_cpu_shutdown → watchdog reset) still
// runs after we've snapshotted the registers and a stack-scan backtrace.
extern "C" {
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern void __real_bk_trap_udef(struct arm_registers *regs);
extern void __real_bk_trap_pabt(struct arm_registers *regs);
extern void __real_bk_trap_dabt(struct arm_registers *regs);
static void capture_crash(uint8_t exception, struct arm_registers *regs) {
s_raw_crash_data.pc = regs->pc;
s_raw_crash_data.lr = regs->lr;
s_raw_crash_data.sp = regs->sp;
s_raw_crash_data.cpsr = regs->cpsr;
s_raw_crash_data.exception = exception;
s_raw_crash_data.backtrace_count = scan_backtrace(regs->sp, regs->pc, s_raw_crash_data.backtrace, MAX_BACKTRACE);
// Write version + magic last so a partial write is invalidated.
s_raw_crash_data.version = CRASH_DATA_VERSION;
s_raw_crash_data.magic = CRASH_MAGIC;
}
void __wrap_bk_trap_udef(struct arm_registers *regs) {
capture_crash(CRASH_EXC_UNDEF, regs);
__real_bk_trap_udef(regs);
}
void __wrap_bk_trap_pabt(struct arm_registers *regs) {
capture_crash(CRASH_EXC_PABT, regs);
__real_bk_trap_pabt(regs);
}
void __wrap_bk_trap_dabt(struct arm_registers *regs) {
capture_crash(CRASH_EXC_DABT, regs);
__real_bk_trap_dabt(regs);
}
// NOLINTEND(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
} // extern "C"
#endif // USE_BK72XX_CRASH_HANDLER
#endif // USE_BK72XX
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#ifdef USE_BK72XX
#include "esphome/core/defines.h"
#ifdef USE_BK72XX_CRASH_HANDLER
namespace esphome::bk72xx {
/// Read crash data from .noinit memory and clear the magic marker so the
/// next boot doesn't re-report it. Crash data is copied into a static and
/// remains accessible via crash_handler_log() / crash_handler_has_data() for
/// the rest of this boot session.
void crash_handler_read_and_clear();
/// Log crash data if a crash was detected on previous boot.
void crash_handler_log();
/// Returns true if crash data was found this boot.
bool crash_handler_has_data();
} // namespace esphome::bk72xx
#endif // USE_BK72XX_CRASH_HANDLER
#endif // USE_BK72XX
@@ -30,19 +30,6 @@ void BluetoothProxy::setup() {
this->configured_scan_active_ = this->parent_->get_scan_active();
this->parent_->add_scanner_state_listener(this);
this->set_interval(100, [this]() {
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
this->flush_pending_advertisements_();
return;
}
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect();
}
}
});
}
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
@@ -133,6 +120,25 @@ void BluetoothProxy::dump_config() {
YESNO(this->active_), this->connection_count_);
}
void BluetoothProxy::loop() {
// Run advertisement flush / connection cleanup every 100ms
uint32_t now = App.get_loop_component_start_time();
if (now - this->last_advertisement_flush_time_ < 100)
return;
this->last_advertisement_flush_time_ = now;
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
this->flush_pending_advertisements_();
return;
}
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect();
}
}
}
esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() {
return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
}
@@ -201,7 +207,6 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
this->log_connection_info_(connection, "v3 without cache");
}
uint64_to_bd_addr(msg.address, connection->remote_bda_);
connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type));
connection->set_state(espbt::ClientState::DISCOVERED);
this->send_connections_free();
@@ -65,6 +65,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override;
void dump_config() override;
void setup() override;
void loop() override;
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
void register_connection(BluetoothConnection *connection) {
@@ -176,6 +177,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
// BLE advertisement batching
api::BluetoothLERawAdvertisementsResponse response_;
// Group 3: 4-byte types
uint32_t last_advertisement_flush_time_{0};
// Pre-allocated response message - always ready to send
api::BluetoothConnectionsFreeResponse connections_free_response_;
+27 -27
View File
@@ -78,43 +78,43 @@ void BME680Component::setup() {
}
// Read calibration
uint8_t cal1[25];
if (!this->read_bytes(BME680_REGISTER_COEFF1, cal1, 25)) {
uint8_t coeff1[25];
if (!this->read_bytes(BME680_REGISTER_COEFF1, coeff1, 25)) {
this->mark_failed();
return;
}
uint8_t cal2[16];
if (!this->read_bytes(BME680_REGISTER_COEFF2, cal2, 16)) {
uint8_t coeff2[16];
if (!this->read_bytes(BME680_REGISTER_COEFF2, coeff2, 16)) {
this->mark_failed();
return;
}
this->calibration_.t1 = cal2[9] << 8 | cal2[8];
this->calibration_.t2 = cal1[2] << 8 | cal1[1];
this->calibration_.t3 = cal1[3];
this->calibration_.t1 = coeff2[9] << 8 | coeff2[8];
this->calibration_.t2 = coeff1[2] << 8 | coeff1[1];
this->calibration_.t3 = coeff1[3];
this->calibration_.h1 = cal2[2] << 4 | (cal2[1] & 0x0F);
this->calibration_.h2 = cal2[0] << 4 | cal2[1] >> 4;
this->calibration_.h3 = cal2[3];
this->calibration_.h4 = cal2[4];
this->calibration_.h5 = cal2[5];
this->calibration_.h6 = cal2[6];
this->calibration_.h7 = cal2[7];
this->calibration_.h1 = coeff2[2] << 4 | (coeff2[1] & 0x0F);
this->calibration_.h2 = coeff2[0] << 4 | coeff2[1] >> 4;
this->calibration_.h3 = coeff2[3];
this->calibration_.h4 = coeff2[4];
this->calibration_.h5 = coeff2[5];
this->calibration_.h6 = coeff2[6];
this->calibration_.h7 = coeff2[7];
this->calibration_.p1 = cal1[6] << 8 | cal1[5];
this->calibration_.p2 = cal1[8] << 8 | cal1[7];
this->calibration_.p3 = cal1[9];
this->calibration_.p4 = cal1[12] << 8 | cal1[11];
this->calibration_.p5 = cal1[14] << 8 | cal1[13];
this->calibration_.p6 = cal1[16];
this->calibration_.p7 = cal1[15];
this->calibration_.p8 = cal1[20] << 8 | cal1[19];
this->calibration_.p9 = cal1[22] << 8 | cal1[21];
this->calibration_.p10 = cal1[23];
this->calibration_.p1 = coeff1[6] << 8 | coeff1[5];
this->calibration_.p2 = coeff1[8] << 8 | coeff1[7];
this->calibration_.p3 = coeff1[9];
this->calibration_.p4 = coeff1[12] << 8 | coeff1[11];
this->calibration_.p5 = coeff1[14] << 8 | coeff1[13];
this->calibration_.p6 = coeff1[16];
this->calibration_.p7 = coeff1[15];
this->calibration_.p8 = coeff1[20] << 8 | coeff1[19];
this->calibration_.p9 = coeff1[22] << 8 | coeff1[21];
this->calibration_.p10 = coeff1[23];
this->calibration_.gh1 = cal2[14];
this->calibration_.gh2 = cal2[12] << 8 | cal2[13];
this->calibration_.gh3 = cal2[15];
this->calibration_.gh1 = coeff2[14];
this->calibration_.gh2 = coeff2[12] << 8 | coeff2[13];
this->calibration_.gh3 = coeff2[15];
uint8_t temp_var = 0;
if (!this->read_byte(0x02, &temp_var)) {
+2 -1
View File
@@ -19,6 +19,7 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -101,7 +102,7 @@ async def setup_button_core_(var, config):
async def register_button(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_button(var))
queue_entity_register("button", config)
CORE.register_platform_component("button", var)
await setup_button_core_(var, config)
+6 -2
View File
@@ -49,7 +49,11 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
IS_PLATFORM_COMPONENT = True
@@ -442,7 +446,7 @@ async def setup_climate_core_(var, config):
async def register_climate(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_climate(var))
queue_entity_register("climate", config)
CORE.register_platform_component("climate", var)
await setup_climate_core_(var, config)
+2 -1
View File
@@ -374,7 +374,8 @@ void Climate::save_state_(const ClimateTraits &traits) {
#define TEMP_IGNORE_MEMACCESS
#endif
ClimateDeviceRestoreState state{};
// initialize as zero to prevent random data on stack triggering erase
// initialize as zero (including padding) to prevent random data on stack triggering erase
// NOLINTNEXTLINE(bugprone-raw-memory-call-on-non-trivial-type) -- intentional bytewise zero for RTC save
memset(&state, 0, sizeof(ClimateDeviceRestoreState));
#ifdef TEMP_IGNORE_MEMACCESS
#pragma GCC diagnostic pop
+2 -1
View File
@@ -39,6 +39,7 @@ from esphome.const import (
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -232,7 +233,7 @@ async def setup_cover_core_(var, config):
async def register_cover(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_cover(var))
queue_entity_register("cover", config)
CORE.register_platform_component("cover", var)
await setup_cover_core_(var, config)
+6 -2
View File
@@ -22,7 +22,11 @@ from esphome.const import (
CONF_YEAR,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@rfdarter", "@jesserockz"]
@@ -160,7 +164,7 @@ async def register_datetime(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
entity_type = config[CONF_TYPE].lower()
cg.add(getattr(cg.App, f"register_{entity_type}")(var))
queue_entity_register(entity_type, config)
CORE.register_platform_component(entity_type, var)
await setup_datetime_core_(var, config)
+9 -4
View File
@@ -17,11 +17,13 @@ constexpr std::uintptr_t MBR_PARAM_PAGE_ADDR = 0xFFC;
constexpr std::uintptr_t MBR_BOOTLOADER_ADDR = 0xFF8;
static inline uint32_t read_mem_u32(uintptr_t addr) {
return *reinterpret_cast<volatile uint32_t *>(addr); // NOLINT(performance-no-int-to-ptr)
// NOLINTNEXTLINE(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
return *reinterpret_cast<volatile uint32_t *>(addr);
}
static inline uint8_t read_mem_u8(uintptr_t addr) {
return *reinterpret_cast<volatile uint8_t *>(addr); // NOLINT(performance-no-int-to-ptr)
// NOLINTNEXTLINE(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
return *reinterpret_cast<volatile uint8_t *>(addr);
}
// defines from https://github.com/adafruit/Adafruit_nRF52_Bootloader which prints those information
@@ -98,6 +100,7 @@ void DebugComponent::log_partition_info_() {
#define NRF_PERIPH_ENABLED(periph, reg) \
YESNO(((reg)->ENABLE & periph##_ENABLE_ENABLE_Msk) == (periph##_ENABLE_ENABLE_Enabled << periph##_ENABLE_ENABLE_Pos))
// NOLINTBEGIN(clang-analyzer-core.FixedAddressDereference) -- nRF peripheral registers are MMIO at fixed addresses
static void log_peripherals_info() {
// most peripherals are enabled only when in use so ESP_LOGV is enough
ESP_LOGV(TAG, "Peripherals status:");
@@ -131,6 +134,7 @@ static void log_peripherals_info() {
YESNO((NRF_CRYPTOCELL->ENABLE & CRYPTOCELL_ENABLE_ENABLE_Msk) ==
(CRYPTOCELL_ENABLE_ENABLE_Enabled << CRYPTOCELL_ENABLE_ENABLE_Pos)));
}
// NOLINTEND(clang-analyzer-core.FixedAddressDereference)
#undef NRF_PERIPH_ENABLED
#endif
@@ -159,8 +163,9 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
char *buf = buffer.data();
// Main supply status
const char *supply_status =
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
// NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- NRF_POWER is MMIO at a fixed address
auto regstatus = nrf_power_mainregstatus_get(NRF_POWER);
const char *supply_status = (regstatus == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
ESP_LOGD(TAG, "Main supply status: %s", supply_status);
pos = buf_append_str(buf, size, pos, "|Main supply status: ");
pos = buf_append_str(buf, size, pos, supply_status);
+8 -5
View File
@@ -193,11 +193,14 @@ def _validate_ex1_wakeup_mode(value):
def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
if not CORE.is_bk72xx:
return value
max_duration = core.TimePeriod(hours=36)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
if CORE.is_bk72xx:
max_duration = core.TimePeriod(hours=36)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
elif CORE.using_zephyr:
max_duration = core.TimePeriod(days=49)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 49 days on Zephyr")
return value
@@ -9,18 +9,11 @@ static const char *const TAG = "deep_sleep";
// 5 seconds for deep sleep to ensure clean disconnect from Home Assistant
static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000;
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
std::atomic<DeepSleepComponent *> global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void DeepSleepComponent::setup() {
#ifdef USE_ZEPHYR
k_sem_init(&this->wakeup_sem_, 0, 1);
#endif
global_has_deep_sleep = true;
this->schedule_sleep_();
// It can be used from another thread for waking up the device.
// It should be called as last item in setup.
global_deep_sleep.store(this);
}
void DeepSleepComponent::schedule_sleep_() {
@@ -4,8 +4,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <atomic>
#ifdef USE_ESP32
#include <esp_sleep.h>
#endif
@@ -15,10 +13,6 @@
#include "esphome/core/time.h"
#endif
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
#endif
#include <cinttypes>
namespace esphome {
@@ -125,9 +119,6 @@ class DeepSleepComponent : public Component {
void prevent_deep_sleep();
void allow_deep_sleep();
#ifdef USE_ZEPHYR
void wakeup();
#endif
protected:
// Returns nullopt if no run duration is set. Otherwise, returns the run
@@ -167,9 +158,6 @@ class DeepSleepComponent : public Component {
optional<uint32_t> run_duration_;
bool next_enter_deep_sleep_{false};
bool prevent_{false};
#ifdef USE_ZEPHYR
k_sem wakeup_sem_;
#endif
};
extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -256,8 +244,5 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
};
extern std::atomic<DeepSleepComponent *>
global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace deep_sleep
} // namespace esphome
@@ -1,17 +1,13 @@
#include "deep_sleep_component.h"
#ifdef USE_ZEPHYR
#include "esphome/core/log.h"
#include "esphome/core/wake.h"
#include <zephyr/sys/poweroff.h>
#include <zephyr/kernel.h>
#include <zephyr/stats/stats.h>
#include <zephyr/pm/pm.h>
namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep";
void DeepSleepComponent::wakeup() { k_sem_give(&this->wakeup_sem_); }
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() {}
@@ -19,9 +15,8 @@ void DeepSleepComponent::dump_config_platform_() {}
bool DeepSleepComponent::prepare_to_sleep_() { return true; }
void DeepSleepComponent::deep_sleep_() {
k_timeout_t sleep_duration = K_FOREVER;
if (this->sleep_duration_.has_value()) {
sleep_duration = K_USEC(*this->sleep_duration_);
esphome::internal::wakeable_delay(static_cast<uint32_t>(*this->sleep_duration_ / 1000));
} else {
#ifndef USE_ZIGBEE
// the device can be woken up through one of the following signals:
@@ -33,11 +28,12 @@ void DeepSleepComponent::deep_sleep_() {
//
// The system is reset when it wakes up from System OFF mode.
sys_poweroff();
#else
esphome::internal::wakeable_delay(UINT32_MAX);
#endif
}
// It might wake up immediately if k_sem_give was called again after wake up
int ret = k_sem_take(&this->wakeup_sem_, sleep_duration);
if (ret == 0) {
const bool woke = esphome::wake_request_take();
if (woke) {
ESP_LOGD(TAG, "Woken up by another thread");
} else {
ESP_LOGD(TAG, "Timeout expired (normal sleep)");
@@ -29,7 +29,7 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component {
protected:
void control(const AlarmControlPanelCall &call) override {
auto state = call.get_state().value_or(ACP_STATE_DISARMED);
auto code = call.get_code();
const auto &code = call.get_code();
switch (state) {
case ACP_STATE_ARMED_AWAY:
if (this->get_requires_code_to_arm()) {
@@ -104,8 +104,9 @@ int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) {
if (this->is_full()) {
ESP_LOGE(TAG, "Command queue is full");
return -1;
} else if (this->is_empty())
} else if (this->is_empty()) {
front_++;
}
rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE;
commands_[rear_] = std::move(cmd); // Transfer ownership using std::move
return 1;
@@ -56,8 +56,7 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
// limit amount of lights per universe and received
// packet.count is the number of DMX bytes including start code; divide by channels to get the number of lights
int lights_in_packet = (packet.count > 0) ? (packet.count - 1) / channels_ : 0;
int output_end =
std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + lights_in_packet));
int output_end = std::min({it->size(), output_offset + get_lights_per_universe(), output_offset + lights_in_packet});
auto *input_data = packet.values + 1;
auto effect_name = get_name();
+17 -2
View File
@@ -5,6 +5,15 @@
// Implementation based on:
// https://github.com/sciosense/ENS160_driver
// For best performance, the sensor shall be operated in normal indoor air in the range -5 to 60°C
// (typical: 25°C); relative humidity: 20 to 80%RH (typical: 50%RH), non-condensing with no aggressive
// or poisonous gases present. Prolonged exposure to environments outside these conditions can affect
// performance and lifetime of the sensor.
// The sensor is designed for indoor use and is not waterproof or dustproof. It should be protected from
// water, condensation, dust, and aggressive gases. Note that the status will only be stored in non-volatile
// memory after an initial 24 h of continuous operation. If unpowered before the conclusion of that period,
// the ENS160 will resume "Initial Start-up" mode after re-powering.
#include "ens160_base.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
@@ -14,7 +23,9 @@ namespace ens160_base {
static const char *const TAG = "ens160";
static const uint8_t ENS160_BOOTING = 10;
// Datasheet specifies 10ms, but some users report that 10ms is not sufficient for the
// sensor to boot and be ready for commands. 11ms seems to be a safe value.
static const uint8_t ENS160_BOOTING = 11;
static const uint16_t ENS160_PART_ID = 0x0160;
@@ -91,6 +102,8 @@ void ENS160Component::setup() {
this->mark_failed();
return;
}
delay(ENS160_BOOTING);
// clear command
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_NOP)) {
this->error_code_ = WRITE_FAILED;
@@ -102,6 +115,7 @@ void ENS160Component::setup() {
this->mark_failed();
return;
}
delay(ENS160_BOOTING);
// read firmware version
if (!this->write_byte(ENS160_REG_COMMAND, ENS160_COMMAND_GET_APPVER)) {
@@ -109,6 +123,8 @@ void ENS160Component::setup() {
this->mark_failed();
return;
}
delay(ENS160_BOOTING);
uint8_t version_data[3];
if (!this->read_bytes(ENS160_REG_GPR_READ_4, version_data, 3)) {
this->error_code_ = READ_FAILED;
@@ -223,7 +239,6 @@ void ENS160Component::update() {
if (this->aqi_ != nullptr) {
// remove reserved bits, just in case they are used in future
data_aqi = ENS160_DATA_AQI & data_aqi;
this->aqi_->publish_state(data_aqi);
}
+8
View File
@@ -729,6 +729,9 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
"dev": cv.Version(5, 5, 4),
}
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
cv.Version(
6, 0, 1
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
cv.Version(
6, 0, 0
): "https://github.com/pioarduino/platform-espressif32.git#prep_IDF6",
@@ -1724,6 +1727,11 @@ async def to_code(config):
CORE.relative_internal_path(".espressif")
)
# Both ESP-IDF and ESP32 Arduino builds generate IDF app metadata. Keep
# volatile build path/time data out of the binary so equivalent projects can
# produce reproducible outputs and downstream tooling can reuse artifacts.
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
+2 -67
View File
@@ -1,17 +1,8 @@
#ifdef USE_ESP32
#include "esphome/core/defines.h"
#include "crash_handler.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/defines.h"
#include "preferences.h"
#include <esp_clk_tree.h>
#include <esp_cpu.h>
#include <esp_idf_version.h>
#include <esp_ota_ops.h>
#include <esp_task_wdt.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
@@ -22,63 +13,7 @@ extern "C" __attribute__((weak)) void initArduino() {}
namespace esphome {
void HOT yield() { vPortYield(); }
// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig),
// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because
// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32.
// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract.
uint32_t IRAM_ATTR HOT millis() {
#if CONFIG_FREERTOS_HZ == 1000
if (xPortInIsrContext()) [[unlikely]] {
return xTaskGetTickCountFromISR();
}
return xTaskGetTickCount();
#else
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
#endif
}
// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is
// safe because the two are never cross-compared: millis() values are only used for
// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while
// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME),
// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly,
// so the Scheduler is internally consistent on the esp_timer clock.
uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); }
void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
esp_restart();
// restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield();
}
}
void arch_init() {
#ifdef USE_ESP32_CRASH_HANDLER
// Read crash data from previous boot before anything else
esp32::crash_handler_read_and_clear();
#endif
// Enable the task watchdog only on the loop task (from which we're currently running)
esp_task_wdt_add(nullptr);
// Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled,
// in which case safe_mode will mark it valid after confirming successful boot.
#ifndef USE_OTA_ROLLBACK
esp_ota_mark_app_valid_cancel_rollback();
#endif
}
void HOT arch_feed_wdt() { esp_task_wdt_reset(); }
uint32_t arch_get_cpu_cycle_count() { return esp_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
return freq;
}
// HAL functions live in hal.cpp. This file keeps only the loop task setup.
TaskHandle_t loop_task_handle = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static StaticTask_t loop_task_tcb; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static StackType_t
+71
View File
@@ -0,0 +1,71 @@
#ifdef USE_ESP32
// defines.h must come before crash_handler.h so USE_ESP32_CRASH_HANDLER is set
// before crash_handler.h's #ifdef-guarded namespace block is parsed.
#include "esphome/core/defines.h"
#include "crash_handler.h"
#include "esphome/core/hal.h"
#include <esp_clk_tree.h>
#include <esp_ota_ops.h>
#include <esp_system.h>
#include <esp_task_wdt.h>
#include <esp_timer.h>
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
// Empty esp32 namespace block to satisfy ci-custom's lint_namespace check.
// HAL functions live in namespace esphome (root) — they are not part of the
// esp32 component's API.
namespace esphome::esp32 {} // namespace esphome::esp32
namespace esphome {
// Use xTaskGetTickCount() when tick rate is 1 kHz (ESPHome's default via sdkconfig),
// falling back to esp_timer for non-standard rates. IRAM_ATTR is required because
// Wiegand and ZyAura call millis() from IRAM_ATTR ISR handlers on ESP32.
// xTaskGetTickCountFromISR() is used in ISR context to satisfy the FreeRTOS API contract.
uint32_t IRAM_ATTR HOT millis() {
#if CONFIG_FREERTOS_HZ == 1000
if (xPortInIsrContext()) [[unlikely]] {
return xTaskGetTickCountFromISR();
}
return xTaskGetTickCount();
#else
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
#endif
}
void arch_restart() {
esp_restart();
// restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield();
}
}
void arch_init() {
#ifdef USE_ESP32_CRASH_HANDLER
// Read crash data from previous boot before anything else
esp32::crash_handler_read_and_clear();
#endif
// Enable the task watchdog only on the loop task (from which we're currently running)
esp_task_wdt_add(nullptr);
// Handle OTA rollback: mark partition valid immediately unless USE_OTA_ROLLBACK is enabled,
// in which case safe_mode will mark it valid after confirming successful boot.
#ifndef USE_OTA_ROLLBACK
esp_ota_mark_app_valid_cancel_rollback();
#endif
}
uint32_t arch_get_cpu_freq_hz() {
uint32_t freq = 0;
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq);
return freq;
}
} // namespace esphome
#endif // USE_ESP32
+17 -1
View File
@@ -18,6 +18,12 @@ struct NVSData {
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// open() runs from app_main() before the logger is initialized, so any failure
// must be deferred until after global_logger is set. This is emitted from the
// first make_preference() call, which runs from the generated setup() after
// log->pre_setup() has run at EARLY_INIT priority.
static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
@@ -70,12 +76,14 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
}
void ESP32Preferences::open() {
// Runs from app_main() before the logger is initialized; any logging here
// must be deferred. See s_open_err and make_preference() below.
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err == 0)
return;
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
s_open_err = err;
nvs_flash_deinit();
nvs_flash_erase();
nvs_flash_init();
@@ -87,6 +95,14 @@ void ESP32Preferences::open() {
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
if (s_open_err != ESP_OK) {
if (this->nvs_handle == 0) {
ESP_LOGW(TAG, "nvs_open failed: %s - NVS unavailable", esp_err_to_name(s_open_err));
} else {
ESP_LOGW(TAG, "nvs_open failed: %s - erased NVS", esp_err_to_name(s_open_err));
}
s_open_err = ESP_OK;
}
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = this->nvs_handle;
pref->key = type;
+64 -13
View File
@@ -13,14 +13,21 @@
* and printf() calls in SDK components are only in debug/assert paths
* (gpio_dump_io_configuration, ringbuf diagnostics) that are either
* GC'd or never called. Crash backtraces and panic output are
* unaffected they use esp_rom_printf() which is a ROM function
* unaffected; they use esp_rom_printf() which is a ROM function
* and does not go through libc.
*
* These stubs redirect through vsnprintf() (which uses _svfprintf_r
* already in the binary) and fwrite(), allowing the linker to
* dead-code eliminate _vfprintf_r.
* On picolibc (default for IDF >= 5 on RISC-V, IDF >= 6 everywhere) we
* route output through a stack-allocated cookie FILE that forwards each
* byte to the real target stream via fputc(). Picolibc's tinystdio
* vfprintf walks the FILE::put callback one character at a time, so this
* costs ~32 bytes of stack for the cookie struct vs. a 512-byte format
* buffer. The buffered path overflows the loopTask stack on IDF 6.
*
* Saves ~11 KB of flash.
* On newlib (IDF <= 5 on Xtensa) we keep the original snprintf-then-fwrite
* path because that loopTask stack budget has plenty of headroom for the
* 512-byte buffer; the picolibc-only crash above does not affect it.
*
* Saves ~11 KB of flash on newlib, ~2.8 KB on picolibc.
*
* To disable these wraps, set enable_full_printf: true in the esp32
* advanced config section.
@@ -30,10 +37,55 @@
#include <cstdarg>
#include <cstdio>
#ifndef __PICOLIBC__
#include "esp_system.h"
#endif
namespace esphome::esp32 {}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
#ifdef __PICOLIBC__
#include <cstddef>
#include <type_traits>
extern int __real_vfprintf(FILE *stream, const char *fmt, va_list ap);
namespace {
struct CookieFile {
FILE base;
FILE *target;
};
// cookie_put() recovers CookieFile* from FILE* via reinterpret_cast, which is
// only well-defined when FILE is the first member at offset 0 and CookieFile
// is standard-layout.
static_assert(offsetof(CookieFile, base) == 0, "FILE must be the first member of CookieFile");
static_assert(std::is_standard_layout<CookieFile>::value, "CookieFile must be standard-layout");
int cookie_put(char c, FILE *stream) {
auto *cookie = reinterpret_cast<CookieFile *>(stream);
return fputc(static_cast<unsigned char>(c), cookie->target);
}
const FILE COOKIE_FILE_TEMPLATE = FDEV_SETUP_STREAM(cookie_put, nullptr, nullptr, _FDEV_SETUP_WRITE);
} // namespace
int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) {
CookieFile cookie;
cookie.base = COOKIE_FILE_TEMPLATE;
cookie.target = stream;
return __real_vfprintf(&cookie.base, fmt, ap);
}
int __wrap_vprintf(const char *fmt, va_list ap) { return __wrap_vfprintf(stdout, fmt, ap); }
#else // !__PICOLIBC__
static constexpr size_t PRINTF_BUFFER_SIZE = 512;
// These stubs are essentially dead code at runtime — ESPHome replaces the
@@ -55,14 +107,18 @@ static int write_printf_buffer(FILE *stream, char *buf, int len) {
return len;
}
// NOLINTBEGIN(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" {
int __wrap_vprintf(const char *fmt, va_list ap) {
char buf[PRINTF_BUFFER_SIZE];
return write_printf_buffer(stdout, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
}
int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) {
char buf[PRINTF_BUFFER_SIZE];
return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
}
#endif // __PICOLIBC__
int __wrap_printf(const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
@@ -71,11 +127,6 @@ int __wrap_printf(const char *fmt, ...) {
return len;
}
int __wrap_vfprintf(FILE *stream, const char *fmt, va_list ap) {
char buf[PRINTF_BUFFER_SIZE];
return write_printf_buffer(stream, buf, vsnprintf(buf, sizeof(buf), fmt, ap));
}
int __wrap_fprintf(FILE *stream, const char *fmt, ...) {
va_list ap;
va_start(ap, fmt);
+1 -1
View File
@@ -104,7 +104,7 @@ ESPBTUUID ESPBTUUID::as_128bit() const {
} else {
uuid32 = this->uuid_.uuid.uuid16;
}
for (uint8_t i = 0; i < this->uuid_.len; i++) {
for (uint16_t i = 0; i < this->uuid_.len; i++) {
data[12 + i] = ((uuid32 >> i * 8) & 0xFF);
}
return ESPBTUUID::from_raw(data);
@@ -166,8 +166,9 @@ void ESP32BLETracker::loop() {
ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts;
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d, active: %d",
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
this->client_state_counts_.disconnecting, this->client_state_counts_.active);
}
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
@@ -190,10 +191,18 @@ void ESP32BLETracker::loop() {
*/
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
// all clients are idle (their state changes increment version when they finish)
// no clients are in the transient CONNECTING / DISCOVERED / DISCONNECTING states
// (their state changes increment version when they finish). CONNECTED / ESTABLISHED
// clients do NOT block this branch — the coex revert below has its own active-count gate.
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(false);
// Only revert to BALANCE when no connections are active. Established connections
// continue to need PREFER_BT so peer GATT responses can reach us while WiFi traffic
// (advertisement upload, log streaming) competes for the shared radio. Reverting too
// early causes Bluedroid to time out at ~20s and synthesize status=133.
if (!counts.active) {
this->update_coex_preference_(false);
}
#endif
if (this->scan_continuous_) {
this->start_scan_(false); // first = false
@@ -701,9 +710,10 @@ void ESP32BLETracker::dump_config() {
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
ESP_LOGCONFIG(TAG,
" Scanner State: %s\n"
" Connecting: %d, discovered: %d, disconnecting: %d",
" Connecting: %d, discovered: %d, disconnecting: %d, active: %d",
this->scanner_state_to_string_(this->scanner_state_), this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting,
this->client_state_counts_.active);
if (this->scan_start_fail_count_) {
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
}
@@ -160,9 +160,13 @@ struct ClientStateCounts {
uint8_t connecting = 0;
uint8_t discovered = 0;
uint8_t disconnecting = 0;
// CONNECTED + ESTABLISHED clients. Tracked so coex stays at PREFER_BT
// while active connections may still need to send/receive GATT traffic.
uint8_t active = 0;
bool operator==(const ClientStateCounts &other) const {
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting;
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting &&
active == other.active;
}
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
@@ -381,6 +385,10 @@ class ESP32BLETracker : public Component,
case ClientState::CONNECTING:
counts.connecting++;
break;
case ClientState::CONNECTED:
case ClientState::ESTABLISHED:
counts.active++;
break;
default:
break;
}
@@ -216,6 +216,7 @@ void ESP32TouchComponent::setup() {
// Do initial oneshot scans to populate baseline values
for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) {
err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS);
App.feed_wdt(); // 3 scans with 2s timeout might exceed WDT, so feed it here to be safe
if (err != ESP_OK) {
ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err));
}
+5
View File
@@ -314,6 +314,11 @@ async def to_code(config):
for symbol in ("vprintf", "printf", "fprintf"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# Wrap Arduino's millis() so all callers (including Arduino libraries and ISR
# handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply
# implementation in the Arduino ESP8266 core.
cg.add_build_flag("-Wl,--wrap=millis")
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
+2 -35
View File
@@ -3,45 +3,12 @@
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/time_64.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <Arduino.h>
#include <core_esp8266_features.h>
extern "C" {
#include <user_interface.h>
}
namespace esphome {
void HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
void HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
system_restart();
// restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield();
}
}
void arch_init() {}
void HOT arch_feed_wdt() { system_soft_wdt_feed(); }
uint8_t progmem_read_byte(const uint8_t *addr) {
return pgm_read_byte(addr); // NOLINT
}
const char *progmem_read_ptr(const char *const *addr) {
return reinterpret_cast<const char *>(pgm_read_ptr(addr)); // NOLINT
}
uint16_t progmem_read_uint16(const uint16_t *addr) {
return pgm_read_word(addr); // NOLINT
}
uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return F_CPU; }
// HAL functions live in hal.cpp. This file keeps only the ESP8266-specific
// firmware bootstrap (Tasmota OTA magic bytes, optional GPIO pre-init).
void force_link_symbols() {
// Tasmota uses magic bytes in the binary to check if an OTA firmware is compatible
+1
View File
@@ -140,6 +140,7 @@ void IRAM_ATTR ISRInternalGPIOPin::digital_write(bool value) {
void IRAM_ATTR ISRInternalGPIOPin::clear_interrupt() {
auto *arg = reinterpret_cast<ISRPinArg *>(arg_);
// NOLINTNEXTLINE(clang-analyzer-core.FixedAddressDereference) -- GPIO_REG_WRITE is MMIO at a fixed address
GPIO_REG_WRITE(GPIO_STATUS_W1TC_ADDRESS, 1UL << arg->pin);
}
+111
View File
@@ -0,0 +1,111 @@
#ifdef USE_ESP8266
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <Arduino.h>
#include <core_esp8266_features.h>
extern "C" {
#include <user_interface.h>
}
// Empty esp8266 namespace block to satisfy ci-custom's lint_namespace check.
// HAL functions live in namespace esphome (root) — they are not part of the
// esp8266 component's API.
namespace esphome::esp8266 {} // namespace esphome::esp8266
namespace esphome {
// yield(), micros(), millis_64(), delayMicroseconds(), arch_feed_wdt(),
// progmem_read_*() are inlined in core/hal/hal_esp8266.h.
//
// Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit
// multiplies on the LX106). Tracks a running ms counter from 32-bit
// system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis
// (via -Wl,--wrap=millis) so Arduino libs and IRAM_ATTR ISR handlers (e.g.
// Wiegand, ZyAura) also get the fast version. xt_rsil(15) guards the static
// state against ISR re-entry; the critical section is bounded (≤10 while-loop
// iterations, ~100 ns on the common path, or a constant-time /1000 ~2.5 μs on
// the rare path — well under WiFi's ~10 μs ISR latency budget). NMIs (level
// >15) are not masked, but the ESP8266 SDK's NMI handlers don't call millis().
//
// system_get_time() wraps every ~71.6 min; unsigned (now_us - last_us) handles
// one wrap. The main loop calls millis() at 60+ Hz, so delta stays tiny — a
// >71 min block would trip the watchdog long before it could matter here.
static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000;
static constexpr uint32_t US_PER_MS = 1000;
uint32_t IRAM_ATTR HOT millis() {
// Struct packs the three statics so the compiler loads one base address
// instead of three separate literal pool entries (saves ~8 bytes IRAM).
static struct {
uint32_t cache;
uint32_t remainder;
uint32_t last_us;
} state = {0, 0, 0};
uint32_t ps = xt_rsil(15);
uint32_t now_us = system_get_time();
uint32_t delta = now_us - state.last_us;
state.last_us = now_us;
state.remainder += delta;
if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) {
// Rare path: large gap (WiFi scan, boot, long block). Constant-time
// conversion keeps the critical section bounded.
uint32_t ms = state.remainder / US_PER_MS;
state.cache += ms;
// Reuse ms instead of `remainder %= US_PER_MS` — `%` would compile to a
// second __umodsi3 call on the LX106 (no hardware divide).
state.remainder -= ms * US_PER_MS;
} else {
// Common path: small gap. At most ~10 iterations since remainder was
// < threshold (10 ms) on entry and delta adds at most one more threshold
// before exiting this branch.
while (state.remainder >= US_PER_MS) {
state.cache++;
state.remainder -= US_PER_MS;
}
}
uint32_t result = state.cache;
xt_wsr_ps(ps);
return result;
}
// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object
// call to the original millis() that --wrap can't intercept, so calling ::delay()
// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still
// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and
// WiFi run correctly. Theoretically less power-efficient than Arduino's
// os_timer-based delay() for long waits, but nearly all ESPHome delays are short
// (sensor/I²C/SPI settling in the 1100 ms range) where the difference is
// negligible.
void HOT delay(uint32_t ms) {
if (ms == 0) {
optimistic_yield(1000);
return;
}
uint32_t start = millis();
while (millis() - start < ms) {
optimistic_yield(1000);
}
}
void arch_restart() {
system_restart();
// restart() doesn't always end execution
while (true) { // NOLINT(clang-diagnostic-unreachable-code)
yield();
}
}
} // namespace esphome
// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator.
// Requires -Wl,--wrap=millis in build flags (added by __init__.py).
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); }
// Note: Arduino's init() registers a 60-second overflow timer for micros64().
// We leave it running — wrapping init() as a no-op would break micros64()'s
// overflow tracking, and the timer's cost is negligible (~3 μs per 60 s).
#endif // USE_ESP8266
+2 -2
View File
@@ -51,7 +51,7 @@ static inline bool esp_rtc_user_mem_read(uint32_t index, uint32_t *dest) {
if (index >= ESP_RTC_USER_MEM_SIZE_WORDS) {
return false;
}
*dest = ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr)
*dest = ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr,clang-analyzer-core.FixedAddressDereference)
return true;
}
@@ -64,7 +64,7 @@ static inline bool esp_rtc_user_mem_write(uint32_t index, uint32_t value) {
}
auto *ptr = &ESP_RTC_USER_MEM[index]; // NOLINT(performance-no-int-to-ptr)
*ptr = value;
*ptr = value; // NOLINT(clang-analyzer-core.FixedAddressDereference)
return true;
}
+13 -1
View File
@@ -292,6 +292,7 @@ void ESPHomeOTAComponent::handle_data_() {
bool update_started = false;
size_t total = 0;
uint32_t last_progress = 0;
uint32_t last_data_ms = 0;
uint8_t buf[OTA_BUFFER_SIZE];
char *sbuf = reinterpret_cast<char *>(buf);
size_t ota_size;
@@ -350,8 +351,18 @@ void ESPHomeOTAComponent::handle_data_() {
// Acknowledge MD5 OK - 1 byte
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
// Track when we last received data so a silently-vanished peer (no FIN/RST
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
// can't wedge the device indefinitely. Without this, the loop only exits
// on actual data, EOF, or a non-EWOULDBLOCK error from read(), and lwIP
// TCP keepalive isn't enabled here.
last_data_ms = millis();
while (total < ota_size) {
// TODO: timeout check
if (millis() - last_data_ms > OTA_SOCKET_TIMEOUT_DATA) {
ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA);
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
size_t remaining = ota_size - total;
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
ssize_t read = this->client_->read(buf, requested);
@@ -369,6 +380,7 @@ void ESPHomeOTAComponent::handle_data_() {
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
last_data_ms = millis();
error_code = this->backend_->write(buf, read);
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Flash write err %d", error_code);
+8 -8
View File
@@ -26,9 +26,9 @@ espnow_ns = cg.esphome_ns.namespace("espnow")
ESPNowComponent = espnow_ns.class_("ESPNowComponent", cg.Component)
# Handler interfaces that other components can use to register callbacks
ESPNowReceivedPacketHandler = espnow_ns.class_("ESPNowReceivedPacketHandler")
ESPNowReceivePacketHandler = espnow_ns.class_("ESPNowReceivePacketHandler")
ESPNowUnknownPeerHandler = espnow_ns.class_("ESPNowUnknownPeerHandler")
ESPNowBroadcastedHandler = espnow_ns.class_("ESPNowBroadcastedHandler")
ESPNowBroadcastHandler = espnow_ns.class_("ESPNowBroadcastHandler")
ESPNowRecvInfo = espnow_ns.class_("ESPNowRecvInfo")
ESPNowRecvInfoConstRef = ESPNowRecvInfo.operator("const").operator("ref")
@@ -48,10 +48,10 @@ OnUnknownPeerTrigger = espnow_ns.class_(
"OnUnknownPeerTrigger", ESPNowHandlerTrigger, ESPNowUnknownPeerHandler
)
OnReceiveTrigger = espnow_ns.class_(
"OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivedPacketHandler
"OnReceiveTrigger", ESPNowHandlerTrigger, ESPNowReceivePacketHandler
)
OnBroadcastedTrigger = espnow_ns.class_(
"OnBroadcastedTrigger", ESPNowHandlerTrigger, ESPNowBroadcastedHandler
OnBroadcastTrigger = espnow_ns.class_(
"OnBroadcastTrigger", ESPNowHandlerTrigger, ESPNowBroadcastHandler
)
@@ -94,7 +94,7 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_ON_BROADCAST): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastedTrigger),
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(OnBroadcastTrigger),
cv.Optional(CONF_ADDRESS): cv.mac_address,
}
),
@@ -140,11 +140,11 @@ async def to_code(config):
for on_receive in config.get(CONF_ON_RECEIVE, []):
trigger = await _trigger_to_code(on_receive)
cg.add(var.register_received_handler(trigger))
cg.add(var.register_receive_handler(trigger))
for on_receive in config.get(CONF_ON_BROADCAST, []):
trigger = await _trigger_to_code(on_receive)
cg.add(var.register_broadcasted_handler(trigger))
cg.add(var.register_broadcast_handler(trigger))
# ========================================== A C T I O N S ================================================
+14 -13
View File
@@ -67,6 +67,7 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
}
}
protected:
void play(const Ts &...x) override { /* ignore - see play_complex */
}
@@ -75,7 +76,6 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
this->error_.stop();
}
protected:
ActionList<Ts...> sent_;
ActionList<Ts...> error_;
@@ -89,7 +89,7 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
TEMPLATABLE_VALUE(peer_address_t, address);
public:
protected:
void play(const Ts &...x) override {
peer_address_t address = this->address_.value(x...);
this->parent_->add_peer(address.data());
@@ -99,7 +99,7 @@ template<typename... Ts> class AddPeerAction : public Action<Ts...>, public Pare
template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public Parented<ESPNowComponent> {
TEMPLATABLE_VALUE(peer_address_t, address);
public:
protected:
void play(const Ts &...x) override {
peer_address_t address = this->address_.value(x...);
this->parent_->del_peer(address.data());
@@ -107,8 +107,9 @@ template<typename... Ts> class DeletePeerAction : public Action<Ts...>, public P
};
template<typename... Ts> class SetChannelAction : public Action<Ts...>, public Parented<ESPNowComponent> {
public:
TEMPLATABLE_VALUE(uint8_t, channel)
protected:
void play(const Ts &...x) override {
if (this->parent_->is_wifi_enabled()) {
return;
@@ -125,9 +126,9 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *,
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
}
explicit OnReceiveTrigger() : has_address_(false) {}
explicit OnReceiveTrigger() {}
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -138,7 +139,7 @@ class OnReceiveTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *,
protected:
bool has_address_{false};
uint8_t address_[ESP_NOW_ETH_ALEN];
uint8_t address_[ESP_NOW_ETH_ALEN]{};
};
class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowUnknownPeerHandler {
@@ -148,15 +149,15 @@ class OnUnknownPeerTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_
return false; // Return false to continue processing other internal handlers
}
};
class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowBroadcastedHandler {
class OnBroadcastTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_t *, uint8_t>,
public ESPNowBroadcastHandler {
public:
explicit OnBroadcastedTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
explicit OnBroadcastTrigger(std::array<uint8_t, ESP_NOW_ETH_ALEN> address) : has_address_(true) {
memcpy(this->address_, address.data(), ESP_NOW_ETH_ALEN);
}
explicit OnBroadcastedTrigger() : has_address_(false) {}
explicit OnBroadcastTrigger() {}
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override {
bool match = !this->has_address_ || (memcmp(this->address_, info.src_addr, ESP_NOW_ETH_ALEN) == 0);
if (!match)
return false;
@@ -167,7 +168,7 @@ class OnBroadcastedTrigger : public Trigger<const ESPNowRecvInfo &, const uint8_
protected:
bool has_address_{false};
uint8_t address_[ESP_NOW_ETH_ALEN];
uint8_t address_[ESP_NOW_ETH_ALEN]{};
};
} // namespace esphome::espnow
@@ -299,13 +299,13 @@ void ESPNowComponent::loop() {
format_hex_pretty_to(hex_buf, packet->packet_.receive.data, packet->packet_.receive.size));
#endif
if (memcmp(info.des_addr, ESPNOW_BROADCAST_ADDR, ESP_NOW_ETH_ALEN) == 0) {
for (auto *handler : this->broadcasted_handlers_) {
if (handler->on_broadcasted(info, packet->packet_.receive.data, packet->packet_.receive.size))
for (auto *handler : this->broadcast_handlers_) {
if (handler->on_broadcast(info, packet->packet_.receive.data, packet->packet_.receive.size))
break; // If a handler returns true, stop processing further handlers
}
} else {
for (auto *handler : this->received_handlers_) {
if (handler->on_received(info, packet->packet_.receive.data, packet->packet_.receive.size))
for (auto *handler : this->receive_handlers_) {
if (handler->on_receive(info, packet->packet_.receive.data, packet->packet_.receive.size))
break; // If a handler returns true, stop processing further handlers
}
}
+11 -13
View File
@@ -31,8 +31,8 @@ using peer_address_t = std::array<uint8_t, ESP_NOW_ETH_ALEN>;
enum class ESPNowTriggers : uint8_t {
TRIGGER_NONE = 0,
ON_NEW_PEER = 1,
ON_RECEIVED = 2,
ON_BROADCASTED = 3,
ON_RECEIVE = 2,
ON_BROADCAST = 3,
ON_SUCCEED = 10,
ON_FAILED = 11,
};
@@ -74,18 +74,18 @@ class ESPNowReceivedPacketHandler {
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
virtual bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
/// Handler interface for receiving broadcasted ESPNow packets
/// Handler interface for receiving ESPNow broadcast packets
/// Components should inherit from this class to handle incoming ESPNow data
class ESPNowBroadcastedHandler {
class ESPNowBroadcastHandler {
public:
/// Called when a broadcasted ESPNow packet is received
/// Called when an ESPNow broadcast packet is received
/// @param info Information about the received packet (sender MAC, etc.)
/// @param data Pointer to the received data payload
/// @param size Size of the received data in bytes
/// @return true if the packet was handled, false otherwise
virtual bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
virtual bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) = 0;
};
class ESPNowComponent : public Component {
@@ -136,13 +136,11 @@ class ESPNowComponent : public Component {
esp_err_t send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
const send_callback_t &callback = nullptr);
void register_received_handler(ESPNowReceivedPacketHandler *handler) { this->received_handlers_.push_back(handler); }
void register_receive_handler(ESPNowReceivedPacketHandler *handler) { this->receive_handlers_.push_back(handler); }
void register_unknown_peer_handler(ESPNowUnknownPeerHandler *handler) {
this->unknown_peer_handlers_.push_back(handler);
}
void register_broadcasted_handler(ESPNowBroadcastedHandler *handler) {
this->broadcasted_handlers_.push_back(handler);
}
void register_broadcast_handler(ESPNowBroadcastHandler *handler) { this->broadcast_handlers_.push_back(handler); }
protected:
friend void on_data_received(const esp_now_recv_info_t *info, const uint8_t *data, int size);
@@ -156,8 +154,8 @@ class ESPNowComponent : public Component {
void send_();
std::vector<ESPNowUnknownPeerHandler *> unknown_peer_handlers_;
std::vector<ESPNowReceivedPacketHandler *> received_handlers_;
std::vector<ESPNowBroadcastedHandler *> broadcasted_handlers_;
std::vector<ESPNowReceivedPacketHandler *> receive_handlers_;
std::vector<ESPNowBroadcastHandler *> broadcast_handlers_;
std::vector<ESPNowPeer> peers_{};
@@ -26,10 +26,10 @@ void ESPNowTransport::setup() {
this->peer_address_[5]);
// Register received handler
this->parent_->register_received_handler(this);
this->parent_->register_receive_handler(this);
// Register broadcasted handler
this->parent_->register_broadcasted_handler(this);
// Register broadcast handler
this->parent_->register_broadcast_handler(this);
}
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
@@ -56,7 +56,7 @@ void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
});
}
bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
bool ESPNowTransport::on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
@@ -71,7 +71,7 @@ bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *dat
return false; // Allow other handlers to run
}
bool ESPNowTransport::on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
bool ESPNowTransport::on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
@@ -15,7 +15,7 @@ namespace espnow {
class ESPNowTransport : public packet_transport::PacketTransport,
public Parented<ESPNowComponent>,
public ESPNowReceivedPacketHandler,
public ESPNowBroadcastedHandler {
public ESPNowBroadcastHandler {
public:
void setup() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
@@ -25,8 +25,8 @@ class ESPNowTransport : public packet_transport::PacketTransport,
}
// ESPNow handler interface
bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
bool on_receive(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
bool on_broadcast(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
protected:
void send_packet(const std::vector<uint8_t> &buf) const override;
+2 -1
View File
@@ -19,6 +19,7 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -108,7 +109,7 @@ async def setup_event_core_(var, config, *, event_types: list[str]):
async def register_event(var, config, *, event_types: list[str]):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_event(var))
queue_entity_register("event", config)
CORE.register_platform_component("event", var)
await setup_event_core_(var, config, event_types=event_types)
@@ -1,5 +1,6 @@
import logging
from pathlib import Path
from typing import Any
from esphome import git, loader
import esphome.config_validation as cv
@@ -17,7 +18,7 @@ from esphome.const import (
TYPE_GIT,
TYPE_LOCAL,
)
from esphome.core import CORE
from esphome.core import CORE, TimePeriodSeconds
_LOGGER = logging.getLogger(__name__)
@@ -35,17 +36,15 @@ CONFIG_SCHEMA = cv.ensure_list(
)
async def to_code(config):
async def to_code(config: dict[str, Any]) -> None:
pass
def _process_git_config(config: dict, refresh, skip_update: bool = False) -> str:
# When skip_update is True, use NEVER_REFRESH to prevent updates
actual_refresh = git.NEVER_REFRESH if skip_update else refresh
def _process_git_config(config: dict[str, Any], refresh: TimePeriodSeconds) -> Path:
repo_dir, _ = git.clone_or_update(
url=config[CONF_URL],
ref=config.get(CONF_REF),
refresh=actual_refresh,
refresh=refresh,
domain=DOMAIN,
username=config.get(CONF_USERNAME),
password=config.get(CONF_PASSWORD),
@@ -72,12 +71,12 @@ def _process_git_config(config: dict, refresh, skip_update: bool = False) -> str
return components_dir
def _process_single_config(config: dict, skip_update: bool = False):
def _process_single_config(config: dict[str, Any]) -> None:
conf = config[CONF_SOURCE]
if conf[CONF_TYPE] == TYPE_GIT:
with cv.prepend_path([CONF_SOURCE]):
components_dir = _process_git_config(
config[CONF_SOURCE], config[CONF_REFRESH], skip_update
config[CONF_SOURCE], config[CONF_REFRESH]
)
elif conf[CONF_TYPE] == TYPE_LOCAL:
components_dir = Path(CORE.relative_config_path(conf[CONF_PATH]))
@@ -107,7 +106,7 @@ def _process_single_config(config: dict, skip_update: bool = False):
loader.install_meta_finder(components_dir, allowed_components=allowed_components)
def do_external_components_pass(config: dict, skip_update: bool = False) -> None:
def do_external_components_pass(config: dict[str, Any]) -> None:
conf = config.get(DOMAIN)
if conf is None:
return
@@ -115,4 +114,4 @@ def do_external_components_pass(config: dict, skip_update: bool = False) -> None
conf = CONFIG_SCHEMA(conf)
for i, c in enumerate(conf):
with cv.prepend_path(i):
_process_single_config(c, skip_update)
_process_single_config(c)
+2 -2
View File
@@ -35,7 +35,7 @@ void EZOSensor::update() {
}
if (!found) {
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
auto ezo_command = make_unique<EzoCommand>();
ezo_command->command = "R";
ezo_command->command_type = EzoCommandType::EZO_READ;
ezo_command->delay_ms = 900;
@@ -162,7 +162,7 @@ void EZOSensor::loop() {
}
void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) {
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
auto ezo_command = make_unique<EzoCommand>();
ezo_command->command = command;
ezo_command->command_type = command_type;
ezo_command->delay_ms = delay_ms;
+41 -14
View File
@@ -31,13 +31,19 @@ from esphome.const import (
CONF_TRIGGER_ID,
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core import CORE, CoroPriority, Lambda, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import LambdaExpression
IS_PLATFORM_COMPONENT = True
fan_ns = cg.esphome_ns.namespace("fan")
Fan = fan_ns.class_("Fan", cg.EntityBase)
FanCall = fan_ns.class_("FanCall")
FanDirection = fan_ns.enum("FanDirection", is_class=True)
FAN_DIRECTION_ENUM = {
@@ -292,7 +298,7 @@ async def setup_fan_core_(var, config):
async def register_fan(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_fan(var))
queue_entity_register("fan", config)
CORE.register_platform_component("fan", var)
await setup_fan_core_(var, config)
@@ -343,17 +349,38 @@ async def fan_turn_off_to_code(config, action_id, template_arg, args):
)
async def fan_turn_on_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)
if (oscillating := config.get(CONF_OSCILLATING)) is not None:
template_ = await cg.templatable(oscillating, args, cg.bool_)
cg.add(var.set_oscillating(template_))
if (speed := config.get(CONF_SPEED)) is not None:
template_ = await cg.templatable(speed, args, cg.int_)
cg.add(var.set_speed(template_))
if (direction := config.get(CONF_DIRECTION)) is not None:
template_ = await cg.templatable(direction, args, FanDirection)
cg.add(var.set_direction(template_))
return var
# All configured fields are folded into a single stateless lambda whose
# constants live in flash; the action stores only a function pointer.
FIELDS = (
(CONF_OSCILLATING, "set_oscillating", cg.bool_),
(CONF_SPEED, "set_speed", cg.int_),
(CONF_DIRECTION, "set_direction", FanDirection),
)
fwd_args = ", ".join(name for _, name in args)
body_lines: list[str] = []
for conf_key, setter, type_ in FIELDS:
if (value := config.get(conf_key)) is None:
continue
if isinstance(value, Lambda):
inner = await cg.process_lambda(value, args, return_type=type_)
body_lines.append(f"call.{setter}(({inner})({fwd_args}));")
else:
body_lines.append(f"call.{setter}({cg.safe_exp(value)});")
# Match TurnOnAction::ApplyFn signature: const Ts &... for trigger args.
apply_args = [
(FanCall.operator("ref"), "call"),
*((t.operator("const").operator("ref"), n) for t, n in args),
]
apply_lambda = LambdaExpression(
["\n".join(body_lines)],
apply_args,
capture="",
return_type=cg.void,
)
return cg.new_Pvariable(action_id, template_arg, paren, apply_lambda)
@automation.register_action(
+9 -14
View File
@@ -7,29 +7,24 @@
namespace esphome {
namespace fan {
// All configured fields are baked into a single stateless lambda whose
// constants live in flash. The action only stores one function pointer
// plus one parent pointer, regardless of how many fields the user set.
// Trigger args are forwarded to the apply function so user lambdas
// (e.g. `speed: !lambda "return x;"`) keep working.
template<typename... Ts> class TurnOnAction : public Action<Ts...> {
public:
explicit TurnOnAction(Fan *state) : state_(state) {}
TEMPLATABLE_VALUE(bool, oscillating)
TEMPLATABLE_VALUE(int, speed)
TEMPLATABLE_VALUE(FanDirection, direction)
using ApplyFn = void (*)(FanCall &, const Ts &...);
TurnOnAction(Fan *state, ApplyFn apply) : state_(state), apply_(apply) {}
void play(const Ts &...x) override {
auto call = this->state_->turn_on();
if (this->oscillating_.has_value()) {
call.set_oscillating(this->oscillating_.value(x...));
}
if (this->speed_.has_value()) {
call.set_speed(this->speed_.value(x...));
}
if (this->direction_.has_value()) {
call.set_direction(this->direction_.value(x...));
}
this->apply_(call, x...);
call.perform();
}
Fan *state_;
ApplyFn apply_;
};
template<typename... Ts> class TurnOffAction : public Action<Ts...> {
+11 -13
View File
@@ -3,11 +3,12 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace feedback {
namespace esphome::feedback {
static const char *const TAG = "feedback.cover";
static constexpr uint32_t DIRECTION_CHANGE_TIMEOUT_ID = 1;
using namespace esphome::cover;
void FeedbackCover::setup() {
@@ -37,7 +38,7 @@ void FeedbackCover::setup() {
}
#endif
this->last_recompute_time_ = this->start_dir_time_ = millis();
this->last_recompute_time_ = this->start_dir_time_ = App.get_loop_component_start_time();
}
CoverTraits FeedbackCover::get_traits() {
@@ -135,7 +136,7 @@ void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop
#endif
void FeedbackCover::endstop_reached_(bool open_endstop) {
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
@@ -174,7 +175,7 @@ void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool
if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
#endif
{
auto now = millis();
const uint32_t now = App.get_loop_component_start_time();
this->current_operation = operation;
this->start_dir_time_ = this->last_recompute_time_ = now;
this->publish_state();
@@ -306,7 +307,7 @@ void FeedbackCover::control(const CoverCall &call) {
void FeedbackCover::stop_prev_trigger_() {
if (this->direction_change_waittime_.has_value()) {
this->cancel_timeout("direction_change");
this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID);
}
if (this->prev_command_trigger_ != nullptr) {
this->prev_command_trigger_->stop_action();
@@ -374,12 +375,10 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
// check if we have a wait time
if (this->direction_change_waittime_.has_value() && dir != COVER_OPERATION_IDLE &&
this->current_operation != COVER_OPERATION_IDLE && dir != this->current_operation) {
const uint32_t waittime = *this->direction_change_waittime_;
ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
this->start_direction_(COVER_OPERATION_IDLE);
this->set_timeout("direction_change", *this->direction_change_waittime_,
[this, dir]() { this->start_direction_(dir); });
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, waittime, [this, dir]() { this->start_direction_(dir); });
} else {
this->set_current_operation_(dir, true);
this->prev_command_trigger_ = trig;
@@ -395,7 +394,7 @@ void FeedbackCover::recompute_position_() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
float dir;
float action_dur;
float min_pos;
@@ -451,5 +450,4 @@ void FeedbackCover::recompute_position_() {
this->last_recompute_time_ = now;
}
} // namespace feedback
} // namespace esphome
} // namespace esphome::feedback
+2 -4
View File
@@ -8,8 +8,7 @@
#endif
#include "esphome/components/cover/cover.h"
namespace esphome {
namespace feedback {
namespace esphome::feedback {
class FeedbackCover : public cover::Cover, public Component {
public:
@@ -85,5 +84,4 @@ class FeedbackCover : public cover::Cover, public Component {
uint32_t update_interval_{1000};
};
} // namespace feedback
} // namespace esphome
} // namespace esphome::feedback
+20 -28
View File
@@ -85,7 +85,7 @@ void HonClimate::set_horizontal_airflow(hon_protocol::HorizontalSwingMode direct
this->force_send_control_ = true;
}
std::string HonClimate::get_cleaning_status_text() const {
const char *HonClimate::get_cleaning_status_text() const {
switch (this->cleaning_status_) {
case CleaningState::SELF_CLEAN:
return "Self clean";
@@ -134,29 +134,22 @@ haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haie
}
// All OK
hon_protocol::DeviceVersionAnswer *answr = (hon_protocol::DeviceVersionAnswer *) data;
char tmp[9];
tmp[8] = 0;
strncpy(tmp, answr->protocol_version, 8);
this->hvac_hardware_info_ = HardwareInfo();
this->hvac_hardware_info_.value().protocol_version_ = std::string(tmp);
strncpy(tmp, answr->software_version, 8);
this->hvac_hardware_info_.value().software_version_ = std::string(tmp);
strncpy(tmp, answr->hardware_version, 8);
this->hvac_hardware_info_.value().hardware_version_ = std::string(tmp);
strncpy(tmp, answr->device_name, 8);
this->hvac_hardware_info_.value().device_name_ = std::string(tmp);
HardwareInfo info{}; // zero-init guarantees null-termination
strncpy(info.protocol_version_, answr->protocol_version, HARDWARE_INFO_STR_SIZE - 1);
strncpy(info.software_version_, answr->software_version, HARDWARE_INFO_STR_SIZE - 1);
strncpy(info.hardware_version_, answr->hardware_version, HARDWARE_INFO_STR_SIZE - 1);
strncpy(info.device_name_, answr->device_name, HARDWARE_INFO_STR_SIZE - 1);
info.functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
info.functions_[1] = (answr->functions[1] & 0x02) != 0; // controller-device mode support
info.functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
info.functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
info.functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
this->use_crc_ = info.functions_[2];
#ifdef USE_TEXT_SENSOR
this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, this->hvac_hardware_info_.value().device_name_);
this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION,
this->hvac_hardware_info_.value().protocol_version_);
this->update_sub_text_sensor_(SubTextSensorType::APPLIANCE_NAME, info.device_name_);
this->update_sub_text_sensor_(SubTextSensorType::PROTOCOL_VERSION, info.protocol_version_);
#endif
this->hvac_hardware_info_.value().functions_[0] = (answr->functions[1] & 0x01) != 0; // interactive mode support
this->hvac_hardware_info_.value().functions_[1] =
(answr->functions[1] & 0x02) != 0; // controller-device mode support
this->hvac_hardware_info_.value().functions_[2] = (answr->functions[1] & 0x04) != 0; // crc support
this->hvac_hardware_info_.value().functions_[3] = (answr->functions[1] & 0x08) != 0; // multiple AC support
this->hvac_hardware_info_.value().functions_[4] = (answr->functions[1] & 0x20) != 0; // roles support
this->use_crc_ = this->hvac_hardware_info_.value().functions_[2];
this->hvac_hardware_info_ = info;
this->set_phase(ProtocolPhases::SENDING_INIT_2);
return result;
} else {
@@ -347,10 +340,9 @@ void HonClimate::dump_config() {
" Device software version: %s\n"
" Device hardware version: %s\n"
" Device name: %s",
this->hvac_hardware_info_.value().protocol_version_.c_str(),
this->hvac_hardware_info_.value().software_version_.c_str(),
this->hvac_hardware_info_.value().hardware_version_.c_str(),
this->hvac_hardware_info_.value().device_name_.c_str());
this->hvac_hardware_info_.value().protocol_version_,
this->hvac_hardware_info_.value().software_version_,
this->hvac_hardware_info_.value().hardware_version_, this->hvac_hardware_info_.value().device_name_);
ESP_LOGCONFIG(TAG, " Device features:%s%s%s%s%s",
(this->hvac_hardware_info_.value().functions_[0] ? " interactive" : ""),
(this->hvac_hardware_info_.value().functions_[1] ? " controller-device" : ""),
@@ -460,7 +452,7 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
if (this->action_request_.has_value()) {
if (this->action_request_.value().message.has_value()) {
this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
this->action_request_.value().message.reset();
this->action_request_.value().message.reset(); // NOLINT(bugprone-unchecked-optional-access)
} else {
// Message already sent, reseting request and return to idle
this->action_request_.reset();
@@ -796,7 +788,7 @@ void HonClimate::set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSe
}
}
void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const std::string &value) {
void HonClimate::update_sub_text_sensor_(SubTextSensorType type, const char *value) {
size_t index = (size_t) type;
if (this->sub_text_sensors_[index] != nullptr)
this->sub_text_sensors_[index]->publish_state(value);
+7 -6
View File
@@ -90,7 +90,7 @@ class HonClimate : public HaierClimateBase {
void set_sub_text_sensor(SubTextSensorType type, text_sensor::TextSensor *sens);
protected:
void update_sub_text_sensor_(SubTextSensorType type, const std::string &value);
void update_sub_text_sensor_(SubTextSensorType type, const char *value);
text_sensor::TextSensor *sub_text_sensors_[(size_t) SubTextSensorType::SUB_TEXT_SENSOR_TYPE_COUNT]{nullptr};
#endif
#ifdef USE_SWITCH
@@ -116,7 +116,7 @@ class HonClimate : public HaierClimateBase {
void set_vertical_airflow(hon_protocol::VerticalSwingMode direction);
esphome::optional<hon_protocol::HorizontalSwingMode> get_horizontal_airflow() const;
void set_horizontal_airflow(hon_protocol::HorizontalSwingMode direction);
std::string get_cleaning_status_text() const;
const char *get_cleaning_status_text() const;
CleaningState get_cleaning_status() const;
void start_self_cleaning();
void start_steri_cleaning();
@@ -166,11 +166,12 @@ class HonClimate : public HaierClimateBase {
void fill_control_messages_queue_();
void clear_control_messages_queue_();
static constexpr size_t HARDWARE_INFO_STR_SIZE = 9;
struct HardwareInfo {
std::string protocol_version_;
std::string software_version_;
std::string hardware_version_;
std::string device_name_;
char protocol_version_[HARDWARE_INFO_STR_SIZE];
char software_version_[HARDWARE_INFO_STR_SIZE];
char hardware_version_[HARDWARE_INFO_STR_SIZE];
char device_name_[HARDWARE_INFO_STR_SIZE];
bool functions_[5];
};
@@ -191,7 +191,7 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
if (this->action_request_.has_value()) {
if (this->action_request_.value().message.has_value()) {
this->send_message_(this->action_request_.value().message.value(), this->use_crc_);
this->action_request_.value().message.reset();
this->action_request_.value().message.reset(); // NOLINT(bugprone-unchecked-optional-access)
} else {
// Message already sent, reseting request and return to idle
this->action_request_.reset();
@@ -210,8 +210,9 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
#ifdef USE_WIFI
else if (this->send_wifi_signal_ &&
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) {
this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
}
#endif
} break;
default:
+2 -7
View File
@@ -8,7 +8,6 @@
#include <csignal>
#include <sched.h>
#include <time.h>
#include <cmath>
#include <cstdlib>
namespace {
@@ -22,9 +21,7 @@ void HOT yield() { ::sched_yield(); }
uint32_t IRAM_ATTR HOT millis() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
time_t seconds = spec.tv_sec;
uint32_t ms = round(spec.tv_nsec / 1e6);
return ((uint32_t) seconds) * 1000U + ms;
return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000);
}
uint64_t millis_64() {
struct timespec spec;
@@ -43,9 +40,7 @@ void HOT delay(uint32_t ms) {
uint32_t IRAM_ATTR HOT micros() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
time_t seconds = spec.tv_sec;
uint32_t us = round(spec.tv_nsec / 1e3);
return ((uint32_t) seconds) * 1000000U + us;
return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000);
}
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) {
struct timespec ts;
@@ -462,7 +462,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
this->request_headers_.push_back({key, value});
}
void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); }
void add_collect_header(const char *value) { this->lower_case_collect_headers_.emplace_back(value); }
void init_json(size_t count) { this->json_.init(count); }
void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.push_back({key, value}); }
@@ -243,7 +243,7 @@ int HttpContainerArduino::read(uint8_t *buf, size_t max_len) {
// Non-chunked path
int available_data = stream_ptr->available();
size_t remaining = (this->content_length > 0) ? (this->content_length - this->bytes_read_) : max_len;
int bufsize = std::min(max_len, std::min(remaining, (size_t) available_data));
int bufsize = std::min({max_len, remaining, (size_t) available_data});
if (bufsize == 0) {
this->duration_ms += (millis() - start);
+1 -1
View File
@@ -5,7 +5,7 @@
#include "i2c_bus.h"
#include "esphome/core/component.h"
struct device;
struct device; // NOLINT(readability-identifier-naming) - forward decl of Zephyr's device type
namespace esphome::i2c {
+14 -7
View File
@@ -744,21 +744,28 @@ async def write_image(config, all_frames=False):
if frame_count <= 1:
_LOGGER.warning("Image file %s has no animation frames", path)
total_rows = height * frame_count
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
if byte_order := config.get(CONF_BYTE_ORDER):
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
# Encode each frame with its own encoder and concatenate. This keeps every
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
# correct without needing to know the total frame count.
byte_order = config.get(CONF_BYTE_ORDER)
combined_data: list[int] = []
encoder: ImageEncoder | None = None
for frame_index in range(frame_count):
image.seek(frame_index)
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
if byte_order is not None:
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
pixels = encoder.convert(image.resize((width, height)), path).getdata()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
encoder.end_image()
combined_data.extend(encoder.data)
rhs = [HexInt(x) for x in encoder.data]
rhs = [HexInt(x) for x in combined_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
image_type = get_image_type_enum(type)
trans_value = get_transparency_enum(encoder.transparency)
+2 -2
View File
@@ -12,7 +12,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import setup_entity
from esphome.core.entity_helpers import queue_entity_register, setup_entity
from esphome.coroutine import CoroPriority
from esphome.types import ConfigType
@@ -54,8 +54,8 @@ async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None:
"""Register an infrared device with the core."""
cg.add_define("USE_IR_RF")
await cg.register_component(var, config)
queue_entity_register("infrared", config)
await setup_infrared_core_(var, config)
cg.add(cg.App.register_infrared(var))
CORE.register_platform_component("infrared", var)
@@ -41,12 +41,12 @@ bool InkbirdIbstH1Mini::parse_device(const esp32_ble_tracker::ESPBTDevice &devic
ESP_LOGVV(TAG, "parse_device(): service_data is expected to be empty");
return false;
}
auto mnf_datas = device.get_manufacturer_datas();
const auto &mnf_datas = device.get_manufacturer_datas();
if (mnf_datas.size() != 1) {
ESP_LOGVV(TAG, "parse_device(): manufacturer_datas is expected to have a single element");
return false;
}
auto mnf_data = mnf_datas[0];
const auto &mnf_data = mnf_datas[0];
if (mnf_data.uuid.get_uuid().len != ESP_UUID_LEN_16) {
ESP_LOGVV(TAG, "parse_device(): manufacturer data element is expected to have uuid of length 16");
return false;
+111 -1
View File
@@ -1,13 +1,73 @@
#include "ir_rf_proxy.h"
#include <cinttypes>
#include "esphome/core/log.h"
namespace esphome::ir_rf_proxy {
static const char *const TAG = "ir_rf_proxy";
// ========== Shared transmit helper ==========
// Static template: all instantiations occur in this translation unit.
template<typename CallT>
static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter, uint32_t carrier_frequency,
const CallT &call) {
if (transmitter == nullptr) {
ESP_LOGW(TAG, "No transmitter configured");
return;
}
if (!call.has_raw_timings()) {
ESP_LOGE(TAG, "No raw timings provided");
return;
}
auto transmit_call = transmitter->transmit();
auto *transmit_data = transmit_call.get_data();
transmit_data->set_carrier_frequency(carrier_frequency);
if (call.is_packed()) {
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
call.get_repeat_count());
} else if (call.is_base64url()) {
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return;
}
constexpr int32_t max_timing_us = 500000;
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
return;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
call.get_repeat_count());
} else {
transmit_data->set_data(call.get_raw_timings());
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
call.get_repeat_count());
}
if (call.get_repeat_count() > 0) {
transmit_call.set_send_times(call.get_repeat_count());
}
transmit_call.perform();
}
// ========== IrRfProxy (Infrared platform) ==========
#ifdef USE_IR_RF
void IrRfProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"IR/RF Proxy '%s'\n"
"IR Proxy '%s'\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
@@ -20,4 +80,54 @@ void IrRfProxy::dump_config() {
}
}
void IrRfProxy::control(const infrared::InfraredCall &call) {
uint32_t carrier = call.get_carrier_frequency().value_or(0);
transmit_raw_timings(this->transmitter_, carrier, call);
}
#endif // USE_IR_RF
// ========== RfProxy (Radio Frequency platform) ==========
#ifdef USE_RADIO_FREQUENCY
void RfProxy::setup() {
this->traits_.set_supports_transmitter(this->transmitter_ != nullptr);
this->traits_.set_supports_receiver(this->receiver_ != nullptr);
// remote_transmitter/receiver always uses OOK (on-off keying)
this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK);
if (this->receiver_ != nullptr) {
this->receiver_->register_listener(this);
}
}
void RfProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"RF Proxy '%s'\n"
" Backend: remote_transmitter/receiver\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
YESNO(this->traits_.get_supports_receiver()));
const auto &traits = this->traits_;
if (traits.get_frequency_min_hz() > 0) {
if (traits.get_frequency_min_hz() == traits.get_frequency_max_hz()) {
ESP_LOGCONFIG(TAG, " Frequency: %.3f MHz (fixed)", traits.get_frequency_min_hz() / 1e6f);
} else {
ESP_LOGCONFIG(TAG, " Frequency Range: %.3f - %.3f MHz", traits.get_frequency_min_hz() / 1e6f,
traits.get_frequency_max_hz() / 1e6f);
}
}
}
void RfProxy::control(const radio_frequency::RadioFrequencyCall &call) {
// RF: no IR carrier modulation
transmit_raw_timings(this->transmitter_, 0, call);
}
#endif // USE_RADIO_FREQUENCY
} // namespace esphome::ir_rf_proxy
@@ -4,10 +4,19 @@
// without following the normal breaking changes policy. Use at your own risk.
// Once the API is considered stable, this warning will be removed.
#include "esphome/components/remote_base/remote_base.h"
#ifdef USE_IR_RF
#include "esphome/components/infrared/infrared.h"
#endif
#ifdef USE_RADIO_FREQUENCY
#include "esphome/components/radio_frequency/radio_frequency.h"
#endif
namespace esphome::ir_rf_proxy {
#ifdef USE_IR_RF
/// IrRfProxy - Infrared platform implementation using remote_transmitter/receiver as backend
class IrRfProxy : public infrared::Infrared {
public:
@@ -26,8 +35,36 @@ class IrRfProxy : public infrared::Infrared {
void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); }
protected:
void control(const infrared::InfraredCall &call) override;
// RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF
uint32_t frequency_khz_{0};
};
#endif // USE_IR_RF
#ifdef USE_RADIO_FREQUENCY
/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend
class RfProxy : public radio_frequency::RadioFrequency {
public:
RfProxy() = default;
void setup() override;
void dump_config() override;
/// Set the remote transmitter component
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
/// Set the remote receiver component
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
/// Set the fixed carrier frequency in Hz (metadata: advertised via traits, does not tune hardware)
void set_frequency_hz(uint32_t freq_hz) { this->traits_.set_fixed_frequency_hz(freq_hz); }
protected:
void control(const radio_frequency::RadioFrequencyCall &call) override;
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
remote_base::RemoteReceiverBase *receiver_{nullptr};
};
#endif // USE_RADIO_FREQUENCY
} // namespace esphome::ir_rf_proxy
@@ -0,0 +1,68 @@
"""Radio Frequency platform implementation using remote_base (remote_transmitter/receiver)."""
import esphome.codegen as cg
from esphome.components import radio_frequency, remote_receiver, remote_transmitter
import esphome.config_validation as cv
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
import esphome.final_validate as fv
from esphome.types import ConfigType
from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["radio_frequency"]
RfProxy = ir_rf_proxy_ns.class_("RfProxy", radio_frequency.RadioFrequency)
CONFIG_SCHEMA = cv.All(
radio_frequency.radio_frequency_schema(RfProxy).extend(
{
cv.Optional(CONF_FREQUENCY): cv.frequency,
cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id(
remote_receiver.RemoteReceiverComponent
),
cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
}
),
cv.has_exactly_one_key(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID),
)
def _final_validate(config: ConfigType) -> None:
"""Validate that RF transmitters have carrier duty set to 100%."""
if CONF_REMOTE_TRANSMITTER_ID not in config:
return
transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID]
full_config = fv.full_config.get()
transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1]
transmitter_config = full_config.get_config_for_path(transmitter_path)
duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT)
if duty_percent is not None and duty_percent != 100:
raise cv.Invalid(
f"Transmitter '{transmitter_id}' must have '{CONF_CARRIER_DUTY_PERCENT}' "
"set to 100% for RF transmission. Dedicated RF hardware handles modulation; "
"applying a carrier duty cycle would corrupt the signal"
)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
"""Code generation for remote_base radio frequency platform."""
var = await radio_frequency.new_radio_frequency(config)
if CONF_FREQUENCY in config:
cg.add(var.set_frequency_hz(int(config[CONF_FREQUENCY])))
if CONF_REMOTE_TRANSMITTER_ID in config:
transmitter = await cg.get_variable(config[CONF_REMOTE_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter))
if CONF_REMOTE_RECEIVER_ID in config:
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
cg.add(var.set_receiver(receiver))
+3 -2
View File
@@ -39,7 +39,8 @@ bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
}
JsonDocument parse_json(const uint8_t *data, size_t len) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape) false positives with
// ArduinoJson
if (data == nullptr || len == 0) {
ESP_LOGE(TAG, "No data to parse");
return JsonObject(); // return unbound object
@@ -63,7 +64,7 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
}
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
return JsonObject(); // return unbound object
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape)
}
SerializationBuffer<> JsonBuilder::serialize() {
@@ -139,12 +139,12 @@ void KamstrupKMPComponent::clear_uart_rx_buffer_() {
void KamstrupKMPComponent::read_command_(uint16_t command) {
uint8_t buffer[20] = {0};
int buffer_len = 0;
size_t buffer_len = 0;
int data;
int timeout = 250; // ms
// Read the data from the UART
while (timeout > 0 && buffer_len < static_cast<int>(sizeof(buffer))) {
while (timeout > 0 && buffer_len < sizeof(buffer)) {
if (this->available()) {
data = this->read();
if (data > -1) {
@@ -183,7 +183,7 @@ void KamstrupKMPComponent::read_command_(uint16_t command) {
// Decode
uint8_t msg[20] = {0};
int msg_len = 0;
for (int i = 1; i < buffer_len - 1; i++) {
for (size_t i = 1; i < buffer_len - 1; i++) {
if (buffer[i] == 0x1B) {
msg[msg_len++] = buffer[i + 1] ^ 0xFF;
i++;
+22 -5
View File
@@ -479,6 +479,20 @@ async def component_to_code(config):
# RAM-executable output section and prints a post-link placement summary.
if FAMILY_COMPONENT[config[CONF_FAMILY]] != COMPONENT_BK72XX:
cg.add_platformio_option("extra_scripts", ["pre:patch_linker.py"])
else:
# BK72XX: wrap the SDK's exception trap handlers (defined in LibreTiny's
# intc.c fixup, called from the closed-source SDK's do_undefined/pabort/
# dabort) so we can capture register state + a stack-scan backtrace into
# a .noinit region before the SDK's own register dump + bk_cpu_shutdown
# chain runs. The .noinit region is injected into the generated linker
# script by patch_bk72xx_noinit.py so it survives the watchdog reset.
# bk72xx/__init__.py is auto-generated, so the define is set here
# rather than there. (defines.h has the matching #ifdef USE_BK72XX
# block for static analysis / IDE only — it doesn't drive the build.)
cg.add_define("USE_BK72XX_CRASH_HANDLER")
for sym in ("bk_trap_udef", "bk_trap_pabt", "bk_trap_dabt"):
cg.add_build_flag(f"-Wl,--wrap={sym}")
cg.add_platformio_option("extra_scripts", ["pre:patch_bk72xx_noinit.py"])
# dummy version code
cg.add_define("USE_ARDUINO_VERSION_CODE", cg.RawExpression("VERSION_CODE(0, 0, 0)"))
# decrease web server stack size (16k words -> 4k words)
@@ -568,8 +582,11 @@ async def component_to_code(config):
# Called by writer.py
def copy_files() -> None:
script_dir = Path(__file__).parent
patch_linker_file = script_dir / "patch_linker.py.script"
copy_file_if_changed(
patch_linker_file,
CORE.relative_build_path("patch_linker.py"),
)
for src_name, dst_name in (
("patch_linker.py.script", "patch_linker.py"),
("patch_bk72xx_noinit.py.script", "patch_bk72xx_noinit.py"),
):
copy_file_if_changed(
script_dir / src_name,
CORE.relative_build_path(dst_name),
)
+4
View File
@@ -58,6 +58,7 @@ COMPONENT_RTL87XX = "rtl87xx"
FAMILY_BK7231N = "BK7231N"
FAMILY_BK7231Q = "BK7231Q"
FAMILY_BK7231T = "BK7231T"
FAMILY_BK7238 = "BK7238"
FAMILY_BK7251 = "BK7251"
FAMILY_LN882H = "LN882H"
FAMILY_RTL8710B = "RTL8710B"
@@ -66,6 +67,7 @@ FAMILIES = [
FAMILY_BK7231N,
FAMILY_BK7231Q,
FAMILY_BK7231T,
FAMILY_BK7238,
FAMILY_BK7251,
FAMILY_LN882H,
FAMILY_RTL8710B,
@@ -75,6 +77,7 @@ FAMILY_FRIENDLY = {
FAMILY_BK7231N: "BK7231N",
FAMILY_BK7231Q: "BK7231Q",
FAMILY_BK7231T: "BK7231T",
FAMILY_BK7238: "BK7238",
FAMILY_BK7251: "BK7251",
FAMILY_LN882H: "LN882H",
FAMILY_RTL8710B: "RTL8710B",
@@ -84,6 +87,7 @@ FAMILY_COMPONENT = {
FAMILY_BK7231N: COMPONENT_BK72XX,
FAMILY_BK7231Q: COMPONENT_BK72XX,
FAMILY_BK7231T: COMPONENT_BK72XX,
FAMILY_BK7238: COMPONENT_BK72XX,
FAMILY_BK7251: COMPONENT_BK72XX,
FAMILY_LN882H: COMPONENT_LN882X,
FAMILY_RTL8710B: COMPONENT_RTL87XX,
+2 -77
View File
@@ -1,81 +1,6 @@
#ifdef USE_LIBRETINY
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/time_64.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <FreeRTOS.h>
#include <task.h>
void setup();
void loop();
namespace esphome {
void HOT yield() { ::yield(); }
// Inline the tick read so esphome::millis() matches MillisInternal::get()'s fast
// path instead of going through the Arduino core's out-of-line ::millis() wrapper.
//
// RTL87xx / LN882x (1 kHz): xTaskGetTickCount() is already ms. IRAM_ATTR + ISR
// dispatch are needed because ISR handlers (e.g. rotary_encoder) call millis().
//
// BK72xx (500 Hz): ticks * portTICK_PERIOD_MS (== 2). IRAM_ATTR and ISR dispatch
// are both unnecessary — the SDK masks FIQ + IRQ during flash writes (see hal.h),
// so no ISR runs while flash is stalled.
#if defined(USE_RTL87XX) || defined(USE_LN882X)
uint32_t IRAM_ATTR HOT millis() {
static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick");
return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount();
}
#elif defined(USE_BK72XX)
uint32_t HOT millis() {
static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick");
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
#else
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
#endif
uint64_t millis_64() { return Millis64Impl::compute(millis()); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void HOT delay(uint32_t ms) { ::delay(ms); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
void arch_init() {
libretiny::setup_preferences();
lt_wdt_enable(10000L);
#ifdef USE_BK72XX
// BK72xx SDK creates the main Arduino task at priority 3, which is lower than
// all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop
// stalls whenever WiFi background processing runs, because the main task
// cannot resume until every higher-priority task finishes.
//
// By contrast, RTL87xx creates the main task at osPriorityRealtime (highest).
//
// Raise to priority 6: above WiFi/LwIP tasks (4-5) so they don't preempt the
// main loop, but below the TCP/IP thread (7) so packet processing keeps priority.
// This is safe because ESPHome yields voluntarily via wakeable_delay() and
// the Arduino mainTask yield() after each loop() iteration.
static constexpr UBaseType_t MAIN_TASK_PRIORITY = 6;
static_assert(MAIN_TASK_PRIORITY < configMAX_PRIORITIES, "MAIN_TASK_PRIORITY must be less than configMAX_PRIORITIES");
vTaskPrioritySet(nullptr, MAIN_TASK_PRIORITY);
#endif
#if LT_GPIO_RECOVER
lt_gpio_recover();
#endif
}
void arch_restart() {
lt_reboot();
while (1) {
}
}
void HOT arch_feed_wdt() { lt_wdt_feed(); }
uint32_t arch_get_cpu_cycle_count() { return lt_cpu_get_cycle_count(); }
uint32_t arch_get_cpu_freq_hz() { return lt_cpu_get_freq(); }
} // namespace esphome
// HAL functions live in hal.cpp. core.cpp is intentionally empty for
// libretiny — there is no extra component bootstrap to keep here.
#endif // USE_LIBRETINY
+62
View File
@@ -0,0 +1,62 @@
#ifdef USE_LIBRETINY
#include "core.h"
#include "esphome/core/hal.h"
#include "preferences.h"
#ifdef USE_BK72XX_CRASH_HANDLER
#include "esphome/components/bk72xx/crash_handler.h"
#endif
#include <FreeRTOS.h>
#include <task.h>
// Empty libretiny namespace block to satisfy ci-custom's lint_namespace check.
// HAL functions live in namespace esphome (root) — they are not part of the
// libretiny component's API.
namespace esphome::libretiny {} // namespace esphome::libretiny
namespace esphome {
// yield(), delay(), micros(), millis(), millis_64(), delayMicroseconds(),
// arch_feed_wdt(), arch_get_cpu_cycle_count(), arch_get_cpu_freq_hz()
// inlined in core/hal/hal_libretiny.h.
void arch_init() {
#ifdef USE_BK72XX_CRASH_HANDLER
// Snapshot any crash data left in .noinit by the previous boot before the
// watchdog or anything else can perturb it.
bk72xx::crash_handler_read_and_clear();
#endif
libretiny::setup_preferences();
lt_wdt_enable(10000L);
#ifdef USE_BK72XX
// BK72xx SDK creates the main Arduino task at priority 3, which is lower than
// all WiFi (4-5), LwIP (4), and TCP/IP (7) tasks. This causes ~100ms loop
// stalls whenever WiFi background processing runs, because the main task
// cannot resume until every higher-priority task finishes.
//
// By contrast, RTL87xx creates the main task at osPriorityRealtime (highest).
//
// Raise to priority 6: above WiFi/LwIP tasks (4-5) so they don't preempt the
// main loop, but below the TCP/IP thread (7) so packet processing keeps priority.
// This is safe because ESPHome yields voluntarily via wakeable_delay() and
// the Arduino mainTask yield() after each loop() iteration.
static constexpr UBaseType_t MAIN_TASK_PRIORITY = 6;
static_assert(MAIN_TASK_PRIORITY < configMAX_PRIORITIES, "MAIN_TASK_PRIORITY must be less than configMAX_PRIORITIES");
vTaskPrioritySet(nullptr, MAIN_TASK_PRIORITY);
#endif
#if LT_GPIO_RECOVER
lt_gpio_recover();
#endif
}
void arch_restart() {
lt_reboot();
while (1) {
}
}
} // namespace esphome
#endif // USE_LIBRETINY

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