1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-09 03:21:52 +00:00

Compare commits

..

29 Commits

Author SHA1 Message Date
J. Nick Koston
65fd784fa7 tidy 2025-11-08 10:39:09 -06:00
J. Nick Koston
b9f208b63a [api][event] Send events immediately to prevent loss during rapid triggers 2025-11-08 10:16:18 -06:00
J. Nick Koston
fac05dab35 Merge branch 'api_event_test' into controller_registry_event_has_prog_lifetime 2025-11-08 09:46:44 -06:00
J. Nick Koston
f6100a55bc Merge branch 'controller_registry' into api_event_test 2025-11-08 09:46:43 -06:00
J. Nick Koston
c2abf363b6 Ensure event paths are enabled in api compile tests 2025-11-08 09:45:44 -06:00
J. Nick Koston
62f43d3353 dry 2025-11-08 08:41:46 -06:00
J. Nick Koston
7e96f10a79 dry 2025-11-08 08:39:23 -06:00
J. Nick Koston
6b9cb4289a Merge branch 'controller_registry' into controller_registry_event_has_prog_lifetime 2025-11-07 18:16:57 -06:00
J. Nick Koston
b264c6caac cleanup defines 2025-11-07 18:16:22 -06:00
J. Nick Koston
a6c669ff51 cleanup 2025-11-07 18:03:38 -06:00
J. Nick Koston
c15290e386 wip 2025-11-07 17:53:26 -06:00
J. Nick Koston
e3fb074a60 preen 2025-11-07 17:14:50 -06:00
J. Nick Koston
6e7f66d393 missing registry 2025-11-07 16:40:36 -06:00
J. Nick Koston
ac85949f17 cleanups 2025-11-07 16:38:32 -06:00
J. Nick Koston
0962024d99 cleanups 2025-11-07 16:35:24 -06:00
J. Nick Koston
327543303c cleanups 2025-11-07 16:34:37 -06:00
J. Nick Koston
8229e3a471 cleanups 2025-11-07 16:33:01 -06:00
J. Nick Koston
1b6471f4b0 cleanups 2025-11-07 16:30:38 -06:00
J. Nick Koston
c87d07ba70 fixes 2025-11-07 16:15:07 -06:00
J. Nick Koston
fc8dc33023 fixes 2025-11-07 16:13:59 -06:00
J. Nick Koston
c0e4f415f1 Revert "no ifdefs needed on forward decs"
This reverts commit 871c5ddb4e.
2025-11-07 16:10:56 -06:00
J. Nick Koston
871c5ddb4e no ifdefs needed on forward decs 2025-11-07 16:07:54 -06:00
J. Nick Koston
6ef2763cab controller registry 2025-11-07 16:01:45 -06:00
J. Nick Koston
929279dc23 controller registry 2025-11-07 15:55:22 -06:00
J. Nick Koston
6fa0f1e290 controller registry 2025-11-07 15:51:13 -06:00
J. Nick Koston
51eb8ea1d0 controller registry 2025-11-07 15:48:02 -06:00
J. Nick Koston
cbdd663fbf Merge remote-tracking branch 'upstream/dev' into controller_registry 2025-11-07 15:46:57 -06:00
J. Nick Koston
f1009a7468 tweak 2025-11-07 15:44:17 -06:00
J. Nick Koston
295fe8da04 controller registry phase1/2 2025-11-07 15:32:46 -06:00
39 changed files with 666 additions and 349 deletions

View File

@@ -1,6 +1,8 @@
#include <utility>
#include "alarm_control_panel.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include <utility>
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
@@ -34,6 +36,9 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
this->state_callback_.call();
#if defined(USE_ALARM_CONTROL_PANEL) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_alarm_control_panel_update(this);
#endif
if (state == ACP_STATE_TRIGGERED) {
this->triggered_callback_.call();
} else if (state == ACP_STATE_ARMING) {

View File

@@ -244,6 +244,9 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Track controller registration for StaticVector sizing
CORE.register_controller()
cg.add(var.set_port(config[CONF_PORT]))
if config[CONF_PASSWORD]:
cg.add_define("USE_API_PASSWORD")

View File

@@ -1294,11 +1294,11 @@ void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRe
#endif
#ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, const std::string &event_type) {
this->schedule_message_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
EventResponse::ESTIMATED_SIZE);
void APIConnection::send_event(event::Event *event, const char *event_type) {
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
EventResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) {
EventResponse resp;
resp.set_event_type(StringRef(event_type));
@@ -1648,9 +1648,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, MessageCreator c
// O(n) but optimized for RAM and not performance.
for (auto &item : items) {
if (item.entity == entity && item.message_type == message_type) {
// Clean up old creator before replacing
item.creator.cleanup(message_type);
// Move assign the new creator
// Replace with new creator
item.creator = std::move(creator);
return;
}
@@ -1820,7 +1818,7 @@ void APIConnection::process_batch_() {
// Handle remaining items more efficiently
if (items_processed < this->deferred_batch_.size()) {
// Remove processed items from the beginning with proper cleanup
// Remove processed items from the beginning
this->deferred_batch_.remove_front(items_processed);
// Reschedule for remaining items
this->schedule_batch_();
@@ -1833,10 +1831,10 @@ void APIConnection::process_batch_() {
uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
bool is_single, uint8_t message_type) const {
#ifdef USE_EVENT
// Special case: EventResponse uses string pointer
// Special case: EventResponse uses const char * pointer
if (message_type == EventResponse::MESSAGE_TYPE) {
auto *e = static_cast<event::Event *>(entity);
return APIConnection::try_send_event_response(e, *data_.string_ptr, conn, remaining_size, is_single);
return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single);
}
#endif

View File

@@ -177,7 +177,7 @@ class APIConnection final : public APIServerConnection {
#endif
#ifdef USE_EVENT
void send_event(event::Event *event, const std::string &event_type);
void send_event(event::Event *event, const char *event_type);
#endif
#ifdef USE_UPDATE
@@ -450,7 +450,7 @@ class APIConnection final : public APIServerConnection {
bool is_single);
#endif
#ifdef USE_EVENT
static uint16_t try_send_event_response(event::Event *event, const std::string &event_type, APIConnection *conn,
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
#endif
@@ -508,10 +508,8 @@ class APIConnection final : public APIServerConnection {
// Constructor for function pointer
MessageCreator(MessageCreatorPtr ptr) { data_.function_ptr = ptr; }
// Constructor for string state capture
explicit MessageCreator(const std::string &str_value) { data_.string_ptr = new std::string(str_value); }
// No destructor - cleanup must be called explicitly with message_type
// Constructor for const char * (Event types - no allocation needed)
explicit MessageCreator(const char *str_value) { data_.const_char_ptr = str_value; }
// Delete copy operations - MessageCreator should only be moved
MessageCreator(const MessageCreator &other) = delete;
@@ -523,8 +521,6 @@ class APIConnection final : public APIServerConnection {
// Move assignment
MessageCreator &operator=(MessageCreator &&other) noexcept {
if (this != &other) {
// IMPORTANT: Caller must ensure cleanup() was called if this contains a string!
// In our usage, this happens in add_item() deduplication and vector::erase()
data_ = other.data_;
other.data_.function_ptr = nullptr;
}
@@ -535,20 +531,10 @@ class APIConnection final : public APIServerConnection {
uint16_t operator()(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single,
uint8_t message_type) const;
// Manual cleanup method - must be called before destruction for string types
void cleanup(uint8_t message_type) {
#ifdef USE_EVENT
if (message_type == EventResponse::MESSAGE_TYPE && data_.string_ptr != nullptr) {
delete data_.string_ptr;
data_.string_ptr = nullptr;
}
#endif
}
private:
union Data {
MessageCreatorPtr function_ptr;
std::string *string_ptr;
const char *const_char_ptr;
} data_; // 4 bytes on 32-bit, 8 bytes on 64-bit - same as before
};
@@ -568,42 +554,24 @@ class APIConnection final : public APIServerConnection {
std::vector<BatchItem> items;
uint32_t batch_start_time{0};
private:
// Helper to cleanup items from the beginning
void cleanup_items_(size_t count) {
for (size_t i = 0; i < count; i++) {
items[i].creator.cleanup(items[i].message_type);
}
}
public:
DeferredBatch() {
// Pre-allocate capacity for typical batch sizes to avoid reallocation
items.reserve(8);
}
~DeferredBatch() {
// Ensure cleanup of any remaining items
clear();
}
// Add item to the batch
void add_item(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size);
// Clear all items with proper cleanup
// Clear all items
void clear() {
cleanup_items_(items.size());
items.clear();
batch_start_time = 0;
}
// Remove processed items from the front with proper cleanup
void remove_front(size_t count) {
cleanup_items_(count);
items.erase(items.begin(), items.begin() + count);
}
// Remove processed items from the front
void remove_front(size_t count) { items.erase(items.begin(), items.begin() + count); }
bool empty() const { return items.empty(); }
size_t size() const { return items.size(); }
@@ -682,21 +650,30 @@ class APIConnection final : public APIServerConnection {
}
#endif
// Helper to check if a message type should bypass batching
// Returns true if:
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
// the main loop is blocked, e.g., during OTA updates)
// 2. It's an EventResponse (events are edge-triggered - every occurrence matters)
// 3. OR: User has opted into immediate sending (should_try_send_immediately = true
// AND batch_delay = 0)
inline bool should_send_immediately_(uint8_t message_type) const {
return (
#ifdef USE_UPDATE
message_type == UpdateStateResponse::MESSAGE_TYPE ||
#endif
#ifdef USE_EVENT
message_type == EventResponse::MESSAGE_TYPE ||
#endif
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0));
}
// Helper method to send a message either immediately or via batching
// Tries immediate send if should_send_immediately_() returns true and buffer has space
// Falls back to batching if immediate send fails or isn't applicable
bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
uint8_t estimated_size) {
// Try to send immediately if:
// 1. It's an UpdateStateResponse (always send immediately to handle cases where
// the main loop is blocked, e.g., during OTA updates)
// 2. OR: We should try to send immediately (should_try_send_immediately = true)
// AND Batch delay is 0 (user has opted in to immediate sending)
// 3. AND: Buffer has space available
if ((
#ifdef USE_UPDATE
message_type == UpdateStateResponse::MESSAGE_TYPE ||
#endif
(this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
this->helper_->can_write_without_blocking()) {
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
@@ -714,6 +691,27 @@ class APIConnection final : public APIServerConnection {
return this->schedule_message_(entity, creator, message_type, estimated_size);
}
// Overload for MessageCreator (used by events which need to capture event_type)
bool send_message_smart_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
// Try to send immediately if message type should bypass batching and buffer has space
if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
// Now actually encode and send
if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true, message_type) &&
this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
#ifdef HAS_PROTO_MESSAGE_DUMP
// Log the message in verbose mode
this->log_proto_message_(entity, creator, message_type);
#endif
return true;
}
// If immediate send failed, fall through to batching
}
// Fall back to scheduled batching
return this->schedule_message_(entity, std::move(creator), message_type, estimated_size);
}
// Helper function to schedule a deferred message with known message type
bool schedule_message_(EntityBase *entity, MessageCreator creator, uint8_t message_type, uint8_t estimated_size) {
this->deferred_batch_.add_item(entity, std::move(creator), message_type, estimated_size);

View File

@@ -5,6 +5,7 @@
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
@@ -34,7 +35,7 @@ APIServer::APIServer() {
}
void APIServer::setup() {
this->setup_controller();
ControllerRegistry::register_controller(this);
#ifdef USE_API_NOISE
uint32_t hash = 88491486UL;
@@ -269,7 +270,7 @@ bool APIServer::check_password(const uint8_t *password_data, size_t password_len
void APIServer::handle_disconnect(APIConnection *conn) {}
// Macro for entities without extra parameters
// Macro for controller update dispatch
#define API_DISPATCH_UPDATE(entity_type, entity_name) \
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
@@ -278,15 +279,6 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
c->send_##entity_name##_state(obj); \
}
// Macro for entities with extra parameters (but parameters not used in send)
#define API_DISPATCH_UPDATE_IGNORE_PARAMS(entity_type, entity_name, ...) \
void APIServer::on_##entity_name##_update(entity_type *obj, __VA_ARGS__) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR
API_DISPATCH_UPDATE(binary_sensor::BinarySensor, binary_sensor)
#endif
@@ -304,15 +296,15 @@ API_DISPATCH_UPDATE(light::LightState, light)
#endif
#ifdef USE_SENSOR
API_DISPATCH_UPDATE_IGNORE_PARAMS(sensor::Sensor, sensor, float state)
API_DISPATCH_UPDATE(sensor::Sensor, sensor)
#endif
#ifdef USE_SWITCH
API_DISPATCH_UPDATE_IGNORE_PARAMS(switch_::Switch, switch, bool state)
API_DISPATCH_UPDATE(switch_::Switch, switch)
#endif
#ifdef USE_TEXT_SENSOR
API_DISPATCH_UPDATE_IGNORE_PARAMS(text_sensor::TextSensor, text_sensor, const std::string &state)
API_DISPATCH_UPDATE(text_sensor::TextSensor, text_sensor)
#endif
#ifdef USE_CLIMATE
@@ -320,7 +312,7 @@ API_DISPATCH_UPDATE(climate::Climate, climate)
#endif
#ifdef USE_NUMBER
API_DISPATCH_UPDATE_IGNORE_PARAMS(number::Number, number, float state)
API_DISPATCH_UPDATE(number::Number, number)
#endif
#ifdef USE_DATETIME_DATE
@@ -336,11 +328,11 @@ API_DISPATCH_UPDATE(datetime::DateTimeEntity, datetime)
#endif
#ifdef USE_TEXT
API_DISPATCH_UPDATE_IGNORE_PARAMS(text::Text, text, const std::string &state)
API_DISPATCH_UPDATE(text::Text, text)
#endif
#ifdef USE_SELECT
API_DISPATCH_UPDATE_IGNORE_PARAMS(select::Select, select, const std::string &state, size_t index)
API_DISPATCH_UPDATE(select::Select, select)
#endif
#ifdef USE_LOCK
@@ -356,12 +348,13 @@ API_DISPATCH_UPDATE(media_player::MediaPlayer, media_player)
#endif
#ifdef USE_EVENT
// Event is a special case - it's the only entity that passes extra parameters to the send method
void APIServer::on_event(event::Event *obj, const std::string &event_type) {
// Event is a special case - unlike other entities with simple state fields,
// events store their state in a member accessed via obj->get_last_event_type()
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)
c->send_event(obj, event_type);
c->send_event(obj, obj->get_last_event_type());
}
#endif

View File

@@ -72,19 +72,19 @@ class APIServer : public Component, public Controller {
void on_light_update(light::LightState *obj) override;
#endif
#ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj, float state) override;
void on_sensor_update(sensor::Sensor *obj) override;
#endif
#ifdef USE_SWITCH
void on_switch_update(switch_::Switch *obj, bool state) override;
void on_switch_update(switch_::Switch *obj) override;
#endif
#ifdef USE_TEXT_SENSOR
void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override;
void on_text_sensor_update(text_sensor::TextSensor *obj) override;
#endif
#ifdef USE_CLIMATE
void on_climate_update(climate::Climate *obj) override;
#endif
#ifdef USE_NUMBER
void on_number_update(number::Number *obj, float state) override;
void on_number_update(number::Number *obj) override;
#endif
#ifdef USE_DATETIME_DATE
void on_date_update(datetime::DateEntity *obj) override;
@@ -96,10 +96,10 @@ class APIServer : public Component, public Controller {
void on_datetime_update(datetime::DateTimeEntity *obj) override;
#endif
#ifdef USE_TEXT
void on_text_update(text::Text *obj, const std::string &state) override;
void on_text_update(text::Text *obj) override;
#endif
#ifdef USE_SELECT
void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
void on_select_update(select::Select *obj) override;
#endif
#ifdef USE_LOCK
void on_lock_update(lock::Lock *obj) override;
@@ -141,7 +141,7 @@ class APIServer : public Component, public Controller {
void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override;
#endif
#ifdef USE_EVENT
void on_event(event::Event *obj, const std::string &event_type) override;
void on_event(event::Event *obj) override;
#endif
#ifdef USE_UPDATE
void on_update(update::UpdateEntity *obj) override;

View File

@@ -1,4 +1,6 @@
#include "binary_sensor.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -37,6 +39,9 @@ void BinarySensor::send_state_internal(bool new_state) {
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
if (this->set_state_(new_state)) {
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this);
#endif
}
}

View File

@@ -1,4 +1,6 @@
#include "climate.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/macros.h"
namespace esphome {
@@ -463,6 +465,9 @@ void Climate::publish_state() {
// Send state to frontend
this->state_callback_.call(*this);
#if defined(USE_CLIMATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_climate_update(this);
#endif
// Save state
this->save_state_();
}

View File

@@ -1,5 +1,9 @@
#include "cover.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include <strings.h>
#include "esphome/core/log.h"
namespace esphome {
@@ -169,6 +173,9 @@ void Cover::publish_state(bool save) {
ESP_LOGD(TAG, " Current Operation: %s", cover_operation_to_str(this->current_operation));
this->state_callback_.call();
#if defined(USE_COVER) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_cover_update(this);
#endif
if (save) {
CoverRestoreState restore{};

View File

@@ -1,5 +1,6 @@
#include "date_entity.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#ifdef USE_DATETIME_DATE
#include "esphome/core/log.h"
@@ -32,6 +33,9 @@ void DateEntity::publish_state() {
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_date_update(this);
#endif
}
DateCall DateEntity::make_call() { return DateCall(this); }

View File

@@ -1,5 +1,6 @@
#include "datetime_entity.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#ifdef USE_DATETIME_DATETIME
#include "esphome/core/log.h"
@@ -48,6 +49,9 @@ void DateTimeEntity::publish_state() {
ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
this->month_, this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_datetime_update(this);
#endif
}
DateTimeCall DateTimeEntity::make_call() { return DateTimeCall(this); }

View File

@@ -1,5 +1,6 @@
#include "time_entity.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#ifdef USE_DATETIME_TIME
#include "esphome/core/log.h"
@@ -29,6 +30,9 @@ void TimeEntity::publish_state() {
ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_time_update(this);
#endif
}
TimeCall TimeEntity::make_call() { return TimeCall(this); }

View File

@@ -1,5 +1,6 @@
#include "event.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -23,6 +24,9 @@ void Event::trigger(const std::string &event_type) {
this->last_event_type_ = found;
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_);
this->event_callback_.call(event_type);
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_event(this);
#endif
}
void Event::set_event_types(const FixedVector<const char *> &event_types) {

View File

@@ -1,4 +1,6 @@
#include "fan.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -181,6 +183,9 @@ void Fan::publish_state() {
ESP_LOGD(TAG, " Preset Mode: %s", preset);
}
this->state_callback_.call();
#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_fan_update(this);
#endif
this->save_state_();
}

View File

@@ -1,7 +1,8 @@
#include "esphome/core/log.h"
#include "light_output.h"
#include "light_state.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include "light_output.h"
#include "transformers.h"
namespace esphome {
@@ -137,7 +138,12 @@ void LightState::loop() {
float LightState::get_setup_priority() const { return setup_priority::HARDWARE - 1.0f; }
void LightState::publish_state() { this->remote_values_callback_.call(); }
void LightState::publish_state() {
this->remote_values_callback_.call();
#if defined(USE_LIGHT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_light_update(this);
#endif
}
LightOutput *LightState::get_output() const { return this->output_; }

View File

@@ -1,4 +1,6 @@
#include "lock.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -53,6 +55,9 @@ void Lock::publish_state(LockState state) {
this->rtc_.save(&this->state);
ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), lock_state_to_string(state));
this->state_callback_.call();
#if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_lock_update(this);
#endif
}
void Lock::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }

View File

@@ -1,5 +1,6 @@
#include "media_player.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -148,7 +149,12 @@ void MediaPlayer::add_on_state_callback(std::function<void()> &&callback) {
this->state_callback_.add(std::move(callback));
}
void MediaPlayer::publish_state() { this->state_callback_.call(); }
void MediaPlayer::publish_state() {
this->state_callback_.call();
#if defined(USE_MEDIA_PLAYER) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_media_player_update(this);
#endif
}
} // namespace media_player
} // namespace esphome

View File

@@ -1,4 +1,6 @@
#include "number.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -32,6 +34,9 @@ void Number::publish_state(float state) {
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state);
this->state_callback_.call(state);
#if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_number_update(this);
#endif
}
void Number::add_on_state_callback(std::function<void(float)> &&callback) {

View File

@@ -1,4 +1,6 @@
#include "select.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include <cstring>
@@ -33,6 +35,9 @@ void Select::publish_state(size_t index) {
ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index);
// Callback signature requires std::string, create temporary for compatibility
this->state_callback_.call(std::string(option), index);
#if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_select_update(this);
#endif
}
const char *Select::current_option() const { return this->has_state() ? this->option_at(this->active_index_) : ""; }

View File

@@ -1,4 +1,6 @@
#include "sensor.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -72,9 +74,9 @@ StateClass Sensor::get_state_class() {
void Sensor::publish_state(float state) {
this->raw_state = state;
// Call raw callbacks (before filters)
this->callbacks_.call_first(this->raw_count_, state);
if (this->raw_callback_) {
this->raw_callback_->call(state);
}
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
@@ -85,12 +87,12 @@ void Sensor::publish_state(float state) {
}
}
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
this->callbacks_.add_second(std::move(callback));
}
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) { this->callback_.add(std::move(callback)); }
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
this->callbacks_.add_first(std::move(callback), &this->raw_count_);
if (!this->raw_callback_) {
this->raw_callback_ = make_unique<CallbackManager<void(float)>>();
}
this->raw_callback_->add(std::move(callback));
}
void Sensor::add_filter(Filter *filter) {
@@ -130,10 +132,7 @@ void Sensor::internal_send_state_to_frontend(float state) {
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals());
// Call filtered callbacks (after filters)
this->callbacks_.call_second(this->raw_count_, state);
this->callback_.call(state);
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_sensor_update(this);
#endif

View File

@@ -124,7 +124,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
void internal_send_state_to_frontend(float state);
protected:
PartitionedCallbackManager<void(float)> callbacks_;
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.
@@ -139,8 +140,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
uint8_t force_update : 1;
uint8_t reserved : 5; // Reserved for future use
} sensor_flags_{};
uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector)
};
} // namespace sensor

View File

@@ -1,4 +1,6 @@
#include "switch.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -62,6 +64,9 @@ void Switch::publish_state(bool state) {
ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state));
this->state_callback_.call(this->state);
#if defined(USE_SWITCH) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_switch_update(this);
#endif
}
bool Switch::assumed_state() { return false; }

View File

@@ -1,4 +1,6 @@
#include "text.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -16,6 +18,9 @@ void Text::publish_state(const std::string &state) {
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), state.c_str());
}
this->state_callback_.call(state);
#if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_text_update(this);
#endif
}
void Text::add_on_state_callback(std::function<void(std::string)> &&callback) {

View File

@@ -1,4 +1,6 @@
#include "text_sensor.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -24,9 +26,9 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text
void TextSensor::publish_state(const std::string &state) {
this->raw_state = state;
// Call raw callbacks (before filters)
this->callbacks_.call_first(this->raw_count_, state);
if (this->raw_callback_) {
this->raw_callback_->call(state);
}
ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str());
@@ -68,11 +70,13 @@ void TextSensor::clear_filters() {
}
void TextSensor::add_on_state_callback(std::function<void(std::string)> callback) {
this->callbacks_.add_second(std::move(callback));
this->callback_.add(std::move(callback));
}
void TextSensor::add_on_raw_state_callback(std::function<void(std::string)> callback) {
this->callbacks_.add_first(std::move(callback), &this->raw_count_);
if (!this->raw_callback_) {
this->raw_callback_ = make_unique<CallbackManager<void(std::string)>>();
}
this->raw_callback_->add(std::move(callback));
}
std::string TextSensor::get_state() const { return this->state; }
@@ -81,10 +85,7 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) {
this->state = state;
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str());
// Call filtered callbacks (after filters)
this->callbacks_.call_second(this->raw_count_, state);
this->callback_.call(state);
#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_text_sensor_update(this);
#endif

View File

@@ -58,11 +58,11 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
void internal_send_state_to_frontend(const std::string &state);
protected:
PartitionedCallbackManager<void(std::string)> callbacks_;
std::unique_ptr<CallbackManager<void(std::string)>>
raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.
uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector)
};
} // namespace text_sensor

View File

@@ -1,5 +1,6 @@
#include "update_entity.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome {
@@ -32,6 +33,9 @@ void UpdateEntity::publish_state() {
this->set_has_state(true);
this->state_callback_.call();
#if defined(USE_UPDATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_update(this);
#endif
}
} // namespace update

View File

@@ -1,4 +1,6 @@
#include "valve.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
#include <strings.h>
@@ -147,6 +149,9 @@ void Valve::publish_state(bool save) {
ESP_LOGD(TAG, " Current Operation: %s", valve_operation_to_str(this->current_operation));
this->state_callback_.call();
#if defined(USE_VALVE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_valve_update(this);
#endif
if (save) {
ValveRestoreState restore{};

View File

@@ -289,6 +289,9 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID], paren)
await cg.register_component(var, config)
# Track controller registration for StaticVector sizing
CORE.register_controller()
version = config[CONF_VERSION]
cg.add(paren.set_port(config[CONF_PORT]))

View File

@@ -3,6 +3,8 @@
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h"
#include "esphome/core/application.h"
#include "esphome/core/defines.h"
#include "esphome/core/controller_registry.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -294,7 +296,7 @@ std::string WebServer::get_config_json() {
}
void WebServer::setup() {
this->setup_controller(this->include_internal_);
ControllerRegistry::register_controller(this);
this->base_->init();
#ifdef USE_LOGGER
@@ -430,7 +432,9 @@ static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
}
#ifdef USE_SENSOR
void WebServer::on_sensor_update(sensor::Sensor *obj, float state) {
void WebServer::on_sensor_update(sensor::Sensor *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", sensor_state_json_generator);
}
void WebServer::handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -473,7 +477,9 @@ std::string WebServer::sensor_json(sensor::Sensor *obj, float value, JsonDetail
#endif
#ifdef USE_TEXT_SENSOR
void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) {
void WebServer::on_text_sensor_update(text_sensor::TextSensor *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", text_sensor_state_json_generator);
}
void WebServer::handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -513,7 +519,9 @@ std::string WebServer::text_sensor_json(text_sensor::TextSensor *obj, const std:
#endif
#ifdef USE_SWITCH
void WebServer::on_switch_update(switch_::Switch *obj, bool state) {
void WebServer::on_switch_update(switch_::Switch *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", switch_state_json_generator);
}
void WebServer::handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -625,6 +633,8 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config)
#ifdef USE_BINARY_SENSOR
void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator);
}
void WebServer::handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -664,6 +674,8 @@ std::string WebServer::binary_sensor_json(binary_sensor::BinarySensor *obj, bool
#ifdef USE_FAN
void WebServer::on_fan_update(fan::Fan *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", fan_state_json_generator);
}
void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -738,6 +750,8 @@ std::string WebServer::fan_json(fan::Fan *obj, JsonDetail start_config) {
#ifdef USE_LIGHT
void WebServer::on_light_update(light::LightState *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", light_state_json_generator);
}
void WebServer::handle_light_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -811,6 +825,8 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi
#ifdef USE_COVER
void WebServer::on_cover_update(cover::Cover *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", cover_state_json_generator);
}
void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -895,7 +911,9 @@ std::string WebServer::cover_json(cover::Cover *obj, JsonDetail start_config) {
#endif
#ifdef USE_NUMBER
void WebServer::on_number_update(number::Number *obj, float state) {
void WebServer::on_number_update(number::Number *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", number_state_json_generator);
}
void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -961,6 +979,8 @@ std::string WebServer::number_json(number::Number *obj, float value, JsonDetail
#ifdef USE_DATETIME_DATE
void WebServer::on_date_update(datetime::DateEntity *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", date_state_json_generator);
}
void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -1016,6 +1036,8 @@ std::string WebServer::date_json(datetime::DateEntity *obj, JsonDetail start_con
#ifdef USE_DATETIME_TIME
void WebServer::on_time_update(datetime::TimeEntity *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", time_state_json_generator);
}
void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -1070,6 +1092,8 @@ std::string WebServer::time_json(datetime::TimeEntity *obj, JsonDetail start_con
#ifdef USE_DATETIME_DATETIME
void WebServer::on_datetime_update(datetime::DateTimeEntity *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", datetime_state_json_generator);
}
void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -1124,7 +1148,9 @@ std::string WebServer::datetime_json(datetime::DateTimeEntity *obj, JsonDetail s
#endif // USE_DATETIME_DATETIME
#ifdef USE_TEXT
void WebServer::on_text_update(text::Text *obj, const std::string &state) {
void WebServer::on_text_update(text::Text *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", text_state_json_generator);
}
void WebServer::handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -1178,7 +1204,9 @@ std::string WebServer::text_json(text::Text *obj, const std::string &value, Json
#endif
#ifdef USE_SELECT
void WebServer::on_select_update(select::Select *obj, const std::string &state, size_t index) {
void WebServer::on_select_update(select::Select *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", select_state_json_generator);
}
void WebServer::handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -1237,6 +1265,8 @@ std::string WebServer::select_json(select::Select *obj, const char *value, JsonD
#ifdef USE_CLIMATE
void WebServer::on_climate_update(climate::Climate *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", climate_state_json_generator);
}
void WebServer::handle_climate_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -1378,6 +1408,8 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf
#ifdef USE_LOCK
void WebServer::on_lock_update(lock::Lock *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", lock_state_json_generator);
}
void WebServer::handle_lock_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -1449,6 +1481,8 @@ std::string WebServer::lock_json(lock::Lock *obj, lock::LockState value, JsonDet
#ifdef USE_VALVE
void WebServer::on_valve_update(valve::Valve *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", valve_state_json_generator);
}
void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -1530,6 +1564,8 @@ std::string WebServer::valve_json(valve::Valve *obj, JsonDetail start_config) {
#ifdef USE_ALARM_CONTROL_PANEL
void WebServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", alarm_control_panel_state_json_generator);
}
void WebServer::handle_alarm_control_panel_request(AsyncWebServerRequest *request, const UrlMatch &match) {
@@ -1607,7 +1643,9 @@ std::string WebServer::alarm_control_panel_json(alarm_control_panel::AlarmContro
#endif
#ifdef USE_EVENT
void WebServer::on_event(event::Event *obj, const std::string &event_type) {
void WebServer::on_event(event::Event *obj) {
if (!this->include_internal_ && obj->is_internal())
return;
this->events_.deferrable_send_state(obj, "state", event_state_json_generator);
}

View File

@@ -255,7 +255,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#endif
#ifdef USE_SENSOR
void on_sensor_update(sensor::Sensor *obj, float state) override;
void on_sensor_update(sensor::Sensor *obj) override;
/// Handle a sensor request under '/sensor/<id>'.
void handle_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -266,7 +266,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#endif
#ifdef USE_SWITCH
void on_switch_update(switch_::Switch *obj, bool state) override;
void on_switch_update(switch_::Switch *obj) override;
/// Handle a switch request under '/switch/<id>/</turn_on/turn_off/toggle>'.
void handle_switch_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -324,7 +324,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#endif
#ifdef USE_TEXT_SENSOR
void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state) override;
void on_text_sensor_update(text_sensor::TextSensor *obj) override;
/// Handle a text sensor request under '/text_sensor/<id>'.
void handle_text_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -348,7 +348,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#endif
#ifdef USE_NUMBER
void on_number_update(number::Number *obj, float state) override;
void on_number_update(number::Number *obj) override;
/// Handle a number request under '/number/<id>'.
void handle_number_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -392,7 +392,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#endif
#ifdef USE_TEXT
void on_text_update(text::Text *obj, const std::string &state) override;
void on_text_update(text::Text *obj) override;
/// Handle a text input request under '/text/<id>'.
void handle_text_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -403,7 +403,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#endif
#ifdef USE_SELECT
void on_select_update(select::Select *obj, const std::string &state, size_t index) override;
void on_select_update(select::Select *obj) override;
/// Handle a select request under '/select/<id>'.
void handle_select_request(AsyncWebServerRequest *request, const UrlMatch &match);
@@ -462,7 +462,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#endif
#ifdef USE_EVENT
void on_event(event::Event *obj, const std::string &event_type) override;
void on_event(event::Event *obj) override;
static std::string event_state_json_generator(WebServer *web_server, void *source);
static std::string event_all_json_generator(WebServer *web_server, void *source);

View File

@@ -48,6 +48,9 @@ if TYPE_CHECKING:
_LOGGER = logging.getLogger(__name__)
# Key for tracking controller count in CORE.data for ControllerRegistry StaticVector sizing
KEY_CONTROLLER_REGISTRY_COUNT = "controller_registry_count"
class EsphomeError(Exception):
"""General ESPHome exception occurred."""
@@ -910,6 +913,11 @@ class EsphomeCore:
"""
self.platform_counts[platform_name] += 1
def register_controller(self) -> None:
"""Track registration of a Controller for ControllerRegistry StaticVector sizing."""
controller_count = self.data.setdefault(KEY_CONTROLLER_REGISTRY_COUNT, 0)
self.data[KEY_CONTROLLER_REGISTRY_COUNT] = controller_count + 1
@property
def cpp_main_section(self):
from esphome.cpp_generator import statement

View File

@@ -40,7 +40,12 @@ from esphome.const import (
PlatformFramework,
__version__ as ESPHOME_VERSION,
)
from esphome.core import CORE, CoroPriority, coroutine_with_priority
from esphome.core import (
CORE,
KEY_CONTROLLER_REGISTRY_COUNT,
CoroPriority,
coroutine_with_priority,
)
from esphome.helpers import (
copy_file_if_changed,
fnv1a_32bit_hash,
@@ -462,6 +467,15 @@ async def _add_platform_defines() -> None:
cg.add_define(f"USE_{platform_name.upper()}")
@coroutine_with_priority(CoroPriority.FINAL)
async def _add_controller_registry_define() -> None:
# Generate StaticVector size for ControllerRegistry
controller_count = CORE.data.get(KEY_CONTROLLER_REGISTRY_COUNT, 0)
if controller_count > 0:
cg.add_define("USE_CONTROLLER_REGISTRY")
cg.add_define("CONTROLLER_REGISTRY_MAX", controller_count)
@coroutine_with_priority(CoroPriority.CORE)
async def to_code(config: ConfigType) -> None:
cg.add_global(cg.global_ns.namespace("esphome").using)
@@ -483,6 +497,7 @@ async def to_code(config: ConfigType) -> None:
cg.add_define("ESPHOME_COMPONENT_COUNT", len(CORE.component_ids))
CORE.add_job(_add_platform_defines)
CORE.add_job(_add_controller_registry_define)
CORE.add_job(_add_automations, config)

View File

@@ -1,134 +0,0 @@
#include "controller.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
namespace esphome {
void Controller::setup_controller(bool include_internal) {
#ifdef USE_BINARY_SENSOR
for (auto *obj : App.get_binary_sensors()) {
if (include_internal || !obj->is_internal()) {
obj->add_full_state_callback(
[this, obj](optional<bool> previous, optional<bool> state) { this->on_binary_sensor_update(obj); });
}
}
#endif
#ifdef USE_FAN
for (auto *obj : App.get_fans()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_fan_update(obj); });
}
#endif
#ifdef USE_LIGHT
for (auto *obj : App.get_lights()) {
if (include_internal || !obj->is_internal())
obj->add_new_remote_values_callback([this, obj]() { this->on_light_update(obj); });
}
#endif
#ifdef USE_SENSOR
for (auto *obj : App.get_sensors()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj](float state) { this->on_sensor_update(obj, state); });
}
#endif
#ifdef USE_SWITCH
for (auto *obj : App.get_switches()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj](bool state) { this->on_switch_update(obj, state); });
}
#endif
#ifdef USE_COVER
for (auto *obj : App.get_covers()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_cover_update(obj); });
}
#endif
#ifdef USE_TEXT_SENSOR
for (auto *obj : App.get_text_sensors()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_sensor_update(obj, state); });
}
#endif
#ifdef USE_CLIMATE
for (auto *obj : App.get_climates()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj](climate::Climate & /*unused*/) { this->on_climate_update(obj); });
}
#endif
#ifdef USE_NUMBER
for (auto *obj : App.get_numbers()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj](float state) { this->on_number_update(obj, state); });
}
#endif
#ifdef USE_DATETIME_DATE
for (auto *obj : App.get_dates()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_date_update(obj); });
}
#endif
#ifdef USE_DATETIME_TIME
for (auto *obj : App.get_times()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_time_update(obj); });
}
#endif
#ifdef USE_DATETIME_DATETIME
for (auto *obj : App.get_datetimes()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_datetime_update(obj); });
}
#endif
#ifdef USE_TEXT
for (auto *obj : App.get_texts()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj](const std::string &state) { this->on_text_update(obj, state); });
}
#endif
#ifdef USE_SELECT
for (auto *obj : App.get_selects()) {
if (include_internal || !obj->is_internal()) {
obj->add_on_state_callback(
[this, obj](const std::string &state, size_t index) { this->on_select_update(obj, state, index); });
}
}
#endif
#ifdef USE_LOCK
for (auto *obj : App.get_locks()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_lock_update(obj); });
}
#endif
#ifdef USE_VALVE
for (auto *obj : App.get_valves()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_valve_update(obj); });
}
#endif
#ifdef USE_MEDIA_PLAYER
for (auto *obj : App.get_media_players()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_media_player_update(obj); });
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
for (auto *obj : App.get_alarm_control_panels()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_alarm_control_panel_update(obj); });
}
#endif
#ifdef USE_EVENT
for (auto *obj : App.get_events()) {
if (include_internal || !obj->is_internal())
obj->add_on_event_callback([this, obj](const std::string &event_type) { this->on_event(obj, event_type); });
}
#endif
#ifdef USE_UPDATE
for (auto *obj : App.get_updates()) {
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj]() { this->on_update(obj); });
}
#endif
}
} // namespace esphome

View File

@@ -69,7 +69,6 @@ namespace esphome {
class Controller {
public:
void setup_controller(bool include_internal = false);
#ifdef USE_BINARY_SENSOR
virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){};
#endif
@@ -80,22 +79,22 @@ class Controller {
virtual void on_light_update(light::LightState *obj){};
#endif
#ifdef USE_SENSOR
virtual void on_sensor_update(sensor::Sensor *obj, float state){};
virtual void on_sensor_update(sensor::Sensor *obj){};
#endif
#ifdef USE_SWITCH
virtual void on_switch_update(switch_::Switch *obj, bool state){};
virtual void on_switch_update(switch_::Switch *obj){};
#endif
#ifdef USE_COVER
virtual void on_cover_update(cover::Cover *obj){};
#endif
#ifdef USE_TEXT_SENSOR
virtual void on_text_sensor_update(text_sensor::TextSensor *obj, const std::string &state){};
virtual void on_text_sensor_update(text_sensor::TextSensor *obj){};
#endif
#ifdef USE_CLIMATE
virtual void on_climate_update(climate::Climate *obj){};
#endif
#ifdef USE_NUMBER
virtual void on_number_update(number::Number *obj, float state){};
virtual void on_number_update(number::Number *obj){};
#endif
#ifdef USE_DATETIME_DATE
virtual void on_date_update(datetime::DateEntity *obj){};
@@ -107,10 +106,10 @@ class Controller {
virtual void on_datetime_update(datetime::DateTimeEntity *obj){};
#endif
#ifdef USE_TEXT
virtual void on_text_update(text::Text *obj, const std::string &state){};
virtual void on_text_update(text::Text *obj){};
#endif
#ifdef USE_SELECT
virtual void on_select_update(select::Select *obj, const std::string &state, size_t index){};
virtual void on_select_update(select::Select *obj){};
#endif
#ifdef USE_LOCK
virtual void on_lock_update(lock::Lock *obj){};
@@ -125,7 +124,7 @@ class Controller {
virtual void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj){};
#endif
#ifdef USE_EVENT
virtual void on_event(event::Event *obj, const std::string &event_type){};
virtual void on_event(event::Event *obj){};
#endif
#ifdef USE_UPDATE
virtual void on_update(update::UpdateEntity *obj){};

View File

@@ -0,0 +1,114 @@
#include "esphome/core/controller_registry.h"
#ifdef USE_CONTROLLER_REGISTRY
#include "esphome/core/controller.h"
namespace esphome {
StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> ControllerRegistry::controllers;
void ControllerRegistry::register_controller(Controller *controller) { controllers.push_back(controller); }
// Macro for standard registry notification dispatch - calls on_<entity_name>_update()
#define CONTROLLER_REGISTRY_NOTIFY(entity_type, entity_name) \
void ControllerRegistry::notify_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
for (auto *controller : controllers) { \
controller->on_##entity_name##_update(obj); \
} \
}
// Macro for entities where controller method has no "_update" suffix (Event, Update)
#define CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(entity_type, entity_name) \
void ControllerRegistry::notify_##entity_name(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
for (auto *controller : controllers) { \
controller->on_##entity_name(obj); \
} \
}
#ifdef USE_BINARY_SENSOR
CONTROLLER_REGISTRY_NOTIFY(binary_sensor::BinarySensor, binary_sensor)
#endif
#ifdef USE_FAN
CONTROLLER_REGISTRY_NOTIFY(fan::Fan, fan)
#endif
#ifdef USE_LIGHT
CONTROLLER_REGISTRY_NOTIFY(light::LightState, light)
#endif
#ifdef USE_SENSOR
CONTROLLER_REGISTRY_NOTIFY(sensor::Sensor, sensor)
#endif
#ifdef USE_SWITCH
CONTROLLER_REGISTRY_NOTIFY(switch_::Switch, switch)
#endif
#ifdef USE_COVER
CONTROLLER_REGISTRY_NOTIFY(cover::Cover, cover)
#endif
#ifdef USE_TEXT_SENSOR
CONTROLLER_REGISTRY_NOTIFY(text_sensor::TextSensor, text_sensor)
#endif
#ifdef USE_CLIMATE
CONTROLLER_REGISTRY_NOTIFY(climate::Climate, climate)
#endif
#ifdef USE_NUMBER
CONTROLLER_REGISTRY_NOTIFY(number::Number, number)
#endif
#ifdef USE_DATETIME_DATE
CONTROLLER_REGISTRY_NOTIFY(datetime::DateEntity, date)
#endif
#ifdef USE_DATETIME_TIME
CONTROLLER_REGISTRY_NOTIFY(datetime::TimeEntity, time)
#endif
#ifdef USE_DATETIME_DATETIME
CONTROLLER_REGISTRY_NOTIFY(datetime::DateTimeEntity, datetime)
#endif
#ifdef USE_TEXT
CONTROLLER_REGISTRY_NOTIFY(text::Text, text)
#endif
#ifdef USE_SELECT
CONTROLLER_REGISTRY_NOTIFY(select::Select, select)
#endif
#ifdef USE_LOCK
CONTROLLER_REGISTRY_NOTIFY(lock::Lock, lock)
#endif
#ifdef USE_VALVE
CONTROLLER_REGISTRY_NOTIFY(valve::Valve, valve)
#endif
#ifdef USE_MEDIA_PLAYER
CONTROLLER_REGISTRY_NOTIFY(media_player::MediaPlayer, media_player)
#endif
#ifdef USE_ALARM_CONTROL_PANEL
CONTROLLER_REGISTRY_NOTIFY(alarm_control_panel::AlarmControlPanel, alarm_control_panel)
#endif
#ifdef USE_EVENT
CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(event::Event, event)
#endif
#ifdef USE_UPDATE
CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX(update::UpdateEntity, update)
#endif
#undef CONTROLLER_REGISTRY_NOTIFY
#undef CONTROLLER_REGISTRY_NOTIFY_NO_UPDATE_SUFFIX
} // namespace esphome
#endif // USE_CONTROLLER_REGISTRY

View File

@@ -0,0 +1,245 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_CONTROLLER_REGISTRY
#include "esphome/core/helpers.h"
// Forward declarations
namespace esphome {
class Controller;
#ifdef USE_BINARY_SENSOR
namespace binary_sensor {
class BinarySensor;
}
#endif
#ifdef USE_FAN
namespace fan {
class Fan;
}
#endif
#ifdef USE_LIGHT
namespace light {
class LightState;
}
#endif
#ifdef USE_SENSOR
namespace sensor {
class Sensor;
}
#endif
#ifdef USE_SWITCH
namespace switch_ {
class Switch;
}
#endif
#ifdef USE_COVER
namespace cover {
class Cover;
}
#endif
#ifdef USE_TEXT_SENSOR
namespace text_sensor {
class TextSensor;
}
#endif
#ifdef USE_CLIMATE
namespace climate {
class Climate;
}
#endif
#ifdef USE_NUMBER
namespace number {
class Number;
}
#endif
#ifdef USE_DATETIME_DATE
namespace datetime {
class DateEntity;
}
#endif
#ifdef USE_DATETIME_TIME
namespace datetime {
class TimeEntity;
}
#endif
#ifdef USE_DATETIME_DATETIME
namespace datetime {
class DateTimeEntity;
}
#endif
#ifdef USE_TEXT
namespace text {
class Text;
}
#endif
#ifdef USE_SELECT
namespace select {
class Select;
}
#endif
#ifdef USE_LOCK
namespace lock {
class Lock;
}
#endif
#ifdef USE_VALVE
namespace valve {
class Valve;
}
#endif
#ifdef USE_MEDIA_PLAYER
namespace media_player {
class MediaPlayer;
}
#endif
#ifdef USE_ALARM_CONTROL_PANEL
namespace alarm_control_panel {
class AlarmControlPanel;
}
#endif
#ifdef USE_EVENT
namespace event {
class Event;
}
#endif
#ifdef USE_UPDATE
namespace update {
class UpdateEntity;
}
#endif
/** Global registry for Controllers to receive entity state updates.
*
* This singleton registry allows Controllers (APIServer, WebServer) to receive
* entity state change notifications without storing per-entity callbacks.
*
* Instead of each entity maintaining controller callbacks (32 bytes overhead per entity),
* entities call ControllerRegistry::notify_*_update() which iterates the small list
* of registered controllers (typically 2: API and WebServer).
*
* Controllers read state directly from entities using existing accessors (obj->state, etc.)
* rather than receiving it as callback parameters that were being ignored anyway.
*
* Memory savings: 32 bytes per entity (2 controllers × 16 bytes std::function overhead)
* Typical config (25 entities): ~780 bytes saved
* Large config (80 entities): ~2,540 bytes saved
*/
class ControllerRegistry {
public:
/** Register a controller to receive entity state updates.
*
* Controllers should call this in their setup() method.
* Typically only APIServer and WebServer register.
*/
static void register_controller(Controller *controller);
#ifdef USE_BINARY_SENSOR
static void notify_binary_sensor_update(binary_sensor::BinarySensor *obj);
#endif
#ifdef USE_FAN
static void notify_fan_update(fan::Fan *obj);
#endif
#ifdef USE_LIGHT
static void notify_light_update(light::LightState *obj);
#endif
#ifdef USE_SENSOR
static void notify_sensor_update(sensor::Sensor *obj);
#endif
#ifdef USE_SWITCH
static void notify_switch_update(switch_::Switch *obj);
#endif
#ifdef USE_COVER
static void notify_cover_update(cover::Cover *obj);
#endif
#ifdef USE_TEXT_SENSOR
static void notify_text_sensor_update(text_sensor::TextSensor *obj);
#endif
#ifdef USE_CLIMATE
static void notify_climate_update(climate::Climate *obj);
#endif
#ifdef USE_NUMBER
static void notify_number_update(number::Number *obj);
#endif
#ifdef USE_DATETIME_DATE
static void notify_date_update(datetime::DateEntity *obj);
#endif
#ifdef USE_DATETIME_TIME
static void notify_time_update(datetime::TimeEntity *obj);
#endif
#ifdef USE_DATETIME_DATETIME
static void notify_datetime_update(datetime::DateTimeEntity *obj);
#endif
#ifdef USE_TEXT
static void notify_text_update(text::Text *obj);
#endif
#ifdef USE_SELECT
static void notify_select_update(select::Select *obj);
#endif
#ifdef USE_LOCK
static void notify_lock_update(lock::Lock *obj);
#endif
#ifdef USE_VALVE
static void notify_valve_update(valve::Valve *obj);
#endif
#ifdef USE_MEDIA_PLAYER
static void notify_media_player_update(media_player::MediaPlayer *obj);
#endif
#ifdef USE_ALARM_CONTROL_PANEL
static void notify_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj);
#endif
#ifdef USE_EVENT
static void notify_event(event::Event *obj);
#endif
#ifdef USE_UPDATE
static void notify_update(update::UpdateEntity *obj);
#endif
protected:
static StaticVector<Controller *, CONTROLLER_REGISTRY_MAX> controllers;
};
} // namespace esphome
#endif // USE_CONTROLLER_REGISTRY

View File

@@ -28,6 +28,7 @@
#define USE_BUTTON
#define USE_CAMERA
#define USE_CLIMATE
#define USE_CONTROLLER_REGISTRY
#define USE_COVER
#define USE_DATETIME
#define USE_DATETIME_DATE
@@ -296,6 +297,7 @@
#define USE_DASHBOARD_IMPORT
// Default counts for static analysis
#define CONTROLLER_REGISTRY_MAX 2
#define ESPHOME_COMPONENT_COUNT 50
#define ESPHOME_DEVICE_COUNT 10
#define ESPHOME_AREA_COUNT 10

View File

@@ -869,67 +869,6 @@ template<typename... Ts> class CallbackManager<void(Ts...)> {
std::vector<std::function<void(Ts...)>> callbacks_;
};
template<typename... X> class PartitionedCallbackManager;
/** Helper class for callbacks partitioned into two sections.
*
* Uses a single vector partitioned into two sections: [first_0, ..., first_m-1, second_0, ..., second_n-1]
* The partition point is tracked externally by the caller (typically stored in the entity class for optimal alignment).
*
* Memory efficient: Only stores a 4-byte pointer. The partition count lives in the entity class where it can be
* packed with other small fields to avoid padding waste.
*
* @tparam Ts The arguments for the callbacks, wrapped in void().
*/
template<typename... Ts> class PartitionedCallbackManager<void(Ts...)> {
public:
/// Add a callback to the first partition.
void add_first(std::function<void(Ts...)> &&callback, uint8_t *first_count) {
if (!this->callbacks_) {
this->callbacks_ = make_unique<std::vector<std::function<void(Ts...)>>>();
}
// Add to first partition: append then swap into position
this->callbacks_->push_back(std::move(callback));
if (*first_count < this->callbacks_->size() - 1) {
std::swap((*this->callbacks_)[*first_count], (*this->callbacks_)[this->callbacks_->size() - 1]);
}
(*first_count)++;
}
/// Add a callback to the second partition.
void add_second(std::function<void(Ts...)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = make_unique<std::vector<std::function<void(Ts...)>>>();
}
// Add to second partition: just append (already at end after first partition)
this->callbacks_->push_back(std::move(callback));
}
/// Call all callbacks in the first partition.
void call_first(uint8_t first_count, Ts... args) {
if (this->callbacks_) {
for (size_t i = 0; i < first_count; i++) {
(*this->callbacks_)[i](args...);
}
}
}
/// Call all callbacks in the second partition.
void call_second(uint8_t first_count, Ts... args) {
if (this->callbacks_) {
for (size_t i = first_count; i < this->callbacks_->size(); i++) {
(*this->callbacks_)[i](args...);
}
}
}
protected:
/// Partitioned callback storage: [first_0, ..., first_m-1, second_0, ..., second_n-1]
std::unique_ptr<std::vector<std::function<void(Ts...)>>> callbacks_;
};
/// Helper class to deduplicate items in a series of values.
template<typename T> class Deduplicator {
public:

View File

@@ -178,6 +178,14 @@ api:
- logger.log: "Skipped loops"
- logger.log: "After combined test"
event:
- platform: template
name: Test Event
id: test_event
event_types:
- single_click
- double_click
globals:
- id: api_continuation_test_counter
type: int