Compare commits

...

97 Commits

Author SHA1 Message Date
J. Nick Koston 36250682b0 [core] Split hal.h into per-platform headers under core/hal/
Mirror the wake.{h,cpp} → wake/wake_<platform>.{h,cpp} decomposition
that PR #15978 did. After this change esphome/core/hal.h is a thin
dispatcher and each platform's HAL bits (IRAM_ATTR / PROGMEM macros,
in_isr_context(), the inline yield/delay/micros/millis/millis_64
wrappers, plus ESP8266's progmem_read_*) live in their own header
under esphome/core/hal/.

Scope is headers only — there is no esphome/core/hal.cpp today (every
out-of-line implementation lives in esphome/components/<platform>/core.cpp
alongside platform-specific concerns) so no new .cpp files are added
and no FILTER_SOURCE_FILES entries are needed in core/config.py.
recursive_sources=True on the core manifest already picks up the new
.h files automatically.

No public API moves, no symbol renames, no behavior change. Pure code
motion. The only observable difference is the dispatcher #errors when
no USE_* is set (today an unknown platform silently fell through to
the else branch with empty IRAM_ATTR/PROGMEM); this matches wake.h's
behavior.
2026-04-28 21:19:21 -05:00
J. Nick Koston 4f75647f63 Merge upstream/dev into inline-micros-esp32
Resolves conflict in esphome/components/esp8266/core.cpp per PR #15977 plan:
keep #15662's fast millis() accumulator and optimistic_yield delay() body;
drop the upstream wrappers for yield()/millis_64()/micros() since those
are now always-inlined in hal.h.
2026-04-28 20:54:39 -05:00
J. Nick Koston eec770d622 [core] Use ETag in external_files cache to fix re-downloads from raw.githubusercontent.com (#16020) 2026-04-29 13:52:09 +12:00
J. Nick Koston d7b21a84a3 [git] Make ref fetches and submodule updates shallow (#16014) 2026-04-29 13:49:51 +12:00
J. Nick Koston f05243bd9d [api] Add 48-bit MAC address varint fast path for BLE advertisements (#15988) 2026-04-29 13:48:35 +12:00
J. Nick Koston 35cb28edfe [output] Gate FloatOutput power scaling fields behind USE_OUTPUT_FLOAT_POWER_SCALING (#15998) 2026-04-29 13:27:22 +12:00
J. Nick Koston 1363f661e6 [core] Inline ContinuationAction in If/While/RepeatAction (#16040) 2026-04-28 21:26:25 -04:00
J. Nick Koston 8af499b591 [api] Use custom deleter to fix incomplete-type error on macOS libc++ (#16050) 2026-04-28 21:26:21 -04:00
Jonathan Swoboda 1a57d9bc2f [sprinkler][pn532] Fix bugprone-unchecked-optional-access (#16102) 2026-04-29 01:04:19 +00:00
J. Nick Koston 9768380856 [api] Hoist memw out of socket ready check to once per main-loop iter (#15996) 2026-04-29 13:04:10 +12:00
J. Nick Koston 676f26919e [mdns] Drive MDNS.update() polling from IP state events on ESP8266/RP2040 (#15961) 2026-04-29 13:02:21 +12:00
J. Nick Koston 29d3a3a498 [esp8266] Replace millis() with fast accumulator, wrap Arduino callers (#15662) 2026-04-29 12:58:00 +12:00
Jonathan Swoboda 77b76ac48a [inkbird_ibsth1_mini][speaker][speaker_source] Fix performance-unnecessary-copy-initialization (#16101) 2026-04-29 00:56:03 +00:00
Clyde Stubbs 0b5835284a [lvgl] Additional layout features (#16041) 2026-04-29 12:35:24 +12:00
Jonathan Swoboda 15df477472 [core] Reduce copies in Callback/CallbackManager call paths (#16093)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-04-28 19:41:28 -04:00
Jonathan Swoboda be0ee73847 [i2c] NOLINT readability-identifier-naming on Zephyr struct forward-decl (#16099) 2026-04-28 19:22:42 -04:00
Jonathan Swoboda a241c9e622 [online_image][sim800l] Use std::string::starts_with for prefix checks (#16097) 2026-04-28 19:02:39 -04:00
Jonathan Swoboda 2f433c78bd [haier] Brace single-statement else-if in smartair2_climate (#16098) 2026-04-28 18:56:36 -04:00
Jonathan Swoboda e39c474577 [binary_sensor] Bind at_index_ once in MultiClick on_state_ (#16095) 2026-04-28 22:13:35 +00:00
Jonathan Swoboda a62e3fe4fc [json] NOLINT StackAddressEscape false positive in parse_json (#16091) 2026-04-28 21:35:40 +00:00
Jonathan Swoboda 7d6b9bee19 [wifi] Avoid copying EAP config in three connect handlers (#16094) 2026-04-28 21:22:29 +00:00
Jonathan Swoboda ab6bda50e4 [esp32_ble] Widen loop variable in as_128bit() to match uuid_.len type (#16088) 2026-04-28 20:58:40 +00:00
Jonathan Swoboda 3d195d748c [ezo] Use make_unique to construct EzoCommand (#16092) 2026-04-28 20:50:15 +00:00
Jonathan Swoboda 16cf4fb5e8 [nextion] Use std::string::starts_with for HTTPS URL check (#16090) 2026-04-28 20:47:20 +00:00
Jonathan Swoboda 70503442f4 [dfrobot_sen0395] Brace single-statement else-if in enqueue() (#16089) 2026-04-28 20:37:29 +00:00
Jonathan Swoboda 594b269dba [bme680] Rename cal1/cal2 to coeff1/coeff2 (#16087) 2026-04-28 20:33:57 +00:00
Clyde Stubbs 8157c721a5 [mapping] Implement default value (#15861) 2026-04-29 06:31:37 +10:00
Clyde Stubbs 9af557de6d [lvgl] Add utility gradient function (#16048) 2026-04-29 06:29:38 +10:00
Jonathan Swoboda 1f4136e76f [pipsolar] Guard handle_qmod_ against empty message (#16085) 2026-04-28 16:29:09 -04:00
Jonathan Swoboda c8dffcc9b8 [tlc5971] Remove dead bit-banging delay code (#16086) 2026-04-28 15:28:33 -05:00
dependabot[bot] 44fbb7f5a9 Bump CodSpeedHQ/action from 4.14.0 to 4.15.0 (#16084)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-28 15:10:21 -05:00
Jonathan Swoboda eb01d43feb [spi][http_request][demo] Fix latent clang-tidy issues in headers (#16080) 2026-04-28 16:09:35 -04:00
Jonathan Swoboda 7891fd5cf1 Add dependencies.lock to .gitignore (#16081) 2026-04-29 07:38:31 +12:00
Jonathan Swoboda 4ee9cc432b [ci] Install requirements_dev.txt in the cached venv (#16082) 2026-04-29 07:37:46 +12:00
Mat931 42ff10afe5 [watchdog] Fix WatchdogManager on single core apps (#16074) 2026-04-28 17:32:44 +00:00
tomaszduda23 6b3df66bdc [nrf52] make reset pin optional (#11684)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-28 12:20:38 -05:00
tomaszduda23 968878a62d [nrf52] implement wake_loop_threadsafe/wakeable_delay (#16032)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-28 16:35:12 +00:00
J. Nick Koston daf3f4d2f1 [core] wakeable_delay: yield on already-woken fast path (ESP8266, RP2040) (#16045) 2026-04-28 10:41:17 -05:00
Bonne Eggleston 52e8c50f45 [modbus] Split modbus_server from modbus_controller (#15509)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-04-28 10:21:25 -05:00
J. Nick Koston 0a4d9b430f [ci] Add import-time regression check for esphome.__main__ (#15954) 2026-04-28 14:05:12 +00:00
J. Nick Koston 0759a3c681 [core] Split wake.{h,cpp} into per-platform files (#15978) 2026-04-28 08:48:13 -05:00
Egor Vorontsov 8921e3bb3f [api] add open states for lock to api.proto (#15901)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
2026-04-28 07:49:16 -05:00
Clyde Stubbs 52f80618d4 [lvgl] Allow a binary sensor to report checked or pressed state (#16073)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
2026-04-28 22:00:29 +10:00
Jesse Hills 876c8c4c2a [ci-custom] Lint imports of esphome.components.const outside components (#16068)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 05:59:02 -05:00
Darafei Praliaskouski 41458d72e0 [esp32] Make Arduino app metadata reproducible (#16053) 2026-04-28 05:58:34 -05:00
Brandon Harvey 49d3df2698 [automation] Fix codegen type for component.resume update_interval (#16069)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 05:27:20 -05:00
J. Nick Koston 792f2e8363 [ota] Add wall-clock timeout to OTA data transfer loop (#16047) 2026-04-28 17:29:42 +12:00
J. Nick Koston 42c9fdc87e [feedback] Use App.get_loop_component_start_time() and constexpr timeout id (#16063) 2026-04-28 16:39:08 +12:00
Jesse Hills 5f6bbb98ce Merge branch 'release' into dev 2026-04-28 15:40:19 +12:00
Jesse Hills 4e0509435a Merge pull request #16067 from esphome/bump-2026.4.3
2026.4.3
2026-04-28 15:39:24 +12:00
J. Nick Koston a03de7cea2 [core] Freshen loop_component_start_time_ before scheduler dispatch (#16064) 2026-04-28 13:23:08 +12:00
Jesse Hills 95b5ab7e78 Bump version to 2026.4.3 2026-04-28 12:58:29 +12:00
J. Nick Koston 3ac0939f55 [image] Fix RGB565+alpha rendering for multi-frame animations (#16017)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-28 12:58:29 +12:00
Jesse Hills 191d3bc7e4 [esp32_touch] Feed wdt (#16066) 2026-04-28 12:58:29 +12:00
Edward Firmo a186f6fea9 [nextion] Unify TFT upload ack timeout to 5000ms (#15960) 2026-04-28 12:58:29 +12:00
Mat931 aea88aef5e [esp32][wifi] Fix bootloop and WiFi connection issue if nvs partition is missing or has non-default label (#16025)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-28 12:58:29 +12:00
J. Nick Koston 433bbdb016 [rotary_encoder][at581x] Fix templatable int field types (#16015) 2026-04-28 12:58:29 +12:00
J. Nick Koston 4137d93cbf [wifi] Fix stale wifi.connected after state transition (#15966) 2026-04-28 12:58:29 +12:00
J. Nick Koston 6a5919ee87 [deep_sleep] Fix sleep_duration codegen type to uint32_t (#15965) 2026-04-28 12:58:29 +12:00
Jesse Hills b753ee4e94 [time] Handle Windows EINVAL when validating POSIX TZ strings (#15934) 2026-04-28 12:58:29 +12:00
Clyde Stubbs c26ea52620 [lvgl] Triggers on tabview tabs fix (#15935) 2026-04-28 12:58:29 +12:00
J. Nick Koston 39a69385fb [image] Fix RGB565+alpha rendering for multi-frame animations (#16017)
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-28 12:57:42 +12:00
Jesse Hills a34836c290 [esp32_touch] Feed wdt (#16066) 2026-04-28 11:27:08 +12:00
Edward Firmo 01ac223913 [nextion] Unify TFT upload ack timeout to 5000ms (#15960) 2026-04-28 08:30:40 +12:00
Mat931 7198c912c7 [esp32][wifi] Fix bootloop and WiFi connection issue if nvs partition is missing or has non-default label (#16025)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-27 12:41:28 -05:00
Kevin Ahrendt 24c6a0d711 [audio] Bump microDecoder library to v0.2.0 (#16054) 2026-04-27 12:17:02 +00:00
plazarre dec5d0449b [esp32_ble_tracker] Hold COEX_PREFER_BT for the lifetime of any active connection (#16036)
Co-authored-by: Paul Lazarre <plazarre@gmail.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
2026-04-27 06:51:54 -05:00
J. Nick Koston 79b741b8dc [core] Combine entity register + configure_entity_ into one call (#16030) 2026-04-27 03:03:39 +00:00
tomaszduda23 112646a9c4 [zigbee] add router for nrf52 (#16034) 2026-04-26 23:02:09 -04:00
J. Nick Koston 2e096bb036 [core] Combine set_component_source_ + register_component_ into one call (#16029) 2026-04-26 21:54:15 -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
J. Nick Koston 9f5121e271 [core] Suppress redundant-declaration warning for ESP32 esp_timer_get_time forward decl 2026-04-24 12:33:41 -05:00
J. Nick Koston a1e3ec7118 [core] Keep esp8266 millis/delay out-of-line for #15662 compatibility 2026-04-24 12:02:57 -05:00
J. Nick Koston d3bae21d13 [core] Extend HAL inlining to yield/delay/millis_64 + libretiny + rp2040
Extends the prior commit to cover more wrappers and platforms:

- ESP32: also inline yield() and delay()
- ESP8266: also inline yield(), delay(), millis(), millis_64()
- LibreTiny: inline yield(), delay(), micros(), per-variant millis()
  fast paths, and millis_64() (via Millis64Impl::compute, now reachable
  from hal.h since time_64.h dropped its helpers.h dep)
- RP2040: also inline yield(), delay(), micros()

Consolidates the ESP8266/LibreTiny/RP2040 Arduino-flavored ::yield /
::delay / ::micros wrappers into a single shared block in hal.h.

LibreTiny note: the prior IRAM_ATTR on the wrapper was decorative —
::micros(), ::yield(), ::delay() and xTaskGetTickCount all live in
flash on every libretiny family (realtek-amb, beken-72xx,
lightning-ln882h all checked), so an IRAM ISR call would have crashed
the same way an inlined direct call does.

Also drops the helpers.h include from time_64.h (only used for the
ESPHOME_ALWAYS_INLINE macro, replaced with the raw attribute) so
time_64.h is light enough for hal.h to include.
2026-04-24 11:51:23 -05:00
J. Nick Koston 9d138e73c9 [core] Suppress redundant-declaration warning for ESP8266 micros() forward decl 2026-04-24 11:35:03 -05:00
J. Nick Koston e23a6bf59f [core] Inline micros()/millis_64() at the HAL layer
Replaces the per-function ``__attribute__((optimize("O2")))`` approach in
#15693 with a direct inline definition of micros() / millis_64() in hal.h
for ESP32, plus inline definitions for ESP8266 micros() and RP2040
millis()/millis_64().

The original goal of #15693 was to inline micros() into the main loop so
that ``call esphome::micros() → call esp_timer_get_time()`` collapses to
a single ``call esp_timer_get_time``. That wrapper-collapse benefits
every micros() call site, not just loop_task — so the real fix is to
mark the wrapper inline, not to bump the loop's optimization level.

Doing this at the HAL layer also makes runtime_stats measurements more
accurate: each timing read no longer hides a wrapper call/return between
the component end-time capture and the underlying clock read.

Refactor: move ``micros_to_millis<>()`` from helpers.h to a new
lightweight ``time_conversion.h`` so hal.h can include it without
pulling the rest of helpers.h into every TU that includes hal.h.
2026-04-24 11:23:16 -05:00
233 changed files with 6112 additions and 1681 deletions
+33 -3
View File
@@ -39,7 +39,7 @@ jobs:
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Generate cache-key
id: cache-key
run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
run: echo key="${{ hashFiles('requirements.txt', 'requirements_dev.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
id: python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
@@ -58,7 +58,7 @@ jobs:
python -m venv venv
. venv/bin/activate
python --version
pip install -r requirements.txt -r requirements_test.txt pre-commit
pip install -r requirements.txt -r requirements_dev.txt -r requirements_test.txt pre-commit
pip install -e .
pylint:
@@ -108,6 +108,34 @@ jobs:
script/generate-esp32-boards.py --check
script/generate-rp2040-boards.py --check
import-time:
name: Check import esphome.__main__ time
runs-on: ubuntu-24.04
needs:
- common
- determine-jobs
if: needs.determine-jobs.outputs.import-time == 'true'
steps:
- name: Check out code from GitHub
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Restore Python
uses: ./.github/actions/restore-python
with:
python-version: ${{ env.DEFAULT_PYTHON }}
cache-key: ${{ needs.common.outputs.cache-key }}
- name: Check import time against budget and write waterfall HAR
run: |
. venv/bin/activate
script/check_import_time.py --check --har importtime.har
- name: Upload waterfall HAR
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: import-time-waterfall
path: importtime.har
if-no-files-found: ignore
retention-days: 14
pytest:
name: Run pytest
strategy:
@@ -176,6 +204,7 @@ jobs:
clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
python-linters: ${{ steps.determine.outputs.python-linters }}
import-time: ${{ steps.determine.outputs.import-time }}
changed-components: ${{ steps.determine.outputs.changed-components }}
changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
@@ -219,6 +248,7 @@ jobs:
echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
echo "import-time=$(echo "$output" | jq -r '.import_time')" >> $GITHUB_OUTPUT
echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
@@ -339,7 +369,7 @@ jobs:
echo "binary=$BINARY" >> $GITHUB_OUTPUT
- name: Run CodSpeed benchmarks
uses: CodSpeedHQ/action@658a901452bb54c799643e060733b7afe9121b8d # v4.14.0
uses: CodSpeedHQ/action@c381be0bfd20e844fb45594f6aa182ffcd94545c # v4.15.0
with:
run: ${{ steps.build.outputs.binary }}
mode: simulation
+1
View File
@@ -146,5 +146,6 @@ sdkconfig.*
/components
/managed_components
/dependencies.lock
api-docs/
+1
View File
@@ -347,6 +347,7 @@ esphome/components/modbus_controller/select/* @martgras @stegm
esphome/components/modbus_controller/sensor/* @martgras
esphome/components/modbus_controller/switch/* @martgras
esphome/components/modbus_controller/text_sensor/* @martgras
esphome/components/modbus_server/* @exciton
esphome/components/mopeka_ble/* @Fabian-Schmidt @spbrogan
esphome/components/mopeka_pro_check/* @spbrogan
esphome/components/mopeka_std_check/* @Fabian-Schmidt
+1 -1
View File
@@ -597,7 +597,7 @@ async def component_resume_action_to_code(
comp = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, comp)
if CONF_UPDATE_INTERVAL in config:
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, int)
template_ = await cg.templatable(config[CONF_UPDATE_INTERVAL], args, cg.uint32)
cg.add(var.set_update_interval(template_))
return var
@@ -13,7 +13,11 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@grahambrown11", "@hwstar"]
@@ -181,7 +185,7 @@ async def setup_alarm_control_panel_core_(var, config):
async def register_alarm_control_panel(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_alarm_control_panel(var))
queue_entity_register("alarm_control_panel", config)
CORE.register_platform_component("alarm_control_panel", var)
await setup_alarm_control_panel_core_(var, config)
+6 -1
View File
@@ -62,7 +62,12 @@ void Animation::set_frame(int frame) {
}
void Animation::update_data_start_() {
const uint32_t image_size = this->get_width_stride() * this->height_;
uint32_t image_size = this->get_width_stride() * this->height_;
// RGB565 with an alpha channel stores the alpha plane immediately after the RGB
// plane within each frame, so the per-frame stride includes the alpha bytes.
if (this->type_ == image::IMAGE_TYPE_RGB565 && this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) {
image_size += static_cast<uint32_t>(this->width_) * this->height_;
}
this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_;
}
+12 -1
View File
@@ -1025,6 +1025,13 @@ message CameraImageRequest {
bool stream = 2;
}
// ==================== TEMPERATURE UNIT ====================
enum TemperatureUnit {
TEMPERATURE_UNIT_CELSIUS = 0;
TEMPERATURE_UNIT_FAHRENHEIT = 1;
TEMPERATURE_UNIT_KELVIN = 2;
}
// ==================== CLIMATE ====================
enum ClimateMode {
CLIMATE_MODE_OFF = 0;
@@ -1110,6 +1117,7 @@ message ListEntitiesClimateResponse {
float visual_max_humidity = 25;
uint32 device_id = 26 [(field_ifdef) = "USE_DEVICES"];
uint32 feature_flags = 27;
TemperatureUnit temperature_unit = 28;
}
message ClimateStateResponse {
option (id) = 47;
@@ -1203,6 +1211,7 @@ message ListEntitiesWaterHeaterResponse {
repeated WaterHeaterMode supported_modes = 11 [(container_pointer_no_template) = "water_heater::WaterHeaterModeMask"];
// Bitmask of WaterHeaterFeature flags
uint32 supported_features = 12;
TemperatureUnit temperature_unit = 13;
}
message WaterHeaterStateResponse {
@@ -1410,6 +1419,8 @@ enum LockState {
LOCK_STATE_JAMMED = 3;
LOCK_STATE_LOCKING = 4;
LOCK_STATE_UNLOCKING = 5;
LOCK_STATE_OPENING = 6;
LOCK_STATE_OPEN = 7;
}
enum LockCommand {
LOCK_UNLOCK = 0;
@@ -1628,7 +1639,7 @@ message BluetoothLEAdvertisementResponse {
message BluetoothLERawAdvertisement {
option (inline_encode) = true;
uint64 address = 1 [(force) = true];
uint64 address = 1 [(force) = true, (mac_address) = true];
sint32 rssi = 2 [(force) = true];
uint32 address_type = 3 [(max_value) = 4];
+6
View File
@@ -110,4 +110,10 @@ extend google.protobuf.FieldOptions {
// length varint calculations and direct byte writes, since the length
// varint is guaranteed to be 1 byte.
optional uint32 max_data_length = 50018;
// mac_address: Field is a 48-bit MAC address stored in a uint64.
// Emits encode_varint_raw_48bit which has a 7-byte fast path that avoids
// the per-byte loop when the upper bits are non-zero (the common case
// for real MAC addresses, since OUIs occupy the top 24 bits).
optional bool mac_address = 50019 [default=false];
}
+6 -2
View File
@@ -1439,6 +1439,7 @@ uint8_t *ListEntitiesClimateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 26, this->device_id);
#endif
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 27, this->feature_flags);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 28, static_cast<uint32_t>(this->temperature_unit));
return pos;
}
uint32_t ListEntitiesClimateResponse::calculate_size() const {
@@ -1488,6 +1489,7 @@ uint32_t ListEntitiesClimateResponse::calculate_size() const {
size += ProtoSize::calc_uint32(2, this->device_id);
#endif
size += ProtoSize::calc_uint32(2, this->feature_flags);
size += this->temperature_unit ? 3 : 0;
return size;
}
uint8_t *ClimateStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -1645,6 +1647,7 @@ uint8_t *ListEntitiesWaterHeaterResponse::encode(ProtoWriteBuffer &buffer PROTO_
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 11, static_cast<uint32_t>(it), true);
}
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 12, this->supported_features);
ProtoEncode::encode_uint32(pos PROTO_ENCODE_DEBUG_ARG, 13, static_cast<uint32_t>(this->temperature_unit));
return pos;
}
uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const {
@@ -1667,6 +1670,7 @@ uint32_t ListEntitiesWaterHeaterResponse::calculate_size() const {
size += this->supported_modes->size() * 2;
}
size += ProtoSize::calc_uint32(1, this->supported_features);
size += this->temperature_unit ? 2 : 0;
return size;
}
uint8_t *WaterHeaterStateResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const {
@@ -2348,7 +2352,7 @@ BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer PROTO_ENCO
uint8_t *len_pos = pos;
ProtoEncode::reserve_byte(pos PROTO_ENCODE_DEBUG_ARG);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 8);
ProtoEncode::encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
ProtoEncode::encode_varint_raw_48bit(pos PROTO_ENCODE_DEBUG_ARG, sub_msg.address);
ProtoEncode::write_raw_byte(pos PROTO_ENCODE_DEBUG_ARG, 16);
ProtoEncode::encode_varint_raw_short(pos PROTO_ENCODE_DEBUG_ARG, encode_zigzag32(sub_msg.rssi));
if (sub_msg.address_type) {
@@ -2369,7 +2373,7 @@ BluetoothLERawAdvertisementsResponse::calculate_size() const {
for (uint16_t i = 0; i < this->advertisements_len; i++) {
auto &sub_msg = this->advertisements[i];
size += 2;
size += ProtoSize::calc_uint64_force(1, sub_msg.address);
size += ProtoSize::calc_uint64_48bit_force(1, sub_msg.address);
size += ProtoSize::calc_sint32_force(1, sub_msg.rssi);
size += sub_msg.address_type ? 2 : 0;
size += 2 + sub_msg.data_len;
+11 -2
View File
@@ -92,6 +92,11 @@ enum SupportsResponseType : uint32_t {
SUPPORTS_RESPONSE_STATUS = 100,
};
#endif
enum TemperatureUnit : uint32_t {
TEMPERATURE_UNIT_CELSIUS = 0,
TEMPERATURE_UNIT_FAHRENHEIT = 1,
TEMPERATURE_UNIT_KELVIN = 2,
};
#ifdef USE_CLIMATE
enum ClimateMode : uint32_t {
CLIMATE_MODE_OFF = 0,
@@ -176,6 +181,8 @@ enum LockState : uint32_t {
LOCK_STATE_JAMMED = 3,
LOCK_STATE_LOCKING = 4,
LOCK_STATE_UNLOCKING = 5,
LOCK_STATE_OPENING = 6,
LOCK_STATE_OPEN = 7,
};
enum LockCommand : uint32_t {
LOCK_UNLOCK = 0,
@@ -1372,7 +1379,7 @@ class CameraImageRequest final : public ProtoDecodableMessage {
class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 46;
static constexpr uint8_t ESTIMATED_SIZE = 150;
static constexpr uint8_t ESTIMATED_SIZE = 153;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_climate_response"); }
#endif
@@ -1394,6 +1401,7 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
float visual_min_humidity{0.0f};
float visual_max_humidity{0.0f};
uint32_t feature_flags{0};
enums::TemperatureUnit temperature_unit{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1471,7 +1479,7 @@ class ClimateCommandRequest final : public CommandProtoMessage {
class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 132;
static constexpr uint8_t ESTIMATED_SIZE = 63;
static constexpr uint8_t ESTIMATED_SIZE = 65;
#ifdef HAS_PROTO_MESSAGE_DUMP
const LogString *message_name() const override { return LOG_STR("list_entities_water_heater_response"); }
#endif
@@ -1480,6 +1488,7 @@ class ListEntitiesWaterHeaterResponse final : public InfoResponseProtoMessage {
float target_temperature_step{0.0f};
const water_heater::WaterHeaterModeMask *supported_modes{};
uint32_t supported_features{0};
enums::TemperatureUnit temperature_unit{};
uint8_t *encode(ProtoWriteBuffer &buffer PROTO_ENCODE_DEBUG_PARAM) const;
uint32_t calculate_size() const;
#ifdef HAS_PROTO_MESSAGE_DUMP
+18
View File
@@ -297,6 +297,18 @@ template<> const char *proto_enum_to_string<enums::SupportsResponseType>(enums::
}
}
#endif
template<> const char *proto_enum_to_string<enums::TemperatureUnit>(enums::TemperatureUnit value) {
switch (value) {
case enums::TEMPERATURE_UNIT_CELSIUS:
return ESPHOME_PSTR("TEMPERATURE_UNIT_CELSIUS");
case enums::TEMPERATURE_UNIT_FAHRENHEIT:
return ESPHOME_PSTR("TEMPERATURE_UNIT_FAHRENHEIT");
case enums::TEMPERATURE_UNIT_KELVIN:
return ESPHOME_PSTR("TEMPERATURE_UNIT_KELVIN");
default:
return ESPHOME_PSTR("UNKNOWN");
}
}
#ifdef USE_CLIMATE
template<> const char *proto_enum_to_string<enums::ClimateMode>(enums::ClimateMode value) {
switch (value) {
@@ -475,6 +487,10 @@ template<> const char *proto_enum_to_string<enums::LockState>(enums::LockState v
return ESPHOME_PSTR("LOCK_STATE_LOCKING");
case enums::LOCK_STATE_UNLOCKING:
return ESPHOME_PSTR("LOCK_STATE_UNLOCKING");
case enums::LOCK_STATE_OPENING:
return ESPHOME_PSTR("LOCK_STATE_OPENING");
case enums::LOCK_STATE_OPEN:
return ESPHOME_PSTR("LOCK_STATE_OPEN");
default:
return ESPHOME_PSTR("UNKNOWN");
}
@@ -1539,6 +1555,7 @@ const char *ListEntitiesClimateResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("device_id"), this->device_id);
#endif
dump_field(out, ESPHOME_PSTR("feature_flags"), this->feature_flags);
dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit));
return out.c_str();
}
const char *ClimateStateResponse::dump_to(DumpBuffer &out) const {
@@ -1612,6 +1629,7 @@ const char *ListEntitiesWaterHeaterResponse::dump_to(DumpBuffer &out) const {
dump_field(out, ESPHOME_PSTR("supported_modes"), static_cast<enums::WaterHeaterMode>(it), 4);
}
dump_field(out, ESPHOME_PSTR("supported_features"), this->supported_features);
dump_field(out, ESPHOME_PSTR("temperature_unit"), static_cast<enums::TemperatureUnit>(this->temperature_unit));
return out.c_str();
}
const char *WaterHeaterStateResponse::dump_to(DumpBuffer &out) const {
@@ -21,6 +21,7 @@ void APIServerConnectionBase::log_receive_message_(const LogString *name) {
}
#endif
#ifdef USE_API
void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) {
// Check authentication/connection requirements
switch (msg_type) {
@@ -706,5 +707,6 @@ void APIConnection::read_message_(uint32_t msg_size, uint32_t msg_type, const ui
break;
}
}
#endif // USE_API
} // namespace esphome::api
+5
View File
@@ -30,6 +30,11 @@ APIServer *global_api_server = nullptr; // NOLINT(cppcoreguidelines-avoid-non-c
APIServer::APIServer() { global_api_server = this; }
// Custom deleter defined here so `delete` sees the complete APIConnection type.
// This prevents libc++ from emitting an "incomplete type" error when other
// translation units only have the forward declaration of APIConnection.
void APIServer::APIConnectionDeleter::operator()(APIConnection *p) const { delete p; }
void APIServer::socket_failed_(const LogString *msg) {
ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno);
this->destroy_socket_();
+8 -2
View File
@@ -193,7 +193,13 @@ class APIServer final : public Component,
// Range-for view over the populated slice [0, api_connection_count_). Read-only with respect
// to ownership — callers get `const unique_ptr&` so they can invoke non-const methods on the
// APIConnection but cannot reset/move the slot and break the count invariant.
using APIConnectionPtr = std::unique_ptr<APIConnection>;
// Custom deleter is defined out-of-line in api_server.cpp so libc++ does not
// eagerly instantiate `delete static_cast<APIConnection *>(p)` here, where
// only the forward declaration of APIConnection is visible (incomplete type).
struct APIConnectionDeleter {
void operator()(APIConnection *p) const;
};
using APIConnectionPtr = std::unique_ptr<APIConnection, APIConnectionDeleter>;
class ActiveClientsView {
const APIConnectionPtr *begin_;
const APIConnectionPtr *end_;
@@ -292,7 +298,7 @@ class APIServer final : public Component,
uint32_t last_connected_{0};
// Slots [0, api_connection_count_) are populated; trailing slots are always nullptr.
std::array<std::unique_ptr<APIConnection>, MAX_API_CONNECTIONS> clients_{};
std::array<APIConnectionPtr, MAX_API_CONNECTIONS> clients_{};
// Vectors and strings (12 bytes each on 32-bit)
// Shared proto write buffer for all connections.
// Not pre-allocated: all send paths call prepare_first_message_buffer() which
+34
View File
@@ -342,6 +342,32 @@ class ProtoEncode {
}
encode_varint_raw_loop(pos PROTO_ENCODE_DEBUG_ARG, value);
}
/// Encode a 48-bit MAC address (stored in a uint64) as varint.
/// Real MAC addresses occupy the full 48 bits (OUI in upper 24), so the
/// fast path -- any non-zero bit in the top 6 of 48 -- emits exactly 7 bytes
/// with no per-byte branch. Falls back to the general loop otherwise.
/// Caller must guarantee value fits in 48 bits (checked in debug builds).
static inline void ESPHOME_ALWAYS_INLINE encode_varint_raw_48bit(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint64_t value) {
#ifdef ESPHOME_DEBUG_API
assert(value < (1ULL << (MAC_ADDRESS_SIZE * 8)) && "encode_varint_raw_48bit: value exceeds 48 bits");
#endif
// 7-byte varint holds 49 bits (7 * 7), so a 48-bit value needs all 7 bytes
// whenever bit 42 or higher is set (i.e. value >= 1 << (48 - 6)).
if (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6))) [[likely]] {
PROTO_ENCODE_CHECK_BOUNDS(pos, 7);
pos[0] = static_cast<uint8_t>(value | 0x80);
pos[1] = static_cast<uint8_t>((value >> 7) | 0x80);
pos[2] = static_cast<uint8_t>((value >> 14) | 0x80);
pos[3] = static_cast<uint8_t>((value >> 21) | 0x80);
pos[4] = static_cast<uint8_t>((value >> 28) | 0x80);
pos[5] = static_cast<uint8_t>((value >> 35) | 0x80);
pos[6] = static_cast<uint8_t>(value >> 42);
pos += 7;
return;
}
encode_varint_raw_64(pos PROTO_ENCODE_DEBUG_ARG, value);
}
static inline void ESPHOME_ALWAYS_INLINE encode_field_raw(uint8_t *__restrict__ &pos PROTO_ENCODE_DEBUG_PARAM,
uint32_t field_id, uint32_t type) {
encode_varint_raw(pos PROTO_ENCODE_DEBUG_ARG, (field_id << 3) | type);
@@ -817,6 +843,14 @@ class ProtoSize {
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_force(uint32_t field_id_size, uint64_t value) {
return field_id_size + varint(value);
}
/// 48-bit MAC address variant: matches encode_varint_raw_48bit's fast path.
/// When any of the top 6 of 48 bits is set the encoded varint is 7 bytes;
/// otherwise fall back to the general size calculation.
/// Caller must guarantee value fits in 48 bits (encoder asserts in debug).
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE calc_uint64_48bit_force(uint32_t field_id_size,
uint64_t value) {
return field_id_size + (value >= (1ULL << (MAC_ADDRESS_SIZE * 8 - 6)) ? 7 : varint(value));
}
static constexpr uint32_t calc_length(uint32_t field_id_size, size_t len) {
return len ? field_id_size + varint(static_cast<uint32_t>(len)) + static_cast<uint32_t>(len) : 0;
}
+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:
+1 -1
View File
@@ -220,7 +220,7 @@ async def to_code(config):
data = _get_data()
if data.micro_decoder_support:
add_idf_component(name="esphome/micro-decoder", ref="0.1.1")
add_idf_component(name="esphome/micro-decoder", ref="0.2.0")
# All codecs are enabled by default in micro-decoder, so disable the ones that aren't requested to save flash
if not data.flac_support:
+1 -1
View File
@@ -154,7 +154,7 @@ void BH1750Sensor::loop() {
break;
}
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
ESP_LOGV(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
this->status_clear_warning();
this->publish_state(lx);
this->state_ = IDLE;
+2 -1
View File
@@ -62,6 +62,7 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -624,7 +625,7 @@ async def setup_binary_sensor_core_(var, config):
async def register_binary_sensor(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_binary_sensor(var))
queue_entity_register("binary_sensor", config)
CORE.register_platform_component("binary_sensor", var)
await setup_binary_sensor_core_(var, config)
@@ -50,29 +50,31 @@ void MultiClickTriggerBase::on_state_(bool state) {
return;
}
if (*this->at_index_ == this->timing_count_) {
// at_index_ has a value here (the !has_value() branch above returns).
size_t at_index = *this->at_index_;
if (at_index == this->timing_count_) {
this->trigger_();
return;
}
MultiClickTriggerEvent evt = this->timing_[*this->at_index_];
MultiClickTriggerEvent evt = this->timing_[at_index];
if (evt.max_length != 4294967294UL) {
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, *this->at_index_, evt.min_length, evt.max_length); // NOLINT
ESP_LOGV(TAG, "A i=%zu min=%" PRIu32 " max=%" PRIu32, at_index, evt.min_length, evt.max_length); // NOLINT
this->schedule_is_valid_(evt.min_length);
this->schedule_is_not_valid_(evt.max_length);
} else if (*this->at_index_ + 1 != this->timing_count_) {
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
} else if (at_index + 1 != this->timing_count_) {
ESP_LOGV(TAG, "B i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->schedule_is_valid_(evt.min_length);
} else {
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, *this->at_index_, evt.min_length); // NOLINT
ESP_LOGV(TAG, "C i=%zu min=%" PRIu32, at_index, evt.min_length); // NOLINT
this->is_valid_ = false;
this->cancel_timeout(MULTICLICK_IS_NOT_VALID_ID);
this->set_timeout(MULTICLICK_TRIGGER_ID, evt.min_length, [this]() { this->trigger_(); });
}
*this->at_index_ = *this->at_index_ + 1;
this->at_index_ = at_index + 1;
}
void MultiClickTriggerBase::schedule_cooldown_() {
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
+498 -2
View File
@@ -16,6 +16,7 @@ from esphome.components.libretiny.const import (
FAMILY_BK7231N,
FAMILY_BK7231Q,
FAMILY_BK7231T,
FAMILY_BK7238,
FAMILY_BK7251,
)
@@ -24,16 +25,32 @@ BK72XX_BOARDS = {
"name": "WB2L_M1 Wi-Fi Module",
"family": FAMILY_BK7231N,
},
"xh-wb3s": {
"name": "NiceMCU XH-WB3S",
"family": FAMILY_BK7238,
},
"cbu": {
"name": "CBU Wi-Fi Module",
"family": FAMILY_BK7231N,
},
"t1-u": {
"name": "T1-U Wi-Fi Module",
"family": FAMILY_BK7238,
},
"generic-bk7238-tuya": {
"name": "Generic - BK7238 (Tuya T1)",
"family": FAMILY_BK7238,
},
"t1-m": {
"name": "T1-M Wi-Fi Module",
"family": FAMILY_BK7238,
},
"generic-bk7231t-qfn32-tuya": {
"name": "Generic - BK7231T (Tuya QFN32)",
"name": "Generic - BK7231T (Tuya)",
"family": FAMILY_BK7231T,
},
"generic-bk7231n-qfn32-tuya": {
"name": "Generic - BK7231N (Tuya QFN32)",
"name": "Generic - BK7231N (Tuya)",
"family": FAMILY_BK7231N,
},
"cb1s": {
@@ -64,6 +81,10 @@ BK72XX_BOARDS = {
"name": "Generic - BK7252",
"family": FAMILY_BK7251,
},
"t1-3s": {
"name": "T1-3S Wi-Fi Module",
"family": FAMILY_BK7238,
},
"wb2l": {
"name": "WB2L Wi-Fi Module",
"family": FAMILY_BK7231T,
@@ -80,6 +101,10 @@ BK72XX_BOARDS = {
"name": "CB2S Wi-Fi Module",
"family": FAMILY_BK7231N,
},
"generic-bk7238": {
"name": "Generic - BK7238",
"family": FAMILY_BK7238,
},
"wa2": {
"name": "WA2 Wi-Fi Module",
"family": FAMILY_BK7231Q,
@@ -100,6 +125,10 @@ BK72XX_BOARDS = {
"name": "WB3L Wi-Fi Module",
"family": FAMILY_BK7231T,
},
"t1-2s": {
"name": "T1-2S Wi-Fi Module",
"family": FAMILY_BK7238,
},
"wb2s": {
"name": "WB2S Wi-Fi Module",
"family": FAMILY_BK7231T,
@@ -158,6 +187,83 @@ BK72XX_BOARD_PINS = {
"D12": 22,
"A0": 23,
},
"xh-wb3s": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P7": 7,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM1": 7,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 7,
"D1": 23,
"D2": 14,
"D3": 26,
"D4": 24,
"D5": 6,
"D6": 9,
"D7": 0,
"D8": 1,
"D9": 8,
"D10": 10,
"D11": 11,
"D12": 16,
"D13": 20,
"D14": 21,
"D15": 22,
"D16": 15,
"D17": 17,
"A0": 28,
"A1": 26,
"A2": 24,
"A3": 1,
"A4": 10,
"A5": 20,
},
"cbu": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
@@ -230,6 +336,204 @@ BK72XX_BOARD_PINS = {
"D18": 21,
"A0": 23,
},
"t1-u": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 14,
"D1": 16,
"D2": 23,
"D3": 22,
"D4": 20,
"D5": 1,
"D6": 0,
"D7": 24,
"D8": 9,
"D9": 26,
"D10": 6,
"D11": 8,
"D12": 11,
"D13": 10,
"D14": 28,
"D15": 21,
"D16": 17,
"D17": 15,
"A0": 20,
"A1": 1,
"A2": 24,
"A3": 26,
"A4": 10,
"A5": 28,
},
"generic-bk7238-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P7": 7,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM1": 7,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 0,
"D1": 1,
"D2": 6,
"D3": 7,
"D4": 8,
"D5": 9,
"D6": 10,
"D7": 11,
"D8": 14,
"D9": 15,
"D10": 16,
"D11": 17,
"D12": 20,
"D13": 21,
"D14": 22,
"D15": 23,
"D16": 24,
"D17": 26,
"D18": 28,
"A0": 1,
"A1": 10,
"A2": 20,
"A3": 24,
"A4": 26,
"A5": 28,
},
"t1-m": {
"WIRE2_SCL": 24,
"WIRE2_SDA": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC5": 1,
"ADC6": 10,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P24": 24,
"P26": 26,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCL2": 24,
"SDA2": 26,
"TX1": 11,
"TX2": 0,
"D0": 26,
"D1": 6,
"D2": 8,
"D3": 1,
"D4": 10,
"D5": 11,
"D6": 9,
"D7": 24,
"D11": 0,
"A0": 26,
"A1": 10,
"A2": 1,
"A3": 24,
},
"generic-bk7231t-qfn32-tuya": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
@@ -781,6 +1085,75 @@ BK72XX_BOARD_PINS = {
"A6": 12,
"A7": 13,
},
"t1-3s": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 20,
"D1": 22,
"D2": 6,
"D3": 8,
"D4": 9,
"D5": 23,
"D6": 0,
"D7": 1,
"D8": 24,
"D9": 26,
"D10": 10,
"D11": 11,
"D12": 17,
"D13": 16,
"D14": 15,
"D15": 14,
"A0": 20,
"A1": 1,
"A2": 24,
"A3": 26,
"A4": 10,
},
"wb2l": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
@@ -965,6 +1338,84 @@ BK72XX_BOARD_PINS = {
"D10": 21,
"A0": 23,
},
"generic-bk7238": {
"SPI0_CS": 15,
"SPI0_MISO": 17,
"SPI0_MOSI": 16,
"SPI0_SCK": 14,
"WIRE2_SCL_0": 15,
"WIRE2_SCL_1": 24,
"WIRE2_SDA_0": 17,
"WIRE2_SDA_1": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC3": 20,
"ADC4": 28,
"ADC5": 1,
"ADC6": 10,
"CS": 15,
"MISO": 17,
"MOSI": 16,
"P0": 0,
"P1": 1,
"P6": 6,
"P7": 7,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P14": 14,
"P15": 15,
"P16": 16,
"P17": 17,
"P20": 20,
"P21": 21,
"P22": 22,
"P23": 23,
"P24": 24,
"P26": 26,
"P28": 28,
"PWM0": 6,
"PWM1": 7,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCK": 14,
"TX1": 11,
"TX2": 0,
"D0": 0,
"D1": 1,
"D2": 6,
"D3": 7,
"D4": 8,
"D5": 9,
"D6": 10,
"D7": 11,
"D8": 14,
"D9": 15,
"D10": 16,
"D11": 17,
"D12": 20,
"D13": 21,
"D14": 22,
"D15": 23,
"D16": 24,
"D17": 26,
"D18": 28,
"A0": 1,
"A1": 10,
"A2": 20,
"A3": 24,
"A4": 26,
"A5": 28,
},
"wa2": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
@@ -1235,6 +1686,51 @@ BK72XX_BOARD_PINS = {
"D15": 1,
"A0": 23,
},
"t1-2s": {
"WIRE2_SCL": 24,
"WIRE2_SDA": 26,
"SERIAL1_RX": 10,
"SERIAL1_TX": 11,
"SERIAL2_RX": 1,
"SERIAL2_TX": 0,
"ADC1": 26,
"ADC2": 24,
"ADC5": 1,
"ADC6": 10,
"P0": 0,
"P1": 1,
"P6": 6,
"P8": 8,
"P9": 9,
"P10": 10,
"P11": 11,
"P24": 24,
"P26": 26,
"PWM0": 6,
"PWM2": 8,
"PWM3": 9,
"PWM4": 24,
"PWM5": 26,
"RX1": 10,
"RX2": 1,
"SCL2": 24,
"SDA2": 26,
"TX1": 11,
"TX2": 0,
"D0": 26,
"D1": 6,
"D2": 8,
"D3": 1,
"D4": 10,
"D5": 11,
"D6": 9,
"D7": 24,
"D11": 0,
"A0": 26,
"A1": 10,
"A2": 1,
"A3": 24,
},
"wb2s": {
"WIRE1_SCL": 20,
"WIRE1_SDA": 21,
@@ -30,19 +30,6 @@ void BluetoothProxy::setup() {
this->configured_scan_active_ = this->parent_->get_scan_active();
this->parent_->add_scanner_state_listener(this);
this->set_interval(100, [this]() {
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
this->flush_pending_advertisements_();
return;
}
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect();
}
}
});
}
void BluetoothProxy::on_scanner_state(esp32_ble_tracker::ScannerState state) {
@@ -133,6 +120,25 @@ void BluetoothProxy::dump_config() {
YESNO(this->active_), this->connection_count_);
}
void BluetoothProxy::loop() {
// Run advertisement flush / connection cleanup every 100ms
uint32_t now = App.get_loop_component_start_time();
if (now - this->last_advertisement_flush_time_ < 100)
return;
this->last_advertisement_flush_time_ = now;
if (api::global_api_server->is_connected() && this->api_connection_ != nullptr) {
this->flush_pending_advertisements_();
return;
}
for (uint8_t i = 0; i < this->connection_count_; i++) {
auto *connection = this->connections_[i];
if (connection->get_address() != 0 && !connection->disconnect_pending()) {
connection->disconnect();
}
}
}
esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_parser_type() {
return esp32_ble_tracker::AdvertisementParserType::RAW_ADVERTISEMENTS;
}
@@ -201,7 +207,6 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
this->log_connection_info_(connection, "v3 without cache");
}
uint64_to_bd_addr(msg.address, connection->remote_bda_);
connection->set_remote_addr_type(static_cast<esp_ble_addr_type_t>(msg.address_type));
connection->set_state(espbt::ClientState::DISCOVERED);
this->send_connections_free();
@@ -65,6 +65,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override;
void dump_config() override;
void setup() override;
void loop() override;
esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
void register_connection(BluetoothConnection *connection) {
@@ -176,6 +177,9 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener,
// BLE advertisement batching
api::BluetoothLERawAdvertisementsResponse response_;
// Group 3: 4-byte types
uint32_t last_advertisement_flush_time_{0};
// Pre-allocated response message - always ready to send
api::BluetoothConnectionsFreeResponse connections_free_response_;
+27 -27
View File
@@ -78,43 +78,43 @@ void BME680Component::setup() {
}
// Read calibration
uint8_t cal1[25];
if (!this->read_bytes(BME680_REGISTER_COEFF1, cal1, 25)) {
uint8_t coeff1[25];
if (!this->read_bytes(BME680_REGISTER_COEFF1, coeff1, 25)) {
this->mark_failed();
return;
}
uint8_t cal2[16];
if (!this->read_bytes(BME680_REGISTER_COEFF2, cal2, 16)) {
uint8_t coeff2[16];
if (!this->read_bytes(BME680_REGISTER_COEFF2, coeff2, 16)) {
this->mark_failed();
return;
}
this->calibration_.t1 = cal2[9] << 8 | cal2[8];
this->calibration_.t2 = cal1[2] << 8 | cal1[1];
this->calibration_.t3 = cal1[3];
this->calibration_.t1 = coeff2[9] << 8 | coeff2[8];
this->calibration_.t2 = coeff1[2] << 8 | coeff1[1];
this->calibration_.t3 = coeff1[3];
this->calibration_.h1 = cal2[2] << 4 | (cal2[1] & 0x0F);
this->calibration_.h2 = cal2[0] << 4 | cal2[1] >> 4;
this->calibration_.h3 = cal2[3];
this->calibration_.h4 = cal2[4];
this->calibration_.h5 = cal2[5];
this->calibration_.h6 = cal2[6];
this->calibration_.h7 = cal2[7];
this->calibration_.h1 = coeff2[2] << 4 | (coeff2[1] & 0x0F);
this->calibration_.h2 = coeff2[0] << 4 | coeff2[1] >> 4;
this->calibration_.h3 = coeff2[3];
this->calibration_.h4 = coeff2[4];
this->calibration_.h5 = coeff2[5];
this->calibration_.h6 = coeff2[6];
this->calibration_.h7 = coeff2[7];
this->calibration_.p1 = cal1[6] << 8 | cal1[5];
this->calibration_.p2 = cal1[8] << 8 | cal1[7];
this->calibration_.p3 = cal1[9];
this->calibration_.p4 = cal1[12] << 8 | cal1[11];
this->calibration_.p5 = cal1[14] << 8 | cal1[13];
this->calibration_.p6 = cal1[16];
this->calibration_.p7 = cal1[15];
this->calibration_.p8 = cal1[20] << 8 | cal1[19];
this->calibration_.p9 = cal1[22] << 8 | cal1[21];
this->calibration_.p10 = cal1[23];
this->calibration_.p1 = coeff1[6] << 8 | coeff1[5];
this->calibration_.p2 = coeff1[8] << 8 | coeff1[7];
this->calibration_.p3 = coeff1[9];
this->calibration_.p4 = coeff1[12] << 8 | coeff1[11];
this->calibration_.p5 = coeff1[14] << 8 | coeff1[13];
this->calibration_.p6 = coeff1[16];
this->calibration_.p7 = coeff1[15];
this->calibration_.p8 = coeff1[20] << 8 | coeff1[19];
this->calibration_.p9 = coeff1[22] << 8 | coeff1[21];
this->calibration_.p10 = coeff1[23];
this->calibration_.gh1 = cal2[14];
this->calibration_.gh2 = cal2[12] << 8 | cal2[13];
this->calibration_.gh3 = cal2[15];
this->calibration_.gh1 = coeff2[14];
this->calibration_.gh2 = coeff2[12] << 8 | coeff2[13];
this->calibration_.gh3 = coeff2[15];
uint8_t temp_var = 0;
if (!this->read_byte(0x02, &temp_var)) {
+2 -1
View File
@@ -19,6 +19,7 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -101,7 +102,7 @@ async def setup_button_core_(var, config):
async def register_button(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_button(var))
queue_entity_register("button", config)
CORE.register_platform_component("button", var)
await setup_button_core_(var, config)
+6 -2
View File
@@ -49,7 +49,11 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
IS_PLATFORM_COMPONENT = True
@@ -442,7 +446,7 @@ async def setup_climate_core_(var, config):
async def register_climate(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_climate(var))
queue_entity_register("climate", config)
CORE.register_platform_component("climate", var)
await setup_climate_core_(var, config)
+2 -1
View File
@@ -39,6 +39,7 @@ from esphome.const import (
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -232,7 +233,7 @@ async def setup_cover_core_(var, config):
async def register_cover(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_cover(var))
queue_entity_register("cover", config)
CORE.register_platform_component("cover", var)
await setup_cover_core_(var, config)
+6 -2
View File
@@ -22,7 +22,11 @@ from esphome.const import (
CONF_YEAR,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@rfdarter", "@jesserockz"]
@@ -160,7 +164,7 @@ async def register_datetime(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
entity_type = config[CONF_TYPE].lower()
cg.add(getattr(cg.App, f"register_{entity_type}")(var))
queue_entity_register(entity_type, config)
CORE.register_platform_component(entity_type, var)
await setup_datetime_core_(var, config)
+8 -5
View File
@@ -193,11 +193,14 @@ def _validate_ex1_wakeup_mode(value):
def _validate_sleep_duration(value: core.TimePeriod) -> core.TimePeriod:
if not CORE.is_bk72xx:
return value
max_duration = core.TimePeriod(hours=36)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
if CORE.is_bk72xx:
max_duration = core.TimePeriod(hours=36)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 36 hours on BK72XX")
elif CORE.using_zephyr:
max_duration = core.TimePeriod(days=49)
if value > max_duration:
raise cv.Invalid("sleep duration cannot be more than 49 days on Zephyr")
return value
@@ -9,18 +9,11 @@ static const char *const TAG = "deep_sleep";
// 5 seconds for deep sleep to ensure clean disconnect from Home Assistant
static const uint32_t TEARDOWN_TIMEOUT_DEEP_SLEEP_MS = 5000;
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
std::atomic<DeepSleepComponent *> global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool global_has_deep_sleep = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void DeepSleepComponent::setup() {
#ifdef USE_ZEPHYR
k_sem_init(&this->wakeup_sem_, 0, 1);
#endif
global_has_deep_sleep = true;
this->schedule_sleep_();
// It can be used from another thread for waking up the device.
// It should be called as last item in setup.
global_deep_sleep.store(this);
}
void DeepSleepComponent::schedule_sleep_() {
@@ -4,8 +4,6 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include <atomic>
#ifdef USE_ESP32
#include <esp_sleep.h>
#endif
@@ -15,10 +13,6 @@
#include "esphome/core/time.h"
#endif
#ifdef USE_ZEPHYR
#include <zephyr/kernel.h>
#endif
#include <cinttypes>
namespace esphome {
@@ -125,9 +119,6 @@ class DeepSleepComponent : public Component {
void prevent_deep_sleep();
void allow_deep_sleep();
#ifdef USE_ZEPHYR
void wakeup();
#endif
protected:
// Returns nullopt if no run duration is set. Otherwise, returns the run
@@ -167,9 +158,6 @@ class DeepSleepComponent : public Component {
optional<uint32_t> run_duration_;
bool next_enter_deep_sleep_{false};
bool prevent_{false};
#ifdef USE_ZEPHYR
k_sem wakeup_sem_;
#endif
};
extern bool global_has_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@@ -256,8 +244,5 @@ template<typename... Ts> class AllowDeepSleepAction : public Action<Ts...>, publ
void play(const Ts &...x) override { this->parent_->allow_deep_sleep(); }
};
extern std::atomic<DeepSleepComponent *>
global_deep_sleep; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
} // namespace deep_sleep
} // namespace esphome
@@ -1,17 +1,13 @@
#include "deep_sleep_component.h"
#ifdef USE_ZEPHYR
#include "esphome/core/log.h"
#include "esphome/core/wake.h"
#include <zephyr/sys/poweroff.h>
#include <zephyr/kernel.h>
#include <zephyr/stats/stats.h>
#include <zephyr/pm/pm.h>
namespace esphome::deep_sleep {
static const char *const TAG = "deep_sleep";
void DeepSleepComponent::wakeup() { k_sem_give(&this->wakeup_sem_); }
optional<uint32_t> DeepSleepComponent::get_run_duration_() const { return this->run_duration_; }
void DeepSleepComponent::dump_config_platform_() {}
@@ -19,9 +15,8 @@ void DeepSleepComponent::dump_config_platform_() {}
bool DeepSleepComponent::prepare_to_sleep_() { return true; }
void DeepSleepComponent::deep_sleep_() {
k_timeout_t sleep_duration = K_FOREVER;
if (this->sleep_duration_.has_value()) {
sleep_duration = K_USEC(*this->sleep_duration_);
esphome::internal::wakeable_delay(static_cast<uint32_t>(*this->sleep_duration_ / 1000));
} else {
#ifndef USE_ZIGBEE
// the device can be woken up through one of the following signals:
@@ -33,11 +28,12 @@ void DeepSleepComponent::deep_sleep_() {
//
// The system is reset when it wakes up from System OFF mode.
sys_poweroff();
#else
esphome::internal::wakeable_delay(UINT32_MAX);
#endif
}
// It might wake up immediately if k_sem_give was called again after wake up
int ret = k_sem_take(&this->wakeup_sem_, sleep_duration);
if (ret == 0) {
const bool woke = esphome::wake_request_take();
if (woke) {
ESP_LOGD(TAG, "Woken up by another thread");
} else {
ESP_LOGD(TAG, "Timeout expired (normal sleep)");
@@ -29,7 +29,7 @@ class DemoAlarmControlPanel : public AlarmControlPanel, public Component {
protected:
void control(const AlarmControlPanelCall &call) override {
auto state = call.get_state().value_or(ACP_STATE_DISARMED);
auto code = call.get_code();
const auto &code = call.get_code();
switch (state) {
case ACP_STATE_ARMED_AWAY:
if (this->get_requires_code_to_arm()) {
@@ -104,8 +104,9 @@ int8_t CircularCommandQueue::enqueue(std::unique_ptr<Command> cmd) {
if (this->is_full()) {
ESP_LOGE(TAG, "Command queue is full");
return -1;
} else if (this->is_empty())
} else if (this->is_empty()) {
front_++;
}
rear_ = (rear_ + 1) % COMMAND_QUEUE_SIZE;
commands_[rear_] = std::move(cmd); // Transfer ownership using std::move
return 1;
+5
View File
@@ -1724,6 +1724,11 @@ async def to_code(config):
CORE.relative_internal_path(".espressif")
)
# Both ESP-IDF and ESP32 Arduino builds generate IDF app metadata. Keep
# volatile build path/time data out of the binary so equivalent projects can
# produce reproducible outputs and downstream tooling can reuse artifacts.
add_idf_sdkconfig_option("CONFIG_APP_REPRODUCIBLE_BUILD", True)
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
cg.add_build_flag("-DUSE_ESP_IDF")
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
+1 -10
View File
@@ -22,7 +22,7 @@ extern "C" __attribute__((weak)) void initArduino() {}
namespace esphome {
void HOT yield() { vPortYield(); }
// yield(), delay(), micros(), millis_64() inlined in hal.h.
// 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.
@@ -37,15 +37,6 @@ uint32_t IRAM_ATTR HOT millis() {
return micros_to_millis(static_cast<uint64_t>(esp_timer_get_time()));
#endif
}
// millis_64() stays on esp_timer — a different clock from xTaskGetTickCount(). This is
// safe because the two are never cross-compared: millis() values are only used for
// millis()-vs-millis() deltas (feed_wdt, warn_blocking, component start time), while
// millis_64() is used by the Scheduler and uptime sensors. On ESP32 (USE_NATIVE_64BIT_TIME),
// Scheduler::millis_64_from_(now) discards the 32-bit now and calls millis_64() directly,
// so the Scheduler is internally consistent on the esp_timer clock.
uint64_t HOT millis_64() { return micros_to_millis<uint64_t>(static_cast<uint64_t>(esp_timer_get_time())); }
void HOT delay(uint32_t ms) { vTaskDelay(ms / portTICK_PERIOD_MS); }
uint32_t IRAM_ATTR HOT micros() { return (uint32_t) esp_timer_get_time(); }
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
esp_restart();
+17 -1
View File
@@ -18,6 +18,12 @@ struct NVSData {
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
// open() runs from app_main() before the logger is initialized, so any failure
// must be deferred until after global_logger is set. This is emitted from the
// first make_preference() call, which runs from the generated setup() after
// log->pre_setup() has run at EARLY_INIT priority.
static esp_err_t s_open_err = ESP_OK; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
bool ESP32PreferenceBackend::save(const uint8_t *data, size_t len) {
// try find in pending saves and update that
for (auto &obj : s_pending_save) {
@@ -70,12 +76,14 @@ bool ESP32PreferenceBackend::load(uint8_t *data, size_t len) {
}
void ESP32Preferences::open() {
// Runs from app_main() before the logger is initialized; any logging here
// must be deferred. See s_open_err and make_preference() below.
nvs_flash_init();
esp_err_t err = nvs_open("esphome", NVS_READWRITE, &this->nvs_handle);
if (err == 0)
return;
ESP_LOGW(TAG, "nvs_open failed: %s - erasing NVS", esp_err_to_name(err));
s_open_err = err;
nvs_flash_deinit();
nvs_flash_erase();
nvs_flash_init();
@@ -87,6 +95,14 @@ void ESP32Preferences::open() {
}
ESPPreferenceObject ESP32Preferences::make_preference(size_t length, uint32_t type) {
if (s_open_err != ESP_OK) {
if (this->nvs_handle == 0) {
ESP_LOGW(TAG, "nvs_open failed: %s - NVS unavailable", esp_err_to_name(s_open_err));
} else {
ESP_LOGW(TAG, "nvs_open failed: %s - erased NVS", esp_err_to_name(s_open_err));
}
s_open_err = ESP_OK;
}
auto *pref = new ESP32PreferenceBackend(); // NOLINT(cppcoreguidelines-owning-memory)
pref->nvs_handle = this->nvs_handle;
pref->key = type;
+1 -1
View File
@@ -104,7 +104,7 @@ ESPBTUUID ESPBTUUID::as_128bit() const {
} else {
uuid32 = this->uuid_.uuid.uuid16;
}
for (uint8_t i = 0; i < this->uuid_.len; i++) {
for (uint16_t i = 0; i < this->uuid_.len; i++) {
data[12 + i] = ((uuid32 >> i * 8) & 0xFF);
}
return ESPBTUUID::from_raw(data);
@@ -166,8 +166,9 @@ void ESP32BLETracker::loop() {
ClientStateCounts counts = this->count_client_states_();
if (counts != this->client_state_counts_) {
this->client_state_counts_ = counts;
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d", this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
ESP_LOGD(TAG, "connecting: %d, discovered: %d, disconnecting: %d, active: %d",
this->client_state_counts_.connecting, this->client_state_counts_.discovered,
this->client_state_counts_.disconnecting, this->client_state_counts_.active);
}
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
@@ -190,10 +191,18 @@ void ESP32BLETracker::loop() {
*/
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
// all clients are idle (their state changes increment version when they finish)
// no clients are in the transient CONNECTING / DISCOVERED / DISCONNECTING states
// (their state changes increment version when they finish). CONNECTED / ESTABLISHED
// clients do NOT block this branch — the coex revert below has its own active-count gate.
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
this->update_coex_preference_(false);
// Only revert to BALANCE when no connections are active. Established connections
// continue to need PREFER_BT so peer GATT responses can reach us while WiFi traffic
// (advertisement upload, log streaming) competes for the shared radio. Reverting too
// early causes Bluedroid to time out at ~20s and synthesize status=133.
if (!counts.active) {
this->update_coex_preference_(false);
}
#endif
if (this->scan_continuous_) {
this->start_scan_(false); // first = false
@@ -701,9 +710,10 @@ void ESP32BLETracker::dump_config() {
this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
ESP_LOGCONFIG(TAG,
" Scanner State: %s\n"
" Connecting: %d, discovered: %d, disconnecting: %d",
" Connecting: %d, discovered: %d, disconnecting: %d, active: %d",
this->scanner_state_to_string_(this->scanner_state_), this->client_state_counts_.connecting,
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting,
this->client_state_counts_.active);
if (this->scan_start_fail_count_) {
ESP_LOGCONFIG(TAG, " Scan Start Fail Count: %d", this->scan_start_fail_count_);
}
@@ -160,9 +160,13 @@ struct ClientStateCounts {
uint8_t connecting = 0;
uint8_t discovered = 0;
uint8_t disconnecting = 0;
// CONNECTED + ESTABLISHED clients. Tracked so coex stays at PREFER_BT
// while active connections may still need to send/receive GATT traffic.
uint8_t active = 0;
bool operator==(const ClientStateCounts &other) const {
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting;
return connecting == other.connecting && discovered == other.discovered && disconnecting == other.disconnecting &&
active == other.active;
}
bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
@@ -381,6 +385,10 @@ class ESP32BLETracker : public Component,
case ClientState::CONNECTING:
counts.connecting++;
break;
case ClientState::CONNECTED:
case ClientState::ESTABLISHED:
counts.active++;
break;
default:
break;
}
@@ -216,6 +216,7 @@ void ESP32TouchComponent::setup() {
// Do initial oneshot scans to populate baseline values
for (uint32_t i = 0; i < ONESHOT_SCAN_COUNT; i++) {
err = touch_sensor_trigger_oneshot_scanning(this->sens_handle_, ONESHOT_SCAN_TIMEOUT_MS);
App.feed_wdt(); // 3 scans with 2s timeout might exceed WDT, so feed it here to be safe
if (err != ESP_OK) {
ESP_LOGW(TAG, "Oneshot scan %" PRIu32 " failed: %s", i, esp_err_to_name(err));
}
+5
View File
@@ -314,6 +314,11 @@ async def to_code(config):
for symbol in ("vprintf", "printf", "fprintf"):
cg.add_build_flag(f"-Wl,--wrap={symbol}")
# Wrap Arduino's millis() so all callers (including Arduino libraries and ISR
# handlers) use our fast accumulator instead of the expensive 4x 64-bit multiply
# implementation in the Arduino ESP8266 core.
cg.add_build_flag("-Wl,--wrap=millis")
cg.add_platformio_option("board_build.flash_mode", config[CONF_BOARD_FLASH_MODE])
ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
+77 -5
View File
@@ -15,11 +15,75 @@ extern "C" {
namespace esphome {
void HOT yield() { ::yield(); }
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
uint64_t millis_64() { return Millis64Impl::compute(::millis()); }
void HOT delay(uint32_t ms) { ::delay(ms); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
// yield(), micros(), millis_64() inlined in hal.h.
// Fast accumulator replacement for Arduino's millis() (~3.3 μs via 4× 64-bit
// multiplies on the LX106). Tracks a running ms counter from 32-bit
// system_get_time() deltas using pure 32-bit ops. Installed as __wrap_millis
// (via -Wl,--wrap=millis) so Arduino libs and IRAM_ATTR ISR handlers (e.g.
// Wiegand, ZyAura) also get the fast version. xt_rsil(15) guards the static
// state against ISR re-entry; the critical section is bounded (≤10 while-loop
// iterations, ~100 ns on the common path, or a constant-time /1000 ~2.5 μs on
// the rare path — well under WiFi's ~10 μs ISR latency budget). NMIs (level
// >15) are not masked, but the ESP8266 SDK's NMI handlers don't call millis().
//
// system_get_time() wraps every ~71.6 min; unsigned (now_us - last_us) handles
// one wrap. The main loop calls millis() at 60+ Hz, so delta stays tiny — a
// >71 min block would trip the watchdog long before it could matter here.
static constexpr uint32_t MILLIS_RARE_PATH_THRESHOLD_US = 10000;
static constexpr uint32_t US_PER_MS = 1000;
uint32_t IRAM_ATTR HOT millis() {
// Struct packs the three statics so the compiler loads one base address
// instead of three separate literal pool entries (saves ~8 bytes IRAM).
static struct {
uint32_t cache;
uint32_t remainder;
uint32_t last_us;
} state = {0, 0, 0};
uint32_t ps = xt_rsil(15);
uint32_t now_us = system_get_time();
uint32_t delta = now_us - state.last_us;
state.last_us = now_us;
state.remainder += delta;
if (state.remainder >= MILLIS_RARE_PATH_THRESHOLD_US) {
// Rare path: large gap (WiFi scan, boot, long block). Constant-time
// conversion keeps the critical section bounded.
uint32_t ms = state.remainder / US_PER_MS;
state.cache += ms;
// Reuse ms instead of `remainder %= US_PER_MS` — `%` would compile to a
// second __umodsi3 call on the LX106 (no hardware divide).
state.remainder -= ms * US_PER_MS;
} else {
// Common path: small gap. At most ~10 iterations since remainder was
// < threshold (10 ms) on entry and delta adds at most one more threshold
// before exiting this branch.
while (state.remainder >= US_PER_MS) {
state.cache++;
state.remainder -= US_PER_MS;
}
}
uint32_t result = state.cache;
xt_wsr_ps(ps);
return result;
}
// Poll-based delay that avoids ::delay() — Arduino's __delay has an intra-object
// call to the original millis() that --wrap can't intercept, so calling ::delay()
// would keep the slow Arduino millis body alive in IRAM. optimistic_yield still
// enters esp_schedule()/esp_suspend_within_cont() via yield(), so SDK tasks and
// WiFi run correctly. Theoretically less power-efficient than Arduino's
// os_timer-based delay() for long waits, but nearly all ESPHome delays are short
// (sensor/I²C/SPI settling in the 1100 ms range) where the difference is
// negligible.
void HOT delay(uint32_t ms) {
if (ms == 0) {
optimistic_yield(1000);
return;
}
uint32_t start = millis();
while (millis() - start < ms) {
optimistic_yield(1000);
}
}
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); }
void arch_restart() {
system_restart();
@@ -78,4 +142,12 @@ extern "C" void resetPins() { // NOLINT
} // namespace esphome
// Linker wrap: redirect all ::millis() calls (Arduino libs, ISRs) to our accumulator.
// Requires -Wl,--wrap=millis in build flags (added by __init__.py).
// NOLINTNEXTLINE(bugprone-reserved-identifier,cert-dcl37-c,cert-dcl51-cpp,readability-identifier-naming)
extern "C" uint32_t IRAM_ATTR __wrap_millis() { return esphome::millis(); }
// Note: Arduino's init() registers a 60-second overflow timer for micros64().
// We leave it running — wrapping init() as a no-op would break micros64()'s
// overflow tracking, and the timer's cost is negligible (~3 μs per 60 s).
#endif // USE_ESP8266
+13 -1
View File
@@ -292,6 +292,7 @@ void ESPHomeOTAComponent::handle_data_() {
bool update_started = false;
size_t total = 0;
uint32_t last_progress = 0;
uint32_t last_data_ms = 0;
uint8_t buf[OTA_BUFFER_SIZE];
char *sbuf = reinterpret_cast<char *>(buf);
size_t ota_size;
@@ -350,8 +351,18 @@ void ESPHomeOTAComponent::handle_data_() {
// Acknowledge MD5 OK - 1 byte
this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
// Track when we last received data so a silently-vanished peer (no FIN/RST
// delivered, e.g. uploader killed mid-transfer or NAT/router dropped state)
// can't wedge the device indefinitely. Without this, the loop only exits
// on actual data, EOF, or a non-EWOULDBLOCK error from read(), and lwIP
// TCP keepalive isn't enabled here.
last_data_ms = millis();
while (total < ota_size) {
// TODO: timeout check
if (millis() - last_data_ms > OTA_SOCKET_TIMEOUT_DATA) {
ESP_LOGW(TAG, "No data received for %u ms", (unsigned) OTA_SOCKET_TIMEOUT_DATA);
error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
size_t remaining = ota_size - total;
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
ssize_t read = this->client_->read(buf, requested);
@@ -369,6 +380,7 @@ void ESPHomeOTAComponent::handle_data_() {
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
}
last_data_ms = millis();
error_code = this->backend_->write(buf, read);
if (error_code != ota::OTA_RESPONSE_OK) {
ESP_LOGW(TAG, "Flash write err %d", error_code);
+2 -1
View File
@@ -19,6 +19,7 @@ from esphome.const import (
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
)
@@ -108,7 +109,7 @@ async def setup_event_core_(var, config, *, event_types: list[str]):
async def register_event(var, config, *, event_types: list[str]):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_event(var))
queue_entity_register("event", config)
CORE.register_platform_component("event", var)
await setup_event_core_(var, config, event_types=event_types)
@@ -1,5 +1,6 @@
import logging
from pathlib import Path
from typing import Any
from esphome import git, loader
import esphome.config_validation as cv
@@ -17,7 +18,7 @@ from esphome.const import (
TYPE_GIT,
TYPE_LOCAL,
)
from esphome.core import CORE
from esphome.core import CORE, TimePeriodSeconds
_LOGGER = logging.getLogger(__name__)
@@ -35,17 +36,15 @@ CONFIG_SCHEMA = cv.ensure_list(
)
async def to_code(config):
async def to_code(config: dict[str, Any]) -> None:
pass
def _process_git_config(config: dict, refresh, skip_update: bool = False) -> str:
# When skip_update is True, use NEVER_REFRESH to prevent updates
actual_refresh = git.NEVER_REFRESH if skip_update else refresh
def _process_git_config(config: dict[str, Any], refresh: TimePeriodSeconds) -> Path:
repo_dir, _ = git.clone_or_update(
url=config[CONF_URL],
ref=config.get(CONF_REF),
refresh=actual_refresh,
refresh=refresh,
domain=DOMAIN,
username=config.get(CONF_USERNAME),
password=config.get(CONF_PASSWORD),
@@ -72,12 +71,12 @@ def _process_git_config(config: dict, refresh, skip_update: bool = False) -> str
return components_dir
def _process_single_config(config: dict, skip_update: bool = False):
def _process_single_config(config: dict[str, Any]) -> None:
conf = config[CONF_SOURCE]
if conf[CONF_TYPE] == TYPE_GIT:
with cv.prepend_path([CONF_SOURCE]):
components_dir = _process_git_config(
config[CONF_SOURCE], config[CONF_REFRESH], skip_update
config[CONF_SOURCE], config[CONF_REFRESH]
)
elif conf[CONF_TYPE] == TYPE_LOCAL:
components_dir = Path(CORE.relative_config_path(conf[CONF_PATH]))
@@ -107,7 +106,7 @@ def _process_single_config(config: dict, skip_update: bool = False):
loader.install_meta_finder(components_dir, allowed_components=allowed_components)
def do_external_components_pass(config: dict, skip_update: bool = False) -> None:
def do_external_components_pass(config: dict[str, Any]) -> None:
conf = config.get(DOMAIN)
if conf is None:
return
@@ -115,4 +114,4 @@ def do_external_components_pass(config: dict, skip_update: bool = False) -> None
conf = CONFIG_SCHEMA(conf)
for i, c in enumerate(conf):
with cv.prepend_path(i):
_process_single_config(c, skip_update)
_process_single_config(c)
+2 -2
View File
@@ -35,7 +35,7 @@ void EZOSensor::update() {
}
if (!found) {
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
auto ezo_command = make_unique<EzoCommand>();
ezo_command->command = "R";
ezo_command->command_type = EzoCommandType::EZO_READ;
ezo_command->delay_ms = 900;
@@ -162,7 +162,7 @@ void EZOSensor::loop() {
}
void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) {
std::unique_ptr<EzoCommand> ezo_command(new EzoCommand);
auto ezo_command = make_unique<EzoCommand>();
ezo_command->command = command;
ezo_command->command_type = command_type;
ezo_command->delay_ms = delay_ms;
+6 -2
View File
@@ -32,7 +32,11 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
IS_PLATFORM_COMPONENT = True
@@ -292,7 +296,7 @@ async def setup_fan_core_(var, config):
async def register_fan(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_fan(var))
queue_entity_register("fan", config)
CORE.register_platform_component("fan", var)
await setup_fan_core_(var, config)
+10 -10
View File
@@ -3,11 +3,12 @@
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome {
namespace feedback {
namespace esphome::feedback {
static const char *const TAG = "feedback.cover";
static constexpr uint32_t DIRECTION_CHANGE_TIMEOUT_ID = 1;
using namespace esphome::cover;
void FeedbackCover::setup() {
@@ -37,7 +38,7 @@ void FeedbackCover::setup() {
}
#endif
this->last_recompute_time_ = this->start_dir_time_ = millis();
this->last_recompute_time_ = this->start_dir_time_ = App.get_loop_component_start_time();
}
CoverTraits FeedbackCover::get_traits() {
@@ -135,7 +136,7 @@ void FeedbackCover::set_close_endstop(binary_sensor::BinarySensor *close_endstop
#endif
void FeedbackCover::endstop_reached_(bool open_endstop) {
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
this->position = open_endstop ? COVER_OPEN : COVER_CLOSED;
@@ -174,7 +175,7 @@ void FeedbackCover::set_current_operation_(cover::CoverOperation operation, bool
if (!is_triggered || (this->open_feedback_ == nullptr || this->close_feedback_ == nullptr))
#endif
{
auto now = millis();
const uint32_t now = App.get_loop_component_start_time();
this->current_operation = operation;
this->start_dir_time_ = this->last_recompute_time_ = now;
this->publish_state();
@@ -306,7 +307,7 @@ void FeedbackCover::control(const CoverCall &call) {
void FeedbackCover::stop_prev_trigger_() {
if (this->direction_change_waittime_.has_value()) {
this->cancel_timeout("direction_change");
this->cancel_timeout(DIRECTION_CHANGE_TIMEOUT_ID);
}
if (this->prev_command_trigger_ != nullptr) {
this->prev_command_trigger_->stop_action();
@@ -377,7 +378,7 @@ void FeedbackCover::start_direction_(CoverOperation dir) {
ESP_LOGD(TAG, "'%s' - Reversing direction.", this->name_.c_str());
this->start_direction_(COVER_OPERATION_IDLE);
this->set_timeout("direction_change", *this->direction_change_waittime_,
this->set_timeout(DIRECTION_CHANGE_TIMEOUT_ID, *this->direction_change_waittime_,
[this, dir]() { this->start_direction_(dir); });
} else {
@@ -395,7 +396,7 @@ void FeedbackCover::recompute_position_() {
if (this->current_operation == COVER_OPERATION_IDLE)
return;
const uint32_t now = millis();
const uint32_t now = App.get_loop_component_start_time();
float dir;
float action_dur;
float min_pos;
@@ -451,5 +452,4 @@ void FeedbackCover::recompute_position_() {
this->last_recompute_time_ = now;
}
} // namespace feedback
} // namespace esphome
} // namespace esphome::feedback
+2 -4
View File
@@ -8,8 +8,7 @@
#endif
#include "esphome/components/cover/cover.h"
namespace esphome {
namespace feedback {
namespace esphome::feedback {
class FeedbackCover : public cover::Cover, public Component {
public:
@@ -85,5 +84,4 @@ class FeedbackCover : public cover::Cover, public Component {
uint32_t update_interval_{1000};
};
} // namespace feedback
} // namespace esphome
} // namespace esphome::feedback
@@ -210,8 +210,9 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
#ifdef USE_WIFI
else if (this->send_wifi_signal_ &&
(std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) {
this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
}
#endif
} break;
default:
+2 -7
View File
@@ -8,7 +8,6 @@
#include <csignal>
#include <sched.h>
#include <time.h>
#include <cmath>
#include <cstdlib>
namespace {
@@ -22,9 +21,7 @@ void HOT yield() { ::sched_yield(); }
uint32_t IRAM_ATTR HOT millis() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
time_t seconds = spec.tv_sec;
uint32_t ms = round(spec.tv_nsec / 1e6);
return ((uint32_t) seconds) * 1000U + ms;
return static_cast<uint32_t>(spec.tv_sec * 1000ULL + spec.tv_nsec / 1000000);
}
uint64_t millis_64() {
struct timespec spec;
@@ -43,9 +40,7 @@ void HOT delay(uint32_t ms) {
uint32_t IRAM_ATTR HOT micros() {
struct timespec spec;
clock_gettime(CLOCK_MONOTONIC, &spec);
time_t seconds = spec.tv_sec;
uint32_t us = round(spec.tv_nsec / 1e3);
return ((uint32_t) seconds) * 1000000U + us;
return static_cast<uint32_t>(spec.tv_sec * 1000000ULL + spec.tv_nsec / 1000);
}
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) {
struct timespec ts;
@@ -462,7 +462,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> {
this->request_headers_.push_back({key, value});
}
void add_collect_header(const char *value) { this->lower_case_collect_headers_.push_back(value); }
void add_collect_header(const char *value) { this->lower_case_collect_headers_.emplace_back(value); }
void init_json(size_t count) { this->json_.init(count); }
void add_json(const char *key, TemplatableValue<std::string, Ts...> value) { this->json_.push_back({key, value}); }
+1 -1
View File
@@ -5,7 +5,7 @@
#include "i2c_bus.h"
#include "esphome/core/component.h"
struct device;
struct device; // NOLINT(readability-identifier-naming) - forward decl of Zephyr's device type
namespace esphome::i2c {
+14 -7
View File
@@ -744,21 +744,28 @@ async def write_image(config, all_frames=False):
if frame_count <= 1:
_LOGGER.warning("Image file %s has no animation frames", path)
total_rows = height * frame_count
encoder = IMAGE_TYPE[type](width, total_rows, transparency, dither, invert_alpha)
if byte_order := config.get(CONF_BYTE_ORDER):
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
# Encode each frame with its own encoder and concatenate. This keeps every
# frame self-contained on disk (e.g. RGB565+alpha emits [RGB plane | alpha plane]
# per frame) so animation frame stepping in image.cpp / animation.cpp stays
# correct without needing to know the total frame count.
byte_order = config.get(CONF_BYTE_ORDER)
combined_data: list[int] = []
encoder: ImageEncoder | None = None
for frame_index in range(frame_count):
image.seek(frame_index)
encoder = IMAGE_TYPE[type](width, height, transparency, dither, invert_alpha)
if byte_order is not None:
# Check for valid type has already been done in validate_settings
encoder.set_big_endian(byte_order == "BIG_ENDIAN")
pixels = encoder.convert(image.resize((width, height)), path).getdata()
for row in range(height):
for col in range(width):
encoder.encode(pixels[row * width + col])
encoder.end_row()
encoder.end_image()
encoder.end_image()
combined_data.extend(encoder.data)
rhs = [HexInt(x) for x in encoder.data]
rhs = [HexInt(x) for x in combined_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
image_type = get_image_type_enum(type)
trans_value = get_transparency_enum(encoder.transparency)
+2 -2
View File
@@ -12,7 +12,7 @@ import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.core import CORE, coroutine_with_priority
from esphome.core.entity_helpers import setup_entity
from esphome.core.entity_helpers import queue_entity_register, setup_entity
from esphome.coroutine import CoroPriority
from esphome.types import ConfigType
@@ -54,8 +54,8 @@ async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None:
"""Register an infrared device with the core."""
cg.add_define("USE_IR_RF")
await cg.register_component(var, config)
queue_entity_register("infrared", config)
await setup_infrared_core_(var, config)
cg.add(cg.App.register_infrared(var))
CORE.register_platform_component("infrared", var)
@@ -41,12 +41,12 @@ bool InkbirdIbstH1Mini::parse_device(const esp32_ble_tracker::ESPBTDevice &devic
ESP_LOGVV(TAG, "parse_device(): service_data is expected to be empty");
return false;
}
auto mnf_datas = device.get_manufacturer_datas();
const auto &mnf_datas = device.get_manufacturer_datas();
if (mnf_datas.size() != 1) {
ESP_LOGVV(TAG, "parse_device(): manufacturer_datas is expected to have a single element");
return false;
}
auto mnf_data = mnf_datas[0];
const auto &mnf_data = mnf_datas[0];
if (mnf_data.uuid.get_uuid().len != ESP_UUID_LEN_16) {
ESP_LOGVV(TAG, "parse_device(): manufacturer data element is expected to have uuid of length 16");
return false;
+111 -1
View File
@@ -1,13 +1,73 @@
#include "ir_rf_proxy.h"
#include <cinttypes>
#include "esphome/core/log.h"
namespace esphome::ir_rf_proxy {
static const char *const TAG = "ir_rf_proxy";
// ========== Shared transmit helper ==========
// Static template: all instantiations occur in this translation unit.
template<typename CallT>
static void transmit_raw_timings(remote_base::RemoteTransmitterBase *transmitter, uint32_t carrier_frequency,
const CallT &call) {
if (transmitter == nullptr) {
ESP_LOGW(TAG, "No transmitter configured");
return;
}
if (!call.has_raw_timings()) {
ESP_LOGE(TAG, "No raw timings provided");
return;
}
auto transmit_call = transmitter->transmit();
auto *transmit_data = transmit_call.get_data();
transmit_data->set_carrier_frequency(carrier_frequency);
if (call.is_packed()) {
transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(),
call.get_packed_count());
ESP_LOGD(TAG, "Transmitting packed raw timings: count=%" PRIu16 ", repeat=%" PRIu32, call.get_packed_count(),
call.get_repeat_count());
} else if (call.is_base64url()) {
if (!transmit_data->set_data_from_base64url(call.get_base64url_data())) {
ESP_LOGE(TAG, "Invalid base64url data");
return;
}
constexpr int32_t max_timing_us = 500000;
for (int32_t timing : transmit_data->get_data()) {
int32_t abs_timing = timing < 0 ? -timing : timing;
if (abs_timing > max_timing_us) {
ESP_LOGE(TAG, "Invalid timing value: %" PRId32 " µs (max %" PRId32 ")", timing, max_timing_us);
return;
}
}
ESP_LOGD(TAG, "Transmitting base64url raw timings: count=%zu, repeat=%" PRIu32, transmit_data->get_data().size(),
call.get_repeat_count());
} else {
transmit_data->set_data(call.get_raw_timings());
ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%" PRIu32, call.get_raw_timings().size(),
call.get_repeat_count());
}
if (call.get_repeat_count() > 0) {
transmit_call.set_send_times(call.get_repeat_count());
}
transmit_call.perform();
}
// ========== IrRfProxy (Infrared platform) ==========
#ifdef USE_IR_RF
void IrRfProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"IR/RF Proxy '%s'\n"
"IR Proxy '%s'\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
@@ -20,4 +80,54 @@ void IrRfProxy::dump_config() {
}
}
void IrRfProxy::control(const infrared::InfraredCall &call) {
uint32_t carrier = call.get_carrier_frequency().value_or(0);
transmit_raw_timings(this->transmitter_, carrier, call);
}
#endif // USE_IR_RF
// ========== RfProxy (Radio Frequency platform) ==========
#ifdef USE_RADIO_FREQUENCY
void RfProxy::setup() {
this->traits_.set_supports_transmitter(this->transmitter_ != nullptr);
this->traits_.set_supports_receiver(this->receiver_ != nullptr);
// remote_transmitter/receiver always uses OOK (on-off keying)
this->traits_.add_supported_modulation(radio_frequency::RadioFrequencyModulation::RADIO_FREQUENCY_MODULATION_OOK);
if (this->receiver_ != nullptr) {
this->receiver_->register_listener(this);
}
}
void RfProxy::dump_config() {
ESP_LOGCONFIG(TAG,
"RF Proxy '%s'\n"
" Backend: remote_transmitter/receiver\n"
" Supports Transmitter: %s\n"
" Supports Receiver: %s",
this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()),
YESNO(this->traits_.get_supports_receiver()));
const auto &traits = this->traits_;
if (traits.get_frequency_min_hz() > 0) {
if (traits.get_frequency_min_hz() == traits.get_frequency_max_hz()) {
ESP_LOGCONFIG(TAG, " Frequency: %.3f MHz (fixed)", traits.get_frequency_min_hz() / 1e6f);
} else {
ESP_LOGCONFIG(TAG, " Frequency Range: %.3f - %.3f MHz", traits.get_frequency_min_hz() / 1e6f,
traits.get_frequency_max_hz() / 1e6f);
}
}
}
void RfProxy::control(const radio_frequency::RadioFrequencyCall &call) {
// RF: no IR carrier modulation
transmit_raw_timings(this->transmitter_, 0, call);
}
#endif // USE_RADIO_FREQUENCY
} // namespace esphome::ir_rf_proxy
@@ -4,10 +4,19 @@
// without following the normal breaking changes policy. Use at your own risk.
// Once the API is considered stable, this warning will be removed.
#include "esphome/components/remote_base/remote_base.h"
#ifdef USE_IR_RF
#include "esphome/components/infrared/infrared.h"
#endif
#ifdef USE_RADIO_FREQUENCY
#include "esphome/components/radio_frequency/radio_frequency.h"
#endif
namespace esphome::ir_rf_proxy {
#ifdef USE_IR_RF
/// IrRfProxy - Infrared platform implementation using remote_transmitter/receiver as backend
class IrRfProxy : public infrared::Infrared {
public:
@@ -26,8 +35,36 @@ class IrRfProxy : public infrared::Infrared {
void set_receiver_frequency(uint32_t frequency_hz) { this->get_traits().set_receiver_frequency_hz(frequency_hz); }
protected:
void control(const infrared::InfraredCall &call) override;
// RF frequency in kHz (Hz / 1000); 0 = infrared, non-zero = RF
uint32_t frequency_khz_{0};
};
#endif // USE_IR_RF
#ifdef USE_RADIO_FREQUENCY
/// RfProxy - Radio Frequency platform implementation using remote_transmitter/receiver as backend
class RfProxy : public radio_frequency::RadioFrequency {
public:
RfProxy() = default;
void setup() override;
void dump_config() override;
/// Set the remote transmitter component
void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; }
/// Set the remote receiver component
void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; }
/// Set the fixed carrier frequency in Hz (metadata: advertised via traits, does not tune hardware)
void set_frequency_hz(uint32_t freq_hz) { this->traits_.set_fixed_frequency_hz(freq_hz); }
protected:
void control(const radio_frequency::RadioFrequencyCall &call) override;
remote_base::RemoteTransmitterBase *transmitter_{nullptr};
remote_base::RemoteReceiverBase *receiver_{nullptr};
};
#endif // USE_RADIO_FREQUENCY
} // namespace esphome::ir_rf_proxy
@@ -0,0 +1,68 @@
"""Radio Frequency platform implementation using remote_base (remote_transmitter/receiver)."""
import esphome.codegen as cg
from esphome.components import radio_frequency, remote_receiver, remote_transmitter
import esphome.config_validation as cv
from esphome.const import CONF_CARRIER_DUTY_PERCENT, CONF_FREQUENCY
import esphome.final_validate as fv
from esphome.types import ConfigType
from . import CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID, ir_rf_proxy_ns
CODEOWNERS = ["@kbx81"]
DEPENDENCIES = ["radio_frequency"]
RfProxy = ir_rf_proxy_ns.class_("RfProxy", radio_frequency.RadioFrequency)
CONFIG_SCHEMA = cv.All(
radio_frequency.radio_frequency_schema(RfProxy).extend(
{
cv.Optional(CONF_FREQUENCY): cv.frequency,
cv.Optional(CONF_REMOTE_RECEIVER_ID): cv.use_id(
remote_receiver.RemoteReceiverComponent
),
cv.Optional(CONF_REMOTE_TRANSMITTER_ID): cv.use_id(
remote_transmitter.RemoteTransmitterComponent
),
}
),
cv.has_exactly_one_key(CONF_REMOTE_RECEIVER_ID, CONF_REMOTE_TRANSMITTER_ID),
)
def _final_validate(config: ConfigType) -> None:
"""Validate that RF transmitters have carrier duty set to 100%."""
if CONF_REMOTE_TRANSMITTER_ID not in config:
return
transmitter_id = config[CONF_REMOTE_TRANSMITTER_ID]
full_config = fv.full_config.get()
transmitter_path = full_config.get_path_for_id(transmitter_id)[:-1]
transmitter_config = full_config.get_config_for_path(transmitter_path)
duty_percent = transmitter_config.get(CONF_CARRIER_DUTY_PERCENT)
if duty_percent is not None and duty_percent != 100:
raise cv.Invalid(
f"Transmitter '{transmitter_id}' must have '{CONF_CARRIER_DUTY_PERCENT}' "
"set to 100% for RF transmission. Dedicated RF hardware handles modulation; "
"applying a carrier duty cycle would corrupt the signal"
)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config: ConfigType) -> None:
"""Code generation for remote_base radio frequency platform."""
var = await radio_frequency.new_radio_frequency(config)
if CONF_FREQUENCY in config:
cg.add(var.set_frequency_hz(int(config[CONF_FREQUENCY])))
if CONF_REMOTE_TRANSMITTER_ID in config:
transmitter = await cg.get_variable(config[CONF_REMOTE_TRANSMITTER_ID])
cg.add(var.set_transmitter(transmitter))
if CONF_REMOTE_RECEIVER_ID in config:
receiver = await cg.get_variable(config[CONF_REMOTE_RECEIVER_ID])
cg.add(var.set_receiver(receiver))
+3 -2
View File
@@ -39,7 +39,8 @@ bool parse_json(const uint8_t *data, size_t len, const json_parse_t &f) {
}
JsonDocument parse_json(const uint8_t *data, size_t len) {
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
// NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape) false positives with
// ArduinoJson
if (data == nullptr || len == 0) {
ESP_LOGE(TAG, "No data to parse");
return JsonObject(); // return unbound object
@@ -63,7 +64,7 @@ JsonDocument parse_json(const uint8_t *data, size_t len) {
}
ESP_LOGE(TAG, "Parse error: %s", err.c_str());
return JsonObject(); // return unbound object
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks,clang-analyzer-core.StackAddressEscape)
}
SerializationBuffer<> JsonBuilder::serialize() {
+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,
+1 -27
View File
@@ -3,7 +3,6 @@
#include "core.h"
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/time_64.h"
#include "esphome/core/helpers.h"
#include "preferences.h"
@@ -15,32 +14,7 @@ void loop();
namespace esphome {
void HOT yield() { ::yield(); }
// Inline the tick read so esphome::millis() matches MillisInternal::get()'s fast
// path instead of going through the Arduino core's out-of-line ::millis() wrapper.
//
// RTL87xx / LN882x (1 kHz): xTaskGetTickCount() is already ms. IRAM_ATTR + ISR
// dispatch are needed because ISR handlers (e.g. rotary_encoder) call millis().
//
// BK72xx (500 Hz): ticks * portTICK_PERIOD_MS (== 2). IRAM_ATTR and ISR dispatch
// are both unnecessary — the SDK masks FIQ + IRQ during flash writes (see hal.h),
// so no ISR runs while flash is stalled.
#if defined(USE_RTL87XX) || defined(USE_LN882X)
uint32_t IRAM_ATTR HOT millis() {
static_assert(configTICK_RATE_HZ == 1000, "millis() fast path requires 1 kHz FreeRTOS tick");
return in_isr_context() ? xTaskGetTickCountFromISR() : xTaskGetTickCount();
}
#elif defined(USE_BK72XX)
uint32_t HOT millis() {
static_assert(configTICK_RATE_HZ == 500, "BK72xx millis() fast path assumes 500 Hz FreeRTOS tick");
return xTaskGetTickCount() * portTICK_PERIOD_MS;
}
#else
uint32_t IRAM_ATTR HOT millis() { return ::millis(); }
#endif
uint64_t millis_64() { return Millis64Impl::compute(millis()); }
uint32_t IRAM_ATTR HOT micros() { return ::micros(); }
void HOT delay(uint32_t ms) { ::delay(ms); }
// yield(), delay(), micros(), millis(), millis_64() inlined in hal.h.
void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { ::delayMicroseconds(us); }
void arch_init() {
+6 -2
View File
@@ -40,7 +40,11 @@ from esphome.const import (
CONF_WHITE,
)
from esphome.core import CORE, ID, CoroPriority, HexInt, Lambda, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -405,7 +409,7 @@ async def setup_light_core_(light_var, config, output_var):
async def register_light(output_var, config):
light_var = cg.new_Pvariable(config[CONF_ID], output_var)
cg.add(cg.App.register_light(light_var))
queue_entity_register("light", config)
CORE.register_platform_component("light", light_var)
await cg.register_component(light_var, config)
await setup_light_core_(light_var, config, output_var)
+6 -2
View File
@@ -13,7 +13,11 @@ from esphome.const import (
CONF_WEB_SERVER,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_entity,
)
from esphome.cpp_generator import MockObjClass
CODEOWNERS = ["@esphome/core"]
@@ -112,7 +116,7 @@ async def _setup_lock_core(var, config):
async def register_lock(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_lock(var))
queue_entity_register("lock", config)
CORE.register_platform_component("lock", var)
await _setup_lock_core(var, config)
@@ -4,15 +4,25 @@ from esphome.components.binary_sensor import (
new_binary_sensor,
)
import esphome.config_validation as cv
from esphome.const import CONF_STATE
from ..defines import CONF_WIDGET
from ..lvcode import EVENT_ARG, LambdaContext, LvContext, lvgl_static
from ..types import LV_EVENT, lv_pseudo_button_t
from ..defines import CONF_WIDGET, LV_OBJ_FLAG, LvConstant
from ..lvcode import EVENT_ARG, UPDATE_EVENT, LambdaContext, LvContext, lvgl_static
from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t
from ..widgets import Widget, get_widgets, wait_for_widgets
STATE_PRESSED = "PRESSED"
STATE_CHECKED = "CHECKED"
BS_STATE = LvConstant(
"LV_STATE_",
STATE_PRESSED,
STATE_CHECKED,
)
CONFIG_SCHEMA = binary_sensor_schema(BinarySensor).extend(
{
cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t),
cv.Optional(CONF_STATE, default=STATE_PRESSED): BS_STATE.one_of,
}
)
@@ -22,16 +32,23 @@ async def to_code(config):
widget = await get_widgets(config, CONF_WIDGET)
widget = widget[0]
assert isinstance(widget, Widget)
state = await BS_STATE.process(config[CONF_STATE])
await wait_for_widgets()
async with LambdaContext(EVENT_ARG) as pressed_ctx:
pressed_ctx.add(sensor.publish_state(widget.is_pressed()))
is_pressed = str(state) == str(LV_STATE.PRESSED)
test_expr = widget.is_pressed() if is_pressed else widget.is_checked()
async with LambdaContext(EVENT_ARG) as test_ctx:
test_ctx.add(sensor.publish_state(test_expr))
async with LvContext() as ctx:
ctx.add(sensor.publish_initial_state(widget.is_pressed()))
ctx.add(sensor.publish_initial_state(test_expr))
if is_pressed:
events = [LV_EVENT.PRESSED, LV_EVENT.RELEASED]
widget.add_flag(LV_OBJ_FLAG.CLICKABLE)
else:
events = [LV_EVENT.VALUE_CHANGED, UPDATE_EVENT]
ctx.add(
lvgl_static.add_event_cb(
widget.obj,
await pressed_ctx.get_lambda(),
LV_EVENT.PRESSED,
LV_EVENT.RELEASED,
await test_ctx.get_lambda(),
*events,
)
)
+5 -1
View File
@@ -1,3 +1,5 @@
from operator import itemgetter
from esphome import config_validation as cv
import esphome.codegen as cg
from esphome.const import (
@@ -11,6 +13,7 @@ from esphome.core import ID
from esphome.cpp_generator import MockObj
from .defines import CONF_GRADIENTS, CONF_OPA, LV_DITHER, add_define, add_warning
from .helpers import add_lv_use
from .lv_validation import lv_color, lv_percentage, opacity
from .lvcode import lv
from .types import lv_color_t, lv_gradient_t, lv_opa_t
@@ -50,6 +53,7 @@ GRADIENT_SCHEMA = cv.ensure_list(
async def gradients_to_code(config):
add_lv_use("gradient")
max_stops = 2
if any(CONF_DITHER in x for x in config.get(CONF_GRADIENTS, ())):
add_warning(
@@ -58,7 +62,7 @@ async def gradients_to_code(config):
for gradient in config.get(CONF_GRADIENTS, ()):
var = MockObj(cg.new_Pvariable(gradient[CONF_ID]), "->")
idbase = gradient[CONF_ID].id
stops = gradient[CONF_STOPS]
stops = sorted(gradient[CONF_STOPS], key=itemgetter(CONF_POSITION))
max_stops = max(max_stops, len(stops))
if gradient[CONF_DIRECTION].startswith("VER"):
lv.grad_vertical_init(var)
+97 -22
View File
@@ -1,3 +1,4 @@
import math
import re
import textwrap
@@ -85,6 +86,22 @@ def grid_free_space(value):
grid_spec = cv.Any(size, LvConstant("LV_GRID_", "CONTENT").one_of, grid_free_space)
def grid_dimension(value):
"""
Validator for a grid `rows` or `columns` value.
Accepts either a positive integer (interpreted as that many cells of equal
`LV_GRID_FR(1)` size) or a non-empty list of grid specs.
"""
if isinstance(value, int):
value = cv.int_range(min=1)(value)
return ["LV_GRID_FR(1)"] * value
result = cv.Schema([grid_spec])(value)
if not result:
raise cv.Invalid("Grid dimension list must contain at least one entry")
return result
GRID_CELL_SCHEMA = {
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
@@ -184,7 +201,16 @@ class DirectionalLayout(FlexLayout):
class GridLayout(Layout):
_GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)\s*x\s*(\d+)\s*$")
# Match shorthand grid layout strings: "NxM", "Nx" or "xM".
# At least one of the two numbers must be present; this is enforced after matching.
_GRID_LAYOUT_REGEX = re.compile(r"^\s*(\d+)?\s*x\s*(\d+)?\s*$")
@staticmethod
def _match_shorthand(layout):
match = GridLayout._GRID_LAYOUT_REGEX.match(layout)
if match is None or (match.group(1) is None and match.group(2) is None):
return None
return match
def get_type(self):
return TYPE_GRID
@@ -192,7 +218,7 @@ class GridLayout(Layout):
def get_layout_schemas(self, config: dict) -> tuple:
layout = config.get(CONF_LAYOUT)
if isinstance(layout, str):
if GridLayout._GRID_LAYOUT_REGEX.match(layout):
if GridLayout._match_shorthand(layout):
return (
cv.string,
{
@@ -213,59 +239,107 @@ class GridLayout(Layout):
if not isinstance(layout, dict) or layout.get(CONF_TYPE).lower() != TYPE_GRID:
return None, {}
x_default = (
"center" if isinstance(layout.get(CONF_GRID_ROWS), int) else cv.UNDEFINED
)
y_default = (
"center" if isinstance(layout.get(CONF_GRID_COLUMNS), int) else cv.UNDEFINED
)
x_align = layout.get(CONF_GRID_CELL_X_ALIGN, x_default)
y_align = layout.get(CONF_GRID_CELL_Y_ALIGN, y_default)
return (
{
cv.Required(CONF_TYPE): cv.one_of(TYPE_GRID, lower=True),
cv.Required(CONF_GRID_ROWS): [grid_spec],
cv.Required(CONF_GRID_COLUMNS): [grid_spec],
cv.Optional(CONF_GRID_ROWS): grid_dimension,
cv.Optional(CONF_GRID_COLUMNS): grid_dimension,
cv.Optional(CONF_GRID_COLUMN_ALIGN): grid_alignments,
cv.Optional(CONF_GRID_ROW_ALIGN): grid_alignments,
cv.Optional(CONF_PAD_ROW): padding,
cv.Optional(CONF_PAD_COLUMN): padding,
cv.Optional(CONF_MULTIPLE_WIDGETS_PER_CELL, default=False): cv.boolean,
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
},
{
cv.Optional(CONF_GRID_CELL_ROW_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_COLUMN_POS): cv.positive_int,
cv.Optional(CONF_GRID_CELL_ROW_SPAN): cv.int_range(min=1),
cv.Optional(CONF_GRID_CELL_COLUMN_SPAN): cv.int_range(min=1),
cv.Optional(CONF_GRID_CELL_X_ALIGN): grid_alignments,
cv.Optional(CONF_GRID_CELL_Y_ALIGN): grid_alignments,
cv.Optional(CONF_GRID_CELL_X_ALIGN, default=x_align): grid_alignments,
cv.Optional(CONF_GRID_CELL_Y_ALIGN, default=y_align): grid_alignments,
},
)
def validate(self, config: dict):
"""
Validate the grid layout.
The `layout:` key may be a dictionary with `rows` and `columns` keys, or a string in the format "rows x columns".
The `layout:` key may be a dictionary with `rows` and/or `columns` keys, or a
shorthand string in the format "<rows>x<columns>", "<rows>x" or "x<columns>".
Either dimension may be omitted, in which case it will be calculated from the
other dimension and the number of configured widgets.
Either all cells must have a row and column,
or none, in which case the grid layout is auto-generated.
:param config:
:return: The config updated with auto-generated values
"""
layout = config.get(CONF_LAYOUT)
widgets = config.get(CONF_WIDGETS, [])
num_widgets = len(widgets)
if isinstance(layout, str):
# If the layout is a string, assume it is in the format "rows x columns", implying
# a grid layout with the specified number of rows and columns each with CONTENT sizing.
# Shorthand string: "<rows>x<columns>", "<rows>x" or "x<columns>".
# Each dimension defaults to LV_GRID_FR(1). A missing dimension is
# calculated from the other dimension and the number of widgets.
layout = layout.strip()
match = GridLayout._GRID_LAYOUT_REGEX.match(layout)
if match:
rows = int(match.group(1))
cols = int(match.group(2))
layout = {
CONF_TYPE: TYPE_GRID,
CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows,
CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols,
}
config[CONF_LAYOUT] = layout
else:
match = GridLayout._match_shorthand(layout)
if not match:
raise cv.Invalid(
f"Invalid grid layout format: {config}, expected 'rows x columns'",
f"Invalid grid layout format: {layout!r}, expected "
"'<rows>x<columns>', '<rows>x' or 'x<columns>'",
[CONF_LAYOUT],
)
rows_int = int(match.group(1)) if match.group(1) is not None else None
cols_int = int(match.group(2)) if match.group(2) is not None else None
for label, val in (("row", rows_int), ("column", cols_int)):
if val is not None and val < 1:
raise cv.Invalid(
f"Invalid grid layout {layout!r}: {label} count must be "
"at least 1",
[CONF_LAYOUT],
)
if rows_int is not None and cols_int is not None:
rows = rows_int
cols = cols_int
elif rows_int is not None:
rows = rows_int
cols = max(1, math.ceil(num_widgets / rows)) if num_widgets else 1
else:
cols = cols_int
rows = max(1, math.ceil(num_widgets / cols)) if num_widgets else 1
layout = {
CONF_TYPE: TYPE_GRID,
CONF_GRID_ROWS: ["LV_GRID_FR(1)"] * rows,
CONF_GRID_COLUMNS: ["LV_GRID_FR(1)"] * cols,
}
config[CONF_LAYOUT] = layout
# should be guaranteed to be a dict at this point
assert isinstance(layout, dict)
assert layout.get(CONF_TYPE).lower() == TYPE_GRID
rows_list = layout.get(CONF_GRID_ROWS)
cols_list = layout.get(CONF_GRID_COLUMNS)
if rows_list is None and cols_list is None:
raise cv.Invalid(
"Grid layout requires at least one of 'rows' or 'columns' to be "
"specified",
[CONF_LAYOUT],
)
if rows_list is None:
cols = len(cols_list)
rows = max(1, math.ceil(num_widgets / cols)) if num_widgets else 1
layout[CONF_GRID_ROWS] = ["LV_GRID_FR(1)"] * rows
elif cols_list is None:
rows = len(rows_list)
cols = max(1, math.ceil(num_widgets / rows)) if num_widgets else 1
layout[CONF_GRID_COLUMNS] = ["LV_GRID_FR(1)"] * cols
allow_multiple = layout.get(CONF_MULTIPLE_WIDGETS_PER_CELL, False)
rows = len(layout[CONF_GRID_ROWS])
columns = len(layout[CONF_GRID_COLUMNS])
@@ -379,7 +453,8 @@ def append_layout_schema(schema, config: dict):
textwrap.dedent(
"""
Invalid 'layout' value
layout choices are 'horizontal', 'vertical', '<rows>x<cols>',
layout choices are 'horizontal', 'vertical',
'<rows>x<cols>', '<rows>x', 'x<cols>',
or a dictionary with a 'type' key
"""
),
+26
View File
@@ -864,6 +864,32 @@ void lv_scale_draw_event_cb(lv_event_t *e, int16_t range_start, int16_t range_en
}
#endif // USE_LVGL_SCALE
#ifdef USE_LVGL_GRADIENT
/**
*
* @param dsc The gradient descriptor containing the color stops
* @param pos The current position to calculate the color for
* @return The color for the given position
*/
lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos) {
if (dsc->stops_count == 0)
return lv_color_black();
if (dsc->stops_count == 1 || pos <= dsc->stops[0].frac)
return dsc->stops[0].color;
if (pos >= dsc->stops[dsc->stops_count - 1].frac)
return dsc->stops[dsc->stops_count - 1].color;
int i = 1;
while (i < dsc->stops_count && dsc->stops[i].frac < pos)
i++;
auto *stop1 = &dsc->stops[i - 1];
auto *stop2 = &dsc->stops[i];
int32_t range = stop2->frac - stop1->frac;
int32_t offset = pos - stop1->frac;
return lv_color_mix(stop2->color, stop1->color, range == 0 ? 0 : (offset * 255) / range);
}
#endif
static void lv_container_constructor(const lv_obj_class_t *class_p, lv_obj_t *obj) {
LV_TRACE_OBJ_CREATE("begin");
LV_UNUSED(class_p);
+10
View File
@@ -115,6 +115,16 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int value);
#endif
#ifdef USE_LVGL_GRADIENT
/**
*
* @param dsc The gradient descriptor containing the color stops
* @param pos The current position to calculate the color for
* @return The color for the given position
*/
lv_color_t lv_grad_calculate_color(const lv_grad_dsc_t *dsc, int32_t pos);
#endif
// Parent class for things that wrap an LVGL object
class LvCompound {
public:
+81 -32
View File
@@ -1,18 +1,27 @@
from collections.abc import Callable
import difflib
import esphome.codegen as cg
from esphome.components.const import KEY_METADATA
import esphome.config_validation as cv
from esphome.const import CONF_FROM, CONF_ID, CONF_TO
from esphome.core import CORE
from esphome.cpp_generator import MockObj, VariableDeclarationExpression, add_global
from esphome.core import CORE, ID
from esphome.cpp_generator import (
MockObj,
MockObjClass,
VariableDeclarationExpression,
add_global,
)
from esphome.loader import get_component
CODEOWNERS = ["@clydebarrow"]
MULTI_CONF = True
DOMAIN = "mapping"
mapping_ns = cg.esphome_ns.namespace("mapping")
mapping_class = mapping_ns.class_("Mapping")
CONF_DEFAULT_VALUE = "default_value"
CONF_ENTRIES = "entries"
CONF_CLASS = "class"
@@ -22,11 +31,18 @@ class IndexType:
Represents a type of index in a map.
"""
def __init__(self, validator, data_type, conversion):
def __init__(
self, validator: Callable, data_type: MockObj, conversion: Callable = None
) -> None:
self.validator = validator
self.data_type = data_type
self.conversion = conversion
async def convert_value(self, value):
if self.conversion:
return self.conversion(value)
return await cg.get_variable(value)
INDEX_TYPES = {
"int": IndexType(cv.int_, cg.int_, int),
@@ -38,6 +54,12 @@ INDEX_TYPES = {
}
class MappingMetaData:
def __init__(self, from_: IndexType, to_: IndexType) -> None:
self.from_ = from_
self.to_ = to_
def to_schema(value):
"""
Generate a schema for the 'to' field of a map. This can be either one of the index types or a class name.
@@ -60,7 +82,7 @@ BASE_SCHEMA = cv.Schema(
)
def get_object_type(to_):
def get_object_type(to_) -> MockObjClass | None:
"""
Get the object type from a string. Possible formats:
xxx The name of a component which defines INSTANCE_TYPE
@@ -81,25 +103,60 @@ def get_object_type(to_):
return None
def get_all_mapping_metadata() -> dict[str, MappingMetaData]:
"""Get all mapping metadata."""
return CORE.data.setdefault(DOMAIN, {}).setdefault(KEY_METADATA, {})
def get_mapping_metadata(mapping_id: str) -> MappingMetaData:
"""Get mapping metadata by ID for use by other components."""
return get_all_mapping_metadata()[mapping_id]
def add_metadata(
mapping_id: ID,
from_: IndexType,
to_: IndexType,
) -> None:
get_all_mapping_metadata()[mapping_id.id] = MappingMetaData(from_, to_)
def map_schema(config):
config = BASE_SCHEMA(config)
if CONF_ENTRIES not in config or not isinstance(config[CONF_ENTRIES], dict):
raise cv.Invalid("an entries list is required for a map")
raise cv.Invalid("an entries dictionary is required for a mapping")
entries = config[CONF_ENTRIES]
if len(entries) == 0:
raise cv.Invalid("Map must have at least one entry")
raise cv.Invalid("A mapping must have at least one entry")
to_ = config[CONF_TO]
if to_ in INDEX_TYPES:
value_type = INDEX_TYPES[to_].validator
value_type = INDEX_TYPES[to_]
else:
value_type = get_object_type(to_)
if value_type is None:
object_type = get_object_type(to_)
if object_type is None:
matches = difflib.get_close_matches(to_, CORE.id_classes)
raise cv.Invalid(
f"No known mappable class name matches '{to_}'; did you mean one of {', '.join(matches)}?"
)
value_type = cv.use_id(value_type)
config[CONF_ENTRIES] = {k: value_type(v) for k, v in entries.items()}
validator = cv.use_id(object_type)
value_type = IndexType(validator, object_type)
config[CONF_ENTRIES] = {k: value_type.validator(v) for k, v in entries.items()}
if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None:
config[CONF_DEFAULT_VALUE] = value_type.validator(default_value)
unexpected_keys = config.keys() - {
CONF_ENTRIES,
CONF_TO,
CONF_FROM,
CONF_ID,
CONF_DEFAULT_VALUE,
}
if unexpected_keys:
errors = [
cv.Invalid(f"Unexpected key '{k}'", path=[k]) for k in unexpected_keys
]
raise cv.MultipleInvalid(errors)
add_metadata(config[CONF_ID], INDEX_TYPES[config[CONF_FROM]], value_type)
return config
@@ -107,29 +164,19 @@ CONFIG_SCHEMA = map_schema
async def to_code(config):
entries = config[CONF_ENTRIES]
from_ = config[CONF_FROM]
to_ = config[CONF_TO]
index_conversion = INDEX_TYPES[from_].conversion
index_type = INDEX_TYPES[from_].data_type
if to_ in INDEX_TYPES:
value_conversion = INDEX_TYPES[to_].conversion
value_type = INDEX_TYPES[to_].data_type
entries = {
index_conversion(key): value_conversion(value)
for key, value in entries.items()
}
else:
entries = {
index_conversion(key): await cg.get_variable(value)
for key, value in entries.items()
}
value_type = get_object_type(to_)
if list(entries.values())[0].op != ".":
value_type = value_type.operator("ptr")
varid = config[CONF_ID]
metadata = get_mapping_metadata(varid.id)
entries = {
metadata.from_.conversion(key): await metadata.to_.convert_value(value)
for key, value in config[CONF_ENTRIES].items()
}
value_type = metadata.to_.data_type
# entries guaranteed to be non-empty here.
value_0 = list(entries.values())[0]
if isinstance(value_0, MockObj) and value_0.op != ".":
value_type = value_type.operator("ptr")
varid.type = mapping_class.template(
index_type,
metadata.from_.data_type,
value_type,
)
var = MockObj(varid, ".")
@@ -139,4 +186,6 @@ async def to_code(config):
for key, value in entries.items():
cg.add(var.set(key, value))
if (default_value := config.get(CONF_DEFAULT_VALUE)) is not None:
cg.add(var.set_default_value(await metadata.to_.convert_value(default_value)))
return var
+9
View File
@@ -40,6 +40,9 @@ template<typename K, typename V> class Mapping {
if (it != this->map_.end()) {
return V{it->second};
}
if (this->default_value_.has_value()) {
return this->default_value_.value();
}
if constexpr (std::is_pointer_v<K>) {
esph_log_e(TAG, "Key '%p' not found in mapping", key);
} else if constexpr (std::is_same_v<K, std::string>) {
@@ -69,11 +72,17 @@ template<typename K, typename V> class Mapping {
if (it != this->map_.end()) {
return it->second.c_str(); // safe since value remains in map
}
if (this->default_value_.has_value()) {
return this->default_value_.value();
}
return "";
}
void set_default_value(const V &default_value) { this->default_value_ = default_value; }
protected:
std::map<key_t, value_t, std::less<key_t>, RAMAllocator<std::pair<key_t, value_t>>> map_;
std::optional<V> default_value_{};
};
} // namespace esphome::mapping
+39
View File
@@ -14,6 +14,7 @@ from esphome.const import (
from esphome.core import CORE, Lambda, coroutine_with_priority
from esphome.coroutine import CoroPriority
from esphome.cpp_generator import LambdaExpression
import esphome.final_validate as fv
from esphome.types import ConfigType
CODEOWNERS = ["@esphome/core"]
@@ -61,6 +62,28 @@ def _consume_mdns_sockets(config: ConfigType) -> ConfigType:
return config
def _require_network_interface(config: ConfigType) -> ConfigType:
"""Require a network interface for mDNS on Arduino/LEAmDNS platforms.
On ESP8266 and RP2040 the C++ implementation needs at least one IP state
listener (WiFi on ESP8266; WiFi or Ethernet on RP2040) to arm its polling
window. Reject at config time rather than silently producing a component
that never initializes.
"""
if config.get(CONF_DISABLED) or not (CORE.is_esp8266 or CORE.is_rp2040):
return config
full_config = fv.full_config.get()
has_wifi = "wifi" in full_config
has_ethernet = CORE.is_rp2040 and "ethernet" in full_config
if not (has_wifi or has_ethernet):
options = "'wifi'" if CORE.is_esp8266 else "'wifi' or 'ethernet'"
raise cv.Invalid(
"mdns on this platform requires a network interface — "
f"add a {options} component to your configuration."
)
return config
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -74,6 +97,9 @@ CONFIG_SCHEMA = cv.All(
)
FINAL_VALIDATE_SCHEMA = _require_network_interface
def mdns_txt_record(key: str, value: str) -> cg.RawExpression:
"""Create a mDNS TXT record.
@@ -169,6 +195,19 @@ async def to_code(config):
elif CORE.is_rp2040:
cg.add_library("LEAmDNS", None)
# Subscribe to the network IP state listener(s) so MDNS.update() is only
# scheduled during the probe+announce phase. Same on_ip_state() override
# serves both WiFi and Ethernet (signatures match).
if CORE.is_esp8266 or CORE.is_rp2040:
if "wifi" in CORE.config:
from esphome.components import wifi
wifi.request_wifi_ip_state_listener()
if CORE.is_rp2040 and "ethernet" in CORE.config:
from esphome.components import ethernet
ethernet.request_ethernet_ip_state_listener()
if CORE.is_esp32:
add_idf_component(name="espressif/mdns", ref="1.11.0")
+57 -24
View File
@@ -5,6 +5,22 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
// On ESP8266 and RP2040 the scheduler-backed MDNS.update() polling window is armed by
// IP state listener events on whichever network interface is configured.
#if (defined(USE_ESP8266) || defined(USE_RP2040)) && \
((defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS)) || \
(defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS)))
#include "esphome/components/network/ip_address.h"
#define USE_MDNS_EVENT_DRIVEN_POLLING
#if defined(USE_WIFI) && defined(USE_WIFI_IP_STATE_LISTENERS)
#include "esphome/components/wifi/wifi_component.h"
#define USE_MDNS_WIFI_LISTENER
#endif
#if defined(USE_ETHERNET) && defined(USE_ETHERNET_IP_STATE_LISTENERS)
#include "esphome/components/ethernet/ethernet_component.h"
#define USE_MDNS_ETHERNET_LISTENER
#endif
#endif
namespace esphome::mdns {
@@ -40,33 +56,40 @@ struct MDNSService {
FixedVector<MDNSTXTRecord> txt_records;
};
class MDNSComponent final : public Component {
class MDNSComponent final : public Component
#ifdef USE_MDNS_WIFI_LISTENER
,
public wifi::WiFiIPStateListener
#endif
#ifdef USE_MDNS_ETHERNET_LISTENER
,
public ethernet::EthernetIPStateListener
#endif
{
public:
void setup() override;
void dump_config() override;
// Polling interval for MDNS.update() on platforms that require it (ESP8266, RP2040).
//
// On these platforms, MDNS.update() calls _process(true) which only manages timer-driven
// state machines (probe/announce timeouts and service query cache TTLs). Incoming mDNS
// packets are handled independently via the lwIP onRx UDP callback and are NOT affected
// by how often update() is called.
//
// The shortest internal timer is the 250ms probe interval (RFC 6762 Section 8.1).
// Announcement intervals are 1000ms and cache TTL checks are on the order of seconds
// to minutes. A 50ms polling interval provides sufficient resolution for all timers
// while completely removing mDNS from the per-iteration loop list.
//
// In steady state (after the ~8 second boot probe/announce phase completes), update()
// checks timers that are set to never expire, making every call pure overhead.
//
// Tasmota uses a 50ms main loop cycle with mDNS working correctly, confirming this
// interval is safe in production.
//
// By using set_interval() instead of overriding loop(), the component is excluded from
// the main loop list via has_overridden_loop(), eliminating all per-iteration overhead
// including virtual dispatch.
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
// LEAmDNS has meaningful work only during the probe+announce phase (3×250ms probes +
// 8×1000ms announces, ~9s). Afterwards every internal timer is resetToNeverExpires()
// and update() becomes pure overhead. We arm a bounded polling window from IP state
// listener events so update() runs only during that phase.
static constexpr uint32_t MDNS_UPDATE_INTERVAL_MS = 50;
// Must exceed LEAmDNS's longest restart-to-announce-complete path:
// MDNS_PROBE_DELAY (250ms) × MDNS_PROBE_COUNT (3) = 750ms probing
// + MDNS_ANNOUNCE_DELAY (1000ms) × MDNS_ANNOUNCE_COUNT (8) = 8000ms announcing
// + rand() % MDNS_PROBE_DELAY jitter on first probe (0250ms)
// + debounced schedule_function() hop when statusChangeCB fires on ESP8266
// ≈ 9s nominal. 15s gives ~6s margin to absorb main-loop blocking (long
// component setup, WiFi scan, flash writes) that could stretch the deadlines
// between our polls. If LEAmDNS ever extends its phase (upstream library
// update) this constant needs to grow. Constants defined in LEAmDNS_Priv.h
// (ESP8266 core 3.1.2 / arduino-pico 5.5.1).
static constexpr uint32_t MDNS_POLL_WINDOW_MS = 15000;
static constexpr uint32_t MDNS_POLL_ID = 0;
static constexpr uint32_t MDNS_POLL_STOP_ID = 1;
#endif
float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; }
#ifdef USE_MDNS_EXTRA_SERVICES
@@ -87,7 +110,17 @@ class MDNSComponent final : public Component {
}
#endif
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void on_ip_state(const network::IPAddresses &ips, const network::IPAddress &dns1,
const network::IPAddress &dns2) override;
#endif
protected:
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
/// Arm a fresh MDNS_POLL_WINDOW_MS polling window. Idempotent — re-arming replaces
/// the previous window via the scheduler's atomic cancel-and-add on matching IDs.
void start_polling_window_();
#endif
/// Helper to set up services and MAC buffers, then call platform-specific registration
using PlatformRegisterFn = void (*)(MDNSComponent *, StaticVector<MDNSService, MDNS_SERVICE_COUNT> &);
@@ -130,8 +163,8 @@ class MDNSComponent final : public Component {
#ifdef USE_MDNS_STORE_SERVICES
StaticVector<MDNSService, MDNS_SERVICE_COUNT> services_{};
#endif
#ifdef USE_RP2040
bool was_connected_{false};
#if defined(USE_RP2040) && defined(USE_MDNS_EVENT_DRIVEN_POLLING)
// RP2040 defers MDNS.begin() until the first IP-up event; this tracks that.
bool initialized_{false};
#endif
void compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUNT> &services, char *mac_address_buf);
+28 -5
View File
@@ -8,6 +8,8 @@
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "mdns_component.h"
// wifi_component.h is pulled in transitively by mdns_component.h when
// USE_MDNS_WIFI_LISTENER is defined.
namespace esphome::mdns {
@@ -36,15 +38,36 @@ static void register_esp8266(MDNSComponent *, StaticVector<MDNSService, MDNS_SER
}
}
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::start_polling_window_() {
// uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add.
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
}
#endif
void MDNSComponent::setup() {
this->setup_buffers_and_register_(register_esp8266);
// Schedule MDNS.update() via set_interval() instead of overriding loop().
// This removes the component from the per-iteration loop list entirely,
// eliminating virtual dispatch overhead on every main loop cycle.
// See MDNS_UPDATE_INTERVAL_MS comment in mdns_component.h for safety analysis.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
#ifdef USE_MDNS_WIFI_LISTENER
// LEAmDNS's own LwipIntf::statusChangeCB drives _restart() on netif changes; we just
// arm the window around the initial probe/announce and each reconnect. Unconditional
// here is safe: setup_priority::AFTER_CONNECTION guarantees the network is up.
wifi::global_wifi_component->add_ip_state_listener(this);
this->start_polling_window_();
#endif
}
#ifdef USE_MDNS_WIFI_LISTENER
void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &,
const network::IPAddress &) {
// IP listener only fires on acquisition (not loss), so any notification is a fresh
// IP worth re-arming for. start_polling_window_() is idempotent.
if (ips[0].is_set()) {
this->start_polling_window_();
}
}
#endif
void MDNSComponent::on_shutdown() {
MDNS.close();
delay(10);
+54 -32
View File
@@ -6,9 +6,10 @@
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "mdns_component.h"
// wifi_component.h / ethernet_component.h are pulled in transitively by
// mdns_component.h when their respective listener defines are active.
// Arduino-Pico's PolledTimeout.h (pulled in by ESP8266mDNS.h) redefines IRAM_ATTR to empty.
// Save and restore our definition around the include to avoid a redefinition warning.
#pragma push_macro("IRAM_ATTR")
#undef IRAM_ATTR
#include <ESP8266mDNS.h>
@@ -20,10 +21,7 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
MDNS.begin(App.get_name().c_str());
for (const auto &service : services) {
// Strip the leading underscore from the proto and service_type. While it is
// part of the wire protocol to have an underscore, and for example ESP-IDF
// expects the underscore to be there, the ESP8266 implementation always adds
// the underscore itself.
// ESP8266mDNS always adds the leading underscore itself, so strip it here.
auto *proto = MDNS_STR_ARG(service.proto);
while (*proto == '_') {
proto++;
@@ -40,34 +38,58 @@ static void register_rp2040(MDNSComponent *, StaticVector<MDNSService, MDNS_SERV
}
}
void MDNSComponent::setup() {
// RP2040's LEAmDNS library registers a LwipIntf::stateUpCB() callback to restart
// mDNS when the network interface reconnects. However, stateUpCB() is stubbed out
// in arduino-pico's LwipIntfCB.cpp because the original ESP8266 implementation used
// schedule_function() which doesn't exist in arduino-pico, and the callback can't
// safely run directly since netif status callbacks fire from IRQ context
// (PICO_CYW43_ARCH_THREADSAFE_BACKGROUND) while _restart() allocates UDP sockets.
//
// Workaround: defer MDNS.begin() and service registration until the network is
// connected (has an IP), then call notifyAPChange() on subsequent reconnects to
// restart mDNS probing and announcing — all from main loop context so it's
// thread-safe.
this->set_interval(MDNS_UPDATE_INTERVAL_MS, [this]() {
bool connected = network::is_connected();
if (connected && !this->was_connected_) {
if (!this->initialized_) {
this->setup_buffers_and_register_(register_rp2040);
this->initialized_ = true;
} else {
MDNS.notifyAPChange();
}
}
this->was_connected_ = connected;
if (this->initialized_) {
MDNS.update();
}
});
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::start_polling_window_() {
// uint32_t-ID set_interval/set_timeout already does atomic cancel-and-add.
this->set_interval(MDNS_POLL_ID, MDNS_UPDATE_INTERVAL_MS, []() { MDNS.update(); });
this->set_timeout(MDNS_POLL_STOP_ID, MDNS_POLL_WINDOW_MS, [this]() { this->cancel_interval(MDNS_POLL_ID); });
}
#endif
void MDNSComponent::setup() {
// arduino-pico stubs out LwipIntf::stateUpCB (the netif status callback LEAmDNS uses
// on ESP8266 for auto-restart), so we must drive begin()/notifyAPChange() from our
// own IP state listener. Both WiFi and Ethernet have the same listener signature —
// one on_ip_state() override serves both.
#ifdef USE_MDNS_WIFI_LISTENER
wifi::global_wifi_component->add_ip_state_listener(this);
// AFTER_CONNECTION priority means the network may already be up; the listener only
// fires on subsequent changes, so seed the current state.
{
const auto ips = wifi::global_wifi_component->wifi_sta_ip_addresses();
if (ips[0].is_set()) {
this->on_ip_state(ips, wifi::global_wifi_component->get_dns_address(0),
wifi::global_wifi_component->get_dns_address(1));
}
}
#endif
#ifdef USE_MDNS_ETHERNET_LISTENER
ethernet::global_eth_component->add_ip_state_listener(this);
if (ethernet::global_eth_component->is_connected()) {
const auto ips = ethernet::global_eth_component->get_ip_addresses();
if (ips[0].is_set()) {
this->on_ip_state(ips, network::IPAddress{}, network::IPAddress{});
}
}
#endif
}
#ifdef USE_MDNS_EVENT_DRIVEN_POLLING
void MDNSComponent::on_ip_state(const network::IPAddresses &ips, const network::IPAddress &,
const network::IPAddress &) {
// Listener only fires on IP acquisition (not loss); every event is a fresh IP.
if (!ips[0].is_set()) {
return;
}
if (!this->initialized_) {
this->setup_buffers_and_register_(register_rp2040);
this->initialized_ = true;
} else {
MDNS.notifyAPChange();
}
this->start_polling_window_();
}
#endif
void MDNSComponent::on_shutdown() {
MDNS.close();
+2 -1
View File
@@ -21,6 +21,7 @@ from esphome.core import CORE
from esphome.core.entity_helpers import (
entity_duplicate_validator,
inherit_property_from,
queue_entity_register,
setup_entity,
)
from esphome.coroutine import CoroPriority, coroutine_with_priority
@@ -262,7 +263,7 @@ async def setup_media_player_core_(var, config):
async def register_media_player(var, config):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_media_player(var))
queue_entity_register("media_player", config)
CORE.register_platform_component("media_player", var)
await setup_media_player_core_(var, config)
@@ -352,7 +352,7 @@ void MitsubishiCN105::set_target_temperature(float target_temperature) {
ESP_LOGD(TAG, "Setting temperature out-of-range: %.1f", target_temperature);
return;
}
this->status_.target_temperature = std::round(target_temperature);
this->status_.target_temperature = target_temperature;
this->pending_updates_.set(UpdateFlag::TEMPERATURE);
}
@@ -387,9 +387,9 @@ void MitsubishiCN105::apply_settings_() {
if (this->pending_updates_.has(UpdateFlag::TEMPERATURE)) {
payload[1] |= 0x04;
if (this->use_temperature_encoding_b_) {
payload[14] = static_cast<uint8_t>(this->status_.target_temperature * 2.0f + 128.0f);
payload[14] = static_cast<uint8_t>(std::round(this->status_.target_temperature * 2.0f) + 128);
} else {
payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - this->status_.target_temperature);
payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - std::round(this->status_.target_temperature));
}
}
@@ -3,11 +3,8 @@ import binascii
from esphome import automation
import esphome.codegen as cg
from esphome.components import modbus
from esphome.components.const import CONF_ENABLED
from esphome.components.modbus.helpers import (
CPP_TYPE_REGISTER_MAP,
MODBUS_REGISTER_TYPE,
SENSOR_VALUE_TYPE,
TYPE_REGISTER_MAP,
ModbusRegisterType,
)
@@ -29,11 +26,10 @@ from .const import (
CONF_ON_OFFLINE,
CONF_ON_ONLINE,
CONF_REGISTER_COUNT,
CONF_REGISTER_LAST_ADDRESS,
CONF_REGISTER_TYPE,
CONF_REGISTER_VALUE,
CONF_RESPONSE_SIZE,
CONF_SERVER_COURTESY_RESPONSE,
CONF_SERVER_REGISTERS,
CONF_SKIP_UPDATES,
CONF_VALUE_TYPE,
)
@@ -42,9 +38,6 @@ CODEOWNERS = ["@martgras"]
AUTO_LOAD = ["modbus"]
CONF_READ_LAMBDA = "read_lambda"
CONF_WRITE_LAMBDA = "write_lambda"
CONF_SERVER_REGISTERS = "server_registers"
MULTI_CONF = True
modbus_controller_ns = cg.esphome_ns.namespace("modbus_controller")
@@ -53,30 +46,9 @@ ModbusController = modbus_controller_ns.class_(
)
SensorItem = modbus_controller_ns.struct("SensorItem")
ServerCourtesyResponse = modbus_controller_ns.struct("ServerCourtesyResponse")
ServerRegister = modbus_controller_ns.struct("ServerRegister")
_LOGGER = logging.getLogger(__name__)
SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=False): cv.boolean,
cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t,
cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t,
}
)
ModbusServerRegisterSchema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ServerRegister),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
@@ -85,12 +57,16 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(
CONF_COMMAND_THROTTLE, default="0ms"
): cv.positive_time_period_milliseconds,
cv.Optional(CONF_SERVER_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA,
cv.Optional(CONF_SERVER_COURTESY_RESPONSE): cv.invalid(
"This option has been removed. Use modbus_server component instead: https://esphome.io/components/modbus_server/"
),
cv.Optional(CONF_MAX_CMD_RETRIES, default=4): cv.positive_int,
cv.Optional(CONF_OFFLINE_SKIP_UPDATES, default=0): cv.positive_int,
cv.Optional(
CONF_SERVER_REGISTERS,
): cv.ensure_list(ModbusServerRegisterSchema),
): cv.invalid(
"This option has been removed. Use modbus_server component instead: https://esphome.io/components/modbus_server/"
),
cv.Optional(CONF_ON_COMMAND_SENT): automation.validate_automation({}),
cv.Optional(CONF_ON_ONLINE): automation.validate_automation({}),
cv.Optional(CONF_ON_OFFLINE): automation.validate_automation({}),
@@ -142,11 +118,9 @@ def validate_modbus_register(config):
def _final_validate(config):
if CONF_SERVER_COURTESY_RESPONSE in config or CONF_SERVER_REGISTERS in config:
return modbus.final_validate_modbus_device("modbus_controller", role="server")(
config
)
return config
return modbus.final_validate_modbus_device("modbus_controller", role="client")(
config
)
FINAL_VALIDATE_SCHEMA = _final_validate
@@ -228,53 +202,8 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_allow_duplicate_commands(config[CONF_ALLOW_DUPLICATE_COMMANDS]))
cg.add(var.set_command_throttle(config[CONF_COMMAND_THROTTLE]))
if server_courtesy_response := config.get(CONF_SERVER_COURTESY_RESPONSE):
cg.add(
var.set_server_courtesy_response(
cg.StructInitializer(
ServerCourtesyResponse,
("enabled", server_courtesy_response[CONF_ENABLED]),
(
"register_last_address",
server_courtesy_response[CONF_REGISTER_LAST_ADDRESS],
),
("register_value", server_courtesy_response[CONF_REGISTER_VALUE]),
)
)
)
cg.add(var.set_max_cmd_retries(config[CONF_MAX_CMD_RETRIES]))
cg.add(var.set_offline_skip_updates(config[CONF_OFFLINE_SKIP_UPDATES]))
if CONF_SERVER_REGISTERS in config:
for server_register in config[CONF_SERVER_REGISTERS]:
server_register_var = cg.new_Pvariable(
server_register[CONF_ID],
server_register[CONF_ADDRESS],
server_register[CONF_VALUE_TYPE],
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
)
cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]]
cg.add(
server_register_var.set_read_lambda(
cg.TemplateArguments(cpp_type),
await cg.process_lambda(
server_register[CONF_READ_LAMBDA],
[(cg.uint16, "address")],
return_type=cpp_type,
),
)
)
if CONF_WRITE_LAMBDA in server_register:
cg.add(
server_register_var.set_write_lambda(
cg.TemplateArguments(cpp_type),
await cg.process_lambda(
server_register[CONF_WRITE_LAMBDA],
parameters=[(cg.uint16, "address"), (cpp_type, "x")],
return_type=cg.bool_,
),
)
)
cg.add(var.add_server_register(server_register_var))
await register_modbus_device(var, config)
await automation.build_callback_automations(var, config, _CALLBACK_AUTOMATIONS)
@@ -18,6 +18,7 @@ CONF_REGISTER_TYPE = "register_type"
CONF_REGISTER_VALUE = "register_value"
CONF_RESPONSE_SIZE = "response_size"
CONF_SERVER_COURTESY_RESPONSE = "server_courtesy_response"
CONF_SERVER_REGISTERS = "server_registers"
CONF_SKIP_UPDATES = "skip_updates"
CONF_USE_WRITE_MULTIPLE = "use_write_multiple"
CONF_VALUE_TYPE = "value_type"
@@ -112,167 +112,6 @@ void ModbusController::on_modbus_error(uint8_t function_code, uint8_t exception_
}
}
void ModbusController::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
uint16_t number_of_registers) {
ESP_LOGD(TAG,
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
std::vector<uint16_t> sixteen_bit_response;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool found = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
if (!server_register->read_lambda) {
break;
}
int64_t value = server_register->read_lambda();
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
server_register->address, static_cast<size_t>(server_register->value_type),
server_register->register_count, server_register->format_value(value).c_str());
std::vector<uint16_t> payload;
payload.reserve(server_register->register_count * 2);
modbus::helpers::number_to_payload(payload, value, server_register->value_type);
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
current_address += server_register->register_count;
found = true;
break;
}
}
if (!found) {
if (this->server_courtesy_response_.enabled &&
(current_address <= this->server_courtesy_response_.register_last_address)) {
ESP_LOGD(TAG,
"Could not match any register to address 0x%02X, but default allowed. "
"Returning default value: %d.",
current_address, this->server_courtesy_response_.register_value);
sixteen_bit_response.push_back(this->server_courtesy_response_.register_value);
current_address += 1; // Just increment by 1, as the default response is a single register
} else {
ESP_LOGW(TAG,
"Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
current_address);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
}
}
std::vector<uint8_t> response;
for (auto v : sixteen_bit_response) {
auto decoded_value = decode_value(v);
response.push_back(decoded_value[0]);
response.push_back(decoded_value[1]);
}
this->send(function_code, start_address, number_of_registers, response.size(), response.data());
}
void ModbusController::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) {
uint16_t number_of_registers;
uint16_t payload_offset;
if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
if (data.size() < 5) {
ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size());
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
uint16_t payload_size = data[4];
if (payload_size != number_of_registers * 2) {
ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.",
payload_size, number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
if (data.size() < 5 + payload_size) {
ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(),
5 + payload_size);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
payload_offset = 5;
} else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
if (data.size() < 4) {
ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size());
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
number_of_registers = 1;
payload_offset = 2;
} else {
ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return;
}
uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8);
ESP_LOGD(TAG,
"Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
auto for_each_register = [this, start_address, number_of_registers, payload_offset](
const std::function<bool(ServerRegister *, uint16_t offset)> &callback) -> bool {
uint16_t offset = payload_offset;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool ok = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
ok = callback(server_register, offset);
current_address += server_register->register_count;
offset += server_register->register_count * sizeof(uint16_t);
break;
}
}
if (!ok) {
return false;
}
}
return true;
};
// check all registers are writable before writing to any of them:
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
return server_register->write_lambda != nullptr;
})) {
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return;
}
// Actually write to the registers:
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
return server_register->write_lambda(number);
})) {
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
return;
}
std::vector<uint8_t> response;
response.reserve(6);
response.push_back(this->address_);
response.push_back(function_code);
response.insert(response.end(), data.begin(), data.begin() + 4);
this->send_raw(response);
}
SensorSet ModbusController::find_sensors_(ModbusRegisterType register_type, uint16_t start_address) const {
auto reg_it = std::find_if(
std::begin(this->register_ranges_), std::end(this->register_ranges_),
@@ -472,14 +311,8 @@ void ModbusController::dump_config() {
"ModbusController:\n"
" Address: 0x%02X\n"
" Max Command Retries: %d\n"
" Offline Skip Updates: %d\n"
" Server Courtesy Response:\n"
" Enabled: %s\n"
" Register Last Address: 0x%02X\n"
" Register Value: %d",
this->address_, this->max_cmd_retries_, this->offline_skip_updates_,
this->server_courtesy_response_.enabled ? "true" : "false",
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
" Offline Skip Updates: %d\n",
this->address_, this->max_cmd_retries_, this->offline_skip_updates_);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGCONFIG(TAG, "sensormap");
@@ -493,11 +326,6 @@ void ModbusController::dump_config() {
ESP_LOGCONFIG(TAG, " Range type=%u start=0x%X count=%d skip_updates=%d", static_cast<uint8_t>(it.register_type),
it.start_address, it.register_count, it.skip_updates);
}
ESP_LOGCONFIG(TAG, "server registers");
for (auto &r : this->server_registers_) {
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
static_cast<uint8_t>(r->value_type), r->register_count);
}
#endif
}
@@ -120,82 +120,6 @@ class SensorItem {
bool force_new_range{false};
};
struct ServerCourtesyResponse {
bool enabled{false};
uint16_t register_last_address{0xFFFF};
uint16_t register_value{0};
};
class ServerRegister {
using ReadLambda = std::function<int64_t()>;
using WriteLambda = std::function<bool(int64_t value)>;
public:
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) {
this->address = address;
this->value_type = value_type;
this->register_count = register_count;
}
template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
this->read_lambda = [this, user_read_lambda]() -> int64_t {
T user_value = user_read_lambda(this->address);
if constexpr (std::is_same_v<T, float>) {
return bit_cast<uint32_t>(user_value);
} else {
return static_cast<int64_t>(user_value);
}
};
}
template<typename T>
void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
this->write_lambda = [this, user_write_lambda](int64_t number) {
if constexpr (std::is_same_v<T, float>) {
float float_value = bit_cast<float>(static_cast<uint32_t>(number));
return user_write_lambda(this->address, float_value);
}
return user_write_lambda(this->address, static_cast<T>(number));
};
}
// Formats a raw value into a string representation based on the value type for debugging
std::string format_value(int64_t value) const {
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
// plus null terminator = 43, rounded to 44 for 4-byte alignment
char buf[44];
switch (this->value_type) {
case SensorValueType::U_WORD:
case SensorValueType::U_DWORD:
case SensorValueType::U_DWORD_R:
case SensorValueType::U_QWORD:
case SensorValueType::U_QWORD_R:
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
return buf;
case SensorValueType::S_WORD:
case SensorValueType::S_DWORD:
case SensorValueType::S_DWORD_R:
case SensorValueType::S_QWORD:
case SensorValueType::S_QWORD_R:
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
return buf;
case SensorValueType::FP32_R:
case SensorValueType::FP32:
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
return buf;
default:
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
return buf;
}
}
uint16_t address{0};
SensorValueType value_type{SensorValueType::RAW};
uint8_t register_count{0};
ReadLambda read_lambda;
WriteLambda write_lambda;
};
// ModbusController::create_register_ranges_ tries to optimize register range
// for this the sensors must be ordered by register_type, start_address and bitmask
class SensorItemsComparator {
@@ -367,16 +291,10 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
void queue_command(const ModbusCommandItem &command);
/// Registers a sensor with the controller. Called by esphomes code generator
void add_sensor_item(SensorItem *item) { sensorset_.insert(item); }
/// Registers a server register with the controller. Called by esphomes code generator
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
/// called when a modbus response was parsed without errors
void on_modbus_data(const std::vector<uint8_t> &data) override;
/// called when a modbus error response was received
void on_modbus_error(uint8_t function_code, uint8_t exception_code) override;
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
/// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
/// default delegate called by process_modbus_data when a response has retrieved from the incoming queue
void on_register_data(ModbusRegisterType register_type, uint16_t start_address, const std::vector<uint8_t> &data);
/// default delegate called by process_modbus_data when a response for a write response has retrieved from the
@@ -413,12 +331,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
void set_max_cmd_retries(uint8_t max_cmd_retries) { this->max_cmd_retries_ = max_cmd_retries; }
/// get how many times a command will be (re)sent if no response is received
uint8_t get_max_cmd_retries() { return this->max_cmd_retries_; }
/// Called by esphome generated code to set the server courtesy response object
void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
this->server_courtesy_response_ = server_courtesy_response;
}
/// Get the server courtesy response object
ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
protected:
/// parse sensormap_ and create range of sequential addresses
@@ -435,8 +347,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
void dump_sensors_();
/// Collection of all sensors for this component
SensorSet sensorset_;
/// Collection of all server registers for this component
std::vector<ServerRegister *> server_registers_{};
/// Continuous range of modbus registers
std::vector<RegisterRange> register_ranges_{};
/// Hold the pending requests to be sent
@@ -461,9 +371,6 @@ class ModbusController : public PollingComponent, public modbus::ModbusDevice {
CallbackManager<void(int, int)> online_callback_{};
/// Server offline callback
CallbackManager<void(int, int)> offline_callback_{};
/// Server courtesy response
ServerCourtesyResponse server_courtesy_response_{
.enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
};
/** Convert vector<uint8_t> response payload to float.
@@ -0,0 +1,124 @@
import esphome.codegen as cg
from esphome.components import modbus
from esphome.components.const import CONF_ENABLED
from esphome.components.modbus.helpers import (
CPP_TYPE_REGISTER_MAP,
SENSOR_VALUE_TYPE,
TYPE_REGISTER_MAP,
)
import esphome.config_validation as cv
from esphome.const import CONF_ADDRESS, CONF_ID
from .const import (
CONF_COURTESY_RESPONSE,
CONF_READ_LAMBDA,
CONF_REGISTER_LAST_ADDRESS,
CONF_REGISTER_VALUE,
CONF_REGISTERS,
CONF_VALUE_TYPE,
CONF_WRITE_LAMBDA,
)
CODEOWNERS = ["@exciton"]
AUTO_LOAD = ["modbus"]
MULTI_CONF = True
modbus_server_ns = cg.esphome_ns.namespace("modbus_server")
ModbusServer = modbus_server_ns.class_(
"ModbusServer", cg.Component, modbus.ModbusDevice
)
ServerCourtesyResponse = modbus_server_ns.struct("ServerCourtesyResponse")
ServerRegister = modbus_server_ns.struct("ServerRegister")
SERVER_COURTESY_RESPONSE_SCHEMA = cv.Schema(
{
cv.Optional(CONF_ENABLED, default=False): cv.boolean,
cv.Optional(CONF_REGISTER_LAST_ADDRESS, default=0xFFFF): cv.hex_uint16_t,
cv.Optional(CONF_REGISTER_VALUE, default=0): cv.hex_uint16_t,
}
)
ModbusServerRegisterSchema = cv.Schema(
{
cv.GenerateID(): cv.declare_id(ServerRegister),
cv.Required(CONF_ADDRESS): cv.positive_int,
cv.Optional(CONF_VALUE_TYPE, default="U_WORD"): cv.enum(SENSOR_VALUE_TYPE),
cv.Required(CONF_READ_LAMBDA): cv.returning_lambda,
cv.Optional(CONF_WRITE_LAMBDA): cv.returning_lambda,
}
)
CONFIG_SCHEMA = cv.All(
cv.Schema(
{
cv.GenerateID(): cv.declare_id(ModbusServer),
cv.Optional(CONF_COURTESY_RESPONSE): SERVER_COURTESY_RESPONSE_SCHEMA,
cv.Optional(
CONF_REGISTERS,
): cv.ensure_list(ModbusServerRegisterSchema),
}
).extend(modbus.modbus_device_schema(0x01)),
)
def _final_validate(config):
return modbus.final_validate_modbus_device("modbus_server", role="server")(config)
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
if server_courtesy_response := config.get(CONF_COURTESY_RESPONSE):
cg.add(
var.set_server_courtesy_response(
cg.StructInitializer(
ServerCourtesyResponse,
("enabled", server_courtesy_response[CONF_ENABLED]),
(
"register_last_address",
server_courtesy_response[CONF_REGISTER_LAST_ADDRESS],
),
("register_value", server_courtesy_response[CONF_REGISTER_VALUE]),
)
)
)
if CONF_REGISTERS in config:
for server_register in config[CONF_REGISTERS]:
server_register_var = cg.new_Pvariable(
server_register[CONF_ID],
server_register[CONF_ADDRESS],
server_register[CONF_VALUE_TYPE],
TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]],
)
cpp_type = CPP_TYPE_REGISTER_MAP[server_register[CONF_VALUE_TYPE]]
cg.add(
server_register_var.set_read_lambda(
cg.TemplateArguments(cpp_type),
await cg.process_lambda(
server_register[CONF_READ_LAMBDA],
[(cg.uint16, "address")],
return_type=cpp_type,
),
)
)
if CONF_WRITE_LAMBDA in server_register:
cg.add(
server_register_var.set_write_lambda(
cg.TemplateArguments(cpp_type),
await cg.process_lambda(
server_register[CONF_WRITE_LAMBDA],
parameters=[(cg.uint16, "address"), (cpp_type, "x")],
return_type=cg.bool_,
),
)
)
cg.add(var.add_server_register(server_register_var))
cg.add(var.set_address(config[CONF_ADDRESS]))
await cg.register_component(var, config)
return await modbus.register_modbus_device(var, config)
@@ -0,0 +1,7 @@
CONF_REGISTER_LAST_ADDRESS = "register_last_address"
CONF_REGISTER_VALUE = "register_value"
CONF_VALUE_TYPE = "value_type"
CONF_COURTESY_RESPONSE = "courtesy_response"
CONF_READ_LAMBDA = "read_lambda"
CONF_WRITE_LAMBDA = "write_lambda"
CONF_REGISTERS = "registers"
@@ -0,0 +1,192 @@
#include "modbus_server.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome::modbus_server {
using modbus::ModbusFunctionCode;
using modbus::ModbusExceptionCode;
static const char *const TAG = "modbus_server";
void ModbusServer::on_modbus_read_registers(uint8_t function_code, uint16_t start_address,
uint16_t number_of_registers) {
ESP_LOGD(TAG,
"Received read holding/input registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_READ) {
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
std::vector<uint16_t> sixteen_bit_response;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool found = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
if (!server_register->read_lambda) {
break;
}
int64_t value = server_register->read_lambda();
ESP_LOGD(TAG, "Matched register. Address: 0x%02X. Value type: %zu. Register count: %u. Value: %s.",
server_register->address, static_cast<size_t>(server_register->value_type),
server_register->register_count, server_register->format_value(value).c_str());
std::vector<uint16_t> payload;
payload.reserve(server_register->register_count * 2);
modbus::helpers::number_to_payload(payload, value, server_register->value_type);
sixteen_bit_response.insert(sixteen_bit_response.end(), payload.cbegin(), payload.cend());
current_address += server_register->register_count;
found = true;
break;
}
}
if (!found) {
if (this->server_courtesy_response_.enabled &&
(current_address <= this->server_courtesy_response_.register_last_address)) {
ESP_LOGD(TAG,
"Could not match any register to address 0x%02X, but default allowed. "
"Returning default value: %d.",
current_address, this->server_courtesy_response_.register_value);
sixteen_bit_response.push_back(this->server_courtesy_response_.register_value);
current_address += 1; // Just increment by 1, as the default response is a single register
} else {
ESP_LOGW(TAG,
"Could not match any register to address 0x%02X and default not allowed. Sending exception response.",
current_address);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_ADDRESS);
return;
}
}
}
std::vector<uint8_t> response;
for (auto v : sixteen_bit_response) {
auto decoded_value = decode_value(v);
response.push_back(decoded_value[0]);
response.push_back(decoded_value[1]);
}
this->send(function_code, start_address, number_of_registers, response.size(), response.data());
}
void ModbusServer::on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) {
uint16_t number_of_registers;
uint16_t payload_offset;
if (function_code == ModbusFunctionCode::WRITE_MULTIPLE_REGISTERS) {
if (data.size() < 5) {
ESP_LOGW(TAG, "Write multiple registers data too short (%zu bytes)", data.size());
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
number_of_registers = uint16_t(data[3]) | (uint16_t(data[2]) << 8);
if (number_of_registers == 0 || number_of_registers > modbus::MAX_NUM_OF_REGISTERS_TO_WRITE) {
ESP_LOGW(TAG, "Invalid number of registers %d. Sending exception response.", number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
uint16_t payload_size = data[4];
if (payload_size != number_of_registers * 2) {
ESP_LOGW(TAG, "Payload size of %d bytes is not 2 times the number of registers (%d). Sending exception response.",
payload_size, number_of_registers);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
if (data.size() < 5 + payload_size) {
ESP_LOGW(TAG, "Write multiple registers payload truncated (%zu bytes, expected %u)", data.size(),
5 + payload_size);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
payload_offset = 5;
} else if (function_code == ModbusFunctionCode::WRITE_SINGLE_REGISTER) {
if (data.size() < 4) {
ESP_LOGW(TAG, "Write single register data too short (%zu bytes)", data.size());
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_DATA_VALUE);
return;
}
number_of_registers = 1;
payload_offset = 2;
} else {
ESP_LOGW(TAG, "Invalid function code 0x%X. Sending exception response.", function_code);
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return;
}
uint16_t start_address = uint16_t(data[1]) | (uint16_t(data[0]) << 8);
ESP_LOGD(TAG,
"Received write holding registers for device 0x%X. FC: 0x%X. Start address: 0x%X. Number of registers: "
"0x%X.",
this->address_, function_code, start_address, number_of_registers);
auto for_each_register = [this, start_address, number_of_registers, payload_offset](
const std::function<bool(ServerRegister *, uint16_t offset)> &callback) -> bool {
uint16_t offset = payload_offset;
for (uint16_t current_address = start_address; current_address < start_address + number_of_registers;) {
bool ok = false;
for (auto *server_register : this->server_registers_) {
if (server_register->address == current_address) {
ok = callback(server_register, offset);
current_address += server_register->register_count;
offset += server_register->register_count * sizeof(uint16_t);
break;
}
}
if (!ok) {
return false;
}
}
return true;
};
// check all registers are writable before writing to any of them:
if (!for_each_register([](ServerRegister *server_register, uint16_t offset) -> bool {
return server_register->write_lambda != nullptr;
})) {
this->send_error(function_code, ModbusExceptionCode::ILLEGAL_FUNCTION);
return;
}
// Actually write to the registers:
if (!for_each_register([&data](ServerRegister *server_register, uint16_t offset) {
int64_t number = modbus::helpers::payload_to_number(data, server_register->value_type, offset, 0xFFFFFFFF);
return server_register->write_lambda(number);
})) {
this->send_error(function_code, ModbusExceptionCode::SERVICE_DEVICE_FAILURE);
return;
}
std::vector<uint8_t> response;
response.reserve(6);
response.push_back(this->address_);
response.push_back(function_code);
response.insert(response.end(), data.begin(), data.begin() + 4);
this->send_raw(response);
}
void ModbusServer::dump_config() {
ESP_LOGCONFIG(TAG,
"ModbusServer:\n"
" Address: 0x%02X\n"
" Server Courtesy Response:\n"
" Enabled: %s\n"
" Register Last Address: 0x%02X\n"
" Register Value: %" PRIu16,
this->address_, this->server_courtesy_response_.enabled ? "true" : "false",
this->server_courtesy_response_.register_last_address, this->server_courtesy_response_.register_value);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
ESP_LOGCONFIG(TAG, "server registers");
for (auto &r : this->server_registers_) {
ESP_LOGCONFIG(TAG, " Address=0x%02X value_type=%u register_count=%u", r->address,
static_cast<uint8_t>(r->value_type), r->register_count);
}
#endif
}
} // namespace esphome::modbus_server
@@ -0,0 +1,119 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/modbus/modbus.h"
#include "esphome/components/modbus/modbus_helpers.h"
#include "esphome/core/automation.h"
#include <utility>
#include <vector>
namespace esphome::modbus_server {
using modbus::helpers::SensorValueType;
struct ServerCourtesyResponse {
bool enabled{false};
uint16_t register_last_address{0xFFFF};
uint16_t register_value{0};
};
class ServerRegister {
using ReadLambda = std::function<int64_t()>;
using WriteLambda = std::function<bool(int64_t value)>;
public:
ServerRegister(uint16_t address, SensorValueType value_type, uint8_t register_count) {
this->address = address;
this->value_type = value_type;
this->register_count = register_count;
}
template<typename T> void set_read_lambda(const std::function<T(uint16_t address)> &&user_read_lambda) {
this->read_lambda = [this, user_read_lambda]() -> int64_t {
T user_value = user_read_lambda(this->address);
if constexpr (std::is_same_v<T, float>) {
return bit_cast<uint32_t>(user_value);
} else {
return static_cast<int64_t>(user_value);
}
};
}
template<typename T>
void set_write_lambda(const std::function<bool(uint16_t address, const T v)> &&user_write_lambda) {
this->write_lambda = [this, user_write_lambda](int64_t number) {
if constexpr (std::is_same_v<T, float>) {
float float_value = bit_cast<float>(static_cast<uint32_t>(number));
return user_write_lambda(this->address, float_value);
}
return user_write_lambda(this->address, static_cast<T>(number));
};
}
// Formats a raw value into a string representation based on the value type for debugging
std::string format_value(int64_t value) const {
// max 44: float with %.1f can be up to 42 chars (3.4e38 → 39 integer digits + sign + decimal + 1 digit)
// plus null terminator = 43, rounded to 44 for 4-byte alignment
char buf[44];
switch (this->value_type) {
case SensorValueType::U_WORD:
case SensorValueType::U_DWORD:
case SensorValueType::U_DWORD_R:
case SensorValueType::U_QWORD:
case SensorValueType::U_QWORD_R:
buf_append_printf(buf, sizeof(buf), 0, "%" PRIu64, static_cast<uint64_t>(value));
return buf;
case SensorValueType::S_WORD:
case SensorValueType::S_DWORD:
case SensorValueType::S_DWORD_R:
case SensorValueType::S_QWORD:
case SensorValueType::S_QWORD_R:
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
return buf;
case SensorValueType::FP32_R:
case SensorValueType::FP32:
buf_append_printf(buf, sizeof(buf), 0, "%.1f", bit_cast<float>(static_cast<uint32_t>(value)));
return buf;
default:
buf_append_printf(buf, sizeof(buf), 0, "%" PRId64, value);
return buf;
}
}
uint16_t address{0};
SensorValueType value_type{SensorValueType::RAW};
uint8_t register_count{0};
ReadLambda read_lambda;
WriteLambda write_lambda;
};
class ModbusServer : public Component, public modbus::ModbusDevice {
public:
void dump_config() override;
/// Not used for ModbusServer.
void on_modbus_data(const std::vector<uint8_t> &data) override{};
/// Registers a server register with the controller. Called by esphomes code generator
void add_server_register(ServerRegister *server_register) { server_registers_.push_back(server_register); }
/// called when a modbus request (function code 0x03 or 0x04) was parsed without errors
void on_modbus_read_registers(uint8_t function_code, uint16_t start_address, uint16_t number_of_registers) final;
/// called when a modbus request (function code 0x06 or 0x10) was parsed without errors
void on_modbus_write_registers(uint8_t function_code, const std::vector<uint8_t> &data) final;
/// Called by esphome generated code to set the server courtesy response object
void set_server_courtesy_response(const ServerCourtesyResponse &server_courtesy_response) {
this->server_courtesy_response_ = server_courtesy_response;
}
/// Get the server courtesy response object
ServerCourtesyResponse get_server_courtesy_response() const { return this->server_courtesy_response_; }
protected:
/// Collection of all server registers for this component
std::vector<ServerRegister *> server_registers_{};
/// Server courtesy response
ServerCourtesyResponse server_courtesy_response_{
.enabled = false, .register_last_address = 0xFFFF, .register_value = 0};
};
} // namespace esphome::modbus_server
@@ -16,6 +16,13 @@ namespace esphome::nextion {
static const char *const TAG = "nextion.upload.arduino";
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
// Timeout for display acknowledgment during TFT upload (ms).
// A single value is used for all chunks; the happy path returns as soon as
// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field
// reports showed the previous 500ms steady-state value was too tight for
// some firmware variants.
static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000;
// Followed guide
// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
@@ -80,14 +87,14 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) {
recv_string.clear();
this->write_array(buffer, buffer_size);
App.feed_wdt();
this->recv_ret_string_(recv_string, this->upload_first_chunk_sent_ ? 500 : 5000, true);
this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true);
this->content_length_ -= read_len;
const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
ESP_LOGD(TAG, "Upload: %0.2f%% (%" PRIu32 " left, heap: %" PRIu32 ")", upload_percentage, this->content_length_,
EspClass::getFreeHeap());
this->upload_first_chunk_sent_ = true;
if (recv_string.empty()) {
ESP_LOGW(TAG, "No response from display during upload");
ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS);
allocator.deallocate(buffer, 4096);
buffer = nullptr;
return -1;
@@ -324,7 +331,7 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
#ifdef USE_ESP8266
WiFiClient *Nextion::get_wifi_client_() {
if (this->tft_url_.compare(0, 6, "https:") == 0) {
if (this->tft_url_.starts_with("https:")) {
if (this->wifi_client_secure_ == nullptr) {
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
this->wifi_client_secure_ = new BearSSL::WiFiClientSecure();
@@ -19,6 +19,13 @@ namespace esphome::nextion {
static const char *const TAG = "nextion.upload.esp32";
static constexpr size_t NEXTION_MAX_RESPONSE_LOG_BYTES = 16;
// Timeout for display acknowledgment during TFT upload (ms).
// A single value is used for all chunks; the happy path returns as soon as
// 0x05/0x08 arrives, so this only bounds failed-detection latency. Field
// reports showed the previous 500ms steady-state value was too tight for
// some firmware variants.
static constexpr uint32_t NEXTION_UPLOAD_ACK_TIMEOUT_MS = 5000;
// Followed guide
// https://unofficialnextion.com/t/nextion-upload-protocol-v1-2-the-fast-one/1044/2
@@ -96,7 +103,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
recv_string.clear();
this->write_array(buffer, buffer_size);
App.feed_wdt();
this->recv_ret_string_(recv_string, upload_first_chunk_sent_ ? 500 : 5000, true);
this->recv_ret_string_(recv_string, NEXTION_UPLOAD_ACK_TIMEOUT_MS, true);
this->content_length_ -= read_len;
const float upload_percentage = 100.0f * (this->tft_size_ - this->content_length_) / this->tft_size_;
#ifdef USE_PSRAM
@@ -109,7 +116,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r
#endif
upload_first_chunk_sent_ = true;
if (recv_string.empty()) {
ESP_LOGW(TAG, "No response from display during upload");
ESP_LOGW(TAG, "No response from display after %" PRIu32 "ms", NEXTION_UPLOAD_ACK_TIMEOUT_MS);
allocator.deallocate(buffer, 4096);
buffer = nullptr;
return -1;
+20 -8
View File
@@ -141,6 +141,22 @@ CONF_UICR_ERASE = "uicr_erase"
VOLTAGE_LEVELS = [1.8, 2.1, 2.4, 2.7, 3.0, 3.3]
_DFU_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate),
cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
)
def _dfu_schema(value: bool | ConfigType) -> ConfigType:
if isinstance(value, bool):
if not value:
raise cv.Invalid("Use 'dfu: true' or specify a configuration dict")
return _DFU_SCHEMA({})
return _DFU_SCHEMA(value)
CONFIG_SCHEMA = cv.All(
_detect_bootloader,
set_core_data,
@@ -150,12 +166,7 @@ CONFIG_SCHEMA = cv.All(
cv.string_strict, cv.ByteLength(max=BOARD_MAX_LENGTH)
),
cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True),
cv.Optional(CONF_DFU): cv.Schema(
{
cv.GenerateID(): cv.declare_id(DeviceFirmwareUpdate),
cv.Required(CONF_RESET_PIN): pins.gpio_output_pin_schema,
}
),
cv.Optional(CONF_DFU): _dfu_schema,
cv.Optional(CONF_DCDC, default=True): cv.boolean,
cv.Optional(CONF_REG0): cv.Schema(
{
@@ -321,8 +332,9 @@ async def to_code(config: ConfigType) -> None:
async def _dfu_to_code(dfu_config):
cg.add_define("USE_NRF52_DFU")
var = cg.new_Pvariable(dfu_config[CONF_ID])
pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(pin))
if CONF_RESET_PIN in dfu_config:
pin = await cg.gpio_pin_expression(dfu_config[CONF_RESET_PIN])
cg.add(var.set_reset_pin(pin))
zephyr_add_prj_conf("CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True)
await cg.register_component(var, dfu_config)
+20 -7
View File
@@ -2,24 +2,34 @@
#ifdef USE_NRF52_DFU
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "esphome/components/zephyr/cdc_acm.h"
namespace esphome {
namespace nrf52 {
#include <hal/nrf_power.h>
namespace esphome::nrf52 {
static const char *const TAG = "dfu";
static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS
static const uint8_t DFU_MAGIC_UF2_RESET = 0x57; // Adafruit nRF52 bootloader UF2 magic
void DeviceFirmwareUpdate::setup() {
this->reset_pin_->setup();
if (this->reset_pin_ != nullptr) {
this->reset_pin_->setup();
}
#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT)
zephyr::global_cdc_acm->add_on_rate_callback([this](const device *, uint32_t rate) {
if (rate == 1200) {
volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C;
(*dbl_reset_mem) = DFU_DBL_RESET_MAGIC;
this->reset_pin_->digital_write(true);
if (this->reset_pin_ != nullptr) {
this->reset_pin_->digital_write(true);
} else {
NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET;
App.reboot();
}
}
});
#endif
@@ -27,10 +37,13 @@ void DeviceFirmwareUpdate::setup() {
void DeviceFirmwareUpdate::dump_config() {
ESP_LOGCONFIG(TAG, "DFU:");
LOG_PIN(" RESET Pin: ", this->reset_pin_);
if (this->reset_pin_ != nullptr) {
LOG_PIN(" RESET Pin: ", this->reset_pin_);
} else {
ESP_LOGCONFIG(TAG, " Method: GPREGRET");
}
}
} // namespace nrf52
} // namespace esphome
} // namespace esphome::nrf52
#endif
+3 -5
View File
@@ -5,8 +5,7 @@
#include "esphome/core/component.h"
#include "esphome/core/gpio.h"
namespace esphome {
namespace nrf52 {
namespace esphome::nrf52 {
class DeviceFirmwareUpdate : public Component {
public:
void setup() override;
@@ -14,10 +13,9 @@ class DeviceFirmwareUpdate : public Component {
void dump_config() override;
protected:
GPIOPin *reset_pin_;
GPIOPin *reset_pin_{nullptr};
};
} // namespace nrf52
} // namespace esphome
} // namespace esphome::nrf52
#endif
+2 -1
View File
@@ -82,6 +82,7 @@ from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core.config import UNIT_OF_MEASUREMENT_MAX_LENGTH
from esphome.core.entity_helpers import (
entity_duplicate_validator,
queue_entity_register,
setup_device_class,
setup_entity,
setup_unit_of_measurement,
@@ -301,7 +302,7 @@ async def register_number(
):
if not CORE.has_id(config[CONF_ID]):
var = cg.Pvariable(config[CONF_ID], var)
cg.add(cg.App.register_number(var))
queue_entity_register("number", config)
CORE.register_platform_component("number", var)
await setup_number_core_(
var, config, min_value=min_value, max_value=max_value, step=step
@@ -28,7 +28,7 @@ bool OnlineImage::validate_url_(const std::string &url) {
ESP_LOGE(TAG, "URL is too long");
return false;
}
if (url.compare(0, 7, "http://") != 0 && url.compare(0, 8, "https://") != 0) {
if (!url.starts_with("http://") && !url.starts_with("https://")) {
ESP_LOGE(TAG, "URL must start with http:// or https://");
return false;
}
+9 -1
View File
@@ -54,10 +54,16 @@ async def setup_output_platform_(obj, config):
power_supply_ = await cg.get_variable(config[CONF_POWER_SUPPLY])
cg.add(obj.set_power_supply(power_supply_))
if CONF_MAX_POWER in config:
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
cg.add(obj.set_max_power(config[CONF_MAX_POWER]))
if CONF_MIN_POWER in config:
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
cg.add(obj.set_min_power(config[CONF_MIN_POWER]))
if CONF_ZERO_MEANS_ZERO in config:
# Only emit when zero_means_zero is actually enabled. The schema defaults to False
# so this key is always present; emitting unconditionally would force
# USE_OUTPUT_FLOAT_POWER_SCALING on for every output, defeating the gate.
if config.get(CONF_ZERO_MEANS_ZERO):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
cg.add(obj.set_zero_means_zero(config[CONF_ZERO_MEANS_ZERO]))
@@ -121,6 +127,7 @@ async def output_set_level_to_code(config, action_id, template_arg, args):
synchronous=True,
)
async def output_set_min_power_to_code(config, action_id, template_arg, args):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MIN_POWER], args, cg.float_)
@@ -140,6 +147,7 @@ async def output_set_min_power_to_code(config, action_id, template_arg, args):
synchronous=True,
)
async def output_set_max_power_to_code(config, action_id, template_arg, args):
cg.add_define("USE_OUTPUT_FLOAT_POWER_SCALING")
paren = await cg.get_variable(config[CONF_ID])
var = cg.new_Pvariable(action_id, template_arg, paren)
template_ = await cg.templatable(config[CONF_MAX_POWER], args, cg.float_)
+3
View File
@@ -2,6 +2,7 @@
#include "esphome/core/component.h"
#include "esphome/core/automation.h"
#include "esphome/core/defines.h"
#include "esphome/components/output/binary_output.h"
#include "esphome/components/output/float_output.h"
@@ -40,6 +41,7 @@ template<typename... Ts> class SetLevelAction : public Action<Ts...> {
FloatOutput *output_;
};
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
template<typename... Ts> class SetMinPowerAction : public Action<Ts...> {
public:
SetMinPowerAction(FloatOutput *output) : output_(output) {}
@@ -63,6 +65,7 @@ template<typename... Ts> class SetMaxPowerAction : public Action<Ts...> {
protected:
FloatOutput *output_;
};
#endif // USE_OUTPUT_FLOAT_POWER_SCALING
} // namespace output
} // namespace esphome
+6 -2
View File
@@ -7,13 +7,15 @@ namespace output {
static const char *const TAG = "output.float";
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
void FloatOutput::set_max_power(float max_power) {
this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to MIN>=MAX>=1.0
this->max_power_ = clamp(max_power, this->min_power_, 1.0f); // Clamp to min_power <= max <= 1.0
}
void FloatOutput::set_min_power(float min_power) {
this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0>=MIN>=MAX
this->min_power_ = clamp(min_power, 0.0f, this->max_power_); // Clamp to 0.0 <= min <= max_power
}
#endif
void FloatOutput::set_level(float state) {
state = clamp(state, 0.0f, 1.0f);
@@ -26,8 +28,10 @@ void FloatOutput::set_level(float state) {
}
#endif
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
if (state != 0.0f || !this->zero_means_zero_) // regardless of min_power_, 0.0 means off
state = (state * (this->max_power_ - this->min_power_)) + this->min_power_;
#endif
if (this->is_inverted())
state = 1.0f - state;
+48 -3
View File
@@ -1,11 +1,13 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "binary_output.h"
namespace esphome {
namespace output {
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
#define LOG_FLOAT_OUTPUT(this) \
LOG_BINARY_OUTPUT(this) \
if (this->max_power_ != 1.0f) { \
@@ -14,6 +16,9 @@ namespace output {
if (this->min_power_ != 0.0f) { \
ESP_LOGCONFIG(TAG, " Min Power: %.1f%%", this->min_power_ * 100.0f); \
}
#else
#define LOG_FLOAT_OUTPUT(this) LOG_BINARY_OUTPUT(this)
#endif
/** Base class for all output components that can output a variable level, like PWM.
*
@@ -22,14 +27,18 @@ namespace output {
* makes using maths much easier and (in theory) supports all possible bit depths.
*
* If you want to create a FloatOutput yourself, you essentially just have to override write_state(float).
* That method will be called for you with inversion and max-min power and offset to min power already applied.
* That method will be called for you with inversion already applied. When USE_OUTPUT_FLOAT_POWER_SCALING is
* enabled (set automatically by Python codegen if any output uses min_power/max_power/zero_means_zero or the
* matching runtime actions), the value will additionally have max-min power scaling and offset to min_power
* applied; otherwise only inversion is applied.
*
* This interface is compatible with BinaryOutput (and will automatically convert the binary states to floating
* point states for you). Additionally, this class provides a way for users to set a minimum and/or maximum power
* output
* output (gated on USE_OUTPUT_FLOAT_POWER_SCALING).
*/
class FloatOutput : public BinaryOutput {
public:
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
/** Set the maximum power output of this component.
*
* All values are multiplied by max_power - min_power and offset to min_power to get the adjusted value.
@@ -51,6 +60,32 @@ class FloatOutput : public BinaryOutput {
* @param zero_means_zero True if a 0 state should mean 0 and not min_power.
*/
void set_zero_means_zero(bool zero_means_zero) { this->zero_means_zero_ = zero_means_zero; }
#else
// Compile-time guards for users calling these methods from lambdas (documented usage at
// https://esphome.io/components/output/#output-set_min_power_action). When power scaling
// is compiled out, these template stubs fail to compile with an actionable error pointing
// at the user's lambda. Templating on a default-false bool means static_assert only fires
// on instantiation (i.e. when the user actually calls the method), not on every parse.
template<bool _use_output_float_power_scaling = false> void set_max_power(float max_power) {
static_assert(_use_output_float_power_scaling,
"set_max_power() requires USE_OUTPUT_FLOAT_POWER_SCALING. "
"To enable it, add 'max_power: 100%' (or any value) to one output entry in your YAML — "
"the codegen will then keep the scaling fields. "
"See https://esphome.io/components/output/ for details.");
}
template<bool _use_output_float_power_scaling = false> void set_min_power(float min_power) {
static_assert(_use_output_float_power_scaling,
"set_min_power() requires USE_OUTPUT_FLOAT_POWER_SCALING. "
"To enable it, add 'min_power: 0%' (or any value) to one output entry in your YAML — "
"the codegen will then keep the scaling fields. "
"See https://esphome.io/components/output/ for details.");
}
template<bool _use_output_float_power_scaling = false> void set_zero_means_zero(bool zero_means_zero) {
static_assert(_use_output_float_power_scaling,
"set_zero_means_zero() requires USE_OUTPUT_FLOAT_POWER_SCALING. "
"To enable it, add 'zero_means_zero: true' to one output entry in your YAML.");
}
#endif
/** Set the level of this float output, this is called from the front-end.
*
@@ -69,20 +104,30 @@ class FloatOutput : public BinaryOutput {
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
/// Get the maximum power output.
float get_max_power() const { return this->max_power_; }
/// Get the minimum power output.
float get_min_power() const { return this->min_power_; }
#else
/// Get the maximum power output.
float get_max_power() const { return 1.0f; }
/// Get the minimum power output.
float get_min_power() const { return 0.0f; }
#endif
protected:
/// Implement BinarySensor's write_enabled; this should never be called.
void write_state(bool state) override;
virtual void write_state(float state) = 0;
#ifdef USE_OUTPUT_FLOAT_POWER_SCALING
float max_power_{1.0f};
float min_power_{0.0f};
bool zero_means_zero_;
bool zero_means_zero_{false};
#endif
};
} // namespace output
+24 -15
View File
@@ -205,7 +205,7 @@ CONFIG_SCHEMA = cv.Any( # under `packages:` we can have either:
)
def _process_remote_package(config: dict, skip_update: bool = False) -> dict:
def _process_remote_package(config: dict[str, Any]) -> dict[str, Any]:
"""Clone/update a git repo and load the YAML files listed in the package definition.
Returns ``{"packages": {<filename>: <loaded_yaml>, ...}}`` so the caller
@@ -215,11 +215,10 @@ def _process_remote_package(config: dict, skip_update: bool = False) -> dict:
If loading fails after cloning, attempts a revert and retry in case
a prior cached checkout is stale.
"""
actual_refresh = git.NEVER_REFRESH if skip_update else config[CONF_REFRESH]
repo_dir, revert = git.clone_or_update(
url=config[CONF_URL],
ref=config.get(CONF_REF),
refresh=actual_refresh,
refresh=config[CONF_REFRESH],
domain=DOMAIN,
username=config.get(CONF_USERNAME),
password=config.get(CONF_PASSWORD),
@@ -378,9 +377,8 @@ def _substitute_package_definition(
Local package contents are left untouched they will be substituted
later during the main substitution pass.
"""
if isinstance(package_config, str) or (
isinstance(package_config, dict) and is_remote_package(package_config)
):
def do_substitute(package_config: dict | str) -> dict | str:
# Collect undefined-variable errors (rather than raising strict) so the
# path walked through a remote-package dict is preserved and the user
# sees which field (url / path / ref / ...) referenced the undefined
@@ -394,6 +392,22 @@ def _substitute_package_definition(
errors=errors,
)
raise_first_undefined(errors, "package definition")
return package_config
if isinstance(package_config, str):
return do_substitute(package_config)
if isinstance(package_config, dict) and is_remote_package(package_config):
# Mark vars as literal to avoid substituting variables in the vars block itself, since they are meant to be
# passed as-is to the package YAML and may contain their own substitution expressions that should not
# be prematurely evaluated here.
if CONF_FILES in package_config:
for file_def in package_config[CONF_FILES]:
if isinstance(file_def, dict) and CONF_VARS in file_def:
file_def[CONF_VARS] = yaml_util.make_literal(file_def[CONF_VARS])
package_config = do_substitute(package_config)
return package_config
@@ -441,11 +455,9 @@ class _PackageProcessor:
self,
substitutions: UserDict,
command_line_substitutions: dict[str, Any] | None,
skip_update: bool,
) -> None:
self.substitutions = substitutions
self.parent_context = UserDict(command_line_substitutions or {})
self.skip_update = skip_update
def resolve_package(
self,
@@ -493,7 +505,7 @@ class _PackageProcessor:
)
if is_remote_package(package_config):
package_config = _process_remote_package(package_config, self.skip_update)
package_config = _process_remote_package(package_config)
return package_config
def collect_substitutions(self, package_config: dict) -> None:
@@ -537,11 +549,10 @@ class _PackageProcessor:
def do_packages_pass(
config: dict,
config: dict[str, Any],
*,
command_line_substitutions: dict[str, Any] | None = None,
skip_update: bool = False,
) -> dict:
) -> dict[str, Any]:
"""Load, validate, and flatten all packages in the config.
Returns the config with all packages loaded in-place (but not yet merged)
@@ -556,9 +567,7 @@ def do_packages_pass(
config.pop(CONF_SUBSTITUTIONS, {}), command_line_substitutions
)
)
processor = _PackageProcessor(
substitutions, command_line_substitutions, skip_update
)
processor = _PackageProcessor(substitutions, command_line_substitutions)
_update_substitutions_context(processor.parent_context, substitutions)
context_vars = push_context(
+7 -3
View File
@@ -433,13 +433,17 @@ void Pipsolar::handle_qpigs_(const char *message) {
}
void Pipsolar::handle_qmod_(const char *message) {
std::string mode;
char device_mode = char(message[1]);
if (this->last_qmod_) {
this->last_qmod_->publish_state(message);
}
// QMOD response is "(M" where M is the device-mode character. Bail out if the
// message is shorter than 2 chars (e.g. empty error response from
// handle_poll_error_) — reading message[1] would otherwise be out of bounds.
if (message[0] == '\0' || message[1] == '\0')
return;
if (this->device_mode_) {
mode = device_mode;
std::string mode;
mode = char(message[1]);
this->device_mode_->publish_state(mode);
}
}
+2 -1
View File
@@ -317,6 +317,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) {
if (!this->rd_start_time_.has_value()) {
this->rd_start_time_ = millis();
}
const uint32_t rd_start_time = *this->rd_start_time_;
while (true) {
if (this->is_read_ready()) {
@@ -324,7 +325,7 @@ enum PN532ReadReady PN532::read_ready_(bool block) {
break;
}
if (millis() - *this->rd_start_time_ > 100) {
if (millis() - rd_start_time > 100) {
ESP_LOGV(TAG, "Timed out waiting for readiness from PN532!");
this->rd_ready_ = TIMEOUT;
break;

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