8000
Skip to content

[ota] Replace std::function callbacks with listener interface - #12167

Merged
bdraco merged 13 commits into
devfrom
ota_listeners
Dec 19, 2025
Merged

[ota] Replace std::function callbacks with listener interface#12167
bdraco merged 13 commits into
devfrom
ota_listeners

Conversation

@bdraco
@bdraco bdraco commented Nov 28, 2025
Copy link
Copy Markdown
Member

What does this implement/fix?

Refactors the OTA component to use listener interfaces instead of std::function callbacks, following the pattern established in #12155 for WiFi.

Changes

Memory and Performance Benefits

  • Replaces std::function<void(OTAState, 8000 float, uint8_t)> (16+ bytes each) with OTAStateListener* pointers (4 bytes each)
  • Virtual function dispatch (~2 cycles) vs std::function indirection (~6+ cycles)
  • Eliminates std::function template instantiation overhead from firmware
  • Measured savings: 352 bytes flash on a real-world config (the CI memory impact analysis is not accurate for this PR since it merges all affected components into one config which doesn't reflect typical usage and it doesn't handle the defines correctly)
Baseline: RAM: 40324 bytes, Flash: 898135 bytes
After:    RAM: 40324 bytes, Flash: 897783 bytes
Savings:  352 bytes flash

Architecture

New Listener Interface:

class OTAStateListener {
 public:
  virtual void on_ota_state(OTAState state, float progress, uint8_t error) = 0;
};

OTA Component Changes:

  • OTAComponent now maintains std::vector<OTAStateListener*> instead of CallbackManager
  • Added add_state_listener(OTAStateListener*) for registering listeners
  • Added notify_state_() for notifications (also notifies global listeners automatically)
  • Added notify_state_deferred_() for thread-safe notifications from separate tasks

Trigger Updates:

  • All OTA triggers (OTAStartTrigger, OTAEndTrigger, OTAProgressTrigger, etc.) now implement OTAStateListener interface directly
  • Simple state triggers use a template class OTAStateTrigger<OTAState State> to reduce code duplication

Global Listener Updates:

  • esp32_ble_tracker, micro_wake_word, and speaker_media_player now implement OTAGlobalStateListener interface instead of using lambda callbacks
  • These components properly guard OTA code with #ifdef USE_OTA_STATE_LISTENER

Consumer Updates:

  • HttpRequestUpdate now implements OTAStateListener instead of using lambda callback
  • esphome and http_request OTA use notify_state_()
  • web_server OTA uses notify_state_deferred_() for thread safety

Python Changes

  • Renamed USE_OTA_STATE_CALLBACK to USE_OTA_STATE_LISTENER
  • Added request_ota_state_listeners() function for components to request the feature
  • Feature is only compiled in when actually needed (on-demand)
  • Components should use ota.request_ota_state_listeners() instead of adding cg.add_define("USE_OTA_STATE_LISTENER") directly - this is more future-proof as it allows the OTA component to manage the feature enablement

Types of changes

  • Bugfix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Developer breaking change (an API change that could break external components)
  • Code quality improvements to existing code or addition of tests
  • Other

Related issue or feature (if applicable):

Pull request in esphome-docs with documentation (if applicable):

  • N/A (internal API change)

Test Environment

  • ESP32
  • ESP32 IDF
  • ESP8266
  • RP2040
  • BK72xx
  • RTL87xx
  • nRF52840

Example entry for config.yaml:

# No configuration changes - this is an internal refactoring
# Existing OTA configurations continue to work unchanged

ota:
  - platform: esphome
    on_begin:
      - logger.log: "OTA starting"
    on_progress:
      - logger.log:
          format: "OTA progress: %.1f%%"
          args: ['x']
    on_end:
      - logger.log: "OTA complete"
    on_error:
      - logger.log: "OTA failed"

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:

  • Documentation added/updated in esphome-docs. (N/A - no user-facing changes)

Migration Guide for External Components

If you have an external component that uses the OTA state callback API:

Before:

ota->add_on_state_callback([](ota::OTAState state, float progress, uint8_t error) {
  // handle state
});

After:

class MyComponent : public ota::OTAStateListener {
 public:
  void on_ota_state(ota::OTAState state, float progress, uint8_t error) override {
    // handle state
  }
};

// In setup:
ota->add_state_listener(this);

For global OTA state (listening to any OTA platform):

Before:

#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
    [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
      // handle state
    });
#endif

After:

// In header - inherit from OTAGlobalStateListener:
#ifdef USE_OTA_STATE_LISTENER
class MyComponent : public Component, public ota::OTAGlobalStateListener {
#else
class MyComponent : public Component {
#endif
 public:
#ifdef USE_OTA_STATE_LISTENER
  void on_ota_global_state(ota::OTAState state, float progress, uint8_t error,
                           ota::OTAComponent *comp) override;
#endif
};

// In cpp - register and implement:
#ifdef USE_OTA_STATE_LISTENER
  ota::get_global_ota_callback()->add_global_state_listener(this);
#endif

#ifdef USE_OTA_STATE_LISTENER
void MyComponent::on_ota_global_state(ota::OTAState state, float progress, uint8_t error,
                                      ota::OTAComponent *comp) {
  // handle state
}
#endif

Python code generation:

# Call this to enable the listener feature
ota.request_ota_state_listeners()

@bdraco
bdraco marked this pull request as ready for review November 28, 2025 19:46
@bdraco
bdraco requested a review from a team as a code owner November 28, 2025 19:46
Copilot AI review requested due to automatic review settings November 28, 2025 19:46
@github-actions
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#12167
    components: [esp32_ble_tracker, esphome, http_request, micro_wake_word, ota, speaker, web_server]
    refresh: 1h

(Added by the PR bot)

@bdraco
bdraco marked this pull request as draft November 28, 2025 19:46
@github-actions
Copy link
Copy Markdown
Contributor

👋 Hi there! This PR modifies 13 file(s) with codeowners.

@oarcher, @jesserockz, @kahrendt, @synesthesiam - As codeowner(s) of the affected files, your review would be appreciated! 🙏

Note: Automatic review request may have failed, but you're still welcome to review.

@codecov-commenter
codecov-commenter commented Nov 28, 2025
Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 72.66%. Comparing base (2b337aa) to head (813c986).

Additional details and impacted files
@@           Coverage Diff           @@
##              dev   #12167   +/-   ##
=======================================
  Coverage   72.66%   72.66%           
=======================================
  Files          53       53           
  Lines       11193    11193           
  Branches     1517     1517           
=======================================
  Hits         8133     8133           
  Misses       2667     2667           
  Partials      393      393           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

< 8000 div data-view-component="true" class="TimelineItem-body">
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

This PR refactors the OTA component to replace std::function callbacks with listener interfaces, following the pattern established in PR #12155 for WiFi. This change reduces memory footprint (16+ bytes per callback down to 4 bytes per pointer) and improves performance through more efficient virtual function dispatch.

Key Changes:

  • Introduces OTAStateListener and OTAGlobalStateListener interfaces for per-component and global OTA state notifications
  • Replaces CallbackManager with std::vector<OTAStateListener*> in OTAComponent
  • Updates all OTA triggers to implement OTAStateListener directly instead of using lambdas
  • Adds notify_state_() and notify_state_deferred_() methods for thread-safe notifications

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
esphome/core/defines.h Renames USE_OTA_STATE_CALLBACK to USE_OTA_STATE_LISTENER
esphome/components/ota/ota_backend.h Introduces listener interfaces and updates OTA component API
esphome/components/ota/ota_backend.cpp Implements bridge pattern for global listeners
esphome/components/ota/automation.h Converts all OTA triggers to implement OTAStateListener
esphome/components/ota/init.py Adds request_ota_state_listeners() for on-demand feature compilation
esphome/components/http_request/update/* Updates to use listener interface
esphome/components/http_request/ota/* Updates to use notify_state_()
esphome/components/esphome/ota/* Updates to use notify_state_()
esphome/components/web_server/ota/* Updates to use notify_state_deferred_() for thread safety
esphome/components/speaker/media_player/init.py Updates to use request_ota_state_listeners()
esphome/components/micro_wake_word/init.py Updates to use request_ota_state_listeners()
esphome/components/esp32_ble_tracker/init.py Updates to use request_ota_state_listeners()

You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

Comment thread esphome/components/ota/ota_backend.h Outdated
Comment thread esphome/components/ota/automation.h Outdated
Comment thread esphome/components/ota/automation.h
Comment thread esphome/components/ota/automation.h
Comment thread esphome/components/ota/automation.h
Comment thread esphome/components/ota/automation.h
Comment thread esphome/components/ota/ota_backend.h
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

Copilot reviewed 20 out of 20 changed files in this pull request and generated 2 comments.


You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

Comment thread esphome/components/ota/ota_backend.h
Comment thread esphome/components/ota/ota_backend.h
@bdraco
bdraco commented Nov 28, 2025
Copy link
Copy Markdown
Member Author

esp32_ble_tracker disconnect still works fine

[14:15:49.236][I][esp32_ble_client:137]: [0] [] Disconnect requested, but already IDLE
[14:15:49.239][I][esp32_ble_client:137]: [1] [] Disconnect requested, but already IDLE
[14:15:49.241][I][esp32_ble_client:137]: [2] [] Disconnect requested, but already IDLE
[14:15:49.242][I][esp32_ble_client:137]: [3] [] Disconnect requested, but already IDLE

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

Copilot reviewed 20 out of 20 changed files in this pull request and generated no new comments.


You can also share your feedback on Copilot code review for a chance to win a $100 gift card. Take the survey.

@github-actions
github-actions Bot commented Nov 28, 2025
Copy link
Copy Markdown
Contributor

Memory Impact Analysis

Components: esp32_ble_tracker, esphome, http_request, micro_wake_word, ota, speaker, web_server
Platform: esp32-idf

Metric Target Branch This PR Change
RAM 70,620 bytes 70,620 bytes ➡️ +0 bytes (0.00%)
Flash 1,970,410 bytes 1,970,470 bytes 📈 🔸 +60 bytes (+0.00%)

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 merged configuration with 7 components.

@bdraco
bdraco commented Nov 28, 2025
Copy link
Copy Markdown
Member Author

web server ota callbacks with defer ok as well

[14:38:13.018][D][esp32_ble_client:142]: [0] [49:22:08:15:00:BB] Disconnect before connected, disconnect scheduled
[14:38:13.139][I][esp32_ble_client:137]: [1] [] Disconnect requested, but already IDLE
[14:38:13.142][I][esp32_ble_client:137]: [2] [] Disconnect requested, but already IDLE
[14:38:13.145][I][esp32_ble_client:137]: [3] [] Disconnect requested, but already IDLE
[14:38:13.148][W][component:481]: web_server.ota took a long time for an operation (4561 ms)
[14:38:13.151][W][component:484]: Components should block for at most 30 ms
[14:38:13.154][W][component:481]: api took a long time for an operation (4567 ms)
[14:38:13.198][W][component:484]: Components should block for at most 30 ms
[14:38:13.206][I][web_server.ota:104][httpd]: OTA Update Start: firmware.bin
[14:38:13.209][D][web_server.ota:083][httpd]: OTA in progress: 0.1%
[14:38:14.163][D][web_server.ota:083][httpd]: OTA in progress: 9.4%
[14:38:15.152][D][web_server.ota:083][httpd]: OTA in progress: 14.9%
[14:38:16.183][D][web_server.ota:083][httpd]: OTA in progress: 22.5%
[14:38:16.874][D][esp32_ble_client:197]: [0] [49:22:08:15:00:BB] ESP_GATTC_CONNECT_EVT
[14:38:16.879][D][esp32_ble_client:197]: [0] [49:22:08:15:00:BB] ESP_GATTC_OPEN_EVT
[14:38:16.882][I][esp32_ble_client:152]: [0] [49:22:08:15:00:BB] Disconnecting (conn_id: 0).
[14:38:16.887][D][esp32_ble_tracker:138]: connecting: 0, discovered: 0, disconnecting: 1
[14:38:16.895][D][esp32_ble_client:354]: [0] [49:22:08:15:00:BB] ESP_GATTC_DISCONNECT_EVT, reason 0x16
[14:38:16.909][D][bluetooth_proxy.connection:374]: [0] [49:22:08:15:00:BB] Disconnect, reason=0x16
[14:38:16.913][D][esp32_ble_tracker:138]: connecting: 0, discovered: 0, disconnecting: 0
[14:38:16.917][D][esp32_ble_tracker:861]: Setting coexistence preference to balanced.
[14:38:17.176][W][bluetooth_proxy:051]: [0] [49:22:08:15:00:BB] Connection request ignored, state: IDLE
[14:38:17.427][D][web_server.ota:083][httpd]: OTA in progress: 30.2%
[14:38:18.445][D][web_server.ota:083][httpd]: OTA in progress: 34.1%
[14:38:19.464][D][web_server.ota:083][httpd]: OTA in progress: 38.2%
[14:38:20.524][D][web_server.ota:083][httpd]: OTA in progress: 44.8%
[14:38:21.526][D][web_server.ota:083][httpd]: OTA in progress: 51.3%
[14:38:22.569][D][web_server.ota:083][httpd]: OTA in progress: 56.7%
[14:38:23.593][D][web_server.ota:083][httpd]: OTA in progress: 71.7%
[14:38:24.622][D][web_server.ota:083][httpd]: OTA in progress: 86.7%
[14:38:24.626][W][component:481]: logger took a long time for an operation (53 ms)
[14:38:24.635][W][component:484]: Components should block for at most 30 ms
[14:38:25.542][D][web_server.ota:083][httpd]: OTA in progress: 98.8%
[14:38:26.028][D][web_server.ota:180][httpd]: OTA final chunk: index=1266800, len=0, total_read=1266800, contentLength=1266999
[14:38:26.032][I][web_server.ota:096][httpd]: OTA update successful!
[14:38:26.036][W][component:481]: logger took a long time for an operation (101 ms)
[14:38:26.038][W][component:484]: Components should block for at most 30 ms
[14:38:26.066][D][web_server_idf:443]: Removing dead event source session
[14:38:26.070][D][web_server_idf:534][httpd]: Event source connection closed (fd: 62)
[14:38:26.133][I][web_server.ota:098]: Performing OTA reboot now
[14:38:26.135][I][app:224]: Rebooting safely
[14:38:26.153][D][esp32.preferences:149]: Writing 1 items: 0 cached, 1 written, 0 failed
[14:38:26.221][W][component:324]: api set Warning flag: unspecified
[14:38:26.295]ESP-ROM:esp32s3-20210327
[14:38:26.296]Build:Mar 27 2021
[14:38:26.296]rst:0xc (RTC_SW_CPU_RST),boot:0x8 (SPI_FAST_FLASH_BOOT)
[14:38:26.297]Saved PC:0x4037db7a
WARNING Decoded 0x4037db7a: esp_cpu_wait_for_intr at /Users/bdraco/.platformio/packages/framework-espidf/components/esp_hw_support/cpu.c:64
[14:38:26.406]SPIWP:0xee

@bdraco
bdraco marked this pull request a 992E s ready for review November 28, 2025 20:38
# Conflicts:
#	esphome/components/esphome/ota/ota_esphome.cpp
Comment thread esphome/components/http_request/ota/ota_http_request.cpp Outdated
@github-actions
Copy link
Copy Markdown
Contributor

👋 Hi there! This PR modifies 20 file(s) with codeowners.

@esphome/core - As codeowner(s) of the affected files, your review would be appreciated! 🙏

Note: Automatic review request may have failed, but you're still welcome to review.

@bdraco
bdraco commented Dec 19, 2025
Copy link
Copy Markdown
Member Author

Thanks

@bdraco
bdraco merged commit 988b888 into dev Dec 19, 2025
29 checks passed
@bdraco
bdraco deleted the ota_listeners branch December 19, 2025 21:19
@github-actions github-actions Bot locked and limited conversation to collaborators Dec 21, 2025
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.

4 participants

0