[api] Store Home Assistant state subscriptions in flash instead of heap (#12008)

This commit is contained in:
J. Nick Koston
2025-12-09 16:26:11 +01:00
committed by GitHub
parent 443f9c3f57
commit 72c74bc0b3
17 changed files with 144 additions and 72 deletions
+8 -3
View File
@@ -1580,7 +1580,12 @@ bool APIConnection::send_device_info_response(const DeviceInfoRequest &msg) {
#ifdef USE_API_HOMEASSISTANT_STATES
void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
for (auto &it : this->parent_->get_state_subs()) {
if (it.entity_id == msg.entity_id && it.attribute.value() == msg.attribute) {
// Compare entity_id and attribute with message fields
bool entity_match = (strcmp(it.entity_id, msg.entity_id.c_str()) == 0);
bool attribute_match = (it.attribute != nullptr && strcmp(it.attribute, msg.attribute.c_str()) == 0) ||
(it.attribute == nullptr && msg.attribute.empty());
if (entity_match && attribute_match) {
it.callback(msg.state);
}
}
@@ -1959,8 +1964,8 @@ void APIConnection::process_state_subscriptions_() {
SubscribeHomeAssistantStateResponse resp;
resp.set_entity_id(StringRef(it.entity_id));
// Avoid string copy by directly using the optional's value if it exists
resp.set_attribute(it.attribute.has_value() ? StringRef(it.attribute.value()) : StringRef(""));
// Avoid string copy by using the const char* pointer if it exists
resp.set_attribute(it.attribute != nullptr ? StringRef(it.attribute) : StringRef(""));
resp.once = it.once;
if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
+44 -13
View File
@@ -419,25 +419,56 @@ void APIServer::handle_action_response(uint32_t call_id, bool success, const std
#endif // USE_API_HOMEASSISTANT_SERVICES
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper to add subscription (reduces duplication)
void APIServer::add_state_subscription_(const char *entity_id, const char *attribute,
std::function<void(std::string)> f, bool once) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = entity_id, .attribute = attribute, .callback = std::move(f), .once = once,
// entity_id_dynamic_storage and attribute_dynamic_storage remain nullptr (no heap allocation)
});
}
// Helper to add subscription with heap-allocated strings (reduces duplication)
void APIServer::add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f, bool once) {
HomeAssistantStateSubscription sub;
// Allocate heap storage for the strings
sub.entity_id_dynamic_storage = std::make_unique<std::string>(std::move(entity_id));
sub.entity_id = sub.entity_id_dynamic_storage->c_str();
if (attribute.has_value()) {
sub.attribute_dynamic_storage = std::make_unique<std::string>(std::move(attribute.value()));
sub.attribute = sub.attribute_dynamic_storage->c_str();
} else {
sub.attribute = nullptr;
}
sub.callback = std::move(f);
sub.once = once;
this->state_subs_.push_back(std::move(sub));
}
// New const char* overload (for internal components - zero allocation)
void APIServer::subscribe_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(std::string)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), false);
}
void APIServer::get_home_assistant_state(const char *entity_id, const char *attribute,
std::function<void(std::string)> f) {
this->add_state_subscription_(entity_id, attribute, std::move(f), true);
}
// Existing std::string overload (for custom_api_device.h - heap allocation)
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = false,
});
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), false);
}
void APIServer::get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f) {
this->state_subs_.push_back(HomeAssistantStateSubscription{
.entity_id = std::move(entity_id),
.attribute = std::move(attribute),
.callback = std::move(f),
.once = true,
});
};
this->add_state_subscription_(std::move(entity_id), std::move(attribute), std::move(f), true);
}
const std::vector<APIServer::HomeAssistantStateSubscription> &APIServer::get_state_subs() const {
return this->state_subs_;
+20 -2
View File
@@ -190,16 +190,27 @@ class APIServer : public Component,
#ifdef USE_API_HOMEASSISTANT_STATES
struct HomeAssistantStateSubscription {
std::string entity_id;
optional<std::string> attribute;
const char *entity_id; // Pointer to flash (internal) or heap (external)
const char *attribute; // Pointer to flash or nullptr (nullptr means no attribute)
std::function<void(std::string)> callback;
bool once;
// Dynamic storage for external components using std::string API (custom_api_device.h)
// These are only allocated when using the std::string overload (nullptr for const char* overload)
std::unique_ptr<std::string> entity_id_dynamic_storage;
std::unique_ptr<std::string> attribute_dynamic_storage;
};
// New const char* overload (for internal components - zero allocation)
void subscribe_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f);
void get_home_assistant_state(const char *entity_id, const char *attribute, std::function<void(std::string)> f);
// Existing std::string overload (for custom_api_device.h - heap allocation)
void subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f);
void get_home_assistant_state(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f);
const std::vector<HomeAssistantStateSubscription> &get_state_subs() const;
#endif
#ifdef USE_API_USER_DEFINED_ACTIONS
@@ -220,6 +231,13 @@ class APIServer : public Component,
bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
const psk_t &active_psk, bool make_active);
#endif // USE_API_NOISE
#ifdef USE_API_HOMEASSISTANT_STATES
// Helper methods to reduce code duplication
void add_state_subscription_(const char *entity_id, const char *attribute, std::function<void(std::string)> f,
bool once);
void add_state_subscription_(std::string entity_id, optional<std::string> attribute,
std::function<void(std::string)> f, bool once);
#endif // USE_API_HOMEASSISTANT_STATES
// Pointers and pointer-like types first (4 bytes each)
std::unique_ptr<socket::Socket> socket_ = nullptr;
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
@@ -19,11 +19,10 @@ void HomeassistantBinarySensor::setup() {
case PARSE_ON:
case PARSE_OFF:
bool new_state = val == PARSE_ON;
if (this->attribute_.has_value()) {
ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_.c_str(),
this->attribute_.value().c_str(), ONOFF(new_state));
if (this->attribute_ != nullptr) {
ESP_LOGD(TAG, "'%s::%s': Got attribute state %s", this->entity_id_, this->attribute_, ONOFF(new_state));
} else {
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state));
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state));
}
if (this->initial_) {
this->publish_initial_state(new_state);
@@ -37,9 +36,9 @@ void HomeassistantBinarySensor::setup() {
}
void HomeassistantBinarySensor::dump_config() {
LOG_BINARY_SENSOR("", "Homeassistant Binary Sensor", this);
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
if (this->attribute_.has_value()) {
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str());
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
if (this->attribute_ != nullptr) {
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_);
}
}
float HomeassistantBinarySensor::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
@@ -8,15 +8,15 @@ namespace homeassistant {
class HomeassistantBinarySensor : public binary_sensor::BinarySensor, public Component {
public:
void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; }
void set_attribute(const std::string &attribute) { attribute_ = attribute; }
void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; }
void set_attribute(const char *attribute) { this->attribute_ = attribute; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
std::string entity_id_;
optional<std::string> attribute_;
const char *entity_id_{nullptr};
const char *attribute_{nullptr};
bool initial_{true};
};
@@ -12,21 +12,21 @@ static const char *const TAG = "homeassistant.number";
void HomeassistantNumber::state_changed_(const std::string &state) {
auto number_value = parse_number<float>(state);
if (!number_value.has_value()) {
ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str());
ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_, state.c_str());
this->publish_state(NAN);
return;
}
if (this->state == number_value.value()) {
return;
}
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), state.c_str());
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, state.c_str());
this->publish_state(number_value.value());
}
void HomeassistantNumber::min_retrieved_(const std::string &min) {
auto min_value = parse_number<float>(min);
if (!min_value.has_value()) {
ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_.c_str(), min.c_str());
ESP_LOGE(TAG, "'%s': Can't convert 'min' value '%s' to number!", this->entity_id_, min.c_str());
return;
}
ESP_LOGD(TAG, "'%s': Min retrieved: %s", get_name().c_str(), min.c_str());
@@ -36,7 +36,7 @@ void HomeassistantNumber::min_retrieved_(const std::string &min) {
void HomeassistantNumber::max_retrieved_(const std::string &max) {
auto max_value = parse_number<float>(max);
if (!max_value.has_value()) {
ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_.c_str(), max.c_str());
ESP_LOGE(TAG, "'%s': Can't convert 'max' value '%s' to number!", this->entity_id_, max.c_str());
return;
}
ESP_LOGD(TAG, "'%s': Max retrieved: %s", get_name().c_str(), max.c_str());
@@ -46,7 +46,7 @@ void HomeassistantNumber::max_retrieved_(const std::string &max) {
void HomeassistantNumber::step_retrieved_(const std::string &step) {
auto step_value = parse_number<float>(step);
if (!step_value.has_value()) {
ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_.c_str(), step.c_str());
ESP_LOGE(TAG, "'%s': Can't convert 'step' value '%s' to number!", this->entity_id_, step.c_str());
return;
}
ESP_LOGD(TAG, "'%s': Step Retrieved %s", get_name().c_str(), step.c_str());
@@ -55,22 +55,19 @@ void HomeassistantNumber::step_retrieved_(const std::string &step) {
void HomeassistantNumber::setup() {
api::global_api_server->subscribe_home_assistant_state(
this->entity_id_, nullopt, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1));
this->entity_id_, nullptr, std::bind(&HomeassistantNumber::state_changed_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(
this->entity_id_, optional<std::string>("min"),
std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1));
this->entity_id_, "min", std::bind(&HomeassistantNumber::min_retrieved_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(
this->entity_id_, optional<std::string>("max"),
std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1));
this->entity_id_, "max", std::bind(&HomeassistantNumber::max_retrieved_, this, std::placeholders::_1));
api::global_api_server->get_home_assistant_state(
this->entity_id_, optional<std::string>("step"),
std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1));
this->entity_id_, "step", std::bind(&HomeassistantNumber::step_retrieved_, this, std::placeholders::_1));
}
void HomeassistantNumber::dump_config() {
LOG_NUMBER("", "Homeassistant Number", this);
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
}
float HomeassistantNumber::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
@@ -11,7 +11,7 @@ namespace homeassistant {
class HomeassistantNumber : public number::Number, public Component {
public:
void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; }
void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; }
void setup() override;
void dump_config() override;
@@ -25,7 +25,7 @@ class HomeassistantNumber : public number::Number, public Component {
void control(float value) override;
std::string entity_id_;
const char *entity_id_{nullptr};
};
} // namespace homeassistant
} // namespace esphome
@@ -12,25 +12,24 @@ void HomeassistantSensor::setup() {
this->entity_id_, this->attribute_, [this](const std::string &state) {
auto val = parse_number<float>(state);
if (!val.has_value()) {
ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_.c_str(), state.c_str());
ESP_LOGW(TAG, "'%s': Can't convert '%s' to number!", this->entity_id_, state.c_str());
this->publish_state(NAN);
return;
}
if (this->attribute_.has_value()) {
ESP_LOGD(TAG, "'%s::%s': Got attribute state %.2f", this->entity_id_.c_str(),
this->attribute_.value().c_str(), *val);
if (this->attribute_ != nullptr) {
ESP_LOGD(TAG, "'%s::%s': Got attribute state %.2f", this->entity_id_, this->attribute_, *val);
} else {
ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_.c_str(), *val);
ESP_LOGD(TAG, "'%s': Got state %.2f", this->entity_id_, *val);
}
this->publish_state(*val);
});
}
void HomeassistantSensor::dump_config() {
LOG_SENSOR("", "Homeassistant Sensor", this);
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
if (this->attribute_.has_value()) {
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str());
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
if (this->attribute_ != nullptr) {
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_);
}
}
float HomeassistantSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
@@ -8,15 +8,15 @@ namespace homeassistant {
class HomeassistantSensor : public sensor::Sensor, public Component {
public:
void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; }
void set_attribute(const std::string &attribute) { attribute_ = attribute; }
void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; }
void set_attribute(const char *attribute) { this->attribute_ = attribute; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
std::string entity_id_;
optional<std::string> attribute_;
const char *entity_id_{nullptr};
const char *attribute_{nullptr};
};
} // namespace homeassistant
@@ -10,7 +10,7 @@ static const char *const TAG = "homeassistant.switch";
using namespace esphome::switch_;
void HomeassistantSwitch::setup() {
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullopt, [this](const std::string &state) {
api::global_api_server->subscribe_home_assistant_state(this->entity_id_, nullptr, [this](const std::string &state) {
auto val = parse_on_off(state.c_str());
switch (val) {
case PARSE_NONE:
@@ -20,7 +20,7 @@ void HomeassistantSwitch::setup() {
case PARSE_ON:
case PARSE_OFF:
bool new_state = val == PARSE_ON;
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_.c_str(), ONOFF(new_state));
ESP_LOGD(TAG, "'%s': Got state %s", this->entity_id_, ONOFF(new_state));
this->publish_state(new_state);
break;
}
@@ -29,7 +29,7 @@ void HomeassistantSwitch::setup() {
void HomeassistantSwitch::dump_config() {
LOG_SWITCH("", "Homeassistant Switch", this);
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
}
float HomeassistantSwitch::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
@@ -8,14 +8,14 @@ namespace homeassistant {
class HomeassistantSwitch : public switch_::Switch, public Component {
public:
void set_entity_id(const std::string &entity_id) { this->entity_id_ = entity_id; }
void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
void write_state(bool state) override;
std::string entity_id_;
const char *entity_id_{nullptr};
};
} // namespace homeassistant
@@ -10,20 +10,19 @@ static const char *const TAG = "homeassistant.text_sensor";
void HomeassistantTextSensor::setup() {
api::global_api_server->subscribe_home_assistant_state(
this->entity_id_, this->attribute_, [this](const std::string &state) {
if (this->attribute_.has_value()) {
ESP_LOGD(TAG, "'%s::%s': Got attribute state '%s'", this->entity_id_.c_str(),
this->attribute_.value().c_str(), state.c_str());
if (this->attribute_ != nullptr) {
ESP_LOGD(TAG, "'%s::%s': Got attribute state '%s'", this->entity_id_, this->attribute_, state.c_str());
} else {
ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_.c_str(), state.c_str());
ESP_LOGD(TAG, "'%s': Got state '%s'", this->entity_id_, state.c_str());
}
this->publish_state(state);
});
}
void HomeassistantTextSensor::dump_config() {
LOG_TEXT_SENSOR("", "Homeassistant Text Sensor", this);
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_.c_str());
if (this->attribute_.has_value()) {
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_.value().c_str());
ESP_LOGCONFIG(TAG, " Entity ID: '%s'", this->entity_id_);
if (this->attribute_ != nullptr) {
ESP_LOGCONFIG(TAG, " Attribute: '%s'", this->attribute_);
}
}
float HomeassistantTextSensor::get_setup_priority() const { return setup_priority::AFTER_CONNECTION; }
@@ -8,15 +8,15 @@ namespace homeassistant {
class HomeassistantTextSensor : public text_sensor::TextSensor, public Component {
public:
void set_entity_id(const std::string &entity_id) { entity_id_ = entity_id; }
void set_attribute(const std::string &attribute) { attribute_ = attribute; }
void set_entity_id(const char *entity_id) { this->entity_id_ = entity_id; }
void set_attribute(const char *attribute) { this->attribute_ = attribute; }
void setup() override;
void dump_config() override;
float get_setup_priority() const override;
protected:
std::string entity_id_;
optional<std::string> attribute_;
const char *entity_id_{nullptr};
const char *attribute_{nullptr};
};
} // namespace homeassistant