Compare commits

..

180 Commits

Author SHA1 Message Date
J. Nick Koston ab233e6d83 [improv_serial] Reduce per-loop overhead
- Cache UART selection at setup time so each loop iteration no longer
  dereferences global_logger and pays for a non-inlined Logger::get_uart()
  call before the read switch.
- Use App.get_loop_component_start_time() once per loop instead of two
  millis() calls (especially relevant on ESP8266 where millis() involves
  interrupt-locked 64-bit timer access).
- Move read_byte_() to the header as ESPHOME_ALWAYS_INLINE so the call/ret
  pair and optional<uint8_t> staging are elided at the call sites in loop().
2026-04-26 09:16:16 -05:00
Johan Henkens 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 Koston 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 Koston 8dbdcfc128 [bk72xx] Prepare for BK7238 support (#16018) 2026-04-26 07:24:07 -05:00
J. Nick Koston 8950afc3c4 [bluetooth_proxy] Drop redundant remote_bda_ write in connect handler (#16000) 2026-04-26 07:23:53 -05:00
J. Nick Koston 04d067196d [rotary_encoder][at581x] Fix templatable int field types (#16015) 2026-04-26 07:23:41 -05:00
J. Nick Koston 502c010465 [bh1750] Downgrade per-reading Illuminance log to verbose (#16005) 2026-04-26 07:23:24 -05:00
J. Nick Koston 180105bb4b [bluetooth_proxy] Partial revert of loop() → set_interval migration (#15992) 2026-04-26 07:23:08 -05:00
J. Nick Koston 4c0dfb0e0d [core] Raise ESP32 WDT feed interval to 1/5 of configured timeout (#15984) 2026-04-26 07:22:50 -05:00
J. Nick Koston df987a7ffb [ci-custom] Suggest uint32_to_str/int8_to_str for integer formatting (#15970) 2026-04-26 07:22:34 -05:00
Boris Krivonog c8d4420408 [mitsubishi_cn105] add support for half-degree temperature setpoint (#15919) 2026-04-26 07:19:49 -05:00
Darafei Praliaskouski 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
Darafei Praliaskouski 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 Koston dc57969afd [host] Use integer math in millis()/micros() (#15994) 2026-04-26 08:39:24 +00:00
J. Nick Koston f092e619d8 [rtttl] Gate on_finished_playback callback storage behind define (#16003) 2026-04-26 00:03:59 -05:00
J. Nick Koston 58f6ad2d0c [safe_mode] Use StaticCallbackManager for on_safe_mode (#16002) 2026-04-26 00:01:21 -05:00
Keith Burzinski 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 Koston 4cab262ef8 [ci] Trigger CodSpeed benchmarks on host platform changes (#15995) 2026-04-25 17:18:21 -04:00
dependabot[bot] 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 Koston 4f8feb86f0 [dashboard] Add --no-states support to logs WebSocket handler (#15993) 2026-04-25 15:43:05 -05:00
Javier Peletier 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] 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] 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
dependabot[bot] f62972c2c6 Bump ruff from 0.15.11 to 0.15.12 (#15981)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-24 19:34:00 +00:00
dependabot[bot] f36efbc762 Update tzdata requirement from >=2026.1 to >=2026.2 (#15980)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-24 19:27:12 +00:00
Kevin Ahrendt 9caf9ee023 [sendspin] Bumps sendspin-cpp library for a bugfix (#15976) 2026-04-24 11:53:03 -05:00
Kevin Ahrendt 94e300389c [sendspin] remove year and track number text sensors and refactor (#15975) 2026-04-24 15:35:32 +00:00
Kevin Ahrendt 55bcf33446 [sendspin] Add metadata sensor component (#15971) 2026-04-24 14:32:47 +00:00
Kevin Ahrendt f132b7dc07 [media_player][speaker][speaker_source] Centralize preferred format codegen (#14771) 2026-04-24 14:09:03 +00:00
J. Nick Koston baa6d5f96b [web_server_idf] Fix cross-thread race on SSE session state (#15967) 2026-04-24 08:11:47 -05:00
J. Nick Koston 773b4d887b [core] Scheduler: don't sleep while defer queue is non-empty (#15968) 2026-04-24 08:11:29 -05:00
Kevin Ahrendt ac7f0f0b74 [sendspin] Add a metadata text sensor component (#15969) 2026-04-24 11:07:00 +00:00
Kevin Ahrendt bc7f35b569 [sendspin] Add a Sendspin media source component for playing audio (PR4) (#15950)
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-24 10:00:22 +00:00
J. Nick Koston ae02ab3865 [wifi] Fix stale wifi.connected after state transition (#15966) 2026-04-24 03:42:36 -05:00
J. Nick Koston eceb534895 [deep_sleep] Fix sleep_duration codegen type to uint32_t (#15965) 2026-04-24 07:19:59 +00:00
tomaszduda23 404620b99c [deep_sleep][logger][zephyr][zigbee] add deep sleep support with zigbee wakeup (#13950)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 22:31:46 -04:00
Kevin Ahrendt 3ccaa771a7 [sendspin] Add a group media player controller (PR3) (#15948)
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-24 01:46:25 +00:00
Kevin Ahrendt b4a86e46b2 [sendspin] Add controller role and sendspin.switch action (PR2) (#15929)
Co-authored-by: Copilot <copilot@github.com>
2026-04-23 20:22:47 -05:00
Kevin Ahrendt ddf1426f86 [sendspin] Add initial Sendspin hub component (PR1) (#15924)
Co-authored-by: Copilot <copilot@github.com>
2026-04-23 22:09:36 +00:00
J. Nick Koston 90d7bfe02e [ci] Auto-close PRs opened from a fork's default branch (#15957) 2026-04-23 16:36:32 -05:00
Kevin Ahrendt d759f1a567 [audio_http] Add a media source for playing audio from HTTP URLs (#15741)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 15:53:52 -05:00
luar123 f757cd1210 [zigbee][core] Add support for Zigbee binary sensors on ESP32 H2 and C6 (#11553)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-23 12:46:56 -04:00
Paulus Schoutsen 9b45b046a8 [core] Allow finding all devices as target that match mac suffix (#13135) 2026-04-23 08:43:32 -05:00
J. Nick Koston 70ae614abd [api] Fall back to plaintext for logger connections (#15938) 2026-04-23 08:23:38 -05:00
J. Nick Koston 8f9b91eece [wifi] Avoid BDK 3.0.78 wifi_event_sta_disconnected_t collision on BK72xx (#15942) 2026-04-23 08:22:17 -05:00
J. Nick Koston 3ca86fc3fc [core] Raise WDT_FEED_INTERVAL_MS to 2000ms on BK72xx (#15943) 2026-04-23 08:21:46 -05:00
J. Nick Koston b38db617a2 [core] Clean up stale includes and inline yield_with_select_ in application (#15945) 2026-04-23 08:21:05 -05:00
J. Nick Koston 13fe881f70 [scheduler][core] Lock-free fast-path on ESPHOME_THREAD_MULTI_NO_ATOMICS via __atomic builtins (#15947) 2026-04-23 08:20:31 -05:00
J. Nick Koston 50c181671c [ci] Better explain too-big bot review message (#15939) 2026-04-23 06:47:16 -05:00
PolarGoose 43a371caab [dsmr] Small refactoring: Move Aes128GcmDecryptorImpl type inside esphome::dsmr namespace. (#15940) 2026-04-23 04:08:49 -05:00
dependabot[bot] 64290d32a1 Bump aioesphomeapi from 44.20.0 to 44.21.0 (#15941)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 03:32:12 -05:00
J. Nick Koston 9685d4eb0b [core] feed_wdt wraps feed_wdt_with_time (#15932) 2026-04-23 01:15:44 -05:00
Keith Burzinski 4c2efd4165 [radio_frequency] Add experimental radio_frequency entity type (base component + API) (#15556) 2026-04-23 01:15:25 -05:00
J. Nick Koston 6f00ea1457 [core] Move host socket-select wake mechanism into wake.h/wake.cpp (#15931) 2026-04-23 15:53:10 +12:00
Jonathan Swoboda a881121110 [ota] Make set_auth_password() lambda-callable via empty-password opt-in (#15928) 2026-04-22 23:06:31 -04:00
dependabot[bot] f8167c9a70 Bump aioesphomeapi from 44.19.0 to 44.20.0 (#15936)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 02:40:19 +00:00
Jesse Hills e1d629f0d2 [time] Handle Windows EINVAL when validating POSIX TZ strings (#15934) 2026-04-23 14:35:13 +12:00
Clyde Stubbs 224cc7b419 [lvgl] Triggers on tabview tabs fix (#15935) 2026-04-23 14:35:00 +12:00
Jesse Hills 4d4347d33a Merge branch 'release' into dev 2026-04-23 14:10:54 +12:00
Jesse Hills 6ca5b31fab Merge pull request #15933 from esphome/bump-2026.4.2
2026.4.2
2026-04-23 14:10:10 +12:00
dependabot[bot] 17f9269841 Update wheel requirement from <0.47,>=0.43 to >=0.43,<0.48 (#15926)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 19:12:15 -05:00
dependabot[bot] 6253947311 Bump click from 8.3.2 to 8.3.3 (#15927)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 19:12:02 -05:00
Jesse Hills 00b71208a6 Bump version to 2026.4.2 2026-04-23 11:18:39 +12:00
Keith Burzinski 76eb8f697f [usb_uart] Derive TX output chunk count from buffer_size config (#15909) 2026-04-23 11:18:39 +12:00
Jonathan Swoboda 2a3bd8bc85 [io_expanders] Self-heal interrupt-driven expanders when INT stays asserted across the read (#15923) 2026-04-23 11:18:39 +12:00
Keith Burzinski 629da4d878 [esp32] Add Secure Boot V1 ECDSA signing scheme for pre-rev-3.0 ESP32 (#15882) 2026-04-23 11:18:39 +12:00
Jonathan Swoboda 5c2ceb63e0 [ld2412] Fix null deref in set_basic_config when entities unconfigured (#15893) 2026-04-23 11:18:39 +12:00
Jonathan Swoboda 92cb6dd7fd [core] Fix Pvariable placement new losing subclass identity (#15881) 2026-04-23 11:18:39 +12:00
Jonathan Swoboda 06e5931ad7 [image] Fix rodata bloat for multi-frame RGB565+alpha animations (#15873) 2026-04-23 11:18:39 +12:00
Clyde Stubbs dc5b06285d [lvgl] Fix update of textarea attached to keyboard (#15866) 2026-04-23 11:18:38 +12:00
Clyde Stubbs 3d0a2421a6 [lvgl] Fix overloads for setting images on styles (#15864) 2026-04-23 11:18:38 +12:00
Clyde Stubbs 22f6791dea [lvgl] Fix format of hello world page (#15868) 2026-04-23 11:18:38 +12:00
Keith Burzinski 70b1d9a087 [api_protobuf] Support compound ifdef conditions in proto generator (#15930) 2026-04-22 17:57:15 -05:00
Keith Burzinski 36720c8495 [usb_uart] Derive TX output chunk count from buffer_size config (#15909) 2026-04-23 09:16:14 +12:00
Jonathan Swoboda c48ab2ef92 [io_expanders] Self-heal interrupt-driven expanders when INT stays asserted across the read (#15923) 2026-04-23 09:05:15 +12:00
Keith Burzinski 162ee2ecaf [i2s_audio] Split speaker into base class and standard subclass (#15404) 2026-04-22 14:40:18 -05:00
Asela Fernando a73bac0b5f [ac_dimmer] Zero-crossing interrupt type (#15862)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-04-22 14:57:53 -04:00
Rishab Mehta 4e84611ae7 [internal_temperature] Fix internal Temperature discrepancy on BK7231T (#15771)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-04-22 17:50:59 +00:00
PolarGoose ea2e36e55a [dsmr] Improve performance. Add missing sensors. Remove Crypto-no-arduino. (#15875) 2026-04-22 13:49:14 -04:00
Michael Turner fcbc4d64fe [one_wire] Reset bus before SKIP ROM command (#14669)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-04-22 13:20:02 -04:00
Timothy dcd103cec0 [cse7761] bidirectional active power (#15162)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-04-22 13:11:18 -04:00
Ludovic BOUÉ 5e715692d6 [network] Reorder IPv6 configuration for network components (#11694)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-04-22 17:01:20 +00:00
rwrozelle d5263cd46e [esp32] add watchdog_timeout configuration variable (#15908)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-22 13:01:23 +00:00
J. Nick Koston c399cd2fa2 [core] RAII guard for component loop phase (#15897) 2026-04-22 14:04:29 +02:00
J. Nick Koston f6bf6dc8e5 [core] Dedupe yield() fast path in wakeable_delay and always-inline (#15915) 2026-04-22 13:52:40 +02:00
J. Nick Koston e35b435f02 [libretiny] Inline xTaskGetTickCount() for millis() fast path (#15918) 2026-04-22 13:52:27 +02:00
J. Nick Koston 886cd7ab72 [core] Collapse adjacent USE_HOST ifdef blocks in Application (#15914) 2026-04-22 07:47:01 -04:00
dependabot[bot] 73714dc489 Bump aioesphomeapi from 44.18.0 to 44.19.0 (#15920)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 12:26:25 +02:00
dependabot[bot] 5218bbd791 Update argcomplete requirement from >=2.0.0 to >=3.6.3 (#15921)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 12:19:47 +02:00
J. Nick Koston 23ad30cb4c [esp32] Use xTaskGetTickCount() for millis() when tick rate is 1kHz (#15661) 2026-04-22 06:44:53 +02:00
J. Nick Koston a3b49d1ed9 [core] Use MAC_ADDRESS_BUFFER_SIZE constant instead of duplicated literal (#15913) 2026-04-22 04:43:33 +00:00
J. Nick Koston 9c80cbf19c [light] Reduce validate_ clamp code size and speed up unit-range clamps (#15728) 2026-04-22 16:34:26 +12:00
J. Nick Koston 699cf9690a [core] Optimize value_accuracy_to_buf to avoid snprintf (#15596) 2026-04-22 16:31:34 +12:00
J. Nick Koston 67576d4879 [rp2040] Tune oversized lwIP defaults for ESPHome (#14843) 2026-04-22 06:29:13 +02:00
J. Nick Koston edcf96d057 [wifi] Use queue abstraction for LibreTiny WiFi events (#15343) 2026-04-22 06:24:09 +02:00
dependabot[bot] bb81c91d0c Update tzdata requirement from >=2021.1 to >=2026.1 (#15911)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 03:08:58 +00:00
dependabot[bot] 78f1467be4 Bump aioesphomeapi from 44.17.0 to 44.18.0 (#15912)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 03:08:42 +00:00
dependabot[bot] da44d43981 Update pyparsing requirement from >=3.0 to >=3.3.2 (#15910)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-22 05:07:48 +02:00
Javier Peletier 9cebce1b6e [substitutions] Improve error messages with include stack trace (#15874)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-22 03:19:01 +02:00
Jesse Hills b20fedd806 [bl0906] Disable loop when idle and introduce BL0906Stage enum (#15884)
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-22 03:18:21 +02:00
Keith Burzinski ee91ad8f06 [esp32] Add Secure Boot V1 ECDSA signing scheme for pre-rev-3.0 ESP32 (#15882) 2026-04-22 11:25:05 +12:00
dependabot[bot] 7560112144 Bump aioesphomeapi from 44.16.1 to 44.17.0 (#15906)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 22:08:41 +02:00
Geoff 43c6b839cd [sensor] Filter to round to significant digits (#11157)
Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-04-21 14:00:03 +00:00
Edward Firmo 0c9d443a5c [esp32_ble] Add use_psram option to offload BT memory allocation to SPIRAM (#15644) 2026-04-21 15:18:46 +02:00
J. Nick Koston 14defb69b6 [template] Use placement new for template text restore saver (#15883) 2026-04-21 13:04:13 +00:00
Egor Vorontsov 3a6f3dfb94 [lock] Implemented open states support (#15120)
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-21 13:03:07 +00:00
J. Nick Koston 7bd36e0c8d [debug] Migrate trivial buf_append_printf sites to buf_append_str (#15885) 2026-04-21 12:51:45 +00:00
J. Nick Koston e4f413adad [core] decouple main loop cadence from scheduler wake timing (#15792) 2026-04-21 14:48:21 +02:00
J. Nick Koston 1504ac3d19 [core] Replace strnlen in buf_append_str for Zephyr compatibility (#15892) 2026-04-21 12:32:29 +00:00
J. Nick Koston 947c714f89 [core] Inline api_is_connected() for hot-path callers (#15888) 2026-04-21 13:48:33 +02:00
J. Nick Koston e4d5886383 [zwave_proxy] Inline loop() hot-path fast-paths for response_handler_ and process_uart_ (#15887) 2026-04-21 13:48:16 +02:00
J. Nick Koston f504099485 [api] Replace clients_ std::vector with compile-time std::array + uint8_t count (#15889) 2026-04-21 13:47:37 +02:00
Jonathan Swoboda cb56f9a9bf [qmc5883l] Use GPIO interrupt when DRDY pin is configured (#15876) 2026-04-21 07:47:16 -04:00
Jonathan Swoboda 26a656af29 [ld2412] Fix null deref in set_basic_config when entities unconfigured (#15893) 2026-04-21 07:47:07 -04:00
dependabot[bot] a8bd035b62 Bump CodSpeedHQ/action from 4.13.1 to 4.14.0 (#15880)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-21 04:44:25 +02:00
J. Nick Koston f05fa45747 [sensor] Specialize throttle_with_priority NaN-only case (#15823) 2026-04-21 04:41:13 +02:00
J. Nick Koston 78875abee4 [core] Make buf_append_str PROGMEM-aware on ESP8266 (#15738)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-21 04:40:40 +02:00
J. Nick Koston 37608c2656 [ltr390] Reduce data polling delay and timeout (#15507) 2026-04-21 04:40:24 +02:00
J. Nick Koston a5b1f3eece [core] Remove pre-sleep socket scan from fast select path (#15639) 2026-04-21 04:40:03 +02:00
J. Nick Koston 0d3a3552da [core] Move heap-allocating helpers to alloc_helpers.h/cpp (#15623) 2026-04-21 04:39:49 +02:00
J. Nick Koston 0a0176d600 [core] raise WDT_FEED_INTERVAL_MS from 3 ms to 300 ms (#15846) 2026-04-21 04:38:12 +02:00
J. Nick Koston 4cb7ea2584 [light] Force-inline LightCall::set_flag_/clear_flag_ (#15729) 2026-04-21 04:37:56 +02:00
Jonathan Swoboda a43ee15b56 [core] Fix Pvariable placement new losing subclass identity (#15881) 2026-04-20 22:33:48 -04:00
Jonathan Swoboda 213ab312d2 [image] Fix rodata bloat for multi-frame RGB565+alpha animations (#15873) 2026-04-20 16:27:34 -04:00
Kevin Ahrendt 94f30d5950 [micro_wake_word] Use ESPMicroSpeechFeatures from Espressif registry (v1.2.3) (#15879) 2026-04-20 16:26:47 -04:00
Elvin Luff 6af341bb5b [epaper_spi] Support SSD1683 and GDEY042T81 4.2 inch display (#13910) 2026-04-20 09:34:31 -04:00
Clyde Stubbs 82656cb0cf [mipi_dsi] Add Seeed reTerminal d1001 display (#15867) 2026-04-20 09:28:52 -04:00
Rui Marinho b72f5447c3 [modbus] Simplify payload size validation in modbus_helpers (#15838) 2026-04-20 09:24:07 -04:00
Clyde Stubbs 73b8e8ac09 [lvgl] Fix update of textarea attached to keyboard (#15866) 2026-04-20 09:15:51 -04:00
Clyde Stubbs 9459f0426d [lvgl] Fix overloads for setting images on styles (#15864) 2026-04-20 09:14:15 -04:00
Clyde Stubbs 0dae41aa22 [lvgl] Fix format of hello world page (#15868) 2026-04-20 09:13:42 -04:00
Thomas Rupprecht 7321e6e52f [rtttl] allow any control parameters order and default value fallback (#14438)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-04-20 09:10:05 -04:00
guillempages f0c21520aa [mipi_rgb] Add definitions for sunton displays (#15858) 2026-04-20 21:56:56 +10:00
Jesse Hills b0c133201f Merge branch 'release' into dev 2026-04-20 13:53:30 +12:00
Jesse Hills 572fb83015 Merge pull request #15859 from esphome/bump-2026.4.1
2026.4.1
2026-04-20 13:52:45 +12:00
Clyde Stubbs 0d3db2b670 [lvgl] Fix angles for arc (#15860) 2026-04-20 12:08:35 +12:00
Clyde Stubbs e5f6a734ba [lvgl] Fix angles for arc (#15860) 2026-04-20 12:08:07 +12:00
J. Nick Koston bab9cd3e7a [runtime_stats] Track main loop active time and report overhead (#15743) 2026-04-20 11:20:39 +12:00
Jesse Hills 36812591eb Bump version to 2026.4.1 2026-04-20 10:20:56 +12:00
Javier Peletier 1862c6115f [packages] Improve error messages with include stack and fix missing path propagation (#15844)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-04-20 10:20:56 +12:00
J. Nick Koston ef780886c3 [substitutions] Fix substitutions: !include file.yaml regression (#15850) 2026-04-20 10:20:56 +12:00
J. Nick Koston 602305b20d [core] Default PollingComponent() to 1ms when codegen is bypassed (#15831) 2026-04-20 10:20:56 +12:00
dependabot[bot] 78701debec Bump aioesphomeapi from 44.16.0 to 44.16.1 (#15836)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 10:20:56 +12:00
J. Nick Koston 08ac61ae94 [core] Feed WDT unconditionally in main loop to fix empty-config panic (#15830) 2026-04-20 10:20:16 +12:00
Clyde Stubbs 6d5340f253 [lvgl] Fix crash with snow on rotated display (#15822) 2026-04-20 10:18:05 +12:00
Clyde Stubbs e2dfef5ddc [runtime_image] Fix RGB order (#15813) 2026-04-20 10:18:05 +12:00
Jonathan Swoboda 1d88027618 [esp32] Downgrade unneeded ignore_pin_validation_error to a warning (#15811) 2026-04-20 10:18:05 +12:00
J. Nick Koston 9841deec31 [core] Fix DelayAction compile error with non-const reference args (#15814) 2026-04-20 10:18:05 +12:00
Jonathan Swoboda ed5852c2d6 [ethernet] Fix SPI3_HOST default breaking compile on variants without SPI3 (#15809)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-20 10:18:05 +12:00
J. Nick Koston b26601a3dc [core] coerce set_interval(0) / update_interval: 0ms to 1ms (#15799) 2026-04-20 10:18:05 +12:00
Clyde Stubbs f5806818cd [image] Fix byte order handling (#15800) 2026-04-20 10:18:05 +12:00
Clyde Stubbs c3e739eba9 [mipi_spi] Drawing fixes for native display (#15802) 2026-04-20 10:18:05 +12:00
Jonathan Swoboda b167b64f06 [lvgl] Guard lv_image_set_src wrapper with LV_USE_IMAGE (#15789) 2026-04-20 10:18:05 +12:00
Jonathan Swoboda 722cfae04c [esp32] Accept unquoted minimum_chip_revision values (#15785) 2026-04-20 10:18:05 +12:00
J. Nick Koston 9cb2b562b9 [ili9xxx] Guard against null buffer in display_() when allocation fails (#15786) 2026-04-20 10:18:05 +12:00
J. Nick Koston 81fb6712fe [bundle] Force-resolve nested IncludeFile during file discovery (#15762) 2026-04-20 10:18:05 +12:00
Jonathan Swoboda 227dfa3730 [qmc5883l] Move per-update log line from DEBUG to VERBOSE (#15781) 2026-04-20 10:18:05 +12:00
J. Nick Koston aa80bdbbc6 [time] Fix RTC is_valid() rejecting valid times after day_of_year cleanup (#15763) 2026-04-20 10:18:05 +12:00
J. Nick Koston 914ed10bcc [core] Diagnose missing cg.templatable in codegen for TEMPLATABLE_VALUE fields (#15758) 2026-04-20 10:18:05 +12:00
Boris Krivonog 92c99a7d41 [mitsubishi_cn105] use HEAT_COOL mode to enable temperature slider (#15748) 2026-04-20 10:18:05 +12:00
Clyde Stubbs af1aaba547 [lvgl] Clean the build if lv_conf.h changes (#15777) 2026-04-20 10:18:05 +12:00
dependabot[bot] 5a2b7546f6 Bump aioesphomeapi from 44.15.0 to 44.16.0 (#15757)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-20 10:18:05 +12:00
Jonathan Swoboda 4047d5af5f [sx126x][sx127x] Fix frequency precision loss from float32 codegen (#15753) 2026-04-20 10:18:05 +12:00
Jonathan Swoboda 6857e1ceb4 [st7789v] Fix swapped offset_width/offset_height in model presets (#15755) 2026-04-20 10:18:04 +12:00
J. Nick Koston 4479212008 [core] Inline feed_wdt hot path with out-of-line slow path (#15656)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-20 10:18:04 +12:00
J. Nick Koston cb90ac45c3 [core] Fix app_state_ status bits clobbered for non-looping components (#15658) 2026-04-20 10:18:04 +12:00
J. Nick Koston 1847666e75 [core] Default PollingComponent() to not run when codegen is bypassed (#15832) 2026-04-19 17:05:27 -05:00
Javier Peletier aad1318b4a [packages] Improve error messages with include stack and fix missing path propagation (#15844)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-04-19 21:09:14 +00:00
J. Nick Koston 7a23a339e9 [substitutions] Fix substitutions: !include file.yaml regression (#15850) 2026-04-20 09:00:31 +12:00
J. Nick Koston 38d894dfe7 [ld2412] Fix flaky integration test race condition (#15833) 2026-04-18 08:17:22 -05:00
J. Nick Koston b293be23b0 [ci] Honor CONFLICTS_WITH when grouping component tests (#15834)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-04-18 08:11:38 -05:00
J. Nick Koston ccb53e34ca [core] Default PollingComponent() to 1ms when codegen is bypassed (#15831) 2026-04-18 09:04:51 -04:00
dependabot[bot] ec9d59f3dc Bump aioesphomeapi from 44.16.0 to 44.16.1 (#15836)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-18 12:32:36 +00:00
J. Nick Koston df72aa26c0 [core] Feed WDT unconditionally in main loop to fix empty-config panic (#15830) 2026-04-18 11:58:54 +00:00
Clyde Stubbs d3691c7ca5 [lvgl] Fix crash with snow on rotated display (#15822) 2026-04-18 09:17:28 +10:00
J. Nick Koston 562ce541a0 [bme680_bsec] [bme68x_bsec2] Mark the two BSEC variants as mutually exclusive (#15826) 2026-04-17 17:54:24 -05:00
J. Nick Koston 6ebe1e92eb [ci] Scope local pylint pre-commit hook to esphome/ (#15818) 2026-04-17 17:54:12 -05:00
Clyde Stubbs 1bf455cfbb [runtime_image] Fix RGB order (#15813) 2026-04-18 06:42:45 +10:00
Clyde Stubbs 290e213cd0 [mipi_spi] Add Sunton ESP32-2424S012 (#15812) 2026-04-18 06:41:33 +10:00
Jonathan Swoboda b1b0005574 [esp32] Downgrade unneeded ignore_pin_validation_error to a warning (#15811) 2026-04-17 16:14:54 -04:00
353 changed files with 13681 additions and 3661 deletions
+1 -1
View File
@@ -1 +1 @@
075ed2142432dc59883bb52db8ac11270f952851d6400deae080f5468c7cb592
1b1ce6324c50c4595703c7df0a8a479b4fe84b71ff1a8793cce1a16f17a33324
+25 -5
View File
@@ -41,16 +41,36 @@ function generateReviewMessages(finalLabels, originalLabelCount, deprecatedInfo,
let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
message +=
`Hey @${prAuthor}, thanks for the contribution! Just a heads up, ` +
`this PR is on the large side `;
if (tooManyLabels && tooManyChanges) {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
message +=
`(${nonTestChanges} line changes excluding tests, across ` +
`${originalLabelCount} different components/areas)`;
} else if (tooManyLabels) {
message += `This PR affects ${originalLabelCount} different components/areas.`;
message +=
`(it touches ${originalLabelCount} different components/areas)`;
} else {
message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
message += `(${nonTestChanges} line changes excluding tests)`;
}
message += ` Please consider breaking it down into smaller, focused PRs to make review easier and reduce the risk of conflicts.\n\n`;
message += `For guidance on breaking down large PRs, see: https://developers.esphome.io/contributing/submitting-your-work/#how-to-approach-large-submissions`;
message += `, which makes it harder for maintainers to review.\n\n`;
message +=
`Smaller, focused PRs tend to be reviewed much faster since they ` +
`fit into the short gaps between other maintainer work; large ones ` +
`often have to wait for a rare long uninterrupted block of time. ` +
`If you can break this up into smaller pieces that can be reviewed ` +
`independently, it will almost certainly land faster overall.\n\n`;
message +=
`Before putting more time in, it's also worth popping into ` +
`\`#devs\` on [Discord](https://esphome.io/chat) so we can help ` +
`you scope things and flag anything already in flight.\n\n`;
message +=
`For more details (including how to split the work up), see: ` +
`https://developers.esphome.io/contributing/submitting-your-work/` +
`#how-to-approach-large-submissions`;
messages.push(message);
}
+1 -1
View File
@@ -339,7 +339,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@db35df748deb45fdef0960669f57d627c1956c30 # v4
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation
@@ -0,0 +1,72 @@
name: Close PR From Fork Default Branch
on:
# pull_request_target is required so we have permission to comment and close PRs from forks.
pull_request_target:
types: [opened, reopened]
permissions:
pull-requests: write
issues: write
jobs:
close:
name: Close PR opened from fork's default branch
runs-on: ubuntu-latest
if: >-
github.event.pull_request.head.repo.full_name != github.event.pull_request.base.repo.full_name
&& github.event.pull_request.head.ref == github.event.repository.default_branch
steps:
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
script: |
const { owner, repo } = context.repo;
const prNumber = context.payload.pull_request.number;
const author = context.payload.pull_request.user.login;
const defaultBranch = context.payload.repository.default_branch;
const headRepo = context.payload.pull_request.head.repo.full_name;
const body = [
`Hi @${author}, thanks for opening a pull request! :tada:`,
``,
`It looks like this PR was opened from the \`${defaultBranch}\` branch of your fork (\`${headRepo}\`), which is the same name as this repository's default branch. Working directly on \`${defaultBranch}\` in your fork causes a few problems:`,
``,
`- Your fork's \`${defaultBranch}\` branch will permanently diverge from \`esphome/esphome:${defaultBranch}\`, making it hard to keep your fork up to date.`,
`- Any additional commits you push to \`${defaultBranch}\` will be added to this PR, so you can't easily work on multiple changes at once.`,
`- Pushing maintainer fixes to your branch is awkward, since it means committing directly to your fork's default branch.`,
`- It makes local collaboration painful — \`${defaultBranch}\` in a checkout becomes ambiguous between upstream and your fork, and maintainers end up with naming collisions when fetching your branch.`,
``,
`Please re-open this as a new PR from a dedicated feature branch. The usual flow looks like:`,
``,
`\`\`\`bash`,
`# Make sure your fork's ${defaultBranch} is up to date with upstream`,
`git remote add upstream https://github.com/${owner}/${repo}.git # if you haven't already`,
`git fetch upstream`,
`git checkout ${defaultBranch}`,
`git reset --hard upstream/${defaultBranch}`,
`git push --force-with-lease origin ${defaultBranch}`,
``,
`# Create a new branch for your change and cherry-pick / re-apply your commits there`,
`git checkout -b my-feature-branch upstream/${defaultBranch}`,
`# ...re-apply your changes, then:`,
`git push origin my-feature-branch`,
`\`\`\``,
``,
`Then open a new pull request from \`my-feature-branch\` into \`${owner}/${repo}:${defaultBranch}\`.`,
``,
`Closing this PR for now — sorry for the friction, and thanks again for contributing! :heart:`,
].join('\n');
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
await github.rest.pulls.update({
owner,
repo,
pull_number: prNumber,
state: 'closed',
});
+2 -1
View File
@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.15.11
rev: v0.15.12
hooks:
# Run the linter.
- id: ruff
@@ -58,6 +58,7 @@ repos:
entry: python3 script/run-in-env.py pylint
language: system
types: [python]
files: ^esphome/.+\.py$
- id: clang-tidy-hash
name: Update clang-tidy hash
entry: python script/clang_tidy_hash.py --update-if-changed
+8 -1
View File
@@ -56,6 +56,7 @@ esphome/components/audio_adc/* @kbx81
esphome/components/audio_dac/* @kbx81
esphome/components/audio_file/* @kahrendt
esphome/components/audio_file/media_source/* @kahrendt
esphome/components/audio_http/* @kahrendt
esphome/components/axs15231/* @clydebarrow
esphome/components/b_parasite/* @rbaron
esphome/components/ballu/* @bazuchan
@@ -403,6 +404,7 @@ esphome/components/qmp6988/* @andrewpc
esphome/components/qr_code/* @wjtje
esphome/components/qspi_dbi/* @clydebarrow
esphome/components/qwiic_pir/* @kahrendt
esphome/components/radio_frequency/* @kbx81
esphome/components/radon_eye_ble/* @jeffeb3
esphome/components/radon_eye_rd200/* @jeffeb3
esphome/components/rc522/* @glmnet
@@ -438,6 +440,11 @@ esphome/components/sen0321/* @notjj
esphome/components/sen21231/* @shreyaskarnik
esphome/components/sen5x/* @martgras
esphome/components/sen6x/* @martgras @mebner86 @mikelawrence @tuct
esphome/components/sendspin/* @kahrendt
esphome/components/sendspin/media_player/* @kahrendt
esphome/components/sendspin/media_source/* @kahrendt
esphome/components/sendspin/sensor/* @kahrendt
esphome/components/sendspin/text_sensor/* @kahrendt
esphome/components/sensirion_common/* @martgras
esphome/components/sensor/* @esphome/core
esphome/components/serial_proxy/* @kbx81
@@ -599,6 +606,6 @@ esphome/components/xxtea/* @clydebarrow
esphome/components/zephyr/* @tomaszduda23
esphome/components/zephyr_mcumgr/ota/* @tomaszduda23
esphome/components/zhlt01/* @cfeenstra1024
esphome/components/zigbee/* @tomaszduda23
esphome/components/zigbee/* @luar123 @tomaszduda23
esphome/components/zio_ultrasonic/* @kahrendt
esphome/components/zwave_proxy/* @kbx81
+1
View File
@@ -4,4 +4,5 @@ include requirements.txt
recursive-include esphome *.yaml
recursive-include esphome *.cpp *.h *.tcc *.c
recursive-include esphome *.py.script
recursive-include esphome *.jinja
recursive-include esphome LICENSE.txt
+108 -13
View File
@@ -39,6 +39,7 @@ from esphome.const import (
CONF_MDNS,
CONF_MQTT,
CONF_NAME,
CONF_NAME_ADD_MAC_SUFFIX,
CONF_OTA,
CONF_PASSWORD,
CONF_PLATFORM,
@@ -71,6 +72,7 @@ from esphome.util import (
run_external_process,
safe_print,
)
from esphome.zeroconf import discover_mdns_devices
_LOGGER = logging.getLogger(__name__)
@@ -204,6 +206,64 @@ def _resolve_with_cache(address: str, purpose: Purpose) -> list[str]:
return [address]
def _populate_mdns_cache(hosts_to_addresses: dict[str, list[str]]) -> None:
"""Store discovered ``host -> [ips]`` entries in ``CORE.address_cache``.
Ensures ``CORE.address_cache`` exists, then records each mDNS hostname so
the downstream resolution path (``resolve_ip_address``) can skip opening a
second Zeroconf client.
"""
from esphome.address_cache import AddressCache
if CORE.address_cache is None:
CORE.address_cache = AddressCache()
for host, addresses in hosts_to_addresses.items():
if addresses:
_LOGGER.debug("Caching mDNS result %s -> %s", host, addresses)
CORE.address_cache.add_mdns_addresses(host, addresses)
def _discover_mac_suffix_devices() -> list[str] | None:
"""Discover ``<name>-<mac>.local`` devices and cache their IPs.
Returns:
- ``None`` when discovery isn't applicable (``name_add_mac_suffix`` off,
mDNS disabled, or ``CORE.address`` is already an IP). Callers should
then fall back to whatever default OTA address they normally use.
- ``[]`` when discovery ran but found nothing. Callers should NOT fall
back to the base name: with ``name_add_mac_suffix`` enabled, the base
name by definition doesn't exist on the network.
- A non-empty sorted list of ``.local`` hostnames on success.
Populates ``CORE.address_cache`` so downstream resolution (``espota2`` or
``aioesphomeapi`` via :func:`_resolve_network_devices`) reuses the IPs we
already have without opening a second Zeroconf client.
"""
if not (has_name_add_mac_suffix() and has_mdns() and has_non_ip_address()):
return None
_LOGGER.info("Discovering devices...")
if not (discovered := discover_mdns_devices(CORE.name)):
_LOGGER.warning(
"No devices matching '%s-<mac>.local' were discovered.", CORE.name
)
return []
_populate_mdns_cache(discovered)
return list(discovered)
def _ota_hostnames_for_default(purpose: Purpose) -> list[str]:
"""Return OTA hostname(s) for the ``--device OTA`` / default-resolve path.
When ``name_add_mac_suffix`` is enabled, returns discovered
``<name>-<mac>.local`` hostnames (possibly empty — in which case the
caller should not fall back to the base name). Otherwise falls back to
the cache-resolved ``CORE.address``.
"""
if (discovered := _discover_mac_suffix_devices()) is not None:
return discovered
return _resolve_with_cache(CORE.address, purpose)
def choose_upload_log_host(
default: list[str] | str | None,
check_default: str | None,
@@ -242,14 +302,14 @@ def choose_upload_log_host(
resolved.append("MQTT")
if has_api() and has_non_ip_address() and has_resolvable_address():
resolved.extend(_resolve_with_cache(CORE.address, purpose))
resolved.extend(_ota_hostnames_for_default(purpose))
elif purpose == Purpose.UPLOADING:
if has_ota() and has_mqtt_ip_lookup():
resolved.append("MQTTIP")
if has_ota() and has_non_ip_address() and has_resolvable_address():
resolved.extend(_resolve_with_cache(CORE.address, purpose))
resolved.extend(_ota_hostnames_for_default(purpose))
else:
resolved.append(device)
if not resolved:
@@ -281,22 +341,29 @@ def choose_upload_log_host(
elif bootsel.permission_error:
bootsel_permission_error = True
def add_ota_options() -> None:
"""Add OTA options, using mDNS discovery if name_add_mac_suffix is enabled."""
if (discovered := _discover_mac_suffix_devices()) is not None:
# Discovery was applicable. Use whatever we found — on empty,
# intentionally skip the base-name fallback since with
# name_add_mac_suffix on, the base name doesn't exist on the net.
for host in discovered:
options.append((f"Over The Air ({host})", host))
elif has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
if purpose == Purpose.LOGGING:
if has_mqtt_logging():
mqtt_config = CORE.config[CONF_MQTT]
options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
if has_api():
if has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
add_ota_options()
elif purpose == Purpose.UPLOADING and has_ota():
if has_resolvable_address():
options.append((f"Over The Air ({CORE.address})", CORE.address))
if has_mqtt_ip_lookup():
options.append(("Over The Air (MQTT IP lookup)", "MQTTIP"))
add_ota_options()
# Show helpful BOOTSEL instructions for RP2040 when no BOOTSEL device is found
if (
@@ -407,7 +474,17 @@ def has_resolvable_address() -> bool:
return not CORE.address.endswith(".local")
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
def has_name_add_mac_suffix() -> bool:
"""Check if name_add_mac_suffix is enabled in the config."""
if CORE.config is None:
return False
esphome_config = CORE.config.get(CONF_ESPHOME, {})
return esphome_config.get(CONF_NAME_ADD_MAC_SUFFIX, False)
def mqtt_get_ip(
config: ConfigType, username: str, password: str, client_id: str
) -> list[str]:
from esphome import mqtt
return mqtt.get_esphome_device_ip(config, username, password, client_id)
@@ -420,6 +497,9 @@ def _resolve_network_devices(
This function filters the devices list to:
- Replace MQTT/MQTTIP magic strings with actual IP addresses via MQTT lookup
- Expand hostnames that are already in ``CORE.address_cache`` to their
cached IPs so downstream code (e.g. aioesphomeapi) doesn't open a second
Zeroconf client to resolve them
- Deduplicate addresses while preserving order
- Only resolve MQTT once even if multiple MQTT strings are present
- If MQTT resolution fails, log a warning and continue with other devices
@@ -444,13 +524,29 @@ def _resolve_network_devices(
mqtt_ips = mqtt_get_ip(
config, args.username, args.password, args.client_id
)
network_devices.extend(mqtt_ips)
# pylint can't infer mqtt_get_ip's return through its
# lazy ``from esphome import mqtt`` import, so it flags
# the genexpr below.
network_devices.extend(
addr
for addr in mqtt_ips # pylint: disable=not-an-iterable
if addr not in network_devices
)
except EsphomeError as err:
_LOGGER.warning(
"MQTT IP discovery failed (%s), will try other devices if available",
err,
)
mqtt_resolved = True
continue
# If the hostname is already in the address cache (e.g. populated by
# mDNS discovery), substitute the cached IPs so aioesphomeapi doesn't
# open its own Zeroconf to re-resolve it.
if CORE.address_cache and (cached := CORE.address_cache.get_addresses(device)):
network_devices.extend(
addr for addr in cached if addr not in network_devices
)
elif device not in network_devices:
# Regular network address or IP - add if not already present
network_devices.append(device)
@@ -569,7 +665,6 @@ def wrap_to_code(name, comp):
@functools.wraps(comp.to_code)
async def wrapped(conf):
cg.add(cg.ComponentMarker(name))
cg.add(cg.LineComment(f"{name}:"))
if comp.config_schema is not None:
conf_str = yaml_util.dump(conf)
+11
View File
@@ -101,6 +101,17 @@ class AddressCache:
"""Check if any cache entries exist."""
return bool(self.mdns_cache or self.dns_cache)
def add_mdns_addresses(self, hostname: str, addresses: list[str]) -> None:
"""Store resolved mDNS addresses for ``hostname`` in the cache.
Callers that discover ``.local`` hosts (e.g. via mDNS browse) can use
this to avoid a second resolution round-trip during the upload path.
No-op when ``addresses`` is empty.
"""
if not addresses:
return
self.mdns_cache[normalize_hostname(hostname)] = addresses
@classmethod
def from_cli_args(
cls, mdns_args: Iterable[str], dns_args: Iterable[str]
+56
View File
@@ -0,0 +1,56 @@
"""Helpers for running an async coroutine from sync code via a daemon thread.
``asyncio.run(coro())`` in the main thread blocks until the loop's cleanup
cycle finishes, which can add hundreds of milliseconds before the caller
receives the result. Running the loop in a daemon thread lets the caller
observe the result as soon as the coroutine completes while cleanup finishes
in the background.
"""
from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable
import threading
from typing import Generic, TypeVar
_T = TypeVar("_T")
class AsyncThreadRunner(threading.Thread, Generic[_T]):
"""Run an async coroutine in a daemon thread and expose its result.
The runner catches all exceptions from the coroutine and stores them in
``exception`` so ``event`` is always set — this prevents callers waiting
on ``event`` from hanging forever when the coroutine crashes.
Typical usage::
runner = AsyncThreadRunner(lambda: my_coro(arg))
runner.start()
if not runner.event.wait(timeout=5.0):
... # timed out
if runner.exception is not None:
raise runner.exception
result = runner.result
"""
def __init__(self, coro_factory: Callable[[], Awaitable[_T]]) -> None:
super().__init__(daemon=True)
self._coro_factory = coro_factory
self.result: _T | None = None
self.exception: BaseException | None = None
self.event = threading.Event()
async def _runner(self) -> None:
try:
self.result = await self._coro_factory()
except Exception as exc: # pylint: disable=broad-except
# Capture all exceptions so ``event`` is always set — otherwise a
# crash would hang the waiter forever.
self.exception = exc
finally:
self.event.set()
def run(self) -> None:
asyncio.run(self._runner())
-2
View File
@@ -10,10 +10,8 @@
# pylint: disable=unused-import
from esphome.cpp_generator import ( # noqa: F401
ArrayInitializer,
ComponentMarker,
Expression,
FlashStringLiteral,
IIFEUnsafeStatement,
LineComment,
LogStringLiteral,
MockObj,
+15 -9
View File
@@ -190,7 +190,7 @@ void AcDimmer::setup() {
this->zero_cross_pin_->setup();
this->store_.zero_cross_pin = this->zero_cross_pin_->to_isr();
this->zero_cross_pin_->attach_interrupt(&AcDimmerDataStore::s_gpio_intr, &this->store_,
gpio::INTERRUPT_FALLING_EDGE);
this->zero_cross_interrupt_type_);
}
#ifdef USE_ESP8266
@@ -226,19 +226,25 @@ void AcDimmer::write_state(float state) {
void AcDimmer::dump_config() {
ESP_LOGCONFIG(TAG,
"AcDimmer:\n"
" Min Power: %.1f%%\n"
" Init with half cycle: %s",
" Min Power: %.1f%%\n"
" Init with half cycle: %s",
this->store_.min_power / 10.0f, YESNO(this->init_with_half_cycle_));
LOG_PIN(" Output Pin: ", this->gate_pin_);
LOG_PIN(" Zero-Cross Pin: ", this->zero_cross_pin_);
if (method_ == DIM_METHOD_LEADING_PULSE) {
ESP_LOGCONFIG(TAG, " Method: leading pulse");
} else if (method_ == DIM_METHOD_LEADING) {
ESP_LOGCONFIG(TAG, " Method: leading");
if (this->zero_cross_interrupt_type_ == gpio::INTERRUPT_RISING_EDGE) {
ESP_LOGCONFIG(TAG, " Interrupt Type: rising");
} else if (this->zero_cross_interrupt_type_ == gpio::INTERRUPT_FALLING_EDGE) {
ESP_LOGCONFIG(TAG, " Interrupt Type: falling");
} else {
ESP_LOGCONFIG(TAG, " Method: trailing");
ESP_LOGCONFIG(TAG, " Interrupt Type: any");
}
if (method_ == DIM_METHOD_LEADING_PULSE) {
ESP_LOGCONFIG(TAG, " Method: leading pulse");
} else if (method_ == DIM_METHOD_LEADING) {
ESP_LOGCONFIG(TAG, " Method: leading");
} else {
ESP_LOGCONFIG(TAG, " Method: trailing");
}
LOG_FLOAT_OUTPUT(this);
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
}
+2
View File
@@ -48,6 +48,7 @@ class AcDimmer : public output::FloatOutput, public Component {
void dump_config() override;
void set_gate_pin(InternalGPIOPin *gate_pin) { gate_pin_ = gate_pin; }
void set_zero_cross_pin(InternalGPIOPin *zero_cross_pin) { zero_cross_pin_ = zero_cross_pin; }
void set_zero_cross_interrupt_type(gpio::InterruptType type) { zero_cross_interrupt_type_ = type; }
void set_init_with_half_cycle(bool init_with_half_cycle) { init_with_half_cycle_ = init_with_half_cycle; }
void set_method(DimMethod method) { method_ = method; }
@@ -56,6 +57,7 @@ class AcDimmer : public output::FloatOutput, public Component {
InternalGPIOPin *gate_pin_;
InternalGPIOPin *zero_cross_pin_;
gpio::InterruptType zero_cross_interrupt_type_;
AcDimmerDataStore store_;
bool init_with_half_cycle_;
DimMethod method_;
+14
View File
@@ -7,6 +7,8 @@ from esphome.core import CORE
CODEOWNERS = ["@glmnet"]
gpio_ns = cg.esphome_ns.namespace("gpio")
ac_dimmer_ns = cg.esphome_ns.namespace("ac_dimmer")
AcDimmer = ac_dimmer_ns.class_("AcDimmer", output.FloatOutput, cg.Component)
@@ -17,15 +19,26 @@ DIM_METHODS = {
"TRAILING": DimMethod.DIM_METHOD_TRAILING,
}
ZC_INTERRUPT_TYPES = {
"RISING": gpio_ns.INTERRUPT_RISING_EDGE,
"FALLING": gpio_ns.INTERRUPT_FALLING_EDGE,
"ANY": gpio_ns.INTERRUPT_ANY_EDGE,
}
CONF_GATE_PIN = "gate_pin"
CONF_ZERO_CROSS_PIN = "zero_cross_pin"
CONF_INIT_WITH_HALF_CYCLE = "init_with_half_cycle"
CONF_ZERO_CROSS_INTERRUPT_TYPE = "zero_cross_interrupt_type"
CONFIG_SCHEMA = cv.All(
output.FLOAT_OUTPUT_SCHEMA.extend(
{
cv.Required(CONF_ID): cv.declare_id(AcDimmer),
cv.Required(CONF_GATE_PIN): pins.internal_gpio_output_pin_schema,
cv.Required(CONF_ZERO_CROSS_PIN): pins.internal_gpio_input_pin_schema,
cv.Optional(CONF_ZERO_CROSS_INTERRUPT_TYPE, default="FALLING"): cv.enum(
ZC_INTERRUPT_TYPES, upper=True, space="_"
),
cv.Optional(CONF_INIT_WITH_HALF_CYCLE, default=True): cv.boolean,
cv.Optional(CONF_METHOD, default="leading pulse"): cv.enum(
DIM_METHODS, upper=True, space="_"
@@ -54,5 +67,6 @@ async def to_code(config):
cg.add(var.set_gate_pin(pin))
pin = await cg.gpio_pin_expression(config[CONF_ZERO_CROSS_PIN])
cg.add(var.set_zero_cross_pin(pin))
cg.add(var.set_zero_cross_interrupt_type(config[CONF_ZERO_CROSS_INTERRUPT_TYPE]))
cg.add(var.set_init_with_half_cycle(config[CONF_INIT_WITH_HALF_CYCLE]))
cg.add(var.set_method(config[CONF_METHOD]))
+4 -2
View File
@@ -2,6 +2,8 @@
#include <cstdio>
#include <cstring>
#include "esphome/core/alloc_helpers.h"
namespace esphome {
namespace anova {
@@ -105,14 +107,14 @@ void AnovaCodec::decode(const uint8_t *data, uint16_t length) {
}
case READ_TARGET_TEMPERATURE:
case SET_TARGET_TEMPERATURE: {
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
this->target_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
if (this->fahrenheit_)
this->target_temp_ = ftoc(this->target_temp_);
this->has_target_temp_ = true;
break;
}
case READ_CURRENT_TEMPERATURE: {
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f);
this->current_temp_ = parse_number<float>(str_until(buf, '\r')).value_or(0.0f); // NOLINT
if (this->fahrenheit_)
this->current_temp_ = ftoc(this->current_temp_);
this->has_current_temp_ = true;
+5 -6
View File
@@ -291,12 +291,12 @@ CONFIG_SCHEMA = cv.All(
cv.SplitDefault(
CONF_MAX_CONNECTIONS,
esp8266=4, # ~40KB free RAM, each connection uses ~500-1000 bytes
esp32=8, # 520KB RAM available
esp32=5, # 520KB RAM available
rp2040=4, # 264KB RAM but LWIP constraints
bk72xx=8, # Moderate RAM
rtl87xx=8, # Moderate RAM
bk72xx=5, # Moderate RAM
rtl87xx=5, # Moderate RAM
host=8, # Abundant resources
ln882x=8, # Moderate RAM
ln882x=5, # Moderate RAM
): cv.int_range(min=1, max=20),
# Maximum queued send buffers per connection before dropping connection
# Each buffer uses ~8-12 bytes overhead plus actual message size
@@ -336,8 +336,7 @@ async def to_code(config: ConfigType) -> None:
cg.add(var.set_batch_delay(config[CONF_BATCH_DELAY]))
if CONF_LISTEN_BACKLOG in config:
cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
if CONF_MAX_CONNECTIONS in config:
cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
cg.add_define("MAX_API_CONNECTIONS", config[CONF_MAX_CONNECTIONS])
cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
# Set USE_API_USER_DEFINED_ACTIONS if any services are enabled
+38 -6
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 {
@@ -2544,27 +2553,50 @@ message ListEntitiesInfraredResponse {
message InfraredRFTransmitRawTimingsRequest {
option (id) = 136;
option (source) = SOURCE_CLIENT;
option (ifdef) = "USE_IR_RF";
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance
uint32 carrier_frequency = 3; // Carrier frequency in Hz
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
fixed32 key = 2 [(force) = true]; // Key identifying the transmitter instance
uint32 carrier_frequency = 3; // Carrier frequency in Hz
uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.)
repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off)
uint32 modulation = 6; // RadioFrequencyModulation enum value (0 = OOK; ignored for IR entities)
}
// Event message for received infrared/RF data
message InfraredRFReceiveEvent {
option (id) = 137;
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_IR_RF";
option (ifdef) = "USE_IR_RF || USE_RADIO_FREQUENCY";
option (no_delay) = true;
uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"];
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
fixed32 key = 2 [(force) = true]; // Key identifying the receiver instance
repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}
// ==================== RADIO FREQUENCY ====================
// Lists available radio frequency entity instances
message ListEntitiesRadioFrequencyResponse {
option (id) = 148;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_RADIO_FREQUENCY";
string object_id = 1 [(max_data_length) = 120, (force) = true];
fixed32 key = 2 [(force) = true];
string name = 3 [(max_data_length) = 120, (force) = true];
string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON", (max_data_length) = 63];
bool disabled_by_default = 5;
EntityCategory entity_category = 6;
uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"];
uint32 capabilities = 8; // Bitmask of RadioFrequencyCapabilityFlags: bit 0 = transmitter, bit 1 = receiver
uint32 frequency_min = 9; // Minimum tunable frequency in Hz; if min == max (non-zero): fixed frequency; 0 = unspecified
uint32 frequency_max = 10; // Maximum tunable frequency in Hz; 0 = unspecified
uint32 supported_modulations = 11; // Bitmask of supported RadioFrequencyModulation values (bit N = modulation N supported)
}
// ==================== SERIAL PROXY ====================
enum SerialProxyParity {
+56 -8
View File
@@ -49,6 +49,9 @@
#ifdef USE_INFRARED
#include "esphome/components/infrared/infrared.h"
#endif
#ifdef USE_RADIO_FREQUENCY
#include "esphome/components/radio_frequency/radio_frequency.h"
#endif
namespace esphome::api {
@@ -100,6 +103,12 @@ static const int CAMERA_STOP_STREAM = 5000;
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \
if ((entity_var) == nullptr) \
return;
// Helper macro for multi-entity dispatch: looks up an entity by key and device_id without early return or make_call().
// Use when multiple entity types must be checked in sequence (at most one will match).
#define ENTITY_COMMAND_LOOKUP(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id)
#else // No device support, use simpler macros
// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call
// object
@@ -115,6 +124,12 @@ static const int CAMERA_STOP_STREAM = 5000;
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
if ((entity_var) == nullptr) \
return;
// Helper macro for multi-entity dispatch: looks up an entity by key without early return or make_call().
// Use when multiple entity types must be checked in sequence (at most one will match).
#define ENTITY_COMMAND_LOOKUP(entity_type, entity_var, getter_name) \
entity_type *entity_var = App.get_##getter_name##_by_key(msg.key)
#endif // USE_DEVICES
APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) : parent_(parent) {
@@ -1471,19 +1486,36 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c
}
#endif
#ifdef USE_IR_RF
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
// TODO: When RF is implemented, add a field to the message to distinguish IR vs RF
// and dispatch to the appropriate entity type based on that field.
// Dispatch by key: infrared entities are checked first, then radio frequency entities.
// The key is unique across all entity instances on a device, so at most one lookup will succeed.
#ifdef USE_INFRARED
ENTITY_COMMAND_MAKE_CALL(infrared::Infrared, infrared, infrared)
call.set_carrier_frequency(msg.carrier_frequency);
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
call.set_repeat_count(msg.repeat_count);
call.perform();
ENTITY_COMMAND_LOOKUP(infrared::Infrared, infrared, infrared);
if (infrared != nullptr) {
auto call = infrared->make_call();
call.set_carrier_frequency(msg.carrier_frequency);
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
call.set_repeat_count(msg.repeat_count);
call.perform();
return;
}
#endif
#ifdef USE_RADIO_FREQUENCY
ENTITY_COMMAND_LOOKUP(radio_frequency::RadioFrequency, radio_frequency, radio_frequency);
if (radio_frequency != nullptr) {
auto call = radio_frequency->make_call();
call.set_frequency(msg.carrier_frequency);
call.set_modulation(static_cast<radio_frequency::RadioFrequencyModulation>(msg.modulation));
call.set_repeat_count(msg.repeat_count);
call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
call.perform();
}
#endif
}
#endif
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { this->send_message(msg); }
#endif
@@ -1580,6 +1612,19 @@ uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection
}
#endif
#ifdef USE_RADIO_FREQUENCY
uint16_t APIConnection::try_send_radio_frequency_info(EntityBase *entity, APIConnection *conn,
uint32_t remaining_size) {
auto *rf = static_cast<radio_frequency::RadioFrequency *>(entity);
ListEntitiesRadioFrequencyResponse msg;
msg.capabilities = rf->get_capability_flags();
msg.frequency_min = rf->get_traits().get_frequency_min_hz();
msg.frequency_max = rf->get_traits().get_frequency_max_hz();
msg.supported_modulations = rf->get_traits().get_supported_modulations();
return fill_and_encode_entity_info(rf, msg, conn, remaining_size);
}
#endif
#ifdef USE_UPDATE
bool APIConnection::send_update_state(update::UpdateEntity *update) {
return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE);
@@ -2341,6 +2386,9 @@ uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item,
#ifdef USE_INFRARED
CASE_INFO_ONLY(infrared, ListEntitiesInfraredResponse)
#endif
#ifdef USE_RADIO_FREQUENCY
CASE_INFO_ONLY(radio_frequency, ListEntitiesRadioFrequencyResponse)
#endif
#ifdef USE_EVENT
CASE_INFO_ONLY(event, ListEntitiesEventResponse)
#endif
+4 -1
View File
@@ -223,7 +223,7 @@ class APIConnection final : public APIServerConnectionBase {
void on_water_heater_command_request(const WaterHeaterCommandRequest &msg);
#endif
#ifdef USE_IR_RF
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg);
void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg);
#endif
@@ -612,6 +612,9 @@ class APIConnection final : public APIServerConnectionBase {
#ifdef USE_INFRARED
static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
#endif
#ifdef USE_RADIO_FREQUENCY
static uint16_t try_send_radio_frequency_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size);
#endif
#ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
uint32_t remaining_size);
+48 -1
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 {
@@ -3861,7 +3865,7 @@ uint32_t ListEntitiesInfraredResponse::calculate_size() const {
return size;
}
#endif
#ifdef USE_IR_RF
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
#ifdef USE_DEVICES
@@ -3875,6 +3879,9 @@ bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, proto
case 4:
this->repeat_count = value;
break;
case 6:
this->modulation = value;
break;
default:
return false;
}
@@ -3928,6 +3935,46 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const {
return size;
}
#endif
#ifdef USE_RADIO_FREQUENCY
uint8_t *ListEntitiesRadioFrequencyResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
uint8_t *__restrict__ pos = buffer.get_pos();
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 10, this->object_id);
ProtoEncode::write_tag_and_fixed32(pos PROTO_ENCODE_DEBUG_ARG, 21, this->key);
ProtoEncode::encode_short_string_force(pos PROTO_ENCODE_DEBUG_ARG, 26, this->name);
#ifdef USE_ENTITY_ICON
ProtoEncode::encode_string(pos PROTO_ENCODE_DEBUG_ARG, 4, this->icon);
#endif
ProtoEncode::encode_bool(pos PROTO_ENCODE_DEBUG_ARG, 5, this->disabled_by_default);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 6, static_cast<uint32_t>(this->entity_category));
#ifdef USE_DEVICES
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 7, this->device_id);
#endif
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 8, this->capabilities);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 9, this->frequency_min);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 10, this->frequency_max);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, this->supported_modulations);
return pos;
}
uint32_t ListEntitiesRadioFrequencyResponse::calculate_size() const {
uint32_t size = 0;
size += 2 + this->object_id.size();
size += 5;
size += 2 + this->name.size();
#ifdef USE_ENTITY_ICON
size += !this->icon.empty() ? 2 + this->icon.size() : 0;
#endif
size += ProtoSize::calc_bool(1, this->disabled_by_default);
size += this->entity_category ? 2 : 0;
#ifdef USE_DEVICES
size += ProtoSize::calc_uint32(1, this->device_id);
#endif
size += ProtoSize::calc_uint32(1, this->capabilities);
size += ProtoSize::calc_uint32(1, this->frequency_min);
size += ProtoSize::calc_uint32(1, this->frequency_max);
size += ProtoSize::calc_uint32(1, this->supported_modulations);
return size;
}
#endif
#ifdef USE_SERIAL_PROXY
bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, proto_varint_value_t value) {
switch (field_id) {
+33 -4
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,
@@ -1372,7 +1377,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 +1399,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 +1477,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 +1486,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
@@ -3054,11 +3061,11 @@ class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage {
protected:
};
#endif
#ifdef USE_IR_RF
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 136;
static constexpr uint8_t ESTIMATED_SIZE = 220;
static constexpr uint8_t ESTIMATED_SIZE = 224;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("infrared_rf_transmit_raw_timings_request"); }
#endif
@@ -3071,6 +3078,7 @@ class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage {
const uint8_t *timings_data_{nullptr};
uint16_t timings_length_{0};
uint16_t timings_count_{0};
uint32_t modulation{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
@@ -3101,6 +3109,27 @@ class InfraredRFReceiveEvent final : public ProtoMessage {
protected:
};
#endif
#ifdef USE_RADIO_FREQUENCY
class ListEntitiesRadioFrequencyResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 148;
static constexpr uint8_t ESTIMATED_SIZE = 56;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_radio_frequency_response"); }
#endif
uint32_t capabilities{0};
uint32_t frequency_min{0};
uint32_t frequency_max{0};
uint32_t supported_modulations{0};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif
protected:
};
#endif
#ifdef USE_SERIAL_PROXY
class SerialProxyConfigureRequest final : public ProtoDecodableMessage {
public:
+37 -1
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) {
@@ -1539,6 +1551,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 +1625,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 {
@@ -2576,7 +2590,7 @@ const char *ListEntitiesInfraredResponse::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_IR_RF
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("InfraredRFTransmitRawTimingsRequest"));
#ifdef USE_DEVICES
@@ -2591,6 +2605,7 @@ const char *InfraredRFTransmitRawTimingsRequest::dump_to(DumpBuffer &out) const
out.append_p(ESPHOME_PSTR(" values, "));
append_uint(out, this->timings_length_);
out.append_p(ESPHOME_PSTR(" bytes]\n"));
dump_field(out, ESPHOME_PSTR("modulation"), this->modulation);
return out.c_str();
}
const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
@@ -2605,6 +2620,27 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const {
return out.c_str();
}
#endif
#ifdef USE_RADIO_FREQUENCY
const char *ListEntitiesRadioFrequencyResponse::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("ListEntitiesRadioFrequencyResponse"));
dump_field(out, ESPHOME_PSTR("object_id"), this->object_id);
dump_field(out, ESPHOME_PSTR("key"), this->key);
dump_field(out, ESPHOME_PSTR("name"), this->name);
#ifdef USE_ENTITY_ICON
dump_field(out, ESPHOME_PSTR("icon"), this->icon);
#endif
dump_field(out, ESPHOME_PSTR("disabled_by_default"), this->disabled_by_default);
dump_field(out, ESPHOME_PSTR("entity_category"), static_cast<enums::EntityCategory>(this->entity_category));
#ifdef USE_DEVICES
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
#endif
dump_field(out, ESPHOME_PSTR("capabilities"), this->capabilities);
dump_field(out, ESPHOME_PSTR("frequency_min"), this->frequency_min);
dump_field(out, ESPHOME_PSTR("frequency_max"), this->frequency_max);
dump_field(out, ESPHOME_PSTR("supported_modulations"), this->supported_modulations);
return out.c_str();
}
#endif
#ifdef USE_SERIAL_PROXY
const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const {
MessageDumpHelper helper(out, ESPHOME_PSTR("SerialProxyConfigureRequest"));
+1 -1
View File
@@ -625,7 +625,7 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
break;
}
#endif
#ifdef USE_IR_RF
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: {
InfraredRFTransmitRawTimingsRequest msg;
msg.decode(msg_data, msg_size);
+1 -1
View File
@@ -211,7 +211,7 @@ class APIServerConnectionBase {
void on_z_wave_proxy_request(const ZWaveProxyRequest &value){};
#endif
#ifdef USE_IR_RF
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){};
#endif
+33 -30
View File
@@ -118,7 +118,7 @@ void APIServer::loop() {
this->accept_new_connections_();
}
if (this->clients_.empty()) {
if (this->api_connection_count_ == 0) {
// Check reboot timeout - done in loop to avoid scheduler heap churn
// (cancelled scheduler items sit in heap memory until their scheduled time)
if (this->reboot_timeout_ != 0) {
@@ -135,15 +135,15 @@ void APIServer::loop() {
// Check network connectivity once for all clients
if (!network::is_connected()) {
// Network is down - disconnect all clients
for (auto &client : this->clients_) {
for (auto &client : this->active_clients()) {
client->on_fatal_error();
client->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Network down; disconnect"));
}
// Continue to process and clean up the clients below
}
size_t client_index = 0;
while (client_index < this->clients_.size()) {
uint8_t client_index = 0;
while (client_index < this->api_connection_count_) {
auto &client = this->clients_[client_index];
// Common case: process active client
@@ -161,7 +161,7 @@ void APIServer::loop() {
}
}
void APIServer::remove_client_(size_t client_index) {
void APIServer::remove_client_(uint8_t client_index) {
auto &client = this->clients_[client_index];
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
@@ -179,14 +179,17 @@ void APIServer::remove_client_(size_t client_index) {
// Close socket now (was deferred from on_fatal_error to allow getpeername)
client->helper_->close();
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
// Swap-and-reset: move the removed client to the trailing slot and null it out so slots
// [api_connection_count_, N) remain nullptr.
const uint8_t last_index = this->api_connection_count_ - 1;
if (client_index < last_index) {
std::swap(this->clients_[client_index], this->clients_[last_index]);
}
this->clients_.pop_back();
this->clients_[last_index].reset();
this->api_connection_count_--;
// Last client disconnected - set warning and start tracking for reboot timeout
if (this->clients_.empty() && this->reboot_timeout_ != 0) {
if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0) {
this->status_set_warning(LOG_STR("waiting for client connection"));
this->last_connected_ = App.get_loop_component_start_time();
}
@@ -210,8 +213,8 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
sock->getpeername_to(peername);
// Check if we're at the connection limit
if (this->clients_.size() >= this->max_connections_) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", this->max_connections_, peername);
if (this->api_connection_count_ >= MAX_API_CONNECTIONS) {
ESP_LOGW(TAG, "Max connections (%d), rejecting %s", MAX_API_CONNECTIONS, peername);
// Immediately close - socket destructor will handle cleanup
sock.reset();
continue;
@@ -220,11 +223,11 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() {
ESP_LOGD(TAG, "Accept %s", peername);
auto *conn = new APIConnection(std::move(sock), this);
this->clients_.emplace_back(conn);
this->clients_[this->api_connection_count_++].reset(conn);
conn->start();
// First client connected - clear warning and update timestamp
if (this->clients_.size() == 1 && this->reboot_timeout_ != 0) {
if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0) {
this->status_clear_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
@@ -237,7 +240,7 @@ void APIServer::dump_config() {
" Address: %s:%u\n"
" Listen backlog: %u\n"
" Max connections: %u",
network::get_use_address(), this->port_, this->listen_backlog_, this->max_connections_);
network::get_use_address(), this->port_, this->listen_backlog_, MAX_API_CONNECTIONS);
#ifdef USE_API_NOISE
ESP_LOGCONFIG(TAG, " Noise encryption: %s", YESNO(this->noise_ctx_.has_psk()));
if (!this->noise_ctx_.has_psk()) {
@@ -255,7 +258,7 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) { \
for (auto &c : this->active_clients()) { \
if (c->flags_.state_subscription) \
c->send_##entity_name##_state(obj); \
} \
@@ -337,7 +340,7 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) {
for (auto &c : this->active_clients()) {
if (c->flags_.state_subscription)
c->send_event(obj);
}
@@ -349,7 +352,7 @@ void APIServer::on_event(event::Event *obj) {
void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) {
for (auto &c : this->active_clients()) {
if (c->flags_.state_subscription)
c->send_update_state(obj);
}
@@ -360,12 +363,12 @@ void APIServer::on_update(update::UpdateEntity *obj) {
void APIServer::on_zwave_proxy_request(const ZWaveProxyRequest &msg) {
// We could add code to manage a second subscription type, but, since this message type is
// very infrequent and small, we simply send it to all clients
for (auto &c : this->clients_)
for (auto &c : this->active_clients())
c->send_message(msg);
}
#endif
#ifdef USE_IR_RF
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key,
const std::vector<int32_t> *timings) {
InfraredRFReceiveEvent resp{};
@@ -375,7 +378,7 @@ void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_
resp.key = key;
resp.timings = timings;
for (auto &c : this->clients_)
for (auto &c : this->active_clients())
c->send_infrared_rf_receive_event(resp);
}
#endif
@@ -392,7 +395,7 @@ void APIServer::set_batch_delay(uint16_t batch_delay) { this->batch_delay_ = bat
#ifdef USE_API_HOMEASSISTANT_SERVICES
void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call) {
for (auto &client : this->clients_) {
for (auto &client : this->active_clients()) {
client->send_homeassistant_action(call);
}
}
@@ -532,7 +535,7 @@ bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString
return;
}
ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
for (auto &c : this->clients_) {
for (auto &c : this->active_clients()) {
DisconnectRequest req;
c->send_message(req);
}
@@ -583,7 +586,7 @@ bool APIServer::clear_noise_psk(bool make_active) {
#ifdef USE_HOMEASSISTANT_TIME
void APIServer::request_time() {
for (auto &client : this->clients_) {
for (auto &client : this->active_clients()) {
if (!client->flags_.remove && client->is_authenticated()) {
client->send_time_request();
return; // Only request from one client to avoid clock conflicts
@@ -593,8 +596,8 @@ void APIServer::request_time() {
#endif
bool APIServer::is_connected_with_state_subscription() const {
for (const auto &client : this->clients_) {
if (client->flags_.state_subscription) {
for (uint8_t i = 0; i < this->api_connection_count_; i++) {
if (this->clients_[i]->flags_.state_subscription) {
return true;
}
}
@@ -609,7 +612,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
// we would be filling a buffer we are trying to clear
return;
}
for (auto &c : this->clients_) {
for (auto &c : this->active_clients()) {
if (!c->flags_.remove && c->get_log_subscription_level() >= level)
c->try_send_log_message(level, tag, message, message_len);
}
@@ -618,7 +621,7 @@ void APIServer::on_log(uint8_t level, const char *tag, const char *message, size
#ifdef USE_CAMERA
void APIServer::on_camera_image(const std::shared_ptr<camera::CameraImage> &image) {
for (auto &c : this->clients_) {
for (auto &c : this->active_clients()) {
if (!c->flags_.remove)
c->set_camera_state(image);
}
@@ -635,7 +638,7 @@ void APIServer::on_shutdown() {
this->batch_delay_ = 5;
// Send disconnect requests to all connected clients
for (auto &c : this->clients_) {
for (auto &c : this->active_clients()) {
DisconnectRequest req;
if (!c->send_message(req)) {
// If we can't send the disconnect request directly (tx_buffer full),
@@ -653,7 +656,7 @@ bool APIServer::teardown() {
this->loop();
// Return true only when all clients have been torn down
return this->clients_.empty();
return this->api_connection_count_ == 0;
}
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
+27 -8
View File
@@ -21,6 +21,8 @@
#include "esphome/components/camera/camera.h"
#endif
#include <array>
#include <memory>
#include <vector>
namespace esphome::api {
@@ -63,7 +65,6 @@ class APIServer final : public Component,
void set_batch_delay(uint16_t batch_delay);
uint16_t get_batch_delay() const { return batch_delay_; }
void set_listen_backlog(uint8_t listen_backlog) { this->listen_backlog_ = listen_backlog; }
void set_max_connections(uint8_t max_connections) { this->max_connections_ = max_connections; }
// Get reference to shared buffer for API connections
APIBuffer &get_shared_buffer_ref() { return shared_write_buffer_; }
@@ -182,13 +183,30 @@ class APIServer final : public Component,
#ifdef USE_ZWAVE_PROXY
void on_zwave_proxy_request(const ZWaveProxyRequest &msg);
#endif
#ifdef USE_IR_RF
#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector<int32_t> *timings);
#endif
bool is_connected() const { return !this->clients_.empty(); }
bool is_connected() const { return this->api_connection_count_ != 0; }
bool is_connected_with_state_subscription() const;
// 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>;
class ActiveClientsView {
const APIConnectionPtr *begin_;
const APIConnectionPtr *end_;
public:
ActiveClientsView(const APIConnectionPtr *b, const APIConnectionPtr *e) : begin_(b), end_(e) {}
const APIConnectionPtr *begin() const { return this->begin_; }
const APIConnectionPtr *end() const { return this->end_; }
};
ActiveClientsView active_clients() const {
return {this->clients_.data(), this->clients_.data() + this->api_connection_count_};
}
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
const char *entity_id; // Pointer to flash (internal) or heap (external)
@@ -234,8 +252,8 @@ class APIServer final : public Component,
protected:
// Accept incoming socket connections. Only called when socket has pending connections.
void __attribute__((noinline)) accept_new_connections_();
// Remove a disconnected client by index. Swaps with last element and pops.
void __attribute__((noinline)) remove_client_(size_t client_index);
// Remove a disconnected client by index. Swaps with the last populated slot and resets it.
void __attribute__((noinline)) remove_client_(uint8_t client_index);
#ifdef USE_API_NOISE
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
@@ -273,8 +291,9 @@ class APIServer final : public Component,
uint32_t reboot_timeout_{300000};
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_{};
// Vectors and strings (12 bytes each on 32-bit)
std::vector<std::unique_ptr<APIConnection>> clients_;
// Shared proto write buffer for all connections.
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
// reserves the exact needed size. Pre-allocating here would cause heap fragmentation
@@ -309,10 +328,10 @@ class APIServer final : public Component,
uint16_t port_{6053};
uint16_t batch_delay_{100};
// Connection limits - these defaults will be overridden by config values
// from cv.SplitDefault in __init__.py which sets platform-specific defaults
// from cv.SplitDefault in __init__.py which sets platform-specific defaults.
uint8_t listen_backlog_{4};
uint8_t max_connections_{8};
bool shutting_down_ = false;
uint8_t api_connection_count_{0};
// 7 bytes used, 1 byte padding
#ifdef USE_API_NOISE
+18 -1
View File
@@ -93,7 +93,24 @@ async def async_run_logs(
config, raw_line, backtrace_state=backtrace_state
)
stop = await async_run(cli, on_log, name=name, subscribe_states=subscribe_states)
# Safe to fall back to plaintext here only for this diagnostics use
# case: the stream is one-way from device to client, and this code
# never accepts commands or acts on any message the device sends.
# An on-path attacker could still both inject fabricated log lines
# and passively read the device's log output (and any state data
# delivered when subscribe_states is enabled), so this does lose
# confidentiality as well as authentication/integrity. That tradeoff
# is acceptable for operator-visible logs, which aioesphomeapi also
# warns may come from an unverified device. Never mirror this opt-in
# for any connection that sends data to the device or uses Home
# Assistant actions.
stop = await async_run(
cli,
on_log,
name=name,
subscribe_states=subscribe_states,
allow_plaintext_fallback=True,
)
try:
await asyncio.Event().wait()
finally:
+3
View File
@@ -79,6 +79,9 @@ LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWater
#ifdef USE_INFRARED
LIST_ENTITIES_HANDLER(infrared, infrared::Infrared, ListEntitiesInfraredResponse)
#endif
#ifdef USE_RADIO_FREQUENCY
LIST_ENTITIES_HANDLER(radio_frequency, radio_frequency::RadioFrequency, ListEntitiesRadioFrequencyResponse)
#endif
#ifdef USE_EVENT
LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse)
#endif
+3
View File
@@ -87,6 +87,9 @@ class ListEntitiesIterator final : public ComponentIterator {
#ifdef USE_INFRARED
bool on_infrared(infrared::Infrared *entity) override;
#endif
#ifdef USE_RADIO_FREQUENCY
bool on_radio_frequency(radio_frequency::RadioFrequency *entity) override;
#endif
#ifdef USE_EVENT
bool on_event(event::Event *entity) override;
#endif
+3
View File
@@ -82,6 +82,9 @@ class InitialStateIterator final : public ComponentIterator {
#ifdef USE_INFRARED
bool on_infrared(infrared::Infrared *infrared) override { return true; };
#endif
#ifdef USE_RADIO_FREQUENCY
bool on_radio_frequency(radio_frequency::RadioFrequency *radio_frequency) override { return true; };
#endif
#ifdef USE_EVENT
bool on_event(event::Event *event) override { return true; };
#endif
+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:
@@ -0,0 +1,163 @@
#include "audio_http_media_source.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <algorithm>
namespace esphome::audio_http {
static const char *const TAG = "audio_http_media_source";
// Decoder task / buffer tuning. Kept here as constants so the header stays free of magic numbers.
static constexpr size_t DEFAULT_TRANSFER_BUFFER_SIZE = 8 * 1024; // Staging buffer between HTTP reader and decoder
static constexpr uint32_t HTTP_TIMEOUT_MS = 5000; // HTTP connect/read timeout
static constexpr uint32_t AUDIO_WRITE_TIMEOUT_MS = 50; // Max blocking time per on_audio_write() call
static constexpr uint32_t READER_WRITE_TIMEOUT_MS = 50; // Max blocking time when writing into the ring buffer
static constexpr uint8_t READER_TASK_PRIORITY = 2;
static constexpr uint8_t DECODER_TASK_PRIORITY = 2;
static constexpr size_t READER_TASK_STACK_SIZE = 4096;
static constexpr size_t DECODER_TASK_STACK_SIZE = 5120;
static constexpr uint32_t PAUSE_POLL_DELAY_MS = 20;
static constexpr const char *const HTTP_URI_PREFIX = "http://";
static constexpr const char *const HTTPS_URI_PREFIX = "https://";
void AudioHTTPMediaSource::dump_config() {
ESP_LOGCONFIG(TAG,
"Audio HTTP Media Source:\n"
" Buffer Size: %zu bytes\n"
" Decoder Task Stack in PSRAM: %s",
this->buffer_size_, YESNO(this->decoder_task_stack_in_psram_));
}
void AudioHTTPMediaSource::setup() {
this->disable_loop();
micro_decoder::DecoderConfig config;
config.ring_buffer_size = this->buffer_size_;
// Keep the transfer buffer smaller than the ring buffer so the reader can top up the ring
// while the decoder is still draining it, instead of oscillating between empty and full.
config.transfer_buffer_size = std::min(DEFAULT_TRANSFER_BUFFER_SIZE, this->buffer_size_ / 2);
config.http_timeout_ms = HTTP_TIMEOUT_MS;
config.audio_write_timeout_ms = AUDIO_WRITE_TIMEOUT_MS;
config.reader_write_timeout_ms = READER_WRITE_TIMEOUT_MS;
config.reader_priority = READER_TASK_PRIORITY;
config.decoder_priority = DECODER_TASK_PRIORITY;
config.reader_stack_size = READER_TASK_STACK_SIZE;
config.decoder_stack_size = DECODER_TASK_STACK_SIZE;
config.decoder_stack_in_psram = this->decoder_task_stack_in_psram_;
this->decoder_ = std::make_unique<micro_decoder::DecoderSource>(config);
if (this->decoder_ == nullptr) {
ESP_LOGE(TAG, "Failed to allocate decoder");
this->mark_failed();
return;
}
this->decoder_->set_listener(this); // We inherit from micro_decoder::DecoderListener
}
void AudioHTTPMediaSource::loop() { this->decoder_->loop(); }
bool AudioHTTPMediaSource::can_handle(const std::string &uri) const {
return uri.starts_with(HTTP_URI_PREFIX) || uri.starts_with(HTTPS_URI_PREFIX);
}
// Called from the orchestrator's main loop, so no synchronization needed with loop()
bool AudioHTTPMediaSource::play_uri(const std::string &uri) {
if (!this->is_ready() || this->is_failed() || this->status_has_error() || !this->has_listener()) {
return false;
}
// Check if source is already playing
if (this->get_state() != media_source::MediaSourceState::IDLE) {
ESP_LOGE(TAG, "Cannot play '%s': source is busy", uri.c_str());
return false;
}
// Validate URI starts with "http://" or "https://"
if (!uri.starts_with(HTTP_URI_PREFIX) && !uri.starts_with(HTTPS_URI_PREFIX)) {
ESP_LOGE(TAG, "Invalid URI: '%s'", uri.c_str());
return false;
}
if (this->decoder_->play_url(uri)) {
this->pause_.store(false, std::memory_order_relaxed);
this->enable_loop();
return true;
}
ESP_LOGE(TAG, "Failed to start playback of '%s'", uri.c_str());
return false;
}
// Called from the orchestrator's main loop, so no synchronization needed with loop()
void AudioHTTPMediaSource::handle_command(media_source::MediaSourceCommand command) {
switch (command) {
case media_source::MediaSourceCommand::STOP:
this->decoder_->stop();
break;
case media_source::MediaSourceCommand::PAUSE:
// Only valid while actively playing; ignoring from IDLE/ERROR/PAUSED prevents the state
// machine from getting stuck in PAUSED when no playback is active (which would block the
// next play_uri() call via its IDLE-state precondition).
if (this->get_state() != media_source::MediaSourceState::PLAYING)
break;
// PAUSE does not stop the decoder task. Instead, on_audio_write() returns 0 and temporarily
// yields, which fills the ring buffer and applies back pressure that effectively pauses both
// the decoder and HTTP reader tasks.
this->set_state_(media_source::MediaSourceState::PAUSED);
this->pause_.store(true, std::memory_order_relaxed);
break;
case media_source::MediaSourceCommand::PLAY:
// Only resume from PAUSED; don't fabricate a PLAYING state from IDLE/ERROR.
if (this->get_state() != media_source::MediaSourceState::PAUSED)
break;
this->set_state_(media_source::MediaSourceState::PLAYING);
this->pause_.store(false, std::memory_order_relaxed);
break;
default:
break;
}
}
// Called from the decoder task. Forwards to the orchestrator's listener, which is responsible for
// being thread-safe with respect to its own audio writer.
size_t AudioHTTPMediaSource::on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) {
if (this->pause_.load(std::memory_order_relaxed)) {
vTaskDelay(pdMS_TO_TICKS(PAUSE_POLL_DELAY_MS));
return 0;
}
return this->write_output(data, length, timeout_ms, this->stream_info_);
}
// Called from the decoder task before the first on_audio_write().
void AudioHTTPMediaSource::on_stream_info(const micro_decoder::AudioStreamInfo &info) {
this->stream_info_ = audio::AudioStreamInfo(info.get_bits_per_sample(), info.get_channels(), info.get_sample_rate());
}
// microDecoder invokes on_state_change() from inside decoder_->loop(), so this runs on the main
// loop thread and it's safe to call set_state_() directly.
void AudioHTTPMediaSource::on_state_change(micro_decoder::DecoderState state) {
switch (state) {
case micro_decoder::DecoderState::IDLE:
this->set_state_(media_source::MediaSourceState::IDLE);
this->disable_loop();
break;
case micro_decoder::DecoderState::PLAYING:
this->set_state_(media_source::MediaSourceState::PLAYING);
break;
case micro_decoder::DecoderState::FAILED:
this->set_state_(media_source::MediaSourceState::ERROR);
break;
default:
break;
}
}
} // namespace esphome::audio_http
#endif // USE_ESP32
@@ -0,0 +1,59 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "esphome/components/audio/audio.h"
#include "esphome/components/media_source/media_source.h"
#include "esphome/core/component.h"
#include <micro_decoder/decoder_source.h>
#include <micro_decoder/types.h>
#include <atomic>
#include <memory>
#include <string>
namespace esphome::audio_http {
// Inherits from two unrelated listener-style interfaces:
// - media_source::MediaSource: this source reports state and writes audio *to* an orchestrator
// (the orchestrator calls set_listener() on us with a MediaSourceListener*).
// - micro_decoder::DecoderListener: the underlying decoder calls back *into* us with decoded
// audio and state changes (we call decoder_->set_listener(this) in setup()).
// The two set_listener() methods live on different base classes and serve opposite directions.
class AudioHTTPMediaSource : public Component, public media_source::MediaSource, public micro_decoder::DecoderListener {
public:
void setup() override;
void loop() override;
void dump_config() override;
void set_buffer_size(size_t buffer_size) { this->buffer_size_ = buffer_size; }
void set_task_stack_in_psram(bool task_stack_in_psram) { this->decoder_task_stack_in_psram_ = task_stack_in_psram; }
// MediaSource interface implementation
bool play_uri(const std::string &uri) override;
void handle_command(media_source::MediaSourceCommand command) override;
bool can_handle(const std::string &uri) const override;
// DecoderListener interface implementation
size_t on_audio_write(const uint8_t *data, size_t length, uint32_t timeout_ms) override;
void on_stream_info(const micro_decoder::AudioStreamInfo &info) override;
void on_state_change(micro_decoder::DecoderState state) override;
protected:
std::unique_ptr<micro_decoder::DecoderSource> decoder_;
audio::AudioStreamInfo stream_info_;
size_t buffer_size_{50000};
// Written from the main loop in handle_command(), read from the decoder task in
// on_audio_write(). Must be atomic to avoid a data race.
std::atomic<bool> pause_{false};
bool decoder_task_stack_in_psram_{false};
};
} // namespace esphome::audio_http
#endif // USE_ESP32
@@ -0,0 +1,59 @@
from typing import Any
import esphome.codegen as cg
from esphome.components import audio, esp32, media_source, psram
import esphome.config_validation as cv
from esphome.const import CONF_BUFFER_SIZE, CONF_ID, CONF_TASK_STACK_IN_PSRAM
from esphome.types import ConfigType
CODEOWNERS = ["@kahrendt"]
AUTO_LOAD = ["audio"]
audio_http_ns = cg.esphome_ns.namespace("audio_http")
AudioHTTPMediaSource = audio_http_ns.class_(
"AudioHTTPMediaSource", cg.Component, media_source.MediaSource
)
def _request_micro_decoder(config: ConfigType) -> ConfigType:
audio.request_micro_decoder_support()
return config
def _validate_task_stack_in_psram(value: Any) -> bool:
# Only require the psram component when actually enabling PSRAM stacks; validating
# the boolean first means `false` doesn't trigger the requires_component check.
if value := cv.boolean(value):
return cv.requires_component(psram.DOMAIN)(value)
return value
CONFIG_SCHEMA = cv.All(
media_source.media_source_schema(
AudioHTTPMediaSource,
)
.extend(
{
cv.Optional(CONF_BUFFER_SIZE, default=50000): cv.int_range(
min=5000, max=1000000
),
cv.Optional(CONF_TASK_STACK_IN_PSRAM): _validate_task_stack_in_psram,
}
)
.extend(cv.COMPONENT_SCHEMA),
cv.only_on_esp32,
_request_micro_decoder,
)
async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
await media_source.register_media_source(var, config)
if config.get(CONF_TASK_STACK_IN_PSRAM):
cg.add(var.set_task_stack_in_psram(True))
esp32.add_idf_sdkconfig_option(
"CONFIG_SPIRAM_ALLOW_STACK_EXTERNAL_MEMORY", True
)
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
+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;
+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,
+47 -20
View File
@@ -20,58 +20,77 @@ constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data)
}
void BL0906::loop() {
if (this->current_channel_ == UINT8_MAX) {
return;
}
while (this->available())
this->flush();
if (this->current_channel_ == 0) {
if (this->current_stage_ == STAGE_IDLE) {
// Woken up between cycles to drain the action queue. Go back to sleep.
this->handle_actions_();
this->disable_loop();
return;
}
if (this->current_stage_ == STAGE_TEMP) {
// Temperature
this->read_data_(BL0906_TEMPERATURE, BL0906_TREF, this->temperature_sensor_);
} else if (this->current_channel_ == 1) {
} else if (this->current_stage_ == STAGE_CHANNEL_1) {
this->read_data_(BL0906_I_1_RMS, BL0906_IREF, this->current_1_sensor_);
this->read_data_(BL0906_WATT_1, BL0906_PREF, this->power_1_sensor_);
this->read_data_(BL0906_CF_1_CNT, BL0906_EREF, this->energy_1_sensor_);
} else if (this->current_channel_ == 2) {
} else if (this->current_stage_ == STAGE_CHANNEL_2) {
this->read_data_(BL0906_I_2_RMS, BL0906_IREF, this->current_2_sensor_);
this->read_data_(BL0906_WATT_2, BL0906_PREF, this->power_2_sensor_);
this->read_data_(BL0906_CF_2_CNT, BL0906_EREF, this->energy_2_sensor_);
} else if (this->current_channel_ == 3) {
} else if (this->current_stage_ == STAGE_CHANNEL_3) {
this->read_data_(BL0906_I_3_RMS, BL0906_IREF, this->current_3_sensor_);
this->read_data_(BL0906_WATT_3, BL0906_PREF, this->power_3_sensor_);
this->read_data_(BL0906_CF_3_CNT, BL0906_EREF, this->energy_3_sensor_);
} else if (this->current_channel_ == 4) {
} else if (this->current_stage_ == STAGE_CHANNEL_4) {
this->read_data_(BL0906_I_4_RMS, BL0906_IREF, this->current_4_sensor_);
this->read_data_(BL0906_WATT_4, BL0906_PREF, this->power_4_sensor_);
this->read_data_(BL0906_CF_4_CNT, BL0906_EREF, this->energy_4_sensor_);
} else if (this->current_channel_ == 5) {
} else if (this->current_stage_ == STAGE_CHANNEL_5) {
this->read_data_(BL0906_I_5_RMS, BL0906_IREF, this->current_5_sensor_);
this->read_data_(BL0906_WATT_5, BL0906_PREF, this->power_5_sensor_);
this->read_data_(BL0906_CF_5_CNT, BL0906_EREF, this->energy_5_sensor_);
} else if (this->current_channel_ == 6) {
} else if (this->current_stage_ == STAGE_CHANNEL_6) {
this->read_data_(BL0906_I_6_RMS, BL0906_IREF, this->current_6_sensor_);
this->read_data_(BL0906_WATT_6, BL0906_PREF, this->power_6_sensor_);
this->read_data_(BL0906_CF_6_CNT, BL0906_EREF, this->energy_6_sensor_);
} else if (this->current_channel_ == UINT8_MAX - 2) {
} else if (this->current_stage_ == STAGE_FREQ) {
// Frequency
this->read_data_(BL0906_FREQUENCY, BL0906_FREF, frequency_sensor_);
this->read_data_(BL0906_FREQUENCY, BL0906_FREF, this->frequency_sensor_);
// Voltage
this->read_data_(BL0906_V_RMS, BL0906_UREF, voltage_sensor_);
} else if (this->current_channel_ == UINT8_MAX - 1) {
this->read_data_(BL0906_V_RMS, BL0906_UREF, this->voltage_sensor_);
} else if (this->current_stage_ == STAGE_POWER) {
// Total power
this->read_data_(BL0906_WATT_SUM, BL0906_WATT, this->total_power_sensor_);
// Total Energy
this->read_data_(BL0906_CF_SUM_CNT, BL0906_CF, this->total_energy_sensor_);
} else {
this->current_channel_ = UINT8_MAX - 2; // Go to frequency and voltage
return;
}
this->current_channel_++;
this->advance_stage_();
this->handle_actions_();
}
void BL0906::advance_stage_() {
switch (this->current_stage_) {
case STAGE_CHANNEL_6:
this->current_stage_ = STAGE_FREQ;
break;
case STAGE_FREQ:
this->current_stage_ = STAGE_POWER;
break;
case STAGE_POWER:
// Cycle complete; sleep until the next update().
this->current_stage_ = STAGE_IDLE;
this->disable_loop();
break;
default:
this->current_stage_ = static_cast<BL0906Stage>(this->current_stage_ + 1);
break;
}
}
void BL0906::setup() {
while (this->available())
this->flush();
@@ -85,12 +104,20 @@ void BL0906::setup() {
this->bias_correction_(BL0906_RMSOS_6, 0.01200, 0); // Calibration current_6
this->write_array(USR_WRPROT_ONLYREAD, sizeof(USR_WRPROT_ONLYREAD));
// Loop stays idle until the first update() or enqueued action.
this->disable_loop();
}
void BL0906::update() { this->current_channel_ = 0; }
void BL0906::update() {
this->current_stage_ = STAGE_TEMP;
this->enable_loop();
}
size_t BL0906::enqueue_action_(ActionCallbackFuncPtr function) {
this->action_queue_.push_back(function);
// Ensure the queue is serviced even if the read cycle has already completed.
this->enable_loop();
return this->action_queue_.size();
}
+18 -1
View File
@@ -12,6 +12,22 @@
namespace esphome {
namespace bl0906 {
// Stage values for the read state machine. After STAGE_CHANNEL_6 the state machine
// jumps to the two sentinel stages below, then to STAGE_IDLE which marks the cycle
// as complete and disables the loop.
enum BL0906Stage : uint8_t {
STAGE_TEMP = 0, // chip temperature
STAGE_CHANNEL_1 = 1, // per-phase current + power + energy
STAGE_CHANNEL_2 = 2,
STAGE_CHANNEL_3 = 3,
STAGE_CHANNEL_4 = 4,
STAGE_CHANNEL_5 = 5,
STAGE_CHANNEL_6 = 6,
STAGE_FREQ = UINT8_MAX - 2, // frequency + voltage
STAGE_POWER = UINT8_MAX - 1, // total power + total energy
STAGE_IDLE = UINT8_MAX, // cycle complete
};
struct DataPacket { // NOLINT(altera-struct-pack-align)
uint8_t l{0};
uint8_t m{0};
@@ -79,7 +95,8 @@ class BL0906 : public PollingComponent, public uart::UARTDevice {
void bias_correction_(uint8_t address, float measurements, float correction);
uint8_t current_channel_{0};
BL0906Stage current_stage_{STAGE_IDLE};
void advance_stage_();
size_t enqueue_action_(ActionCallbackFuncPtr function);
void handle_actions_();
@@ -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_;
@@ -6,6 +6,7 @@ from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Fr
CODEOWNERS = ["@trvrnrth"]
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["sensor", "text_sensor"]
CONFLICTS_WITH = ["bme68x_bsec2"]
MULTI_CONF = True
CONF_BME680_BSEC_ID = "bme680_bsec_id"
@@ -13,6 +13,7 @@ from esphome.const import (
)
CODEOWNERS = ["@neffs", "@kbx81"]
CONFLICTS_WITH = ["bme680_bsec"]
DOMAIN = "bme68x_bsec2"
+8 -5
View File
@@ -204,24 +204,27 @@ void CSE7761Component::get_data_() {
value = this->read_(CSE7761_REG_RMSIA, 3);
this->data_.current_rms[0] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA
value = this->read_(CSE7761_REG_POWERPA, 4);
this->data_.active_power[0] = (0 == this->data_.current_rms[0]) ? 0 : ((uint32_t) abs((int) value));
// PowerPA is two's complement signed 32-bit per datasheet
this->data_.active_power[0] = (0 == this->data_.current_rms[0]) ? 0 : static_cast<int32_t>(value);
value = this->read_(CSE7761_REG_RMSIB, 3);
this->data_.current_rms[1] = ((value >= 0x800000) || (value < 1600)) ? 0 : value; // No load threshold of 10mA
value = this->read_(CSE7761_REG_POWERPB, 4);
this->data_.active_power[1] = (0 == this->data_.current_rms[1]) ? 0 : ((uint32_t) abs((int) value));
// PowerPB is two's complement signed 32-bit per datasheet
this->data_.active_power[1] = (0 == this->data_.current_rms[1]) ? 0 : static_cast<int32_t>(value);
// convert values and publish to sensors
float voltage = (float) this->data_.voltage_rms / this->coefficient_by_unit_(RMS_UC);
float voltage = static_cast<float>(this->data_.voltage_rms) / this->coefficient_by_unit_(RMS_UC);
if (this->voltage_sensor_ != nullptr) {
this->voltage_sensor_->publish_state(voltage);
}
for (uint8_t channel = 0; channel < 2; channel++) {
// Active power = PowerPA * PowerPAC * 1000 / 0x80000000
float active_power = (float) this->data_.active_power[channel] / this->coefficient_by_unit_(POWER_PAC); // W
float amps = (float) this->data_.current_rms[channel] / this->coefficient_by_unit_(RMS_IAC); // A
float active_power =
static_cast<float>(this->data_.active_power[channel]) / this->coefficient_by_unit_(POWER_PAC); // W
float amps = static_cast<float>(this->data_.current_rms[channel]) / this->coefficient_by_unit_(RMS_IAC); // A
ESP_LOGD(TAG, "Channel %d power %f W, current %f A", channel + 1, active_power, amps);
if (channel == 0) {
if (this->power_sensor_1_ != nullptr) {
+1 -3
View File
@@ -11,10 +11,8 @@ struct CSE7761DataStruct {
uint32_t frequency = 0;
uint32_t voltage_rms = 0;
uint32_t current_rms[2] = {0};
uint32_t energy[2] = {0};
uint32_t active_power[2] = {0};
int32_t active_power[2] = {0};
uint16_t coefficient[8] = {0};
uint8_t energy_update = 0;
bool ready = false;
};
+1 -1
View File
@@ -30,7 +30,7 @@ void DebugComponent::dump_config() {
char device_info_buffer[DEVICE_INFO_BUFFER_SIZE];
ESP_LOGD(TAG, "ESPHome version %s", ESPHOME_VERSION);
size_t pos = buf_append_printf(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, "%s", ESPHOME_VERSION);
size_t pos = buf_append_str(device_info_buffer, DEVICE_INFO_BUFFER_SIZE, 0, ESPHOME_VERSION);
this->free_heap_ = get_free_heap_();
ESP_LOGD(TAG, "Free Heap Size: %" PRIu32 " bytes", this->free_heap_);
+15 -8
View File
@@ -224,17 +224,21 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *model = ESPHOME_VARIANT;
// Build features string
pos = buf_append_printf(buf, size, pos, "|Chip: %s Features:", model);
pos = buf_append_str(buf, size, pos, "|Chip: ");
pos = buf_append_str(buf, size, pos, model);
pos = buf_append_str(buf, size, pos, " Features:");
bool first_feature = true;
for (const auto &feature : CHIP_FEATURES) {
if (info.features & feature.bit) {
pos = buf_append_printf(buf, size, pos, "%s%s", first_feature ? "" : ", ", feature.name);
pos = buf_append_str(buf, size, pos, first_feature ? "" : ", ");
pos = buf_append_str(buf, size, pos, feature.name);
first_feature = false;
info.features &= ~feature.bit;
}
}
if (info.features != 0) {
pos = buf_append_printf(buf, size, pos, "%sOther:0x%" PRIx32, first_feature ? "" : ", ", info.features);
pos = buf_append_str(buf, size, pos, first_feature ? "" : ", ");
pos = buf_append_printf(buf, size, pos, "Other:0x%" PRIx32, info.features);
}
pos = buf_append_printf(buf, size, pos, " Cores:%u Revision:%u", info.cores, info.revision);
@@ -267,17 +271,20 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
// Framework detection
#ifdef USE_ARDUINO
ESP_LOGD(TAG, " Framework: Arduino");
pos = buf_append_printf(buf, size, pos, "|Framework: Arduino");
pos = buf_append_str(buf, size, pos, "|Framework: Arduino");
#else
ESP_LOGD(TAG, " Framework: ESP-IDF");
pos = buf_append_printf(buf, size, pos, "|Framework: ESP-IDF");
pos = buf_append_str(buf, size, pos, "|Framework: ESP-IDF");
#endif
pos = buf_append_printf(buf, size, pos, "|ESP-IDF: %s", esp_get_idf_version());
pos = buf_append_str(buf, size, pos, "|ESP-IDF: ");
pos = buf_append_str(buf, size, pos, esp_get_idf_version());
pos = buf_append_printf(buf, size, pos, "|EFuse MAC: %02X:%02X:%02X:%02X:%02X:%02X", mac[0], mac[1], mac[2], mac[3],
mac[4], mac[5]);
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Wakeup: %s", wakeup_cause);
pos = buf_append_str(buf, size, pos, "|Reset: ");
pos = buf_append_str(buf, size, pos, reset_reason);
pos = buf_append_str(buf, size, pos, "|Wakeup: ");
pos = buf_append_str(buf, size, pos, wakeup_cause);
return pos;
}
+6 -3
View File
@@ -38,9 +38,12 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
lt_get_version(), lt_cpu_get_model_name(), lt_cpu_get_model(), lt_cpu_get_freq_mhz(), mac_id,
lt_get_board_code(), flash_kib, ram_kib, reset_reason);
pos = buf_append_printf(buf, size, pos, "|Version: %s", LT_BANNER_STR + 10);
pos = buf_append_printf(buf, size, pos, "|Reset Reason: %s", reset_reason);
pos = buf_append_printf(buf, size, pos, "|Chip Name: %s", lt_cpu_get_model_name());
pos = buf_append_str(buf, size, pos, "|Version: ");
pos = buf_append_str(buf, size, pos, LT_BANNER_STR + 10);
pos = buf_append_str(buf, size, pos, "|Reset Reason: ");
pos = buf_append_str(buf, size, pos, reset_reason);
pos = buf_append_str(buf, size, pos, "|Chip Name: ");
pos = buf_append_str(buf, size, pos, lt_cpu_get_model_name());
pos = buf_append_printf(buf, size, pos, "|Chip ID: 0x%06" PRIX32, mac_id);
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 " KiB", flash_kib);
pos = buf_append_printf(buf, size, pos, "|RAM: %" PRIu32 " KiB", ram_kib);
+18 -8
View File
@@ -162,14 +162,18 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
const char *supply_status =
(nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_NORMAL) ? "Normal voltage." : "High voltage.";
ESP_LOGD(TAG, "Main supply status: %s", supply_status);
pos = buf_append_printf(buf, size, pos, "|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);
// Regulator stage 0
if (nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) {
const char *reg0_type = nrf_power_dcdcen_vddh_get(NRF_POWER) ? "DC/DC" : "LDO";
const char *reg0_voltage = regout0_to_str((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos);
ESP_LOGD(TAG, "Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: %s, %s", reg0_type, reg0_voltage);
pos = buf_append_str(buf, size, pos, "|Regulator stage 0: ");
pos = buf_append_str(buf, size, pos, reg0_type);
pos = buf_append_str(buf, size, pos, ", ");
pos = buf_append_str(buf, size, pos, reg0_voltage);
#ifdef USE_NRF52_REG0_VOUT
if ((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) >> UICR_REGOUT0_VOUT_Pos != USE_NRF52_REG0_VOUT) {
ESP_LOGE(TAG, "Regulator stage 0: expected %s", regout0_to_str(USE_NRF52_REG0_VOUT));
@@ -177,13 +181,14 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
#endif
} else {
ESP_LOGD(TAG, "Regulator stage 0: disabled");
pos = buf_append_printf(buf, size, pos, "|Regulator stage 0: disabled");
pos = buf_append_str(buf, size, pos, "|Regulator stage 0: disabled");
}
// Regulator stage 1
const char *reg1_type = nrf_power_dcdcen_get(NRF_POWER) ? "DC/DC" : "LDO";
ESP_LOGD(TAG, "Regulator stage 1: %s", reg1_type);
pos = buf_append_printf(buf, size, pos, "|Regulator stage 1: %s", reg1_type);
pos = buf_append_str(buf, size, pos, "|Regulator stage 1: ");
pos = buf_append_str(buf, size, pos, reg1_type);
// USB power state
const char *usb_state;
@@ -197,7 +202,8 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
usb_state = "disconnected";
}
ESP_LOGD(TAG, "USB power state: %s", usb_state);
pos = buf_append_printf(buf, size, pos, "|USB power state: %s", usb_state);
pos = buf_append_str(buf, size, pos, "|USB power state: ");
pos = buf_append_str(buf, size, pos, usb_state);
// Power-fail comparator
bool enabled;
@@ -302,14 +308,18 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
break;
}
ESP_LOGD(TAG, "Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s, VDDH: %s", pof_voltage, vddh_voltage);
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: ");
pos = buf_append_str(buf, size, pos, pof_voltage);
pos = buf_append_str(buf, size, pos, ", VDDH: ");
pos = buf_append_str(buf, size, pos, vddh_voltage);
} else {
ESP_LOGD(TAG, "Power-fail comparator: %s", pof_voltage);
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: %s", pof_voltage);
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: ");
pos = buf_append_str(buf, size, pos, pof_voltage);
}
} else {
ESP_LOGD(TAG, "Power-fail comparator: disabled");
pos = buf_append_printf(buf, size, pos, "|Power-fail comparator: disabled");
pos = buf_append_str(buf, size, pos, "|Power-fail comparator: disabled");
}
auto package = [](uint32_t value) {
+6 -2
View File
@@ -14,6 +14,7 @@ from esphome.components.esp32 import (
VARIANT_ESP32S3,
get_esp32_variant,
)
from esphome.components.zephyr import zephyr_add_prj_conf
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
@@ -33,6 +34,7 @@ from esphome.const import (
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_NRF52,
PlatformFramework,
)
from esphome.core import CORE
@@ -304,7 +306,7 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX]),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_NRF52]),
validate_config,
)
@@ -369,6 +371,8 @@ async def to_code(config):
if CONF_TOUCH_WAKEUP in config:
cg.add(var.set_touch_wakeup(config[CONF_TOUCH_WAKEUP]))
if CORE.using_zephyr and "zigbee" not in CORE.loaded_integrations:
zephyr_add_prj_conf("POWEROFF", True)
cg.add_define("USE_DEEP_SLEEP")
@@ -413,7 +417,7 @@ async def deep_sleep_enter_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 CONF_SLEEP_DURATION in config:
template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.int32)
template_ = await cg.templatable(config[CONF_SLEEP_DURATION], args, cg.uint32)
cg.add(var.set_sleep_duration(template_))
if CONF_UNTIL in config:
@@ -59,6 +59,8 @@ void DeepSleepComponent::deep_sleep_() {
lt_deep_sleep_enter();
}
bool DeepSleepComponent::should_teardown_() { return true; }
} // namespace esphome::deep_sleep
#endif // USE_BK72XX
@@ -9,11 +9,22 @@ 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)
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)
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_() {
this->next_enter_deep_sleep_ = false;
const optional<uint32_t> run_duration = get_run_duration_();
if (run_duration.has_value()) {
ESP_LOGI(TAG, "Scheduling in %" PRIu32 " ms", *run_duration);
@@ -58,13 +69,17 @@ void DeepSleepComponent::begin_sleep(bool manual) {
if (this->sleep_duration_.has_value()) {
ESP_LOGI(TAG, "Sleeping for %" PRId64 "us", *this->sleep_duration_);
}
App.run_safe_shutdown_hooks();
// It's critical to teardown components cleanly for deep sleep to ensure
// Home Assistant sees a clean disconnect instead of marking the device unavailable
App.teardown_components(TEARDOWN_TIMEOUT_DEEP_SLEEP_MS);
App.run_powerdown_hooks();
if (this->should_teardown_()) {
App.run_safe_shutdown_hooks();
// It's critical to teardown components cleanly for deep sleep to ensure
// Home Assistant sees a clean disconnect instead of marking the device unavailable
App.teardown_components(TEARDOWN_TIMEOUT_DEEP_SLEEP_MS);
App.run_powerdown_hooks();
}
this->deep_sleep_();
this->schedule_sleep_();
}
float DeepSleepComponent::get_setup_priority() const { return setup_priority::LATE; }
@@ -4,6 +4,7 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <atomic>
#ifdef USE_ESP32
#include <esp_sleep.h>
@@ -14,6 +15,10 @@
#include "esphome/core/time.h"
#endif
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
#endif
#include <cinttypes>
namespace esphome {
@@ -120,6 +125,9 @@ 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
@@ -129,6 +137,8 @@ class DeepSleepComponent : public Component {
void dump_config_platform_();
bool prepare_to_sleep_();
void deep_sleep_();
void schedule_sleep_();
bool should_teardown_();
#ifdef USE_BK72XX
bool pin_prevents_sleep_(WakeUpPinItem &pinItem) const;
@@ -157,6 +167,9 @@ 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)
@@ -243,5 +256,8 @@ 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
@@ -165,6 +165,8 @@ void DeepSleepComponent::deep_sleep_() {
esp_deep_sleep_start();
}
bool DeepSleepComponent::should_teardown_() { return true; }
} // namespace deep_sleep
} // namespace esphome
#endif // USE_ESP32
@@ -18,6 +18,8 @@ void DeepSleepComponent::deep_sleep_() {
ESP.deepSleep(this->sleep_duration_.value_or(0)); // NOLINT(readability-static-accessed-through-instance)
}
bool DeepSleepComponent::should_teardown_() { return true; }
} // namespace deep_sleep
} // namespace esphome
#endif
@@ -0,0 +1,60 @@
#include "deep_sleep_component.h"
#ifdef USE_ZEPHYR
#include "esphome/core/log.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_() {}
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_);
} else {
#ifndef USE_ZIGBEE
// the device can be woken up through one of the following signals:
// - The DETECT signal, optionally generated by the GPIO peripheral.
// - The ANADETECT signal, optionally generated by the LPCOMP module.
// - The SENSE signal, optionally generated by the NFC module to wake-on-field.
// - Detecting a valid USB voltage on the VBUS pin (VBUS,DETECT).
// - A reset.
//
// The system is reset when it wakes up from System OFF mode.
sys_poweroff();
#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) {
ESP_LOGD(TAG, "Woken up by another thread");
} else {
ESP_LOGD(TAG, "Timeout expired (normal sleep)");
}
}
bool DeepSleepComponent::should_teardown_() {
if (this->sleep_duration_.has_value()) {
return false;
}
#ifdef USE_ZIGBEE
return false;
#else
return true;
#endif
}
} // namespace esphome::deep_sleep
#endif
+49 -16
View File
@@ -1,8 +1,19 @@
import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import uart
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_RECEIVE_TIMEOUT, CONF_UART_ID
from esphome.const import (
CONF_ID,
CONF_RECEIVE_TIMEOUT,
CONF_RX_BUFFER_SIZE,
CONF_UART_ID,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@glmnet", "@PolarGoose"]
@@ -21,8 +32,7 @@ CONF_MAX_TELEGRAM_LENGTH = "max_telegram_length"
CONF_REQUEST_INTERVAL = "request_interval"
CONF_REQUEST_PIN = "request_pin"
# Hack to prevent compile error due to ambiguity with lib namespace
dsmr_ns = cg.esphome_ns.namespace("esphome::dsmr")
dsmr_ns = cg.esphome_ns.namespace("dsmr")
Dsmr = dsmr_ns.class_("Dsmr", cg.Component, uart.UARTDevice)
@@ -54,24 +64,47 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
uart_component = await cg.get_variable(config[CONF_UART_ID])
var = cg.new_Pvariable(config[CONF_ID], uart_component, config[CONF_CRC_CHECK])
cg.add(var.set_max_telegram_length(config[CONF_MAX_TELEGRAM_LENGTH]))
if CONF_DECRYPTION_KEY in config:
cg.add(var.set_decryption_key(config[CONF_DECRYPTION_KEY]))
await cg.register_component(var, config)
if CONF_REQUEST_PIN in config:
request_pin = await cg.gpio_pin_expression(config[CONF_REQUEST_PIN])
cg.add(var.set_request_pin(request_pin))
cg.add(var.set_request_interval(config[CONF_REQUEST_INTERVAL].total_milliseconds))
cg.add(var.set_receive_timeout(config[CONF_RECEIVE_TIMEOUT].total_milliseconds))
else:
request_pin = cg.nullptr
decryption_key = config.get(CONF_DECRYPTION_KEY)
if decryption_key is None:
decryption_key = cg.nullptr
var = cg.new_Pvariable(
config[CONF_ID],
uart_component,
config[CONF_CRC_CHECK],
config[CONF_MAX_TELEGRAM_LENGTH],
config[CONF_REQUEST_INTERVAL].total_milliseconds,
config[CONF_RECEIVE_TIMEOUT].total_milliseconds,
request_pin,
decryption_key,
)
await cg.register_component(var, config)
cg.add_build_flag("-DDSMR_GAS_MBUS_ID=" + str(config[CONF_GAS_MBUS_ID]))
cg.add_build_flag("-DDSMR_WATER_MBUS_ID=" + str(config[CONF_WATER_MBUS_ID]))
cg.add_build_flag("-DDSMR_THERMAL_MBUS_ID=" + str(config[CONF_THERMAL_MBUS_ID]))
# DSMR Parser
cg.add_library("esphome/dsmr_parser", "1.1.0")
cg.add_library("esphome/dsmr_parser", "1.4.0")
# Crypto
cg.add_library("polargoose/Crypto-no-arduino", "0.4.0")
def final_validate(config: ConfigType) -> ConfigType:
full_config = fv.full_config.get()
for uart_conf in full_config["uart"]:
if uart_conf[CONF_ID] == config[CONF_UART_ID]:
rx_buffer_size = uart_conf[CONF_RX_BUFFER_SIZE]
if rx_buffer_size < 1500:
_LOGGER.warning(
"UART '%s' rx_buffer_size should be bigger than 1500 bytes to avoid packet losses (currently %d bytes).",
config[CONF_UART_ID],
rx_buffer_size,
)
break
return config
FINAL_VALIDATE_SCHEMA = final_validate
+141 -266
View File
@@ -1,315 +1,183 @@
#include "dsmr.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
// Ignore Zephyr. It doesn't have any encryption library.
#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST)
#include <AES.h>
#include <Crypto.h>
#include <GCM.h>
#include "dsmr.h"
#include "esphome/core/log.h"
#include <dsmr_parser/util.h>
namespace esphome::dsmr {
static const char *const TAG = "dsmr";
static constexpr auto &TAG = "dsmr";
static void log_callback(dsmr_parser::LogLevel level, const char *fmt, va_list args) {
std::array<char, 256> buf;
vsnprintf(buf.data(), buf.size(), fmt, args);
switch (level) {
case dsmr_parser::LogLevel::ERROR:
ESP_LOGE(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::WARNING:
ESP_LOGW(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::INFO:
ESP_LOGI(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::VERBOSE:
ESP_LOGV(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::VERY_VERBOSE:
ESP_LOGVV(TAG, "%s", buf.data());
break;
case dsmr_parser::LogLevel::DEBUG:
ESP_LOGD(TAG, "%s", buf.data());
break;
}
}
void Dsmr::setup() {
this->telegram_ = new char[this->max_telegram_len_]; // NOLINT
dsmr_parser::Logger::set_log_function(log_callback);
if (this->request_pin_ != nullptr) {
this->request_pin_->setup();
}
}
void Dsmr::loop() {
if (this->ready_to_request_data_()) {
if (this->decryption_key_.empty()) {
this->receive_telegram_();
} else {
this->receive_encrypted_telegram_();
}
if (!this->ready_to_request_data_()) {
return;
}
if (this->encryption_enabled_) {
this->receive_encrypted_telegram_();
} else {
this->receive_telegram_();
}
}
bool Dsmr::ready_to_request_data_() {
// When using a request pin, then wait for the next request interval.
if (this->request_pin_ != nullptr) {
if (!this->requesting_data_ && this->request_interval_reached_()) {
this->start_requesting_data_();
}
}
// Otherwise, sink serial data until next request interval.
else {
if (this->request_interval_reached_()) {
this->start_requesting_data_();
}
if (!this->requesting_data_) {
this->drain_rx_buffer_();
}
if (!this->requesting_data_ && this->request_interval_reached_()) {
this->start_requesting_data_();
}
return this->requesting_data_;
}
bool Dsmr::request_interval_reached_() {
bool Dsmr::request_interval_reached_() const {
if (this->last_request_time_ == 0) {
return true;
}
return millis() - this->last_request_time_ > this->request_interval_;
}
bool Dsmr::receive_timeout_reached_() { return millis() - this->last_read_time_ > this->receive_timeout_; }
bool Dsmr::available_within_timeout_() {
// Data are available for reading on the UART bus?
// Then we can start reading right away.
if (this->available()) {
this->last_read_time_ = millis();
return true;
}
// When we're not in the process of reading a telegram, then there is
// no need to actively wait for new data to come in.
if (!header_found_) {
return false;
}
// A telegram is being read. The smart meter might not deliver a telegram
// in one go, but instead send it in chunks with small pauses in between.
// When the UART RX buffer cannot hold a full telegram, then make sure
// that the UART read buffer does not overflow while other components
// perform their work in their loop. Do this by not returning control to
// the main loop, until the read timeout is reached.
if (this->parent_->get_rx_buffer_size() < this->max_telegram_len_) {
while (!this->receive_timeout_reached_()) {
delay(5);
if (this->available()) {
this->last_read_time_ = millis();
return true;
}
}
}
// No new data has come in during the read timeout? Then stop reading the
// telegram and start waiting for the next one to arrive.
if (this->receive_timeout_reached_()) {
ESP_LOGW(TAG, "Timeout while reading data for telegram");
this->reset_telegram_();
}
return false;
}
void Dsmr::start_requesting_data_() {
if (!this->requesting_data_) {
if (this->request_pin_ != nullptr) {
ESP_LOGV(TAG, "Start requesting data from P1 port");
this->request_pin_->digital_write(true);
} else {
ESP_LOGV(TAG, "Start reading data from P1 port");
}
this->requesting_data_ = true;
this->last_request_time_ = millis();
if (this->requesting_data_) {
return;
}
ESP_LOGV(TAG, "Start reading data from P1 port");
this->flush_rx_buffer_();
if (this->request_pin_ != nullptr) {
ESP_LOGV(TAG, "Set request pin to 1");
this->request_pin_->digital_write(true);
}
this->requesting_data_ = true;
this->last_request_time_ = millis();
}
void Dsmr::stop_requesting_data_() {
if (this->requesting_data_) {
if (this->request_pin_ != nullptr) {
ESP_LOGV(TAG, "Stop requesting data from P1 port");
this->request_pin_->digital_write(false);
} else {
ESP_LOGV(TAG, "Stop reading data from P1 port");
}
this->drain_rx_buffer_();
this->requesting_data_ = false;
if (!this->requesting_data_) {
return;
}
ESP_LOGV(TAG, "Stop reading data from P1 port");
if (this->request_pin_ != nullptr) {
ESP_LOGV(TAG, "Set request pin to 0");
this->request_pin_->digital_write(false);
}
this->requesting_data_ = false;
}
void Dsmr::drain_rx_buffer_() {
uint8_t buf[64];
size_t avail;
while ((avail = this->available()) > 0) {
if (!this->read_array(buf, std::min(avail, sizeof(buf)))) {
break;
}
void Dsmr::flush_rx_buffer_() {
ESP_LOGV(TAG, "Flush UART RX buffer");
while (!this->uart_read_chunk_().empty()) {
}
}
void Dsmr::reset_telegram_() {
this->header_found_ = false;
this->footer_found_ = false;
this->bytes_read_ = 0;
this->crypt_bytes_read_ = 0;
this->crypt_telegram_len_ = 0;
}
void Dsmr::receive_telegram_() {
while (this->available_within_timeout_()) {
// Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[64];
size_t avail = this->available();
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read))
for (auto data = this->uart_read_chunk_(); !data.empty(); data = this->uart_read_chunk_()) {
for (uint8_t byte : data) {
const auto telegram = this->packet_accumulator_.process_byte(byte);
if (!telegram) { // No full packet received yet
continue;
}
if (this->parse_telegram_(telegram.value())) {
return;
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
const char c = static_cast<char>(buf[i]);
// Find a new telegram header, i.e. forward slash.
if (c == '/') {
ESP_LOGV(TAG, "Header of telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
if (!this->header_found_)
continue;
// Check for buffer overflow.
if (this->bytes_read_ >= this->max_telegram_len_) {
this->reset_telegram_();
ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_);
return;
}
// Some v2.2 or v3 meters will send a new value which starts with '('
// in a new line, while the value belongs to the previous ObisId. For
// proper parsing, remove these new line characters.
if (c == '(') {
while (true) {
auto previous_char = this->telegram_[this->bytes_read_ - 1];
if (previous_char == '\n' || previous_char == '\r') {
this->bytes_read_--;
} else {
break;
}
}
}
// Store the byte in the buffer.
this->telegram_[this->bytes_read_] = c;
this->bytes_read_++;
// Check for a footer, i.e. exclamation mark, followed by a hex checksum.
if (c == '!') {
ESP_LOGV(TAG, "Footer of telegram found");
this->footer_found_ = true;
continue;
}
// Check for the end of the hex checksum, i.e. a newline.
if (this->footer_found_ && c == '\n') {
// Parse the telegram and publish sensor values.
this->parse_telegram();
this->reset_telegram_();
return;
}
}
}
}
}
void Dsmr::receive_encrypted_telegram_() {
while (this->available_within_timeout_()) {
// Read all available bytes in batches to reduce UART call overhead.
uint8_t buf[64];
size_t avail = this->available();
while (avail > 0) {
size_t to_read = std::min(avail, sizeof(buf));
if (!this->read_array(buf, to_read))
return;
avail -= to_read;
for (size_t i = 0; i < to_read; i++) {
const char c = static_cast<char>(buf[i]);
// Find a new telegram start byte.
if (!this->header_found_) {
if ((uint8_t) c != 0xDB) {
continue;
}
ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found");
this->reset_telegram_();
this->header_found_ = true;
}
// Check for buffer overflow.
if (this->crypt_bytes_read_ >= this->max_telegram_len_) {
this->reset_telegram_();
ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_);
return;
}
// Store the byte in the buffer.
this->crypt_telegram_[this->crypt_bytes_read_] = c;
this->crypt_bytes_read_++;
// Read the length of the incoming encrypted telegram.
if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) {
// Complete header + data bytes
this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]);
ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_);
}
// Check for the end of the encrypted telegram.
if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) {
continue;
}
ESP_LOGV(TAG, "End of encrypted telegram found");
// Decrypt the encrypted telegram.
GCM<AES128> *gcmaes128{new GCM<AES128>()};
gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize());
// the iv is 8 bytes of the system title + 4 bytes frame counter
// system title is at byte 2 and frame counter at byte 15
for (int i = 10; i < 14; i++)
this->crypt_telegram_[i] = this->crypt_telegram_[i + 4];
constexpr uint16_t iv_size{12};
gcmaes128->setIV(&this->crypt_telegram_[2], iv_size);
gcmaes128->decrypt(reinterpret_cast<uint8_t *>(this->telegram_),
// the ciphertext start at byte 18
&this->crypt_telegram_[18],
// cipher size
this->crypt_bytes_read_ - 17);
delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory)
this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_);
ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_);
ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_);
// Parse the decrypted telegram and publish sensor values.
this->parse_telegram();
this->reset_telegram_();
return;
for (auto data = this->uart_read_chunk_(); !data.empty(); data = this->uart_read_chunk_()) {
for (uint8_t byte : data) {
if (this->buffer_pos_ >= this->buffer_.size()) { // Reset buffer if overflow
ESP_LOGW(TAG, "Encrypted buffer overflow, resetting");
this->buffer_pos_ = 0;
}
this->buffer_[this->buffer_pos_] = byte;
this->buffer_pos_++;
}
this->last_read_time_ = millis();
}
// Detect inter-frame delay. If no byte is received for more than receive_timeout, then the packet is complete.
if (millis() - this->last_read_time_ > this->receive_timeout_ && this->buffer_pos_ > 0) {
ESP_LOGV(TAG, "Encrypted telegram received (%zu bytes)", this->buffer_pos_);
const auto telegram = this->dlms_decryptor_.decrypt_inplace({this->buffer_.data(), this->buffer_pos_});
// Reset buffer position for the next packet
this->buffer_pos_ = 0;
this->last_read_time_ = 0;
if (!telegram) { // decryption failed
return;
}
// Parse and publish the telegram
this->parse_telegram_(telegram.value());
}
}
bool Dsmr::parse_telegram() {
MyData data;
ESP_LOGV(TAG, "Trying to parse telegram");
bool Dsmr::parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram) {
this->stop_requesting_data_();
const auto &res = dsmr_parser::P1Parser::parse(
data, this->telegram_, this->bytes_read_, false,
this->crc_check_); // Parse telegram according to data definition. Ignore unknown values.
if (res.err) {
// Parsing error, show it
auto err_str = res.fullError(this->telegram_, this->telegram_ + this->bytes_read_);
ESP_LOGE(TAG, "%s", err_str.c_str());
return false;
} else {
this->status_clear_warning();
this->publish_sensors(data);
ESP_LOGV(TAG, "Trying to parse telegram (%zu bytes)", telegram.content().size());
ESP_LOGVV(TAG, "Telegram content:\n %.*s", static_cast<int>(telegram.content().size()), telegram.content().data());
// publish the telegram, after publishing the sensors so it can also trigger action based on latest values
if (this->s_telegram_ != nullptr) {
this->s_telegram_->publish_state(this->telegram_, this->bytes_read_);
}
return true;
MyData data;
if (const bool res = dsmr_parser::DsmrParser::parse(data, telegram); !res) {
ESP_LOGE(TAG, "Failed to parse telegram");
return false;
}
this->status_clear_warning();
this->publish_sensors(data);
// Publish the telegram, after publishing the sensors so it can also trigger action based on latest values
if (this->s_telegram_ != nullptr) {
this->s_telegram_->publish_state(telegram.content().data(), telegram.content().size());
}
return true;
}
void Dsmr::dump_config() {
ESP_LOGCONFIG(TAG,
"DSMR:\n"
" Max telegram length: %d\n"
" Max telegram length: %zu\n"
" Receive timeout: %.1fs",
this->max_telegram_len_, this->receive_timeout_ / 1e3f);
this->buffer_.size(), this->receive_timeout_ / 1e3f);
if (this->request_pin_ != nullptr) {
LOG_PIN(" Request Pin: ", this->request_pin_);
}
@@ -324,30 +192,37 @@ void Dsmr::dump_config() {
DSMR_TEXT_SENSOR_LIST(DSMR_LOG_TEXT_SENSOR, )
}
void Dsmr::set_decryption_key(const char *decryption_key) {
void Dsmr::set_decryption_key_(const char *decryption_key) {
if (decryption_key == nullptr || decryption_key[0] == '\0') {
ESP_LOGI(TAG, "Disabling decryption");
this->decryption_key_.clear();
if (this->crypt_telegram_ != nullptr) {
delete[] this->crypt_telegram_;
this->crypt_telegram_ = nullptr;
}
this->encryption_enabled_ = false;
return;
}
if (!parse_hex(decryption_key, this->decryption_key_, 16)) {
ESP_LOGE(TAG, "Error, decryption key must be 32 hex characters");
this->decryption_key_.clear();
auto key = dsmr_parser::Aes128GcmDecryptionKey::from_hex(decryption_key);
if (!key) {
ESP_LOGE(TAG, "Error, decryption key has incorrect format");
this->encryption_enabled_ = false;
return;
}
ESP_LOGI(TAG, "Decryption key is set");
// Verbose level prints decryption key
ESP_LOGV(TAG, "Using decryption key: %s", decryption_key);
if (this->crypt_telegram_ == nullptr) {
this->crypt_telegram_ = new uint8_t[this->max_telegram_len_]; // NOLINT
this->gcm_decryptor_.set_encryption_key(key.value());
this->encryption_enabled_ = true;
}
std::span<uint8_t> Dsmr::uart_read_chunk_() {
const auto avail = this->available();
if (avail == 0) {
return {};
}
size_t to_read = std::min(avail, uart_chunk_reading_buf_.size());
if (!this->read_array(uart_chunk_reading_buf_.data(), to_read)) {
return {};
}
return {uart_chunk_reading_buf_.data(), to_read};
}
} // namespace esphome::dsmr
#endif
+69 -63
View File
@@ -1,31 +1,46 @@
#pragma once
// Ignore Zephyr. It doesn't have any encryption library.
#if defined(USE_ESP32) || defined(USE_ARDUINO) || defined(USE_HOST)
#include "esphome/core/component.h"
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/uart/uart.h"
#include "esphome/core/log.h"
#include <dsmr_parser/dlms_packet_decryptor.h>
#include <dsmr_parser/fields.h>
#include <dsmr_parser/packet_accumulator.h>
#include <dsmr_parser/parser.h>
#include <array>
#include <span>
#include <vector>
#if __has_include(<psa/crypto.h>)
#include <dsmr_parser/decryption/aes128gcm_tfpsa.h>
#elif __has_include(<mbedtls/gcm.h>)
#if __has_include(<mbedtls/esp_config.h>)
#include <mbedtls/esp_config.h>
#endif
#include <dsmr_parser/decryption/aes128gcm_mbedtls.h>
#elif __has_include(<bearssl/bearssl.h>)
#include <dsmr_parser/decryption/aes128gcm_bearssl.h>
#else
#error "The platform doesn't provide a compatible encryption library for dsmr_parser"
#endif
namespace esphome::dsmr {
using namespace dsmr_parser::fields;
// DSMR_**_LIST generated by ESPHome and written in esphome/core/defines
#if !defined(DSMR_SENSOR_LIST) && !defined(DSMR_TEXT_SENSOR_LIST)
// Neither set, set it to a dummy value to not break build
#define DSMR_TEXT_SENSOR_LIST(F, SEP) F(identification)
#endif
#if defined(DSMR_SENSOR_LIST) && defined(DSMR_TEXT_SENSOR_LIST)
#define DSMR_BOTH ,
#if __has_include(<psa/crypto.h>)
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmTfPsa;
#elif __has_include(<mbedtls/gcm.h>)
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmMbedTls;
#else
#define DSMR_BOTH
using Aes128GcmDecryptorImpl = dsmr_parser::Aes128GcmBearSsl;
#endif
using namespace dsmr_parser::fields;
#ifndef DSMR_SENSOR_LIST
#define DSMR_SENSOR_LIST(F, SEP)
#endif
@@ -34,21 +49,33 @@ using namespace dsmr_parser::fields;
#define DSMR_TEXT_SENSOR_LIST(F, SEP)
#endif
#define DSMR_DATA_SENSOR(s) s
#define DSMR_IDENTITY(s) s
#define DSMR_COMMA ,
#define DSMR_PREPEND_COMMA(...) __VA_OPT__(, ) __VA_ARGS__
using MyData = dsmr_parser::ParsedData<DSMR_TEXT_SENSOR_LIST(DSMR_DATA_SENSOR, DSMR_COMMA)
DSMR_BOTH DSMR_SENSOR_LIST(DSMR_DATA_SENSOR, DSMR_COMMA)>;
#ifdef DSMR_TEXT_SENSOR_LIST_DEFINED
using MyData = dsmr_parser::ParsedData<DSMR_TEXT_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA)
DSMR_PREPEND_COMMA(DSMR_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA))>;
#else
using MyData = dsmr_parser::ParsedData<DSMR_SENSOR_LIST(DSMR_IDENTITY, DSMR_COMMA)>;
#endif
class Dsmr : public Component, public uart::UARTDevice {
public:
Dsmr(uart::UARTComponent *uart, bool crc_check) : uart::UARTDevice(uart), crc_check_(crc_check) {}
Dsmr(uart::UARTComponent *uart, bool crc_check, size_t max_telegram_length, uint32_t request_interval,
uint32_t receive_timeout, GPIOPin *request_pin, const char *decryption_key)
: uart::UARTDevice(uart),
request_interval_(request_interval),
receive_timeout_(receive_timeout),
request_pin_(request_pin),
buffer_(max_telegram_length),
packet_accumulator_(buffer_, crc_check) {
this->set_decryption_key_(decryption_key);
}
void setup() override;
void loop() override;
bool parse_telegram();
void publish_sensors(MyData &data) {
#define DSMR_PUBLISH_SENSOR(s) \
if (data.s##_present && this->s_##s##_ != nullptr) \
@@ -57,20 +84,15 @@ class Dsmr : public Component, public uart::UARTDevice {
#define DSMR_PUBLISH_TEXT_SENSOR(s) \
if (data.s##_present && this->s_##s##_ != nullptr) \
s_##s##_->publish_state(data.s.c_str());
s_##s##_->publish_state(data.s.data(), data.s.size());
DSMR_TEXT_SENSOR_LIST(DSMR_PUBLISH_TEXT_SENSOR, )
};
void dump_config() override;
void set_decryption_key(const char *decryption_key);
// Remove before 2026.8.0
ESPDEPRECATED("Pass .c_str() - e.g. set_decryption_key(key.c_str()). Removed in 2026.8.0", "2026.2.0")
void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key(decryption_key.c_str()); }
void set_max_telegram_length(size_t length) { this->max_telegram_len_ = length; }
void set_request_pin(GPIOPin *request_pin) { this->request_pin_ = request_pin; }
void set_request_interval(uint32_t interval) { this->request_interval_ = interval; }
void set_receive_timeout(uint32_t timeout) { this->receive_timeout_ = timeout; }
ESPDEPRECATED("Use 'decryption_key' configuration parameter. This method will be removed in 2026.8.0", "2026.2.0")
void set_decryption_key(const std::string &decryption_key) { this->set_decryption_key_(decryption_key.c_str()); }
// Sensor setters
#define DSMR_SET_SENSOR(s) \
@@ -85,56 +107,40 @@ class Dsmr : public Component, public uart::UARTDevice {
void set_telegram(text_sensor::TextSensor *sensor) { s_telegram_ = sensor; }
protected:
void set_decryption_key_(const char *decryption_key);
void receive_telegram_();
void receive_encrypted_telegram_();
void reset_telegram_();
void drain_rx_buffer_();
void flush_rx_buffer_();
/// Wait for UART data to become available within the read timeout.
///
/// The smart meter might provide data in chunks, causing available() to
/// return 0. When we're already reading a telegram, then we don't return
/// right away (to handle further data in an upcoming loop) but wait a
/// little while using this method to see if more data are incoming.
/// By not returning, we prevent other components from taking so much
/// time that the UART RX buffer overflows and bytes of the telegram get
/// lost in the process.
bool available_within_timeout_();
// Request telegram
uint32_t request_interval_;
bool request_interval_reached_();
GPIOPin *request_pin_{nullptr};
uint32_t last_request_time_{0};
bool requesting_data_{false};
bool parse_telegram_(const dsmr_parser::DsmrUnencryptedTelegram &telegram);
bool request_interval_reached_() const;
bool ready_to_request_data_();
void start_requesting_data_();
void stop_requesting_data_();
std::span<uint8_t> uart_read_chunk_();
// Read telegram
// Config
uint32_t request_interval_;
uint32_t receive_timeout_;
bool receive_timeout_reached_();
size_t max_telegram_len_;
char *telegram_{nullptr};
size_t bytes_read_{0};
uint8_t *crypt_telegram_{nullptr};
size_t crypt_telegram_len_{0};
size_t crypt_bytes_read_{0};
uint32_t last_read_time_{0};
bool header_found_{false};
bool footer_found_{false};
// handled outside dsmr
GPIOPin *request_pin_{nullptr};
text_sensor::TextSensor *s_telegram_{nullptr};
// Sensor member pointers
#define DSMR_DECLARE_SENSOR(s) sensor::Sensor *s_##s##_{nullptr};
DSMR_SENSOR_LIST(DSMR_DECLARE_SENSOR, )
#define DSMR_DECLARE_TEXT_SENSOR(s) text_sensor::TextSensor *s_##s##_{nullptr};
DSMR_TEXT_SENSOR_LIST(DSMR_DECLARE_TEXT_SENSOR, )
std::vector<uint8_t> decryption_key_{};
bool crc_check_;
// State
uint32_t last_request_time_{0};
uint32_t last_read_time_{0};
bool requesting_data_{false};
bool encryption_enabled_{false};
size_t buffer_pos_{0};
std::vector<uint8_t> buffer_;
dsmr_parser::PacketAccumulator packet_accumulator_;
Aes128GcmDecryptorImpl gcm_decryptor_;
dsmr_parser::DlmsPacketDecryptor dlms_decryptor_{gcm_decryptor_};
std::array<uint8_t, 256> uart_chunk_reading_buf_;
};
} // namespace esphome::dsmr
#endif
+81
View File
@@ -10,6 +10,7 @@ from esphome.const import (
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_POWER,
DEVICE_CLASS_POWER_FACTOR,
DEVICE_CLASS_REACTIVE_POWER,
DEVICE_CLASS_VOLTAGE,
DEVICE_CLASS_WATER,
@@ -119,6 +120,42 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff1_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff2_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_delivered_tariff3_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff1_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff2_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("energy_returned_tariff3_il"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT_HOURS,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("total_imported_energy"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOVOLT_AMPS_REACTIVE_HOURS,
accuracy_decimals=3,
@@ -511,6 +548,12 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_GAS,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("gas_delivered_gj"): sensor.sensor_schema(
unit_of_measurement=UNIT_GIGA_JOULE,
accuracy_decimals=3,
device_class=DEVICE_CLASS_ENERGY,
state_class=STATE_CLASS_TOTAL_INCREASING,
),
cv.Optional("water_delivered"): sensor.sensor_schema(
unit_of_measurement=UNIT_CUBIC_METER,
accuracy_decimals=3,
@@ -614,6 +657,12 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_demand_net"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("active_demand_abs"): sensor.sensor_schema(
unit_of_measurement=UNIT_KILOWATT,
accuracy_decimals=3,
@@ -728,6 +777,37 @@ CONFIG_SCHEMA = cv.Schema(
device_class=DEVICE_CLASS_POWER,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor_l1"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor_l2"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("power_factor_l3"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("min_power_factor"): sensor.sensor_schema(
accuracy_decimals=3,
device_class=DEVICE_CLASS_POWER_FACTOR,
state_class=STATE_CLASS_MEASUREMENT,
),
cv.Optional("period_3_for_instantaneous_values"): sensor.sensor_schema(
unit_of_measurement=UNIT_SECOND,
accuracy_decimals=0,
device_class=DEVICE_CLASS_DURATION,
state_class=STATE_CLASS_MEASUREMENT,
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -746,6 +826,7 @@ async def to_code(config):
sensors.append(f"F({key})")
if sensors:
cg.add_define("DSMR_SENSOR_LIST_DEFINED")
cg.add_define(
"DSMR_SENSOR_LIST(F, sep)", cg.RawExpression(" sep ".join(sensors))
)
+3
View File
@@ -15,7 +15,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional("p1_version_be"): text_sensor.text_sensor_schema(),
cv.Optional("timestamp"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_tariff"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_tariff_il"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_failure_log"): text_sensor.text_sensor_schema(),
cv.Optional("electricity_failure_log_il"): text_sensor.text_sensor_schema(),
cv.Optional("message_short"): text_sensor.text_sensor_schema(),
cv.Optional("message_long"): text_sensor.text_sensor_schema(),
cv.Optional("equipment_id"): text_sensor.text_sensor_schema(),
@@ -52,6 +54,7 @@ async def to_code(config):
text_sensors.append(f"F({key})")
if text_sensors:
cg.add_define("DSMR_TEXT_SENSOR_LIST_DEFINED")
cg.add_define(
"DSMR_TEXT_SENSOR_LIST(F, sep)",
cg.RawExpression(" sep ".join(text_sensors)),
@@ -0,0 +1,97 @@
#include "epaper_spi_ssd1683.h"
#include <algorithm>
#include "esphome/core/log.h"
namespace esphome::epaper_spi {
static constexpr const char *const TAG = "epaper_spi.mono";
void EPaperSSD1683::refresh_screen(bool partial) {
ESP_LOGV(TAG, "Refresh screen");
this->cmd_data(0x3C, {partial ? (uint8_t) 0x80 : (uint8_t) 0x01});
// On partial update, set red RAM to inverse to remove BW ghosting
this->cmd_data(0x21, {partial ? (uint8_t) 0x80 : (uint8_t) 0x40, (uint8_t) 0x00});
// Set full update to 0xD7 for fast update, 0xF7 for normal
// Fast update flashes less and draws sooner but is in busy state for the same amount of time
// Manufacturer recommends not using fast update all the time, TODO expose this to the user
this->cmd_data(0x22, {partial ? (uint8_t) 0xFC : (uint8_t) 0xF7});
this->command(0x20);
}
// Puts the display into deep sleep mode 1, only way to get out is to reset the display
// Mode 1 retains RAM while sleeping, necessary for future partial and window updates
void EPaperSSD1683::deep_sleep() {
if (this->is_using_partial_update_()) {
ESP_LOGV(TAG, "Deep sleep mode 1");
this->cmd_data(0x10, {0x01}); // deep sleep, retain RAM
} else {
ESP_LOGV(TAG, "Deep sleep mode 2");
this->cmd_data(0x10, {0x03}); // deep sleep, lose RAM
}
}
void EPaperSSD1683::set_window() {
// if not using partial update, the display will go into deep sleep mode 2, so must rewrite entire
// buffer since the display RAM will not retain contents
if (!this->is_using_partial_update_()) {
this->x_low_ = 0;
this->x_high_ = this->width_;
this->y_low_ = 0;
this->y_high_ = this->height_;
}
// round x-coordinates to byte boundaries
this->x_low_ /= 8;
this->x_high_ += 7;
this->x_high_ /= 8;
this->cmd_data(0x44, {(uint8_t) this->x_low_, (uint8_t) (this->x_high_ - 1)});
this->cmd_data(0x45, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256), (uint8_t) (this->y_high_ - 1),
(uint8_t) ((this->y_high_ - 1) / 256)});
this->cmd_data(0x4E, {(uint8_t) this->x_low_});
this->cmd_data(0x4F, {(uint8_t) this->y_low_, (uint8_t) (this->y_low_ / 256)});
}
bool HOT EPaperSSD1683::transfer_data() {
auto start_time = millis();
if (this->current_data_index_ == 0) {
if (this->send_red_) {
// round to byte boundaries
this->set_window();
}
// for monochrome, we need to send red on every refresh to prevent dirty pixels
// when doing a partial refresh
this->command(this->send_red_ ? 0x26 : 0x24);
this->current_data_index_ = this->y_low_; // actually current line
}
size_t row_length = this->x_high_ - this->x_low_;
FixedVector<uint8_t> bytes_to_send{};
bytes_to_send.init(row_length);
ESP_LOGV(TAG, "Writing %u bytes at line %zu at %ums", row_length, this->current_data_index_, (unsigned) millis());
this->start_data_();
while (this->current_data_index_ != this->y_high_) {
size_t data_idx = this->current_data_index_ * this->row_width_ + this->x_low_;
for (size_t i = 0; i != row_length; i++) {
bytes_to_send[i] = this->buffer_[data_idx++];
}
++this->current_data_index_;
this->write_array(&bytes_to_send.front(), row_length); // NOLINT
if (millis() - start_time > MAX_TRANSFER_TIME) {
// Let the main loop run and come back next loop
this->disable();
return false;
}
}
this->disable();
this->current_data_index_ = 0;
if (this->send_red_) {
this->send_red_ = false;
return false;
}
this->send_red_ = true;
return true;
}
} // namespace esphome::epaper_spi
@@ -0,0 +1,22 @@
#pragma once
#include "epaper_spi_mono.h"
namespace esphome::epaper_spi {
/**
* A class for Solomon SSD1683 epaper displays.
*/
class EPaperSSD1683 : public EPaperMono {
public:
EPaperSSD1683(const char *name, uint16_t width, uint16_t height, const uint8_t *init_sequence,
size_t init_sequence_length)
: EPaperMono(name, width, height, init_sequence, init_sequence_length) {}
protected:
void refresh_screen(bool partial) override;
void deep_sleep() override;
void set_window() override;
bool transfer_data() override;
};
} // namespace esphome::epaper_spi
@@ -0,0 +1,27 @@
from esphome.const import CONF_DATA_RATE
from . import EpaperModel
class SSD1683(EpaperModel):
def __init__(self, name, class_name="EPaperSSD1683", data_rate="20MHz", **defaults):
defaults[CONF_DATA_RATE] = data_rate
super().__init__(name, class_name, **defaults)
# fmt: off
def get_init_sequence(self, config: dict):
_width, height = self.get_dimensions(config)
return (
(0x01, (height - 1) % 256, (height - 1) // 256, 0x00), # Set column gate limit
(0x18, 0x80), # Select internal Temp sensor
(0x11, 0x03), # Set transform
)
ssd1683 = SSD1683("ssd1683")
goodisplay_gdey042t81 = ssd1683.extend(
"goodisplay-gdey042t81-4.2",
width=400,
height=300,
)
+88 -20
View File
@@ -33,6 +33,7 @@ from esphome.const import (
CONF_TYPE,
CONF_VARIANT,
CONF_VERSION,
CONF_WATCHDOG_TIMEOUT,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_NAME,
@@ -128,23 +129,30 @@ ASSERTION_LEVELS = {
SIGNING_SCHEMES = {
"rsa3072": "CONFIG_SECURE_SIGNED_APPS_RSA_SCHEME",
"ecdsa256": "CONFIG_SECURE_SIGNED_APPS_ECDSA_V2_SCHEME",
"ecdsa_v1": "CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME",
}
# Chip variants that only support one signing scheme for Secure Boot V2.
# Chip variants that only support one V2 signing scheme.
# Based on SOC_SECURE_BOOT_V2_RSA / SOC_SECURE_BOOT_V2_ECC in soc_caps.h.
# Variants not listed in either set support both RSA and ECDSA
# Variants not listed in either set support both RSA and ECDSA V2
# (e.g. C5, C6, H2, P4). New variants should be added to the
# appropriate set if they only support one scheme.
SIGNED_OTA_RSA_ONLY_VARIANTS = {
VARIANT_ESP32,
# Note: VARIANT_ESP32 is not listed here because it supports V2 RSA only
# when minimum_chip_revision >= 3.0, which requires special handling.
SIGNED_OTA_V2_RSA_ONLY_VARIANTS = {
VARIANT_ESP32S2,
VARIANT_ESP32S3,
VARIANT_ESP32C3,
}
SIGNED_OTA_ECC_ONLY_VARIANTS = {
SIGNED_OTA_V2_ECC_ONLY_VARIANTS = {
VARIANT_ESP32C2,
VARIANT_ESP32C61,
}
# V1 ECDSA (Secure Boot V1) is only supported on the original ESP32.
# Based on SOC_SECURE_BOOT_V1 in soc_caps.h.
SIGNED_OTA_V1_ECDSA_VARIANTS = {
VARIANT_ESP32,
}
COMPILER_OPTIMIZATIONS = {
"DEBUG": "CONFIG_COMPILER_OPTIMIZATION_DEBUG",
@@ -991,25 +999,73 @@ def final_validate(config):
if signed_ota := advanced.get(CONF_SIGNED_OTA_VERIFICATION):
scheme = signed_ota[CONF_SIGNING_SCHEME]
variant = config[CONF_VARIANT]
scheme_variant_conflicts = {
"ecdsa256": (SIGNED_OTA_RSA_ONLY_VARIANTS, "rsa3072"),
"rsa3072": (SIGNED_OTA_ECC_ONLY_VARIANTS, "ecdsa256"),
}
if (conflict := scheme_variant_conflicts.get(scheme)) and variant in conflict[
0
]:
min_rev = advanced.get(CONF_MINIMUM_CHIP_REVISION)
scheme_path = [
CONF_FRAMEWORK,
CONF_ADVANCED,
CONF_SIGNED_OTA_VERIFICATION,
CONF_SIGNING_SCHEME,
]
# V1 ECDSA is only available on the original ESP32
if scheme == "ecdsa_v1" and variant not in SIGNED_OTA_V1_ECDSA_VARIANTS:
errs.append(
cv.Invalid(
f"Signing scheme '{scheme}' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
path=[
CONF_FRAMEWORK,
CONF_ADVANCED,
CONF_SIGNED_OTA_VERIFICATION,
CONF_SIGNING_SCHEME,
],
f"Signing scheme 'ecdsa_v1' is only supported on "
f"{VARIANT_FRIENDLY[VARIANT_ESP32]}. "
f"Use 'rsa3072' or 'ecdsa256' instead.",
path=scheme_path,
)
)
elif variant == VARIANT_ESP32:
# On ESP32, V2 RSA requires minimum_chip_revision >= 3.0
# Note: string comparison works here because cv.one_of constrains
# min_rev to known ESP32_CHIP_REVISIONS values ("0.0".."3.1").
if scheme == "rsa3072" and (min_rev is None or min_rev < "3.0"):
errs.append(
cv.Invalid(
f"Signing scheme 'rsa3072' on {VARIANT_FRIENDLY[variant]} "
f"requires minimum_chip_revision: '3.0' or higher "
f"(Secure Boot V2 RSA needs chip revision 3.0+). "
f"For older chip revisions, use 'ecdsa_v1' instead.",
path=scheme_path,
)
)
# ESP32 does not support V2 ECDSA (no SOC_SECURE_BOOT_V2_ECC)
elif scheme == "ecdsa256":
errs.append(
cv.Invalid(
f"Signing scheme 'ecdsa256' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use 'rsa3072' (with "
f"minimum_chip_revision: '3.0') or 'ecdsa_v1' instead.",
path=scheme_path,
)
)
# V1 on rev 3.0+ -- suggest V2 RSA for stronger security
elif scheme == "ecdsa_v1" and min_rev is not None and min_rev >= "3.0":
_LOGGER.info(
"Using Secure Boot V1 ECDSA on %s rev %s. "
"Consider using 'rsa3072' (Secure Boot V2 RSA) for "
"stronger security on chip revision 3.0+.",
VARIANT_FRIENDLY[variant],
min_rev,
)
else:
# Non-ESP32 variants: check V2 scheme-variant compatibility
scheme_variant_conflicts = {
"ecdsa256": (SIGNED_OTA_V2_RSA_ONLY_VARIANTS, "rsa3072"),
"rsa3072": (SIGNED_OTA_V2_ECC_ONLY_VARIANTS, "ecdsa256"),
}
if (
conflict := scheme_variant_conflicts.get(scheme)
) and variant in conflict[0]:
errs.append(
cv.Invalid(
f"Signing scheme '{scheme}' is not supported on "
f"{VARIANT_FRIENDLY[variant]}. Use '{conflict[1]}' instead.",
path=scheme_path,
)
)
if CONF_OTA not in full_config:
_LOGGER.warning(
"Signed OTA verification is enabled but no OTA component is configured. "
@@ -1452,6 +1508,10 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
cv.Optional(CONF_WATCHDOG_TIMEOUT, default="5s"): cv.All(
cv.positive_time_period_seconds,
cv.Range(min=cv.TimePeriod(seconds=5), max=cv.TimePeriod(seconds=60)),
),
}
),
_detect_variant,
@@ -1669,6 +1729,10 @@ async def to_code(config):
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
if use_platformio:
cg.add_platformio_option("framework", "espidf")
# Strip volatile build path/time metadata from PlatformIO-managed
# ESP-IDF builds so equivalent projects can produce reproducible
# outputs and downstream tooling can safely reuse artifacts.
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
# Wrap std::__throw_* functions to abort immediately, eliminating ~3KB of
# exception class overhead. See throw_stubs.cpp for implementation.
@@ -1819,6 +1883,10 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_PANIC", True)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0", False)
add_idf_sdkconfig_option("CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1", False)
add_idf_sdkconfig_option(
"CONFIG_ESP_TASK_WDT_TIMEOUT_S",
config[CONF_WATCHDOG_TIMEOUT].total_seconds,
)
# Disable dynamic log level control to save memory
add_idf_sdkconfig_option("CONFIG_LOG_DYNAMIC_LEVEL_CONTROL", False)
+20 -1
View File
@@ -23,7 +23,26 @@ extern "C" __attribute__((weak)) void initArduino() {}
namespace esphome {
void HOT yield() { vPortYield(); }
uint32_t IRAM_ATTR HOT millis() { return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time())); }
// 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(); }
+9 -3
View File
@@ -172,10 +172,16 @@ def validate_gpio_pin(pin):
exc,
)
else:
# Throw an exception if used for a pin that would not have resulted
# in a validation error anyway!
# `ignore_pin_validation_error` only suppresses an error raised by the
# variant's pin_validation above (e.g. SPI flash/PSRAM pins, invalid pin
# numbers). If that didn't raise, the option is a no-op -- warn so the
# user can clean it up, but don't block the build.
if ignore_pin_validation_warning:
raise cv.Invalid(f"GPIO{pin[CONF_NUMBER]} is not a reserved pin")
_LOGGER.warning(
"GPIO%d has no validation errors to ignore; "
"remove `ignore_pin_validation_error: true` from this pin.",
pin[CONF_NUMBER],
)
return pin
+120 -3
View File
@@ -5,6 +5,7 @@ import json # noqa: E402
import os # noqa: E402
import pathlib # noqa: E402
import shutil # noqa: E402
import subprocess # noqa: E402
from glob import glob # noqa: E402
@@ -25,6 +26,114 @@ def _parse_sdkconfig(sdkconfig_path):
return options
def _generate_v1_verification_key(env):
"""Generate the V1 ECDSA verification key binary and assembly source file.
Secure Boot V1 embeds the public verification key directly in the app binary
as a compiled object (via a .S assembly file). The ESP-IDF CMake build generates
these files via custom commands, but PlatformIO's SCons bridge does not execute
them. This function replicates that logic:
1. Extracts the raw public key from the PEM signing key using espsecure.
2. Generates the .S assembly source that embeds the key bytes.
"""
build_dir = pathlib.Path(env.subst("$BUILD_DIR"))
project_dir = pathlib.Path(env.subst("$PROJECT_DIR"))
pioenv = env.subst("$PIOENV")
sdkconfig = _parse_sdkconfig(project_dir / f"sdkconfig.{pioenv}")
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") != "y":
return
bin_path = build_dir / "signature_verification_key.bin"
asm_path = build_dir / "signature_verification_key.bin.S"
# Determine the source of the verification key
if sdkconfig.get("CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES") == "y":
# Extract public key from the signing key
signing_key = sdkconfig.get("CONFIG_SECURE_BOOT_SIGNING_KEY")
if not signing_key:
return
signing_key_path = pathlib.Path(signing_key)
if not signing_key_path.exists():
print(f"Error: V1 ECDSA signing key not found: {signing_key_path}")
env.Exit(1)
return
if not bin_path.exists() or bin_path.stat().st_mtime < signing_key_path.stat().st_mtime:
python_exe = env.subst("$PYTHONEXE")
result = subprocess.run(
[python_exe, "-m", "espsecure", "extract_public_key",
"--keyfile", str(signing_key_path), str(bin_path)],
capture_output=True, text=True,
)
if result.returncode != 0:
print(f"Error extracting V1 verification key: {result.stderr}")
env.Exit(1)
return
print(f"Extracted V1 ECDSA verification key from {signing_key_path.name}")
else:
# User-provided verification key -- should already be a raw binary file
verification_key = sdkconfig.get("CONFIG_SECURE_BOOT_VERIFICATION_KEY")
if not verification_key:
return
verification_key_path = pathlib.Path(verification_key)
if not verification_key_path.exists():
print(f"Error: Verification key not found: {verification_key_path}")
env.Exit(1)
return
shutil.copyfile(str(verification_key_path), str(bin_path))
if not bin_path.exists():
return
# Generate the .S assembly file from the binary key data.
# Replicates ESP-IDF's data_file_embed_asm.cmake with RENAME_TO=signature_verification_key_bin.
# The file is needed in both the app build dir and the bootloader build dir, since
# the bootloader also embeds the verification key when CONFIG_SECURE_SIGNED_ON_BOOT_NO_SECURE_BOOT
# is enabled. PlatformIO's SCons bridge does not execute the CMake custom commands that
# normally generate these files.
data = bin_path.read_bytes()
varname = "signature_verification_key_bin"
lines = []
lines.append(f"/* Data converted from {bin_path.name} */")
lines.append(".data")
lines.append("#if !defined (__APPLE__) && !defined (__linux__)")
lines.append(".section .rodata.embedded")
lines.append("#endif")
lines.append(f"\n.global {varname}")
lines.append(f"{varname}:")
lines.append(f"\n.global _binary_{varname}_start")
lines.append(f"_binary_{varname}_start: /* for objcopy compatibility */")
# Format binary data as .byte lines (16 bytes per line)
for i in range(0, len(data), 16):
chunk = data[i:i + 16]
hex_bytes = ", ".join(f"0x{b:02x}" for b in chunk)
lines.append(f".byte {hex_bytes}")
lines.append(f"\n.global _binary_{varname}_end")
lines.append(f"_binary_{varname}_end: /* for objcopy compatibility */")
lines.append(f"\n.global {varname}_length")
lines.append(f"{varname}_length:")
lines.append(f".long {len(data)}")
lines.append("")
lines.append('#if defined (__linux__)')
lines.append('.section .note.GNU-stack,"",@progbits')
lines.append("#endif")
asm_content = "\n".join(lines) + "\n"
# Write to app build dir and bootloader build dir
asm_path.write_text(asm_content)
bootloader_dir = build_dir / "bootloader"
if bootloader_dir.is_dir():
bootloader_bin = bootloader_dir / "signature_verification_key.bin"
bootloader_asm = bootloader_dir / "signature_verification_key.bin.S"
shutil.copyfile(str(bin_path), str(bootloader_bin))
bootloader_asm.write_text(asm_content)
def sign_firmware(source, target, env):
"""
Sign the firmware binary using espsecure.py if signed OTA verification is enabled.
@@ -55,9 +164,12 @@ def sign_firmware(source, target, env):
env.Exit(1)
return
# ESPHome only exposes RSA3072 and ECDSA256 (both Secure Boot V2 schemes),
# so the espsecure signature version is always 2.
sign_version = "2"
# Determine espsecure signature version from the signing scheme:
# V1 ECDSA (Secure Boot V1) uses --version 1, V2 RSA/ECDSA use --version 2.
if sdkconfig.get("CONFIG_SECURE_SIGNED_APPS_ECDSA_SCHEME") == "y":
sign_version = "1"
else:
sign_version = "2"
firmware_name = os.path.basename(env.subst("$PROGNAME")) + ".bin"
firmware_path = build_dir / firmware_name
@@ -217,6 +329,11 @@ def esp32_copy_ota_bin(source, target, env):
print(f"Copied firmware to {new_file_name}")
# Generate V1 ECDSA verification key files before build starts.
# Workaround for PlatformIO not executing CMake custom commands that extract
# the public key and generate the .S assembly file for Secure Boot V1.
_generate_v1_verification_key(env) # noqa: F821
# Run signing first, then merge, then ota copy
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", sign_firmware) # noqa: F821
env.AddPostAction("$BUILD_DIR/${PROGNAME}.bin", merge_factory_bin) # noqa: F821
+20
View File
@@ -7,6 +7,7 @@ from typing import Any
from esphome import automation
import esphome.codegen as cg
from esphome.components.const import CONF_USE_PSRAM
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
from esphome.components.esp32.const import VARIANT_ESP32C2
import esphome.config_validation as cv
@@ -342,6 +343,9 @@ CONFIG_SCHEMA = cv.Schema(
cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS)
),
cv.Optional(CONF_USE_PSRAM): cv.All(
cv.only_on_esp32, cv.requires_component("psram"), cv.boolean
),
}
).extend(cv.COMPONENT_SCHEMA)
@@ -598,6 +602,22 @@ async def to_code(config):
add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
# When PSRAM and BT are used together, Bluedroid should prefer SPIRAM for
# heap allocations and use dynamic (heap-based) environment memory tables
# instead of large static DRAM arrays. This frees ~40 kB of internal RAM.
# Reference: Espressif ADF Design Considerations
# https://espressif-docs.readthedocs-hosted.com/projects/esp-adf/en/latest/
# design-guide/design-considerations.html
if config.get(CONF_USE_PSRAM, False):
cg.add_define("USE_ESP32_BLE_PSRAM")
# CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST is only available on ESP32
# (BTDM dual-mode controller). BLE-only SoCs (C3, S3, C2, H2) do not
# expose this Kconfig symbol; applying it there would cause a build error.
if get_esp32_variant() == const.VARIANT_ESP32:
add_idf_sdkconfig_option("CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST", True)
# CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY applies to all Bluedroid-enabled variants.
add_idf_sdkconfig_option("CONFIG_BT_BLE_DYNAMIC_ENV_MEMORY", True)
# Register the core BLE loggers that are always needed
register_bt_logger(BTLoggers.GAP, BTLoggers.BTM, BTLoggers.HCI)
+4 -3
View File
@@ -257,11 +257,9 @@ bool ESP32BLE::ble_setup_() {
if (this->name_ != nullptr) {
if (App.is_name_add_mac_suffix_enabled()) {
// MAC address length: 12 hex chars + null terminator
constexpr size_t mac_address_len = 13;
// MAC address suffix length (last 6 characters of 12-char MAC address string)
constexpr size_t mac_address_suffix_len = 6;
char mac_addr[mac_address_len];
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac_addr);
const char *mac_suffix_ptr = mac_addr + mac_address_suffix_len;
make_name_with_suffix_to(name_buffer, sizeof(name_buffer), this->name_, strlen(this->name_), '-', mac_suffix_ptr,
@@ -667,6 +665,9 @@ void ESP32BLE::dump_config() {
" MAC address: %s\n"
" IO Capability: %s",
mac_s, io_capability_s);
#ifdef USE_ESP32_BLE_PSRAM
ESP_LOGCONFIG(TAG, " PSRAM BLE allocation: enabled");
#endif
#ifdef ESPHOME_ESP32_BLE_EXTENDED_AUTH_PARAMS
const char *auth_req_mode_s = "<default>";
+7 -3
View File
@@ -150,10 +150,14 @@ async def to_code(config: ConfigType) -> None:
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_port(config[CONF_PORT]))
# Password could be set to an empty string and we can assume that means no password
if config.get(CONF_PASSWORD):
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
# Compile the auth path whenever `password:` is present in YAML, even if empty.
# An empty password opts in to the auth code path so set_auth_password() can be
# called at runtime (e.g. to rotate the password from a lambda). When `password:`
# is omitted entirely, the auth path is excluded to save flash on small devices.
if CONF_PASSWORD in config:
cg.add_define("USE_OTA_PASSWORD")
if config[CONF_PASSWORD]:
cg.add(var.set_auth_password(config[CONF_PASSWORD]))
cg.add_define("USE_OTA_VERSION", config[CONF_VERSION])
# Build flag so lwip_fast_select.c (a .c file that can't include defines.h) sees it.
cg.add_build_flag("-DUSE_OTA_PLATFORM_ESPHOME")
@@ -28,6 +28,14 @@ class ESPHomeOTAComponent final : public ota::OTAComponent {
};
#ifdef USE_OTA_PASSWORD
void set_auth_password(const std::string &password) { password_ = password; }
#else
// Stub so lambdas referencing set_auth_password() produce a clear error instead of
// a cryptic "no member" diagnostic. Only fires if the stub is actually instantiated.
template<bool B = false> void set_auth_password(const std::string &) {
static_assert(B, "set_auth_password() requires the OTA auth path to be compiled. "
"Add 'password: \"\"' (empty string) to your 'ota: - platform: esphome' "
"config to enable runtime password rotation.");
}
#endif // USE_OTA_PASSWORD
/// Manually set the port OTA should listen on
@@ -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 -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;
@@ -22,7 +22,7 @@ void HttpRequestComponent::dump_config() {
}
std::string HttpContainer::get_response_header(const std::string &header_name) {
auto lower = str_lower_case(header_name);
auto lower = str_lower_case(header_name); // NOLINT
for (const auto &entry : this->response_headers_) {
if (entry.name == lower) {
ESP_LOGD(TAG, "Header with name %s found with value %s", lower.c_str(), entry.value.c_str());
@@ -11,6 +11,7 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/alloc_helpers.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -400,7 +401,7 @@ class HttpRequestComponent : public Component {
std::vector<std::string> lower;
lower.reserve(collect_headers.size());
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h));
lower.push_back(str_lower_case(h)); // NOLINT
}
return this->perform(url, method, body, request_headers, lower);
}
@@ -415,7 +416,7 @@ class HttpRequestComponent : public Component {
std::vector<std::string> lower;
lower.reserve(collect_headers.size());
for (const auto &h : collect_headers) {
lower.push_back(str_lower_case(h));
lower.push_back(str_lower_case(h)); // NOLINT
}
return this->perform(url, method, body, std::vector<Header>(request_headers.begin(), request_headers.end()), lower);
}
@@ -161,7 +161,7 @@ std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &ur
container->response_headers_.clear();
auto header_count = container->client_.headers();
for (int i = 0; i < header_count; i++) {
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str());
const std::string header_name = str_lower_case(container->client_.headerName(i).c_str()); // NOLINT
if (should_collect_header(lower_case_collect_headers, header_name)) {
std::string header_value = container->client_.header(i).c_str();
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
@@ -115,7 +115,7 @@ std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url,
container->content_length = container->response_body_.size();
for (auto header : response.headers) {
ESP_LOGD(TAG, "Header: %s: %s", header.first.c_str(), header.second.c_str());
auto lower_name = str_lower_case(header.first);
auto lower_name = str_lower_case(header.first); // NOLINT
if (should_collect_header(lower_case_collect_headers, lower_name)) {
container->response_headers_.push_back({lower_name, header.second});
}
@@ -38,7 +38,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) {
switch (evt->event_id) {
case HTTP_EVENT_ON_HEADER: {
const std::string header_name = str_lower_case(evt->header_key);
const std::string header_name = str_lower_case(evt->header_key); // NOLINT
if (should_collect_header(user_data->lower_case_collect_headers, header_name)) {
const std::string header_value = evt->header_value;
ESP_LOGD(TAG, "Received response header, name: %s, value: %s", header_name.c_str(), header_value.c_str());
@@ -33,13 +33,16 @@ AUTO_LOAD = ["audio"]
CODEOWNERS = ["@jesserockz", "@kahrendt"]
DEPENDENCIES = ["i2s_audio"]
I2SAudioSpeaker = i2s_audio_ns.class_(
"I2SAudioSpeaker", cg.Component, speaker.Speaker, I2SAudioOut
I2SAudioSpeakerBase = i2s_audio_ns.class_(
"I2SAudioSpeakerBase", cg.Component, speaker.Speaker, I2SAudioOut
)
I2SAudioSpeaker = i2s_audio_ns.class_("I2SAudioSpeaker", I2SAudioSpeakerBase)
CONF_DAC_TYPE = "dac_type"
CONF_I2S_COMM_FMT = "i2s_comm_fmt"
I2SCommFmt = i2s_audio_ns.enum("I2SCommFmt", is_class=True)
i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t")
INTERNAL_DAC_OPTIONS = {
CONF_LEFT: i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN,
@@ -183,11 +186,11 @@ async def to_code(config):
await speaker.register_speaker(var, config)
cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN]))
fmt = "std" # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long
fmt = I2SCommFmt.STANDARD # equals stand_i2s, stand_pcm_long, i2s_msb, pcm_long
if config[CONF_I2S_COMM_FMT] in ["stand_msb", "i2s_lsb"]:
fmt = "msb"
fmt = I2SCommFmt.MSB
elif config[CONF_I2S_COMM_FMT] in ["stand_pcm_short", "pcm_short", "pcm"]:
fmt = "pcm"
fmt = I2SCommFmt.PCM
cg.add(var.set_i2s_comm_fmt(fmt))
if config[CONF_TIMEOUT] != CONF_NEVER:
cg.add(var.set_timeout(config[CONF_TIMEOUT]))
@@ -13,36 +13,10 @@
#include "esp_timer.h"
namespace esphome {
namespace i2s_audio {
static const uint32_t DMA_BUFFER_DURATION_MS = 15;
static const size_t DMA_BUFFERS_COUNT = 4;
static const size_t TASK_STACK_SIZE = 4096;
static const ssize_t TASK_PRIORITY = 19;
static const size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1;
namespace esphome::i2s_audio {
static const char *const TAG = "i2s_audio.speaker";
enum SpeakerEventGroupBits : uint32_t {
COMMAND_START = (1 << 0), // indicates loop should start speaker task
COMMAND_STOP = (1 << 1), // stops the speaker task
COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written
TASK_STARTING = (1 << 10),
TASK_RUNNING = (1 << 11),
TASK_STOPPING = (1 << 12),
TASK_STOPPED = (1 << 13),
ERR_ESP_NO_MEM = (1 << 19),
WARN_DROPPED_EVENT = (1 << 20),
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
// Lists the Q15 fixed point scaling factor for volume reduction.
// Has 100 values representing silence and a reduction [49, 48.5, ... 0.5, 0] dB.
// dB to PCM scaling factor formula: floating_point_scale_factor = 2^(-db/6.014)
@@ -56,17 +30,21 @@ static const std::vector<int16_t> Q15_VOLUME_SCALING_FACTORS = {
8218, 8706, 9222, 9770, 10349, 10963, 11613, 12302, 13032, 13805, 14624, 15491, 16410, 17384, 18415,
19508, 20665, 21891, 23189, 24565, 26022, 27566, 29201, 30933, 32767};
void I2SAudioSpeaker::setup() {
void I2SAudioSpeakerBase::setup() {
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
ESP_LOGE(TAG, "Event group creation failed");
this->mark_failed();
return;
}
// Initialize volume control. When audio_dac is configured, this sets the DAC volume.
// When no audio_dac is configured, this initializes software volume control.
this->set_volume(this->volume_);
}
void I2SAudioSpeaker::dump_config() {
void I2SAudioSpeakerBase::dump_config() {
ESP_LOGCONFIG(TAG,
"Speaker:\n"
" Pin: %d\n"
@@ -75,10 +53,9 @@ void I2SAudioSpeaker::dump_config() {
if (this->timeout_.has_value()) {
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value());
}
ESP_LOGCONFIG(TAG, " Communication format: %s", this->i2s_comm_fmt_.c_str());
}
void I2SAudioSpeaker::loop() {
void I2SAudioSpeakerBase::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if ((event_group_bits & SpeakerEventGroupBits::COMMAND_START) && (this->state_ == speaker::STATE_STOPPED)) {
@@ -92,12 +69,12 @@ void I2SAudioSpeaker::loop() {
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING);
}
if (event_group_bits & SpeakerEventGroupBits::TASK_RUNNING) {
ESP_LOGD(TAG, "Started");
ESP_LOGV(TAG, "Started");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
this->state_ = speaker::STATE_RUNNING;
}
if (event_group_bits & SpeakerEventGroupBits::TASK_STOPPING) {
ESP_LOGD(TAG, "Stopping");
ESP_LOGV(TAG, "Stopping");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
this->state_ = speaker::STATE_STOPPING;
}
@@ -111,10 +88,12 @@ void I2SAudioSpeaker::loop() {
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS);
this->status_clear_error();
this->on_task_stopped();
this->state_ = speaker::STATE_STOPPED;
}
// Log any errors encounted by the task
// Log any errors encountered by the task
if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NO_MEM) {
ESP_LOGE(TAG, "Not enough memory");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
@@ -133,14 +112,14 @@ void I2SAudioSpeaker::loop() {
break;
}
if (this->start_i2s_driver_(this->audio_stream_info_) != ESP_OK) {
if (this->start_i2s_driver(this->audio_stream_info_) != ESP_OK) {
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
this->status_momentary_error("driver-faiure", 1000);
this->status_momentary_error("driver-failure", 1000);
break;
}
if (this->speaker_task_handle_ == nullptr) {
xTaskCreate(I2SAudioSpeaker::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
xTaskCreate(I2SAudioSpeakerBase::speaker_task, "speaker_task", TASK_STACK_SIZE, (void *) this, TASK_PRIORITY,
&this->speaker_task_handle_);
if (this->speaker_task_handle_ == nullptr) {
@@ -157,7 +136,7 @@ void I2SAudioSpeaker::loop() {
}
}
void I2SAudioSpeaker::set_volume(float volume) {
void I2SAudioSpeakerBase::set_volume(float volume) {
this->volume_ = volume;
#ifdef USE_AUDIO_DAC
if (this->audio_dac_ != nullptr) {
@@ -166,15 +145,21 @@ void I2SAudioSpeaker::set_volume(float volume) {
}
this->audio_dac_->set_volume(volume);
} else
#endif
#endif // USE_AUDIO_DAC
{
// Fallback to software volume control by using a Q15 fixed point scaling factor
ssize_t decibel_index = remap<ssize_t, float>(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1);
this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index];
// Fallback to software volume control by using a Q15 fixed point scaling factor.
// At maximum volume (1.0), set to INT16_MAX to completely bypass volume processing
// and avoid any floating-point precision issues that could cause slight volume reduction.
if (volume >= 1.0f) {
this->q15_volume_factor_ = INT16_MAX;
} else {
ssize_t decibel_index = remap<ssize_t, float>(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1);
this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index];
}
}
}
void I2SAudioSpeaker::set_mute_state(bool mute_state) {
void I2SAudioSpeakerBase::set_mute_state(bool mute_state) {
this->mute_state_ = mute_state;
#ifdef USE_AUDIO_DAC
if (this->audio_dac_) {
@@ -184,7 +169,7 @@ void I2SAudioSpeaker::set_mute_state(bool mute_state) {
this->audio_dac_->set_mute_off();
}
} else
#endif
#endif // USE_AUDIO_DAC
{
if (mute_state) {
// Fallback to software volume control and scale by 0
@@ -196,11 +181,12 @@ void I2SAudioSpeaker::set_mute_state(bool mute_state) {
}
}
size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
size_t I2SAudioSpeakerBase::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
if (this->is_failed()) {
ESP_LOGE(TAG, "Setup failed; cannot play audio");
return 0;
}
if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) {
this->start();
}
@@ -214,8 +200,8 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick
size_t bytes_written = 0;
if (this->state_ == speaker::STATE_RUNNING) {
std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
if (temp_ring_buffer.use_count() == 2) {
// Only the speaker task and this temp_ring_buffer own the ring buffer, so its safe to write to
if (temp_ring_buffer != nullptr) {
// The weak_ptr locks successfully only while the speaker task owns the ring buffer, so it is safe to write
bytes_written = temp_ring_buffer->write_without_replacement((void *) data, length, ticks_to_wait);
}
}
@@ -223,7 +209,7 @@ size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t tick
return bytes_written;
}
bool I2SAudioSpeaker::has_buffered_data() const {
bool I2SAudioSpeakerBase::has_buffered_data() const {
if (this->audio_ring_buffer_.use_count() > 0) {
std::shared_ptr<RingBuffer> temp_ring_buffer = this->audio_ring_buffer_.lock();
return temp_ring_buffer->available() > 0;
@@ -231,216 +217,27 @@ bool I2SAudioSpeaker::has_buffered_data() const {
return false;
}
void I2SAudioSpeaker::speaker_task(void *params) {
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) params;
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STARTING);
const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT;
// Ensure ring buffer duration is at least the duration of all DMA buffers
const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this_speaker->buffer_duration_ms_);
// The DMA buffers may have more bits per sample, so calculate buffer sizes based in the input audio stream info
const size_t ring_buffer_size = this_speaker->current_stream_info_.ms_to_bytes(ring_buffer_duration);
const uint32_t frames_to_fill_single_dma_buffer =
this_speaker->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS);
const size_t bytes_to_fill_single_dma_buffer =
this_speaker->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer);
bool successful_setup = false;
std::unique_ptr<audio::AudioSourceTransferBuffer> transfer_buffer =
audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer);
if (transfer_buffer != nullptr) {
std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create(ring_buffer_size);
if (temp_ring_buffer.use_count() == 1) {
transfer_buffer->set_source(temp_ring_buffer);
this_speaker->audio_ring_buffer_ = temp_ring_buffer;
successful_setup = true;
}
}
if (!successful_setup) {
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
} else {
bool stop_gracefully = false;
bool tx_dma_underflow = true;
uint32_t frames_written = 0;
uint32_t last_data_received_time = millis();
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
while (this_speaker->pause_state_ || !this_speaker->timeout_.has_value() ||
(millis() - last_data_received_time) <= this_speaker->timeout_.value()) {
uint32_t event_group_bits = xEventGroupGetBits(this_speaker->event_group_);
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
break;
}
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) {
xEventGroupClearBits(this_speaker->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY);
stop_gracefully = true;
}
if (this_speaker->audio_stream_info_ != this_speaker->current_stream_info_) {
// Audio stream info changed, stop the speaker task so it will restart with the proper settings.
break;
}
int64_t write_timestamp;
while (xQueueReceive(this_speaker->i2s_event_queue_, &write_timestamp, 0)) {
// Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes
// on the timing info via the audio_output_callback.
uint32_t frames_sent = frames_to_fill_single_dma_buffer;
if (frames_to_fill_single_dma_buffer > frames_written) {
tx_dma_underflow = true;
frames_sent = frames_written;
const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written;
write_timestamp -= this_speaker->current_stream_info_.frames_to_microseconds(frames_zeroed);
} else {
tx_dma_underflow = false;
}
frames_written -= frames_sent;
if (frames_sent > 0) {
this_speaker->audio_output_callback_(frames_sent, write_timestamp);
}
}
if (this_speaker->pause_state_) {
// Pause state is accessed atomically, so thread safe
// Delay so the task yields, then skip transferring audio data
vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
continue;
}
// Wait half the duration of the data already written to the DMA buffers for new audio data
// The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000
const uint32_t read_delay =
(this_speaker->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2;
size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay));
uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read;
if (bytes_read > 0) {
if (this_speaker->q15_volume_factor_ < INT16_MAX) {
// Apply the software volume adjustment by unpacking the sample into a Q31 fixed-point number, shifting it,
// multiplying by the volume factor, and packing the sample back into the original bytes per sample.
const size_t bytes_per_sample = this_speaker->current_stream_info_.samples_to_bytes(1);
const uint32_t len = bytes_read / bytes_per_sample;
// Use Q16 for samples with 1 or 2 bytes: shifted_sample * gain_factor is Q16 * Q15 -> Q31
int32_t shift = 15; // Q31 -> Q16
int32_t gain_factor = this_speaker->q15_volume_factor_; // Q15
if (bytes_per_sample >= 3) {
// Use Q23 for samples with 3 or 4 bytes: shifted_sample * gain_factor is Q23 * Q8 -> Q31
shift = 8; // Q31 -> Q23
gain_factor >>= 7; // Q15 -> Q8
}
for (uint32_t i = 0; i < len; ++i) {
int32_t sample =
audio::unpack_audio_sample_to_q31(&new_data[i * bytes_per_sample], bytes_per_sample); // Q31
sample >>= shift;
sample *= gain_factor; // Q31
audio::pack_q31_as_audio_sample(sample, &new_data[i * bytes_per_sample], bytes_per_sample);
}
}
#ifdef USE_ESP32_VARIANT_ESP32
// For ESP32 16-bit mono mode, adjacent samples need to be swapped.
if (this_speaker->current_stream_info_.get_channels() == 1 &&
this_speaker->current_stream_info_.get_bits_per_sample() == 16) {
int16_t *samples = reinterpret_cast<int16_t *>(new_data);
size_t sample_count = bytes_read / sizeof(int16_t);
for (size_t i = 0; i + 1 < sample_count; i += 2) {
int16_t tmp = samples[i];
samples[i] = samples[i + 1];
samples[i + 1] = tmp;
}
}
#endif
}
if (transfer_buffer->available() == 0) {
if (stop_gracefully && tx_dma_underflow) {
break;
}
vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2));
} else {
size_t bytes_written = 0;
if (tx_dma_underflow) {
// Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue so timing
// callbacks are accurate. Preload the data.
i2s_channel_disable(this_speaker->tx_handle_);
const i2s_event_callbacks_t callbacks = {
.on_sent = nullptr,
};
i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker);
i2s_channel_preload_data(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(),
transfer_buffer->available(), &bytes_written);
} else {
// Audio is already playing, use regular I2S write to add to the DMA buffers
i2s_channel_write(this_speaker->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(),
&bytes_written, DMA_BUFFER_DURATION_MS);
}
if (bytes_written > 0) {
last_data_received_time = millis();
frames_written += this_speaker->current_stream_info_.bytes_to_frames(bytes_written);
transfer_buffer->decrease_buffer_length(bytes_written);
if (tx_dma_underflow) {
tx_dma_underflow = false;
// Reset the event queue timestamps
// Enable the on_sent callback to accurately track the timestamps of played audio
// Enable the I2S channel to start sending the preloaded audio
xQueueReset(this_speaker->i2s_event_queue_);
const i2s_event_callbacks_t callbacks = {
.on_sent = i2s_on_sent_cb,
};
i2s_channel_register_event_callback(this_speaker->tx_handle_, &callbacks, this_speaker);
i2s_channel_enable(this_speaker->tx_handle_);
}
}
}
}
}
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
if (transfer_buffer != nullptr) {
transfer_buffer.reset();
}
xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::TASK_STOPPED);
while (true) {
// Continuously delay until the loop method deletes the task
vTaskDelay(pdMS_TO_TICKS(10));
}
void I2SAudioSpeakerBase::speaker_task(void *params) {
I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) params;
this_speaker->run_speaker_task();
}
void I2SAudioSpeaker::start() {
void I2SAudioSpeakerBase::start() {
if (!this->is_ready() || this->is_failed() || this->status_has_error())
return;
if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING))
return;
// Mark STARTING immediately to avoid transient STOPPED observations before loop() processes COMMAND_START.
this->state_ = speaker::STATE_STARTING;
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::COMMAND_START);
}
void I2SAudioSpeaker::stop() { this->stop_(false); }
void I2SAudioSpeakerBase::stop() { this->stop_(false); }
void I2SAudioSpeaker::finish() { this->stop_(true); }
void I2SAudioSpeakerBase::finish() { this->stop_(true); }
void I2SAudioSpeaker::stop_(bool wait_on_empty) {
void I2SAudioSpeakerBase::stop_(bool wait_on_empty) {
if (this->is_failed())
return;
if (this->state_ == speaker::STATE_STOPPED)
@@ -453,105 +250,16 @@ void I2SAudioSpeaker::stop_(bool wait_on_empty) {
}
}
esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info) {
this->current_stream_info_ = audio_stream_info; // store the stream info settings the driver will use
if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
// Can't reconfigure I2S bus, so the sample rate must match the configured value
ESP_LOGE(TAG, "Audio stream settings are not compatible with this I2S configuration");
return ESP_ERR_NOT_SUPPORTED;
}
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO &&
(i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) {
// Currently can't handle the case when the incoming audio has more bits per sample than the configured value
ESP_LOGE(TAG, "Audio streams with more bits per sample than the I2S speaker's configuration is not supported");
return ESP_ERR_NOT_SUPPORTED;
}
if (!this->parent_->try_lock()) {
ESP_LOGE(TAG, "Parent I2S bus not free");
return ESP_ERR_INVALID_STATE;
}
uint32_t dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
i2s_chan_config_t chan_cfg = {
.id = this->parent_->get_port(),
.role = this->i2s_role_,
.dma_desc_num = DMA_BUFFERS_COUNT,
.dma_frame_num = dma_buffer_length,
.auto_clear = true,
.intr_priority = 3,
};
/* Allocate a new TX channel and get the handle of this channel */
esp_err_t I2SAudioSpeakerBase::init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg,
size_t event_queue_size) {
esp_err_t err = i2s_new_channel(&chan_cfg, &this->tx_handle_, NULL);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to allocate new I2S channel");
ESP_LOGE(TAG, "I2S channel allocation failed: %s", esp_err_to_name(err));
this->parent_->unlock();
return err;
}
i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT;
#ifdef I2S_CLK_SRC_APLL
if (this->use_apll_) {
clk_src = I2S_CLK_SRC_APLL;
}
#endif
i2s_std_gpio_config_t pin_config = this->parent_->get_pin_config();
i2s_std_clk_config_t clk_cfg = {
.sample_rate_hz = audio_stream_info.get_sample_rate(),
.clk_src = clk_src,
.mclk_multiple = this->mclk_multiple_,
};
i2s_slot_mode_t slot_mode = this->slot_mode_;
i2s_std_slot_mask_t slot_mask = this->std_slot_mask_;
if (audio_stream_info.get_channels() == 1) {
slot_mode = I2S_SLOT_MODE_MONO;
} else if (audio_stream_info.get_channels() == 2) {
slot_mode = I2S_SLOT_MODE_STEREO;
slot_mask = I2S_STD_SLOT_BOTH;
}
i2s_std_slot_config_t std_slot_cfg;
if (this->i2s_comm_fmt_ == "std") {
std_slot_cfg =
I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode);
} else if (this->i2s_comm_fmt_ == "pcm") {
std_slot_cfg =
I2S_STD_PCM_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode);
} else {
std_slot_cfg =
I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode);
}
#ifdef USE_ESP32_VARIANT_ESP32
// There seems to be a bug on the ESP32 (non-variant) platform where setting the slot bit width higher then the bits
// per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems to
// make it play at the correct speed while sending more bits per slot.
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
uint32_t configured_bit_width = static_cast<uint32_t>(this->slot_bit_width_);
std_slot_cfg.ws_width = configured_bit_width;
if (configured_bit_width > 16) {
std_slot_cfg.msb_right = false;
}
}
#else
std_slot_cfg.slot_bit_width = this->slot_bit_width_;
#endif
std_slot_cfg.slot_mask = slot_mask;
pin_config.dout = this->dout_pin_;
i2s_std_config_t std_cfg = {
.clk_cfg = clk_cfg,
.slot_cfg = std_slot_cfg,
.gpio_cfg = pin_config,
};
/* Initialize the channel */
err = i2s_channel_init_std_mode(this->tx_handle_, &std_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to initialize channel");
i2s_del_channel(this->tx_handle_);
@@ -559,23 +267,34 @@ esp_err_t I2SAudioSpeaker::start_i2s_driver_(audio::AudioStreamInfo &audio_strea
this->parent_->unlock();
return err;
}
if (this->i2s_event_queue_ == nullptr) {
this->i2s_event_queue_ = xQueueCreate(I2S_EVENT_QUEUE_COUNT, sizeof(int64_t));
this->i2s_event_queue_ = xQueueCreate(event_queue_size, sizeof(int64_t));
} else {
// Reset queue to clear any stale events from previous task
xQueueReset(this->i2s_event_queue_);
}
i2s_channel_enable(this->tx_handle_);
return err;
return ESP_OK;
}
bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
void I2SAudioSpeakerBase::stop_i2s_driver_() {
if (this->tx_handle_ != nullptr) {
i2s_channel_disable(this->tx_handle_);
i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
}
this->parent_->unlock();
}
bool IRAM_ATTR I2SAudioSpeakerBase::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx) {
int64_t now = esp_timer_get_time();
BaseType_t need_yield1 = pdFALSE;
BaseType_t need_yield2 = pdFALSE;
BaseType_t need_yield3 = pdFALSE;
I2SAudioSpeaker *this_speaker = (I2SAudioSpeaker *) user_ctx;
I2SAudioSpeakerBase *this_speaker = (I2SAudioSpeakerBase *) user_ctx;
if (xQueueIsQueueFullFromISR(this_speaker->i2s_event_queue_)) {
// Queue is full, so discard the oldest event and set the warning flag to inform the user
@@ -589,14 +308,47 @@ bool IRAM_ATTR I2SAudioSpeaker::i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_eve
return need_yield1 | need_yield2 | need_yield3;
}
void I2SAudioSpeaker::stop_i2s_driver_() {
i2s_channel_disable(this->tx_handle_);
i2s_del_channel(this->tx_handle_);
this->tx_handle_ = nullptr;
this->parent_->unlock();
void I2SAudioSpeakerBase::apply_software_volume_(uint8_t *data, size_t bytes_read) {
if (this->q15_volume_factor_ >= INT16_MAX) {
return; // Max volume, no processing needed
}
const size_t bytes_per_sample = this->current_stream_info_.samples_to_bytes(1);
const uint32_t len = bytes_read / bytes_per_sample;
// Use Q16 for samples with 1 or 2 bytes: shifted_sample * gain_factor is Q16 * Q15 -> Q31
int32_t shift = 15; // Q31 -> Q16
int32_t gain_factor = this->q15_volume_factor_; // Q15
if (bytes_per_sample >= 3) {
// Use Q23 for samples with 3 or 4 bytes: shifted_sample * gain_factor is Q23 * Q8 -> Q31
shift = 8; // Q31 -> Q23
gain_factor >>= 7; // Q15 -> Q8
}
for (uint32_t i = 0; i < len; ++i) {
int32_t sample = audio::unpack_audio_sample_to_q31(&data[i * bytes_per_sample], bytes_per_sample); // Q31
sample >>= shift;
sample *= gain_factor; // Q31
audio::pack_q31_as_audio_sample(sample, &data[i * bytes_per_sample], bytes_per_sample);
}
}
} // namespace i2s_audio
} // namespace esphome
void I2SAudioSpeakerBase::swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read) {
#ifdef USE_ESP32_VARIANT_ESP32
// For ESP32 16-bit mono mode, adjacent samples need to be swapped.
if (this->current_stream_info_.get_channels() == 1 && this->current_stream_info_.get_bits_per_sample() == 16) {
int16_t *samples = reinterpret_cast<int16_t *>(data);
size_t sample_count = bytes_read / sizeof(int16_t);
for (size_t i = 0; i + 1 < sample_count; i += 2) {
int16_t tmp = samples[i];
samples[i] = samples[i + 1];
samples[i + 1] = tmp;
}
}
#endif // USE_ESP32_VARIANT_ESP32
}
} // namespace esphome::i2s_audio
#endif // USE_ESP32
@@ -16,10 +16,34 @@
#include "esphome/core/helpers.h"
#include "esphome/core/ring_buffer.h"
namespace esphome {
namespace i2s_audio {
namespace esphome::i2s_audio {
class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Component {
// Shared constants for I2S audio speaker implementations
static constexpr uint32_t DMA_BUFFER_DURATION_MS = 15;
static constexpr size_t TASK_STACK_SIZE = 4096;
static constexpr ssize_t TASK_PRIORITY = 19;
enum SpeakerEventGroupBits : uint32_t {
COMMAND_START = (1 << 0), // indicates loop should start speaker task
COMMAND_STOP = (1 << 1), // stops the speaker task
COMMAND_STOP_GRACEFULLY = (1 << 2), // Stops the speaker task once all data has been written
TASK_STARTING = (1 << 10),
TASK_RUNNING = (1 << 11),
TASK_STOPPING = (1 << 12),
TASK_STOPPED = (1 << 13),
ERR_ESP_NO_MEM = (1 << 19),
WARN_DROPPED_EVENT = (1 << 20),
ALL_BITS = 0x00FFFFFF, // All valid FreeRTOS event group bits
};
/// @brief Abstract base class for I2S audio speaker implementations.
/// Provides shared infrastructure (event groups, ring buffer, volume control, task lifecycle)
/// for derived I2S speaker classes.
class I2SAudioSpeakerBase : public I2SAudioOut, public speaker::Speaker, public Component {
public:
float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; }
@@ -30,7 +54,9 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }
void set_timeout(uint32_t ms) { this->timeout_ = ms; }
void set_dout_pin(uint8_t pin) { this->dout_pin_ = (gpio_num_t) pin; }
void set_i2s_comm_fmt(std::string mode) { this->i2s_comm_fmt_ = std::move(mode); }
/// @brief Get the I2S TX channel handle
i2s_chan_handle_t get_tx_handle() const { return this->tx_handle_; }
void start() override;
void stop() override;
@@ -63,40 +89,55 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
void set_mute_state(bool mute_state) override;
protected:
/// @brief Function for the FreeRTOS task handling audio output.
/// Allocates space for the buffers, reads audio from the ring buffer and writes audio to the I2S port. Stops
/// immmiately after receiving the COMMAND_STOP signal and stops only after the ring buffer is empty after receiving
/// the COMMAND_STOP_GRACEFULLY signal. Stops if the ring buffer hasn't read data for more than timeout_ milliseconds.
/// When stopping, it deallocates the buffers. It communicates its state and any errors via ``event_group_``.
/// @param params I2SAudioSpeaker component
/// @brief FreeRTOS task entry point. Casts params to I2SAudioSpeakerBase and calls run_speaker_task_().
/// @param params I2SAudioSpeakerBase component pointer
static void speaker_task(void *params);
/// @brief The main speaker task loop. Implemented by derived classes for mode-specific behavior.
virtual void run_speaker_task() = 0;
/// @brief Sends a stop command to the speaker task via ``event_group_``.
/// @param wait_on_empty If false, sends the COMMAND_STOP signal. If true, sends the COMMAND_STOP_GRACEFULLY signal.
void stop_(bool wait_on_empty);
/// @brief Callback function used to send playback timestamps the to the speaker task.
/// @brief Callback function used to send playback timestamps to the speaker task.
/// @param handle (i2s_chan_handle_t)
/// @param event (i2s_event_data_t)
/// @param user_ctx (void*) User context pointer that the callback accesses
/// @return True if a higher priority task was interrupted
static bool i2s_on_sent_cb(i2s_chan_handle_t handle, i2s_event_data_t *event, void *user_ctx);
/// @brief Starts the ESP32 I2S driver.
/// Attempts to lock the I2S port, starts the I2S driver using the passed in stream information, and sets the data out
/// pin. If it fails, it will unlock the I2S port and uninstalls the driver, if necessary.
/// @brief Starts the ESP32 I2S driver. Implemented by derived classes for mode-specific configuration.
/// @param audio_stream_info Stream information for the I2S driver.
/// @return ESP_ERR_NOT_ALLOWED if the I2S port can't play the incoming audio stream.
/// ESP_ERR_INVALID_STATE if the I2S port is already locked.
/// ESP_ERR_INVALID_ARG if installing the driver or setting the data outpin fails due to a parameter error.
/// ESP_ERR_NO_MEM if the driver fails to install due to a memory allocation error.
/// ESP_FAIL if setting the data out pin fails due to an IO error
/// ESP_OK if successful
esp_err_t start_i2s_driver_(audio::AudioStreamInfo &audio_stream_info);
/// @return ESP_OK if successful, or an error code
virtual esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) = 0;
/// @brief Shared I2S channel allocation, initialization, and event queue setup.
/// Called by derived start_i2s_driver_() implementations after building mode-specific configs.
/// @param chan_cfg I2S channel configuration
/// @param std_cfg I2S standard mode configuration (clock, slot, GPIO)
/// @param event_queue_size Size of the event queue
/// @return ESP_OK if successful, or an error code. On failure, cleans up channel and unlocks parent.
esp_err_t init_i2s_channel_(const i2s_chan_config_t &chan_cfg, const i2s_std_config_t &std_cfg,
size_t event_queue_size);
/// @brief Stops the I2S driver and unlocks the I2S port
void stop_i2s_driver_();
/// @brief Called in loop() when the task has stopped. Override for mode-specific cleanup.
virtual void on_task_stopped() {}
/// @brief Apply software volume control using Q15 fixed-point scaling.
/// @param data Pointer to audio sample data (modified in place)
/// @param bytes_read Number of bytes of audio data
void apply_software_volume_(uint8_t *data, size_t bytes_read);
/// @brief Swap adjacent 16-bit mono samples for ESP32 (non-variant) hardware quirk.
/// Only applies when running on original ESP32 with 16-bit mono audio.
/// @param data Pointer to audio sample data (modified in place)
/// @param bytes_read Number of bytes of audio data
void swap_esp32_mono_samples_(uint8_t *data, size_t bytes_read);
TaskHandle_t speaker_task_handle_{nullptr};
EventGroupHandle_t event_group_{nullptr};
@@ -115,11 +156,9 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
audio::AudioStreamInfo current_stream_info_; // The currently loaded driver's stream info
gpio_num_t dout_pin_;
std::string i2s_comm_fmt_;
i2s_chan_handle_t tx_handle_;
i2s_chan_handle_t tx_handle_{nullptr};
};
} // namespace i2s_audio
} // namespace esphome
} // namespace esphome::i2s_audio
#endif // USE_ESP32
@@ -0,0 +1,307 @@
#include "i2s_audio_speaker_standard.h"
#ifdef USE_ESP32
#include <driver/i2s_std.h>
#include "esphome/components/audio/audio.h"
#include "esphome/components/audio/audio_transfer_buffer.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esp_timer.h"
namespace esphome::i2s_audio {
static const char *const TAG = "i2s_audio.speaker.std";
static constexpr size_t DMA_BUFFERS_COUNT = 4;
static constexpr size_t I2S_EVENT_QUEUE_COUNT = DMA_BUFFERS_COUNT + 1;
void I2SAudioSpeaker::dump_config() {
I2SAudioSpeakerBase::dump_config();
const char *fmt_str;
switch (this->i2s_comm_fmt_) {
case I2SCommFmt::PCM:
fmt_str = "pcm";
break;
case I2SCommFmt::MSB:
fmt_str = "msb";
break;
default:
fmt_str = "std";
break;
}
ESP_LOGCONFIG(TAG, " Communication format: %s", fmt_str);
}
void I2SAudioSpeaker::run_speaker_task() {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STARTING);
const uint32_t dma_buffers_duration_ms = DMA_BUFFER_DURATION_MS * DMA_BUFFERS_COUNT;
// Ensure ring buffer duration is at least the duration of all DMA buffers
const uint32_t ring_buffer_duration = std::max(dma_buffers_duration_ms, this->buffer_duration_ms_);
// The DMA buffers may have more bits per sample, so calculate buffer sizes based on the input audio stream info
const size_t ring_buffer_size = this->current_stream_info_.ms_to_bytes(ring_buffer_duration);
const uint32_t frames_to_fill_single_dma_buffer = this->current_stream_info_.ms_to_frames(DMA_BUFFER_DURATION_MS);
const size_t bytes_to_fill_single_dma_buffer =
this->current_stream_info_.frames_to_bytes(frames_to_fill_single_dma_buffer);
bool successful_setup = false;
std::unique_ptr<audio::AudioSourceTransferBuffer> transfer_buffer =
audio::AudioSourceTransferBuffer::create(bytes_to_fill_single_dma_buffer);
if (transfer_buffer != nullptr) {
std::shared_ptr<RingBuffer> temp_ring_buffer = RingBuffer::create(ring_buffer_size);
if (temp_ring_buffer.use_count() == 1) {
transfer_buffer->set_source(temp_ring_buffer);
this->audio_ring_buffer_ = temp_ring_buffer;
successful_setup = true;
}
}
if (!successful_setup) {
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::ERR_ESP_NO_MEM);
} else {
bool stop_gracefully = false;
bool tx_dma_underflow = true;
uint32_t frames_written = 0;
uint32_t last_data_received_time = millis();
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_RUNNING);
// Main speaker task loop. Continues while:
// - Paused, OR
// - No timeout configured, OR
// - Timeout hasn't elapsed since last data
while (this->pause_state_ || !this->timeout_.has_value() ||
(millis() - last_data_received_time) <= this->timeout_.value()) {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP) {
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP);
ESP_LOGV(TAG, "Exiting: COMMAND_STOP received");
break;
}
if (event_group_bits & SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY) {
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::COMMAND_STOP_GRACEFULLY);
stop_gracefully = true;
}
if (this->audio_stream_info_ != this->current_stream_info_) {
// Audio stream info changed, stop the speaker task so it will restart with the proper settings.
ESP_LOGV(TAG, "Exiting: stream info changed");
break;
}
int64_t write_timestamp;
while (xQueueReceive(this->i2s_event_queue_, &write_timestamp, 0)) {
// Receives timing events from the I2S on_sent callback. If actual audio data was sent in this event, it passes
// on the timing info via the audio_output_callback.
uint32_t frames_sent = frames_to_fill_single_dma_buffer;
if (frames_to_fill_single_dma_buffer > frames_written) {
tx_dma_underflow = true;
frames_sent = frames_written;
const uint32_t frames_zeroed = frames_to_fill_single_dma_buffer - frames_written;
write_timestamp -= this->current_stream_info_.frames_to_microseconds(frames_zeroed);
} else {
tx_dma_underflow = false;
}
frames_written -= frames_sent;
// Standard I2S mode: fire callback immediately for each event
if (frames_sent > 0) {
this->audio_output_callback_(frames_sent, write_timestamp);
}
}
if (this->pause_state_) {
// Pause state is accessed atomically, so thread safe
// Delay so the task yields, then skip transferring audio data
vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS));
continue;
}
// Wait half the duration of the data already written to the DMA buffers for new audio data
// The millisecond helper modifies the frames_written variable, so use the microsecond helper and divide by 1000
uint32_t read_delay = (this->current_stream_info_.frames_to_microseconds(frames_written) / 1000) / 2;
size_t bytes_read = transfer_buffer->transfer_data_from_source(pdMS_TO_TICKS(read_delay));
uint8_t *new_data = transfer_buffer->get_buffer_end() - bytes_read;
if (bytes_read > 0) {
this->apply_software_volume_(new_data, bytes_read);
this->swap_esp32_mono_samples_(new_data, bytes_read);
}
if (transfer_buffer->available() == 0) {
if (stop_gracefully && tx_dma_underflow) {
break;
}
vTaskDelay(pdMS_TO_TICKS(DMA_BUFFER_DURATION_MS / 2));
} else {
size_t bytes_written = 0;
if (tx_dma_underflow) {
// Temporarily disable channel and callback to reset the I2S driver's internal DMA buffer queue
i2s_channel_disable(this->tx_handle_);
const i2s_event_callbacks_t null_callbacks = {.on_sent = nullptr};
i2s_channel_register_event_callback(this->tx_handle_, &null_callbacks, this);
i2s_channel_preload_data(this->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(),
&bytes_written);
} else {
// Audio is already playing, use regular write to add to the DMA buffers
i2s_channel_write(this->tx_handle_, transfer_buffer->get_buffer_start(), transfer_buffer->available(),
&bytes_written, DMA_BUFFER_DURATION_MS);
}
if (bytes_written > 0) {
last_data_received_time = millis();
frames_written += this->current_stream_info_.bytes_to_frames(bytes_written);
transfer_buffer->decrease_buffer_length(bytes_written);
if (tx_dma_underflow) {
tx_dma_underflow = false;
// Enable the on_sent callback and channel after preload
xQueueReset(this->i2s_event_queue_);
const i2s_event_callbacks_t callbacks = {.on_sent = i2s_on_sent_cb};
i2s_channel_register_event_callback(this->tx_handle_, &callbacks, this);
i2s_channel_enable(this->tx_handle_);
}
}
}
}
}
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPING);
if (transfer_buffer != nullptr) {
transfer_buffer.reset();
}
xEventGroupSetBits(this->event_group_, SpeakerEventGroupBits::TASK_STOPPED);
while (true) {
// Continuously delay until the loop method deletes the task
vTaskDelay(pdMS_TO_TICKS(10));
}
}
esp_err_t I2SAudioSpeaker::start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) {
this->current_stream_info_ = audio_stream_info;
if ((this->i2s_role_ & I2S_ROLE_SLAVE) && (this->sample_rate_ != audio_stream_info.get_sample_rate())) { // NOLINT
// Can't reconfigure I2S bus, so the sample rate must match the configured value
ESP_LOGE(TAG, "Incompatible stream settings");
return ESP_ERR_NOT_SUPPORTED;
}
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO &&
(i2s_slot_bit_width_t) audio_stream_info.get_bits_per_sample() > this->slot_bit_width_) {
// Currently can't handle the case when the incoming audio has more bits per sample than the configured value
ESP_LOGE(TAG, "Stream bits per sample must be less than or equal to the speaker's configuration");
return ESP_ERR_NOT_SUPPORTED;
}
if (!this->parent_->try_lock()) {
ESP_LOGE(TAG, "Parent bus is busy");
return ESP_ERR_INVALID_STATE;
}
uint32_t dma_buffer_length = audio_stream_info.ms_to_frames(DMA_BUFFER_DURATION_MS);
i2s_role_t i2s_role = this->i2s_role_;
i2s_clock_src_t clk_src = I2S_CLK_SRC_DEFAULT;
#if SOC_CLK_APLL_SUPPORTED
if (this->use_apll_) {
clk_src = i2s_clock_src_t::I2S_CLK_SRC_APLL;
}
#endif // SOC_CLK_APLL_SUPPORTED
// Log DMA configuration for debugging
ESP_LOGV(TAG, "I2S DMA config: %zu buffers x %lu frames", (size_t) DMA_BUFFERS_COUNT,
(unsigned long) dma_buffer_length);
i2s_chan_config_t chan_cfg = {
.id = this->parent_->get_port(),
.role = i2s_role,
.dma_desc_num = DMA_BUFFERS_COUNT,
.dma_frame_num = dma_buffer_length,
.auto_clear = true,
.intr_priority = 3,
};
// Build standard I2S clock/slot/gpio configuration
i2s_std_clk_config_t clk_cfg = {
.sample_rate_hz = audio_stream_info.get_sample_rate(),
.clk_src = clk_src,
.mclk_multiple = this->mclk_multiple_,
};
i2s_slot_mode_t slot_mode = this->slot_mode_;
i2s_std_slot_mask_t slot_mask = this->std_slot_mask_;
if (audio_stream_info.get_channels() == 1) {
slot_mode = I2S_SLOT_MODE_MONO;
} else if (audio_stream_info.get_channels() == 2) {
slot_mode = I2S_SLOT_MODE_STEREO;
slot_mask = I2S_STD_SLOT_BOTH;
}
i2s_std_slot_config_t slot_cfg;
switch (this->i2s_comm_fmt_) {
case I2SCommFmt::PCM:
slot_cfg =
I2S_STD_PCM_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode);
break;
case I2SCommFmt::MSB:
slot_cfg =
I2S_STD_MSB_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(), slot_mode);
break;
default:
slot_cfg = I2S_STD_PHILIPS_SLOT_DEFAULT_CONFIG((i2s_data_bit_width_t) audio_stream_info.get_bits_per_sample(),
slot_mode);
break;
}
#ifdef USE_ESP32_VARIANT_ESP32
// There seems to be a bug on the ESP32 (non-variant) platform where setting the slot bit width higher than the
// bits per sample causes the audio to play too fast. Setting the ws_width to the configured slot bit width seems
// to make it play at the correct speed while sending more bits per slot.
if (this->slot_bit_width_ != I2S_SLOT_BIT_WIDTH_AUTO) {
uint32_t configured_bit_width = static_cast<uint32_t>(this->slot_bit_width_);
slot_cfg.ws_width = configured_bit_width;
if (configured_bit_width > 16) {
slot_cfg.msb_right = false;
}
}
#else
slot_cfg.slot_bit_width = this->slot_bit_width_;
#endif // USE_ESP32_VARIANT_ESP32
slot_cfg.slot_mask = slot_mask;
i2s_std_gpio_config_t gpio_cfg = this->parent_->get_pin_config();
gpio_cfg.dout = this->dout_pin_;
i2s_std_config_t std_cfg = {
.clk_cfg = clk_cfg,
.slot_cfg = slot_cfg,
.gpio_cfg = gpio_cfg,
};
esp_err_t err = this->init_i2s_channel_(chan_cfg, std_cfg, I2S_EVENT_QUEUE_COUNT);
if (err != ESP_OK) {
return err;
}
i2s_channel_enable(this->tx_handle_);
return ESP_OK;
}
} // namespace esphome::i2s_audio
#endif // USE_ESP32
@@ -0,0 +1,32 @@
#pragma once
#ifdef USE_ESP32
#include "i2s_audio_speaker.h"
namespace esphome::i2s_audio {
enum class I2SCommFmt : uint8_t {
STANDARD, // Philips / I2S standard
PCM, // PCM short
MSB, // MSB / left-justified
};
/// @brief Standard I2S speaker implementation.
/// Outputs PCM audio data directly to an I2S DAC using the standard I2S protocol.
class I2SAudioSpeaker : public I2SAudioSpeakerBase {
public:
void dump_config() override;
void set_i2s_comm_fmt(I2SCommFmt fmt) { this->i2s_comm_fmt_ = fmt; }
protected:
void run_speaker_task() override;
esp_err_t start_i2s_driver(audio::AudioStreamInfo &audio_stream_info) override;
I2SCommFmt i2s_comm_fmt_{I2SCommFmt::STANDARD};
};
} // namespace esphome::i2s_audio
#endif // USE_ESP32
+1 -1
View File
@@ -756,7 +756,7 @@ async def write_image(config, all_frames=False):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
encoder.end_image()
rhs = [HexInt(x) for x in encoder.data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
@@ -17,6 +17,7 @@ void ImprovSerialComponent::setup() {
global_improv_serial_component = this;
#ifdef USE_ESP32
this->uart_num_ = logger::global_logger->get_uart_num();
this->uart_selection_ = logger::global_logger->get_uart();
#elif defined(USE_ARDUINO)
this->hw_serial_ = logger::global_logger->get_hw_serial();
#endif
@@ -29,7 +30,8 @@ void ImprovSerialComponent::setup() {
}
void ImprovSerialComponent::loop() {
if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
const uint32_t now = App.get_loop_component_start_time();
if (this->last_read_byte_ && (now - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) {
this->last_read_byte_ = 0;
this->rx_buffer_.clear();
ESP_LOGV(TAG, "Timeout");
@@ -38,7 +40,7 @@ void ImprovSerialComponent::loop() {
auto byte = this->read_byte_();
while (byte.has_value()) {
if (this->parse_improv_serial_byte_(byte.value())) {
this->last_read_byte_ = millis();
this->last_read_byte_ = now;
} else {
this->last_read_byte_ = 0;
this->rx_buffer_.clear();
@@ -62,55 +64,6 @@ void ImprovSerialComponent::loop() {
void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); }
optional<uint8_t> ImprovSerialComponent::read_byte_() {
optional<uint8_t> byte;
uint8_t data = 0;
#ifdef USE_ESP32
switch (logger::global_logger->get_uart()) {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
case logger::UART_SELECTION_UART2:
#endif // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32C6 && !USE_ESP32_VARIANT_ESP32C61 &&
// !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3
if (this->uart_num_ >= 0) {
size_t available;
uart_get_buffered_data_len(this->uart_num_, &available);
if (available) {
uart_read_bytes(this->uart_num_, &data, 1, 0);
byte = data;
}
}
break;
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
case logger::UART_SELECTION_USB_CDC:
if (esp_usb_console_available_for_read()) {
esp_usb_console_read_buf((char *) &data, 1);
byte = data;
}
break;
#endif // USE_LOGGER_USB_CDC
#ifdef USE_LOGGER_USB_SERIAL_JTAG
case logger::UART_SELECTION_USB_SERIAL_JTAG: {
if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) {
byte = data;
}
break;
}
#endif // USE_LOGGER_USB_SERIAL_JTAG
default:
break;
}
#elif defined(USE_ARDUINO)
if (this->hw_serial_->available()) {
this->hw_serial_->readBytes(&data, 1);
byte = data;
}
#endif
return byte;
}
void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) {
// First, set length field
this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1;
@@ -134,7 +87,7 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size)
this->tx_header_[TX_CHECKSUM_IDX] = checksum;
#ifdef USE_ESP32
switch (logger::global_logger->get_uart()) {
switch (this->uart_selection_) {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
@@ -1,6 +1,7 @@
#pragma once
#include "esphome/components/improv_base/improv_base.h"
#include "esphome/components/logger/logger.h"
#include "esphome/components/wifi/wifi_component.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
@@ -66,7 +67,53 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
std::vector<uint8_t> build_rpc_settings_response_(improv::Command command);
std::vector<uint8_t> build_version_info_();
optional<uint8_t> read_byte_();
ESPHOME_ALWAYS_INLINE optional<uint8_t> read_byte_() {
optional<uint8_t> byte;
uint8_t data = 0;
#ifdef USE_ESP32
switch (this->uart_selection_) {
case logger::UART_SELECTION_UART0:
case logger::UART_SELECTION_UART1:
#if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \
!defined(USE_ESP32_VARIANT_ESP32C61) && !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3)
case logger::UART_SELECTION_UART2:
#endif
if (this->uart_num_ >= 0) {
size_t available;
uart_get_buffered_data_len(this->uart_num_, &available);
if (available) {
uart_read_bytes(this->uart_num_, &data, 1, 0);
byte = data;
}
}
break;
#if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC)
case logger::UART_SELECTION_USB_CDC:
if (esp_usb_console_available_for_read()) {
esp_usb_console_read_buf((char *) &data, 1);
byte = data;
}
break;
#endif
#ifdef USE_LOGGER_USB_SERIAL_JTAG
case logger::UART_SELECTION_USB_SERIAL_JTAG: {
if (usb_serial_jtag_read_bytes((char *) &data, 1, 0)) {
byte = data;
}
break;
}
#endif
default:
break;
}
#elif defined(USE_ARDUINO)
if (this->hw_serial_->available()) {
this->hw_serial_->readBytes(&data, 1);
byte = data;
}
#endif
return byte;
}
void write_data_(const uint8_t *data = nullptr, size_t size = 0);
uint8_t tx_header_[TX_BUFFER_SIZE] = {
@@ -86,6 +133,7 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase {
#ifdef USE_ESP32
uart_port_t uart_num_;
logger::UARTSelection uart_selection_{logger::UART_SELECTION_UART0};
#elif defined(USE_ARDUINO)
Stream *hw_serial_{nullptr};
#endif
@@ -20,8 +20,6 @@ void InternalTemperatureSensor::update() {
success = (result == 0);
#if defined(USE_LIBRETINY_VARIANT_BK7231N)
temperature = raw * -0.38f + 156.0f;
#elif defined(USE_LIBRETINY_VARIANT_BK7231T)
temperature = raw * 0.04f;
#else // USE_LIBRETINY_VARIANT
temperature = raw * 0.128f;
#endif // USE_LIBRETINY_VARIANT
+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))
+24 -18
View File
@@ -766,32 +766,38 @@ void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY
void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
void LD2412Component::set_basic_config() {
uint8_t min_gate = 1;
uint8_t max_gate = TOTAL_GATES;
uint16_t timeout = DEFAULT_PRESENCE_TIMEOUT;
uint8_t out_pin_level = 0x01;
#ifdef USE_NUMBER
if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() ||
!this->timeout_number_->has_state()) {
return;
if (this->min_distance_gate_number_ != nullptr) {
if (!this->min_distance_gate_number_->has_state())
return;
min_gate = static_cast<int>(this->min_distance_gate_number_->state);
}
if (this->max_distance_gate_number_ != nullptr) {
if (!this->max_distance_gate_number_->has_state())
return;
max_gate = static_cast<int>(this->max_distance_gate_number_->state) + 1;
}
if (this->timeout_number_ != nullptr) {
if (!this->timeout_number_->has_state())
return;
timeout = static_cast<int>(this->timeout_number_->state);
}
#endif
#ifdef USE_SELECT
if (!this->out_pin_level_select_->has_state()) {
return;
if (this->out_pin_level_select_ != nullptr) {
if (!this->out_pin_level_select_->has_state())
return;
out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
}
#endif
uint8_t value[5] = {
#ifdef USE_NUMBER
lowbyte(static_cast<int>(this->min_distance_gate_number_->state)),
lowbyte(static_cast<int>(this->max_distance_gate_number_->state) + 1),
lowbyte(static_cast<int>(this->timeout_number_->state)),
highbyte(static_cast<int>(this->timeout_number_->state)),
#else
1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0,
#endif
#ifdef USE_SELECT
find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()),
#else
0x01, // Default value if not using select
#endif
lowbyte(min_gate), lowbyte(max_gate), lowbyte(timeout), highbyte(timeout), out_pin_level,
};
this->set_config_mode_(true);
this->send_command_(CMD_BASIC_CONF, value, sizeof(value));
+7
View File
@@ -443,6 +443,13 @@ async def component_to_code(config):
# 4-8KB flash). Even if linked, it would use locks, so explicit FreeRTOS
# mutexes are simpler and equivalent.
cg.add_define(ThreadModel.MULTI_NO_ATOMICS)
# Enable FreeRTOS static allocation so FreeRTOSQueue can use
# xQueueCreateStatic (queue storage in BSS, no heap allocation).
# Also moves FreeRTOS internal structures (timer command queue) to BSS.
# BK72xx's FreeRTOSConfig.h doesn't define this, defaulting to 0.
# The -D wins over the #ifndef default in FreeRTOS.h.
# Not enabled on RTL87xx/LN882x — costs more heap than it saves there.
cg.add_build_flag("-DconfigSUPPORT_STATIC_ALLOCATION=1")
# RTL8710B needs FreeRTOS 8.2.3+ for xTaskNotifyGive/ulTaskNotifyTake
# required by AsyncTCP 3.4.3+ (https://github.com/esphome/esphome/issues/10220)
+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,
+23 -2
View File
@@ -16,8 +16,29 @@ 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(); }
uint64_t millis_64() { return Millis64Impl::compute(::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); }
@@ -35,7 +56,7 @@ void arch_init() {
//
// 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 yield_with_select_() and
// 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");
@@ -0,0 +1,52 @@
/*
* FreeRTOS static allocation callbacks for LibreTiny platforms.
*
* Required when configSUPPORT_STATIC_ALLOCATION is enabled. These callbacks
* provide memory for the idle and timer tasks. Following ESP-IDF's approach,
* we allocate from the FreeRTOS heap (pvPortMalloc) rather than using truly
* static buffers, to avoid assumptions about memory layout.
*
* This enables xQueueCreateStatic, xTaskCreateStatic, etc. throughout ESPHome,
* allowing queue storage to live in BSS with zero runtime heap allocation.
*/
#ifdef USE_BK72XX
#include <FreeRTOS.h>
#include <task.h>
#if (configSUPPORT_STATIC_ALLOCATION == 1)
void vApplicationGetIdleTaskMemory(StaticTask_t **ppxIdleTaskTCBBuffer, StackType_t **ppxIdleTaskStackBuffer,
uint32_t *pulIdleTaskStackSize) {
/* Stack grows down on ARM — allocate stack first, then TCB,
* so the stack does not grow into the TCB. */
StackType_t *stack = (StackType_t *) pvPortMalloc(configMINIMAL_STACK_SIZE * sizeof(StackType_t));
StaticTask_t *tcb = (StaticTask_t *) pvPortMalloc(sizeof(StaticTask_t));
configASSERT(stack != NULL);
configASSERT(tcb != NULL);
*ppxIdleTaskTCBBuffer = tcb;
*ppxIdleTaskStackBuffer = stack;
*pulIdleTaskStackSize = configMINIMAL_STACK_SIZE;
}
#if (configUSE_TIMERS == 1)
void vApplicationGetTimerTaskMemory(StaticTask_t **ppxTimerTaskTCBBuffer, StackType_t **ppxTimerTaskStackBuffer,
uint32_t *pulTimerTaskStackSize) {
StackType_t *stack = (StackType_t *) pvPortMalloc(configTIMER_TASK_STACK_DEPTH * sizeof(StackType_t));
StaticTask_t *tcb = (StaticTask_t *) pvPortMalloc(sizeof(StaticTask_t));
configASSERT(stack != NULL);
configASSERT(tcb != NULL);
*ppxTimerTaskTCBBuffer = tcb;
*ppxTimerTaskStackBuffer = stack;
*pulTimerTaskStackSize = configTIMER_TASK_STACK_DEPTH;
}
#endif /* configUSE_TIMERS */
#endif /* configSUPPORT_STATIC_ALLOCATION */
#endif /* USE_BK72XX */
+39 -24
View File
@@ -10,13 +10,10 @@ namespace esphome::light {
static const char *const TAG = "light";
// Helper functions to reduce code size for logging
static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f,
float max = 1.0f) {
if (value < min || value > max) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max);
value = clamp(value, min, max);
}
// Cold-path logger; caller handles the clamp so the in-range hot path avoids
// the spill/reload around the call.
static void log_value_out_of_range(const char *name, float value, const LogString *param_name, float min, float max) {
ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max);
}
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
@@ -57,6 +54,12 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
PROGMEM_STRING_TABLE(ColorModeHumanStrings, "Unknown", "On/Off", "Brightness", "White", "Color temperature",
"Cold/warm white", "RGB", "RGBW", "RGB + color temperature", "RGB + cold/warm white");
// Indices 0-7 match FieldFlags bits 0-7; index 8 is color_temperature.
// PROGMEM_STRING_TABLE is constexpr-init (no RAM guard variable).
PROGMEM_STRING_TABLE(ValidateFieldNames, "Brightness", "Color brightness", "Red", "Green", "Blue", "White",
"Cold white", "Warm white", "Color temperature");
static constexpr uint8_t VALIDATE_CT_INDEX = 8;
static const LogString *color_mode_to_human(ColorMode color_mode) {
return ColorModeHumanStrings::get_log_str(ColorModeBitPolicy::to_bit(color_mode), 0);
}
@@ -277,25 +280,37 @@ LightColorValues LightCall::validate_() {
if (this->has_state())
v.set_state(this->state_);
// clamp_and_log_if_invalid already clamps in-place, so assign directly
// to avoid redundant clamp code from the setter being inlined.
#define VALIDATE_AND_APPLY(field, name_str, ...) \
if (this->has_##field()) { \
clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
v.field##_ = this->field##_; \
// FieldFlags bits 0-7 must match unit_fields_ array indices.
static_assert(FLAG_HAS_BRIGHTNESS == 1u << 0 && FLAG_HAS_COLOR_BRIGHTNESS == 1u << 1 && FLAG_HAS_RED == 1u << 2 &&
FLAG_HAS_GREEN == 1u << 3 && FLAG_HAS_BLUE == 1u << 4 && FLAG_HAS_WHITE == 1u << 5 &&
FLAG_HAS_COLD_WHITE == 1u << 6 && FLAG_HAS_WARM_WHITE == 1u << 7,
"FieldFlags bits 0-7 must match unit_fields_ indices");
// Iterate set bits only (ctz + clear-lowest) — HA can drive perform()
// at high frequency so the hot path is O(popcount).
unsigned active = this->flags_ & CLAMP_FLAGS_MASK;
while (active != 0) {
unsigned bit = __builtin_ctz(active);
active &= active - 1; // clear lowest set bit
float &value = this->unit_fields_[bit];
if (float_out_of_unit_range(value)) {
log_value_out_of_range(name, value, ValidateFieldNames::get_log_str(bit, 0), 0.0f, 1.0f);
value = clamp_unit_float(value);
}
v.unit_fields_[bit] = value;
}
VALIDATE_AND_APPLY(brightness, "Brightness")
VALIDATE_AND_APPLY(color_brightness, "Color brightness")
VALIDATE_AND_APPLY(red, "Red")
VALIDATE_AND_APPLY(green, "Green")
VALIDATE_AND_APPLY(blue, "Blue")
VALIDATE_AND_APPLY(white, "White")
VALIDATE_AND_APPLY(cold_white, "Cold white")
VALIDATE_AND_APPLY(warm_white, "Warm white")
VALIDATE_AND_APPLY(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
#undef VALIDATE_AND_APPLY
// color_temperature: runtime range from traits.
if (this->has_color_temperature()) {
const float ct_min = traits.get_min_mireds();
const float ct_max = traits.get_max_mireds();
if (this->color_temperature_ < ct_min || this->color_temperature_ > ct_max) {
log_value_out_of_range(name, this->color_temperature_, ValidateFieldNames::get_log_str(VALIDATE_CT_INDEX, 0),
ct_min, ct_max);
this->color_temperature_ = clamp(this->color_temperature_, ct_min, ct_max);
}
v.color_temperature_ = this->color_temperature_;
}
v.normalize_color();
+18 -25
View File
@@ -195,25 +195,26 @@ class LightCall {
/// Some color modes also can be set using non-native parameters, transform those calls.
void transform_parameters_(const LightTraits &traits);
// Bitfield flags - each flag indicates whether a corresponding value has been set.
// Bits 0-7 index unit_fields_[] in validate_(); don't reorder (asserts in light_call.cpp).
enum FieldFlags : uint16_t {
FLAG_HAS_STATE = 1 << 0,
FLAG_HAS_TRANSITION = 1 << 1,
FLAG_HAS_FLASH = 1 << 2,
FLAG_HAS_EFFECT = 1 << 3,
FLAG_HAS_BRIGHTNESS = 1 << 4,
FLAG_HAS_COLOR_BRIGHTNESS = 1 << 5,
FLAG_HAS_RED = 1 << 6,
FLAG_HAS_GREEN = 1 << 7,
FLAG_HAS_BLUE = 1 << 8,
FLAG_HAS_WHITE = 1 << 9,
FLAG_HAS_COLOR_TEMPERATURE = 1 << 10,
FLAG_HAS_COLD_WHITE = 1 << 11,
FLAG_HAS_WARM_WHITE = 1 << 12,
FLAG_HAS_BRIGHTNESS = 1 << 0,
FLAG_HAS_COLOR_BRIGHTNESS = 1 << 1,
FLAG_HAS_RED = 1 << 2,
FLAG_HAS_GREEN = 1 << 3,
FLAG_HAS_BLUE = 1 << 4,
FLAG_HAS_WHITE = 1 << 5,
FLAG_HAS_COLD_WHITE = 1 << 6,
FLAG_HAS_WARM_WHITE = 1 << 7,
FLAG_HAS_COLOR_TEMPERATURE = 1 << 8,
FLAG_HAS_STATE = 1 << 9,
FLAG_HAS_TRANSITION = 1 << 10,
FLAG_HAS_FLASH = 1 << 11,
FLAG_HAS_EFFECT = 1 << 12,
FLAG_HAS_COLOR_MODE = 1 << 13,
FLAG_PUBLISH = 1 << 14,
FLAG_SAVE = 1 << 15,
};
static constexpr uint16_t CLAMP_FLAGS_MASK = 0x00FFu; // bits 0-7
inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
@@ -222,7 +223,7 @@ class LightCall {
inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
// Helper to set flag - defaults to true for common case
void set_flag_(FieldFlags flag, bool value = true) {
void set_flag_(FieldFlags flag, bool value = true) ESPHOME_ALWAYS_INLINE {
if (value) {
this->flags_ |= flag;
} else {
@@ -231,7 +232,7 @@ class LightCall {
}
// Helper to clear flag - reduces code size for common case
void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; }
void clear_flag_(FieldFlags flag) ESPHOME_ALWAYS_INLINE { this->flags_ &= ~flag; }
// Helper to log unsupported feature and clear flag - reduces code duplication
void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log);
@@ -239,19 +240,11 @@ class LightCall {
LightState *parent_;
// Light state values - use flags_ to check if a value has been set.
// Group 4-byte aligned members first
uint32_t transition_length_;
uint32_t flash_length_;
uint32_t effect_;
float brightness_;
float color_brightness_;
float red_;
float green_;
float blue_;
float white_;
ESPHOME_LIGHT_UNIT_FIELDS_UNION();
float color_temperature_;
float cold_white_;
float warm_white_;
// Smaller members at the end for better packing
uint16_t flags_{FLAG_PUBLISH | FLAG_SAVE}; // Tracks which values are set
+62 -18
View File
@@ -3,11 +3,62 @@
#include "esphome/core/helpers.h"
#include "color_mode.h"
#include <cmath>
#include <cstdint>
#include <limits>
namespace esphome::light {
inline static uint8_t to_uint8_scale(float x) { return static_cast<uint8_t>(roundf(x * 255.0f)); }
// IEEE 754 bit patterns. Values in [0.0f, 1.0f] have bits <= ONE_F_BITS;
// negatives have the sign bit set (→ huge unsigned). A single unsigned compare
// replaces two soft-float __ltsf2/__gtsf2 calls on ESP8266.
static constexpr uint32_t ONE_F_BITS = 0x3F800000u; // 1.0f
static constexpr uint32_t NEG_ZERO_F_BITS = 0x80000000u; // -0.0f / sign-bit mask
static_assert(sizeof(float) == sizeof(uint32_t), "float must be 32-bit");
static_assert(std::numeric_limits<float>::is_iec559, "IEEE 754 float required");
// Union pun — memcpy/bit_cast don't fold on xtensa-gcc (see api/proto.h).
// -0.0f is numerically zero so it's reported in range (no warning, no clamp).
inline bool float_out_of_unit_range(float x) {
union {
float f;
uint32_t u;
} pun;
pun.f = x;
return pun.u > ONE_F_BITS && pun.u != NEG_ZERO_F_BITS;
}
// Clamps to [0.0f, 1.0f] without float compares. Out of range: sign bit set
// (negatives, -NaN, -Inf) → 0.0f; sign bit clear (>1, +NaN, +Inf) → 1.0f.
inline float clamp_unit_float(float x) {
union {
float f;
uint32_t u;
} pun;
pun.f = x;
if (pun.u <= ONE_F_BITS)
return x;
return (pun.u & NEG_ZERO_F_BITS) ? 0.0f : 1.0f; // sign bit → negative → clamp to 0
}
// Shared anonymous union: eight unit-range floats alias unit_fields_[8] so
// LightCall::validate_() can iterate them as a real array. GCC/Clang ext.
#define ESPHOME_LIGHT_UNIT_FIELDS_UNION() \
union { \
struct { \
float brightness_; \
float color_brightness_; \
float red_; \
float green_; \
float blue_; \
float white_; \
float cold_white_; \
float warm_white_; \
}; \
float unit_fields_[8]; \
}
/** This class represents the color state for a light object.
*
* The representation of the color state is dependent on the active color mode. A color mode consists of multiple
@@ -52,9 +103,9 @@ class LightColorValues {
green_(1.0f),
blue_(1.0f),
white_(1.0f),
color_temperature_{0.0f},
cold_white_{1.0f},
warm_white_{1.0f},
color_temperature_{0.0f},
color_mode_(ColorMode::UNKNOWN) {}
LightColorValues(ColorMode color_mode, float state, float brightness, float color_brightness, float red, float green,
@@ -220,39 +271,39 @@ class LightColorValues {
/// Get the binary true/false state of these light color values.
bool is_on() const { return this->get_state() != 0.0f; }
/// Set the state of these light color values. In range from 0.0 (off) to 1.0 (on)
void set_state(float state) { this->state_ = clamp(state, 0.0f, 1.0f); }
void set_state(float state) { this->state_ = clamp_unit_float(state); }
/// Set the state of these light color values as a binary true/false.
void set_state(bool state) { this->state_ = state ? 1.0f : 0.0f; }
/// Get the brightness property of these light color values. In range 0.0 to 1.0
float get_brightness() const { return this->brightness_; }
/// Set the brightness property of these light color values. In range 0.0 to 1.0
void set_brightness(float brightness) { this->brightness_ = clamp(brightness, 0.0f, 1.0f); }
void set_brightness(float brightness) { this->brightness_ = clamp_unit_float(brightness); }
/// Get the color brightness property of these light color values. In range 0.0 to 1.0
float get_color_brightness() const { return this->color_brightness_; }
/// Set the color brightness property of these light color values. In range 0.0 to 1.0
void set_color_brightness(float brightness) { this->color_brightness_ = clamp(brightness, 0.0f, 1.0f); }
void set_color_brightness(float brightness) { this->color_brightness_ = clamp_unit_float(brightness); }
/// Get the red property of these light color values. In range 0.0 to 1.0
float get_red() const { return this->red_; }
/// Set the red property of these light color values. In range 0.0 to 1.0
void set_red(float red) { this->red_ = clamp(red, 0.0f, 1.0f); }
void set_red(float red) { this->red_ = clamp_unit_float(red); }
/// Get the green property of these light color values. In range 0.0 to 1.0
float get_green() const { return this->green_; }
/// Set the green property of these light color values. In range 0.0 to 1.0
void set_green(float green) { this->green_ = clamp(green, 0.0f, 1.0f); }
void set_green(float green) { this->green_ = clamp_unit_float(green); }
/// Get the blue property of these light color values. In range 0.0 to 1.0
float get_blue() const { return this->blue_; }
/// Set the blue property of these light color values. In range 0.0 to 1.0
void set_blue(float blue) { this->blue_ = clamp(blue, 0.0f, 1.0f); }
void set_blue(float blue) { this->blue_ = clamp_unit_float(blue); }
/// Get the white property of these light color values. In range 0.0 to 1.0
float get_white() const { return white_; }
/// Set the white property of these light color values. In range 0.0 to 1.0
void set_white(float white) { this->white_ = clamp(white, 0.0f, 1.0f); }
void set_white(float white) { this->white_ = clamp_unit_float(white); }
/// Get the color temperature property of these light color values in mired.
float get_color_temperature() const { return this->color_temperature_; }
@@ -277,26 +328,19 @@ class LightColorValues {
/// Get the cold white property of these light color values. In range 0.0 to 1.0.
float get_cold_white() const { return this->cold_white_; }
/// Set the cold white property of these light color values. In range 0.0 to 1.0.
void set_cold_white(float cold_white) { this->cold_white_ = clamp(cold_white, 0.0f, 1.0f); }
void set_cold_white(float cold_white) { this->cold_white_ = clamp_unit_float(cold_white); }
/// Get the warm white property of these light color values. In range 0.0 to 1.0.
float get_warm_white() const { return this->warm_white_; }
/// Set the warm white property of these light color values. In range 0.0 to 1.0.
void set_warm_white(float warm_white) { this->warm_white_ = clamp(warm_white, 0.0f, 1.0f); }
void set_warm_white(float warm_white) { this->warm_white_ = clamp_unit_float(warm_white); }
friend class LightCall;
protected:
float state_; ///< ON / OFF, float for transition
float brightness_;
float color_brightness_;
float red_;
float green_;
float blue_;
float white_;
ESPHOME_LIGHT_UNIT_FIELDS_UNION();
float color_temperature_; ///< Color Temperature in Mired
float cold_white_;
float warm_white_;
ColorMode color_mode_;
};
+2
View File
@@ -35,9 +35,11 @@ LockStateForwarder = lock_ns.class_("LockStateForwarder")
LockState = lock_ns.enum("LockState")
LOCK_STATES = {
"OPEN": LockState.LOCK_STATE_OPEN,
"LOCKED": LockState.LOCK_STATE_LOCKED,
"UNLOCKED": LockState.LOCK_STATE_UNLOCKED,
"JAMMED": LockState.LOCK_STATE_JAMMED,
"OPENING": LockState.LOCK_STATE_OPENING,
"LOCKING": LockState.LOCK_STATE_LOCKING,
"UNLOCKING": LockState.LOCK_STATE_UNLOCKING,
}

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