Merge pull request #9094 from esphome/bump-2025.6.0b2

2025.6.0b2
This commit is contained in:
Jesse Hills 2025-06-16 17:06:59 +12:00 committed by GitHub
commit 426be153db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
81 changed files with 2313 additions and 778 deletions

View File

@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version # could be handy for archiving the generated documentation or if some version
# control system is used. # control system is used.
PROJECT_NUMBER = 2025.6.0b1 PROJECT_NUMBER = 2025.6.0b2
# Using the PROJECT_BRIEF tag one can provide an optional one line description # Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a # for a project that appears at the top of each page and should give viewer a

View File

@ -266,6 +266,7 @@ enum EntityCategory {
// ==================== BINARY SENSOR ==================== // ==================== BINARY SENSOR ====================
message ListEntitiesBinarySensorResponse { message ListEntitiesBinarySensorResponse {
option (id) = 12; option (id) = 12;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BINARY_SENSOR"; option (ifdef) = "USE_BINARY_SENSOR";
@ -282,6 +283,7 @@ message ListEntitiesBinarySensorResponse {
} }
message BinarySensorStateResponse { message BinarySensorStateResponse {
option (id) = 21; option (id) = 21;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BINARY_SENSOR"; option (ifdef) = "USE_BINARY_SENSOR";
option (no_delay) = true; option (no_delay) = true;
@ -296,6 +298,7 @@ message BinarySensorStateResponse {
// ==================== COVER ==================== // ==================== COVER ====================
message ListEntitiesCoverResponse { message ListEntitiesCoverResponse {
option (id) = 13; option (id) = 13;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_COVER"; option (ifdef) = "USE_COVER";
@ -325,6 +328,7 @@ enum CoverOperation {
} }
message CoverStateResponse { message CoverStateResponse {
option (id) = 22; option (id) = 22;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_COVER"; option (ifdef) = "USE_COVER";
option (no_delay) = true; option (no_delay) = true;
@ -367,6 +371,7 @@ message CoverCommandRequest {
// ==================== FAN ==================== // ==================== FAN ====================
message ListEntitiesFanResponse { message ListEntitiesFanResponse {
option (id) = 14; option (id) = 14;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_FAN"; option (ifdef) = "USE_FAN";
@ -395,6 +400,7 @@ enum FanDirection {
} }
message FanStateResponse { message FanStateResponse {
option (id) = 23; option (id) = 23;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_FAN"; option (ifdef) = "USE_FAN";
option (no_delay) = true; option (no_delay) = true;
@ -444,6 +450,7 @@ enum ColorMode {
} }
message ListEntitiesLightResponse { message ListEntitiesLightResponse {
option (id) = 15; option (id) = 15;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LIGHT"; option (ifdef) = "USE_LIGHT";
@ -467,6 +474,7 @@ message ListEntitiesLightResponse {
} }
message LightStateResponse { message LightStateResponse {
option (id) = 24; option (id) = 24;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LIGHT"; option (ifdef) = "USE_LIGHT";
option (no_delay) = true; option (no_delay) = true;
@ -536,6 +544,7 @@ enum SensorLastResetType {
message ListEntitiesSensorResponse { message ListEntitiesSensorResponse {
option (id) = 16; option (id) = 16;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SENSOR"; option (ifdef) = "USE_SENSOR";
@ -557,6 +566,7 @@ message ListEntitiesSensorResponse {
} }
message SensorStateResponse { message SensorStateResponse {
option (id) = 25; option (id) = 25;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SENSOR"; option (ifdef) = "USE_SENSOR";
option (no_delay) = true; option (no_delay) = true;
@ -571,6 +581,7 @@ message SensorStateResponse {
// ==================== SWITCH ==================== // ==================== SWITCH ====================
message ListEntitiesSwitchResponse { message ListEntitiesSwitchResponse {
option (id) = 17; option (id) = 17;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SWITCH"; option (ifdef) = "USE_SWITCH";
@ -587,6 +598,7 @@ message ListEntitiesSwitchResponse {
} }
message SwitchStateResponse { message SwitchStateResponse {
option (id) = 26; option (id) = 26;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SWITCH"; option (ifdef) = "USE_SWITCH";
option (no_delay) = true; option (no_delay) = true;
@ -607,6 +619,7 @@ message SwitchCommandRequest {
// ==================== TEXT SENSOR ==================== // ==================== TEXT SENSOR ====================
message ListEntitiesTextSensorResponse { message ListEntitiesTextSensorResponse {
option (id) = 18; option (id) = 18;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT_SENSOR"; option (ifdef) = "USE_TEXT_SENSOR";
@ -622,6 +635,7 @@ message ListEntitiesTextSensorResponse {
} }
message TextSensorStateResponse { message TextSensorStateResponse {
option (id) = 27; option (id) = 27;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT_SENSOR"; option (ifdef) = "USE_TEXT_SENSOR";
option (no_delay) = true; option (no_delay) = true;
@ -789,6 +803,7 @@ message ExecuteServiceRequest {
// ==================== CAMERA ==================== // ==================== CAMERA ====================
message ListEntitiesCameraResponse { message ListEntitiesCameraResponse {
option (id) = 43; option (id) = 43;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ESP32_CAMERA"; option (ifdef) = "USE_ESP32_CAMERA";
@ -869,6 +884,7 @@ enum ClimatePreset {
} }
message ListEntitiesClimateResponse { message ListEntitiesClimateResponse {
option (id) = 46; option (id) = 46;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CLIMATE"; option (ifdef) = "USE_CLIMATE";
@ -903,6 +919,7 @@ message ListEntitiesClimateResponse {
} }
message ClimateStateResponse { message ClimateStateResponse {
option (id) = 47; option (id) = 47;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CLIMATE"; option (ifdef) = "USE_CLIMATE";
option (no_delay) = true; option (no_delay) = true;
@ -964,6 +981,7 @@ enum NumberMode {
} }
message ListEntitiesNumberResponse { message ListEntitiesNumberResponse {
option (id) = 49; option (id) = 49;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_NUMBER"; option (ifdef) = "USE_NUMBER";
@ -984,6 +1002,7 @@ message ListEntitiesNumberResponse {
} }
message NumberStateResponse { message NumberStateResponse {
option (id) = 50; option (id) = 50;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_NUMBER"; option (ifdef) = "USE_NUMBER";
option (no_delay) = true; option (no_delay) = true;
@ -1007,6 +1026,7 @@ message NumberCommandRequest {
// ==================== SELECT ==================== // ==================== SELECT ====================
message ListEntitiesSelectResponse { message ListEntitiesSelectResponse {
option (id) = 52; option (id) = 52;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SELECT"; option (ifdef) = "USE_SELECT";
@ -1022,6 +1042,7 @@ message ListEntitiesSelectResponse {
} }
message SelectStateResponse { message SelectStateResponse {
option (id) = 53; option (id) = 53;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SELECT"; option (ifdef) = "USE_SELECT";
option (no_delay) = true; option (no_delay) = true;
@ -1045,6 +1066,7 @@ message SelectCommandRequest {
// ==================== SIREN ==================== // ==================== SIREN ====================
message ListEntitiesSirenResponse { message ListEntitiesSirenResponse {
option (id) = 55; option (id) = 55;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN"; option (ifdef) = "USE_SIREN";
@ -1062,6 +1084,7 @@ message ListEntitiesSirenResponse {
} }
message SirenStateResponse { message SirenStateResponse {
option (id) = 56; option (id) = 56;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN"; option (ifdef) = "USE_SIREN";
option (no_delay) = true; option (no_delay) = true;
@ -1102,6 +1125,7 @@ enum LockCommand {
} }
message ListEntitiesLockResponse { message ListEntitiesLockResponse {
option (id) = 58; option (id) = 58;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK"; option (ifdef) = "USE_LOCK";
@ -1123,6 +1147,7 @@ message ListEntitiesLockResponse {
} }
message LockStateResponse { message LockStateResponse {
option (id) = 59; option (id) = 59;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK"; option (ifdef) = "USE_LOCK";
option (no_delay) = true; option (no_delay) = true;
@ -1145,6 +1170,7 @@ message LockCommandRequest {
// ==================== BUTTON ==================== // ==================== BUTTON ====================
message ListEntitiesButtonResponse { message ListEntitiesButtonResponse {
option (id) = 61; option (id) = 61;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BUTTON"; option (ifdef) = "USE_BUTTON";
@ -1196,6 +1222,7 @@ message MediaPlayerSupportedFormat {
} }
message ListEntitiesMediaPlayerResponse { message ListEntitiesMediaPlayerResponse {
option (id) = 63; option (id) = 63;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER"; option (ifdef) = "USE_MEDIA_PLAYER";
@ -1214,6 +1241,7 @@ message ListEntitiesMediaPlayerResponse {
} }
message MediaPlayerStateResponse { message MediaPlayerStateResponse {
option (id) = 64; option (id) = 64;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER"; option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true; option (no_delay) = true;
@ -1735,6 +1763,7 @@ enum AlarmControlPanelStateCommand {
message ListEntitiesAlarmControlPanelResponse { message ListEntitiesAlarmControlPanelResponse {
option (id) = 94; option (id) = 94;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (ifdef) = "USE_ALARM_CONTROL_PANEL";
@ -1752,6 +1781,7 @@ message ListEntitiesAlarmControlPanelResponse {
message AlarmControlPanelStateResponse { message AlarmControlPanelStateResponse {
option (id) = 95; option (id) = 95;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL"; option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true; option (no_delay) = true;
@ -1776,6 +1806,7 @@ enum TextMode {
} }
message ListEntitiesTextResponse { message ListEntitiesTextResponse {
option (id) = 97; option (id) = 97;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT"; option (ifdef) = "USE_TEXT";
@ -1794,6 +1825,7 @@ message ListEntitiesTextResponse {
} }
message TextStateResponse { message TextStateResponse {
option (id) = 98; option (id) = 98;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT"; option (ifdef) = "USE_TEXT";
option (no_delay) = true; option (no_delay) = true;
@ -1818,6 +1850,7 @@ message TextCommandRequest {
// ==================== DATETIME DATE ==================== // ==================== DATETIME DATE ====================
message ListEntitiesDateResponse { message ListEntitiesDateResponse {
option (id) = 100; option (id) = 100;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATE"; option (ifdef) = "USE_DATETIME_DATE";
@ -1832,6 +1865,7 @@ message ListEntitiesDateResponse {
} }
message DateStateResponse { message DateStateResponse {
option (id) = 101; option (id) = 101;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATE"; option (ifdef) = "USE_DATETIME_DATE";
option (no_delay) = true; option (no_delay) = true;
@ -1859,6 +1893,7 @@ message DateCommandRequest {
// ==================== DATETIME TIME ==================== // ==================== DATETIME TIME ====================
message ListEntitiesTimeResponse { message ListEntitiesTimeResponse {
option (id) = 103; option (id) = 103;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME"; option (ifdef) = "USE_DATETIME_TIME";
@ -1873,6 +1908,7 @@ message ListEntitiesTimeResponse {
} }
message TimeStateResponse { message TimeStateResponse {
option (id) = 104; option (id) = 104;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME"; option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true; option (no_delay) = true;
@ -1900,6 +1936,7 @@ message TimeCommandRequest {
// ==================== EVENT ==================== // ==================== EVENT ====================
message ListEntitiesEventResponse { message ListEntitiesEventResponse {
option (id) = 107; option (id) = 107;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT"; option (ifdef) = "USE_EVENT";
@ -1917,6 +1954,7 @@ message ListEntitiesEventResponse {
} }
message EventResponse { message EventResponse {
option (id) = 108; option (id) = 108;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT"; option (ifdef) = "USE_EVENT";
@ -1927,6 +1965,7 @@ message EventResponse {
// ==================== VALVE ==================== // ==================== VALVE ====================
message ListEntitiesValveResponse { message ListEntitiesValveResponse {
option (id) = 109; option (id) = 109;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VALVE"; option (ifdef) = "USE_VALVE";
@ -1952,6 +1991,7 @@ enum ValveOperation {
} }
message ValveStateResponse { message ValveStateResponse {
option (id) = 110; option (id) = 110;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VALVE"; option (ifdef) = "USE_VALVE";
option (no_delay) = true; option (no_delay) = true;
@ -1976,6 +2016,7 @@ message ValveCommandRequest {
// ==================== DATETIME DATETIME ==================== // ==================== DATETIME DATETIME ====================
message ListEntitiesDateTimeResponse { message ListEntitiesDateTimeResponse {
option (id) = 112; option (id) = 112;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME"; option (ifdef) = "USE_DATETIME_DATETIME";
@ -1990,6 +2031,7 @@ message ListEntitiesDateTimeResponse {
} }
message DateTimeStateResponse { message DateTimeStateResponse {
option (id) = 113; option (id) = 113;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME"; option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true; option (no_delay) = true;
@ -2013,6 +2055,7 @@ message DateTimeCommandRequest {
// ==================== UPDATE ==================== // ==================== UPDATE ====================
message ListEntitiesUpdateResponse { message ListEntitiesUpdateResponse {
option (id) = 116; option (id) = 116;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE"; option (ifdef) = "USE_UPDATE";
@ -2028,6 +2071,7 @@ message ListEntitiesUpdateResponse {
} }
message UpdateStateResponse { message UpdateStateResponse {
option (id) = 117; option (id) = 117;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER; option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE"; option (ifdef) = "USE_UPDATE";
option (no_delay) = true; option (no_delay) = true;

View File

@ -248,25 +248,41 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) {
uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
// Calculate size // Calculate size
uint32_t size = 0; uint32_t calculated_size = 0;
msg.calculate_size(size); msg.calculate_size(calculated_size);
// Cache frame sizes to avoid repeated virtual calls
const uint8_t header_padding = conn->helper_->frame_header_padding();
const uint8_t footer_size = conn->helper_->frame_footer_size();
// Calculate total size with padding for buffer allocation // Calculate total size with padding for buffer allocation
uint16_t total_size = size_t total_calculated_size = calculated_size + header_padding + footer_size;
static_cast<uint16_t>(size) + conn->helper_->frame_header_padding() + conn->helper_->frame_footer_size();
// Check if it fits // Check if it fits
if (total_size > remaining_size) { if (total_calculated_size > remaining_size) {
return 0; // Doesn't fit return 0; // Doesn't fit
} }
// Allocate exact buffer space needed (just the payload, not the overhead) // Allocate buffer space - pass payload size, allocation functions add header/footer space
ProtoWriteBuffer buffer = ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size)
is_single ? conn->allocate_single_message_buffer(size) : conn->allocate_batch_message_buffer(size); : conn->allocate_batch_message_buffer(calculated_size);
// Get buffer size after allocation (which includes header padding)
std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref();
size_t size_before_encode = shared_buf.size();
// Encode directly into buffer // Encode directly into buffer
msg.encode(buffer); msg.encode(buffer);
return total_size;
// Calculate actual encoded size (not including header that was already added)
size_t actual_payload_size = shared_buf.size() - size_before_encode;
// Return actual total size (header + actual payload + footer)
size_t actual_total_size = header_padding + actual_payload_size + footer_size;
// Verify that calculate_size() returned the correct value
assert(calculated_size == actual_payload_size);
return static_cast<uint16_t>(actual_total_size);
} }
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
@ -285,7 +301,7 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn
BinarySensorStateResponse resp; BinarySensorStateResponse resp;
resp.state = binary_sensor->state; resp.state = binary_sensor->state;
resp.missing_state = !binary_sensor->has_state(); resp.missing_state = !binary_sensor->has_state();
resp.key = binary_sensor->get_object_id_hash(); fill_entity_state_base(binary_sensor, resp);
return encode_message_to_buffer(resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -319,7 +335,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *
if (traits.get_supports_tilt()) if (traits.get_supports_tilt())
msg.tilt = cover->tilt; msg.tilt = cover->tilt;
msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation); msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation);
msg.key = cover->get_object_id_hash(); fill_entity_state_base(cover, msg);
return encode_message_to_buffer(msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -387,7 +403,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
msg.direction = static_cast<enums::FanDirection>(fan->direction); msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes()) if (traits.supports_preset_modes())
msg.preset_mode = fan->preset_mode; msg.preset_mode = fan->preset_mode;
msg.key = fan->get_object_id_hash(); fill_entity_state_base(fan, msg);
return encode_message_to_buffer(msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -454,7 +470,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
resp.warm_white = values.get_warm_white(); resp.warm_white = values.get_warm_white();
if (light->supports_effects()) if (light->supports_effects())
resp.effect = light->get_effect_name(); resp.effect = light->get_effect_name();
resp.key = light->get_object_id_hash(); fill_entity_state_base(light, resp);
return encode_message_to_buffer(resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -536,7 +552,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection
SensorStateResponse resp; SensorStateResponse resp;
resp.state = sensor->state; resp.state = sensor->state;
resp.missing_state = !sensor->has_state(); resp.missing_state = !sensor->has_state();
resp.key = sensor->get_object_id_hash(); fill_entity_state_base(sensor, resp);
return encode_message_to_buffer(resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -570,7 +586,7 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection
auto *a_switch = static_cast<switch_::Switch *>(entity); auto *a_switch = static_cast<switch_::Switch *>(entity);
SwitchStateResponse resp; SwitchStateResponse resp;
resp.state = a_switch->state; resp.state = a_switch->state;
resp.key = a_switch->get_object_id_hash(); fill_entity_state_base(a_switch, resp);
return encode_message_to_buffer(resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -613,7 +629,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec
TextSensorStateResponse resp; TextSensorStateResponse resp;
resp.state = text_sensor->state; resp.state = text_sensor->state;
resp.missing_state = !text_sensor->has_state(); resp.missing_state = !text_sensor->has_state();
resp.key = text_sensor->get_object_id_hash(); fill_entity_state_base(text_sensor, resp);
return encode_message_to_buffer(resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@ -637,7 +653,7 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
bool is_single) { bool is_single) {
auto *climate = static_cast<climate::Climate *>(entity); auto *climate = static_cast<climate::Climate *>(entity);
ClimateStateResponse resp; ClimateStateResponse resp;
resp.key = climate->get_object_id_hash(); fill_entity_state_base(climate, resp);
auto traits = climate->get_traits(); auto traits = climate->get_traits();
resp.mode = static_cast<enums::ClimateMode>(climate->mode); resp.mode = static_cast<enums::ClimateMode>(climate->mode);
resp.action = static_cast<enums::ClimateAction>(climate->action); resp.action = static_cast<enums::ClimateAction>(climate->action);
@ -746,7 +762,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection
NumberStateResponse resp; NumberStateResponse resp;
resp.state = number->state; resp.state = number->state;
resp.missing_state = !number->has_state(); resp.missing_state = !number->has_state();
resp.key = number->get_object_id_hash(); fill_entity_state_base(number, resp);
return encode_message_to_buffer(resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -787,7 +803,7 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c
resp.year = date->year; resp.year = date->year;
resp.month = date->month; resp.month = date->month;
resp.day = date->day; resp.day = date->day;
resp.key = date->get_object_id_hash(); fill_entity_state_base(date, resp);
return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_date_info(datetime::DateEntity *date) { void APIConnection::send_date_info(datetime::DateEntity *date) {
@ -824,7 +840,7 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c
resp.hour = time->hour; resp.hour = time->hour;
resp.minute = time->minute; resp.minute = time->minute;
resp.second = time->second; resp.second = time->second;
resp.key = time->get_object_id_hash(); fill_entity_state_base(time, resp);
return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_time_info(datetime::TimeEntity *time) { void APIConnection::send_time_info(datetime::TimeEntity *time) {
@ -863,7 +879,7 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio
ESPTime state = datetime->state_as_esptime(); ESPTime state = datetime->state_as_esptime();
resp.epoch_seconds = state.timestamp; resp.epoch_seconds = state.timestamp;
} }
resp.key = datetime->get_object_id_hash(); fill_entity_state_base(datetime, resp);
return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) {
@ -902,7 +918,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c
TextStateResponse resp; TextStateResponse resp;
resp.state = text->state; resp.state = text->state;
resp.missing_state = !text->has_state(); resp.missing_state = !text->has_state();
resp.key = text->get_object_id_hash(); fill_entity_state_base(text, resp);
return encode_message_to_buffer(resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -943,7 +959,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
SelectStateResponse resp; SelectStateResponse resp;
resp.state = select->state; resp.state = select->state;
resp.missing_state = !select->has_state(); resp.missing_state = !select->has_state();
resp.key = select->get_object_id_hash(); fill_entity_state_base(select, resp);
return encode_message_to_buffer(resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -1003,7 +1019,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c
auto *a_lock = static_cast<lock::Lock *>(entity); auto *a_lock = static_cast<lock::Lock *>(entity);
LockStateResponse resp; LockStateResponse resp;
resp.state = static_cast<enums::LockState>(a_lock->state); resp.state = static_cast<enums::LockState>(a_lock->state);
resp.key = a_lock->get_object_id_hash(); fill_entity_state_base(a_lock, resp);
return encode_message_to_buffer(resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -1047,7 +1063,7 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *
ValveStateResponse resp; ValveStateResponse resp;
resp.position = valve->position; resp.position = valve->position;
resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation); resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation);
resp.key = valve->get_object_id_hash(); fill_entity_state_base(valve, resp);
return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_valve_info(valve::Valve *valve) { void APIConnection::send_valve_info(valve::Valve *valve) {
@ -1095,7 +1111,7 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne
resp.state = static_cast<enums::MediaPlayerState>(report_state); resp.state = static_cast<enums::MediaPlayerState>(report_state);
resp.volume = media_player->volume; resp.volume = media_player->volume;
resp.muted = media_player->is_muted(); resp.muted = media_player->is_muted();
resp.key = media_player->get_object_id_hash(); fill_entity_state_base(media_player, resp);
return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) {
@ -1359,7 +1375,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A
auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity); auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity);
AlarmControlPanelStateResponse resp; AlarmControlPanelStateResponse resp;
resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state());
resp.key = a_alarm_control_panel->get_object_id_hash(); fill_entity_state_base(a_alarm_control_panel, resp);
return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
@ -1423,7 +1439,7 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, const std::
uint32_t remaining_size, bool is_single) { uint32_t remaining_size, bool is_single) {
EventResponse resp; EventResponse resp;
resp.event_type = event_type; resp.event_type = event_type;
resp.key = event->get_object_id_hash(); fill_entity_state_base(event, resp);
return encode_message_to_buffer(resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
@ -1461,7 +1477,7 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection
resp.release_summary = update->update_info.summary; resp.release_summary = update->update_info.summary;
resp.release_url = update->update_info.release_url; resp.release_url = update->update_info.release_url;
} }
resp.key = update->get_object_id_hash(); fill_entity_state_base(update, resp);
return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
} }
void APIConnection::send_update_info(update::UpdateEntity *update) { void APIConnection::send_update_info(update::UpdateEntity *update) {
@ -1522,7 +1538,7 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char
buffer.encode_string(3, line, line_length); // string message = 3 buffer.encode_string(3, line, line_length); // string message = 3
// SubscribeLogsResponse - 29 // SubscribeLogsResponse - 29
return this->send_buffer(buffer, 29); return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
} }
HelloResponse APIConnection::hello(const HelloRequest &msg) { HelloResponse APIConnection::hello(const HelloRequest &msg) {
@ -1669,7 +1685,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
return false; return false;
} }
bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) { bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) {
if (!this->try_to_clear_buffer(message_type != 29)) { // SubscribeLogsResponse if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) { // SubscribeLogsResponse
return false; return false;
} }
@ -1791,7 +1807,7 @@ void APIConnection::process_batch_() {
this->batch_first_message_ = true; this->batch_first_message_ = true;
size_t items_processed = 0; size_t items_processed = 0;
uint32_t remaining_size = MAX_PACKET_SIZE; uint16_t remaining_size = std::numeric_limits<uint16_t>::max();
// Track where each message's header padding begins in the buffer // Track where each message's header padding begins in the buffer
// For plaintext: this is where the 6-byte header padding starts // For plaintext: this is where the 6-byte header padding starts
@ -1816,11 +1832,15 @@ void APIConnection::process_batch_() {
packet_info.emplace_back(item.message_type, current_offset, proto_payload_size); packet_info.emplace_back(item.message_type, current_offset, proto_payload_size);
// Update tracking variables // Update tracking variables
items_processed++;
// After first message, set remaining size to MAX_PACKET_SIZE to avoid fragmentation
if (items_processed == 1) {
remaining_size = MAX_PACKET_SIZE;
}
remaining_size -= payload_size; remaining_size -= payload_size;
// Calculate where the next message's header padding will start // Calculate where the next message's header padding will start
// Current buffer size + footer space (that prepare_message_buffer will add for this message) // Current buffer size + footer space (that prepare_message_buffer will add for this message)
current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size; current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size;
items_processed++;
} }
if (items_processed == 0) { if (items_processed == 0) {

View File

@ -240,8 +240,8 @@ class APIConnection : public APIServerConnection {
// - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size()); shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
// Insert header padding bytes so message encoding starts at the correct position // Resize to add header padding so message encoding starts at the correct position
shared_buf.insert(shared_buf.begin(), header_padding, 0); shared_buf.resize(header_padding);
return {&shared_buf}; return {&shared_buf};
} }
@ -249,32 +249,26 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) { ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) {
// Get reference to shared buffer (it maintains state between batch messages) // Get reference to shared buffer (it maintains state between batch messages)
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
size_t current_size = shared_buf.size();
if (is_first_message) { if (is_first_message) {
// For first message, initialize buffer with header padding
uint8_t header_padding = this->helper_->frame_header_padding();
shared_buf.clear(); shared_buf.clear();
shared_buf.reserve(message_size + header_padding);
shared_buf.resize(header_padding);
// Fill header padding with zeros
std::fill(shared_buf.begin(), shared_buf.end(), 0);
} else {
// For subsequent messages, add footer space for previous message and header for this message
uint8_t footer_size = this->helper_->frame_footer_size();
uint8_t header_padding = this->helper_->frame_header_padding();
// Reserve additional space for everything
shared_buf.reserve(current_size + footer_size + header_padding + message_size);
// Single resize to add both footer and header padding
size_t new_size = current_size + footer_size + header_padding;
shared_buf.resize(new_size);
// Fill the newly added bytes with zeros (footer + header padding)
std::fill(shared_buf.begin() + current_size, shared_buf.end(), 0);
} }
size_t current_size = shared_buf.size();
// Calculate padding to add:
// - First message: just header padding
// - Subsequent messages: footer for previous message + header padding for this message
size_t padding_to_add = is_first_message
? this->helper_->frame_header_padding()
: this->helper_->frame_header_padding() + this->helper_->frame_footer_size();
// Reserve space for padding + message
shared_buf.reserve(current_size + padding_to_add + message_size);
// Resize to add the padding bytes
shared_buf.resize(current_size + padding_to_add);
return {&shared_buf}; return {&shared_buf};
} }
@ -288,8 +282,8 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
protected: protected:
// Helper function to fill common entity fields // Helper function to fill common entity info fields
template<typename ResponseT> static void fill_entity_info_base(esphome::EntityBase *entity, ResponseT &response) { static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) {
// Set common fields that are shared by all entity types // Set common fields that are shared by all entity types
response.key = entity->get_object_id_hash(); response.key = entity->get_object_id_hash();
response.object_id = entity->get_object_id(); response.object_id = entity->get_object_id();
@ -303,6 +297,11 @@ class APIConnection : public APIServerConnection {
response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
} }
// Helper function to fill common entity state fields
static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) {
response.key = entity->get_object_id_hash();
}
// Non-template helper to encode any ProtoMessage // Non-template helper to encode any ProtoMessage
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single); uint32_t remaining_size, bool is_single);

View File

@ -21,4 +21,5 @@ extend google.protobuf.MessageOptions {
optional string ifdef = 1038; optional string ifdef = 1038;
optional bool log = 1039 [default=true]; optional bool log = 1039 [default=true];
optional bool no_delay = 1040 [default=false]; optional bool no_delay = 1040 [default=false];
optional string base_class = 1041;
} }

View File

@ -628,6 +628,7 @@ template<> const char *proto_enum_to_string<enums::UpdateCommand>(enums::UpdateC
} }
} }
#endif #endif
bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) { switch (field_id) {
case 2: { case 2: {

View File

@ -253,6 +253,27 @@ enum UpdateCommand : uint32_t {
} // namespace enums } // namespace enums
class InfoResponseProtoMessage : public ProtoMessage {
public:
~InfoResponseProtoMessage() override = default;
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
protected:
};
class StateResponseProtoMessage : public ProtoMessage {
public:
~StateResponseProtoMessage() override = default;
uint32_t key{0};
protected:
};
class HelloRequest : public ProtoMessage { class HelloRequest : public ProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 1; static constexpr uint16_t MESSAGE_TYPE = 1;
@ -484,22 +505,15 @@ class SubscribeStatesRequest : public ProtoMessage {
protected: protected:
}; };
class ListEntitiesBinarySensorResponse : public ProtoMessage { class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 12; static constexpr uint16_t MESSAGE_TYPE = 12;
static constexpr uint16_t ESTIMATED_SIZE = 56; static constexpr uint16_t ESTIMATED_SIZE = 56;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; } static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string device_class{}; std::string device_class{};
bool is_status_binary_sensor{false}; bool is_status_binary_sensor{false};
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -511,14 +525,13 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class BinarySensorStateResponse : public ProtoMessage { class BinarySensorStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 21; static constexpr uint16_t MESSAGE_TYPE = 21;
static constexpr uint16_t ESTIMATED_SIZE = 9; static constexpr uint16_t ESTIMATED_SIZE = 9;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "binary_sensor_state_response"; } static constexpr const char *message_name() { return "binary_sensor_state_response"; }
#endif #endif
uint32_t key{0};
bool state{false}; bool state{false};
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -531,24 +544,17 @@ class BinarySensorStateResponse : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesCoverResponse : public ProtoMessage { class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 13; static constexpr uint16_t MESSAGE_TYPE = 13;
static constexpr uint16_t ESTIMATED_SIZE = 62; static constexpr uint16_t ESTIMATED_SIZE = 62;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_cover_response"; } static constexpr const char *message_name() { return "list_entities_cover_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
bool assumed_state{false}; bool assumed_state{false};
bool supports_position{false}; bool supports_position{false};
bool supports_tilt{false}; bool supports_tilt{false};
std::string device_class{}; std::string device_class{};
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
bool supports_stop{false}; bool supports_stop{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -561,14 +567,13 @@ class ListEntitiesCoverResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class CoverStateResponse : public ProtoMessage { class CoverStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 22; static constexpr uint16_t MESSAGE_TYPE = 22;
static constexpr uint16_t ESTIMATED_SIZE = 19; static constexpr uint16_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "cover_state_response"; } static constexpr const char *message_name() { return "cover_state_response"; }
#endif #endif
uint32_t key{0};
enums::LegacyCoverState legacy_state{}; enums::LegacyCoverState legacy_state{};
float position{0.0f}; float position{0.0f};
float tilt{0.0f}; float tilt{0.0f};
@ -608,24 +613,17 @@ class CoverCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesFanResponse : public ProtoMessage { class ListEntitiesFanResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 14; static constexpr uint16_t MESSAGE_TYPE = 14;
static constexpr uint16_t ESTIMATED_SIZE = 73; static constexpr uint16_t ESTIMATED_SIZE = 73;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_fan_response"; } static constexpr const char *message_name() { return "list_entities_fan_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
bool supports_oscillation{false}; bool supports_oscillation{false};
bool supports_speed{false}; bool supports_speed{false};
bool supports_direction{false}; bool supports_direction{false};
int32_t supported_speed_count{0}; int32_t supported_speed_count{0};
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
std::vector<std::string> supported_preset_modes{}; std::vector<std::string> supported_preset_modes{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -638,14 +636,13 @@ class ListEntitiesFanResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class FanStateResponse : public ProtoMessage { class FanStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 23; static constexpr uint16_t MESSAGE_TYPE = 23;
static constexpr uint16_t ESTIMATED_SIZE = 26; static constexpr uint16_t ESTIMATED_SIZE = 26;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "fan_state_response"; } static constexpr const char *message_name() { return "fan_state_response"; }
#endif #endif
uint32_t key{0};
bool state{false}; bool state{false};
bool oscillating{false}; bool oscillating{false};
enums::FanSpeed speed{}; enums::FanSpeed speed{};
@ -694,17 +691,13 @@ class FanCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesLightResponse : public ProtoMessage { class ListEntitiesLightResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 15; static constexpr uint16_t MESSAGE_TYPE = 15;
static constexpr uint16_t ESTIMATED_SIZE = 85; static constexpr uint16_t ESTIMATED_SIZE = 85;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_light_response"; } static constexpr const char *message_name() { return "list_entities_light_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::vector<enums::ColorMode> supported_color_modes{}; std::vector<enums::ColorMode> supported_color_modes{};
bool legacy_supports_brightness{false}; bool legacy_supports_brightness{false};
bool legacy_supports_rgb{false}; bool legacy_supports_rgb{false};
@ -713,9 +706,6 @@ class ListEntitiesLightResponse : public ProtoMessage {
float min_mireds{0.0f}; float min_mireds{0.0f};
float max_mireds{0.0f}; float max_mireds{0.0f};
std::vector<std::string> effects{}; std::vector<std::string> effects{};
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -727,14 +717,13 @@ class ListEntitiesLightResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class LightStateResponse : public ProtoMessage { class LightStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 24; static constexpr uint16_t MESSAGE_TYPE = 24;
static constexpr uint16_t ESTIMATED_SIZE = 63; static constexpr uint16_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "light_state_response"; } static constexpr const char *message_name() { return "light_state_response"; }
#endif #endif
uint32_t key{0};
bool state{false}; bool state{false};
float brightness{0.0f}; float brightness{0.0f};
enums::ColorMode color_mode{}; enums::ColorMode color_mode{};
@ -803,26 +792,19 @@ class LightCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesSensorResponse : public ProtoMessage { class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 16; static constexpr uint16_t MESSAGE_TYPE = 16;
static constexpr uint16_t ESTIMATED_SIZE = 73; static constexpr uint16_t ESTIMATED_SIZE = 73;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_sensor_response"; } static constexpr const char *message_name() { return "list_entities_sensor_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
std::string unit_of_measurement{}; std::string unit_of_measurement{};
int32_t accuracy_decimals{0}; int32_t accuracy_decimals{0};
bool force_update{false}; bool force_update{false};
std::string device_class{}; std::string device_class{};
enums::SensorStateClass state_class{}; enums::SensorStateClass state_class{};
enums::SensorLastResetType legacy_last_reset_type{}; enums::SensorLastResetType legacy_last_reset_type{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -834,14 +816,13 @@ class ListEntitiesSensorResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class SensorStateResponse : public ProtoMessage { class SensorStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 25; static constexpr uint16_t MESSAGE_TYPE = 25;
static constexpr uint16_t ESTIMATED_SIZE = 12; static constexpr uint16_t ESTIMATED_SIZE = 12;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "sensor_state_response"; } static constexpr const char *message_name() { return "sensor_state_response"; }
#endif #endif
uint32_t key{0};
float state{0.0f}; float state{0.0f};
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -854,21 +835,14 @@ class SensorStateResponse : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesSwitchResponse : public ProtoMessage { class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 17; static constexpr uint16_t MESSAGE_TYPE = 17;
static constexpr uint16_t ESTIMATED_SIZE = 56; static constexpr uint16_t ESTIMATED_SIZE = 56;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_switch_response"; } static constexpr const char *message_name() { return "list_entities_switch_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool assumed_state{false}; bool assumed_state{false};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string device_class{}; std::string device_class{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -881,14 +855,13 @@ class ListEntitiesSwitchResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class SwitchStateResponse : public ProtoMessage { class SwitchStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 26; static constexpr uint16_t MESSAGE_TYPE = 26;
static constexpr uint16_t ESTIMATED_SIZE = 7; static constexpr uint16_t ESTIMATED_SIZE = 7;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "switch_state_response"; } static constexpr const char *message_name() { return "switch_state_response"; }
#endif #endif
uint32_t key{0};
bool state{false}; bool state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -919,20 +892,13 @@ class SwitchCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesTextSensorResponse : public ProtoMessage { class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 18; static constexpr uint16_t MESSAGE_TYPE = 18;
static constexpr uint16_t ESTIMATED_SIZE = 54; static constexpr uint16_t ESTIMATED_SIZE = 54;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_text_sensor_response"; } static constexpr const char *message_name() { return "list_entities_text_sensor_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string device_class{}; std::string device_class{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -945,14 +911,13 @@ class ListEntitiesTextSensorResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class TextSensorStateResponse : public ProtoMessage { class TextSensorStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 27; static constexpr uint16_t MESSAGE_TYPE = 27;
static constexpr uint16_t ESTIMATED_SIZE = 16; static constexpr uint16_t ESTIMATED_SIZE = 16;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "text_sensor_state_response"; } static constexpr const char *message_name() { return "text_sensor_state_response"; }
#endif #endif
uint32_t key{0};
std::string state{}; std::string state{};
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -1249,20 +1214,13 @@ class ExecuteServiceRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
class ListEntitiesCameraResponse : public ProtoMessage { class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 43; static constexpr uint16_t MESSAGE_TYPE = 43;
static constexpr uint16_t ESTIMATED_SIZE = 45; static constexpr uint16_t ESTIMATED_SIZE = 45;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_camera_response"; } static constexpr const char *message_name() { return "list_entities_camera_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1313,17 +1271,13 @@ class CameraImageRequest : public ProtoMessage {
protected: protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesClimateResponse : public ProtoMessage { class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 46; static constexpr uint16_t MESSAGE_TYPE = 46;
static constexpr uint16_t ESTIMATED_SIZE = 151; static constexpr uint16_t ESTIMATED_SIZE = 151;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_climate_response"; } static constexpr const char *message_name() { return "list_entities_climate_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
bool supports_current_temperature{false}; bool supports_current_temperature{false};
bool supports_two_point_target_temperature{false}; bool supports_two_point_target_temperature{false};
std::vector<enums::ClimateMode> supported_modes{}; std::vector<enums::ClimateMode> supported_modes{};
@ -1337,9 +1291,6 @@ class ListEntitiesClimateResponse : public ProtoMessage {
std::vector<std::string> supported_custom_fan_modes{}; std::vector<std::string> supported_custom_fan_modes{};
std::vector<enums::ClimatePreset> supported_presets{}; std::vector<enums::ClimatePreset> supported_presets{};
std::vector<std::string> supported_custom_presets{}; std::vector<std::string> supported_custom_presets{};
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
float visual_current_temperature_step{0.0f}; float visual_current_temperature_step{0.0f};
bool supports_current_humidity{false}; bool supports_current_humidity{false};
bool supports_target_humidity{false}; bool supports_target_humidity{false};
@ -1356,14 +1307,13 @@ class ListEntitiesClimateResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ClimateStateResponse : public ProtoMessage { class ClimateStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 47; static constexpr uint16_t MESSAGE_TYPE = 47;
static constexpr uint16_t ESTIMATED_SIZE = 65; static constexpr uint16_t ESTIMATED_SIZE = 65;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "climate_state_response"; } static constexpr const char *message_name() { return "climate_state_response"; }
#endif #endif
uint32_t key{0};
enums::ClimateMode mode{}; enums::ClimateMode mode{};
float current_temperature{0.0f}; float current_temperature{0.0f};
float target_temperature{0.0f}; float target_temperature{0.0f};
@ -1430,23 +1380,16 @@ class ClimateCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesNumberResponse : public ProtoMessage { class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 49; static constexpr uint16_t MESSAGE_TYPE = 49;
static constexpr uint16_t ESTIMATED_SIZE = 80; static constexpr uint16_t ESTIMATED_SIZE = 80;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_number_response"; } static constexpr const char *message_name() { return "list_entities_number_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
float min_value{0.0f}; float min_value{0.0f};
float max_value{0.0f}; float max_value{0.0f};
float step{0.0f}; float step{0.0f};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string unit_of_measurement{}; std::string unit_of_measurement{};
enums::NumberMode mode{}; enums::NumberMode mode{};
std::string device_class{}; std::string device_class{};
@ -1461,14 +1404,13 @@ class ListEntitiesNumberResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class NumberStateResponse : public ProtoMessage { class NumberStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 50; static constexpr uint16_t MESSAGE_TYPE = 50;
static constexpr uint16_t ESTIMATED_SIZE = 12; static constexpr uint16_t ESTIMATED_SIZE = 12;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "number_state_response"; } static constexpr const char *message_name() { return "number_state_response"; }
#endif #endif
uint32_t key{0};
float state{0.0f}; float state{0.0f};
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -1499,21 +1441,14 @@ class NumberCommandRequest : public ProtoMessage {
protected: protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
}; };
class ListEntitiesSelectResponse : public ProtoMessage { class ListEntitiesSelectResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 52; static constexpr uint16_t MESSAGE_TYPE = 52;
static constexpr uint16_t ESTIMATED_SIZE = 63; static constexpr uint16_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_select_response"; } static constexpr const char *message_name() { return "list_entities_select_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
std::vector<std::string> options{}; std::vector<std::string> options{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1525,14 +1460,13 @@ class ListEntitiesSelectResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class SelectStateResponse : public ProtoMessage { class SelectStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 53; static constexpr uint16_t MESSAGE_TYPE = 53;
static constexpr uint16_t ESTIMATED_SIZE = 16; static constexpr uint16_t ESTIMATED_SIZE = 16;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "select_state_response"; } static constexpr const char *message_name() { return "select_state_response"; }
#endif #endif
uint32_t key{0};
std::string state{}; std::string state{};
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -1565,23 +1499,16 @@ class SelectCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
class ListEntitiesSirenResponse : public ProtoMessage { class ListEntitiesSirenResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 55; static constexpr uint16_t MESSAGE_TYPE = 55;
static constexpr uint16_t ESTIMATED_SIZE = 67; static constexpr uint16_t ESTIMATED_SIZE = 67;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_siren_response"; } static constexpr const char *message_name() { return "list_entities_siren_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
std::vector<std::string> tones{}; std::vector<std::string> tones{};
bool supports_duration{false}; bool supports_duration{false};
bool supports_volume{false}; bool supports_volume{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -1593,14 +1520,13 @@ class ListEntitiesSirenResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class SirenStateResponse : public ProtoMessage { class SirenStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 56; static constexpr uint16_t MESSAGE_TYPE = 56;
static constexpr uint16_t ESTIMATED_SIZE = 7; static constexpr uint16_t ESTIMATED_SIZE = 7;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "siren_state_response"; } static constexpr const char *message_name() { return "siren_state_response"; }
#endif #endif
uint32_t key{0};
bool state{false}; bool state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -1639,20 +1565,13 @@ class SirenCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesLockResponse : public ProtoMessage { class ListEntitiesLockResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 58; static constexpr uint16_t MESSAGE_TYPE = 58;
static constexpr uint16_t ESTIMATED_SIZE = 60; static constexpr uint16_t ESTIMATED_SIZE = 60;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_lock_response"; } static constexpr const char *message_name() { return "list_entities_lock_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
bool assumed_state{false}; bool assumed_state{false};
bool supports_open{false}; bool supports_open{false};
bool requires_code{false}; bool requires_code{false};
@ -1668,14 +1587,13 @@ class ListEntitiesLockResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class LockStateResponse : public ProtoMessage { class LockStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 59; static constexpr uint16_t MESSAGE_TYPE = 59;
static constexpr uint16_t ESTIMATED_SIZE = 7; static constexpr uint16_t ESTIMATED_SIZE = 7;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "lock_state_response"; } static constexpr const char *message_name() { return "lock_state_response"; }
#endif #endif
uint32_t key{0};
enums::LockState state{}; enums::LockState state{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -1709,20 +1627,13 @@ class LockCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesButtonResponse : public ProtoMessage { class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 61; static constexpr uint16_t MESSAGE_TYPE = 61;
static constexpr uint16_t ESTIMATED_SIZE = 54; static constexpr uint16_t ESTIMATED_SIZE = 54;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_button_response"; } static constexpr const char *message_name() { return "list_entities_button_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string device_class{}; std::string device_class{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -1769,20 +1680,13 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesMediaPlayerResponse : public ProtoMessage { class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 63; static constexpr uint16_t MESSAGE_TYPE = 63;
static constexpr uint16_t ESTIMATED_SIZE = 81; static constexpr uint16_t ESTIMATED_SIZE = 81;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_media_player_response"; } static constexpr const char *message_name() { return "list_entities_media_player_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
bool supports_pause{false}; bool supports_pause{false};
std::vector<MediaPlayerSupportedFormat> supported_formats{}; std::vector<MediaPlayerSupportedFormat> supported_formats{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -1796,14 +1700,13 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class MediaPlayerStateResponse : public ProtoMessage { class MediaPlayerStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 64; static constexpr uint16_t MESSAGE_TYPE = 64;
static constexpr uint16_t ESTIMATED_SIZE = 14; static constexpr uint16_t ESTIMATED_SIZE = 14;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "media_player_state_response"; } static constexpr const char *message_name() { return "media_player_state_response"; }
#endif #endif
uint32_t key{0};
enums::MediaPlayerState state{}; enums::MediaPlayerState state{};
float volume{0.0f}; float volume{0.0f};
bool muted{false}; bool muted{false};
@ -2653,20 +2556,13 @@ class VoiceAssistantSetConfiguration : public ProtoMessage {
protected: protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 94; static constexpr uint16_t MESSAGE_TYPE = 94;
static constexpr uint16_t ESTIMATED_SIZE = 53; static constexpr uint16_t ESTIMATED_SIZE = 53;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; } static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
uint32_t supported_features{0}; uint32_t supported_features{0};
bool requires_code{false}; bool requires_code{false};
bool requires_code_to_arm{false}; bool requires_code_to_arm{false};
@ -2681,14 +2577,13 @@ class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class AlarmControlPanelStateResponse : public ProtoMessage { class AlarmControlPanelStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 95; static constexpr uint16_t MESSAGE_TYPE = 95;
static constexpr uint16_t ESTIMATED_SIZE = 7; static constexpr uint16_t ESTIMATED_SIZE = 7;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "alarm_control_panel_state_response"; } static constexpr const char *message_name() { return "alarm_control_panel_state_response"; }
#endif #endif
uint32_t key{0};
enums::AlarmControlPanelState state{}; enums::AlarmControlPanelState state{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -2721,20 +2616,13 @@ class AlarmControlPanelCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesTextResponse : public ProtoMessage { class ListEntitiesTextResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 97; static constexpr uint16_t MESSAGE_TYPE = 97;
static constexpr uint16_t ESTIMATED_SIZE = 64; static constexpr uint16_t ESTIMATED_SIZE = 64;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_text_response"; } static constexpr const char *message_name() { return "list_entities_text_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
uint32_t min_length{0}; uint32_t min_length{0};
uint32_t max_length{0}; uint32_t max_length{0};
std::string pattern{}; std::string pattern{};
@ -2750,14 +2638,13 @@ class ListEntitiesTextResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class TextStateResponse : public ProtoMessage { class TextStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 98; static constexpr uint16_t MESSAGE_TYPE = 98;
static constexpr uint16_t ESTIMATED_SIZE = 16; static constexpr uint16_t ESTIMATED_SIZE = 16;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "text_state_response"; } static constexpr const char *message_name() { return "text_state_response"; }
#endif #endif
uint32_t key{0};
std::string state{}; std::string state{};
bool missing_state{false}; bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -2790,20 +2677,13 @@ class TextCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
class ListEntitiesDateResponse : public ProtoMessage { class ListEntitiesDateResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 100; static constexpr uint16_t MESSAGE_TYPE = 100;
static constexpr uint16_t ESTIMATED_SIZE = 45; static constexpr uint16_t ESTIMATED_SIZE = 45;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_date_response"; } static constexpr const char *message_name() { return "list_entities_date_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -2815,14 +2695,13 @@ class ListEntitiesDateResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class DateStateResponse : public ProtoMessage { class DateStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 101; static constexpr uint16_t MESSAGE_TYPE = 101;
static constexpr uint16_t ESTIMATED_SIZE = 19; static constexpr uint16_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "date_state_response"; } static constexpr const char *message_name() { return "date_state_response"; }
#endif #endif
uint32_t key{0};
bool missing_state{false}; bool missing_state{false};
uint32_t year{0}; uint32_t year{0};
uint32_t month{0}; uint32_t month{0};
@ -2858,20 +2737,13 @@ class DateCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesTimeResponse : public ProtoMessage { class ListEntitiesTimeResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 103; static constexpr uint16_t MESSAGE_TYPE = 103;
static constexpr uint16_t ESTIMATED_SIZE = 45; static constexpr uint16_t ESTIMATED_SIZE = 45;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_time_response"; } static constexpr const char *message_name() { return "list_entities_time_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -2883,14 +2755,13 @@ class ListEntitiesTimeResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class TimeStateResponse : public ProtoMessage { class TimeStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 104; static constexpr uint16_t MESSAGE_TYPE = 104;
static constexpr uint16_t ESTIMATED_SIZE = 19; static constexpr uint16_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "time_state_response"; } static constexpr const char *message_name() { return "time_state_response"; }
#endif #endif
uint32_t key{0};
bool missing_state{false}; bool missing_state{false};
uint32_t hour{0}; uint32_t hour{0};
uint32_t minute{0}; uint32_t minute{0};
@ -2926,20 +2797,13 @@ class TimeCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesEventResponse : public ProtoMessage { class ListEntitiesEventResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 107; static constexpr uint16_t MESSAGE_TYPE = 107;
static constexpr uint16_t ESTIMATED_SIZE = 72; static constexpr uint16_t ESTIMATED_SIZE = 72;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_event_response"; } static constexpr const char *message_name() { return "list_entities_event_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string device_class{}; std::string device_class{};
std::vector<std::string> event_types{}; std::vector<std::string> event_types{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -2953,14 +2817,13 @@ class ListEntitiesEventResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class EventResponse : public ProtoMessage { class EventResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 108; static constexpr uint16_t MESSAGE_TYPE = 108;
static constexpr uint16_t ESTIMATED_SIZE = 14; static constexpr uint16_t ESTIMATED_SIZE = 14;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "event_response"; } static constexpr const char *message_name() { return "event_response"; }
#endif #endif
uint32_t key{0};
std::string event_type{}; std::string event_type{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -2972,20 +2835,13 @@ class EventResponse : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
}; };
class ListEntitiesValveResponse : public ProtoMessage { class ListEntitiesValveResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 109; static constexpr uint16_t MESSAGE_TYPE = 109;
static constexpr uint16_t ESTIMATED_SIZE = 60; static constexpr uint16_t ESTIMATED_SIZE = 60;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_valve_response"; } static constexpr const char *message_name() { return "list_entities_valve_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string device_class{}; std::string device_class{};
bool assumed_state{false}; bool assumed_state{false};
bool supports_position{false}; bool supports_position{false};
@ -3001,14 +2857,13 @@ class ListEntitiesValveResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ValveStateResponse : public ProtoMessage { class ValveStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 110; static constexpr uint16_t MESSAGE_TYPE = 110;
static constexpr uint16_t ESTIMATED_SIZE = 12; static constexpr uint16_t ESTIMATED_SIZE = 12;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "valve_state_response"; } static constexpr const char *message_name() { return "valve_state_response"; }
#endif #endif
uint32_t key{0};
float position{0.0f}; float position{0.0f};
enums::ValveOperation current_operation{}; enums::ValveOperation current_operation{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -3042,20 +2897,13 @@ class ValveCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class ListEntitiesDateTimeResponse : public ProtoMessage { class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 112; static constexpr uint16_t MESSAGE_TYPE = 112;
static constexpr uint16_t ESTIMATED_SIZE = 45; static constexpr uint16_t ESTIMATED_SIZE = 45;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_date_time_response"; } static constexpr const char *message_name() { return "list_entities_date_time_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
@ -3067,14 +2915,13 @@ class ListEntitiesDateTimeResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class DateTimeStateResponse : public ProtoMessage { class DateTimeStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 113; static constexpr uint16_t MESSAGE_TYPE = 113;
static constexpr uint16_t ESTIMATED_SIZE = 12; static constexpr uint16_t ESTIMATED_SIZE = 12;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "date_time_state_response"; } static constexpr const char *message_name() { return "date_time_state_response"; }
#endif #endif
uint32_t key{0};
bool missing_state{false}; bool missing_state{false};
uint32_t epoch_seconds{0}; uint32_t epoch_seconds{0};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
@ -3105,20 +2952,13 @@ class DateTimeCommandRequest : public ProtoMessage {
protected: protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override; bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
}; };
class ListEntitiesUpdateResponse : public ProtoMessage { class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 116; static constexpr uint16_t MESSAGE_TYPE = 116;
static constexpr uint16_t ESTIMATED_SIZE = 54; static constexpr uint16_t ESTIMATED_SIZE = 54;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_update_response"; } static constexpr const char *message_name() { return "list_entities_update_response"; }
#endif #endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string device_class{}; std::string device_class{};
void encode(ProtoWriteBuffer buffer) const override; void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override; void calculate_size(uint32_t &total_size) const override;
@ -3131,14 +2971,13 @@ class ListEntitiesUpdateResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override; bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
}; };
class UpdateStateResponse : public ProtoMessage { class UpdateStateResponse : public StateResponseProtoMessage {
public: public:
static constexpr uint16_t MESSAGE_TYPE = 117; static constexpr uint16_t MESSAGE_TYPE = 117;
static constexpr uint16_t ESTIMATED_SIZE = 61; static constexpr uint16_t ESTIMATED_SIZE = 61;
#ifdef HAS_PROTO_MESSAGE_DUMP #ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "update_state_response"; } static constexpr const char *message_name() { return "update_state_response"; }
#endif #endif
uint32_t key{0};
bool missing_state{false}; bool missing_state{false};
bool in_progress{false}; bool in_progress{false};
bool has_progress{false}; bool has_progress{false};

View File

@ -46,12 +46,10 @@ async def async_run_logs(config: dict[str, Any], address: str) -> None:
time_ = datetime.now() time_ = datetime.now()
message: bytes = msg.message message: bytes = msg.message
text = message.decode("utf8", "backslashreplace") text = message.decode("utf8", "backslashreplace")
if dashboard:
text = text.replace("\033", "\\033")
for parsed_msg in parse_log_message( for parsed_msg in parse_log_message(
text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]" text, f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}]"
): ):
print(parsed_msg) print(parsed_msg.replace("\033", "\\033") if dashboard else parsed_msg)
stop = await async_run(cli, on_log, name=name) stop = await async_run(cli, on_log, name=name)
try: try:

View File

@ -216,7 +216,7 @@ class ProtoWriteBuffer {
this->buffer_->insert(this->buffer_->end(), data, data + len); this->buffer_->insert(this->buffer_->end(), data, data + len);
} }
void encode_string(uint32_t field_id, const std::string &value, bool force = false) { void encode_string(uint32_t field_id, const std::string &value, bool force = false) {
this->encode_string(field_id, value.data(), value.size()); this->encode_string(field_id, value.data(), value.size(), force);
} }
void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) {
this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force); this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);

View File

@ -58,7 +58,7 @@ static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() {
return batch_buffer; return batch_buffer;
} }
bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) {
if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_)
return false; return false;
@ -73,7 +73,7 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p
// Add new advertisements to the batch buffer // Add new advertisements to the batch buffer
for (size_t i = 0; i < count; i++) { for (size_t i = 0; i < count; i++) {
auto &result = advertisements[i]; auto &result = scan_results[i];
uint8_t length = result.adv_data_len + result.scan_rsp_len; uint8_t length = result.adv_data_len + result.scan_rsp_len;
batch_buffer.emplace_back(); batch_buffer.emplace_back();

View File

@ -52,7 +52,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
public: public:
BluetoothProxy(); BluetoothProxy();
bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override;
bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override;
void dump_config() override; void dump_config() override;
void setup() override; void setup() override;
void loop() override; void loop() override;

View File

@ -93,9 +93,8 @@ void BME280Component::setup() {
// Mark as not failed before initializing. Some devices will turn off sensors to save on batteries // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries
// and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component.
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { if (this->is_failed()) {
this->component_state_ &= ~COMPONENT_STATE_MASK; this->reset_to_construction_state();
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
} }
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {

View File

@ -11,25 +11,25 @@ static const char *const TAG = "datetime.date_entity";
void DateEntity::publish_state() { void DateEntity::publish_state() {
if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) {
this->has_state_ = false; this->set_has_state(false);
return; return;
} }
if (this->year_ < 1970 || this->year_ > 3000) { if (this->year_ < 1970 || this->year_ > 3000) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Year must be between 1970 and 3000"); ESP_LOGE(TAG, "Year must be between 1970 and 3000");
return; return;
} }
if (this->month_ < 1 || this->month_ > 12) { if (this->month_ < 1 || this->month_ > 12) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Month must be between 1 and 12"); ESP_LOGE(TAG, "Month must be between 1 and 12");
return; return;
} }
if (this->day_ > days_in_month(this->month_, this->year_)) { if (this->day_ > days_in_month(this->month_, this->year_)) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_);
return; return;
} }
this->has_state_ = true; 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_); ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call(); this->state_callback_.call();
} }

View File

@ -13,9 +13,6 @@ namespace datetime {
class DateTimeBase : public EntityBase { class DateTimeBase : public EntityBase {
public: public:
/// Return whether this Datetime has gotten a full state yet.
bool has_state() const { return this->has_state_; }
virtual ESPTime state_as_esptime() const = 0; virtual ESPTime state_as_esptime() const = 0;
void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
@ -31,8 +28,6 @@ class DateTimeBase : public EntityBase {
#ifdef USE_TIME #ifdef USE_TIME
time::RealTimeClock *rtc_; time::RealTimeClock *rtc_;
#endif #endif
bool has_state_{false};
}; };
#ifdef USE_TIME #ifdef USE_TIME

View File

@ -11,40 +11,40 @@ static const char *const TAG = "datetime.datetime_entity";
void DateTimeEntity::publish_state() { void DateTimeEntity::publish_state() {
if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) {
this->has_state_ = false; this->set_has_state(false);
return; return;
} }
if (this->year_ < 1970 || this->year_ > 3000) { if (this->year_ < 1970 || this->year_ > 3000) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Year must be between 1970 and 3000"); ESP_LOGE(TAG, "Year must be between 1970 and 3000");
return; return;
} }
if (this->month_ < 1 || this->month_ > 12) { if (this->month_ < 1 || this->month_ > 12) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Month must be between 1 and 12"); ESP_LOGE(TAG, "Month must be between 1 and 12");
return; return;
} }
if (this->day_ > days_in_month(this->month_, this->year_)) { if (this->day_ > days_in_month(this->month_, this->year_)) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_);
return; return;
} }
if (this->hour_ > 23) { if (this->hour_ > 23) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Hour must be between 0 and 23"); ESP_LOGE(TAG, "Hour must be between 0 and 23");
return; return;
} }
if (this->minute_ > 59) { if (this->minute_ > 59) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Minute must be between 0 and 59"); ESP_LOGE(TAG, "Minute must be between 0 and 59");
return; return;
} }
if (this->second_ > 59) { if (this->second_ > 59) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Second must be between 0 and 59"); ESP_LOGE(TAG, "Second must be between 0 and 59");
return; return;
} }
this->has_state_ = true; this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, 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->month_, this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call(); this->state_callback_.call();

View File

@ -11,21 +11,21 @@ static const char *const TAG = "datetime.time_entity";
void TimeEntity::publish_state() { void TimeEntity::publish_state() {
if (this->hour_ > 23) { if (this->hour_ > 23) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Hour must be between 0 and 23"); ESP_LOGE(TAG, "Hour must be between 0 and 23");
return; return;
} }
if (this->minute_ > 59) { if (this->minute_ > 59) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Minute must be between 0 and 59"); ESP_LOGE(TAG, "Minute must be between 0 and 59");
return; return;
} }
if (this->second_ > 59) { if (this->second_ > 59) {
this->has_state_ = false; this->set_has_state(false);
ESP_LOGE(TAG, "Second must be between 0 and 59"); ESP_LOGE(TAG, "Second must be between 0 and 59");
return; return;
} }
this->has_state_ = true; this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_); this->second_);
this->state_callback_.call(); this->state_callback_.call();

View File

@ -94,6 +94,13 @@ COMPILER_OPTIMIZATIONS = {
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
} }
ARDUINO_ALLOWED_VARIANTS = [
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
]
def get_cpu_frequencies(*frequencies): def get_cpu_frequencies(*frequencies):
return [str(x) + "MHZ" for x in frequencies] return [str(x) + "MHZ" for x in frequencies]
@ -143,12 +150,17 @@ def set_core_data(config):
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO:
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino"
if variant not in ARDUINO_ALLOWED_VARIANTS:
raise cv.Invalid(
f"ESPHome does not support using the Arduino framework for the {variant}. Please use the ESP-IDF framework instead.",
path=[CONF_FRAMEWORK, CONF_TYPE],
)
CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse(
config[CONF_FRAMEWORK][CONF_VERSION] config[CONF_FRAMEWORK][CONF_VERSION]
) )
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT] CORE.data[KEY_ESP32][KEY_VARIANT] = variant
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {} CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
return config return config
@ -618,6 +630,21 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
) )
def _set_default_framework(config):
if CONF_FRAMEWORK not in config:
config = config.copy()
variant = config[CONF_VARIANT]
if variant in ARDUINO_ALLOWED_VARIANTS:
config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
else:
config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({})
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF
return config
FRAMEWORK_ESP_IDF = "esp-idf" FRAMEWORK_ESP_IDF = "esp-idf"
FRAMEWORK_ARDUINO = "arduino" FRAMEWORK_ARDUINO = "arduino"
FRAMEWORK_SCHEMA = cv.typed_schema( FRAMEWORK_SCHEMA = cv.typed_schema(
@ -627,7 +654,6 @@ FRAMEWORK_SCHEMA = cv.typed_schema(
}, },
lower=True, lower=True,
space="-", space="-",
default_type=FRAMEWORK_ARDUINO,
) )
@ -654,10 +680,11 @@ CONFIG_SCHEMA = cv.All(
), ),
cv.Optional(CONF_PARTITIONS): cv.file_, cv.Optional(CONF_PARTITIONS): cv.file_,
cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True),
cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA,
} }
), ),
_detect_variant, _detect_variant,
_set_default_framework,
set_core_data, set_core_data,
) )

View File

@ -304,20 +304,52 @@ void ESP32BLE::loop() {
BLEEvent *ble_event = this->ble_events_.pop(); BLEEvent *ble_event = this->ble_events_.pop();
while (ble_event != nullptr) { while (ble_event != nullptr) {
switch (ble_event->type_) { switch (ble_event->type_) {
case BLEEvent::GATTS: case BLEEvent::GATTS: {
this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if, esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
&ble_event->event_.gatts.gatts_param); esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if;
esp_ble_gatts_cb_param_t *param = ble_event->event_.gatts.gatts_param;
ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
for (auto *gatts_handler : this->gatts_event_handlers_) {
gatts_handler->gatts_event_handler(event, gatts_if, param);
}
break; break;
case BLEEvent::GATTC: }
this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, case BLEEvent::GATTC: {
&ble_event->event_.gattc.gattc_param); esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event;
esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if;
esp_ble_gattc_cb_param_t *param = ble_event->event_.gattc.gattc_param;
ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event);
for (auto *gattc_handler : this->gattc_event_handlers_) {
gattc_handler->gattc_event_handler(event, gattc_if, param);
}
break; break;
case BLEEvent::GAP: }
this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); case BLEEvent::GAP: {
esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event;
if (gap_event == ESP_GAP_BLE_SCAN_RESULT_EVT) {
// Use the new scan event handler - no memcpy!
for (auto *scan_handler : this->gap_scan_event_handlers_) {
scan_handler->gap_scan_event_handler(ble_event->scan_result());
}
} else if (gap_event == ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT ||
gap_event == ESP_GAP_BLE_SCAN_START_COMPLETE_EVT ||
gap_event == ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT) {
// All three scan complete events have the same structure with just status
// The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
// This is verified at compile-time by static_assert checks in ble_event.h
// The struct already contains our copy of the status (copied in BLEEvent constructor)
ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(
gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
}
}
break; break;
}
default: default:
break; break;
} }
// Destructor will clean up external allocations for GATTC/GATTS
ble_event->~BLEEvent(); ble_event->~BLEEvent();
EVENT_ALLOCATOR.deallocate(ble_event, 1); EVENT_ALLOCATOR.deallocate(ble_event, 1);
ble_event = this->ble_events_.pop(); ble_event = this->ble_events_.pop();
@ -325,61 +357,74 @@ void ESP32BLE::loop() {
if (this->advertising_ != nullptr) { if (this->advertising_ != nullptr) {
this->advertising_->loop(); this->advertising_->loop();
} }
// Log dropped events periodically
size_t dropped = this->ble_events_.get_and_reset_dropped_count();
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %zu BLE events due to buffer overflow", dropped);
}
} }
void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { template<typename... Args> void enqueue_ble_event(Args... args) {
// Check if queue is full before allocating
if (global_ble->ble_events_.full()) {
// Queue is full, drop the event
global_ble->ble_events_.increment_dropped_count();
return;
}
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
if (new_event == nullptr) { if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back // Memory too fragmented to allocate new event. Can only drop it until memory comes back
global_ble->ble_events_.increment_dropped_count();
return; return;
} }
new (new_event) BLEEvent(event, param); new (new_event) BLEEvent(args...);
global_ble->ble_events_.push(new_event);
// Push the event - since we're the only producer and we checked full() above,
// this should always succeed unless we have a bug
if (!global_ble->ble_events_.push(new_event)) {
// This should not happen in SPSC queue with single producer
ESP_LOGE(TAG, "BLE queue push failed unexpectedly");
new_event->~BLEEvent();
EVENT_ALLOCATOR.deallocate(new_event, 1);
}
} // NOLINT(clang-analyzer-unix.Malloc) } // NOLINT(clang-analyzer-unix.Malloc)
void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { // Explicit template instantiations for the friend function
ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);
for (auto *gap_handler : this->gap_event_handlers_) { template void enqueue_ble_event(esp_gatts_cb_event_t, esp_gatt_if_t, esp_ble_gatts_cb_param_t *);
gap_handler->gap_event_handler(event, param); template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gattc_cb_param_t *);
void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) {
// Only queue the 4 GAP events we actually handle
case ESP_GAP_BLE_SCAN_RESULT_EVT:
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
enqueue_ble_event(event, param);
return;
// Ignore these GAP events as they are not relevant for our use case
case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT:
case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT:
return;
default:
break;
} }
ESP_LOGW(TAG, "Ignoring unexpected GAP event type: %d", event);
} }
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) { esp_ble_gatts_cb_param_t *param) {
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); enqueue_ble_event(event, gatts_if, param);
if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
return;
}
new (new_event) BLEEvent(event, gatts_if, param);
global_ble->ble_events_.push(new_event);
} // NOLINT(clang-analyzer-unix.Malloc)
void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
esp_ble_gatts_cb_param_t *param) {
ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event);
for (auto *gatts_handler : this->gatts_event_handlers_) {
gatts_handler->gatts_event_handler(event, gatts_if, param);
}
} }
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); enqueue_ble_event(event, gattc_if, param);
if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
return;
}
new (new_event) BLEEvent(event, gattc_if, param);
global_ble->ble_events_.push(new_event);
} // NOLINT(clang-analyzer-unix.Malloc)
void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
ESP_LOGV(TAG, "(BLE) gattc_event [esp_gatt_if: %d] - %d", gattc_if, event);
for (auto *gattc_handler : this->gattc_event_handlers_) {
gattc_handler->gattc_event_handler(event, gattc_if, param);
}
} }
float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; }

View File

@ -2,6 +2,7 @@
#include "ble_advertising.h" #include "ble_advertising.h"
#include "ble_uuid.h" #include "ble_uuid.h"
#include "ble_scan_result.h"
#include <functional> #include <functional>
@ -22,6 +23,16 @@
namespace esphome { namespace esphome {
namespace esp32_ble { namespace esp32_ble {
// Maximum number of BLE scan results to buffer
#ifdef USE_PSRAM
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32;
#else
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20;
#endif
// Maximum size of the BLE event queue - must be power of 2 for lock-free queue
static constexpr size_t MAX_BLE_QUEUE_SIZE = 64;
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
// NOLINTNEXTLINE(modernize-use-using) // NOLINTNEXTLINE(modernize-use-using)
@ -57,6 +68,11 @@ class GAPEventHandler {
virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0;
}; };
class GAPScanEventHandler {
public:
virtual void gap_scan_event_handler(const BLEScanResult &scan_result) = 0;
};
class GATTcEventHandler { class GATTcEventHandler {
public: public:
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
@ -101,6 +117,9 @@ class ESP32BLE : public Component {
void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback); void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
void register_gap_scan_event_handler(GAPScanEventHandler *handler) {
this->gap_scan_event_handlers_.push_back(handler);
}
void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); }
void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); }
void register_ble_status_event_handler(BLEStatusEventHandler *handler) { void register_ble_status_event_handler(BLEStatusEventHandler *handler) {
@ -113,22 +132,22 @@ class ESP32BLE : public Component {
static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
void real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param);
void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param);
bool ble_setup_(); bool ble_setup_();
bool ble_dismantle_(); bool ble_dismantle_();
bool ble_pre_setup_(); bool ble_pre_setup_();
void advertising_init_(); void advertising_init_();
private:
template<typename... Args> friend void enqueue_ble_event(Args... args);
std::vector<GAPEventHandler *> gap_event_handlers_; std::vector<GAPEventHandler *> gap_event_handlers_;
std::vector<GAPScanEventHandler *> gap_scan_event_handlers_;
std::vector<GATTcEventHandler *> gattc_event_handlers_; std::vector<GATTcEventHandler *> gattc_event_handlers_;
std::vector<GATTsEventHandler *> gatts_event_handlers_; std::vector<GATTsEventHandler *> gatts_event_handlers_;
std::vector<BLEStatusEventHandler *> ble_status_event_handlers_; std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
Queue<BLEEvent> ble_events_; LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
BLEAdvertising *advertising_{}; BLEAdvertising *advertising_{};
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
uint32_t advertising_cycle_time_{}; uint32_t advertising_cycle_time_{};

View File

@ -2,92 +2,232 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <cstddef> // for offsetof
#include <vector> #include <vector>
#include <esp_gap_ble_api.h> #include <esp_gap_ble_api.h>
#include <esp_gattc_api.h> #include <esp_gattc_api.h>
#include <esp_gatts_api.h> #include <esp_gatts_api.h>
#include "ble_scan_result.h"
namespace esphome { namespace esphome {
namespace esp32_ble { namespace esp32_ble {
// Compile-time verification that ESP-IDF scan complete events only contain a status field
// This ensures our reinterpret_cast in ble.cpp is safe
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF scan_param_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF scan_start_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF scan_stop_cmpl structure has unexpected size");
// Verify the status field is at offset 0 (first member)
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) ==
offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl),
"status must be first member of scan_param_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) ==
offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl),
"status must be first member of scan_start_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) ==
offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl),
"status must be first member of scan_stop_cmpl");
// Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop().
// This class stores each event in a single type. // This class stores each event with minimal memory usage.
// GAP events (99% of traffic) don't have the vector overhead.
// GATTC/GATTS events use heap allocation for their param and data.
//
// Event flow:
// 1. ESP-IDF BLE stack calls our static handlers in the BLE task context
// 2. The handlers create a BLEEvent instance, copying only the data we need
// 3. The event is pushed to a thread-safe queue
// 4. In the main loop(), events are popped from the queue and processed
// 5. The event destructor cleans up any external allocations
//
// Thread safety:
// - GAP events: We copy only the fields we need directly into the union
// - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring
// the data remains valid even after the BLE callback returns. The original
// param pointer from ESP-IDF is only valid during the callback.
class BLEEvent { class BLEEvent {
public: public:
BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->event_.gap.gap_event = e;
memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t));
this->type_ = GAP;
};
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->event_.gattc.gattc_event = e;
this->event_.gattc.gattc_if = i;
memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t));
// Need to also make a copy of relevant event data.
switch (e) {
case ESP_GATTC_NOTIFY_EVT:
this->data.assign(p->notify.value, p->notify.value + p->notify.value_len);
this->event_.gattc.gattc_param.notify.value = this->data.data();
break;
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT:
this->data.assign(p->read.value, p->read.value + p->read.value_len);
this->event_.gattc.gattc_param.read.value = this->data.data();
break;
default:
break;
}
this->type_ = GATTC;
};
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->event_.gatts.gatts_event = e;
this->event_.gatts.gatts_if = i;
memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t));
// Need to also make a copy of relevant event data.
switch (e) {
case ESP_GATTS_WRITE_EVT:
this->data.assign(p->write.value, p->write.value + p->write.len);
this->event_.gatts.gatts_param.write.value = this->data.data();
break;
default:
break;
}
this->type_ = GATTS;
};
union {
// NOLINTNEXTLINE(readability-identifier-naming)
struct gap_event {
esp_gap_ble_cb_event_t gap_event;
esp_ble_gap_cb_param_t gap_param;
} gap;
// NOLINTNEXTLINE(readability-identifier-naming)
struct gattc_event {
esp_gattc_cb_event_t gattc_event;
esp_gatt_if_t gattc_if;
esp_ble_gattc_cb_param_t gattc_param;
} gattc;
// NOLINTNEXTLINE(readability-identifier-naming)
struct gatts_event {
esp_gatts_cb_event_t gatts_event;
esp_gatt_if_t gatts_if;
esp_ble_gatts_cb_param_t gatts_param;
} gatts;
} event_;
std::vector<uint8_t> data{};
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)
enum ble_event_t : uint8_t { enum ble_event_t : uint8_t {
GAP, GAP,
GATTC, GATTC,
GATTS, GATTS,
} type_; };
// Constructor for GAP events - no external allocations needed
BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->type_ = GAP;
this->event_.gap.gap_event = e;
if (p == nullptr) {
return; // Invalid event, but we can't log in header file
}
// Only copy the data we actually use for each GAP event type
switch (e) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
// Copy only the fields we use from scan results
memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t));
this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type;
this->event_.gap.scan_result.rssi = p->scan_rst.rssi;
this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len;
this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len;
this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt;
memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv,
ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX);
break;
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_param_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_start_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status;
break;
default:
// We only handle 4 GAP event types, others are dropped
break;
}
}
// Constructor for GATTC events - uses heap allocation
// Creates a copy of the param struct since the original is only valid during the callback
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->type_ = GATTC;
this->event_.gattc.gattc_event = e;
this->event_.gattc.gattc_if = i;
if (p == nullptr) {
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
return; // Invalid event, but we can't log in header file
}
// Heap-allocate param and data
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
// while GAP events (99%) are stored inline to minimize memory usage
this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
// Copy data for events that need it
switch (e) {
case ESP_GATTC_NOTIFY_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len);
this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data();
break;
case ESP_GATTC_READ_CHAR_EVT:
case ESP_GATTC_READ_DESCR_EVT:
this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len);
this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data();
break;
default:
this->event_.gattc.data = nullptr;
break;
}
}
// Constructor for GATTS events - uses heap allocation
// Creates a copy of the param struct since the original is only valid during the callback
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->type_ = GATTS;
this->event_.gatts.gatts_event = e;
this->event_.gatts.gatts_if = i;
if (p == nullptr) {
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
return; // Invalid event, but we can't log in header file
}
// Heap-allocate param and data
// Heap allocation is used because GATTC/GATTS events are rare (<1% of events)
// while GAP events (99%) are stored inline to minimize memory usage
this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
// Copy data for events that need it
switch (e) {
case ESP_GATTS_WRITE_EVT:
this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len);
this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data();
break;
default:
this->event_.gatts.data = nullptr;
break;
}
}
// Destructor to clean up heap allocations
~BLEEvent() {
switch (this->type_) {
case GATTC:
delete this->event_.gattc.gattc_param;
delete this->event_.gattc.data;
break;
case GATTS:
delete this->event_.gatts.gatts_param;
delete this->event_.gatts.data;
break;
default:
break;
}
}
// Disable copy to prevent double-delete
BLEEvent(const BLEEvent &) = delete;
BLEEvent &operator=(const BLEEvent &) = delete;
union {
// NOLINTNEXTLINE(readability-identifier-naming)
struct gap_event {
esp_gap_ble_cb_event_t gap_event;
union {
BLEScanResult scan_result; // 73 bytes
// This matches ESP-IDF's scan complete event structures
// All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout
struct {
esp_bt_status_t status;
} scan_complete; // 1 byte
};
} gap; // 80 bytes total
// NOLINTNEXTLINE(readability-identifier-naming)
struct gattc_event {
esp_gattc_cb_event_t gattc_event;
esp_gatt_if_t gattc_if;
esp_ble_gattc_cb_param_t *gattc_param; // Heap-allocated
std::vector<uint8_t> *data; // Heap-allocated
} gattc; // 16 bytes (pointers only)
// NOLINTNEXTLINE(readability-identifier-naming)
struct gatts_event {
esp_gatts_cb_event_t gatts_event;
esp_gatt_if_t gatts_if;
esp_ble_gatts_cb_param_t *gatts_param; // Heap-allocated
std::vector<uint8_t> *data; // Heap-allocated
} gatts; // 16 bytes (pointers only)
} event_; // 80 bytes
ble_event_t type_;
// Helper methods to access event data
ble_event_t type() const { return type_; }
esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; }
const BLEScanResult &scan_result() const { return event_.gap.scan_result; }
esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; }
}; };
// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)
} // namespace esp32_ble } // namespace esp32_ble
} // namespace esphome } // namespace esphome

View File

@ -0,0 +1,24 @@
#pragma once
#ifdef USE_ESP32
#include <esp_gap_ble_api.h>
namespace esphome {
namespace esp32_ble {
// Structure for BLE scan results - only fields we actually use
struct __attribute__((packed)) BLEScanResult {
esp_bd_addr_t bda;
uint8_t ble_addr_type;
int8_t rssi;
uint8_t ble_adv[ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX];
uint8_t adv_data_len;
uint8_t scan_rsp_len;
uint8_t search_evt;
}; // ~73 bytes vs ~400 bytes for full esp_ble_gap_cb_param_t
} // namespace esp32_ble
} // namespace esphome
#endif

View File

@ -2,52 +2,78 @@
#ifdef USE_ESP32 #ifdef USE_ESP32
#include <mutex> #include <atomic>
#include <queue> #include <cstddef>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
/* /*
* BLE events come in from a separate Task (thread) in the ESP32 stack. Rather * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather
* than trying to deal with various locking strategies, all incoming GAP and GATT * than using mutex-based locking, this lock-free queue allows the BLE
* events will simply be placed on a semaphore guarded queue. The next time the * task to enqueue events without blocking. The main loop() then processes
* component runs loop(), these events are popped off the queue and handed at * these events at a safer time.
* this safer time. *
* This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer.
* The BLE task is the only producer, and the main loop() is the only consumer.
*/ */
namespace esphome { namespace esphome {
namespace esp32_ble { namespace esp32_ble {
template<class T> class Queue { template<class T, size_t SIZE> class LockFreeQueue {
public: public:
Queue() { m_ = xSemaphoreCreateMutex(); } LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
void push(T *element) { bool push(T *element) {
if (element == nullptr) if (element == nullptr)
return; return false;
// It is not called from main loop. Thus it won't block main thread.
xSemaphoreTake(m_, portMAX_DELAY); size_t current_tail = tail_.load(std::memory_order_relaxed);
q_.push(element); size_t next_tail = (current_tail + 1) % SIZE;
xSemaphoreGive(m_);
if (next_tail == head_.load(std::memory_order_acquire)) {
// Buffer full
dropped_count_.fetch_add(1, std::memory_order_relaxed);
return false;
}
buffer_[current_tail] = element;
tail_.store(next_tail, std::memory_order_release);
return true;
} }
T *pop() { T *pop() {
T *element = nullptr; size_t current_head = head_.load(std::memory_order_relaxed);
if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { if (current_head == tail_.load(std::memory_order_acquire)) {
if (!q_.empty()) { return nullptr; // Empty
element = q_.front();
q_.pop();
}
xSemaphoreGive(m_);
} }
T *element = buffer_[current_head];
head_.store((current_head + 1) % SIZE, std::memory_order_release);
return element; return element;
} }
size_t size() const {
size_t tail = tail_.load(std::memory_order_acquire);
size_t head = head_.load(std::memory_order_acquire);
return (tail - head + SIZE) % SIZE;
}
size_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); }
void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); }
bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); }
bool full() const {
size_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
return next_tail == head_.load(std::memory_order_acquire);
}
protected: protected:
std::queue<T *> q_; T *buffer_[SIZE];
SemaphoreHandle_t m_; std::atomic<size_t> head_;
std::atomic<size_t> tail_;
std::atomic<size_t> dropped_count_;
}; };
} // namespace esp32_ble } // namespace esp32_ble

View File

@ -268,6 +268,7 @@ async def to_code(config):
parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID])
cg.add(parent.register_gap_event_handler(var)) cg.add(parent.register_gap_event_handler(var))
cg.add(parent.register_gap_scan_event_handler(var))
cg.add(parent.register_gattc_event_handler(var)) cg.add(parent.register_gattc_event_handler(var))
cg.add(parent.register_ble_status_event_handler(var)) cg.add(parent.register_ble_status_event_handler(var))
cg.add(var.set_parent(parent)) cg.add(var.set_parent(parent))

View File

@ -50,17 +50,15 @@ void ESP32BLETracker::setup() {
ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE"); ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
return; return;
} }
ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param> allocator( RAMAllocator<BLEScanResult> allocator;
ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param>::ALLOW_FAILURE); this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
this->scan_result_buffer_ = allocator.allocate(ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE);
if (this->scan_result_buffer_ == nullptr) { if (this->scan_ring_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!"); ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
this->mark_failed(); this->mark_failed();
} }
global_esp32_ble_tracker = this; global_esp32_ble_tracker = this;
this->scan_result_lock_ = xSemaphoreCreateMutex();
#ifdef USE_OTA #ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback( ota::get_global_ota_callback()->add_on_state_callback(
@ -120,27 +118,31 @@ void ESP32BLETracker::loop() {
} }
bool promote_to_connecting = discovered && !searching && !connecting; bool promote_to_connecting = discovered && !searching && !connecting;
if (this->scanner_state_ == ScannerState::RUNNING && // Process scan results from lock-free SPSC ring buffer
this->scan_result_index_ && // if it looks like we have a scan result we will take the lock // Consumer side: This runs in the main loop thread
xSemaphoreTake(this->scan_result_lock_, 0)) { if (this->scanner_state_ == ScannerState::RUNNING) {
uint32_t index = this->scan_result_index_; // Load our own index with relaxed ordering (we're the only writer)
if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { size_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
}
if (this->raw_advertisements_) { // Load producer's index with acquire to see their latest writes
for (auto *listener : this->listeners_) { size_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
}
for (auto *client : this->clients_) {
client->parse_devices(this->scan_result_buffer_, this->scan_result_index_);
}
}
if (this->parse_advertisements_) { while (read_idx != write_idx) {
for (size_t i = 0; i < index; i++) { // Process one result at a time directly from ring buffer
BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx];
if (this->raw_advertisements_) {
for (auto *listener : this->listeners_) {
listener->parse_devices(&scan_result, 1);
}
for (auto *client : this->clients_) {
client->parse_devices(&scan_result, 1);
}
}
if (this->parse_advertisements_) {
ESPBTDevice device; ESPBTDevice device;
device.parse_scan_rst(this->scan_result_buffer_[i]); device.parse_scan_rst(scan_result);
bool found = false; bool found = false;
for (auto *listener : this->listeners_) { for (auto *listener : this->listeners_) {
@ -161,9 +163,19 @@ void ESP32BLETracker::loop() {
this->print_bt_device_info(device); this->print_bt_device_info(device);
} }
} }
// Move to next entry in ring buffer
read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
// Store with release to ensure reads complete before index update
this->ring_read_index_.store(read_idx, std::memory_order_release);
}
// Log dropped results periodically
size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed);
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped);
} }
this->scan_result_index_ = 0;
xSemaphoreGive(this->scan_result_lock_);
} }
if (this->scanner_state_ == ScannerState::STOPPED) { if (this->scanner_state_ == ScannerState::STOPPED) {
this->end_of_scan_(); // Change state to IDLE this->end_of_scan_(); // Change state to IDLE
@ -370,9 +382,6 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
switch (event) { switch (event) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
this->gap_scan_result_(param->scan_rst);
break;
case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
this->gap_scan_set_param_complete_(param->scan_param_cmpl); this->gap_scan_set_param_complete_(param->scan_param_cmpl);
break; break;
@ -385,11 +394,57 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
default: default:
break; break;
} }
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
client->gap_event_handler(event, param); client->gap_event_handler(event, param);
} }
} }
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
// Lock-free SPSC ring buffer write (Producer side)
// This runs in the ESP-IDF Bluetooth stack callback thread
// IMPORTANT: Only this thread writes to ring_write_index_
// Load our own index with relaxed ordering (we're the only writer)
size_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed);
size_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
// Load consumer's index with acquire to see their latest updates
size_t read_idx = this->ring_read_index_.load(std::memory_order_acquire);
// Check if buffer is full
if (next_write_idx != read_idx) {
// Write to ring buffer
this->scan_ring_buffer_[write_idx] = scan_result;
// Store with release to ensure the write is visible before index update
this->ring_write_index_.store(next_write_idx, std::memory_order_release);
} else {
// Buffer full, track dropped results
this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed);
}
} else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
// Scan finished on its own
if (this->scanner_state_ != ScannerState::RUNNING) {
if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan was not running when scan completed.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan was not started when scan completed.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when scan completed.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when scan completed.");
}
}
this->set_scanner_state_(ScannerState::STOPPED);
}
}
void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) { void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param &param) {
ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status); ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
if (param.status == ESP_BT_STATUS_DONE) { if (param.status == ESP_BT_STATUS_DONE) {
@ -444,34 +499,6 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
this->set_scanner_state_(ScannerState::STOPPED); this->set_scanner_state_(ScannerState::STOPPED);
} }
void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt);
if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
if (xSemaphoreTake(this->scan_result_lock_, 0)) {
if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
this->scan_result_buffer_[this->scan_result_index_++] = param;
}
xSemaphoreGive(this->scan_result_lock_);
}
} else if (param.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
// Scan finished on its own
if (this->scanner_state_ != ScannerState::RUNNING) {
if (this->scanner_state_ == ScannerState::STOPPING) {
ESP_LOGE(TAG, "Scan was not running when scan completed.");
} else if (this->scanner_state_ == ScannerState::STARTING) {
ESP_LOGE(TAG, "Scan was not started when scan completed.");
} else if (this->scanner_state_ == ScannerState::FAILED) {
ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
} else if (this->scanner_state_ == ScannerState::IDLE) {
ESP_LOGE(TAG, "Scan was idle when scan completed.");
} else if (this->scanner_state_ == ScannerState::STOPPED) {
ESP_LOGE(TAG, "Scan was stopped when scan completed.");
}
}
this->set_scanner_state_(ScannerState::STOPPED);
}
}
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) { esp_ble_gattc_cb_param_t *param) {
for (auto *client : this->clients_) { for (auto *client : this->clients_) {
@ -494,13 +521,15 @@ optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData
return ESPBLEiBeacon(data.data.data()); return ESPBLEiBeacon(data.data.data());
} }
void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) { void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
this->scan_result_ = param;
for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++)
this->address_[i] = param.bda[i]; this->address_[i] = scan_result.bda[i];
this->address_type_ = param.ble_addr_type; this->address_type_ = static_cast<esp_ble_addr_type_t>(scan_result.ble_addr_type);
this->rssi_ = param.rssi; this->rssi_ = scan_result.rssi;
this->parse_adv_(param);
// Parse advertisement data directly
uint8_t total_len = scan_result.adv_data_len + scan_result.scan_rsp_len;
this->parse_adv_(scan_result.ble_adv, total_len);
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
ESP_LOGVV(TAG, "Parse Result:"); ESP_LOGVV(TAG, "Parse Result:");
@ -558,13 +587,13 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e
ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str()); ESP_LOGVV(TAG, " Data: %s", format_hex_pretty(data.data).c_str());
} }
ESP_LOGVV(TAG, " Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); ESP_LOGVV(TAG, " Adv data: %s",
format_hex_pretty(scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len).c_str());
#endif #endif
} }
void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) {
size_t offset = 0; size_t offset = 0;
const uint8_t *payload = param.ble_adv;
uint8_t len = param.adv_data_len + param.scan_rsp_len;
while (offset + 2 < len) { while (offset + 2 < len) {
const uint8_t field_length = payload[offset++]; // First byte is length of adv record const uint8_t field_length = payload[offset++]; // First byte is length of adv record

View File

@ -6,6 +6,7 @@
#include "esphome/core/helpers.h" #include "esphome/core/helpers.h"
#include <array> #include <array>
#include <atomic>
#include <string> #include <string>
#include <vector> #include <vector>
@ -62,7 +63,7 @@ class ESPBLEiBeacon {
class ESPBTDevice { class ESPBTDevice {
public: public:
void parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param); void parse_scan_rst(const BLEScanResult &scan_result);
std::string address_str() const; std::string address_str() const;
@ -84,8 +85,6 @@ class ESPBTDevice {
const std::vector<ServiceData> &get_service_datas() const { return service_datas_; } const std::vector<ServiceData> &get_service_datas() const { return service_datas_; }
const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &get_scan_result() const { return scan_result_; }
bool resolve_irk(const uint8_t *irk) const; bool resolve_irk(const uint8_t *irk) const;
optional<ESPBLEiBeacon> get_ibeacon() const { optional<ESPBLEiBeacon> get_ibeacon() const {
@ -98,7 +97,7 @@ class ESPBTDevice {
} }
protected: protected:
void parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param); void parse_adv_(const uint8_t *payload, uint8_t len);
esp_bd_addr_t address_{ esp_bd_addr_t address_{
0, 0,
@ -112,7 +111,6 @@ class ESPBTDevice {
std::vector<ESPBTUUID> service_uuids_{}; std::vector<ESPBTUUID> service_uuids_{};
std::vector<ServiceData> manufacturer_datas_{}; std::vector<ServiceData> manufacturer_datas_{};
std::vector<ServiceData> service_datas_{}; std::vector<ServiceData> service_datas_{};
esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{};
}; };
class ESP32BLETracker; class ESP32BLETracker;
@ -121,9 +119,7 @@ class ESPBTDeviceListener {
public: public:
virtual void on_scan_end() {} virtual void on_scan_end() {}
virtual bool parse_device(const ESPBTDevice &device) = 0; virtual bool parse_device(const ESPBTDevice &device) = 0;
virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; };
return false;
};
virtual AdvertisementParserType get_advertisement_parser_type() { virtual AdvertisementParserType get_advertisement_parser_type() {
return AdvertisementParserType::PARSED_ADVERTISEMENTS; return AdvertisementParserType::PARSED_ADVERTISEMENTS;
}; };
@ -210,6 +206,7 @@ class ESPBTClient : public ESPBTDeviceListener {
class ESP32BLETracker : public Component, class ESP32BLETracker : public Component,
public GAPEventHandler, public GAPEventHandler,
public GAPScanEventHandler,
public GATTcEventHandler, public GATTcEventHandler,
public BLEStatusEventHandler, public BLEStatusEventHandler,
public Parented<ESP32BLE> { public Parented<ESP32BLE> {
@ -240,6 +237,7 @@ class ESP32BLETracker : public Component,
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override; esp_ble_gattc_cb_param_t *param) override;
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override;
void gap_scan_event_handler(const BLEScanResult &scan_result) override;
void ble_before_disabled_event_handler() override; void ble_before_disabled_event_handler() override;
void add_scanner_state_callback(std::function<void(ScannerState)> &&callback) { void add_scanner_state_callback(std::function<void(ScannerState)> &&callback) {
@ -285,14 +283,16 @@ class ESP32BLETracker : public Component,
bool ble_was_disabled_{true}; bool ble_was_disabled_{true};
bool raw_advertisements_{false}; bool raw_advertisements_{false};
bool parse_advertisements_{false}; bool parse_advertisements_{false};
SemaphoreHandle_t scan_result_lock_;
size_t scan_result_index_{0}; // Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results
#ifdef USE_PSRAM // Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler)
const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; // Consumer: ESPHome main loop (loop() method)
#else // This design ensures zero blocking in the BT callback and prevents scan result loss
const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20; BLEScanResult *scan_ring_buffer_;
#endif // USE_PSRAM std::atomic<size_t> ring_write_index_{0}; // Written only by BT callback (producer)
esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; std::atomic<size_t> ring_read_index_{0}; // Written only by main loop (consumer)
std::atomic<size_t> scan_results_dropped_{0}; // Tracks buffer overflow events
esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
int connecting_{0}; int connecting_{0};

View File

@ -57,7 +57,7 @@ void ESP32Camera::dump_config() {
" External Clock: Pin:%d Frequency:%u\n" " External Clock: Pin:%d Frequency:%u\n"
" I2C Pins: SDA:%d SCL:%d\n" " I2C Pins: SDA:%d SCL:%d\n"
" Reset Pin: %d", " Reset Pin: %d",
this->name_.c_str(), YESNO(this->internal_), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3,
conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk, conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk,
conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset);
switch (this->config_.frame_size) { switch (this->config_.frame_size) {

View File

@ -41,39 +41,48 @@ void FanCall::perform() {
void FanCall::validate_() { void FanCall::validate_() {
auto traits = this->parent_.get_traits(); auto traits = this->parent_.get_traits();
if (this->speed_.has_value()) if (this->speed_.has_value()) {
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count()); this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
if (this->binary_state_.has_value() && *this->binary_state_) { // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// when turning on, if neither current nor new speed available, set speed to 100% // "Manually setting a speed must disable any set preset mode"
if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) { this->preset_mode_.clear();
this->speed_ = traits.supported_speed_count();
}
}
if (this->oscillating_.has_value() && !traits.supports_oscillation()) {
ESP_LOGW(TAG, "'%s' - This fan does not support oscillation!", this->parent_.get_name().c_str());
this->oscillating_.reset();
}
if (this->speed_.has_value() && !traits.supports_speed()) {
ESP_LOGW(TAG, "'%s' - This fan does not support speeds!", this->parent_.get_name().c_str());
this->speed_.reset();
}
if (this->direction_.has_value() && !traits.supports_direction()) {
ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str());
this->direction_.reset();
} }
if (!this->preset_mode_.empty()) { if (!this->preset_mode_.empty()) {
const auto &preset_modes = traits.supported_preset_modes(); const auto &preset_modes = traits.supported_preset_modes();
if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { if (preset_modes.find(this->preset_mode_) == preset_modes.end()) {
ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(), ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str());
this->preset_mode_.c_str());
this->preset_mode_.clear(); this->preset_mode_.clear();
} }
} }
// when turning on...
if (!this->parent_.state && this->binary_state_.has_value() &&
*this->binary_state_
// ..,and no preset mode will be active...
&& this->preset_mode_.empty() &&
this->parent_.preset_mode.empty()
// ...and neither current nor new speed is available...
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
// ...set speed to 100%
this->speed_ = traits.supported_speed_count();
}
if (this->oscillating_.has_value() && !traits.supports_oscillation()) {
ESP_LOGW(TAG, "%s: Oscillation not supported", this->parent_.get_name().c_str());
this->oscillating_.reset();
}
if (this->speed_.has_value() && !traits.supports_speed()) {
ESP_LOGW(TAG, "%s: Speed control not supported", this->parent_.get_name().c_str());
this->speed_.reset();
}
if (this->direction_.has_value() && !traits.supports_direction()) {
ESP_LOGW(TAG, "%s: Direction control not supported", this->parent_.get_name().c_str());
this->direction_.reset();
}
} }
FanCall FanRestoreState::to_call(Fan &fan) { FanCall FanRestoreState::to_call(Fan &fan) {

View File

@ -317,15 +317,18 @@ void I2SAudioMicrophone::stop_driver_() {
ESP_LOGW(TAG, "Error uninstalling I2S driver - it may not have started: %s", esp_err_to_name(err)); ESP_LOGW(TAG, "Error uninstalling I2S driver - it may not have started: %s", esp_err_to_name(err));
} }
#else #else
/* Have to stop the channel before deleting it */ if (this->rx_handle_ != nullptr) {
err = i2s_channel_disable(this->rx_handle_); /* Have to stop the channel before deleting it */
if (err != ESP_OK) { err = i2s_channel_disable(this->rx_handle_);
ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); if (err != ESP_OK) {
} ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err));
/* If the handle is not needed any more, delete it to release the channel resources */ }
err = i2s_del_channel(this->rx_handle_); /* If the handle is not needed any more, delete it to release the channel resources */
if (err != ESP_OK) { err = i2s_del_channel(this->rx_handle_);
ESP_LOGW(TAG, "Error deleting I2S channel - it may not have started: %s", esp_err_to_name(err)); if (err != ESP_OK) {
ESP_LOGW(TAG, "Error deleting I2S channel - it may not have started: %s", esp_err_to_name(err));
}
this->rx_handle_ = nullptr;
} }
#endif #endif
this->parent_->unlock(); this->parent_->unlock();

View File

@ -19,9 +19,8 @@ void KMeterISOComponent::setup() {
// Mark as not failed before initializing. Some devices will turn off sensors to save on batteries // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries
// and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component.
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { if (this->is_failed()) {
this->component_state_ &= ~COMPONENT_STATE_MASK; this->reset_to_construction_state();
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
} }
auto err = this->bus_->writev(this->address_, nullptr, 0); auto err = this->bus_->writev(this->address_, nullptr, 0);

View File

@ -116,7 +116,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
if (this->baud_rate_ > 0) { if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_ + msg_start); this->write_msg_(this->tx_buffer_ + msg_start);
} }
this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start); this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start);
global_recursion_guard_ = false; global_recursion_guard_ = false;
} }
@ -129,19 +129,6 @@ inline int Logger::level_for(const char *tag) {
return this->current_level_; return this->current_level_;
} }
void HOT Logger::call_log_callbacks_(int level, const char *tag, const char *msg) {
#ifdef USE_ESP32
// Suppress network-logging if memory constrained
// In some configurations (eg BLE enabled) there may be some transient
// memory exhaustion, and trying to log when OOM can lead to a crash. Skipping
// here usually allows the stack to recover instead.
// See issue #1234 for analysis.
if (xPortGetFreeHeapSize() < 2048)
return;
#endif
this->log_callback_.call(level, tag, msg);
}
Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) {
// add 1 to buffer size for null terminator // add 1 to buffer size for null terminator
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
@ -189,7 +176,7 @@ void Logger::loop() {
this->tx_buffer_size_); this->tx_buffer_size_);
this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_);
this->tx_buffer_[this->tx_buffer_at_] = '\0'; this->tx_buffer_[this->tx_buffer_at_] = '\0';
this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_); this->log_callback_.call(message->level, message->tag, this->tx_buffer_);
// At this point all the data we need from message has been transferred to the tx_buffer // At this point all the data we need from message has been transferred to the tx_buffer
// so we can release the message to allow other tasks to use it as soon as possible. // so we can release the message to allow other tasks to use it as soon as possible.
this->log_buffer_->release_message_main_loop(received_token); this->log_buffer_->release_message_main_loop(received_token);

View File

@ -156,7 +156,6 @@ class Logger : public Component {
#endif #endif
protected: protected:
void call_log_callbacks_(int level, const char *tag, const char *msg);
void write_msg_(const char *msg); void write_msg_(const char *msg);
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
@ -191,7 +190,7 @@ class Logger : public Component {
if (this->baud_rate_ > 0) { if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console
} }
this->call_log_callbacks_(level, tag, this->tx_buffer_); this->log_callback_.call(level, tag, this->tx_buffer_);
} }
// Write the body of the log message to the buffer // Write the body of the log message to the buffer

View File

@ -153,7 +153,7 @@ bool MQTTComponent::send_discovery_() {
if (node_friendly_name.empty()) { if (node_friendly_name.empty()) {
node_friendly_name = node_name; node_friendly_name = node_name;
} }
const std::string &node_area = App.get_area(); std::string node_area = App.get_area();
JsonObject device_info = root.createNestedObject(MQTT_DEVICE); JsonObject device_info = root.createNestedObject(MQTT_DEVICE);
const auto mac = get_mac_address(); const auto mac = get_mac_address();

View File

@ -56,7 +56,7 @@ void NextionBinarySensor::set_state(bool state, bool publish, bool send_to_nexti
this->publish_state(state); this->publish_state(state);
} else { } else {
this->state = state; this->state = state;
this->has_state_ = true; this->set_has_state(true);
} }
this->update_component_settings(); this->update_component_settings();

View File

@ -337,23 +337,26 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
bool Nextion::upload_end_(bool successful) { bool Nextion::upload_end_(bool successful) {
ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful));
this->is_updating_ = false;
this->ignore_is_setup_ = false;
uint32_t baud_rate = this->parent_->get_baud_rate();
if (baud_rate != this->original_baud_rate_) {
ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_);
this->parent_->set_baud_rate(this->original_baud_rate_);
this->parent_->load_settings();
}
if (successful) { if (successful) {
ESP_LOGD(TAG, "Restart"); ESP_LOGD(TAG, "Restart");
delay(1500); // NOLINT delay(1500); // NOLINT
App.safe_reboot(); App.safe_reboot();
delay(1500); // NOLINT
} else { } else {
ESP_LOGE(TAG, "TFT upload failed"); ESP_LOGE(TAG, "TFT upload failed");
this->is_updating_ = false;
this->ignore_is_setup_ = false;
uint32_t baud_rate = this->parent_->get_baud_rate();
if (baud_rate != this->original_baud_rate_) {
ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_);
this->parent_->set_baud_rate(this->original_baud_rate_);
this->parent_->load_settings();
}
} }
return successful; return successful;
} }

View File

@ -337,15 +337,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
bool Nextion::upload_end_(bool successful) { bool Nextion::upload_end_(bool successful) {
ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful)); ESP_LOGD(TAG, "TFT upload done: %s", YESNO(successful));
this->is_updating_ = false;
this->ignore_is_setup_ = false;
uint32_t baud_rate = this->parent_->get_baud_rate();
if (baud_rate != this->original_baud_rate_) {
ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_);
this->parent_->set_baud_rate(this->original_baud_rate_);
this->parent_->load_settings();
}
if (successful) { if (successful) {
ESP_LOGD(TAG, "Restart"); ESP_LOGD(TAG, "Restart");
@ -353,7 +344,18 @@ bool Nextion::upload_end_(bool successful) {
App.safe_reboot(); App.safe_reboot();
} else { } else {
ESP_LOGE(TAG, "TFT upload failed"); ESP_LOGE(TAG, "TFT upload failed");
this->is_updating_ = false;
this->ignore_is_setup_ = false;
uint32_t baud_rate = this->parent_->get_baud_rate();
if (baud_rate != this->original_baud_rate_) {
ESP_LOGD(TAG, "Baud back: %" PRIu32 "->%" PRIu32, baud_rate, this->original_baud_rate_);
this->parent_->set_baud_rate(this->original_baud_rate_);
this->parent_->load_settings();
}
} }
return successful; return successful;
} }

View File

@ -88,7 +88,7 @@ void NextionSensor::set_state(float state, bool publish, bool send_to_nextion) {
} else { } else {
this->raw_state = state; this->raw_state = state;
this->state = state; this->state = state;
this->has_state_ = true; this->set_has_state(true);
} }
} }
this->update_component_settings(); this->update_component_settings();

View File

@ -37,7 +37,7 @@ void NextionTextSensor::set_state(const std::string &state, bool publish, bool s
this->publish_state(state); this->publish_state(state);
} else { } else {
this->state = state; this->state = state;
this->has_state_ = true; this->set_has_state(true);
} }
this->update_component_settings(); this->update_component_settings();

View File

@ -7,7 +7,7 @@ namespace number {
static const char *const TAG = "number"; static const char *const TAG = "number";
void Number::publish_state(float state) { void Number::publish_state(float state) {
this->has_state_ = true; this->set_has_state(true);
this->state = state; this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state); ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state);
this->state_callback_.call(state); this->state_callback_.call(state);

View File

@ -48,9 +48,6 @@ class Number : public EntityBase {
NumberTraits traits; NumberTraits traits;
/// Return whether this number has gotten a full state yet.
bool has_state() const { return has_state_; }
protected: protected:
friend class NumberCall; friend class NumberCall;
@ -63,7 +60,6 @@ class Number : public EntityBase {
virtual void control(float value) = 0; virtual void control(float value) = 0;
CallbackManager<void(float)> state_callback_; CallbackManager<void(float)> state_callback_;
bool has_state_{false};
}; };
} // namespace number } // namespace number

View File

@ -31,7 +31,6 @@ CONFIG_SCHEMA = cv.Schema(
} }
), ),
}, },
cv.only_with_arduino,
).extend(cv.COMPONENT_SCHEMA) ).extend(cv.COMPONENT_SCHEMA)

View File

@ -10,7 +10,7 @@ void Select::publish_state(const std::string &state) {
auto index = this->index_of(state); auto index = this->index_of(state);
const auto *name = this->get_name().c_str(); const auto *name = this->get_name().c_str();
if (index.has_value()) { if (index.has_value()) {
this->has_state_ = true; this->set_has_state(true);
this->state = state; this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value());
this->state_callback_.call(state, index.value()); this->state_callback_.call(state, index.value());

View File

@ -35,9 +35,6 @@ class Select : public EntityBase {
void publish_state(const std::string &state); void publish_state(const std::string &state);
/// Return whether this select component has gotten a full state yet.
bool has_state() const { return has_state_; }
/// Instantiate a SelectCall object to modify this select component's state. /// Instantiate a SelectCall object to modify this select component's state.
SelectCall make_call() { return SelectCall(this); } SelectCall make_call() { return SelectCall(this); }
@ -73,7 +70,6 @@ class Select : public EntityBase {
virtual void control(const std::string &value) = 0; virtual void control(const std::string &value) = 0;
CallbackManager<void(std::string, size_t)> state_callback_; CallbackManager<void(std::string, size_t)> state_callback_;
bool has_state_{false};
}; };
} // namespace select } // namespace select

View File

@ -38,7 +38,9 @@ StateClass Sensor::get_state_class() {
void Sensor::publish_state(float state) { void Sensor::publish_state(float state) {
this->raw_state = state; this->raw_state = state;
this->raw_callback_.call(state); if (this->raw_callback_) {
this->raw_callback_->call(state);
}
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state); ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
@ -51,7 +53,10 @@ void Sensor::publish_state(float state) {
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) { this->callback_.add(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) { void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
this->raw_callback_.add(std::move(callback)); 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) { void Sensor::add_filter(Filter *filter) {
@ -88,13 +93,12 @@ float Sensor::get_raw_state() const { return this->raw_state; }
std::string Sensor::unique_id() { return ""; } std::string Sensor::unique_id() { return ""; }
void Sensor::internal_send_state_to_frontend(float state) { void Sensor::internal_send_state_to_frontend(float state) {
this->has_state_ = true; this->set_has_state(true);
this->state = state; this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), 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().c_str(), this->get_accuracy_decimals()); this->get_unit_of_measurement().c_str(), this->get_accuracy_decimals());
this->callback_.call(state); this->callback_.call(state);
} }
bool Sensor::has_state() const { return this->has_state_; }
} // namespace sensor } // namespace sensor
} // namespace esphome } // namespace esphome

View File

@ -7,6 +7,7 @@
#include "esphome/components/sensor/filter.h" #include "esphome/components/sensor/filter.h"
#include <vector> #include <vector>
#include <memory>
namespace esphome { namespace esphome {
namespace sensor { namespace sensor {
@ -140,9 +141,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
*/ */
float raw_state; float raw_state;
/// Return whether this sensor has gotten a full state (that passed through all filters) yet.
bool has_state() const;
/** Override this method to set the unique ID of this sensor. /** Override this method to set the unique ID of this sensor.
* *
* @deprecated Do not use for new sensors, a suitable unique ID is automatically generated (2023.4). * @deprecated Do not use for new sensors, a suitable unique ID is automatically generated (2023.4).
@ -152,15 +150,14 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
void internal_send_state_to_frontend(float state); void internal_send_state_to_frontend(float state);
protected: protected:
CallbackManager<void(float)> raw_callback_; ///< Storage for raw state 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. CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters. Filter *filter_list_{nullptr}; ///< Store all active filters.
optional<int8_t> accuracy_decimals_; ///< Accuracy in decimals override optional<int8_t> accuracy_decimals_; ///< Accuracy in decimals override
optional<StateClass> state_class_{STATE_CLASS_NONE}; ///< State class override optional<StateClass> state_class_{STATE_CLASS_NONE}; ///< State class override
bool force_update_{false}; ///< Force update mode bool force_update_{false}; ///< Force update mode
bool has_state_{false};
}; };
} // namespace sensor } // namespace sensor

View File

@ -9,10 +9,10 @@ namespace status_led {
static const char *const TAG = "status_led"; static const char *const TAG = "status_led";
void StatusLEDLightOutput::loop() { void StatusLEDLightOutput::loop() {
uint32_t new_state = App.get_app_state() & STATUS_LED_MASK; uint8_t new_state = App.get_app_state() & STATUS_LED_MASK;
if (new_state != this->last_app_state_) { if (new_state != this->last_app_state_) {
ESP_LOGV(TAG, "New app state 0x%08" PRIX32, new_state); ESP_LOGV(TAG, "New app state 0x%02X", new_state);
} }
if ((new_state & STATUS_LED_ERROR) != 0u) { if ((new_state & STATUS_LED_ERROR) != 0u) {

View File

@ -36,7 +36,7 @@ class StatusLEDLightOutput : public light::LightOutput, public Component {
GPIOPin *pin_{nullptr}; GPIOPin *pin_{nullptr};
output::BinaryOutput *output_{nullptr}; output::BinaryOutput *output_{nullptr};
light::LightState *lightstate_{}; light::LightState *lightstate_{};
uint32_t last_app_state_{0xFFFF}; uint8_t last_app_state_{0xFF};
void output_state_(bool state); void output_state_(bool state);
}; };

View File

@ -110,15 +110,7 @@ void TemplateAlarmControlPanel::loop() {
delay = this->arming_night_time_; delay = this->arming_night_time_;
} }
if ((millis() - this->last_update_) > delay) { if ((millis() - this->last_update_) > delay) {
#ifdef USE_BINARY_SENSOR this->bypass_before_arming();
for (auto sensor_info : this->sensor_map_) {
// Check for sensors left on and set to bypass automatically and remove them from monitoring
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) {
ESP_LOGW(TAG, "%s is left on and will be automatically bypassed", sensor_info.first->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index);
}
}
#endif
this->publish_state(this->desired_state_); this->publish_state(this->desired_state_);
} }
return; return;
@ -259,10 +251,23 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p
if (delay > 0) { if (delay > 0) {
this->publish_state(ACP_STATE_ARMING); this->publish_state(ACP_STATE_ARMING);
} else { } else {
this->bypass_before_arming();
this->publish_state(state); this->publish_state(state);
} }
} }
void TemplateAlarmControlPanel::bypass_before_arming() {
#ifdef USE_BINARY_SENSOR
for (auto sensor_info : this->sensor_map_) {
// Check for sensors left on and set to bypass automatically and remove them from monitoring
if ((sensor_info.second.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor_info.first->state)) {
ESP_LOGW(TAG, "'%s' is left on and will be automatically bypassed", sensor_info.first->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(sensor_info.second.store_index);
}
}
#endif
}
void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) { void TemplateAlarmControlPanel::control(const AlarmControlPanelCall &call) {
if (call.get_state()) { if (call.get_state()) {
if (call.get_state() == ACP_STATE_ARMED_AWAY) { if (call.get_state() == ACP_STATE_ARMED_AWAY) {

View File

@ -60,6 +60,7 @@ class TemplateAlarmControlPanel : public alarm_control_panel::AlarmControlPanel,
bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; } bool get_requires_code_to_arm() const override { return this->requires_code_to_arm_; }
bool get_all_sensors_ready() { return this->sensors_ready_; }; bool get_all_sensors_ready() { return this->sensors_ready_; };
void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; } void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
void bypass_before_arming();
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
/** Add a binary_sensor to the alarm_panel. /** Add a binary_sensor to the alarm_panel.

View File

@ -7,7 +7,7 @@ namespace text {
static const char *const TAG = "text"; static const char *const TAG = "text";
void Text::publish_state(const std::string &state) { void Text::publish_state(const std::string &state) {
this->has_state_ = true; this->set_has_state(true);
this->state = state; this->state = state;
if (this->traits.get_mode() == TEXT_MODE_PASSWORD) { if (this->traits.get_mode() == TEXT_MODE_PASSWORD) {
ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), state.c_str()); ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), state.c_str());

View File

@ -28,9 +28,6 @@ class Text : public EntityBase {
void publish_state(const std::string &state); void publish_state(const std::string &state);
/// Return whether this text input has gotten a full state yet.
bool has_state() const { return has_state_; }
/// Instantiate a TextCall object to modify this text component's state. /// Instantiate a TextCall object to modify this text component's state.
TextCall make_call() { return TextCall(this); } TextCall make_call() { return TextCall(this); }
@ -48,7 +45,6 @@ class Text : public EntityBase {
virtual void control(const std::string &value) = 0; virtual void control(const std::string &value) = 0;
CallbackManager<void(std::string)> state_callback_; CallbackManager<void(std::string)> state_callback_;
bool has_state_{false};
}; };
} // namespace text } // namespace text

View File

@ -8,7 +8,9 @@ static const char *const TAG = "text_sensor";
void TextSensor::publish_state(const std::string &state) { void TextSensor::publish_state(const std::string &state) {
this->raw_state = state; this->raw_state = state;
this->raw_callback_.call(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()); ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str());
@ -53,20 +55,22 @@ void TextSensor::add_on_state_callback(std::function<void(std::string)> callback
this->callback_.add(std::move(callback)); this->callback_.add(std::move(callback));
} }
void TextSensor::add_on_raw_state_callback(std::function<void(std::string)> callback) { void TextSensor::add_on_raw_state_callback(std::function<void(std::string)> callback) {
this->raw_callback_.add(std::move(callback)); 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; } std::string TextSensor::get_state() const { return this->state; }
std::string TextSensor::get_raw_state() const { return this->raw_state; } std::string TextSensor::get_raw_state() const { return this->raw_state; }
void TextSensor::internal_send_state_to_frontend(const std::string &state) { void TextSensor::internal_send_state_to_frontend(const std::string &state) {
this->state = state; this->state = state;
this->has_state_ = true; this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str()); ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str());
this->callback_.call(state); this->callback_.call(state);
} }
std::string TextSensor::unique_id() { return ""; } std::string TextSensor::unique_id() { return ""; }
bool TextSensor::has_state() { return this->has_state_; }
} // namespace text_sensor } // namespace text_sensor
} // namespace esphome } // namespace esphome

View File

@ -6,6 +6,7 @@
#include "esphome/components/text_sensor/filter.h" #include "esphome/components/text_sensor/filter.h"
#include <vector> #include <vector>
#include <memory>
namespace esphome { namespace esphome {
namespace text_sensor { namespace text_sensor {
@ -33,6 +34,8 @@ namespace text_sensor {
class TextSensor : public EntityBase, public EntityBase_DeviceClass { class TextSensor : public EntityBase, public EntityBase_DeviceClass {
public: public:
TextSensor() = default;
/// Getter-syntax for .state. /// Getter-syntax for .state.
std::string get_state() const; std::string get_state() const;
/// Getter-syntax for .raw_state /// Getter-syntax for .raw_state
@ -67,17 +70,14 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
*/ */
virtual std::string unique_id(); virtual std::string unique_id();
bool has_state();
void internal_send_state_to_frontend(const std::string &state); void internal_send_state_to_frontend(const std::string &state);
protected: protected:
CallbackManager<void(std::string)> raw_callback_; ///< Storage for raw state callbacks. std::unique_ptr<CallbackManager<void(std::string)>>
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks. 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. Filter *filter_list_{nullptr}; ///< Store all active filters.
bool has_state_{false};
}; };
} // namespace text_sensor } // namespace text_sensor

View File

@ -30,7 +30,7 @@ void UpdateEntity::publish_state() {
ESP_LOGD(TAG, " Progress: %.0f%%", this->update_info_.progress); ESP_LOGD(TAG, " Progress: %.0f%%", this->update_info_.progress);
} }
this->has_state_ = true; this->set_has_state(true);
this->state_callback_.call(); this->state_callback_.call();
} }

View File

@ -28,8 +28,6 @@ enum UpdateState : uint8_t {
class UpdateEntity : public EntityBase, public EntityBase_DeviceClass { class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
public: public:
bool has_state() const { return this->has_state_; }
void publish_state(); void publish_state();
void perform() { this->perform(false); } void perform() { this->perform(false); }
@ -44,7 +42,6 @@ class UpdateEntity : public EntityBase, public EntityBase_DeviceClass {
protected: protected:
UpdateState state_{UPDATE_STATE_UNKNOWN}; UpdateState state_{UPDATE_STATE_UNKNOWN};
UpdateInfo update_info_; UpdateInfo update_info_;
bool has_state_{false};
CallbackManager<void()> state_callback_{}; CallbackManager<void()> state_callback_{};
}; };

View File

@ -13,7 +13,7 @@ static const char *const TAG = "uptime.sensor";
void UptimeTimestampSensor::setup() { void UptimeTimestampSensor::setup() {
this->time_->add_on_time_sync_callback([this]() { this->time_->add_on_time_sync_callback([this]() {
if (this->has_state_) if (this->has_state())
return; // No need to update the timestamp if it's already set return; // No need to update the timestamp if it's already set
auto now = this->time_->now(); auto now = this->time_->now();

View File

@ -8,8 +8,6 @@ CONFIG_SCHEMA = cv.All(
cv.only_with_esp_idf, cv.only_with_esp_idf,
) )
AUTO_LOAD = ["web_server"]
async def to_code(config): async def to_code(config):
# Increase the maximum supported size of headers section in HTTP request packet to be processed by the server # Increase the maximum supported size of headers section in HTTP request packet to be processed by the server

View File

@ -9,10 +9,12 @@
#include "utils.h" #include "utils.h"
#include "web_server_idf.h"
#ifdef USE_WEBSERVER
#include "esphome/components/web_server/web_server.h" #include "esphome/components/web_server/web_server.h"
#include "esphome/components/web_server/list_entities.h" #include "esphome/components/web_server/list_entities.h"
#endif // USE_WEBSERVER
#include "web_server_idf.h"
namespace esphome { namespace esphome {
namespace web_server_idf { namespace web_server_idf {
@ -273,6 +275,7 @@ void AsyncResponseStream::printf(const char *fmt, ...) {
this->print(str); this->print(str);
} }
#ifdef USE_WEBSERVER
AsyncEventSource::~AsyncEventSource() { AsyncEventSource::~AsyncEventSource() {
for (auto *ses : this->sessions_) { for (auto *ses : this->sessions_) {
delete ses; // NOLINT(cppcoreguidelines-owning-memory) delete ses; // NOLINT(cppcoreguidelines-owning-memory)
@ -511,6 +514,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
} }
} }
} }
#endif
} // namespace web_server_idf } // namespace web_server_idf
} // namespace esphome } // namespace esphome

View File

@ -1,6 +1,7 @@
#pragma once #pragma once
#ifdef USE_ESP_IDF #ifdef USE_ESP_IDF
#include "esphome/core/defines.h"
#include <esp_http_server.h> #include <esp_http_server.h>
#include <functional> #include <functional>
@ -12,10 +13,12 @@
#include <vector> #include <vector>
namespace esphome { namespace esphome {
#ifdef USE_WEBSERVER
namespace web_server { namespace web_server {
class WebServer; class WebServer;
class ListEntitiesIterator; class ListEntitiesIterator;
}; // namespace web_server }; // namespace web_server
#endif
namespace web_server_idf { namespace web_server_idf {
#define F(string_literal) (string_literal) #define F(string_literal) (string_literal)
@ -220,6 +223,7 @@ class AsyncWebHandler {
virtual bool isRequestHandlerTrivial() { return true; } virtual bool isRequestHandlerTrivial() { return true; }
}; };
#ifdef USE_WEBSERVER
class AsyncEventSource; class AsyncEventSource;
class AsyncEventSourceResponse; class AsyncEventSourceResponse;
@ -307,10 +311,13 @@ class AsyncEventSource : public AsyncWebHandler {
connect_handler_t on_connect_{}; connect_handler_t on_connect_{};
esphome::web_server::WebServer *web_server_; esphome::web_server::WebServer *web_server_;
}; };
#endif // USE_WEBSERVER
class DefaultHeaders { class DefaultHeaders {
friend class AsyncWebServerRequest; friend class AsyncWebServerRequest;
#ifdef USE_WEBSERVER
friend class AsyncEventSourceResponse; friend class AsyncEventSourceResponse;
#endif
public: public:
// NOLINTNEXTLINE(readability-identifier-naming) // NOLINTNEXTLINE(readability-identifier-naming)

View File

@ -102,7 +102,7 @@ WeikaiRegister &WeikaiRegister::operator|=(uint8_t value) {
// The WeikaiComponent methods // The WeikaiComponent methods
/////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////
void WeikaiComponent::loop() { void WeikaiComponent::loop() {
if ((this->component_state_ & COMPONENT_STATE_MASK) != COMPONENT_STATE_LOOP) if (!this->is_in_loop_state())
return; return;
// If there are some bytes in the receive FIFO we transfers them to the ring buffers // If there are some bytes in the receive FIFO we transfers them to the ring buffers

View File

@ -1,6 +1,6 @@
"""Constants used by esphome.""" """Constants used by esphome."""
__version__ = "2025.6.0b1" __version__ = "2025.6.0b2"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = ( VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@ -66,7 +66,7 @@ void Application::setup() {
[](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); }); [](Component *a, Component *b) { return a->get_loop_priority() > b->get_loop_priority(); });
do { do {
uint32_t new_app_state = STATUS_LED_WARNING; uint8_t new_app_state = STATUS_LED_WARNING;
this->scheduler.call(); this->scheduler.call();
this->feed_wdt(); this->feed_wdt();
for (uint32_t j = 0; j <= i; j++) { for (uint32_t j = 0; j <= i; j++) {
@ -87,7 +87,7 @@ void Application::setup() {
this->calculate_looping_components_(); this->calculate_looping_components_();
} }
void Application::loop() { void Application::loop() {
uint32_t new_app_state = 0; uint8_t new_app_state = 0;
this->scheduler.call(); this->scheduler.call();
@ -117,7 +117,9 @@ void Application::loop() {
// Use the last component's end time instead of calling millis() again // Use the last component's end time instead of calling millis() again
auto elapsed = last_op_end_time - this->last_loop_; auto elapsed = last_op_end_time - this->last_loop_;
if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) { if (elapsed >= this->loop_interval_ || HighFrequencyLoopRequester::is_high_frequency()) {
yield(); // Even if we overran the loop interval, we still need to select()
// to know if any sockets have data ready
this->yield_with_select_(0);
} else { } else {
uint32_t delay_time = this->loop_interval_ - elapsed; uint32_t delay_time = this->loop_interval_ - elapsed;
uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time); uint32_t next_schedule = this->scheduler.next_schedule_in().value_or(delay_time);
@ -126,7 +128,7 @@ void Application::loop() {
next_schedule = std::max(next_schedule, delay_time / 2); next_schedule = std::max(next_schedule, delay_time / 2);
delay_time = std::min(next_schedule, delay_time); delay_time = std::min(next_schedule, delay_time);
this->delay_with_select_(delay_time); this->yield_with_select_(delay_time);
} }
this->last_loop_ = last_op_end_time; this->last_loop_ = last_op_end_time;
@ -215,7 +217,7 @@ void Application::teardown_components(uint32_t timeout_ms) {
// Give some time for I/O operations if components are still pending // Give some time for I/O operations if components are still pending
if (!pending_components.empty()) { if (!pending_components.empty()) {
this->delay_with_select_(1); this->yield_with_select_(1);
} }
// Update time for next iteration // Update time for next iteration
@ -293,8 +295,6 @@ bool Application::is_socket_ready(int fd) const {
// This function is thread-safe for reading the result of select() // This function is thread-safe for reading the result of select()
// However, it should only be called after select() has been executed in the main loop // However, it should only be called after select() has been executed in the main loop
// The read_fds_ is only modified by select() in the main loop // The read_fds_ is only modified by select() in the main loop
if (HighFrequencyLoopRequester::is_high_frequency())
return true; // fd sets via select are not updated in high frequency looping - so force true fallback behavior
if (fd < 0 || fd >= FD_SETSIZE) if (fd < 0 || fd >= FD_SETSIZE)
return false; return false;
@ -302,7 +302,9 @@ bool Application::is_socket_ready(int fd) const {
} }
#endif #endif
void Application::delay_with_select_(uint32_t delay_ms) { void Application::yield_with_select_(uint32_t delay_ms) {
// Delay while monitoring sockets. When delay_ms is 0, always yield() to ensure other tasks run
// since select() with 0 timeout only polls without yielding.
#ifdef USE_SOCKET_SELECT_SUPPORT #ifdef USE_SOCKET_SELECT_SUPPORT
if (!this->socket_fds_.empty()) { if (!this->socket_fds_.empty()) {
// Update fd_set if socket list has changed // Update fd_set if socket list has changed
@ -340,6 +342,10 @@ void Application::delay_with_select_(uint32_t delay_ms) {
ESP_LOGW(TAG, "select() failed with errno %d", errno); ESP_LOGW(TAG, "select() failed with errno %d", errno);
delay(delay_ms); delay(delay_ms);
} }
// When delay_ms is 0, we need to yield since select(0) doesn't yield
if (delay_ms == 0) {
yield();
}
} else { } else {
// No sockets registered, use regular delay // No sockets registered, use regular delay
delay(delay_ms); delay(delay_ms);

View File

@ -87,8 +87,8 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick
class Application { class Application {
public: public:
void pre_setup(const std::string &name, const std::string &friendly_name, const std::string &area, void pre_setup(const std::string &name, const std::string &friendly_name, const char *area, const char *comment,
const char *comment, const char *compilation_time, bool name_add_mac_suffix) { const char *compilation_time, bool name_add_mac_suffix) {
arch_init(); arch_init();
this->name_add_mac_suffix_ = name_add_mac_suffix; this->name_add_mac_suffix_ = name_add_mac_suffix;
if (name_add_mac_suffix) { if (name_add_mac_suffix) {
@ -285,7 +285,7 @@ class Application {
const std::string &get_friendly_name() const { return this->friendly_name_; } const std::string &get_friendly_name() const { return this->friendly_name_; }
/// Get the area of this Application set by pre_setup(). /// Get the area of this Application set by pre_setup().
const std::string &get_area() const { return this->area_; } std::string get_area() const { return this->area_ == nullptr ? "" : this->area_; }
/// Get the comment of this Application set by pre_setup(). /// Get the comment of this Application set by pre_setup().
std::string get_comment() const { return this->comment_; } std::string get_comment() const { return this->comment_; }
@ -332,7 +332,7 @@ class Application {
*/ */
void teardown_components(uint32_t timeout_ms); void teardown_components(uint32_t timeout_ms);
uint32_t get_app_state() const { return this->app_state_; } uint8_t get_app_state() const { return this->app_state_; }
#ifdef USE_BINARY_SENSOR #ifdef USE_BINARY_SENSOR
const std::vector<binary_sensor::BinarySensor *> &get_binary_sensors() { return this->binary_sensors_; } const std::vector<binary_sensor::BinarySensor *> &get_binary_sensors() { return this->binary_sensors_; }
@ -575,7 +575,7 @@ class Application {
void feed_wdt_arch_(); void feed_wdt_arch_();
/// Perform a delay while also monitoring socket file descriptors for readiness /// Perform a delay while also monitoring socket file descriptors for readiness
void delay_with_select_(uint32_t delay_ms); void yield_with_select_(uint32_t delay_ms);
std::vector<Component *> components_{}; std::vector<Component *> components_{};
std::vector<Component *> looping_components_{}; std::vector<Component *> looping_components_{};
@ -646,14 +646,14 @@ class Application {
std::string name_; std::string name_;
std::string friendly_name_; std::string friendly_name_;
std::string area_; const char *area_{nullptr};
const char *comment_{nullptr}; const char *comment_{nullptr};
const char *compilation_time_{nullptr}; const char *compilation_time_{nullptr};
bool name_add_mac_suffix_; bool name_add_mac_suffix_;
uint32_t last_loop_{0}; uint32_t last_loop_{0};
uint32_t loop_interval_{16}; uint32_t loop_interval_{16};
size_t dump_config_at_{SIZE_MAX}; size_t dump_config_at_{SIZE_MAX};
uint32_t app_state_{0}; uint8_t app_state_{0};
Component *current_component_{nullptr}; Component *current_component_{nullptr};
uint32_t loop_component_start_time_{0}; uint32_t loop_component_start_time_{0};

View File

@ -1,6 +1,7 @@
#include "esphome/core/component.h" #include "esphome/core/component.h"
#include <cinttypes> #include <cinttypes>
#include <limits>
#include <utility> #include <utility>
#include "esphome/core/application.h" #include "esphome/core/application.h"
#include "esphome/core/hal.h" #include "esphome/core/hal.h"
@ -29,18 +30,20 @@ const float LATE = -100.0f;
} // namespace setup_priority } // namespace setup_priority
const uint32_t COMPONENT_STATE_MASK = 0xFF; // Component state uses bits 0-1 (4 states)
const uint32_t COMPONENT_STATE_CONSTRUCTION = 0x00; const uint8_t COMPONENT_STATE_MASK = 0x03;
const uint32_t COMPONENT_STATE_SETUP = 0x01; const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00;
const uint32_t COMPONENT_STATE_LOOP = 0x02; const uint8_t COMPONENT_STATE_SETUP = 0x01;
const uint32_t COMPONENT_STATE_FAILED = 0x03; const uint8_t COMPONENT_STATE_LOOP = 0x02;
const uint32_t STATUS_LED_MASK = 0xFF00; const uint8_t COMPONENT_STATE_FAILED = 0x03;
const uint32_t STATUS_LED_OK = 0x0000; // Status LED uses bits 2-3
const uint32_t STATUS_LED_WARNING = 0x0100; const uint8_t STATUS_LED_MASK = 0x0C;
const uint32_t STATUS_LED_ERROR = 0x0200; const uint8_t STATUS_LED_OK = 0x00;
const uint8_t STATUS_LED_WARNING = 0x04; // Bit 2
const uint8_t STATUS_LED_ERROR = 0x08; // Bit 3
const uint32_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning const uint16_t WARN_IF_BLOCKING_OVER_MS = 50U; ///< Initial blocking time allowed without warning
const uint32_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again const uint16_t WARN_IF_BLOCKING_INCREMENT_MS = 10U; ///< How long the blocking time must be larger to warn again
uint32_t global_state = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) uint32_t global_state = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
@ -86,9 +89,9 @@ void Component::call_dump_config() {
} }
} }
uint32_t Component::get_component_state() const { return this->component_state_; } uint8_t Component::get_component_state() const { return this->component_state_; }
void Component::call() { void Component::call() {
uint32_t state = this->component_state_ & COMPONENT_STATE_MASK; uint8_t state = this->component_state_ & COMPONENT_STATE_MASK;
switch (state) { switch (state) {
case COMPONENT_STATE_CONSTRUCTION: case COMPONENT_STATE_CONSTRUCTION:
// State Construction: Call setup and set state to setup // State Construction: Call setup and set state to setup
@ -120,7 +123,13 @@ const char *Component::get_component_source() const {
} }
bool Component::should_warn_of_blocking(uint32_t blocking_time) { bool Component::should_warn_of_blocking(uint32_t blocking_time) {
if (blocking_time > this->warn_if_blocking_over_) { if (blocking_time > this->warn_if_blocking_over_) {
this->warn_if_blocking_over_ = blocking_time + WARN_IF_BLOCKING_INCREMENT_MS; // Prevent overflow when adding increment - if we're about to overflow, just max out
if (blocking_time + WARN_IF_BLOCKING_INCREMENT_MS < blocking_time ||
blocking_time + WARN_IF_BLOCKING_INCREMENT_MS > std::numeric_limits<uint16_t>::max()) {
this->warn_if_blocking_over_ = std::numeric_limits<uint16_t>::max();
} else {
this->warn_if_blocking_over_ = static_cast<uint16_t>(blocking_time + WARN_IF_BLOCKING_INCREMENT_MS);
}
return true; return true;
} }
return false; return false;
@ -131,6 +140,18 @@ void Component::mark_failed() {
this->component_state_ |= COMPONENT_STATE_FAILED; this->component_state_ |= COMPONENT_STATE_FAILED;
this->status_set_error(); this->status_set_error();
} }
void Component::reset_to_construction_state() {
if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) {
ESP_LOGI(TAG, "Component %s is being reset to construction state.", this->get_component_source());
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
// Clear error status when resetting
this->status_clear_error();
}
}
bool Component::is_in_loop_state() const {
return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP;
}
void Component::defer(std::function<void()> &&f) { // NOLINT void Component::defer(std::function<void()> &&f) { // NOLINT
App.scheduler.set_timeout(this, "", 0, std::move(f)); App.scheduler.set_timeout(this, "", 0, std::move(f));
} }

View File

@ -53,19 +53,19 @@ static const uint32_t SCHEDULER_DONT_RUN = 4294967295UL;
ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \ ESP_LOGCONFIG(TAG, " Update Interval: %.1fs", this->get_update_interval() / 1000.0f); \
} }
extern const uint32_t COMPONENT_STATE_MASK; extern const uint8_t COMPONENT_STATE_MASK;
extern const uint32_t COMPONENT_STATE_CONSTRUCTION; extern const uint8_t COMPONENT_STATE_CONSTRUCTION;
extern const uint32_t COMPONENT_STATE_SETUP; extern const uint8_t COMPONENT_STATE_SETUP;
extern const uint32_t COMPONENT_STATE_LOOP; extern const uint8_t COMPONENT_STATE_LOOP;
extern const uint32_t COMPONENT_STATE_FAILED; extern const uint8_t COMPONENT_STATE_FAILED;
extern const uint32_t STATUS_LED_MASK; extern const uint8_t STATUS_LED_MASK;
extern const uint32_t STATUS_LED_OK; extern const uint8_t STATUS_LED_OK;
extern const uint32_t STATUS_LED_WARNING; extern const uint8_t STATUS_LED_WARNING;
extern const uint32_t STATUS_LED_ERROR; extern const uint8_t STATUS_LED_ERROR;
enum class RetryResult { DONE, RETRY }; enum class RetryResult { DONE, RETRY };
extern const uint32_t WARN_IF_BLOCKING_OVER_MS; extern const uint16_t WARN_IF_BLOCKING_OVER_MS;
class Component { class Component {
public: public:
@ -123,7 +123,19 @@ class Component {
*/ */
virtual void on_powerdown() {} virtual void on_powerdown() {}
uint32_t get_component_state() const; uint8_t get_component_state() const;
/** Reset this component back to the construction state to allow setup to run again.
*
* This can be used by components that have recoverable failures to attempt setup again.
*/
void reset_to_construction_state();
/** Check if this component has completed setup and is in the loop state.
*
* @return True if in loop state, false otherwise.
*/
bool is_in_loop_state() const;
/** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called. /** Mark this component as failed. Any future timeouts/intervals/setup/loop will no longer be called.
* *
@ -298,10 +310,15 @@ class Component {
/// Cancel a defer callback using the specified name, name must not be empty. /// Cancel a defer callback using the specified name, name must not be empty.
bool cancel_defer(const std::string &name); // NOLINT bool cancel_defer(const std::string &name); // NOLINT
uint32_t component_state_{0x0000}; ///< State of this component. /// State of this component - each bit has a purpose:
/// Bits 0-1: Component state (0x00=CONSTRUCTION, 0x01=SETUP, 0x02=LOOP, 0x03=FAILED)
/// Bit 2: STATUS_LED_WARNING
/// Bit 3: STATUS_LED_ERROR
/// Bits 4-7: Unused - reserved for future expansion (50% of the bits are free)
uint8_t component_state_{0x00};
float setup_priority_override_{NAN}; float setup_priority_override_{NAN};
const char *component_source_{nullptr}; const char *component_source_{nullptr};
uint32_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s)
std::string error_message_{}; std::string error_message_{};
}; };

View File

@ -12,20 +12,12 @@ void EntityBase::set_name(const char *name) {
this->name_ = StringRef(name); this->name_ = StringRef(name);
if (this->name_.empty()) { if (this->name_.empty()) {
this->name_ = StringRef(App.get_friendly_name()); this->name_ = StringRef(App.get_friendly_name());
this->has_own_name_ = false; this->flags_.has_own_name = false;
} else { } else {
this->has_own_name_ = true; this->flags_.has_own_name = true;
} }
} }
// Entity Internal
bool EntityBase::is_internal() const { return this->internal_; }
void EntityBase::set_internal(bool internal) { this->internal_ = internal; }
// Entity Disabled by Default
bool EntityBase::is_disabled_by_default() const { return this->disabled_by_default_; }
void EntityBase::set_disabled_by_default(bool disabled_by_default) { this->disabled_by_default_ = disabled_by_default; }
// Entity Icon // Entity Icon
std::string EntityBase::get_icon() const { std::string EntityBase::get_icon() const {
if (this->icon_c_str_ == nullptr) { if (this->icon_c_str_ == nullptr) {
@ -35,14 +27,10 @@ std::string EntityBase::get_icon() const {
} }
void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; } void EntityBase::set_icon(const char *icon) { this->icon_c_str_ = icon; }
// Entity Category
EntityCategory EntityBase::get_entity_category() const { return this->entity_category_; }
void EntityBase::set_entity_category(EntityCategory entity_category) { this->entity_category_ = entity_category; }
// Entity Object ID // Entity Object ID
std::string EntityBase::get_object_id() const { std::string EntityBase::get_object_id() const {
// Check if `App.get_friendly_name()` is constant or dynamic. // Check if `App.get_friendly_name()` is constant or dynamic.
if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) { if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) {
// `App.get_friendly_name()` is dynamic. // `App.get_friendly_name()` is dynamic.
return str_sanitize(str_snake_case(App.get_friendly_name())); return str_sanitize(str_snake_case(App.get_friendly_name()));
} else { } else {
@ -61,7 +49,7 @@ void EntityBase::set_object_id(const char *object_id) {
// Calculate Object ID Hash from Entity Name // Calculate Object ID Hash from Entity Name
void EntityBase::calc_object_id_() { void EntityBase::calc_object_id_() {
// Check if `App.get_friendly_name()` is constant or dynamic. // Check if `App.get_friendly_name()` is constant or dynamic.
if (!this->has_own_name_ && App.is_name_add_mac_suffix_enabled()) { if (!this->flags_.has_own_name && App.is_name_add_mac_suffix_enabled()) {
// `App.get_friendly_name()` is dynamic. // `App.get_friendly_name()` is dynamic.
const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name())); const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name()));
// FNV-1 hash // FNV-1 hash

View File

@ -20,7 +20,7 @@ class EntityBase {
void set_name(const char *name); void set_name(const char *name);
// Get whether this Entity has its own name or it should use the device friendly_name. // Get whether this Entity has its own name or it should use the device friendly_name.
bool has_own_name() const { return this->has_own_name_; } bool has_own_name() const { return this->flags_.has_own_name; }
// Get the sanitized name of this Entity as an ID. // Get the sanitized name of this Entity as an ID.
std::string get_object_id() const; std::string get_object_id() const;
@ -29,24 +29,32 @@ class EntityBase {
// Get the unique Object ID of this Entity // Get the unique Object ID of this Entity
uint32_t get_object_id_hash(); uint32_t get_object_id_hash();
// Get/set whether this Entity should be hidden from outside of ESPHome // Get/set whether this Entity should be hidden outside ESPHome
bool is_internal() const; bool is_internal() const { return this->flags_.internal; }
void set_internal(bool internal); void set_internal(bool internal) { this->flags_.internal = internal; }
// Check if this object is declared to be disabled by default. // Check if this object is declared to be disabled by default.
// That means that when the device gets added to Home Assistant (or other clients) it should // That means that when the device gets added to Home Assistant (or other clients) it should
// not be added to the default view by default, and a user action is necessary to manually add it. // not be added to the default view by default, and a user action is necessary to manually add it.
bool is_disabled_by_default() const; bool is_disabled_by_default() const { return this->flags_.disabled_by_default; }
void set_disabled_by_default(bool disabled_by_default); void set_disabled_by_default(bool disabled_by_default) { this->flags_.disabled_by_default = disabled_by_default; }
// Get/set the entity category. // Get/set the entity category.
EntityCategory get_entity_category() const; EntityCategory get_entity_category() const { return static_cast<EntityCategory>(this->flags_.entity_category); }
void set_entity_category(EntityCategory entity_category); void set_entity_category(EntityCategory entity_category) {
this->flags_.entity_category = static_cast<uint8_t>(entity_category);
}
// Get/set this entity's icon // Get/set this entity's icon
std::string get_icon() const; std::string get_icon() const;
void set_icon(const char *icon); void set_icon(const char *icon);
// Check if this entity has state
bool has_state() const { return this->flags_.has_state; }
// Set has_state - for components that need to manually set this
void set_has_state(bool state) { this->flags_.has_state = state; }
protected: protected:
/// The hash_base() function has been deprecated. It is kept in this /// The hash_base() function has been deprecated. It is kept in this
/// class for now, to prevent external components from not compiling. /// class for now, to prevent external components from not compiling.
@ -56,11 +64,17 @@ class EntityBase {
StringRef name_; StringRef name_;
const char *object_id_c_str_{nullptr}; const char *object_id_c_str_{nullptr};
const char *icon_c_str_{nullptr}; const char *icon_c_str_{nullptr};
uint32_t object_id_hash_; uint32_t object_id_hash_{};
bool has_own_name_{false};
bool internal_{false}; // Bit-packed flags to save memory (1 byte instead of 5)
bool disabled_by_default_{false}; struct EntityFlags {
EntityCategory entity_category_{ENTITY_CATEGORY_NONE}; uint8_t has_own_name : 1;
uint8_t internal : 1;
uint8_t disabled_by_default : 1;
uint8_t has_state : 1;
uint8_t entity_category : 2; // Supports up to 4 categories
uint8_t reserved : 2; // Reserved for future use
} flags_{};
}; };
class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming) class EntityBase_DeviceClass { // NOLINT(readability-identifier-naming)

View File

@ -438,7 +438,7 @@ template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::stri
} }
/// Return values for parse_on_off(). /// Return values for parse_on_off().
enum ParseOnOffState { enum ParseOnOffState : uint8_t {
PARSE_NONE = 0, PARSE_NONE = 0,
PARSE_ON, PARSE_ON,
PARSE_OFF, PARSE_OFF,

View File

@ -67,20 +67,6 @@ esp8266:
""" """
ESP32_CONFIG = """ ESP32_CONFIG = """
esp32:
board: {board}
framework:
type: arduino
"""
ESP32S2_CONFIG = """
esp32:
board: {board}
framework:
type: esp-idf
"""
ESP32C3_CONFIG = """
esp32: esp32:
board: {board} board: {board}
framework: framework:
@ -105,8 +91,6 @@ rtl87xx:
HARDWARE_BASE_CONFIGS = { HARDWARE_BASE_CONFIGS = {
"ESP8266": ESP8266_CONFIG, "ESP8266": ESP8266_CONFIG,
"ESP32": ESP32_CONFIG, "ESP32": ESP32_CONFIG,
"ESP32S2": ESP32S2_CONFIG,
"ESP32C3": ESP32C3_CONFIG,
"RP2040": RP2040_CONFIG, "RP2040": RP2040_CONFIG,
"BK72XX": BK72XX_CONFIG, "BK72XX": BK72XX_CONFIG,
"RTL87XX": RTL87XX_CONFIG, "RTL87XX": RTL87XX_CONFIG,

View File

@ -13,7 +13,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile
esptool==4.8.1 esptool==4.8.1
click==8.1.7 click==8.1.7
esphome-dashboard==20250514.0 esphome-dashboard==20250514.0
aioesphomeapi==32.2.1 aioesphomeapi==32.2.3
zeroconf==0.147.0 zeroconf==0.147.0
puremagic==1.29 puremagic==1.29
ruamel.yaml==0.18.14 # dashboard_import ruamel.yaml==0.18.14 # dashboard_import

View File

@ -848,7 +848,10 @@ def calculate_message_estimated_size(desc: descriptor.DescriptorProto) -> int:
return total_size return total_size
def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]: def build_message_type(
desc: descriptor.DescriptorProto,
base_class_fields: dict[str, list[descriptor.FieldDescriptorProto]] = None,
) -> tuple[str, str]:
public_content: list[str] = [] public_content: list[str] = []
protected_content: list[str] = [] protected_content: list[str] = []
decode_varint: list[str] = [] decode_varint: list[str] = []
@ -859,6 +862,12 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]:
dump: list[str] = [] dump: list[str] = []
size_calc: list[str] = [] size_calc: list[str] = []
# Check if this message has a base class
base_class = get_base_class(desc)
common_field_names = set()
if base_class and base_class_fields and base_class in base_class_fields:
common_field_names = {f.name for f in base_class_fields[base_class]}
# Get message ID if it's a service message # Get message ID if it's a service message
message_id: int | None = get_opt(desc, pb.id) message_id: int | None = get_opt(desc, pb.id)
@ -886,8 +895,14 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]:
ti = RepeatedTypeInfo(field) ti = RepeatedTypeInfo(field)
else: else:
ti = TYPE_INFO[field.type](field) ti = TYPE_INFO[field.type](field)
protected_content.extend(ti.protected_content)
public_content.extend(ti.public_content) # Skip field declarations for fields that are in the base class
# but include their encode/decode logic
if field.name not in common_field_names:
protected_content.extend(ti.protected_content)
public_content.extend(ti.public_content)
# Always include encode/decode logic for all fields
encode.append(ti.encode_content) encode.append(ti.encode_content)
size_calc.append(ti.get_size_calculation(f"this->{ti.field_name}")) size_calc.append(ti.get_size_calculation(f"this->{ti.field_name}"))
@ -1001,7 +1016,10 @@ def build_message_type(desc: descriptor.DescriptorProto) -> tuple[str, str]:
prot += "#endif\n" prot += "#endif\n"
public_content.append(prot) public_content.append(prot)
out = f"class {desc.name} : public ProtoMessage {{\n" if base_class:
out = f"class {desc.name} : public {base_class} {{\n"
else:
out = f"class {desc.name} : public ProtoMessage {{\n"
out += " public:\n" out += " public:\n"
out += indent("\n".join(public_content)) + "\n" out += indent("\n".join(public_content)) + "\n"
out += "\n" out += "\n"
@ -1033,6 +1051,132 @@ def get_opt(
return desc.options.Extensions[opt] return desc.options.Extensions[opt]
def get_base_class(desc: descriptor.DescriptorProto) -> str | None:
"""Get the base_class option from a message descriptor."""
if not desc.options.HasExtension(pb.base_class):
return None
return desc.options.Extensions[pb.base_class]
def collect_messages_by_base_class(
messages: list[descriptor.DescriptorProto],
) -> dict[str, list[descriptor.DescriptorProto]]:
"""Group messages by their base_class option."""
base_class_groups = {}
for msg in messages:
base_class = get_base_class(msg)
if base_class:
if base_class not in base_class_groups:
base_class_groups[base_class] = []
base_class_groups[base_class].append(msg)
return base_class_groups
def find_common_fields(
messages: list[descriptor.DescriptorProto],
) -> list[descriptor.FieldDescriptorProto]:
"""Find fields that are common to all messages in the list."""
if not messages:
return []
# Start with fields from the first message
first_msg_fields = {field.name: field for field in messages[0].field}
common_fields = []
# Check each field to see if it exists in all messages with same type
# Field numbers can vary between messages - derived classes handle the mapping
for field_name, field in first_msg_fields.items():
is_common = True
for msg in messages[1:]:
found = False
for other_field in msg.field:
if (
other_field.name == field_name
and other_field.type == field.type
and other_field.label == field.label
):
found = True
break
if not found:
is_common = False
break
if is_common:
common_fields.append(field)
# Sort by field number to maintain order
common_fields.sort(key=lambda f: f.number)
return common_fields
def build_base_class(
base_class_name: str,
common_fields: list[descriptor.FieldDescriptorProto],
) -> tuple[str, str]:
"""Build the base class definition and implementation."""
public_content = []
protected_content = []
# For base classes, we only declare the fields but don't handle encode/decode
# The derived classes will handle encoding/decoding with their specific field numbers
for field in common_fields:
if field.label == 3: # repeated
ti = RepeatedTypeInfo(field)
else:
ti = TYPE_INFO[field.type](field)
# Only add field declarations, not encode/decode logic
protected_content.extend(ti.protected_content)
public_content.extend(ti.public_content)
# Build header
out = f"class {base_class_name} : public ProtoMessage {{\n"
out += " public:\n"
# Add destructor with override
public_content.insert(0, f"~{base_class_name}() override = default;")
# Base classes don't implement encode/decode/calculate_size
# Derived classes handle these with their specific field numbers
cpp = ""
out += indent("\n".join(public_content)) + "\n"
out += "\n"
out += " protected:\n"
out += indent("\n".join(protected_content))
if protected_content:
out += "\n"
out += "};\n"
# No implementation needed for base classes
return out, cpp
def generate_base_classes(
base_class_groups: dict[str, list[descriptor.DescriptorProto]],
) -> tuple[str, str]:
"""Generate all base classes."""
all_headers = []
all_cpp = []
for base_class_name, messages in base_class_groups.items():
# Find common fields
common_fields = find_common_fields(messages)
if common_fields:
# Generate base class
header, cpp = build_base_class(base_class_name, common_fields)
all_headers.append(header)
all_cpp.append(cpp)
return "\n".join(all_headers), "\n".join(all_cpp)
def build_service_message_type( def build_service_message_type(
mt: descriptor.DescriptorProto, mt: descriptor.DescriptorProto,
) -> tuple[str, str] | None: ) -> tuple[str, str] | None:
@ -1134,8 +1278,25 @@ def main() -> None:
mt = file.message_type mt = file.message_type
# Collect messages by base class
base_class_groups = collect_messages_by_base_class(mt)
# Find common fields for each base class
base_class_fields = {}
for base_class_name, messages in base_class_groups.items():
common_fields = find_common_fields(messages)
if common_fields:
base_class_fields[base_class_name] = common_fields
# Generate base classes
if base_class_fields:
base_headers, base_cpp = generate_base_classes(base_class_groups)
content += base_headers
cpp += base_cpp
# Generate message types with base class information
for m in mt: for m in mt:
s, c = build_message_type(m) s, c = build_message_type(m, base_class_fields)
content += s content += s
cpp += c cpp += c

View File

@ -15,7 +15,7 @@ import sys
import tempfile import tempfile
from typing import TextIO from typing import TextIO
from aioesphomeapi import APIClient, APIConnectionError, ReconnectLogic from aioesphomeapi import APIClient, APIConnectionError, LogParser, ReconnectLogic
import pytest import pytest
import pytest_asyncio import pytest_asyncio
@ -119,6 +119,21 @@ async def yaml_config(request: pytest.FixtureRequest, unused_tcp_port: int) -> s
# Add port configuration after api: # Add port configuration after api:
content = content.replace("api:", f"api:\n port: {unused_tcp_port}") content = content.replace("api:", f"api:\n port: {unused_tcp_port}")
# Add debug build flags for integration tests to enable assertions
if "esphome:" in content:
# Check if platformio_options already exists
if "platformio_options:" not in content:
# Add platformio_options with debug flags after esphome:
content = content.replace(
"esphome:",
"esphome:\n"
" # Enable assertions for integration tests\n"
" platformio_options:\n"
" build_flags:\n"
' - "-DDEBUG" # Enable assert() statements\n'
' - "-g" # Add debug symbols',
)
return content return content
@ -350,11 +365,21 @@ async def _read_stream_lines(
stream: asyncio.StreamReader, lines: list[str], output_stream: TextIO stream: asyncio.StreamReader, lines: list[str], output_stream: TextIO
) -> None: ) -> None:
"""Read lines from a stream, append to list, and echo to output stream.""" """Read lines from a stream, append to list, and echo to output stream."""
log_parser = LogParser()
while line := await stream.readline(): while line := await stream.readline():
decoded_line = line.decode("utf-8", errors="replace") decoded_line = (
line.replace(b"\r", b"")
.replace(b"\n", b"")
.decode("utf8", "backslashreplace")
)
lines.append(decoded_line.rstrip()) lines.append(decoded_line.rstrip())
# Echo to stdout/stderr in real-time # Echo to stdout/stderr in real-time
print(decoded_line.rstrip(), file=output_stream, flush=True) # Print without newline to avoid double newlines
print(
log_parser.parse_line(decoded_line, timestamp=""),
file=output_stream,
flush=True,
)
@asynccontextmanager @asynccontextmanager

View File

@ -0,0 +1,161 @@
esphome:
name: message-size-batching-test
host:
api:
# Default batch_delay to test batching
logger:
# Create entities that will produce different protobuf header sizes
# Header size depends on: 1 byte indicator + varint(payload_size) + varint(message_type)
# 4-byte header: type < 128, payload < 128
# 5-byte header: type < 128, payload 128-16383 OR type 128+, payload < 128
# 6-byte header: type 128+, payload 128-16383
# Small select with few options - produces small message
select:
- platform: template
name: "Small Select"
id: small_select
optimistic: true
options:
- "Option A"
- "Option B"
initial_option: "Option A"
update_interval: 5.0s
# Medium select with more options - produces medium message
- platform: template
name: "Medium Select"
id: medium_select
optimistic: true
options:
- "Option 001"
- "Option 002"
- "Option 003"
- "Option 004"
- "Option 005"
- "Option 006"
- "Option 007"
- "Option 008"
- "Option 009"
- "Option 010"
- "Option 011"
- "Option 012"
- "Option 013"
- "Option 014"
- "Option 015"
- "Option 016"
- "Option 017"
- "Option 018"
- "Option 019"
- "Option 020"
initial_option: "Option 001"
update_interval: 5.0s
# Large select with many options - produces larger message
- platform: template
name: "Large Select with Many Options to Create Larger Payload"
id: large_select
optimistic: true
options:
- "Long Option Name 001 - This is a longer option name to increase message size"
- "Long Option Name 002 - This is a longer option name to increase message size"
- "Long Option Name 003 - This is a longer option name to increase message size"
- "Long Option Name 004 - This is a longer option name to increase message size"
- "Long Option Name 005 - This is a longer option name to increase message size"
- "Long Option Name 006 - This is a longer option name to increase message size"
- "Long Option Name 007 - This is a longer option name to increase message size"
- "Long Option Name 008 - This is a longer option name to increase message size"
- "Long Option Name 009 - This is a longer option name to increase message size"
- "Long Option Name 010 - This is a longer option name to increase message size"
- "Long Option Name 011 - This is a longer option name to increase message size"
- "Long Option Name 012 - This is a longer option name to increase message size"
- "Long Option Name 013 - This is a longer option name to increase message size"
- "Long Option Name 014 - This is a longer option name to increase message size"
- "Long Option Name 015 - This is a longer option name to increase message size"
- "Long Option Name 016 - This is a longer option name to increase message size"
- "Long Option Name 017 - This is a longer option name to increase message size"
- "Long Option Name 018 - This is a longer option name to increase message size"
- "Long Option Name 019 - This is a longer option name to increase message size"
- "Long Option Name 020 - This is a longer option name to increase message size"
- "Long Option Name 021 - This is a longer option name to increase message size"
- "Long Option Name 022 - This is a longer option name to increase message size"
- "Long Option Name 023 - This is a longer option name to increase message size"
- "Long Option Name 024 - This is a longer option name to increase message size"
- "Long Option Name 025 - This is a longer option name to increase message size"
- "Long Option Name 026 - This is a longer option name to increase message size"
- "Long Option Name 027 - This is a longer option name to increase message size"
- "Long Option Name 028 - This is a longer option name to increase message size"
- "Long Option Name 029 - This is a longer option name to increase message size"
- "Long Option Name 030 - This is a longer option name to increase message size"
- "Long Option Name 031 - This is a longer option name to increase message size"
- "Long Option Name 032 - This is a longer option name to increase message size"
- "Long Option Name 033 - This is a longer option name to increase message size"
- "Long Option Name 034 - This is a longer option name to increase message size"
- "Long Option Name 035 - This is a longer option name to increase message size"
- "Long Option Name 036 - This is a longer option name to increase message size"
- "Long Option Name 037 - This is a longer option name to increase message size"
- "Long Option Name 038 - This is a longer option name to increase message size"
- "Long Option Name 039 - This is a longer option name to increase message size"
- "Long Option Name 040 - This is a longer option name to increase message size"
- "Long Option Name 041 - This is a longer option name to increase message size"
- "Long Option Name 042 - This is a longer option name to increase message size"
- "Long Option Name 043 - This is a longer option name to increase message size"
- "Long Option Name 044 - This is a longer option name to increase message size"
- "Long Option Name 045 - This is a longer option name to increase message size"
- "Long Option Name 046 - This is a longer option name to increase message size"
- "Long Option Name 047 - This is a longer option name to increase message size"
- "Long Option Name 048 - This is a longer option name to increase message size"
- "Long Option Name 049 - This is a longer option name to increase message size"
- "Long Option Name 050 - This is a longer option name to increase message size"
initial_option: "Long Option Name 001 - This is a longer option name to increase message size"
update_interval: 5.0s
# Text sensors with different value lengths
text_sensor:
- platform: template
name: "Short Text Sensor"
id: short_text_sensor
lambda: |-
return {"OK"};
update_interval: 5.0s
- platform: template
name: "Medium Text Sensor"
id: medium_text_sensor
lambda: |-
return {"This is a medium length text sensor value that should produce a medium sized message"};
update_interval: 5.0s
- platform: template
name: "Long Text Sensor with Very Long Value"
id: long_text_sensor
lambda: |-
return {"This is a very long text sensor value that contains a lot of text to ensure we get a larger protobuf message. The message should be long enough to require a 2-byte varint for the payload size, which happens when the payload exceeds 127 bytes. Let's add even more text here to make sure we exceed that threshold and test the batching of messages with different header sizes properly."};
update_interval: 5.0s
# Text input which can have various lengths
text:
- platform: template
name: "Test Text Input"
id: test_text_input
optimistic: true
mode: text
min_length: 0
max_length: 255
initial_value: "Initial value"
update_interval: 5.0s
# Number entity to add variety (different message type number)
# The ListEntitiesNumberResponse has message type 49
# The NumberStateResponse has message type 50
number:
- platform: template
name: "Test Number with Long Name to Increase Message Size"
id: test_number
optimistic: true
min_value: 0
max_value: 1000
step: 0.1
initial_value: 42.0
update_interval: 5.0s

View File

@ -0,0 +1,58 @@
esphome:
name: host-empty-string-test
host:
api:
batch_delay: 50ms
select:
- platform: template
name: "Select Empty First"
id: select_empty_first
optimistic: true
options:
- "" # Empty string at the beginning
- "Option A"
- "Option B"
- "Option C"
initial_option: "Option A"
- platform: template
name: "Select Empty Middle"
id: select_empty_middle
optimistic: true
options:
- "Option 1"
- "Option 2"
- "" # Empty string in the middle
- "Option 3"
- "Option 4"
initial_option: "Option 1"
- platform: template
name: "Select Empty Last"
id: select_empty_last
optimistic: true
options:
- "Choice X"
- "Choice Y"
- "Choice Z"
- "" # Empty string at the end
initial_option: "Choice X"
# Add a sensor to ensure we have other entities in the list
sensor:
- platform: template
name: "Test Sensor"
id: test_sensor
lambda: |-
return 42.0;
update_interval: 60s
binary_sensor:
- platform: template
name: "Test Binary Sensor"
id: test_binary_sensor
lambda: |-
return true;

View File

@ -0,0 +1,108 @@
esphome:
name: host-test
host:
api:
logger:
# Test various entity types with different flag combinations
sensor:
- platform: template
name: "Test Normal Sensor"
id: normal_sensor
update_interval: 1s
lambda: |-
return 42.0;
- platform: template
name: "Test Internal Sensor"
id: internal_sensor
internal: true
update_interval: 1s
lambda: |-
return 43.0;
- platform: template
name: "Test Disabled Sensor"
id: disabled_sensor
disabled_by_default: true
update_interval: 1s
lambda: |-
return 44.0;
- platform: template
name: "Test Mixed Flags Sensor"
id: mixed_flags_sensor
internal: true
entity_category: diagnostic
update_interval: 1s
lambda: |-
return 45.0;
- platform: template
name: "Test Diagnostic Sensor"
id: diagnostic_sensor
entity_category: diagnostic
update_interval: 1s
lambda: |-
return 46.0;
- platform: template
name: "Test All Flags Sensor"
id: all_flags_sensor
internal: true
disabled_by_default: true
entity_category: diagnostic
update_interval: 1s
lambda: |-
return 47.0;
# Also test other entity types to ensure bit-packing works across all
binary_sensor:
- platform: template
name: "Test Binary Sensor"
entity_category: config
lambda: |-
return true;
text_sensor:
- platform: template
name: "Test Text Sensor"
disabled_by_default: true
lambda: |-
return {"Hello"};
number:
- platform: template
name: "Test Number"
initial_value: 50
min_value: 0
max_value: 100
step: 1
optimistic: true
entity_category: diagnostic
select:
- platform: template
name: "Test Select"
options:
- "Option 1"
- "Option 2"
initial_option: "Option 1"
optimistic: true
internal: true
switch:
- platform: template
name: "Test Switch"
optimistic: true
disabled_by_default: true
entity_category: config
button:
- platform: template
name: "Test Button"
on_press:
- logger.log: "Button pressed"

View File

@ -0,0 +1,34 @@
esphome:
name: host-test
host:
api:
logger:
# Test fan with preset modes and speed settings
fan:
- platform: template
name: "Test Fan with Presets"
id: test_fan_presets
speed_count: 5
preset_modes:
- "Eco"
- "Sleep"
- "Turbo"
has_oscillating: true
has_direction: true
- platform: template
name: "Test Fan Simple"
id: test_fan_simple
speed_count: 3
has_oscillating: false
has_direction: false
- platform: template
name: "Test Fan No Speed"
id: test_fan_no_speed
has_oscillating: true
has_direction: false

View File

@ -0,0 +1,137 @@
esphome:
name: large-message-test
host:
api:
logger:
# Create a select entity with many options to exceed 1390 bytes
select:
- platform: template
name: "Large Select"
id: large_select
optimistic: true
options:
- "Option 000 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 001 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 002 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 003 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 004 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 005 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 006 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 007 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 008 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 009 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 010 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 011 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 012 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 013 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 014 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 015 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 016 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 017 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 018 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 019 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 020 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 021 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 022 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 023 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 024 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 025 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 026 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 027 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 028 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 029 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 030 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 031 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 032 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 033 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 034 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 035 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 036 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 037 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 038 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 039 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 040 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 041 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 042 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 043 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 044 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 045 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 046 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 047 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 048 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 049 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 050 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 051 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 052 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 053 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 054 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 055 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 056 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 057 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 058 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 059 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 060 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 061 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 062 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 063 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 064 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 065 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 066 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 067 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 068 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 069 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 070 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 071 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 072 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 073 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 074 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 075 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 076 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 077 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 078 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 079 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 080 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 081 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 082 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 083 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 084 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 085 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 086 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 087 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 088 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 089 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 090 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 091 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 092 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 093 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 094 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 095 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 096 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 097 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 098 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
- "Option 099 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
initial_option: "Option 000 - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
# Add some other entities to test batching with the large select
sensor:
- platform: template
name: "Test Sensor"
id: test_sensor
lambda: |-
return 42.0;
update_interval: 1s
binary_sensor:
- platform: template
name: "Test Binary Sensor"
id: test_binary_sensor
lambda: |-
return true;
switch:
- platform: template
name: "Test Switch"
id: test_switch
optimistic: true

View File

@ -0,0 +1,194 @@
"""Integration test for API batching with various message sizes."""
from __future__ import annotations
import asyncio
from aioesphomeapi import EntityState, NumberInfo, SelectInfo, TextInfo, TextSensorInfo
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_api_message_size_batching(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test API can batch messages of various sizes correctly."""
# Write, compile and run the ESPHome device, then connect to API
loop = asyncio.get_running_loop()
async with run_compiled(yaml_config), api_client_connected() as client:
# Verify we can get device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "message-size-batching-test"
# List entities - this will batch various sized messages together
entity_info, services = await asyncio.wait_for(
client.list_entities_services(), timeout=5.0
)
# Count different entity types
selects = []
text_sensors = []
text_inputs = []
numbers = []
other_entities = []
for entity in entity_info:
if isinstance(entity, SelectInfo):
selects.append(entity)
elif isinstance(entity, TextSensorInfo):
text_sensors.append(entity)
elif isinstance(entity, TextInfo):
text_inputs.append(entity)
elif isinstance(entity, NumberInfo):
numbers.append(entity)
else:
other_entities.append(entity)
# Verify we have our test entities - exact counts
assert len(selects) == 3, (
f"Expected exactly 3 select entities, got {len(selects)}"
)
assert len(text_sensors) == 3, (
f"Expected exactly 3 text sensor entities, got {len(text_sensors)}"
)
assert len(text_inputs) == 1, (
f"Expected exactly 1 text input entity, got {len(text_inputs)}"
)
# Collect all select entity object_ids for error messages
select_ids = [s.object_id for s in selects]
# Find our specific test entities
small_select = None
medium_select = None
large_select = None
for select in selects:
if select.object_id == "small_select":
small_select = select
elif select.object_id == "medium_select":
medium_select = select
elif (
select.object_id
== "large_select_with_many_options_to_create_larger_payload"
):
large_select = select
assert small_select is not None, (
f"Could not find small_select entity. Found: {select_ids}"
)
assert medium_select is not None, (
f"Could not find medium_select entity. Found: {select_ids}"
)
assert large_select is not None, (
f"Could not find large_select entity. Found: {select_ids}"
)
# Verify the selects have the expected number of options
assert len(small_select.options) == 2, (
f"Expected 2 options for small_select, got {len(small_select.options)}"
)
assert len(medium_select.options) == 20, (
f"Expected 20 options for medium_select, got {len(medium_select.options)}"
)
assert len(large_select.options) == 50, (
f"Expected 50 options for large_select, got {len(large_select.options)}"
)
# Collect all text sensor object_ids for error messages
text_sensor_ids = [t.object_id for t in text_sensors]
# Verify text sensors with different value lengths
short_text_sensor = None
medium_text_sensor = None
long_text_sensor = None
for text_sensor in text_sensors:
if text_sensor.object_id == "short_text_sensor":
short_text_sensor = text_sensor
elif text_sensor.object_id == "medium_text_sensor":
medium_text_sensor = text_sensor
elif text_sensor.object_id == "long_text_sensor_with_very_long_value":
long_text_sensor = text_sensor
assert short_text_sensor is not None, (
f"Could not find short_text_sensor. Found: {text_sensor_ids}"
)
assert medium_text_sensor is not None, (
f"Could not find medium_text_sensor. Found: {text_sensor_ids}"
)
assert long_text_sensor is not None, (
f"Could not find long_text_sensor. Found: {text_sensor_ids}"
)
# Check text input which can have a long max_length
text_input = None
text_input_ids = [t.object_id for t in text_inputs]
for ti in text_inputs:
if ti.object_id == "test_text_input":
text_input = ti
break
assert text_input is not None, (
f"Could not find test_text_input. Found: {text_input_ids}"
)
assert text_input.max_length == 255, (
f"Expected max_length 255, got {text_input.max_length}"
)
# Verify total entity count - messages of various sizes were batched successfully
# We have: 3 selects + 3 text sensors + 1 text input + 1 number = 8 total
total_entities = len(entity_info)
assert total_entities == 8, f"Expected exactly 8 entities, got {total_entities}"
# Check we have the expected entity types
assert len(numbers) == 1, (
f"Expected exactly 1 number entity, got {len(numbers)}"
)
assert len(other_entities) == 0, (
f"Unexpected entity types found: {[type(e).__name__ for e in other_entities]}"
)
# Subscribe to state changes to verify batching works
# Collect keys from entity info to know what states to expect
expected_keys = {entity.key for entity in entity_info}
assert len(expected_keys) == 8, (
f"Expected 8 unique entity keys, got {len(expected_keys)}"
)
received_keys: set[int] = set()
states_future: asyncio.Future[None] = loop.create_future()
def on_state(state: EntityState) -> None:
"""Track when states are received."""
received_keys.add(state.key)
# Check if we've received states from all expected entities
if expected_keys.issubset(received_keys) and not states_future.done():
states_future.set_result(None)
client.subscribe_states(on_state)
# Wait for states with timeout
try:
await asyncio.wait_for(states_future, timeout=5.0)
except asyncio.TimeoutError:
missing_keys = expected_keys - received_keys
pytest.fail(
f"Did not receive states from all entities within 5 seconds. "
f"Missing keys: {missing_keys}, "
f"Received {len(received_keys)} of {len(expected_keys)} expected states"
)
# Verify we received states from all entities
assert expected_keys.issubset(received_keys)
# Check that various message sizes were handled correctly
# Small messages (4-byte header): type < 128, payload < 128
# Medium messages (5-byte header): type < 128, payload 128-16383 OR type 128+, payload < 128
# Large messages (6-byte header): type 128+, payload 128-16383

View File

@ -0,0 +1,110 @@
"""Integration test for protobuf encoding of empty string options in select entities."""
from __future__ import annotations
import asyncio
from aioesphomeapi import EntityState, SelectInfo
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_host_mode_empty_string_options(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test that select entities with empty string options are correctly encoded in protobuf messages.
This tests the fix for the bug where the force parameter was not passed in encode_string,
causing empty strings in repeated fields to be skipped during encoding but included in
size calculation, leading to protobuf decoding errors.
"""
# Write, compile and run the ESPHome device, then connect to API
loop = asyncio.get_running_loop()
async with run_compiled(yaml_config), api_client_connected() as client:
# Verify we can get device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "host-empty-string-test"
# Get list of entities - this will encode ListEntitiesSelectResponse messages
# with empty string options that would trigger the bug
entity_info, services = await client.list_entities_services()
# Find our select entities
select_entities = [e for e in entity_info if isinstance(e, SelectInfo)]
assert len(select_entities) == 3, (
f"Expected 3 select entities, got {len(select_entities)}"
)
# Verify each select entity by name and check their options
selects_by_name = {e.name: e for e in select_entities}
# Check "Select Empty First" - empty string at beginning
assert "Select Empty First" in selects_by_name
empty_first = selects_by_name["Select Empty First"]
assert len(empty_first.options) == 4
assert empty_first.options[0] == "" # Empty string at beginning
assert empty_first.options[1] == "Option A"
assert empty_first.options[2] == "Option B"
assert empty_first.options[3] == "Option C"
# Check "Select Empty Middle" - empty string in middle
assert "Select Empty Middle" in selects_by_name
empty_middle = selects_by_name["Select Empty Middle"]
assert len(empty_middle.options) == 5
assert empty_middle.options[0] == "Option 1"
assert empty_middle.options[1] == "Option 2"
assert empty_middle.options[2] == "" # Empty string in middle
assert empty_middle.options[3] == "Option 3"
assert empty_middle.options[4] == "Option 4"
# Check "Select Empty Last" - empty string at end
assert "Select Empty Last" in selects_by_name
empty_last = selects_by_name["Select Empty Last"]
assert len(empty_last.options) == 4
assert empty_last.options[0] == "Choice X"
assert empty_last.options[1] == "Choice Y"
assert empty_last.options[2] == "Choice Z"
assert empty_last.options[3] == "" # Empty string at end
# If we got here without protobuf decoding errors, the fix is working
# The bug would have caused "Invalid protobuf message" errors with trailing bytes
# Also verify we can interact with the select entities
# Subscribe to state changes
states: dict[int, EntityState] = {}
state_change_future: asyncio.Future[None] = loop.create_future()
def on_state(state: EntityState) -> None:
"""Track state changes."""
states[state.key] = state
# When we receive the state change for our select, resolve the future
if state.key == empty_first.key and not state_change_future.done():
state_change_future.set_result(None)
client.subscribe_states(on_state)
# Try setting a select to an empty string option
# This further tests that empty strings are handled correctly
client.select_command(empty_first.key, "")
# Wait for state update with timeout
try:
await asyncio.wait_for(state_change_future, timeout=5.0)
except asyncio.TimeoutError:
pytest.fail(
"Did not receive state update after setting select to empty string"
)
# Verify the state was set to empty string
assert empty_first.key in states
select_state = states[empty_first.key]
assert hasattr(select_state, "state")
assert select_state.state == ""
# The test passes if no protobuf decoding errors occurred
# With the bug, we would have gotten "Invalid protobuf message" errors

View File

@ -0,0 +1,93 @@
"""Integration test for entity bit-packed fields."""
from __future__ import annotations
import asyncio
from aioesphomeapi import EntityCategory, EntityState
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_host_mode_entity_fields(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test entity bit-packed fields work correctly with all possible values."""
# Write, compile and run the ESPHome device, then connect to API
async with run_compiled(yaml_config), api_client_connected() as client:
# Get all entities
entities = await client.list_entities_services()
# Create a map of entity names to entity info
entity_map = {}
for entity in entities[0]:
if hasattr(entity, "name"):
entity_map[entity.name] = entity
# Test entities that should be visible via API (non-internal)
visible_test_cases = [
# (entity_name, expected_disabled_by_default, expected_entity_category)
("Test Normal Sensor", False, EntityCategory.NONE),
("Test Disabled Sensor", True, EntityCategory.NONE),
("Test Diagnostic Sensor", False, EntityCategory.DIAGNOSTIC),
("Test Switch", True, EntityCategory.CONFIG),
("Test Binary Sensor", False, EntityCategory.CONFIG),
("Test Number", False, EntityCategory.DIAGNOSTIC),
]
# Test entities that should NOT be visible via API (internal)
internal_entities = [
"Test Internal Sensor",
"Test Mixed Flags Sensor",
"Test All Flags Sensor",
"Test Select",
]
# Verify visible entities
for entity_name, expected_disabled, expected_category in visible_test_cases:
assert entity_name in entity_map, (
f"Entity '{entity_name}' not found - it should be visible via API"
)
entity = entity_map[entity_name]
# Check disabled_by_default flag
assert entity.disabled_by_default == expected_disabled, (
f"{entity_name}: disabled_by_default flag mismatch - "
f"expected {expected_disabled}, got {entity.disabled_by_default}"
)
# Check entity_category
assert entity.entity_category == expected_category, (
f"{entity_name}: entity_category mismatch - "
f"expected {expected_category}, got {entity.entity_category}"
)
# Verify internal entities are NOT visible
for entity_name in internal_entities:
assert entity_name not in entity_map, (
f"Entity '{entity_name}' found in API response - "
f"internal entities should not be exposed via API"
)
# Subscribe to states to verify has_state flag works
states: dict[int, EntityState] = {}
state_received = asyncio.Event()
def on_state(state: EntityState) -> None:
states[state.key] = state
state_received.set()
client.subscribe_states(on_state)
# Wait for at least one state
try:
await asyncio.wait_for(state_received.wait(), timeout=5.0)
except asyncio.TimeoutError:
pytest.fail("No states received within 5 seconds")
# Verify we received states (which means has_state flag is working)
assert len(states) > 0, "No states received - has_state flag may not be working"

View File

@ -0,0 +1,152 @@
"""Integration test for fan preset mode behavior."""
from __future__ import annotations
import asyncio
from aioesphomeapi import FanInfo, FanState
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_host_mode_fan_preset(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test fan preset mode behavior according to Home Assistant guidelines."""
# Write, compile and run the ESPHome device, then connect to API
async with run_compiled(yaml_config), api_client_connected() as client:
# Get all fan entities
entities = await client.list_entities_services()
fans: list[FanInfo] = []
for entity_list in entities:
for entity in entity_list:
if isinstance(entity, FanInfo):
fans.append(entity)
# Create a map of fan names to entity info
fan_map = {fan.name: fan for fan in fans}
# Verify we have our test fans
assert "Test Fan with Presets" in fan_map
assert "Test Fan Simple" in fan_map
assert "Test Fan No Speed" in fan_map
# Get fan with presets
fan_presets = fan_map["Test Fan with Presets"]
assert fan_presets.supports_speed is True
assert fan_presets.supported_speed_count == 5
assert fan_presets.supports_oscillation is True
assert fan_presets.supports_direction is True
assert set(fan_presets.supported_preset_modes) == {"Eco", "Sleep", "Turbo"}
# Subscribe to states
states: dict[int, FanState] = {}
state_event = asyncio.Event()
def on_state(state: FanState) -> None:
if isinstance(state, FanState):
states[state.key] = state
state_event.set()
client.subscribe_states(on_state)
# Test 1: Turn on fan without speed or preset - should set speed to 100%
state_event.clear()
client.fan_command(
key=fan_presets.key,
state=True,
)
await asyncio.wait_for(state_event.wait(), timeout=2.0)
fan_state = states[fan_presets.key]
assert fan_state.state is True
assert fan_state.speed_level == 5 # Should be max speed (100%)
assert fan_state.preset_mode == ""
# Turn off
state_event.clear()
client.fan_command(
key=fan_presets.key,
state=False,
)
await asyncio.wait_for(state_event.wait(), timeout=2.0)
# Test 2: Turn on fan with preset mode - should NOT set speed to 100%
state_event.clear()
client.fan_command(
key=fan_presets.key,
state=True,
preset_mode="Eco",
)
await asyncio.wait_for(state_event.wait(), timeout=2.0)
fan_state = states[fan_presets.key]
assert fan_state.state is True
assert fan_state.preset_mode == "Eco"
# Speed should be whatever the preset sets, not forced to 100%
# Test 3: Setting speed should clear preset mode
state_event.clear()
client.fan_command(
key=fan_presets.key,
speed_level=3,
)
await asyncio.wait_for(state_event.wait(), timeout=2.0)
fan_state = states[fan_presets.key]
assert fan_state.state is True
assert fan_state.speed_level == 3
assert fan_state.preset_mode == "" # Preset mode should be cleared
# Test 4: Setting preset mode should work when fan is already on
state_event.clear()
client.fan_command(
key=fan_presets.key,
preset_mode="Sleep",
)
await asyncio.wait_for(state_event.wait(), timeout=2.0)
fan_state = states[fan_presets.key]
assert fan_state.state is True
assert fan_state.preset_mode == "Sleep"
# Turn off
state_event.clear()
client.fan_command(
key=fan_presets.key,
state=False,
)
await asyncio.wait_for(state_event.wait(), timeout=2.0)
# Test 5: Turn on fan with specific speed
state_event.clear()
client.fan_command(
key=fan_presets.key,
state=True,
speed_level=2,
)
await asyncio.wait_for(state_event.wait(), timeout=2.0)
fan_state = states[fan_presets.key]
assert fan_state.state is True
assert fan_state.speed_level == 2
assert fan_state.preset_mode == ""
# Test 6: Test fan with no speed support
fan_no_speed = fan_map["Test Fan No Speed"]
assert fan_no_speed.supports_speed is False
state_event.clear()
client.fan_command(
key=fan_no_speed.key,
state=True,
)
await asyncio.wait_for(state_event.wait(), timeout=2.0)
fan_state = states[fan_no_speed.key]
assert fan_state.state is True
# No speed should be set for fans that don't support speed

View File

@ -0,0 +1,59 @@
"""Integration test for API handling of large messages exceeding batch size."""
from __future__ import annotations
from aioesphomeapi import SelectInfo
import pytest
from .types import APIClientConnectedFactory, RunCompiledFunction
@pytest.mark.asyncio
async def test_large_message_batching(
yaml_config: str,
run_compiled: RunCompiledFunction,
api_client_connected: APIClientConnectedFactory,
) -> None:
"""Test API can handle large messages (>1390 bytes) in batches."""
# Write, compile and run the ESPHome device, then connect to API
async with run_compiled(yaml_config), api_client_connected() as client:
# Verify we can get device info
device_info = await client.device_info()
assert device_info is not None
assert device_info.name == "large-message-test"
# List entities - this will include our select with many options
entity_info, services = await client.list_entities_services()
# Find our large select entity
large_select = None
for entity in entity_info:
if isinstance(entity, SelectInfo) and entity.object_id == "large_select":
large_select = entity
break
assert large_select is not None, "Could not find large_select entity"
# Verify the select has all its options
# We created 100 options with long names
assert len(large_select.options) == 100, (
f"Expected 100 options, got {len(large_select.options)}"
)
# Verify all options are present and correct
for i in range(100):
expected_option = f"Option {i:03d} - This is a very long option name to make the message larger than the typical batch size of 1390 bytes"
assert expected_option in large_select.options, (
f"Missing option: {expected_option}"
)
# Also verify we can still receive other entities in the same batch
# Count total entities - should have at least our select plus some sensors
entity_count = len(entity_info)
assert entity_count >= 4, f"Expected at least 4 entities, got {entity_count}"
# Verify we have different entity types (not just selects)
entity_types = {type(entity).__name__ for entity in entity_info}
assert len(entity_types) >= 2, (
f"Expected multiple entity types, got {entity_types}"
)