1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-01 15:41:52 +00:00

Compare commits

...

55 Commits

Author SHA1 Message Date
Jesse Hills
50b7349fe0 Merge pull request #9234 from esphome/bump-2025.6.2
2025.6.2
2025-06-28 15:47:02 +12:00
Jonathan Swoboda
61b3379f48 [i2c] Disable i2c scan on certain idf versions (#9237) 2025-06-28 13:33:05 +12:00
Samuel Sieb
5010a0f5e7 [mcp23xxx_base] fix pin interrupts (#9244)
Co-authored-by: Samuel Sieb <samuel@sieb.net>
2025-06-28 13:32:57 +12:00
Jesse Hills
948aa13fb9 Bump version to 2025.6.2 2025-06-27 23:16:13 +12:00
scaiper
9e993ac603 [esp32] Change `enable_lwip_mdns_queries default to True` (#9188) 2025-06-27 23:16:12 +12:00
Kevin Ahrendt
9f3f4ead4f [voice_assistant] Support streaming TTS responses and fixes crash for long responses (#9224) 2025-06-27 23:16:12 +12:00
Kevin Ahrendt
068aa0ff1e [speaker] bugfix: continue to block tasks if stop flag is set (#9222) 2025-06-27 23:16:12 +12:00
Kevin Ahrendt
e146c0796a [audio] Bugfix: improve timeout handling (#9221) 2025-06-27 23:16:12 +12:00
Clyde Stubbs
cceab26bfb [lvgl] Fix dangling pointer issue with qrcode (#9190) 2025-06-27 23:16:12 +12:00
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
106 changed files with 2971 additions and 916 deletions

View File

@@ -490,7 +490,7 @@ esphome/components/vbus/* @ssieb
esphome/components/veml3235/* @kbx81
esphome/components/veml7700/* @latonita
esphome/components/version/* @esphome/core
esphome/components/voice_assistant/* @jesserockz
esphome/components/voice_assistant/* @jesserockz @kahrendt
esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
esphome/components/watchdog/* @oarcher
esphome/components/waveshare_epaper/* @clydebarrow

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.6.0b1
PROJECT_NUMBER = 2025.6.2
# 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

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

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

@@ -5,6 +5,7 @@
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#if CONFIG_MBEDTLS_CERTIFICATE_BUNDLE
#include "esp_crt_bundle.h"
@@ -16,13 +17,13 @@ namespace audio {
static const uint32_t READ_WRITE_TIMEOUT_MS = 20;
static const uint32_t CONNECTION_TIMEOUT_MS = 5000;
// The number of times the http read times out with no data before throwing an error
static const uint32_t ERROR_COUNT_NO_DATA_READ_TIMEOUT = 100;
static const uint8_t MAX_FETCHING_HEADER_ATTEMPTS = 6;
static const size_t HTTP_STREAM_BUFFER_SIZE = 2048;
static const uint8_t MAX_REDIRECTION = 5;
static const uint8_t MAX_REDIRECTIONS = 5;
static const char *const TAG = "audio_reader";
// Some common HTTP status codes - borrowed from http_request component accessed 20241224
enum HttpStatus {
@@ -94,7 +95,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
client_config.url = uri.c_str();
client_config.cert_pem = nullptr;
client_config.disable_auto_redirect = false;
client_config.max_redirection_count = 10;
client_config.max_redirection_count = MAX_REDIRECTIONS;
client_config.event_handler = http_event_handler;
client_config.user_data = this;
client_config.buffer_size = HTTP_STREAM_BUFFER_SIZE;
@@ -116,12 +117,29 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
esp_err_t err = esp_http_client_open(this->client_, 0);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to open URL");
this->cleanup_connection_();
return err;
}
int64_t header_length = esp_http_client_fetch_headers(this->client_);
uint8_t reattempt_count = 0;
while ((header_length < 0) && (reattempt_count < MAX_FETCHING_HEADER_ATTEMPTS)) {
this->cleanup_connection_();
if (header_length != -ESP_ERR_HTTP_EAGAIN) {
// Serious error, no recovery
return ESP_FAIL;
} else {
// Reconnect from a fresh state to avoid a bug where it never reads the headers even if made available
this->client_ = esp_http_client_init(&client_config);
esp_http_client_open(this->client_, 0);
header_length = esp_http_client_fetch_headers(this->client_);
++reattempt_count;
}
}
if (header_length < 0) {
ESP_LOGE(TAG, "Failed to fetch headers");
this->cleanup_connection_();
return ESP_FAIL;
}
@@ -135,7 +153,7 @@ esp_err_t AudioReader::start(const std::string &uri, AudioFileType &file_type) {
ssize_t redirect_count = 0;
while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTION)) {
while ((esp_http_client_set_redirection(this->client_) == ESP_OK) && (redirect_count < MAX_REDIRECTIONS)) {
err = esp_http_client_open(this->client_, 0);
if (err != ESP_OK) {
this->cleanup_connection_();
@@ -267,27 +285,29 @@ AudioReaderState AudioReader::http_read_() {
return AudioReaderState::FINISHED;
}
} else if (this->output_transfer_buffer_->free() > 0) {
size_t bytes_to_read = this->output_transfer_buffer_->free();
int received_len =
esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read);
int received_len = esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(),
this->output_transfer_buffer_->free());
if (received_len > 0) {
this->output_transfer_buffer_->increase_buffer_length(received_len);
this->last_data_read_ms_ = millis();
} else if (received_len < 0) {
return AudioReaderState::READING;
} else if (received_len <= 0) {
// HTTP read error
this->cleanup_connection_();
return AudioReaderState::FAILED;
} else {
if (bytes_to_read > 0) {
// Read timed out
if ((millis() - this->last_data_read_ms_) > CONNECTION_TIMEOUT_MS) {
this->cleanup_connection_();
return AudioReaderState::FAILED;
}
delay(READ_WRITE_TIMEOUT_MS);
if (received_len == -1) {
// A true connection error occured, no chance at recovery
this->cleanup_connection_();
return AudioReaderState::FAILED;
}
// Read timed out, manually verify if it has been too long since the last successful read
if ((millis() - this->last_data_read_ms_) > MAX_FETCHING_HEADER_ATTEMPTS * CONNECTION_TIMEOUT_MS) {
ESP_LOGE(TAG, "Timed out");
this->cleanup_connection_();
return AudioReaderState::FAILED;
}
delay(READ_WRITE_TIMEOUT_MS);
}
}

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

@@ -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
@@ -593,7 +605,7 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_MDNS_QUERIES, default=False
CONF_ENABLE_LWIP_MDNS_QUERIES, default=True
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False
@@ -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,
)
@@ -733,7 +760,7 @@ async def to_code(config):
and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER]
):
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, False):
if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True):
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)

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

@@ -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

@@ -1,5 +1,8 @@
import logging
from esphome import pins
import esphome.codegen as cg
from esphome.components import esp32
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
@@ -12,6 +15,8 @@ from esphome.const import (
CONF_SCL,
CONF_SDA,
CONF_TIMEOUT,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
PLATFORM_ESP32,
PLATFORM_ESP8266,
PLATFORM_RP2040,
@@ -19,6 +24,7 @@ from esphome.const import (
from esphome.core import CORE, coroutine_with_priority
import esphome.final_validate as fv
LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
i2c_ns = cg.esphome_ns.namespace("i2c")
I2CBus = i2c_ns.class_("I2CBus")
@@ -40,6 +46,32 @@ def _bus_declare_type(value):
raise NotImplementedError
def validate_config(config):
if (
config[CONF_SCAN]
and CORE.is_esp32
and CORE.using_esp_idf
and esp32.get_esp32_variant()
in [
esp32.const.VARIANT_ESP32C5,
esp32.const.VARIANT_ESP32C6,
esp32.const.VARIANT_ESP32P4,
]
):
version: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
if version.major == 5 and (
(version.minor == 3 and version.patch <= 3)
or (version.minor == 4 and version.patch <= 1)
):
LOGGER.warning(
"There is a bug in esp-idf version %s that breaks I2C scan, I2C scan "
"has been disabled, see https://github.com/esphome/issues/issues/7128",
str(version),
)
config[CONF_SCAN] = False
return config
pin_with_input_and_output_support = pins.internal_gpio_pin_number(
{CONF_OUTPUT: True, CONF_INPUT: True}
)
@@ -65,6 +97,7 @@ CONFIG_SCHEMA = cv.All(
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on([PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_RP2040]),
validate_config,
)

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

@@ -3,7 +3,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_SIZE, CONF_TEXT
from esphome.cpp_generator import MockObjClass
from ..defines import CONF_MAIN, literal
from ..defines import CONF_MAIN
from ..lv_validation import color, color_retmapper, lv_text
from ..lvcode import LocalVariable, lv, lv_expr
from ..schemas import TEXT_SCHEMA
@@ -34,7 +34,7 @@ class QrCodeType(WidgetType):
)
def get_uses(self):
return ("canvas", "img")
return ("canvas", "img", "label")
def obj_creator(self, parent: MockObjClass, config: dict):
dark_color = color_retmapper(config[CONF_DARK_COLOR])
@@ -45,10 +45,8 @@ class QrCodeType(WidgetType):
async def to_code(self, w: Widget, config):
if (value := config.get(CONF_TEXT)) is not None:
value = await lv_text.process(value)
with LocalVariable(
"qr_text", cg.const_char_ptr, value, modifier=""
) as str_obj:
lv.qrcode_update(w.obj, str_obj, literal(f"strlen({str_obj})"))
with LocalVariable("qr_text", cg.std_string, value, modifier="") as str_obj:
lv.qrcode_update(w.obj, str_obj.c_str(), str_obj.size())
qr_code_spec = QrCodeType()

View File

@@ -6,7 +6,11 @@ namespace mcp23xxx_base {
float MCP23XXXBase::get_setup_priority() const { return setup_priority::IO; }
void MCP23XXXGPIOPin::setup() { pin_mode(flags_); }
void MCP23XXXGPIOPin::setup() {
pin_mode(flags_);
this->parent_->pin_interrupt_mode(this->pin_, this->interrupt_mode_);
}
void MCP23XXXGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); }
bool MCP23XXXGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; }
void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); }

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

@@ -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

@@ -343,13 +343,12 @@ void AudioPipeline::read_task(void *params) {
xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED);
// Wait until the pipeline notifies us the source of the media file
EventBits_t event_bits =
xEventGroupWaitBits(this_pipeline->event_group_,
EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP |
EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read
pdFALSE, // Clear the bit on exit
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
EventBits_t event_bits = xEventGroupWaitBits(
this_pipeline->event_group_,
EventGroupBits::READER_COMMAND_INIT_FILE | EventGroupBits::READER_COMMAND_INIT_HTTP, // Bit message to read
pdFALSE, // Clear the bit on exit
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
if (!(event_bits & EventGroupBits::PIPELINE_COMMAND_STOP)) {
xEventGroupClearBits(this_pipeline->event_group_, EventGroupBits::READER_MESSAGE_FINISHED |
@@ -434,12 +433,12 @@ void AudioPipeline::decode_task(void *params) {
xEventGroupSetBits(this_pipeline->event_group_, EventGroupBits::DECODER_MESSAGE_FINISHED);
// Wait until the reader notifies us that the media type is available
EventBits_t event_bits = xEventGroupWaitBits(this_pipeline->event_group_,
EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE |
EventGroupBits::PIPELINE_COMMAND_STOP, // Bit message to read
pdFALSE, // Clear the bit on exit
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
EventBits_t event_bits =
xEventGroupWaitBits(this_pipeline->event_group_,
EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE, // Bit message to read
pdFALSE, // Clear the bit on exit
pdFALSE, // Wait for all the bits,
portMAX_DELAY); // Block indefinitely until bit is set
xEventGroupClearBits(this_pipeline->event_group_,
EventGroupBits::DECODER_MESSAGE_FINISHED | EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE);

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

@@ -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

@@ -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

@@ -17,10 +17,11 @@ from esphome.const import (
AUTO_LOAD = ["socket"]
DEPENDENCIES = ["api", "microphone"]
CODEOWNERS = ["@jesserockz"]
CODEOWNERS = ["@jesserockz", "@kahrendt"]
CONF_ON_END = "on_end"
CONF_ON_INTENT_END = "on_intent_end"
CONF_ON_INTENT_PROGRESS = "on_intent_progress"
CONF_ON_INTENT_START = "on_intent_start"
CONF_ON_LISTENING = "on_listening"
CONF_ON_START = "on_start"
@@ -136,6 +137,9 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_ON_INTENT_START): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_INTENT_PROGRESS): automation.validate_automation(
single=True
),
cv.Optional(CONF_ON_INTENT_END): automation.validate_automation(
single=True
),
@@ -282,6 +286,13 @@ async def to_code(config):
config[CONF_ON_INTENT_START],
)
if CONF_ON_INTENT_PROGRESS in config:
await automation.build_automation(
var.get_intent_progress_trigger(),
[(cg.std_string, "x")],
config[CONF_ON_INTENT_PROGRESS],
)
if CONF_ON_INTENT_END in config:
await automation.build_automation(
var.get_intent_end_trigger(),

View File

@@ -555,7 +555,7 @@ void VoiceAssistant::request_stop() {
break;
case State::AWAITING_RESPONSE:
this->signal_stop_();
break;
// Fallthrough intended to stop a streaming TTS announcement that has potentially started
case State::STREAMING_RESPONSE:
#ifdef USE_MEDIA_PLAYER
// Stop any ongoing media player announcement
@@ -599,6 +599,14 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
switch (msg.event_type) {
case api::enums::VOICE_ASSISTANT_RUN_START:
ESP_LOGD(TAG, "Assist Pipeline running");
#ifdef USE_MEDIA_PLAYER
this->started_streaming_tts_ = false;
for (auto arg : msg.data) {
if (arg.name == "url") {
this->tts_response_url_ = std::move(arg.value);
}
}
#endif
this->defer([this]() { this->start_trigger_->trigger(); });
break;
case api::enums::VOICE_ASSISTANT_WAKE_WORD_START:
@@ -622,6 +630,8 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
if (text.empty()) {
ESP_LOGW(TAG, "No text in STT_END event");
return;
} else if (text.length() > 500) {
text = text.substr(0, 497) + "...";
}
ESP_LOGD(TAG, "Speech recognised as: \"%s\"", text.c_str());
this->defer([this, text]() { this->stt_end_trigger_->trigger(text); });
@@ -631,6 +641,27 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
ESP_LOGD(TAG, "Intent started");
this->defer([this]() { this->intent_start_trigger_->trigger(); });
break;
case api::enums::VOICE_ASSISTANT_INTENT_PROGRESS: {
ESP_LOGD(TAG, "Intent progress");
std::string tts_url_for_trigger = "";
#ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) {
for (const auto &arg : msg.data) {
if ((arg.name == "tts_start_streaming") && (arg.value == "1") && !this->tts_response_url_.empty()) {
this->media_player_->make_call().set_media_url(this->tts_response_url_).set_announcement(true).perform();
this->media_player_wait_for_announcement_start_ = true;
this->media_player_wait_for_announcement_end_ = false;
this->started_streaming_tts_ = true;
tts_url_for_trigger = this->tts_response_url_;
this->tts_response_url_.clear(); // Reset streaming URL
}
}
}
#endif
this->defer([this, tts_url_for_trigger]() { this->intent_progress_trigger_->trigger(tts_url_for_trigger); });
break;
}
case api::enums::VOICE_ASSISTANT_INTENT_END: {
for (auto arg : msg.data) {
if (arg.name == "conversation_id") {
@@ -653,6 +684,9 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
ESP_LOGW(TAG, "No text in TTS_START event");
return;
}
if (text.length() > 500) {
text = text.substr(0, 497) + "...";
}
ESP_LOGD(TAG, "Response: \"%s\"", text.c_str());
this->defer([this, text]() {
this->tts_start_trigger_->trigger(text);
@@ -678,7 +712,7 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) {
ESP_LOGD(TAG, "Response URL: \"%s\"", url.c_str());
this->defer([this, url]() {
#ifdef USE_MEDIA_PLAYER
if (this->media_player_ != nullptr) {
if ((this->media_player_ != nullptr) && (!this->started_streaming_tts_)) {
this->media_player_->make_call().set_media_url(url).set_announcement(true).perform();
this->media_player_wait_for_announcement_start_ = true;

View File

@@ -177,6 +177,7 @@ class VoiceAssistant : public Component {
Trigger<> *get_intent_end_trigger() const { return this->intent_end_trigger_; }
Trigger<> *get_intent_start_trigger() const { return this->intent_start_trigger_; }
Trigger<std::string> *get_intent_progress_trigger() const { return this->intent_progress_trigger_; }
Trigger<> *get_listening_trigger() const { return this->listening_trigger_; }
Trigger<> *get_end_trigger() const { return this->end_trigger_; }
Trigger<> *get_start_trigger() const { return this->start_trigger_; }
@@ -233,6 +234,7 @@ class VoiceAssistant : public Component {
Trigger<> *tts_stream_start_trigger_ = new Trigger<>();
Trigger<> *tts_stream_end_trigger_ = new Trigger<>();
#endif
Trigger<std::string> *intent_progress_trigger_ = new Trigger<std::string>();
Trigger<> *wake_word_detected_trigger_ = new Trigger<>();
Trigger<std::string> *stt_end_trigger_ = new Trigger<std::string>();
Trigger<std::string> *tts_end_trigger_ = new Trigger<std::string>();
@@ -268,6 +270,8 @@ class VoiceAssistant : public Component {
#endif
#ifdef USE_MEDIA_PLAYER
media_player::MediaPlayer *media_player_{nullptr};
std::string tts_response_url_{""};
bool started_streaming_tts_{false};
bool media_player_wait_for_announcement_start_{false};
bool media_player_wait_for_announcement_end_{false};
#endif

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.6.0b1"
__version__ = "2025.6.2"
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

@@ -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

@@ -20,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;
@@ -29,24 +29,32 @@ class EntityBase {
// Get the unique Object ID of this Entity
uint32_t get_object_id_hash();
// Get/set whether this Entity should be hidden from outside of ESPHome
bool is_internal() const;
void set_internal(bool internal);
// Get/set whether this Entity should be hidden outside ESPHome
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.
@@ -56,11 +64,17 @@ class EntityBase {
StringRef name_;
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};
uint32_t object_id_hash_{};
// 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)

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,

View File

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

View File

@@ -5,7 +5,7 @@ import fnmatch
import functools
import inspect
from io import BytesIO, TextIOBase, TextIOWrapper
from ipaddress import _BaseAddress
from ipaddress import _BaseAddress, _BaseNetwork
import logging
import math
import os
@@ -621,6 +621,7 @@ ESPHomeDumper.add_multi_representer(str, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(int, ESPHomeDumper.represent_int)
ESPHomeDumper.add_multi_representer(float, ESPHomeDumper.represent_float)
ESPHomeDumper.add_multi_representer(_BaseAddress, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(_BaseNetwork, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(MACAddress, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(TimePeriod, ESPHomeDumper.represent_stringify)
ESPHomeDumper.add_multi_representer(Lambda, ESPHomeDumper.represent_lambda)

View File

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

View File

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

View File

@@ -646,7 +646,9 @@ lvgl:
on_click:
lvgl.qrcode.update:
id: lv_qr
text: homeassistant.io
text:
format: "A string with a number %d"
args: ['(int)(random_uint32() % 1000)']
- slider:
min_value: 0

View File

@@ -8,4 +8,6 @@ openthread:
pan_id: 0x8f28
ext_pan_id: 0xd63e8e3e495ebbc3
pskc: 0xc23a76e98f1a6483639b1ac1271e2e27
mesh_local_prefix: fd53:145f:ed22:ad81::/64
force_dataset: true

View File

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

View File

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

View File

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

View File

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

View File

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

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