8000
Skip to content

[zigbee] Add on_start automation (3/3) - #18009

Merged
swoboda1337 merged 28 commits into
esphome:devfrom
luar123:on_startup
Aug 6, 2026
Merged

[zigbee] Add on_start automation (3/3)#18009
swoboda1337 merged 28 commits into
esphome:devfrom
luar123:on_startup

Conversation

@luar123
@luar123 luar123 commented Aug 1, 2026
Copy link
Copy Markdown
Contributor

What does this implement/fix?

Add on_start automation. On esp32 register_device is very slow and runs outside of setup, so it makes sense to have a signal once the stack is fully initialized and could be interacted with. Will be used by the time component.

Third PR, depends on #18008

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • New developer-facing feature (adds functionality for component developers; no end-user configuration change)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected) — policy
  • Developer breaking change (an API change that could break external components) — policy
  • Undocumented C++ API change (removal or change of undocumented public methods that lambda users may depend on) — policy
  • Code quality improvements to existing code or addition of tests
  • Other

Related issue or feature (if applicable):

  • fixes

Pull request in esphome.io with documentation (if applicable):

Pull request in developers.esphome.io with developer documentation (if applicable):

  • esphome/developers.esphome.io#<developers.esphome.io PR number goes here>

Test Environment

  • ESP32
  • ESP32 IDF
  • ESP8266
  • RP2040/RP2350
  • BK72xx
  • RTL87xx
  • LN882x
  • nRF52840

Example entry for config.yaml:

# Example config.yaml
zigbee:
  on_start:
    then:
      - logger.log: "started zigbee"

Checklist:

  • The code change is tested and works locally.
  • Tests have been added to verify that the new code works (under tests/ folder).

If user exposed functionality or configuration variables are added/changed:

@esphome
esphome Bot commented Aug 1, 2026
Copy link
Copy Markdown
Contributor

To use the changes from this PR as an external component, add the following to your ESPHome configuration YAML file:

external_components:
  - source: github://pr#18009
    components: [zigbee]
    refresh: 1h

(Added by the PR bot)

@codecov
codecov Bot commented Aug 1, 2026
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.28%. Comparing base (4ccf421) to head (7fcd7dd).
⚠️ Report is 2 commits behind head on dev.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##              dev   #18009   +/-   ##
=======================================
  Coverage   87.28%   87.28%           
=======================================
  Files          64       64           
  Lines       14697    14697           
  Branches     2217     2217           
=======================================
+ Hits        12828    12829    +1     
+ Misses       1560     1558    -2     
- Partials      309      310    +1     

see 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions
github-actions Bot commented Aug 1, 2026
Copy link
Copy Markdown
Contributor

Memory Impact Analysis

Components: zigbee
Platform: nrf52-adafruit

📊 Component Memory Breakdown
Metric Target Branch This PR Change
RAM 66,320 bytes 66,344 bytes 📈 🔸 +24 bytes (+0.04%)
Flash 314,660 bytes 315,156 bytes 📈 🔸 +496 bytes (+0.16%)
Component Target Flash PR Flash Change
app_framework 3,916 bytes 4,080 bytes 📈 +164 bytes (+4.19%)
[esphome]core 6,685 bytes 6,835 bytes 📈 🔸 +150 bytes (+2.24%)
[esphome]zigbee 4,372 bytes 4,520 bytes 📈 🚨 +148 bytes (+3.39%)
🔍 Symbol-Level Changes (click to expand)

Changed Symbols

Symbol Target Size PR Size Change
setup() 2,344 bytes 2,488 bytes 📈 +144 bytes (+6.14%)
esphome::zigbee::ZigbeeComponent::zboss_signal_handler_esphome(unsigned char) 408 bytes 424 bytes 📈 +16 bytes (+3.92%)
setup()::{lambda()#2}::_FUN() 16 bytes 20 bytes 📈 +4 bytes (+25.00%)
zigbee__zigbee_zigbeecomponent_id__pstorage 168 bytes 172 bytes 📈 +4 bytes (+2.38%)
esphome::zigbee::ZigbeeComponent::on_join_(bool) 48 bytes 44 bytes 📉 -4 bytes (-8.33%)

New Symbols (top 15)

Symbol Size
std::Function_handler<void (), esphome::zigbee::ZigbeeComponent::on_start()::{lambda()#1}>::M...std::Function_handler<void (), esphome::zigbee::ZigbeeComponent::on_start()::{lambda()#1}>::_M_invoke(std::_Any_data const&)
60 bytes
esphome::zigbee::ZigbeeComponent::on_start_() 44 bytes
esphome::Action<>::play_complex() 42 bytes
esphome::Action<>::stop_complex() 34 bytes
std::Function_handler<void (), esphome::zigbee::ZigbeeComponent::on_start()::{lambda()#1}>::M...std::Function_handler<void (), esphome::zigbee::ZigbeeComponent::on_start()::{lambda()#1}>::_M_manager(std::_Any_data&, std::_Any_data const&, std::_Manager_operation)
32 bytes
vtable for esphome::StatelessLambdaAction<> 28 bytes
esphome::Action<>::is_running() 20 bytes
esphome::Callback<void ()>::create<esphome::TriggerForwarder<> >(esphome::TriggerForwarder<>&&)::...esphome::Callback<void ()>::create<esphome::TriggerForwarder<> >(esphome::TriggerForwarder<>&&)::{lambda(void*)#1}::_FUN(void*)
20 bytes
setup()::{lambda()#5}::_FUN() 16 bytes
esphome__lambdaaction_id_2__pstorage 16 bytes
CSWTCH.39 12 bytes
esphome__automation_id_3__pstorage 4 bytes
esphome::StatelessLambdaAction<>::play() 4 bytes
esphome::Action<>::stop() 2 bytes

Removed Symbols (top 15)

Symbol Size
CSWTCH.35 12 bytes

Note: This analysis measures static RAM and Flash usage only (compile-time allocation).
Dynamic memory (heap) cannot be measured automatically.
⚠️ You must test this PR on a real device to measure free heap and ensure no runtime memory issues.

This analysis runs automatically when components change. Memory usage is measured from a representative test configuration.

@luar123
luar123 commented Aug 1, 2026
Copy link
Copy Markdown
Contributor Author

@tomaszduda23 please have a look at the nrf52 implementation and if possible test it. Is ZB_ZDO_SIGNAL_SKIP_STARTUP the first signal that is emitted or is this skipped on nrf52?

@luar123 luar123 changed the title [zigbee] Add on_startup automation [zigbee] Add on_start automation Aug 1, 2026
@luar123 luar123 changed the title [zigbee] Add on_start automation [zigbee] Add on_start automation (3/3) Aug 1, 2026
@tomaszduda23
Copy link
Copy Markdown
Contributor

I don't have cordinator to test right now. The clean start looks like this

[15:40:46.661][D][zigbee:035][0x20002178]: ZB_ZDO_SIGNAL_PRODUCTION_CONFIG_READY, status: -1
[15:40:46.664][D][zigbee:032][0x20002178]: ZB_ZDO_SIGNAL_SKIP_STARTUP, status: 0
[15:40:46.668][D][zigbee:052][0x20002178]: ZB_BDB_SIGNAL_DEVICE_FIRST_START, status: 0

@luar123
luar123 commented Aug 2, 2026
Copy link
Copy Markdown
Contributor Author

Thanks, then it should work.

@luar123
luar123 marked this pull request as ready for review August 6, 2026 08:22
@luar123
luar123 requested a review from tomaszduda23 as a code owner August 6, 2026 08:22
@esphome
esphome Bot commented Aug 6, 2026
Copy link
Copy Markdown
Contributor

👋 Hi there! I've automatically requested reviews from codeowners based on the files changed in this PR.

@tomaszduda23 - You've been requested to review this PR as codeowner(s) of 5 file(s) that were modified. Thanks for your time! 🙏

@esphome esphome Bot added small-pr PR < 30 lines and removed medium-pr PR < 100 lines labels Aug 6, 2026
@swoboda1337
swoboda1337 requested a lite review from Copilot August 6, 2026 14:37
@swoboda1337
Copy link
Copy Markdown
Member

@esphbot review

Copilot AI left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new on_start automation hook to the Zigbee component so users (and dependent components like time) can run actions once the Zigbee stack is fully initialized, addressing the ESP32 case where initialization can complete outside setup().

Changes:

  • Adds on_start: to the Zigbee YAML schema and wires it to a new callback automation.
  • Implements add_on_start_callback() and triggers the callback when the stack reaches its “started” state (ESP32 + Zephyr/Nordic).
  • Extends Zigbee component test YAMLs to cover the new on_start configuration path.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
tests/components/zigbee/common_nrf52.yaml Adds zigbee.on_start to NRF52 test config.
tests/components/zigbee/common_esp32.yaml Adds zigbee.on_start to ESP32 test config.
esphome/components/zigbee/zigbee_zephyr.h Adds start callback registration API and storage.
esphome/components/zigbee/zigbee_zephyr.cpp Triggers on_start on Zephyr signal handler path.
esphome/components/zigbee/zigbee_esp32.h Adds start callback registration API and state flag to fire once.
esphome/components/zigbee/zigbee_esp32.cpp Fires on_start once after started_ becomes true; ensures loop is scheduled from the signal handler.
esphome/components/zigbee/init.py Exposes on_start in config schema and registers the callback automation.

Comment thread esphome/components/zigbee/zigbee_zephyr.cpp
@esphbot
esphbot commented Aug 6, 2026
Copy link
Copy Markdown
Contributor

@tomaszduda23: I don't have cordinator to test right now. The clean start looks like this

Log confirms two things. SKIP_STARTUP arrives with status 0, before DEVICE_FIRST_START. So ordering works, and gating on status == RET_OK costs nothing. Note ESP32 handler ignores status as well (zigbee_esp32.cpp:65-68). Gate both platforms or neither.

@esphbot
esphbot commented Aug 6, 2026
Copy link
Copy Markdown
Contributor

PR Review — [zigbee] Add on_start automation (3/3)

Merge-ready. Clean, minimal implementation of on_start; only two non-blocking nits.

Specific things done well:

  • The ESP32 side gets the threading right. started_ is set in the Zigbee task, then enable_loop_soon_any_context() is called, and the callbacks fire from loop() on the main thread — so user automations never run in the radio task's context. I traced the wakeup path: Application::enable_pending_loops_ keeps pending_enable_loop_ set when the component is not yet in LOOP_DONE (application.cpp:481-484), so the request raised from the Zigbee task cannot be swallowed by the disable_loop() at the end of setup() or by the one at the end of loop(). No lost-wakeup race.
  • start_reported_ runs the callbacks exactly once and the existing self-disabling loop() means the added check costs nothing at steady state.
  • The Zephyr side reuses the established defer() pattern from on_join_, keeping both platforms' callbacks on the main loop.
  • The Python side plugs straight into the existing _CALLBACK_AUTOMATIONS table with args=[], so no Trigger class and no new CONF_TRIGGER_ID — matches the preferred build_callback_automation path. Both platform test YAMLs were updated.

What needs attention (both suggestions):

  • Zephyr fires on_start without checking status, and without ESP32's once-guard. Note the ESP32 handler ignores status too, so fix both or neither — Copilot's suggestion applied to Zephyr alone would make the platforms disagree.
  • start_cb_ could be a LazyCallbackManager per the repo's own container guidance, since on_start is usually unset.

🟢 Suggestions

1. on_start fires regardless of SKIP_STARTUP status (and no once-guard on Zephyr)
esphome/components/zigbee/zigbee_zephyr.cpp:33

Two small asymmetries with the ESP32 path, both non-blocking:

  • Status is ignored. Copilot flagged this too. ZB_BDB_SIGNAL_DEVICE_REBOOT right below gates on status == RET_OK, this branch does not. The log @tomaszduda23 posted shows ZB_ZDO_SIGNAL_SKIP_STARTUP, status: 0 on a clean boot, so in practice it is RET_OK; a non-OK status here would mean the stack failed to reach the "initialized, autostart skipped" point and firing user automations then is misleading. Worth noting the ESP32 handler is also unconditional (zigbee_esp32.cpp:65-68 sets started_ = true without checking status), so if you take Copilot's advice, gate both — gating only Zephyr makes the two platforms disagree on what on_start means.

  • No once-guard. ESP32 protects against a repeat with start_reported_; Zephyr calls the callbacks every time the signal arrives. ZB_ZDO_SIGNAL_SKIP_STARTUP should only be emitted once per boot, so this is theoretical, but a guard would make the contract identical on both platforms.

Minor style point on the same line: the repo convention is to prefix member access with this->. The adjacent on_join_(false) calls omit it too, so this is consistent with surrounding code — mentioning only because the line is new.

    case ZB_ZDO_SIGNAL_SKIP_STARTUP:
      ESP_LOGD(TAG, "ZB_ZDO_SIGNAL_SKIP_STARTUP, status: %d", status);
      on_start_();
      break;
2. start_cb_ could be a LazyCallbackManager
esphome/components/zigbee/zigbee_esp32.h:93

on_start is optional and most configs will not define it, which is exactly the case LazyCallbackManager exists for — it stores a nullptr (4 bytes) instead of an empty std::vector (12 bytes) until the first callback is added. AGENTS.md calls this out explicitly ("Prefer it for entity-level callbacks where most instances have no subscribers") and it is already used across button/, climate/, cover/, etc.

Savings are only ~8 bytes here since ZigbeeComponent is a singleton, so this is a nit, not a blocker. The registration method is already correctly templatized, so it is a drop-in type swap in both zigbee_esp32.h and zigbee_zephyr.h.

  CallbackManager<void()> start_cb_{};
  bool start_reported_{false};

Checklist

  • Thread-safety of cross-context callback dispatch
  • No lost wakeup between enable_loop_soon_any_context() and disable_loop()
  • Callbacks fire exactly once per boot — suggestion #1
  • Error/failure status handled on the signal path — suggestion #1
  • Embedded memory guidelines (container choice) — suggestion #2
  • Follows preferred build_callback_automation trigger pattern
  • Test YAML updated for both supported platforms
  • Diff matches PR description, no scope creep

Automated review by Kōan (Claude) HEAD=d72dc81 4 min 53s

@esphbot esphbot left a comment
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tip

No blocking issues found — ready to merge.

Comment thread esphome/components/zigbee/zigbee_esp32.h
@luar123
luar123 commented Aug 6, 2026
Copy link
Copy Markdown
Contributor Author

There is no status on esp32 for the skip_startup signal. Could add it on nrf and also the once-guard, but I guess that is more a theoretical issue.

@swoboda1337
Copy link
Copy Markdown
Member

CI is failing

@luar123
luar123 commented Aug 6, 2026
Copy link
Copy Markdown
Contributor Author

yes, github having github issues....

@swoboda1337
Copy link
Copy Markdown
Member

Yeah GH is still broken

@swoboda1337
swoboda1337 merged commit 4c47051 into esphome:dev Aug 6, 2026
42 checks passed
@swoboda1337
Copy link
Copy Markdown
Member

Thanks

@luar123
luar123 deleted the on_startup branch August 7, 2026 05:08
@github-actions github-actions Bot locked and limited conversation to collaborators Aug 8, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants

0