1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-02 16:11:53 +00:00

Compare commits

..

46 Commits

Author SHA1 Message Date
Jesse Hills
fa34adbf6c Merge pull request #9185 from esphome/bump-2025.6.1
2025.6.1
2025-06-24 07:27:59 +12:00
Jesse Hills
22e360d479 Bump version to 2025.6.1 2025-06-23 23:32:22 +12:00
myhomeiot
649936200e Restore access to BLEScanResult as get_scan_result (#9148) 2025-06-23 23:32:22 +12:00
rwrozelle
5d6e690c12 Fixes for setup of OpenThread either using TLV or entering Credentials directly (#9157)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2025-06-23 23:32:22 +12:00
Jesse Hills
2f2ecadae7 [config validation] Add more ip address / network validators (#9181) 2025-06-23 23:32:22 +12:00
J. Nick Koston
6dfb9eba61 Fix missing BLE GAP events causing RSSI sensor and beacon failures (#9138) 2025-06-23 23:32:22 +12:00
Edward Firmo
24587fe875 [nextion] Fix command spacing double timing and response blocking issues (#9134) 2025-06-23 23:32:22 +12:00
J. Nick Koston
a1aebe6a2c Eliminate memory fragmentation with BLE event pool (#9101) 2025-06-23 23:32:22 +12:00
Jesse Hills
68f5144084 Merge pull request #9126 from esphome/bump-2025.6.0
2025.6.0
2025-06-18 21:41:00 +12:00
Michael Hansen
da5cf99549 Add intent progress event to voice assistant enum (#9103) 2025-06-18 15:15:37 +12:00
Jesse Hills
849c858495 Bump version to 2025.6.0 2025-06-18 14:16:24 +12:00
Jesse Hills
16a0f9db97 Merge pull request #9122 from esphome/bump-2025.6.0b3
2025.6.0b3
2025-06-18 12:37:25 +12:00
Jesse Hills
5269523ca1 Bump version to 2025.6.0b3 2025-06-18 10:17:56 +12:00
J. Nick Koston
89267b9e06 Reduce Switch component memory usage by 8 bytes per instance (#9112) 2025-06-18 10:09:11 +12:00
J. Nick Koston
4bc9646e8f Optimize LightState memory layout (#9113) 2025-06-18 10:09:11 +12:00
Clyde Stubbs
fd83628c49 [spi] Cater for non-word-aligned buffers on esp8266 (#9108) 2025-06-18 10:09:11 +12:00
Kevin Ahrendt
62abfbec9e [i2s_audio] Bugfix: crashes when unlocking i2s bus multiple times (#9100) 2025-06-18 10:09:11 +12:00
Keith Burzinski
7cc0008837 [i2s_audio] Add `dump_config` methods, shorten log messages (#9099) 2025-06-18 10:09:11 +12:00
Jesse Hills
426be153db Merge pull request #9094 from esphome/bump-2025.6.0b2
2025.6.0b2
2025-06-16 17:06:59 +12:00
Jesse Hills
2a81efda0b Remove `std::` prefix as not all platforms have access yet. (#9095) 2025-06-16 12:55:51 +12:00
dependabot[bot]
6bad276589 Bump aioesphomeapi from 32.2.1 to 32.2.3 (#9091)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-16 11:45:58 +12:00
Jesse Hills
47d8048a62 Bump version to 2025.6.0b2 2025-06-16 10:07:07 +12:00
J. Nick Koston
20d7ba5d7c Reduce Component blocking threshold memory usage by 2 bytes per component (#9081) 2025-06-16 10:07:07 +12:00
J. Nick Koston
e435e72654 Add common base classes for entity protobuf messages to reduce duplicate code (#9090) 2025-06-16 10:07:07 +12:00
J. Nick Koston
497d66f7ec Ensure we can send batches where the first message exceeds MAX_PACKET_SIZE (#9068) 2025-06-16 10:07:07 +12:00
Kevin Ahrendt
242b02a416 [i2s_audio] Check for a nullptr before disabling and deleting channel (#9062) 2025-06-16 10:07:07 +12:00
J. Nick Koston
9644a6bb9c Fix protobuf encoding size mismatch by passing force parameter in encode_string (#9074) 2025-06-16 10:07:07 +12:00
J. Nick Koston
70d66062d6 Make BLE queue lock free (#9088) 2025-06-16 10:07:07 +12:00
J. Nick Koston
39f6f9b0dc Implement a lock free ring buffer for BLEScanResult to avoid drops (#9087) 2025-06-16 10:07:07 +12:00
dhewg
0454dd4e07 [fan] fix initial FanCall to properly set speed (#8277) 2025-06-16 10:07:07 +12:00
J. Nick Koston
6f4e76c8f3 Fix unbound BLE event queue growth and reduce memory usage (#9052) 2025-06-16 10:07:07 +12:00
J. Nick Koston
5cdcf2415d Optimize Application area_ from std::string to const char* (#9085) 2025-06-16 10:07:07 +12:00
J. Nick Koston
1719a2e08b Fix API message encoding to return actual size instead of calculated size (#9073) 2025-06-16 10:07:07 +12:00
J. Nick Koston
5640a9fe73 Optimize memory usage by lazy-allocating raw callbacks in sensors (#9077) 2025-06-16 10:07:07 +12:00
J. Nick Koston
4787e22f61 Reduce entity memory usage by eliminating field shadowing and bit-packing (#9076) 2025-06-16 10:07:01 +12:00
J. Nick Koston
fb12e4e66a Small optimizations to api buffer helper (#9071) 2025-06-16 09:49:45 +12:00
J. Nick Koston
77740a1044 Optimize Component and Application state storage from uint32_t to uint8_t (#9082) 2025-06-16 09:49:45 +12:00
J. Nick Koston
1fdfe7578f Make ParseOnOffState enum uint8_t (#9083) 2025-06-16 09:49:45 +12:00
J. Nick Koston
ebecf7047e Fix captive_portal loading entire web_server (#9066) 2025-06-16 09:49:45 +12:00
Jesse Hills
00e8332bf5 [esp32] Dynamically set default framework based on variant (#9060) 2025-06-16 09:49:45 +12:00
Jesse Hills
5fc1f90822 [prometheus] Remove `cv.only_with_arduino` (#9061) 2025-06-16 09:49:45 +12:00
J. Nick Koston
0a1be3d19c Fix misleading comment in API (#9069) 2025-06-16 09:49:45 +12:00
Nate Clark
40db3146b9 Fix BYPASS_AUTO feature to work with or without an arming delay (#9051) 2025-06-16 09:49:45 +12:00
Edward Firmo
535c495b33 [nextion] Remove upload flags reset from success path to prevent TFT corruption (#9064) 2025-06-16 09:49:45 +12:00
J. Nick Koston
592446e430 Always perform select() when loop duration exceeds interval (#9058) 2025-06-16 09:49:45 +12:00
J. Nick Koston
7a5c9a821a Fix dashboard logging being escaped before parser (#9054) 2025-06-16 09:49:45 +12:00
129 changed files with 2955 additions and 1150 deletions

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2025.7.0-dev
PROJECT_NUMBER = 2025.6.1
# 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

View File

@@ -266,6 +266,7 @@ enum EntityCategory {
// ==================== BINARY SENSOR ====================
message ListEntitiesBinarySensorResponse {
option (id) = 12;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BINARY_SENSOR";
@@ -282,6 +283,7 @@ message ListEntitiesBinarySensorResponse {
}
message BinarySensorStateResponse {
option (id) = 21;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BINARY_SENSOR";
option (no_delay) = true;
@@ -296,6 +298,7 @@ message BinarySensorStateResponse {
// ==================== COVER ====================
message ListEntitiesCoverResponse {
option (id) = 13;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_COVER";
@@ -325,6 +328,7 @@ enum CoverOperation {
}
message CoverStateResponse {
option (id) = 22;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_COVER";
option (no_delay) = true;
@@ -367,6 +371,7 @@ message CoverCommandRequest {
// ==================== FAN ====================
message ListEntitiesFanResponse {
option (id) = 14;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_FAN";
@@ -395,6 +400,7 @@ enum FanDirection {
}
message FanStateResponse {
option (id) = 23;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_FAN";
option (no_delay) = true;
@@ -444,6 +450,7 @@ enum ColorMode {
}
message ListEntitiesLightResponse {
option (id) = 15;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LIGHT";
@@ -467,6 +474,7 @@ message ListEntitiesLightResponse {
}
message LightStateResponse {
option (id) = 24;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LIGHT";
option (no_delay) = true;
@@ -536,6 +544,7 @@ enum SensorLastResetType {
message ListEntitiesSensorResponse {
option (id) = 16;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SENSOR";
@@ -557,6 +566,7 @@ message ListEntitiesSensorResponse {
}
message SensorStateResponse {
option (id) = 25;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SENSOR";
option (no_delay) = true;
@@ -571,6 +581,7 @@ message SensorStateResponse {
// ==================== SWITCH ====================
message ListEntitiesSwitchResponse {
option (id) = 17;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SWITCH";
@@ -587,6 +598,7 @@ message ListEntitiesSwitchResponse {
}
message SwitchStateResponse {
option (id) = 26;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SWITCH";
option (no_delay) = true;
@@ -607,6 +619,7 @@ message SwitchCommandRequest {
// ==================== TEXT SENSOR ====================
message ListEntitiesTextSensorResponse {
option (id) = 18;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT_SENSOR";
@@ -622,6 +635,7 @@ message ListEntitiesTextSensorResponse {
}
message TextSensorStateResponse {
option (id) = 27;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT_SENSOR";
option (no_delay) = true;
@@ -789,6 +803,7 @@ message ExecuteServiceRequest {
// ==================== CAMERA ====================
message ListEntitiesCameraResponse {
option (id) = 43;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ESP32_CAMERA";
@@ -869,6 +884,7 @@ enum ClimatePreset {
}
message ListEntitiesClimateResponse {
option (id) = 46;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CLIMATE";
@@ -903,6 +919,7 @@ message ListEntitiesClimateResponse {
}
message ClimateStateResponse {
option (id) = 47;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_CLIMATE";
option (no_delay) = true;
@@ -964,6 +981,7 @@ enum NumberMode {
}
message ListEntitiesNumberResponse {
option (id) = 49;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_NUMBER";
@@ -984,6 +1002,7 @@ message ListEntitiesNumberResponse {
}
message NumberStateResponse {
option (id) = 50;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_NUMBER";
option (no_delay) = true;
@@ -1007,6 +1026,7 @@ message NumberCommandRequest {
// ==================== SELECT ====================
message ListEntitiesSelectResponse {
option (id) = 52;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SELECT";
@@ -1022,6 +1042,7 @@ message ListEntitiesSelectResponse {
}
message SelectStateResponse {
option (id) = 53;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SELECT";
option (no_delay) = true;
@@ -1045,6 +1066,7 @@ message SelectCommandRequest {
// ==================== SIREN ====================
message ListEntitiesSirenResponse {
option (id) = 55;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN";
@@ -1062,6 +1084,7 @@ message ListEntitiesSirenResponse {
}
message SirenStateResponse {
option (id) = 56;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_SIREN";
option (no_delay) = true;
@@ -1102,6 +1125,7 @@ enum LockCommand {
}
message ListEntitiesLockResponse {
option (id) = 58;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK";
@@ -1123,6 +1147,7 @@ message ListEntitiesLockResponse {
}
message LockStateResponse {
option (id) = 59;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_LOCK";
option (no_delay) = true;
@@ -1145,6 +1170,7 @@ message LockCommandRequest {
// ==================== BUTTON ====================
message ListEntitiesButtonResponse {
option (id) = 61;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_BUTTON";
@@ -1196,6 +1222,7 @@ message MediaPlayerSupportedFormat {
}
message ListEntitiesMediaPlayerResponse {
option (id) = 63;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
@@ -1214,6 +1241,7 @@ message ListEntitiesMediaPlayerResponse {
}
message MediaPlayerStateResponse {
option (id) = 64;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_MEDIA_PLAYER";
option (no_delay) = true;
@@ -1615,6 +1643,7 @@ enum VoiceAssistantEvent {
VOICE_ASSISTANT_STT_VAD_END = 12;
VOICE_ASSISTANT_TTS_STREAM_START = 98;
VOICE_ASSISTANT_TTS_STREAM_END = 99;
VOICE_ASSISTANT_INTENT_PROGRESS = 100;
}
message VoiceAssistantEventData {
@@ -1735,6 +1764,7 @@ enum AlarmControlPanelStateCommand {
message ListEntitiesAlarmControlPanelResponse {
option (id) = 94;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
@@ -1752,6 +1782,7 @@ message ListEntitiesAlarmControlPanelResponse {
message AlarmControlPanelStateResponse {
option (id) = 95;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_ALARM_CONTROL_PANEL";
option (no_delay) = true;
@@ -1776,6 +1807,7 @@ enum TextMode {
}
message ListEntitiesTextResponse {
option (id) = 97;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT";
@@ -1794,6 +1826,7 @@ message ListEntitiesTextResponse {
}
message TextStateResponse {
option (id) = 98;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_TEXT";
option (no_delay) = true;
@@ -1818,6 +1851,7 @@ message TextCommandRequest {
// ==================== DATETIME DATE ====================
message ListEntitiesDateResponse {
option (id) = 100;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATE";
@@ -1832,6 +1866,7 @@ message ListEntitiesDateResponse {
}
message DateStateResponse {
option (id) = 101;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATE";
option (no_delay) = true;
@@ -1859,6 +1894,7 @@ message DateCommandRequest {
// ==================== DATETIME TIME ====================
message ListEntitiesTimeResponse {
option (id) = 103;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME";
@@ -1873,6 +1909,7 @@ message ListEntitiesTimeResponse {
}
message TimeStateResponse {
option (id) = 104;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_TIME";
option (no_delay) = true;
@@ -1900,6 +1937,7 @@ message TimeCommandRequest {
// ==================== EVENT ====================
message ListEntitiesEventResponse {
option (id) = 107;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT";
@@ -1917,6 +1955,7 @@ message ListEntitiesEventResponse {
}
message EventResponse {
option (id) = 108;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_EVENT";
@@ -1927,6 +1966,7 @@ message EventResponse {
// ==================== VALVE ====================
message ListEntitiesValveResponse {
option (id) = 109;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VALVE";
@@ -1952,6 +1992,7 @@ enum ValveOperation {
}
message ValveStateResponse {
option (id) = 110;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_VALVE";
option (no_delay) = true;
@@ -1976,6 +2017,7 @@ message ValveCommandRequest {
// ==================== DATETIME DATETIME ====================
message ListEntitiesDateTimeResponse {
option (id) = 112;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME";
@@ -1990,6 +2032,7 @@ message ListEntitiesDateTimeResponse {
}
message DateTimeStateResponse {
option (id) = 113;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_DATETIME_DATETIME";
option (no_delay) = true;
@@ -2013,6 +2056,7 @@ message DateTimeCommandRequest {
// ==================== UPDATE ====================
message ListEntitiesUpdateResponse {
option (id) = 116;
option (base_class) = "InfoResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE";
@@ -2028,6 +2072,7 @@ message ListEntitiesUpdateResponse {
}
message UpdateStateResponse {
option (id) = 117;
option (base_class) = "StateResponseProtoMessage";
option (source) = SOURCE_SERVER;
option (ifdef) = "USE_UPDATE";
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,
uint32_t remaining_size, bool is_single) {
// Calculate size
uint32_t size = 0;
msg.calculate_size(size);
uint32_t calculated_size = 0;
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
uint16_t total_size =
static_cast<uint16_t>(size) + conn->helper_->frame_header_padding() + conn->helper_->frame_footer_size();
size_t total_calculated_size = calculated_size + header_padding + footer_size;
// Check if it fits
if (total_size > remaining_size) {
if (total_calculated_size > remaining_size) {
return 0; // Doesn't fit
}
// Allocate exact buffer space needed (just the payload, not the overhead)
ProtoWriteBuffer buffer =
is_single ? conn->allocate_single_message_buffer(size) : conn->allocate_batch_message_buffer(size);
// Allocate buffer space - pass payload size, allocation functions add header/footer space
ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_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
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
@@ -285,7 +301,7 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn
BinarySensorStateResponse resp;
resp.state = binary_sensor->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);
}
@@ -319,7 +335,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *
if (traits.get_supports_tilt())
msg.tilt = cover->tilt;
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);
}
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);
if (traits.supports_preset_modes())
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);
}
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();
if (light->supports_effects())
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);
}
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;
resp.state = sensor->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);
}
@@ -570,7 +586,7 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection
auto *a_switch = static_cast<switch_::Switch *>(entity);
SwitchStateResponse resp;
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);
}
@@ -613,7 +629,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec
TextSensorStateResponse resp;
resp.state = text_sensor->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);
}
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) {
auto *climate = static_cast<climate::Climate *>(entity);
ClimateStateResponse resp;
resp.key = climate->get_object_id_hash();
fill_entity_state_base(climate, resp);
auto traits = climate->get_traits();
resp.mode = static_cast<enums::ClimateMode>(climate->mode);
resp.action = static_cast<enums::ClimateAction>(climate->action);
@@ -746,7 +762,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection
NumberStateResponse resp;
resp.state = number->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);
}
@@ -787,7 +803,7 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c
resp.year = date->year;
resp.month = date->month;
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);
}
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.minute = time->minute;
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);
}
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();
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);
}
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;
resp.state = text->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);
}
@@ -943,7 +959,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
SelectStateResponse resp;
resp.state = select->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);
}
@@ -1003,7 +1019,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c
auto *a_lock = static_cast<lock::Lock *>(entity);
LockStateResponse resp;
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);
}
@@ -1047,7 +1063,7 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *
ValveStateResponse resp;
resp.position = valve->position;
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);
}
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.volume = media_player->volume;
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);
}
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);
AlarmControlPanelStateResponse resp;
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);
}
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) {
EventResponse resp;
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);
}
@@ -1461,7 +1477,7 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection
resp.release_summary = update->update_info.summary;
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);
}
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
// SubscribeLogsResponse - 29
return this->send_buffer(buffer, 29);
return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE);
}
HelloResponse APIConnection::hello(const HelloRequest &msg) {
@@ -1669,7 +1685,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
return false;
}
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;
}
@@ -1791,7 +1807,7 @@ void APIConnection::process_batch_() {
this->batch_first_message_ = true;
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
// 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);
// 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;
// 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_offset = this->parent_->get_shared_buffer_ref().size() + footer_size;
items_processed++;
}
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)
// - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext)
shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size());
// Insert header padding bytes so message encoding starts at the correct position
shared_buf.insert(shared_buf.begin(), header_padding, 0);
// Resize to add header padding so message encoding starts at the correct position
shared_buf.resize(header_padding);
return {&shared_buf};
}
@@ -249,32 +249,26 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) {
// Get reference to shared buffer (it maintains state between batch messages)
std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
size_t current_size = shared_buf.size();
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.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};
}
@@ -288,8 +282,8 @@ class APIConnection : public APIServerConnection {
ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size);
protected:
// Helper function to fill common entity fields
template<typename ResponseT> static void fill_entity_info_base(esphome::EntityBase *entity, ResponseT &response) {
// Helper function to fill common entity info fields
static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) {
// Set common fields that are shared by all entity types
response.key = entity->get_object_id_hash();
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());
}
// 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
static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);

View File

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

View File

@@ -516,6 +516,8 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V
return "VOICE_ASSISTANT_TTS_STREAM_START";
case enums::VOICE_ASSISTANT_TTS_STREAM_END:
return "VOICE_ASSISTANT_TTS_STREAM_END";
case enums::VOICE_ASSISTANT_INTENT_PROGRESS:
return "VOICE_ASSISTANT_INTENT_PROGRESS";
default:
return "UNKNOWN";
}
@@ -628,6 +630,7 @@ template<> const char *proto_enum_to_string<enums::UpdateCommand>(enums::UpdateC
}
}
#endif
bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 2: {

View File

@@ -208,6 +208,7 @@ enum VoiceAssistantEvent : uint32_t {
VOICE_ASSISTANT_STT_VAD_END = 12,
VOICE_ASSISTANT_TTS_STREAM_START = 98,
VOICE_ASSISTANT_TTS_STREAM_END = 99,
VOICE_ASSISTANT_INTENT_PROGRESS = 100,
};
enum VoiceAssistantTimerEvent : uint32_t {
VOICE_ASSISTANT_TIMER_STARTED = 0,
@@ -253,6 +254,27 @@ enum UpdateCommand : uint32_t {
} // 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 {
public:
static constexpr uint16_t MESSAGE_TYPE = 1;
@@ -484,22 +506,15 @@ class SubscribeStatesRequest : public ProtoMessage {
protected:
};
class ListEntitiesBinarySensorResponse : public ProtoMessage {
class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 12;
static constexpr uint16_t ESTIMATED_SIZE = 56;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; }
#endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string device_class{};
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 calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -511,14 +526,13 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class BinarySensorStateResponse : public ProtoMessage {
class BinarySensorStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 21;
static constexpr uint16_t ESTIMATED_SIZE = 9;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "binary_sensor_state_response"; }
#endif
uint32_t key{0};
bool state{false};
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
@@ -531,24 +545,17 @@ class BinarySensorStateResponse : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesCoverResponse : public ProtoMessage {
class ListEntitiesCoverResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 13;
static constexpr uint16_t ESTIMATED_SIZE = 62;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_cover_response"; }
#endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
bool assumed_state{false};
bool supports_position{false};
bool supports_tilt{false};
std::string device_class{};
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
bool supports_stop{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -561,14 +568,13 @@ class ListEntitiesCoverResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class CoverStateResponse : public ProtoMessage {
class CoverStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 22;
static constexpr uint16_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "cover_state_response"; }
#endif
uint32_t key{0};
enums::LegacyCoverState legacy_state{};
float position{0.0f};
float tilt{0.0f};
@@ -608,24 +614,17 @@ class CoverCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesFanResponse : public ProtoMessage {
class ListEntitiesFanResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 14;
static constexpr uint16_t ESTIMATED_SIZE = 73;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_fan_response"; }
#endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
bool supports_oscillation{false};
bool supports_speed{false};
bool supports_direction{false};
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{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -638,14 +637,13 @@ class ListEntitiesFanResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class FanStateResponse : public ProtoMessage {
class FanStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 23;
static constexpr uint16_t ESTIMATED_SIZE = 26;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "fan_state_response"; }
#endif
uint32_t key{0};
bool state{false};
bool oscillating{false};
enums::FanSpeed speed{};
@@ -694,17 +692,13 @@ class FanCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesLightResponse : public ProtoMessage {
class ListEntitiesLightResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 15;
static constexpr uint16_t ESTIMATED_SIZE = 85;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_light_response"; }
#endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::vector<enums::ColorMode> supported_color_modes{};
bool legacy_supports_brightness{false};
bool legacy_supports_rgb{false};
@@ -713,9 +707,6 @@ class ListEntitiesLightResponse : public ProtoMessage {
float min_mireds{0.0f};
float max_mireds{0.0f};
std::vector<std::string> effects{};
bool disabled_by_default{false};
std::string icon{};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -727,14 +718,13 @@ class ListEntitiesLightResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class LightStateResponse : public ProtoMessage {
class LightStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 24;
static constexpr uint16_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "light_state_response"; }
#endif
uint32_t key{0};
bool state{false};
float brightness{0.0f};
enums::ColorMode color_mode{};
@@ -803,26 +793,19 @@ class LightCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesSensorResponse : public ProtoMessage {
class ListEntitiesSensorResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 16;
static constexpr uint16_t ESTIMATED_SIZE = 73;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_sensor_response"; }
#endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
std::string unit_of_measurement{};
int32_t accuracy_decimals{0};
bool force_update{false};
std::string device_class{};
enums::SensorStateClass state_class{};
enums::SensorLastResetType legacy_last_reset_type{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -834,14 +817,13 @@ class ListEntitiesSensorResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SensorStateResponse : public ProtoMessage {
class SensorStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 25;
static constexpr uint16_t ESTIMATED_SIZE = 12;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "sensor_state_response"; }
#endif
uint32_t key{0};
float state{0.0f};
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
@@ -854,21 +836,14 @@ class SensorStateResponse : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesSwitchResponse : public ProtoMessage {
class ListEntitiesSwitchResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 17;
static constexpr uint16_t ESTIMATED_SIZE = 56;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_switch_response"; }
#endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
bool assumed_state{false};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string device_class{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -881,14 +856,13 @@ class ListEntitiesSwitchResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SwitchStateResponse : public ProtoMessage {
class SwitchStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 26;
static constexpr uint16_t ESTIMATED_SIZE = 7;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "switch_state_response"; }
#endif
uint32_t key{0};
bool state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -919,20 +893,13 @@ class SwitchCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesTextSensorResponse : public ProtoMessage {
class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 18;
static constexpr uint16_t ESTIMATED_SIZE = 54;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_text_sensor_response"; }
#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{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -945,14 +912,13 @@ class ListEntitiesTextSensorResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class TextSensorStateResponse : public ProtoMessage {
class TextSensorStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 27;
static constexpr uint16_t ESTIMATED_SIZE = 16;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "text_sensor_state_response"; }
#endif
uint32_t key{0};
std::string state{};
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
@@ -1249,20 +1215,13 @@ class ExecuteServiceRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ListEntitiesCameraResponse : public ProtoMessage {
class ListEntitiesCameraResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 43;
static constexpr uint16_t ESTIMATED_SIZE = 45;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_camera_response"; }
#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 calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1313,17 +1272,13 @@ class CameraImageRequest : public ProtoMessage {
protected:
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesClimateResponse : public ProtoMessage {
class ListEntitiesClimateResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 46;
static constexpr uint16_t ESTIMATED_SIZE = 151;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_climate_response"; }
#endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
bool supports_current_temperature{false};
bool supports_two_point_target_temperature{false};
std::vector<enums::ClimateMode> supported_modes{};
@@ -1337,9 +1292,6 @@ class ListEntitiesClimateResponse : public ProtoMessage {
std::vector<std::string> supported_custom_fan_modes{};
std::vector<enums::ClimatePreset> supported_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};
bool supports_current_humidity{false};
bool supports_target_humidity{false};
@@ -1356,14 +1308,13 @@ class ListEntitiesClimateResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ClimateStateResponse : public ProtoMessage {
class ClimateStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 47;
static constexpr uint16_t ESTIMATED_SIZE = 65;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "climate_state_response"; }
#endif
uint32_t key{0};
enums::ClimateMode mode{};
float current_temperature{0.0f};
float target_temperature{0.0f};
@@ -1430,23 +1381,16 @@ class ClimateCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesNumberResponse : public ProtoMessage {
class ListEntitiesNumberResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 49;
static constexpr uint16_t ESTIMATED_SIZE = 80;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_number_response"; }
#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 max_value{0.0f};
float step{0.0f};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
std::string unit_of_measurement{};
enums::NumberMode mode{};
std::string device_class{};
@@ -1461,14 +1405,13 @@ class ListEntitiesNumberResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class NumberStateResponse : public ProtoMessage {
class NumberStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 50;
static constexpr uint16_t ESTIMATED_SIZE = 12;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "number_state_response"; }
#endif
uint32_t key{0};
float state{0.0f};
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
@@ -1499,21 +1442,14 @@ class NumberCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
class ListEntitiesSelectResponse : public ProtoMessage {
class ListEntitiesSelectResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 52;
static constexpr uint16_t ESTIMATED_SIZE = 63;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_select_response"; }
#endif
std::string object_id{};
uint32_t key{0};
std::string name{};
std::string unique_id{};
std::string icon{};
std::vector<std::string> options{};
bool disabled_by_default{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1525,14 +1461,13 @@ class ListEntitiesSelectResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SelectStateResponse : public ProtoMessage {
class SelectStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 53;
static constexpr uint16_t ESTIMATED_SIZE = 16;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "select_state_response"; }
#endif
uint32_t key{0};
std::string state{};
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
@@ -1565,23 +1500,16 @@ class SelectCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ListEntitiesSirenResponse : public ProtoMessage {
class ListEntitiesSirenResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 55;
static constexpr uint16_t ESTIMATED_SIZE = 67;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_siren_response"; }
#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{};
bool supports_duration{false};
bool supports_volume{false};
enums::EntityCategory entity_category{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1593,14 +1521,13 @@ class ListEntitiesSirenResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class SirenStateResponse : public ProtoMessage {
class SirenStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 56;
static constexpr uint16_t ESTIMATED_SIZE = 7;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "siren_state_response"; }
#endif
uint32_t key{0};
bool state{false};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -1639,20 +1566,13 @@ class SirenCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesLockResponse : public ProtoMessage {
class ListEntitiesLockResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 58;
static constexpr uint16_t ESTIMATED_SIZE = 60;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_lock_response"; }
#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 supports_open{false};
bool requires_code{false};
@@ -1668,14 +1588,13 @@ class ListEntitiesLockResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class LockStateResponse : public ProtoMessage {
class LockStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 59;
static constexpr uint16_t ESTIMATED_SIZE = 7;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "lock_state_response"; }
#endif
uint32_t key{0};
enums::LockState state{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -1709,20 +1628,13 @@ class LockCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesButtonResponse : public ProtoMessage {
class ListEntitiesButtonResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 61;
static constexpr uint16_t ESTIMATED_SIZE = 54;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_button_response"; }
#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{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -1769,20 +1681,13 @@ class MediaPlayerSupportedFormat : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesMediaPlayerResponse : public ProtoMessage {
class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 63;
static constexpr uint16_t ESTIMATED_SIZE = 81;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_media_player_response"; }
#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};
std::vector<MediaPlayerSupportedFormat> supported_formats{};
void encode(ProtoWriteBuffer buffer) const override;
@@ -1796,14 +1701,13 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class MediaPlayerStateResponse : public ProtoMessage {
class MediaPlayerStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 64;
static constexpr uint16_t ESTIMATED_SIZE = 14;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "media_player_state_response"; }
#endif
uint32_t key{0};
enums::MediaPlayerState state{};
float volume{0.0f};
bool muted{false};
@@ -2653,20 +2557,13 @@ class VoiceAssistantSetConfiguration : public ProtoMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 94;
static constexpr uint16_t ESTIMATED_SIZE = 53;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; }
#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};
bool requires_code{false};
bool requires_code_to_arm{false};
@@ -2681,14 +2578,13 @@ class ListEntitiesAlarmControlPanelResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class AlarmControlPanelStateResponse : public ProtoMessage {
class AlarmControlPanelStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 95;
static constexpr uint16_t ESTIMATED_SIZE = 7;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "alarm_control_panel_state_response"; }
#endif
uint32_t key{0};
enums::AlarmControlPanelState state{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -2721,20 +2617,13 @@ class AlarmControlPanelCommandRequest : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesTextResponse : public ProtoMessage {
class ListEntitiesTextResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 97;
static constexpr uint16_t ESTIMATED_SIZE = 64;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_text_response"; }
#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 max_length{0};
std::string pattern{};
@@ -2750,14 +2639,13 @@ class ListEntitiesTextResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class TextStateResponse : public ProtoMessage {
class TextStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 98;
static constexpr uint16_t ESTIMATED_SIZE = 16;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "text_state_response"; }
#endif
uint32_t key{0};
std::string state{};
bool missing_state{false};
void encode(ProtoWriteBuffer buffer) const override;
@@ -2790,20 +2678,13 @@ class TextCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ListEntitiesDateResponse : public ProtoMessage {
class ListEntitiesDateResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 100;
static constexpr uint16_t ESTIMATED_SIZE = 45;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_date_response"; }
#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 calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -2815,14 +2696,13 @@ class ListEntitiesDateResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class DateStateResponse : public ProtoMessage {
class DateStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 101;
static constexpr uint16_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "date_state_response"; }
#endif
uint32_t key{0};
bool missing_state{false};
uint32_t year{0};
uint32_t month{0};
@@ -2858,20 +2738,13 @@ class DateCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesTimeResponse : public ProtoMessage {
class ListEntitiesTimeResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 103;
static constexpr uint16_t ESTIMATED_SIZE = 45;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_time_response"; }
#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 calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -2883,14 +2756,13 @@ class ListEntitiesTimeResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class TimeStateResponse : public ProtoMessage {
class TimeStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 104;
static constexpr uint16_t ESTIMATED_SIZE = 19;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "time_state_response"; }
#endif
uint32_t key{0};
bool missing_state{false};
uint32_t hour{0};
uint32_t minute{0};
@@ -2926,20 +2798,13 @@ class TimeCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesEventResponse : public ProtoMessage {
class ListEntitiesEventResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 107;
static constexpr uint16_t ESTIMATED_SIZE = 72;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_event_response"; }
#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::vector<std::string> event_types{};
void encode(ProtoWriteBuffer buffer) const override;
@@ -2953,14 +2818,13 @@ class ListEntitiesEventResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class EventResponse : public ProtoMessage {
class EventResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 108;
static constexpr uint16_t ESTIMATED_SIZE = 14;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "event_response"; }
#endif
uint32_t key{0};
std::string event_type{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -2972,20 +2836,13 @@ class EventResponse : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
};
class ListEntitiesValveResponse : public ProtoMessage {
class ListEntitiesValveResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 109;
static constexpr uint16_t ESTIMATED_SIZE = 60;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_valve_response"; }
#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{};
bool assumed_state{false};
bool supports_position{false};
@@ -3001,14 +2858,13 @@ class ListEntitiesValveResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ValveStateResponse : public ProtoMessage {
class ValveStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 110;
static constexpr uint16_t ESTIMATED_SIZE = 12;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "valve_state_response"; }
#endif
uint32_t key{0};
float position{0.0f};
enums::ValveOperation current_operation{};
void encode(ProtoWriteBuffer buffer) const override;
@@ -3042,20 +2898,13 @@ class ValveCommandRequest : public ProtoMessage {
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class ListEntitiesDateTimeResponse : public ProtoMessage {
class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 112;
static constexpr uint16_t ESTIMATED_SIZE = 45;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_date_time_response"; }
#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 calculate_size(uint32_t &total_size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -3067,14 +2916,13 @@ class ListEntitiesDateTimeResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class DateTimeStateResponse : public ProtoMessage {
class DateTimeStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 113;
static constexpr uint16_t ESTIMATED_SIZE = 12;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "date_time_state_response"; }
#endif
uint32_t key{0};
bool missing_state{false};
uint32_t epoch_seconds{0};
void encode(ProtoWriteBuffer buffer) const override;
@@ -3105,20 +2953,13 @@ class DateTimeCommandRequest : public ProtoMessage {
protected:
bool decode_32bit(uint32_t field_id, Proto32Bit value) override;
};
class ListEntitiesUpdateResponse : public ProtoMessage {
class ListEntitiesUpdateResponse : public InfoResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 116;
static constexpr uint16_t ESTIMATED_SIZE = 54;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "list_entities_update_response"; }
#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{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(uint32_t &total_size) const override;
@@ -3131,14 +2972,13 @@ class ListEntitiesUpdateResponse : public ProtoMessage {
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class UpdateStateResponse : public ProtoMessage {
class UpdateStateResponse : public StateResponseProtoMessage {
public:
static constexpr uint16_t MESSAGE_TYPE = 117;
static constexpr uint16_t ESTIMATED_SIZE = 61;
#ifdef HAS_PROTO_MESSAGE_DUMP
static constexpr const char *message_name() { return "update_state_response"; }
#endif
uint32_t key{0};
bool missing_state{false};
bool in_progress{false};
bool has_progress{false};

View File

@@ -227,7 +227,7 @@ bool APIServer::check_password(const std::string &password) const {
void APIServer::handle_disconnect(APIConnection *conn) {}
#ifdef USE_BINARY_SENSOR
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
void APIServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
if (obj->is_internal())
return;
for (auto &c : this->clients_)

View File

@@ -54,7 +54,7 @@ class APIServer : public Component, public Controller {
void handle_disconnect(APIConnection *conn);
#ifdef USE_BINARY_SENSOR
void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override;
void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
#endif
#ifdef USE_COVER
void on_cover_update(cover::Cover *obj) override;

View File

@@ -216,7 +216,7 @@ class ProtoWriteBuffer {
this->buffer_->insert(this->buffer_->end(), data, data + len);
}
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) {
this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force);

View File

@@ -1,10 +1,7 @@
from logging import getLogger
from esphome import automation, core
from esphome.automation import Condition, maybe_simple_id
import esphome.codegen as cg
from esphome.components import mqtt, web_server
from esphome.components.const import CONF_ON_STATE_CHANGE
import esphome.config_validation as cv
from esphome.const import (
CONF_DELAY,
@@ -101,7 +98,6 @@ IS_PLATFORM_COMPONENT = True
CONF_TIME_OFF = "time_off"
CONF_TIME_ON = "time_on"
CONF_TRIGGER_ON_INITIAL_STATE = "trigger_on_initial_state"
DEFAULT_DELAY = "1s"
DEFAULT_TIME_OFF = "100ms"
@@ -131,17 +127,9 @@ MultiClickTriggerEvent = binary_sensor_ns.struct("MultiClickTriggerEvent")
StateTrigger = binary_sensor_ns.class_(
"StateTrigger", automation.Trigger.template(bool)
)
StateChangeTrigger = binary_sensor_ns.class_(
"StateChangeTrigger",
automation.Trigger.template(cg.optional.template(bool), cg.optional.template(bool)),
)
BinarySensorPublishAction = binary_sensor_ns.class_(
"BinarySensorPublishAction", automation.Action
)
BinarySensorInvalidateAction = binary_sensor_ns.class_(
"BinarySensorInvalidateAction", automation.Action
)
# Condition
BinarySensorCondition = binary_sensor_ns.class_("BinarySensorCondition", Condition)
@@ -156,8 +144,6 @@ AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Compon
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
_LOGGER = getLogger(__name__)
FILTER_REGISTRY = Registry()
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
@@ -400,14 +386,6 @@ def validate_click_timing(value):
return value
def validate_publish_initial_state(value):
value = cv.boolean(value)
_LOGGER.warning(
"The 'publish_initial_state' option has been replaced by 'trigger_on_initial_state' and will be removed in a future release"
)
return value
_BINARY_SENSOR_SCHEMA = (
cv.ENTITY_BASE_SCHEMA.extend(web_server.WEBSERVER_SORTING_SCHEMA)
.extend(cv.MQTT_COMPONENT_SCHEMA)
@@ -417,12 +395,7 @@ _BINARY_SENSOR_SCHEMA = (
cv.OnlyWith(CONF_MQTT_ID, "mqtt"): cv.declare_id(
mqtt.MQTTBinarySensorComponent
),
cv.Exclusive(
CONF_PUBLISH_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
): validate_publish_initial_state,
cv.Exclusive(
CONF_TRIGGER_ON_INITIAL_STATE, CONF_TRIGGER_ON_INITIAL_STATE
): cv.boolean,
cv.Optional(CONF_PUBLISH_INITIAL_STATE): cv.boolean,
cv.Optional(CONF_DEVICE_CLASS): validate_device_class,
cv.Optional(CONF_FILTERS): validate_filters,
cv.Optional(CONF_ON_PRESS): automation.validate_automation(
@@ -481,11 +454,6 @@ _BINARY_SENSOR_SCHEMA = (
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger),
}
),
cv.Optional(CONF_ON_STATE_CHANGE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateChangeTrigger),
}
),
}
)
)
@@ -525,10 +493,8 @@ async def setup_binary_sensor_core_(var, config):
if (device_class := config.get(CONF_DEVICE_CLASS)) is not None:
cg.add(var.set_device_class(device_class))
trigger = config.get(CONF_TRIGGER_ON_INITIAL_STATE, False) or config.get(
CONF_PUBLISH_INITIAL_STATE, False
)
cg.add(var.set_trigger_on_initial_state(trigger))
if publish_initial_state := config.get(CONF_PUBLISH_INITIAL_STATE):
cg.add(var.set_publish_initial_state(publish_initial_state))
if inverted := config.get(CONF_INVERTED):
cg.add(var.set_inverted(inverted))
if filters_config := config.get(CONF_FILTERS):
@@ -576,17 +542,6 @@ async def setup_binary_sensor_core_(var, config):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(trigger, [(bool, "x")], conf)
for conf in config.get(CONF_ON_STATE_CHANGE, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger,
[
(cg.optional.template(bool), "x_previous"),
(cg.optional.template(bool), "x"),
],
conf,
)
if mqtt_id := config.get(CONF_MQTT_ID):
mqtt_ = cg.new_Pvariable(mqtt_id, var)
await mqtt.register_mqtt_component(mqtt_, config)
@@ -636,18 +591,3 @@ async def binary_sensor_is_off_to_code(config, condition_id, template_arg, args)
async def to_code(config):
cg.add_define("USE_BINARY_SENSOR")
cg.add_global(binary_sensor_ns.using)
@automation.register_action(
"binary_sensor.invalidate_state",
BinarySensorInvalidateAction,
cv.maybe_simple_value(
{
cv.Required(CONF_ID): cv.use_id(BinarySensor),
},
key=CONF_ID,
),
)
async def binary_sensor_invalidate_state_to_code(config, action_id, template_arg, args):
paren = await cg.get_variable(config[CONF_ID])
return cg.new_Pvariable(action_id, template_arg, paren)

View File

@@ -96,7 +96,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
: parent_(parent), timing_(std::move(timing)) {}
void setup() override {
this->last_state_ = this->parent_->get_state_default(false);
this->last_state_ = this->parent_->state;
auto f = std::bind(&MultiClickTrigger::on_state_, this, std::placeholders::_1);
this->parent_->add_on_state_callback(f);
}
@@ -130,14 +130,6 @@ class StateTrigger : public Trigger<bool> {
}
};
class StateChangeTrigger : public Trigger<optional<bool>, optional<bool> > {
public:
explicit StateChangeTrigger(BinarySensor *parent) {
parent->add_full_state_callback(
[this](optional<bool> old_state, optional<bool> state) { this->trigger(old_state, state); });
}
};
template<typename... Ts> class BinarySensorCondition : public Condition<Ts...> {
public:
BinarySensorCondition(BinarySensor *parent, bool state) : parent_(parent), state_(state) {}
@@ -162,15 +154,5 @@ template<typename... Ts> class BinarySensorPublishAction : public Action<Ts...>
BinarySensor *sensor_;
};
template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts...> {
public:
explicit BinarySensorInvalidateAction(BinarySensor *sensor) : sensor_(sensor) {}
void play(Ts... x) override { this->sensor_->invalidate_state(); }
protected:
BinarySensor *sensor_;
};
} // namespace binary_sensor
} // namespace esphome

View File

@@ -7,25 +7,42 @@ namespace binary_sensor {
static const char *const TAG = "binary_sensor";
void BinarySensor::publish_state(bool new_state) {
void BinarySensor::add_on_state_callback(std::function<void(bool)> &&callback) {
this->state_callback_.add(std::move(callback));
}
void BinarySensor::publish_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(new_state);
this->send_state_internal(state, false);
} else {
this->filter_list_->input(new_state);
this->filter_list_->input(state, false);
}
}
void BinarySensor::publish_initial_state(bool new_state) {
this->invalidate_state();
this->publish_state(new_state);
}
void BinarySensor::send_state_internal(bool new_state) {
// copy the new state to the visible property for backwards compatibility, before any callbacks
this->state = new_state;
// Note that set_state_ de-dups and will only trigger callbacks if the state has actually changed
if (this->set_state_(new_state)) {
ESP_LOGD(TAG, "'%s': New state is %s", this->get_name().c_str(), ONOFF(new_state));
void BinarySensor::publish_initial_state(bool state) {
if (!this->publish_dedup_.next(state))
return;
if (this->filter_list_ == nullptr) {
this->send_state_internal(state, true);
} else {
this->filter_list_->input(state, true);
}
}
void BinarySensor::send_state_internal(bool state, bool is_initial) {
if (is_initial) {
ESP_LOGD(TAG, "'%s': Sending initial state %s", this->get_name().c_str(), ONOFF(state));
} else {
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), ONOFF(state));
}
this->has_state_ = true;
this->state = state;
if (!is_initial || this->publish_initial_state_) {
this->state_callback_.call(state);
}
}
BinarySensor::BinarySensor() : state(false) {}
void BinarySensor::add_filter(Filter *filter) {
filter->parent_ = this;
@@ -43,6 +60,7 @@ void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
this->add_filter(filter);
}
}
bool BinarySensor::has_state() const { return this->has_state_; }
bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace binary_sensor

View File

@@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/filter.h"
@@ -33,39 +34,52 @@ namespace binary_sensor {
* The sub classes should notify the front-end of new states via the publish_state() method which
* handles inverted inputs for you.
*/
class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceClass {
class BinarySensor : public EntityBase, public EntityBase_DeviceClass {
public:
explicit BinarySensor(){};
explicit BinarySensor();
/** Add a callback to be notified of state changes.
*
* @param callback The void(bool) callback.
*/
void add_on_state_callback(std::function<void(bool)> &&callback);
/** Publish a new state to the front-end.
*
* @param new_state The new state.
* @param state The new state.
*/
void publish_state(bool new_state);
void publish_state(bool state);
/** Publish the initial state, this will not make the callback manager send callbacks
* and is meant only for the initial state on boot.
*
* @param new_state The new state.
* @param state The new state.
*/
void publish_initial_state(bool new_state);
void publish_initial_state(bool state);
/// The current reported state of the binary sensor.
bool state{false};
void add_filter(Filter *filter);
void add_filters(const std::vector<Filter *> &filters);
void set_publish_initial_state(bool publish_initial_state) { this->publish_initial_state_ = publish_initial_state; }
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
void send_state_internal(bool new_state);
void send_state_internal(bool state, bool is_initial);
/// Return whether this binary sensor has outputted a state.
virtual bool has_state() const;
virtual bool is_status_binary_sensor() const;
// For backward compatibility, provide an accessible property
bool state{};
protected:
CallbackManager<void(bool)> state_callback_{};
Filter *filter_list_{nullptr};
bool has_state_{false};
bool publish_initial_state_{false};
Deduplicator<bool> publish_dedup_;
};
class BinarySensorInitiallyOff : public BinarySensor {

View File

@@ -9,36 +9,37 @@ namespace binary_sensor {
static const char *const TAG = "sensor.filter";
void Filter::output(bool value) {
if (this->next_ == nullptr) {
this->parent_->send_state_internal(value);
} else {
this->next_->input(value);
}
}
void Filter::input(bool value) {
void Filter::output(bool value, bool is_initial) {
if (!this->dedup_.next(value))
return;
auto b = this->new_value(value);
if (this->next_ == nullptr) {
this->parent_->send_state_internal(value, is_initial);
} else {
this->next_->input(value, is_initial);
}
}
void Filter::input(bool value, bool is_initial) {
auto b = this->new_value(value, is_initial);
if (b.has_value()) {
this->output(*b);
this->output(*b, is_initial);
}
}
optional<bool> DelayedOnOffFilter::new_value(bool value) {
optional<bool> DelayedOnOffFilter::new_value(bool value, bool is_initial) {
if (value) {
this->set_timeout("ON_OFF", this->on_delay_.value(), [this]() { this->output(true); });
this->set_timeout("ON_OFF", this->on_delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
} else {
this->set_timeout("ON_OFF", this->off_delay_.value(), [this]() { this->output(false); });
this->set_timeout("ON_OFF", this->off_delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
}
return {};
}
float DelayedOnOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOnFilter::new_value(bool value) {
optional<bool> DelayedOnFilter::new_value(bool value, bool is_initial) {
if (value) {
this->set_timeout("ON", this->delay_.value(), [this]() { this->output(true); });
this->set_timeout("ON", this->delay_.value(), [this, is_initial]() { this->output(true, is_initial); });
return {};
} else {
this->cancel_timeout("ON");
@@ -48,9 +49,9 @@ optional<bool> DelayedOnFilter::new_value(bool value) {
float DelayedOnFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> DelayedOffFilter::new_value(bool value) {
optional<bool> DelayedOffFilter::new_value(bool value, bool is_initial) {
if (!value) {
this->set_timeout("OFF", this->delay_.value(), [this]() { this->output(false); });
this->set_timeout("OFF", this->delay_.value(), [this, is_initial]() { this->output(false, is_initial); });
return {};
} else {
this->cancel_timeout("OFF");
@@ -60,11 +61,11 @@ optional<bool> DelayedOffFilter::new_value(bool value) {
float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
optional<bool> InvertFilter::new_value(bool value) { return !value; }
optional<bool> InvertFilter::new_value(bool value, bool is_initial) { return !value; }
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
optional<bool> AutorepeatFilter::new_value(bool value) {
optional<bool> AutorepeatFilter::new_value(bool value, bool is_initial) {
if (value) {
// Ignore if already running
if (this->active_timing_ != 0)
@@ -100,7 +101,7 @@ void AutorepeatFilter::next_timing_() {
void AutorepeatFilter::next_value_(bool val) {
const AutorepeatFilterTiming &timing = this->timings_[this->active_timing_ - 2];
this->output(val); // This is at least the second one so not initial
this->output(val, false); // This is at least the second one so not initial
this->set_timeout("ON_OFF", val ? timing.time_on : timing.time_off, [this, val]() { this->next_value_(!val); });
}
@@ -108,18 +109,18 @@ float AutorepeatFilter::get_setup_priority() const { return setup_priority::HARD
LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move(f)) {}
optional<bool> LambdaFilter::new_value(bool value) { return this->f_(value); }
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
optional<bool> SettleFilter::new_value(bool value) {
optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
if (!this->steady_) {
this->set_timeout("SETTLE", this->delay_.value(), [this, value]() {
this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
this->steady_ = true;
this->output(value);
this->output(value, is_initial);
});
return {};
} else {
this->steady_ = false;
this->output(value);
this->output(value, is_initial);
this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
return value;
}

View File

@@ -14,11 +14,11 @@ class BinarySensor;
class Filter {
public:
virtual optional<bool> new_value(bool value) = 0;
virtual optional<bool> new_value(bool value, bool is_initial) = 0;
void input(bool value);
void input(bool value, bool is_initial);
void output(bool value);
void output(bool value, bool is_initial);
protected:
friend BinarySensor;
@@ -30,7 +30,7 @@ class Filter {
class DelayedOnOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
@@ -44,7 +44,7 @@ class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
@@ -56,7 +56,7 @@ class DelayedOnFilter : public Filter, public Component {
class DelayedOffFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
@@ -68,7 +68,7 @@ class DelayedOffFilter : public Filter, public Component {
class InvertFilter : public Filter {
public:
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
};
struct AutorepeatFilterTiming {
@@ -86,7 +86,7 @@ class AutorepeatFilter : public Filter, public Component {
public:
explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;
@@ -102,7 +102,7 @@ class LambdaFilter : public Filter {
public:
explicit LambdaFilter(std::function<optional<bool>(bool)> f);
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
protected:
std::function<optional<bool>(bool)> f_;
@@ -110,7 +110,7 @@ class LambdaFilter : public Filter {
class SettleFilter : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;
optional<bool> new_value(bool value, bool is_initial) override;
float get_setup_priority() const override;

View File

@@ -58,7 +58,7 @@ static std::vector<api::BluetoothLERawAdvertisement> &get_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_)
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
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;
batch_buffer.emplace_back();

View File

@@ -52,7 +52,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
public:
BluetoothProxy();
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 setup() 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
// 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) {
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
if (this->is_failed()) {
this->reset_to_construction_state();
}
if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) {

View File

@@ -3,5 +3,4 @@
CODEOWNERS = ["@esphome/core"]
CONF_DRAW_ROUNDING = "draw_rounding"
CONF_ON_STATE_CHANGE = "on_state_change"
CONF_REQUEST_HEADERS = "request_headers"

View File

@@ -11,25 +11,25 @@ static const char *const TAG = "datetime.date_entity";
void DateEntity::publish_state() {
if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) {
this->has_state_ = false;
this->set_has_state(false);
return;
}
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");
return;
}
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");
return;
}
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_);
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_);
this->state_callback_.call();
}

View File

@@ -13,9 +13,6 @@ namespace datetime {
class DateTimeBase : public EntityBase {
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;
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
time::RealTimeClock *rtc_;
#endif
bool has_state_{false};
};
#ifdef USE_TIME

View File

@@ -11,40 +11,40 @@ static const char *const TAG = "datetime.datetime_entity";
void DateTimeEntity::publish_state() {
if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) {
this->has_state_ = false;
this->set_has_state(false);
return;
}
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");
return;
}
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");
return;
}
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_);
return;
}
if (this->hour_ > 23) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Hour must be between 0 and 23");
return;
}
if (this->minute_ > 59) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Minute must be between 0 and 59");
return;
}
if (this->second_ > 59) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Second must be between 0 and 59");
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_,
this->month_, this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call();

View File

@@ -11,21 +11,21 @@ static const char *const TAG = "datetime.time_entity";
void TimeEntity::publish_state() {
if (this->hour_ > 23) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Hour must be between 0 and 23");
return;
}
if (this->minute_ > 59) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Minute must be between 0 and 59");
return;
}
if (this->second_ > 59) {
this->has_state_ = false;
this->set_has_state(false);
ESP_LOGE(TAG, "Second must be between 0 and 59");
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_,
this->second_);
this->state_callback_.call();

View File

@@ -94,6 +94,13 @@ COMPILER_OPTIMIZATIONS = {
"SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
}
ARDUINO_ALLOWED_VARIANTS = [
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
]
def get_cpu_frequencies(*frequencies):
return [str(x) + "MHZ" for x in frequencies]
@@ -143,12 +150,17 @@ def set_core_data(config):
CORE.data[KEY_ESP32][KEY_COMPONENTS] = {}
elif conf[CONF_TYPE] == 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(
config[CONF_FRAMEWORK][CONF_VERSION]
)
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] = {}
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_ARDUINO = "arduino"
FRAMEWORK_SCHEMA = cv.typed_schema(
@@ -627,7 +654,6 @@ FRAMEWORK_SCHEMA = cv.typed_schema(
},
lower=True,
space="-",
default_type=FRAMEWORK_ARDUINO,
)
@@ -654,10 +680,11 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_PARTITIONS): cv.file_,
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,
_set_default_framework,
set_core_data,
)

View File

@@ -1,6 +1,7 @@
#ifdef USE_ESP32
#include "ble.h"
#include "ble_event_pool.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
@@ -23,9 +24,6 @@ namespace esp32_ble {
static const char *const TAG = "esp32_ble";
static RAMAllocator<BLEEvent> EVENT_ALLOCATOR( // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
void ESP32BLE::setup() {
global_ble = this;
ESP_LOGCONFIG(TAG, "Running setup");
@@ -304,82 +302,191 @@ void ESP32BLE::loop() {
BLEEvent *ble_event = this->ble_events_.pop();
while (ble_event != nullptr) {
switch (ble_event->type_) {
case BLEEvent::GATTS:
this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if,
&ble_event->event_.gatts.gatts_param);
case BLEEvent::GATTS: {
esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event;
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;
case BLEEvent::GATTC:
this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if,
&ble_event->event_.gattc.gattc_param);
}
case BLEEvent::GATTC: {
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;
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;
switch (gap_event) {
case 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());
}
break;
// Scan complete events
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:
// 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;
// Advertising complete events
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// All advertising complete events have the same structure with just status
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.adv_complete));
}
break;
// RSSI complete event
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
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.read_rssi_complete));
}
break;
// Security events
case ESP_GAP_BLE_AUTH_CMPL_EVT:
case ESP_GAP_BLE_SEC_REQ_EVT:
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
case ESP_GAP_BLE_NC_REQ_EVT:
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.security));
}
break;
default:
// Unknown/unhandled event
ESP_LOGW(TAG, "Unhandled GAP event type in loop: %d", gap_event);
break;
}
break;
}
default:
break;
}
ble_event->~BLEEvent();
EVENT_ALLOCATOR.deallocate(ble_event, 1);
// Return the event to the pool
this->ble_event_pool_.release(ble_event);
ble_event = this->ble_events_.pop();
}
if (this->advertising_ != nullptr) {
this->advertising_->loop();
}
// Log dropped events periodically
uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
if (dropped > 0) {
ESP_LOGW(TAG, "Dropped %u 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) {
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
if (new_event == nullptr) {
// Memory too fragmented to allocate new event. Can only drop it until memory comes back
// Helper function to load new event data based on type
void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
event->load_gap_event(e, p);
}
void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
event->load_gattc_event(e, i, p);
}
void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
event->load_gatts_event(e, i, p);
}
template<typename... Args> void enqueue_ble_event(Args... args) {
// Allocate an event from the pool
BLEEvent *event = global_ble->ble_event_pool_.allocate();
if (event == nullptr) {
// No events available - queue is full or we're out of memory
global_ble->ble_events_.increment_dropped_count();
return;
}
new (new_event) BLEEvent(event, param);
global_ble->ble_events_.push(new_event);
} // 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) {
ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event);
for (auto *gap_handler : this->gap_event_handlers_) {
gap_handler->gap_event_handler(event, param);
// Load new event data (replaces previous event)
load_ble_event(event, args...);
// Push the event to the queue
global_ble->ble_events_.push(event);
// Push always succeeds because we're the only producer and the pool ensures we never exceed queue size
}
// Explicit template instantiations for the friend function
template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *);
template void enqueue_ble_event(esp_gatts_cb_event_t, esp_gatt_if_t, esp_ble_gatts_cb_param_t *);
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) {
// Queue GAP events that components need to handle
// Scanning events - used by esp32_ble_tracker
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:
// Advertising events - used by esp32_ble_beacon and esp32_ble server
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
// Connection events - used by ble_client
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
// Security events - used by ble_client and bluetooth_proxy
case ESP_GAP_BLE_AUTH_CMPL_EVT:
case ESP_GAP_BLE_SEC_REQ_EVT:
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
case ESP_GAP_BLE_PASSKEY_REQ_EVT:
case ESP_GAP_BLE_NC_REQ_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,
esp_ble_gatts_cb_param_t *param) {
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
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);
}
enqueue_ble_event(event, gatts_if, param);
}
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) {
BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1);
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);
}
enqueue_ble_event(event, gattc_if, param);
}
float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; }

View File

@@ -2,6 +2,7 @@
#include "ble_advertising.h"
#include "ble_uuid.h"
#include "ble_scan_result.h"
#include <functional>
@@ -11,6 +12,7 @@
#include "esphome/core/helpers.h"
#include "ble_event.h"
#include "ble_event_pool.h"
#include "queue.h"
#ifdef USE_ESP32
@@ -22,6 +24,16 @@
namespace esphome {
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);
// NOLINTNEXTLINE(modernize-use-using)
@@ -57,6 +69,11 @@ class GAPEventHandler {
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 {
public:
virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
@@ -101,6 +118,9 @@ class ESP32BLE : public Component {
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_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_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); }
void register_ble_status_event_handler(BLEStatusEventHandler *handler) {
@@ -113,22 +133,23 @@ 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 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_dismantle_();
bool ble_pre_setup_();
void advertising_init_();
private:
template<typename... Args> friend void enqueue_ble_event(Args... args);
std::vector<GAPEventHandler *> gap_event_handlers_;
std::vector<GAPScanEventHandler *> gap_scan_event_handlers_;
std::vector<GATTcEventHandler *> gattc_event_handlers_;
std::vector<GATTsEventHandler *> gatts_event_handlers_;
std::vector<BLEStatusEventHandler *> ble_status_event_handlers_;
BLEComponentState state_{BLE_COMPONENT_STATE_OFF};
Queue<BLEEvent> ble_events_;
LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_;
BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_;
BLEAdvertising *advertising_{};
esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};
uint32_t advertising_cycle_time_{};

View File

@@ -2,92 +2,399 @@
#ifdef USE_ESP32
#include <cstddef> // for offsetof
#include <vector>
#include <esp_gap_ble_api.h>
#include <esp_gattc_api.h>
#include <esp_gatts_api.h>
#include "ble_scan_result.h"
namespace esphome {
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) == 0,
"status must be first member of scan_param_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == 0,
"status must be first member of scan_start_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == 0,
"status must be first member of scan_stop_cmpl");
// Compile-time verification for advertising complete events
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF adv_data_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_rsp_data_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF scan_rsp_data_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF adv_data_raw_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF adv_start_cmpl structure has unexpected size");
static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param) == sizeof(esp_bt_status_t),
"ESP-IDF adv_stop_cmpl structure has unexpected size");
// Verify the status field is at offset 0 for advertising events
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_cmpl.status) == 0,
"status must be first member of adv_data_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, scan_rsp_data_cmpl.status) == 0,
"status must be first member of scan_rsp_data_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_raw_cmpl.status) == 0,
"status must be first member of adv_data_raw_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_start_cmpl.status) == 0,
"status must be first member of adv_start_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, adv_stop_cmpl.status) == 0,
"status must be first member of adv_stop_cmpl");
// Compile-time verification for RSSI complete event structure
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.status) == 0,
"status must be first member of read_rssi_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(esp_bt_status_t),
"rssi must immediately follow status in read_rssi_cmpl");
static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t),
"remote_addr must follow rssi in read_rssi_cmpl");
// 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.
//
// CRITICAL DESIGN NOTE:
// The heap allocations for GATTC/GATTS events are REQUIRED for memory safety.
// DO NOT attempt to optimize by removing these allocations or storing pointers
// to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime
// than our event processing, and accessing it after the callback returns would
// result in use-after-free bugs and crashes.
class BLEEvent {
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)
enum ble_event_t : uint8_t {
GAP,
GATTC,
GATTS,
} type_;
};
// Type definitions for cleaner method signatures
struct StatusOnlyData {
esp_bt_status_t status;
};
struct RSSICompleteData {
esp_bt_status_t status;
int8_t rssi;
esp_bd_addr_t remote_addr;
};
// 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->init_gap_data_(e, p);
}
// Constructor for GATTC events - uses heap allocation
// IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
// The param pointer from ESP-IDF is only valid during the callback execution.
// Since BLE events are processed asynchronously in the main loop, we must create
// our own copy to ensure the data remains valid until the event is processed.
BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->type_ = GATTC;
this->init_gattc_data_(e, i, p);
}
// Constructor for GATTS events - uses heap allocation
// IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization.
// The param pointer from ESP-IDF is only valid during the callback execution.
// Since BLE events are processed asynchronously in the main loop, we must create
// our own copy to ensure the data remains valid until the event is processed.
BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->type_ = GATTS;
this->init_gatts_data_(e, i, p);
}
// Destructor to clean up heap allocations
~BLEEvent() { this->cleanup_heap_data(); }
// Default constructor for pre-allocation in pool
BLEEvent() : type_(GAP) {}
// Clean up any heap-allocated data
void cleanup_heap_data() {
if (this->type_ == GAP) {
return;
}
if (this->type_ == GATTC) {
delete this->event_.gattc.gattc_param;
delete this->event_.gattc.data;
this->event_.gattc.gattc_param = nullptr;
this->event_.gattc.data = nullptr;
return;
}
if (this->type_ == GATTS) {
delete this->event_.gatts.gatts_param;
delete this->event_.gatts.data;
this->event_.gatts.gatts_param = nullptr;
this->event_.gatts.data = nullptr;
}
}
// Load new event data for reuse (replaces previous event data)
void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GAP;
this->init_gap_data_(e, p);
}
void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GATTC;
this->init_gattc_data_(e, i, p);
}
void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) {
this->cleanup_heap_data();
this->type_ = GATTS;
this->init_gatts_data_(e, i, p);
}
// 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 - Used by: esp32_ble_tracker
// This matches ESP-IDF's scan complete event structures
// All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout
// Used by: esp32_ble_tracker
StatusOnlyData scan_complete; // 1 byte
// Advertising complete events all have same structure
// Used by: esp32_ble_beacon, esp32_ble server components
// ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, ADV_START, ADV_STOP
StatusOnlyData adv_complete; // 1 byte
// RSSI complete event
// Used by: ble_client (ble_rssi_sensor component)
RSSICompleteData read_rssi_complete; // 8 bytes
// Security events - we store the full security union
// Used by: ble_client (automation), bluetooth_proxy, esp32_ble_client
esp_ble_sec_t security; // Variable size, but fits within scan_result size
};
} 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; }
esp_bt_status_t adv_complete_status() const { return event_.gap.adv_complete.status; }
const RSSICompleteData &read_rssi_complete() const { return event_.gap.read_rssi_complete; }
const esp_ble_sec_t &security() const { return event_.gap.security; }
private:
// Initialize GAP event data
void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) {
this->event_.gap.gap_event = e;
if (p == nullptr) {
return; // Invalid event, but we can't log in header file
}
// Copy data based on event type
switch (e) {
case ESP_GAP_BLE_SCAN_RESULT_EVT:
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;
// Advertising complete events - all have same structure with just status
// Used by: esp32_ble_beacon, esp32_ble server components
case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
this->event_.gap.adv_complete.status = p->adv_data_cmpl.status;
break;
case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
this->event_.gap.adv_complete.status = p->scan_rsp_data_cmpl.status;
break;
case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: // Used by: esp32_ble_beacon
this->event_.gap.adv_complete.status = p->adv_data_raw_cmpl.status;
break;
case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: // Used by: esp32_ble_beacon
this->event_.gap.adv_complete.status = p->adv_start_cmpl.status;
break;
case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: // Used by: esp32_ble_beacon
this->event_.gap.adv_complete.status = p->adv_stop_cmpl.status;
break;
// RSSI complete event
// Used by: ble_client (ble_rssi_sensor)
case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
this->event_.gap.read_rssi_complete.status = p->read_rssi_cmpl.status;
this->event_.gap.read_rssi_complete.rssi = p->read_rssi_cmpl.rssi;
memcpy(this->event_.gap.read_rssi_complete.remote_addr, p->read_rssi_cmpl.remote_addr, sizeof(esp_bd_addr_t));
break;
// Security events - copy the entire security union
// Used by: ble_client, bluetooth_proxy, esp32_ble_client
case ESP_GAP_BLE_AUTH_CMPL_EVT: // Used by: bluetooth_proxy, esp32_ble_client
case ESP_GAP_BLE_SEC_REQ_EVT: // Used by: esp32_ble_client
case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: // Used by: ble_client automation
case ESP_GAP_BLE_PASSKEY_REQ_EVT: // Used by: ble_client automation
case ESP_GAP_BLE_NC_REQ_EVT: // Used by: ble_client automation
memcpy(&this->event_.gap.security, &p->ble_security, sizeof(esp_ble_sec_t));
break;
default:
// We only store data for GAP events that components currently use
// Unknown events still get queued and logged in ble.cpp:375 as
// "Unhandled GAP event type in loop" - this helps identify new events
// that components might need in the future
break;
}
}
// Initialize GATTC event data
void init_gattc_data_(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;
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
// IMPORTANT: This heap allocation provides clear ownership semantics:
// - The BLEEvent owns the allocated memory for its lifetime
// - The data remains valid from the BLE callback context until processed in the main loop
// - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p);
// Copy data for events that need it
// The param struct contains pointers (e.g., notify.value) that point to temporary buffers.
// We must copy this data to ensure it remains valid when the event is processed later.
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;
}
}
// Initialize GATTS event data
void init_gatts_data_(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;
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
// IMPORTANT: This heap allocation provides clear ownership semantics:
// - The BLEEvent owns the allocated memory for its lifetime
// - The data remains valid from the BLE callback context until processed in the main loop
// - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory
this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p);
// Copy data for events that need it
// The param struct contains pointers (e.g., write.value) that point to temporary buffers.
// We must copy this data to ensure it remains valid when the event is processed later.
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;
}
}
};
// Verify the gap_event struct hasn't grown beyond expected size
// The gap member in the union should be 80 bytes (including the gap_event enum)
static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes");
// Verify esp_ble_sec_t fits within our union
static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult");
// BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding)
} // namespace esp32_ble
} // namespace esphome

View File

@@ -0,0 +1,72 @@
#pragma once
#ifdef USE_ESP32
#include <atomic>
#include <cstddef>
#include "ble_event.h"
#include "queue.h"
#include "esphome/core/helpers.h"
namespace esphome {
namespace esp32_ble {
// BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation
// Events are allocated on first use and reused thereafter, growing to peak usage
template<uint8_t SIZE> class BLEEventPool {
public:
BLEEventPool() : total_created_(0) {}
~BLEEventPool() {
// Clean up any remaining events in the free list
BLEEvent *event;
while ((event = this->free_list_.pop()) != nullptr) {
delete event;
}
}
// Allocate an event from the pool
// Returns nullptr if pool is full
BLEEvent *allocate() {
// Try to get from free list first
BLEEvent *event = this->free_list_.pop();
if (event != nullptr)
return event;
// Need to create a new event
if (this->total_created_ >= SIZE) {
// Pool is at capacity
return nullptr;
}
// Use internal RAM for better performance
RAMAllocator<BLEEvent> allocator(RAMAllocator<BLEEvent>::ALLOC_INTERNAL);
event = allocator.allocate(1);
if (event == nullptr) {
// Memory allocation failed
return nullptr;
}
// Placement new to construct the object
new (event) BLEEvent();
this->total_created_++;
return event;
}
// Return an event to the pool for reuse
void release(BLEEvent *event) {
if (event != nullptr) {
this->free_list_.push(event);
}
}
private:
LockFreeQueue<BLEEvent, SIZE> free_list_; // Free events ready for reuse
uint8_t total_created_; // Total events created (high water mark)
};
} // namespace esp32_ble
} // namespace esphome
#endif

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,81 @@
#ifdef USE_ESP32
#include <mutex>
#include <queue>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <atomic>
#include <cstddef>
/*
* 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
* events will simply be placed on a semaphore guarded queue. The next time the
* component runs loop(), these events are popped off the queue and handed at
* this safer time.
* than using mutex-based locking, this lock-free queue allows the BLE
* task to enqueue events without blocking. The main loop() then processes
* these events at a 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 esp32_ble {
template<class T> class Queue {
template<class T, uint8_t SIZE> class LockFreeQueue {
public:
Queue() { m_ = xSemaphoreCreateMutex(); }
LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {}
void push(T *element) {
bool push(T *element) {
if (element == nullptr)
return;
// It is not called from main loop. Thus it won't block main thread.
xSemaphoreTake(m_, portMAX_DELAY);
q_.push(element);
xSemaphoreGive(m_);
return false;
uint8_t current_tail = tail_.load(std::memory_order_relaxed);
uint8_t next_tail = (current_tail + 1) % SIZE;
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 *element = nullptr;
uint8_t current_head = head_.load(std::memory_order_relaxed);
if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) {
if (!q_.empty()) {
element = q_.front();
q_.pop();
}
xSemaphoreGive(m_);
if (current_head == tail_.load(std::memory_order_acquire)) {
return nullptr; // Empty
}
T *element = buffer_[current_head];
head_.store((current_head + 1) % SIZE, std::memory_order_release);
return element;
}
size_t size() const {
uint8_t tail = tail_.load(std::memory_order_acquire);
uint8_t head = head_.load(std::memory_order_acquire);
return (tail - head + SIZE) % SIZE;
}
uint16_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 {
uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE;
return next_tail == head_.load(std::memory_order_acquire);
}
protected:
std::queue<T *> q_;
SemaphoreHandle_t m_;
T *buffer_[SIZE];
// Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset)
std::atomic<uint16_t> dropped_count_; // 65535 max - more than enough for drop tracking
// Atomic: written by consumer (pop), read by producer (push) to check if full
std::atomic<uint8_t> head_;
// Atomic: written by producer (push), read by consumer (pop) to check if empty
std::atomic<uint8_t> tail_;
};
} // 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])
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_ble_status_event_handler(var))
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");
return;
}
ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param> allocator(
ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param>::ALLOW_FAILURE);
this->scan_result_buffer_ = allocator.allocate(ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE);
RAMAllocator<BLEScanResult> allocator;
this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
if (this->scan_result_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!");
if (this->scan_ring_buffer_ == nullptr) {
ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
this->mark_failed();
}
global_esp32_ble_tracker = this;
this->scan_result_lock_ = xSemaphoreCreateMutex();
#ifdef USE_OTA
ota::get_global_ota_callback()->add_on_state_callback(
@@ -120,27 +118,31 @@ void ESP32BLETracker::loop() {
}
bool promote_to_connecting = discovered && !searching && !connecting;
if (this->scanner_state_ == ScannerState::RUNNING &&
this->scan_result_index_ && // if it looks like we have a scan result we will take the lock
xSemaphoreTake(this->scan_result_lock_, 0)) {
uint32_t index = this->scan_result_index_;
if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) {
ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up.");
}
// Process scan results from lock-free SPSC ring buffer
// Consumer side: This runs in the main loop thread
if (this->scanner_state_ == ScannerState::RUNNING) {
// Load our own index with relaxed ordering (we're the only writer)
size_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
if (this->raw_advertisements_) {
for (auto *listener : this->listeners_) {
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_);
}
}
// Load producer's index with acquire to see their latest writes
size_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
if (this->parse_advertisements_) {
for (size_t i = 0; i < index; i++) {
while (read_idx != write_idx) {
// 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;
device.parse_scan_rst(this->scan_result_buffer_[i]);
device.parse_scan_rst(scan_result);
bool found = false;
for (auto *listener : this->listeners_) {
@@ -161,9 +163,19 @@ void ESP32BLETracker::loop() {
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) {
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) {
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:
this->gap_scan_set_param_complete_(param->scan_param_cmpl);
break;
@@ -385,11 +394,57 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
default:
break;
}
// Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
for (auto *client : this->clients_) {
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) {
ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status);
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);
}
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,
esp_ble_gattc_cb_param_t *param) {
for (auto *client : this->clients_) {
@@ -494,13 +521,16 @@ optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData
return ESPBLEiBeacon(data.data.data());
}
void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &param) {
this->scan_result_ = param;
void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) {
this->scan_result_ = &scan_result;
for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++)
this->address_[i] = param.bda[i];
this->address_type_ = param.ble_addr_type;
this->rssi_ = param.rssi;
this->parse_adv_(param);
this->address_[i] = scan_result.bda[i];
this->address_type_ = static_cast<esp_ble_addr_type_t>(scan_result.ble_addr_type);
this->rssi_ = scan_result.rssi;
// 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
ESP_LOGVV(TAG, "Parse Result:");
@@ -558,13 +588,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, " 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
}
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;
const uint8_t *payload = param.ble_adv;
uint8_t len = param.adv_data_len + param.scan_rsp_len;
while (offset + 2 < len) {
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 <array>
#include <atomic>
#include <string>
#include <vector>
@@ -62,7 +63,7 @@ class ESPBLEiBeacon {
class ESPBTDevice {
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;
@@ -84,7 +85,8 @@ class ESPBTDevice {
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_; }
// Exposed through a function for use in lambdas
const BLEScanResult &get_scan_result() const { return *scan_result_; }
bool resolve_irk(const uint8_t *irk) const;
@@ -98,7 +100,7 @@ class ESPBTDevice {
}
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_{
0,
@@ -112,7 +114,7 @@ class ESPBTDevice {
std::vector<ESPBTUUID> service_uuids_{};
std::vector<ServiceData> manufacturer_datas_{};
std::vector<ServiceData> service_datas_{};
esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{};
const BLEScanResult *scan_result_{nullptr};
};
class ESP32BLETracker;
@@ -121,9 +123,7 @@ class ESPBTDeviceListener {
public:
virtual void on_scan_end() {}
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) {
return false;
};
virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; };
virtual AdvertisementParserType get_advertisement_parser_type() {
return AdvertisementParserType::PARSED_ADVERTISEMENTS;
};
@@ -210,6 +210,7 @@ class ESPBTClient : public ESPBTDeviceListener {
class ESP32BLETracker : public Component,
public GAPEventHandler,
public GAPScanEventHandler,
public GATTcEventHandler,
public BLEStatusEventHandler,
public Parented<ESP32BLE> {
@@ -240,6 +241,7 @@ class ESP32BLETracker : public Component,
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
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_scan_event_handler(const BLEScanResult &scan_result) override;
void ble_before_disabled_event_handler() override;
void add_scanner_state_callback(std::function<void(ScannerState)> &&callback) {
@@ -285,14 +287,16 @@ class ESP32BLETracker : public Component,
bool ble_was_disabled_{true};
bool raw_advertisements_{false};
bool parse_advertisements_{false};
SemaphoreHandle_t scan_result_lock_;
size_t scan_result_index_{0};
#ifdef USE_PSRAM
const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32;
#else
const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20;
#endif // USE_PSRAM
esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_;
// Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results
// Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler)
// Consumer: ESPHome main loop (loop() method)
// This design ensures zero blocking in the BT callback and prevents scan result loss
BLEScanResult *scan_ring_buffer_;
std::atomic<size_t> ring_write_index_{0}; // Written only by BT callback (producer)
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_set_param_failed_{ESP_BT_STATUS_SUCCESS};
int connecting_{0};

View File

@@ -57,7 +57,7 @@ void ESP32Camera::dump_config() {
" External Clock: Pin:%d Frequency:%u\n"
" I2C Pins: SDA:%d SCL:%d\n"
" 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_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset);
switch (this->config_.frame_size) {

View File

@@ -2,8 +2,8 @@
#include "esp32_touch.h"
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
#include "esphome/core/log.h"
#include "esphome/core/hal.h"
#include <cinttypes>
@@ -293,33 +293,27 @@ uint32_t ESP32TouchComponent::component_touch_pad_read(touch_pad_t tp) {
void ESP32TouchComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
if (this->current_child_ == 0) {
this->should_print_ = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250;
}
if (this->children_.empty()) {
return;
}
auto *child = this->children_[this->current_child_];
child->value_ = this->component_touch_pad_read(child->get_touch_pad());
bool should_print = this->setup_mode_ && now - this->setup_mode_last_log_print_ > 250;
for (auto *child : this->children_) {
child->value_ = this->component_touch_pad_read(child->get_touch_pad());
#if !(defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3))
child->publish_state(child->value_ < child->get_threshold());
child->publish_state(child->value_ < child->get_threshold());
#else
child->publish_state(child->value_ > child->get_threshold());
child->publish_state(child->value_ > child->get_threshold());
#endif
if (this->should_print_) {
ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(),
(uint32_t) child->get_touch_pad(), child->value_);
if (should_print) {
ESP_LOGD(TAG, "Touch Pad '%s' (T%" PRIu32 "): %" PRIu32, child->get_name().c_str(),
(uint32_t) child->get_touch_pad(), child->value_);
}
App.feed_wdt();
}
if (this->should_print_ && this->current_child_ == this->children_.size() - 1) {
if (should_print) {
// Avoid spamming logs
this->setup_mode_last_log_print_ = now;
this->should_print_ = false;
}
this->current_child_ = (this->current_child_ + 1) % this->children_.size();
}
void ESP32TouchComponent::on_shutdown() {

View File

@@ -2,9 +2,9 @@
#ifdef USE_ESP32
#include <esp_idf_version.h>
#include "esphome/components/binary_sensor/binary_sensor.h"
#include "esphome/core/component.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
#include <esp_idf_version.h>
#include <vector>
@@ -73,9 +73,7 @@ class ESP32TouchComponent : public Component {
#endif
std::vector<ESP32TouchBinarySensor *> children_;
uint8_t current_child_{0};
bool setup_mode_{false};
bool should_print_{false};
uint32_t setup_mode_last_log_print_{0};
// common parameters
uint16_t sleep_cycle_{4095};

View File

@@ -41,39 +41,48 @@ void FanCall::perform() {
void FanCall::validate_() {
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());
if (this->binary_state_.has_value() && *this->binary_state_) {
// when turning on, if neither current nor new speed available, set speed to 100%
if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) {
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();
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
// "Manually setting a speed must disable any set preset mode"
this->preset_mode_.clear();
}
if (!this->preset_mode_.empty()) {
const auto &preset_modes = traits.supported_preset_modes();
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(),
this->preset_mode_.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_.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) {

View File

@@ -18,7 +18,7 @@ void I2SAudioComponent::setup() {
static i2s_port_t next_port_num = I2S_NUM_0;
if (next_port_num >= I2S_NUM_MAX) {
ESP_LOGE(TAG, "Too many I2S Audio components");
ESP_LOGE(TAG, "Too many components");
this->mark_failed();
return;
}

View File

@@ -45,7 +45,7 @@ void I2SAudioMicrophone::setup() {
#if SOC_I2S_SUPPORTS_ADC
if (this->adc_) {
if (this->parent_->get_port() != I2S_NUM_0) {
ESP_LOGE(TAG, "Internal ADC only works on I2S0!");
ESP_LOGE(TAG, "Internal ADC only works on I2S0");
this->mark_failed();
return;
}
@@ -55,7 +55,7 @@ void I2SAudioMicrophone::setup() {
{
if (this->pdm_) {
if (this->parent_->get_port() != I2S_NUM_0) {
ESP_LOGE(TAG, "PDM only works on I2S0!");
ESP_LOGE(TAG, "PDM only works on I2S0");
this->mark_failed();
return;
}
@@ -64,14 +64,14 @@ void I2SAudioMicrophone::setup() {
this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS);
if (this->active_listeners_semaphore_ == nullptr) {
ESP_LOGE(TAG, "Failed to create semaphore");
ESP_LOGE(TAG, "Creating semaphore failed");
this->mark_failed();
return;
}
this->event_group_ = xEventGroupCreate();
if (this->event_group_ == nullptr) {
ESP_LOGE(TAG, "Failed to create event group");
ESP_LOGE(TAG, "Creating event group failed");
this->mark_failed();
return;
}
@@ -79,6 +79,15 @@ void I2SAudioMicrophone::setup() {
this->configure_stream_settings_();
}
void I2SAudioMicrophone::dump_config() {
ESP_LOGCONFIG(TAG,
"Microphone:\n"
" Pin: %d\n"
" PDM: %s\n"
" DC offset correction: %s",
static_cast<int8_t>(this->din_pin_), YESNO(this->pdm_), YESNO(this->correct_dc_offset_));
}
void I2SAudioMicrophone::configure_stream_settings_() {
uint8_t channel_count = 1;
#ifdef USE_I2S_LEGACY
@@ -127,6 +136,7 @@ bool I2SAudioMicrophone::start_driver_() {
if (!this->parent_->try_lock()) {
return false; // Waiting for another i2s to return lock
}
this->locked_driver_ = true;
esp_err_t err;
#ifdef USE_I2S_LEGACY
@@ -151,7 +161,7 @@ bool I2SAudioMicrophone::start_driver_() {
config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN);
err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err));
return false;
}
@@ -174,7 +184,7 @@ bool I2SAudioMicrophone::start_driver_() {
err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err));
return false;
}
@@ -183,7 +193,7 @@ bool I2SAudioMicrophone::start_driver_() {
err = i2s_set_pin(this->parent_->get_port(), &pin_config);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error setting I2S pin: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "Error setting pin: %s", esp_err_to_name(err));
return false;
}
}
@@ -198,7 +208,7 @@ bool I2SAudioMicrophone::start_driver_() {
/* Allocate a new RX channel and get the handle of this channel */
err = i2s_new_channel(&chan_cfg, NULL, &this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "Error creating channel: %s", esp_err_to_name(err));
return false;
}
@@ -270,14 +280,14 @@ bool I2SAudioMicrophone::start_driver_() {
err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg);
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "Error initializing channel: %s", esp_err_to_name(err));
return false;
}
/* Before reading data, start the RX channel first */
i2s_channel_enable(this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error enabling I2S Microphone: %s", esp_err_to_name(err));
ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err));
return false;
}
#endif
@@ -304,31 +314,37 @@ void I2SAudioMicrophone::stop_driver_() {
if (this->adc_) {
err = i2s_adc_disable(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error disabling ADC - it may not have started: %s", esp_err_to_name(err));
ESP_LOGW(TAG, "Error disabling ADC: %s", esp_err_to_name(err));
}
}
#endif
err = i2s_stop(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err));
ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err));
}
err = i2s_driver_uninstall(this->parent_->get_port());
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error uninstalling I2S driver - it may not have started: %s", esp_err_to_name(err));
ESP_LOGW(TAG, "Error uninstalling driver: %s", esp_err_to_name(err));
}
#else
/* Have to stop the channel before deleting it */
err = i2s_channel_disable(this->rx_handle_);
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 (err != ESP_OK) {
ESP_LOGW(TAG, "Error deleting I2S channel - it may not have started: %s", esp_err_to_name(err));
if (this->rx_handle_ != nullptr) {
/* Have to stop the channel before deleting it */
err = i2s_channel_disable(this->rx_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Error stopping: %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 (err != ESP_OK) {
ESP_LOGW(TAG, "Error deleting channel: %s", esp_err_to_name(err));
}
this->rx_handle_ = nullptr;
}
#endif
this->parent_->unlock();
if (this->locked_driver_) {
this->parent_->unlock();
this->locked_driver_ = false;
}
}
void I2SAudioMicrophone::mic_task(void *params) {
@@ -400,7 +416,7 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w
// Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call
if (!this->status_has_warning()) {
// Avoid spamming the logs with the error message if its repeated
ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err));
ESP_LOGW(TAG, "Read error: %s", esp_err_to_name(err));
}
this->status_set_warning();
return 0;
@@ -428,19 +444,19 @@ void I2SAudioMicrophone::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & MicrophoneEventGroupBits::TASK_STARTING) {
ESP_LOGD(TAG, "Task started, attempting to allocate buffer");
ESP_LOGV(TAG, "Task started, attempting to allocate buffer");
xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STARTING);
}
if (event_group_bits & MicrophoneEventGroupBits::TASK_RUNNING) {
ESP_LOGD(TAG, "Task is running and reading data");
ESP_LOGV(TAG, "Task is running and reading data");
xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_RUNNING);
this->state_ = microphone::STATE_RUNNING;
}
if ((event_group_bits & MicrophoneEventGroupBits::TASK_STOPPED)) {
ESP_LOGD(TAG, "Task finished, freeing resources and uninstalling I2S driver");
ESP_LOGV(TAG, "Task finished, freeing resources and uninstalling driver");
vTaskDelete(this->task_handle_);
this->task_handle_ = nullptr;
@@ -470,7 +486,8 @@ void I2SAudioMicrophone::loop() {
}
if (!this->start_driver_()) {
this->status_momentary_error("I2S driver failed to start, unloading it and attempting again in 1 second", 1000);
ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second");
this->status_momentary_error("driver_fail", 1000);
this->stop_driver_(); // Stop/frees whatever possibly started
break;
}
@@ -480,7 +497,8 @@ void I2SAudioMicrophone::loop() {
&this->task_handle_);
if (this->task_handle_ == nullptr) {
this->status_momentary_error("Task failed to start, attempting again in 1 second", 1000);
ESP_LOGE(TAG, "Task failed to start, retrying in 1 second");
this->status_momentary_error("task_fail", 1000);
this->stop_driver_(); // Stops the driver to return the lock; will be reloaded in next attempt
}
}

View File

@@ -18,6 +18,7 @@ namespace i2s_audio {
class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, public Component {
public:
void setup() override;
void dump_config() override;
void start() override;
void stop() override;
@@ -80,6 +81,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
bool pdm_{false};
bool correct_dc_offset_;
bool locked_driver_{false};
int32_t dc_offset_{0};
};

View File

@@ -110,29 +110,48 @@ void I2SAudioSpeaker::setup() {
}
}
void I2SAudioSpeaker::dump_config() {
ESP_LOGCONFIG(TAG,
"Speaker:\n"
" Pin: %d\n"
" Buffer duration: %" PRIu32,
static_cast<int8_t>(this->dout_pin_), this->buffer_duration_ms_);
if (this->timeout_.has_value()) {
ESP_LOGCONFIG(TAG, " Timeout: %" PRIu32 " ms", this->timeout_.value());
}
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_DAC
ESP_LOGCONFIG(TAG, " Internal DAC mode: %d", static_cast<int8_t>(this->internal_dac_mode_));
#endif
ESP_LOGCONFIG(TAG, " Communication format: %d", static_cast<int8_t>(this->i2s_comm_fmt_));
#else
ESP_LOGCONFIG(TAG, " Communication format: %s", this->i2s_comm_fmt_.c_str());
#endif
}
void I2SAudioSpeaker::loop() {
uint32_t event_group_bits = xEventGroupGetBits(this->event_group_);
if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) {
ESP_LOGD(TAG, "Starting Speaker");
ESP_LOGD(TAG, "Starting");
this->state_ = speaker::STATE_STARTING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STARTING);
}
if (event_group_bits & SpeakerEventGroupBits::STATE_RUNNING) {
ESP_LOGD(TAG, "Started Speaker");
ESP_LOGD(TAG, "Started");
this->state_ = speaker::STATE_RUNNING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_RUNNING);
this->status_clear_warning();
this->status_clear_error();
}
if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPING) {
ESP_LOGD(TAG, "Stopping Speaker");
ESP_LOGD(TAG, "Stopping");
this->state_ = speaker::STATE_STOPPING;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPING);
}
if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPED) {
if (!this->task_created_) {
ESP_LOGD(TAG, "Stopped Speaker");
ESP_LOGD(TAG, "Stopped");
this->state_ = speaker::STATE_STOPPED;
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS);
this->speaker_task_handle_ = nullptr;
@@ -140,20 +159,19 @@ void I2SAudioSpeaker::loop() {
}
if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) {
this->status_set_error("Failed to start speaker task");
this->status_set_error("Failed to start task");
xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START);
}
if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) {
uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS;
ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits)));
ESP_LOGW(TAG, "Writing failed: %s", esp_err_to_name(err_bit_to_esp_err(error_bits)));
this->status_set_warning();
}
if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED) {
this->status_set_error("Failed to adjust I2S bus to match the incoming audio");
ESP_LOGE(TAG,
"Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8,
this->status_set_error("Failed to adjust bus to match incoming audio");
ESP_LOGE(TAG, "Incompatible audio format: sample rate = %" PRIu32 ", channels = %u, bits per sample = %u",
this->audio_stream_info_.get_sample_rate(), this->audio_stream_info_.get_channels(),
this->audio_stream_info_.get_bits_per_sample());
}
@@ -202,7 +220,7 @@ void I2SAudioSpeaker::set_mute_state(bool mute_state) {
size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) {
if (this->is_failed()) {
ESP_LOGE(TAG, "Cannot play audio, speaker failed to setup");
ESP_LOGE(TAG, "Setup failed; cannot play audio");
return 0;
}
if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) {

View File

@@ -24,6 +24,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp
float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; }
void setup() override;
void dump_config() override;
void loop() override;
void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; }

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
// 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) {
this->component_state_ &= ~COMPONENT_STATE_MASK;
this->component_state_ |= COMPONENT_STATE_CONSTRUCTION;
if (this->is_failed()) {
this->reset_to_construction_state();
}
auto err = this->bus_->writev(this->address_, nullptr, 0);

View File

@@ -17,7 +17,7 @@ namespace light {
class LightOutput;
enum LightRestoreMode {
enum LightRestoreMode : uint8_t {
LIGHT_RESTORE_DEFAULT_OFF,
LIGHT_RESTORE_DEFAULT_ON,
LIGHT_ALWAYS_OFF,
@@ -212,12 +212,18 @@ class LightState : public EntityBase, public Component {
/// Store the output to allow effects to have more access.
LightOutput *output_;
/// Value for storing the index of the currently active effect. 0 if no effect is active
uint32_t active_effect_index_{};
/// The currently active transformer for this light (transition/flash).
std::unique_ptr<LightTransformer> transformer_{nullptr};
/// Whether the light value should be written in the next cycle.
bool next_write_{true};
/// List of effects for this light.
std::vector<LightEffect *> effects_;
/// Value for storing the index of the currently active effect. 0 if no effect is active
uint32_t active_effect_index_{};
/// Default transition length for all transitions in ms.
uint32_t default_transition_length_{};
/// Transition length to use for flash transitions.
uint32_t flash_transition_length_{};
/// Gamma correction factor for the light.
float gamma_correct_{};
/// Object used to store the persisted values of the light.
ESPPreferenceObject rtc_;
@@ -236,19 +242,13 @@ class LightState : public EntityBase, public Component {
*/
CallbackManager<void()> target_state_reached_callback_{};
/// Default transition length for all transitions in ms.
uint32_t default_transition_length_{};
/// Transition length to use for flash transitions.
uint32_t flash_transition_length_{};
/// Gamma correction factor for the light.
float gamma_correct_{};
/// Restore mode of the light.
LightRestoreMode restore_mode_;
/// Initial state of the light.
optional<LightStateRTCState> initial_state_{};
/// List of effects for this light.
std::vector<LightEffect *> effects_;
/// Restore mode of the light.
LightRestoreMode restore_mode_;
/// Whether the light value should be written in the next cycle.
bool next_write_{true};
// for effects, true if a transformer (transition) is active.
bool is_transformer_active_ = false;
};

View File

@@ -116,7 +116,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr
if (this->baud_rate_ > 0) {
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;
}
@@ -129,19 +129,6 @@ inline int Logger::level_for(const char *tag) {
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) {
// add 1 to buffer size for null terminator
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
@@ -189,7 +176,7 @@ void Logger::loop() {
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->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
// 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);

View File

@@ -156,7 +156,6 @@ class Logger : public Component {
#endif
protected:
void call_log_callbacks_(int level, const char *tag, 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
@@ -191,7 +190,7 @@ class Logger : public Component {
if (this->baud_rate_ > 0) {
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

View File

@@ -50,7 +50,7 @@ MCP23016_PIN_SCHEMA = pins.gpio_base_schema(
cv.int_range(min=0, max=15),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=validate_mode,
invertible=True,
invertable=True,
).extend(
{
cv.Required(CONF_MCP23016): cv.use_id(MCP23016),

View File

@@ -60,7 +60,7 @@ MCP23XXX_PIN_SCHEMA = pins.gpio_base_schema(
cv.int_range(min=0, max=15),
modes=[CONF_INPUT, CONF_OUTPUT, CONF_PULLUP],
mode_validator=validate_mode,
invertible=True,
invertable=True,
).extend(
{
cv.Required(CONF_MCP23XXX): cv.use_id(MCP23XXXBase),

View File

@@ -153,7 +153,7 @@ bool MQTTComponent::send_discovery_() {
if (node_friendly_name.empty()) {
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);
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);
} else {
this->state = state;
this->has_state_ = true;
this->set_has_state(true);
}
this->update_component_settings();

View File

@@ -33,6 +33,7 @@ bool Nextion::send_command_(const std::string &command) {
#ifdef USE_NEXTION_COMMAND_SPACING
if (!this->ignore_is_setup_ && !this->command_pacer_.can_send()) {
ESP_LOGN(TAG, "Command spacing: delaying command '%s'", command.c_str());
return false;
}
#endif // USE_NEXTION_COMMAND_SPACING
@@ -43,10 +44,6 @@ bool Nextion::send_command_(const std::string &command) {
const uint8_t to_send[3] = {0xFF, 0xFF, 0xFF};
this->write_array(to_send, sizeof(to_send));
#ifdef USE_NEXTION_COMMAND_SPACING
this->command_pacer_.mark_sent();
#endif // USE_NEXTION_COMMAND_SPACING
return true;
}
@@ -377,12 +374,6 @@ void Nextion::process_nextion_commands_() {
size_t commands_processed = 0;
#endif // USE_NEXTION_MAX_COMMANDS_PER_LOOP
#ifdef USE_NEXTION_COMMAND_SPACING
if (!this->command_pacer_.can_send()) {
return; // Will try again in next loop iteration
}
#endif
size_t to_process_length = 0;
std::string to_process;
@@ -430,6 +421,7 @@ void Nextion::process_nextion_commands_() {
}
#ifdef USE_NEXTION_COMMAND_SPACING
this->command_pacer_.mark_sent(); // Here is where we should mark the command as sent
ESP_LOGN(TAG, "Command spacing: marked command sent at %u ms", millis());
#endif
break;
case 0x02: // invalid Component ID or name was used

View File

@@ -337,23 +337,26 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
bool Nextion::upload_end_(bool 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) {
ESP_LOGD(TAG, "Restart");
delay(1500); // NOLINT
App.safe_reboot();
delay(1500); // NOLINT
} else {
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;
}

View File

@@ -337,15 +337,6 @@ bool Nextion::upload_tft(uint32_t baud_rate, bool exit_reparse) {
bool Nextion::upload_end_(bool 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) {
ESP_LOGD(TAG, "Restart");
@@ -353,7 +344,18 @@ bool Nextion::upload_end_(bool successful) {
App.safe_reboot();
} else {
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;
}

View File

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

View File

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

View File

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

View File

@@ -46,7 +46,7 @@ def set_sdkconfig_options(config):
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PANID", config[CONF_PAN_ID])
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_CHANNEL", config[CONF_CHANNEL])
add_idf_sdkconfig_option(
"CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}"
"CONFIG_OPENTHREAD_NETWORK_MASTERKEY", f"{config[CONF_NETWORK_KEY]:X}".lower()
)
if network_name := config.get(CONF_NETWORK_NAME):
@@ -54,14 +54,14 @@ def set_sdkconfig_options(config):
if (ext_pan_id := config.get(CONF_EXT_PAN_ID)) is not None:
add_idf_sdkconfig_option(
"CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}"
"CONFIG_OPENTHREAD_NETWORK_EXTPANID", f"{ext_pan_id:X}".lower()
)
if (mesh_local_prefix := config.get(CONF_MESH_LOCAL_PREFIX)) is not None:
add_idf_sdkconfig_option(
"CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix:X}"
"CONFIG_OPENTHREAD_MESH_LOCAL_PREFIX", f"{mesh_local_prefix}".lower()
)
if (pskc := config.get(CONF_PSKC)) is not None:
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}")
add_idf_sdkconfig_option("CONFIG_OPENTHREAD_NETWORK_PSKC", f"{pskc:X}".lower())
if CONF_FORCE_DATASET in config:
if config[CONF_FORCE_DATASET]:
@@ -98,7 +98,7 @@ _CONNECTION_SCHEMA = cv.Schema(
cv.Optional(CONF_EXT_PAN_ID): cv.hex_int,
cv.Optional(CONF_NETWORK_NAME): cv.string_strict,
cv.Optional(CONF_PSKC): cv.hex_int,
cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.hex_int,
cv.Optional(CONF_MESH_LOCAL_PREFIX): cv.ipv6network,
}
)

View File

@@ -137,7 +137,7 @@ void OpenThreadSrpComponent::setup() {
// Copy the mdns services to our local instance so that the c_str pointers remain valid for the lifetime of this
// component
this->mdns_services_ = this->mdns_->get_services();
ESP_LOGW(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size());
ESP_LOGD(TAG, "Setting up SRP services. count = %d\n", this->mdns_services_.size());
for (const auto &service : this->mdns_services_) {
otSrpClientBuffersServiceEntry *entry = otSrpClientBuffersAllocateService(instance);
if (!entry) {
@@ -185,11 +185,11 @@ void OpenThreadSrpComponent::setup() {
if (error != OT_ERROR_NONE) {
ESP_LOGW(TAG, "Failed to add service: %s", otThreadErrorToString(error));
}
ESP_LOGW(TAG, "Added service: %s", full_service.c_str());
ESP_LOGD(TAG, "Added service: %s", full_service.c_str());
}
otSrpClientEnableAutoStartMode(instance, srp_start_callback, nullptr);
ESP_LOGW(TAG, "Finished SRP setup");
ESP_LOGD(TAG, "Finished SRP setup");
}
void *OpenThreadSrpComponent::pool_alloc_(size_t size) {

View File

@@ -1,5 +1,6 @@
# Sourced from https://gist.github.com/agners/0338576e0003318b63ec1ea75adc90f9
import binascii
import ipaddress
from esphome.const import CONF_CHANNEL
@@ -37,6 +38,12 @@ def parse_tlv(tlv) -> dict:
if tag in TLV_TYPES:
if tag == 3:
output[TLV_TYPES[tag]] = val.decode("utf-8")
elif tag == 7:
mesh_local_prefix = binascii.hexlify(val).decode("utf-8")
mesh_local_prefix_str = f"{mesh_local_prefix}0000000000000000"
ipv6_bytes = bytes.fromhex(mesh_local_prefix_str)
ipv6_address = ipaddress.IPv6Address(ipv6_bytes)
output[TLV_TYPES[tag]] = f"{ipv6_address}/64"
else:
output[TLV_TYPES[tag]] = int.from_bytes(val)
return output

View File

@@ -53,7 +53,7 @@ PCF8574_PIN_SCHEMA = pins.gpio_base_schema(
cv.int_range(min=0, max=17),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=validate_mode,
invertible=True,
invertable=True,
).extend(
{
cv.Required(CONF_PCF8574): cv.use_id(PCF8574Component),

View File

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

View File

@@ -10,7 +10,7 @@ void Select::publish_state(const std::string &state) {
auto index = this->index_of(state);
const auto *name = this->get_name().c_str();
if (index.has_value()) {
this->has_state_ = true;
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), 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);
/// 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.
SelectCall make_call() { return SelectCall(this); }
@@ -73,7 +70,6 @@ class Select : public EntityBase {
virtual void control(const std::string &value) = 0;
CallbackManager<void(std::string, size_t)> state_callback_;
bool has_state_{false};
};
} // namespace select

View File

@@ -38,7 +38,9 @@ StateClass Sensor::get_state_class() {
void Sensor::publish_state(float 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);
@@ -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_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) {
@@ -88,13 +93,12 @@ float Sensor::get_raw_state() const { return this->raw_state; }
std::string Sensor::unique_id() { return ""; }
void Sensor::internal_send_state_to_frontend(float state) {
this->has_state_ = true;
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
this->get_unit_of_measurement().c_str(), this->get_accuracy_decimals());
this->callback_.call(state);
}
bool Sensor::has_state() const { return this->has_state_; }
} // namespace sensor
} // namespace esphome

View File

@@ -7,6 +7,7 @@
#include "esphome/components/sensor/filter.h"
#include <vector>
#include <memory>
namespace esphome {
namespace sensor {
@@ -140,9 +141,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
*/
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.
*
* @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);
protected:
CallbackManager<void(float)> raw_callback_; ///< Storage for raw state callbacks.
CallbackManager<void(float)> callback_; ///< Storage for filtered 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.
Filter *filter_list_{nullptr}; ///< Store all active filters.
optional<int8_t> accuracy_decimals_; ///< Accuracy in decimals override
optional<StateClass> state_class_{STATE_CLASS_NONE}; ///< State class override
bool force_update_{false}; ///< Force update mode
bool has_state_{false};
};
} // namespace sensor

View File

@@ -95,7 +95,7 @@ SN74HC595_PIN_SCHEMA = pins.gpio_base_schema(
cv.int_range(min=0, max=2047),
modes=[CONF_OUTPUT],
mode_validator=_validate_output_mode,
invertible=True,
invertable=True,
).extend(
{
cv.Required(CONF_SN74HC595): cv.use_id(SN74HC595Component),

View File

@@ -3,7 +3,6 @@
namespace esphome {
namespace spi {
#ifdef USE_ARDUINO
static const char *const TAG = "spi-esp-arduino";
@@ -38,17 +37,31 @@ class SPIDelegateHw : public SPIDelegate {
void write16(uint16_t data) override { this->channel_->transfer16(data); }
#ifdef USE_RP2040
void write_array(const uint8_t *ptr, size_t length) override {
// avoid overwriting the supplied buffer
uint8_t *rxbuf = new uint8_t[length]; // NOLINT(cppcoreguidelines-owning-memory)
memcpy(rxbuf, ptr, length);
this->channel_->transfer((void *) rxbuf, length);
delete[] rxbuf; // NOLINT(cppcoreguidelines-owning-memory)
}
if (length == 1) {
this->channel_->transfer(*ptr);
return;
}
#ifdef USE_RP2040
// avoid overwriting the supplied buffer. Use vector for automatic deallocation
auto rxbuf = std::vector<uint8_t>(length);
memcpy(rxbuf.data(), ptr, length);
this->channel_->transfer((void *) rxbuf.data(), length);
#elif defined(USE_ESP8266)
// ESP8266 SPI library requires the pointer to be word aligned, but the data may not be
// so we need to copy the data to a temporary buffer
if (reinterpret_cast<uintptr_t>(ptr) & 0x3) {
ESP_LOGVV(TAG, "SPI write buffer not word aligned, copying to temporary buffer");
auto txbuf = std::vector<uint8_t>(length);
memcpy(txbuf.data(), ptr, length);
this->channel_->writeBytes(txbuf.data(), length);
} else {
this->channel_->writeBytes(ptr, length);
}
#else
void write_array(const uint8_t *ptr, size_t length) override { this->channel_->writeBytes(ptr, length); }
this->channel_->writeBytes(ptr, length);
#endif
}
void read_array(uint8_t *ptr, size_t length) override { this->channel_->transfer(ptr, length); }

View File

@@ -9,10 +9,10 @@ namespace status_led {
static const char *const TAG = "status_led";
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_) {
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) {

View File

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

View File

@@ -21,7 +21,7 @@ const int RESTORE_MODE_PERSISTENT_MASK = 0x02;
const int RESTORE_MODE_INVERTED_MASK = 0x04;
const int RESTORE_MODE_DISABLED_MASK = 0x08;
enum SwitchRestoreMode {
enum SwitchRestoreMode : uint8_t {
SWITCH_ALWAYS_OFF = !RESTORE_MODE_ON_MASK,
SWITCH_ALWAYS_ON = RESTORE_MODE_ON_MASK,
SWITCH_RESTORE_DEFAULT_OFF = RESTORE_MODE_PERSISTENT_MASK,
@@ -49,12 +49,12 @@ class Switch : public EntityBase, public EntityBase_DeviceClass {
*/
void publish_state(bool state);
/// The current reported state of the binary sensor.
bool state;
/// Indicates whether or not state is to be retrieved from flash and how
SwitchRestoreMode restore_mode{SWITCH_RESTORE_DEFAULT_OFF};
/// The current reported state of the binary sensor.
bool state;
/** Turn this switch on. This is called by the front-end.
*
* For implementing switches, please override write_state.
@@ -123,10 +123,16 @@ class Switch : public EntityBase, public EntityBase_DeviceClass {
*/
virtual void write_state(bool state) = 0;
CallbackManager<void(bool)> state_callback_{};
bool inverted_{false};
Deduplicator<bool> publish_dedup_;
// Pointer first (4 bytes)
ESPPreferenceObject rtc_;
// CallbackManager (12 bytes on 32-bit - contains vector)
CallbackManager<void(bool)> state_callback_{};
// Small types grouped together
Deduplicator<bool> publish_dedup_; // 2 bytes (bool has_value_ + bool last_value_)
bool inverted_{false}; // 1 byte
// Total: 3 bytes, 1 byte padding
};
#define LOG_SWITCH(prefix, type, obj) log_switch((TAG), (prefix), LOG_STR_LITERAL(type), (obj))

View File

@@ -53,7 +53,7 @@ TCA9555_PIN_SCHEMA = pins.gpio_base_schema(
cv.int_range(min=0, max=15),
modes=[CONF_INPUT, CONF_OUTPUT],
mode_validator=validate_mode,
invertible=True,
invertable=True,
).extend(
{
cv.Required(CONF_TCA9555): cv.use_id(TCA9555Component),

View File

@@ -110,15 +110,7 @@ void TemplateAlarmControlPanel::loop() {
delay = this->arming_night_time_;
}
if ((millis() - this->last_update_) > delay) {
#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
this->bypass_before_arming();
this->publish_state(this->desired_state_);
}
return;
@@ -259,10 +251,23 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p
if (delay > 0) {
this->publish_state(ACP_STATE_ARMING);
} else {
this->bypass_before_arming();
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) {
if (call.get_state()) {
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_all_sensors_ready() { return this->sensors_ready_; };
void set_restore_mode(TemplateAlarmControlPanelRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
void bypass_before_arming();
#ifdef USE_BINARY_SENSOR
/** Add a binary_sensor to the alarm_panel.

View File

@@ -6,8 +6,16 @@ namespace template_ {
static const char *const TAG = "template.binary_sensor";
void TemplateBinarySensor::setup() { this->loop(); }
void TemplateBinarySensor::setup() {
if (!this->publish_initial_state_)
return;
if (this->f_ != nullptr) {
this->publish_initial_state(this->f_().value_or(false));
} else {
this->publish_initial_state(false);
}
}
void TemplateBinarySensor::loop() {
if (this->f_ == nullptr)
return;

View File

@@ -7,7 +7,7 @@ namespace text {
static const char *const TAG = "text";
void Text::publish_state(const std::string &state) {
this->has_state_ = true;
this->set_has_state(true);
this->state = state;
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());

View File

@@ -28,9 +28,6 @@ class Text : public EntityBase {
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.
TextCall make_call() { return TextCall(this); }
@@ -48,7 +45,6 @@ class Text : public EntityBase {
virtual void control(const std::string &value) = 0;
CallbackManager<void(std::string)> state_callback_;
bool has_state_{false};
};
} // namespace text

View File

@@ -8,7 +8,9 @@ static const char *const TAG = "text_sensor";
void TextSensor::publish_state(const std::string &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());
@@ -53,20 +55,22 @@ void TextSensor::add_on_state_callback(std::function<void(std::string)> callback
this->callback_.add(std::move(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_raw_state() const { return this->raw_state; }
void TextSensor::internal_send_state_to_frontend(const std::string &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());
this->callback_.call(state);
}
std::string TextSensor::unique_id() { return ""; }
bool TextSensor::has_state() { return this->has_state_; }
} // namespace text_sensor
} // namespace esphome

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ static const char *const TAG = "uptime.sensor";
void UptimeTimestampSensor::setup() {
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
auto now = this->time_->now();

View File

@@ -555,7 +555,7 @@ std::string WebServer::button_json(button::Button *obj, JsonDetail start_config)
#endif
#ifdef USE_BINARY_SENSOR
void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj) {
void WebServer::on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) {
if (this->events_.empty())
return;
this->events_.deferrable_send_state(obj, "state", binary_sensor_state_json_generator);

View File

@@ -269,7 +269,7 @@ class WebServer : public Controller, public Component, public AsyncWebHandler {
#endif
#ifdef USE_BINARY_SENSOR
void on_binary_sensor_update(binary_sensor::BinarySensor *obj) override;
void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state) override;
/// Handle a binary sensor request under '/binary_sensor/<id>'.
void handle_binary_sensor_request(AsyncWebServerRequest *request, const UrlMatch &match);

View File

@@ -8,8 +8,6 @@ CONFIG_SCHEMA = cv.All(
cv.only_with_esp_idf,
)
AUTO_LOAD = ["web_server"]
async def to_code(config):
# 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 "web_server_idf.h"
#ifdef USE_WEBSERVER
#include "esphome/components/web_server/web_server.h"
#include "esphome/components/web_server/list_entities.h"
#include "web_server_idf.h"
#endif // USE_WEBSERVER
namespace esphome {
namespace web_server_idf {
@@ -273,6 +275,7 @@ void AsyncResponseStream::printf(const char *fmt, ...) {
this->print(str);
}
#ifdef USE_WEBSERVER
AsyncEventSource::~AsyncEventSource() {
for (auto *ses : this->sessions_) {
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 esphome

View File

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

View File

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

View File

@@ -3,7 +3,15 @@
from contextlib import contextmanager
from dataclasses import dataclass
from datetime import datetime
from ipaddress import AddressValueError, IPv4Address, ip_address
from ipaddress import (
AddressValueError,
IPv4Address,
IPv4Network,
IPv6Address,
IPv6Network,
ip_address,
ip_network,
)
import logging
import os
import re
@@ -1176,6 +1184,14 @@ def ipv4address(value):
return address
def ipv6address(value):
try:
address = IPv6Address(value)
except AddressValueError as exc:
raise Invalid(f"{value} is not a valid IPv6 address") from exc
return address
def ipv4address_multi_broadcast(value):
address = ipv4address(value)
if not (address.is_multicast or (address == IPv4Address("255.255.255.255"))):
@@ -1193,6 +1209,33 @@ def ipaddress(value):
return address
def ipv4network(value):
"""Validate that the value is a valid IPv4 network."""
try:
network = IPv4Network(value, strict=False)
except ValueError as exc:
raise Invalid(f"{value} is not a valid IPv4 network") from exc
return network
def ipv6network(value):
"""Validate that the value is a valid IPv6 network."""
try:
network = IPv6Network(value, strict=False)
except ValueError as exc:
raise Invalid(f"{value} is not a valid IPv6 network") from exc
return network
def ipnetwork(value):
"""Validate that the value is a valid IP network."""
try:
network = ip_network(value, strict=False)
except ValueError as exc:
raise Invalid(f"{value} is not a valid IP network") from exc
return network
def _valid_topic(value):
"""Validate that this is a valid topic name/filter."""
if value is None: # Used to disable publishing and subscribing

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2025.7.0-dev"
__version__ = "2025.6.1"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
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(); });
do {
uint32_t new_app_state = STATUS_LED_WARNING;
uint8_t new_app_state = STATUS_LED_WARNING;
this->scheduler.call();
this->feed_wdt();
for (uint32_t j = 0; j <= i; j++) {
@@ -87,7 +87,7 @@ void Application::setup() {
this->calculate_looping_components_();
}
void Application::loop() {
uint32_t new_app_state = 0;
uint8_t new_app_state = 0;
this->scheduler.call();
@@ -117,7 +117,9 @@ void Application::loop() {
// Use the last component's end time instead of calling millis() again
auto elapsed = last_op_end_time - this->last_loop_;
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 {
uint32_t delay_time = this->loop_interval_ - elapsed;
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);
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;
@@ -215,7 +217,7 @@ void Application::teardown_components(uint32_t timeout_ms) {
// Give some time for I/O operations if components are still pending
if (!pending_components.empty()) {
this->delay_with_select_(1);
this->yield_with_select_(1);
}
// 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()
// 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
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)
return false;
@@ -302,7 +302,9 @@ bool Application::is_socket_ready(int fd) const {
}
#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
if (!this->socket_fds_.empty()) {
// 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);
delay(delay_ms);
}
// When delay_ms is 0, we need to yield since select(0) doesn't yield
if (delay_ms == 0) {
yield();
}
} else {
// No sockets registered, use regular delay
delay(delay_ms);

View File

@@ -87,8 +87,8 @@ static const uint32_t TEARDOWN_TIMEOUT_REBOOT_MS = 1000; // 1 second for quick
class Application {
public:
void pre_setup(const std::string &name, const std::string &friendly_name, const std::string &area,
const char *comment, const char *compilation_time, bool name_add_mac_suffix) {
void pre_setup(const std::string &name, const std::string &friendly_name, const char *area, const char *comment,
const char *compilation_time, bool name_add_mac_suffix) {
arch_init();
this->name_add_mac_suffix_ = 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_; }
/// 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().
std::string get_comment() const { return this->comment_; }
@@ -332,7 +332,7 @@ class Application {
*/
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
const std::vector<binary_sensor::BinarySensor *> &get_binary_sensors() { return this->binary_sensors_; }
@@ -575,7 +575,7 @@ class Application {
void feed_wdt_arch_();
/// 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 *> looping_components_{};
@@ -646,14 +646,14 @@ class Application {
std::string name_;
std::string friendly_name_;
std::string area_;
const char *area_{nullptr};
const char *comment_{nullptr};
const char *compilation_time_{nullptr};
bool name_add_mac_suffix_;
uint32_t last_loop_{0};
uint32_t loop_interval_{16};
size_t dump_config_at_{SIZE_MAX};
uint32_t app_state_{0};
uint8_t app_state_{0};
Component *current_component_{nullptr};
uint32_t loop_component_start_time_{0};

View File

@@ -1,6 +1,7 @@
#include "esphome/core/component.h"
#include <cinttypes>
#include <limits>
#include <utility>
#include "esphome/core/application.h"
#include "esphome/core/hal.h"
@@ -29,18 +30,20 @@ const float LATE = -100.0f;
} // namespace setup_priority
const uint32_t COMPONENT_STATE_MASK = 0xFF;
const uint32_t COMPONENT_STATE_CONSTRUCTION = 0x00;
const uint32_t COMPONENT_STATE_SETUP = 0x01;
const uint32_t COMPONENT_STATE_LOOP = 0x02;
const uint32_t COMPONENT_STATE_FAILED = 0x03;
const uint32_t STATUS_LED_MASK = 0xFF00;
const uint32_t STATUS_LED_OK = 0x0000;
const uint32_t STATUS_LED_WARNING = 0x0100;
const uint32_t STATUS_LED_ERROR = 0x0200;
// Component state uses bits 0-1 (4 states)
const uint8_t COMPONENT_STATE_MASK = 0x03;
const uint8_t COMPONENT_STATE_CONSTRUCTION = 0x00;
const uint8_t COMPONENT_STATE_SETUP = 0x01;
const uint8_t COMPONENT_STATE_LOOP = 0x02;
const uint8_t COMPONENT_STATE_FAILED = 0x03;
// Status LED uses bits 2-3
const uint8_t STATUS_LED_MASK = 0x0C;
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 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_OVER_MS = 50U; ///< Initial blocking time allowed without warning
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)
@@ -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() {
uint32_t state = this->component_state_ & COMPONENT_STATE_MASK;
uint8_t state = this->component_state_ & COMPONENT_STATE_MASK;
switch (state) {
case COMPONENT_STATE_CONSTRUCTION:
// 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) {
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 false;
@@ -131,6 +140,18 @@ void Component::mark_failed() {
this->component_state_ |= COMPONENT_STATE_FAILED;
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
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); \
}
extern const uint32_t COMPONENT_STATE_MASK;
extern const uint32_t COMPONENT_STATE_CONSTRUCTION;
extern const uint32_t COMPONENT_STATE_SETUP;
extern const uint32_t COMPONENT_STATE_LOOP;
extern const uint32_t COMPONENT_STATE_FAILED;
extern const uint32_t STATUS_LED_MASK;
extern const uint32_t STATUS_LED_OK;
extern const uint32_t STATUS_LED_WARNING;
extern const uint32_t STATUS_LED_ERROR;
extern const uint8_t COMPONENT_STATE_MASK;
extern const uint8_t COMPONENT_STATE_CONSTRUCTION;
extern const uint8_t COMPONENT_STATE_SETUP;
extern const uint8_t COMPONENT_STATE_LOOP;
extern const uint8_t COMPONENT_STATE_FAILED;
extern const uint8_t STATUS_LED_MASK;
extern const uint8_t STATUS_LED_OK;
extern const uint8_t STATUS_LED_WARNING;
extern const uint8_t STATUS_LED_ERROR;
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 {
public:
@@ -123,7 +123,19 @@ class Component {
*/
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.
*
@@ -298,10 +310,15 @@ class Component {
/// Cancel a defer callback using the specified name, name must not be empty.
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};
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_{};
};

View File

@@ -7,10 +7,8 @@ namespace esphome {
void Controller::setup_controller(bool include_internal) {
#ifdef USE_BINARY_SENSOR
for (auto *obj : App.get_binary_sensors()) {
if (include_internal || !obj->is_internal()) {
obj->add_full_state_callback(
[this, obj](optional<bool> previous, optional<bool> state) { this->on_binary_sensor_update(obj); });
}
if (include_internal || !obj->is_internal())
obj->add_on_state_callback([this, obj](bool state) { this->on_binary_sensor_update(obj, state); });
}
#endif
#ifdef USE_FAN

View File

@@ -71,7 +71,7 @@ class Controller {
public:
void setup_controller(bool include_internal = false);
#ifdef USE_BINARY_SENSOR
virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj){};
virtual void on_binary_sensor_update(binary_sensor::BinarySensor *obj, bool state){};
#endif
#ifdef USE_FAN
virtual void on_fan_update(fan::Fan *obj){};

View File

@@ -12,20 +12,12 @@ void EntityBase::set_name(const char *name) {
this->name_ = StringRef(name);
if (this->name_.empty()) {
this->name_ = StringRef(App.get_friendly_name());
this->has_own_name_ = false;
this->flags_.has_own_name = false;
} 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
std::string EntityBase::get_icon() const {
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; }
// 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
std::string EntityBase::get_object_id() const {
// 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.
return str_sanitize(str_snake_case(App.get_friendly_name()));
} else {
@@ -61,7 +49,7 @@ void EntityBase::set_object_id(const char *object_id) {
// Calculate Object ID Hash from Entity Name
void EntityBase::calc_object_id_() {
// 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.
const auto object_id = str_sanitize(str_snake_case(App.get_friendly_name()));
// FNV-1 hash

View File

@@ -3,8 +3,6 @@
#include <string>
#include <cstdint>
#include "string_ref.h"
#include "helpers.h"
#include "log.h"
namespace esphome {
@@ -22,7 +20,7 @@ class EntityBase {
void set_name(const char *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.
std::string get_object_id() const;
@@ -32,23 +30,31 @@ class EntityBase {
uint32_t get_object_id_hash();
// Get/set whether this Entity should be hidden outside ESPHome
bool is_internal() const;
void set_internal(bool internal);
bool is_internal() const { return this->flags_.internal; }
void set_internal(bool internal) { this->flags_.internal = internal; }
// 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
// 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;
void set_disabled_by_default(bool disabled_by_default);
bool is_disabled_by_default() const { return this->flags_.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.
EntityCategory get_entity_category() const;
void set_entity_category(EntityCategory entity_category);
EntityCategory get_entity_category() const { return static_cast<EntityCategory>(this->flags_.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
std::string get_icon() const;
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:
/// The hash_base() function has been deprecated. It is kept in this
/// class for now, to prevent external components from not compiling.
@@ -59,11 +65,16 @@ class EntityBase {
const char *object_id_c_str_{nullptr};
const char *icon_c_str_{nullptr};
uint32_t object_id_hash_{};
bool has_own_name_{false};
bool internal_{false};
bool disabled_by_default_{false};
EntityCategory entity_category_{ENTITY_CATEGORY_NONE};
bool has_state_{};
// Bit-packed flags to save memory (1 byte instead of 5)
struct EntityFlags {
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)
@@ -88,58 +99,4 @@ class EntityBase_UnitOfMeasurement { // NOLINT(readability-identifier-naming)
const char *unit_of_measurement_{nullptr}; ///< Unit of measurement override
};
/**
* An entity that has a state.
* @tparam T The type of the state
*/
template<typename T> class StatefulEntityBase : public EntityBase {
public:
virtual bool has_state() const { return this->state_.has_value(); }
virtual const T &get_state() const { return this->state_.value(); }
virtual T get_state_default(T default_value) const { return this->state_.value_or(default_value); }
void invalidate_state() { this->set_state_({}); }
void add_full_state_callback(std::function<void(optional<T> previous, optional<T> current)> &&callback) {
if (this->full_state_callbacks_ == nullptr)
this->full_state_callbacks_ = new CallbackManager<void(optional<T> previous, optional<T> current)>(); // NOLINT
this->full_state_callbacks_->add(std::move(callback));
}
void add_on_state_callback(std::function<void(T)> &&callback) {
if (this->state_callbacks_ == nullptr)
this->state_callbacks_ = new CallbackManager<void(T)>(); // NOLINT
this->state_callbacks_->add(std::move(callback));
}
void set_trigger_on_initial_state(bool trigger_on_initial_state) {
this->trigger_on_initial_state_ = trigger_on_initial_state;
}
protected:
optional<T> state_{};
/**
* Set a new state for this entity. This will trigger callbacks only if the new state is different from the previous.
*
* @param state The new state.
* @return True if the state was changed, false if it was the same as before.
*/
bool set_state_(const optional<T> &state) {
if (this->state_ != state) {
// call the full state callbacks with the previous and new state
if (this->full_state_callbacks_ != nullptr)
this->full_state_callbacks_->call(this->state_, state);
// trigger legacy callbacks only if the new state is valid and either the trigger on initial state is enabled or
// the previous state was valid
auto had_state = this->has_state();
this->state_ = state;
if (this->state_callbacks_ != nullptr && state.has_value() && (this->trigger_on_initial_state_ || had_state))
this->state_callbacks_->call(state.value());
return true;
}
return false;
}
bool trigger_on_initial_state_{true};
// callbacks with full state and previous state
CallbackManager<void(optional<T> previous, optional<T> current)> *full_state_callbacks_{};
CallbackManager<void(T)> *state_callbacks_{};
};
} // namespace esphome

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().
enum ParseOnOffState {
enum ParseOnOffState : uint8_t {
PARSE_NONE = 0,
PARSE_ON,
PARSE_OFF,

Some files were not shown because too many files have changed in this diff Show More