[api] Add zero-copy support for Home Assistant state response messages (#12585)

This commit is contained in:
J. Nick Koston
2025-12-21 07:31:54 -10:00
committed by GitHub
parent a799ac6488
commit c70eab931e
6 changed files with 61 additions and 23 deletions

View File

@@ -824,9 +824,9 @@ message HomeAssistantStateResponse {
option (no_delay) = true;
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
string entity_id = 1;
string state = 2;
string attribute = 3;
string entity_id = 1 [(pointer_to_buffer) = true];
string state = 2 [(pointer_to_buffer) = true];
string attribute = 3 [(pointer_to_buffer) = true];
}
// ==================== IMPORT TIME ====================

View File

@@ -1582,15 +1582,29 @@ 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()) {
// 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());
// Skip if entity_id is empty (invalid message)
if (msg.entity_id_len == 0) {
return;
}
if (entity_match && attribute_match) {
it.callback(msg.state);
for (auto &it : this->parent_->get_state_subs()) {
// Compare entity_id: check length matches and content matches
size_t entity_id_len = strlen(it.entity_id);
if (entity_id_len != msg.entity_id_len || memcmp(it.entity_id, msg.entity_id, msg.entity_id_len) != 0) {
continue;
}
// Compare attribute: either both have matching attribute, or both have none
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
if (sub_attr_len != msg.attribute_len ||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute, sub_attr_len) != 0)) {
continue;
}
// Create temporary string for callback (callback takes const std::string &)
// Handle empty state (nullptr with len=0)
std::string state(msg.state_len > 0 ? reinterpret_cast<const char *>(msg.state) : "", msg.state_len);
it.callback(state);
}
}
#endif

View File

@@ -966,15 +966,24 @@ void SubscribeHomeAssistantStateResponse::calculate_size(ProtoSize &size) const
}
bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1:
this->entity_id = value.as_string();
case 1: {
// Use raw data directly to avoid allocation
this->entity_id = value.data();
this->entity_id_len = value.size();
break;
case 2:
this->state = value.as_string();
}
case 2: {
// Use raw data directly to avoid allocation
this->state = value.data();
this->state_len = value.size();
break;
case 3:
this->attribute = value.as_string();
}
case 3: {
// Use raw data directly to avoid allocation
this->attribute = value.data();
this->attribute_len = value.size();
break;
}
default:
return false;
}

View File

@@ -1203,13 +1203,16 @@ class SubscribeHomeAssistantStateResponse final : public ProtoMessage {
class HomeAssistantStateResponse final : public ProtoDecodableMessage {
public:
static constexpr uint8_t MESSAGE_TYPE = 40;
static constexpr uint8_t ESTIMATED_SIZE = 27;
static constexpr uint8_t ESTIMATED_SIZE = 57;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *message_name() const override { return "home_assistant_state_response"; }
#endif
std::string entity_id{};
std::string state{};
std::string attribute{};
const uint8_t *entity_id{nullptr};
uint16_t entity_id_len{0};
const uint8_t *state{nullptr};
uint16_t state_len{0};
const uint8_t *attribute{nullptr};
uint16_t attribute_len{0};
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
#endif

View File

@@ -1184,9 +1184,15 @@ void SubscribeHomeAssistantStateResponse::dump_to(std::string &out) const {
}
void HomeAssistantStateResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "HomeAssistantStateResponse");
dump_field(out, "entity_id", this->entity_id);
dump_field(out, "state", this->state);
dump_field(out, "attribute", this->attribute);
out.append(" entity_id: ");
out.append(format_hex_pretty(this->entity_id, this->entity_id_len));
out.append("\n");
out.append(" state: ");
out.append(format_hex_pretty(this->state, this->state_len));
out.append("\n");
out.append(" attribute: ");
out.append(format_hex_pretty(this->attribute, this->attribute_len));
out.append("\n");
}
#endif
void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); }

View File

@@ -179,6 +179,12 @@ async def test_api_homeassistant(
client.send_home_assistant_state("binary_sensor.external_motion", "", "ON")
client.send_home_assistant_state("weather.home", "condition", "sunny")
# Test edge cases for zero-copy implementation safety
# Empty entity_id should be silently ignored (no crash)
client.send_home_assistant_state("", "", "should_be_ignored")
# Empty state with valid entity should work (use different entity to not interfere with test)
client.send_home_assistant_state("sensor.edge_case_empty_state", "", "")
# List entities and services
_, services = await client.list_entities_services()