ESPHome 2026.3.0-dev
Loading...
Searching...
No Matches
api_connection.cpp
Go to the documentation of this file.
1#include "api_connection.h"
2#ifdef USE_API
3#ifdef USE_API_NOISE
5#endif
6#ifdef USE_API_PLAINTEXT
8#endif
9#ifdef USE_API_USER_DEFINED_ACTIONS
10#include "user_services.h"
11#endif
12#include <cerrno>
13#include <cinttypes>
14#include <functional>
15#include <limits>
16#include <new>
17#include <utility>
18#ifdef USE_ESP8266
19#include <pgmspace.h>
20#endif
24#include "esphome/core/hal.h"
25#include "esphome/core/log.h"
27
28#ifdef USE_DEEP_SLEEP
30#endif
31#ifdef USE_HOMEASSISTANT_TIME
33#endif
34#ifdef USE_BLUETOOTH_PROXY
36#endif
37#ifdef USE_CLIMATE
39#endif
40#ifdef USE_VOICE_ASSISTANT
42#endif
43#ifdef USE_ZWAVE_PROXY
45#endif
46#ifdef USE_WATER_HEATER
48#endif
49#ifdef USE_INFRARED
51#endif
52
53namespace esphome::api {
54
55// Read a maximum of 5 messages per loop iteration to prevent starving other components.
56// This is a balance between API responsiveness and allowing other components to run.
57// Since each message could contain multiple protobuf messages when using packet batching,
58// this limits the number of messages processed, not the number of TCP packets.
59static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 5;
60static constexpr uint8_t MAX_PING_RETRIES = 60;
61static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
62static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
63// Timeout for completing the handshake (Noise transport + HelloRequest).
64// A stalled handshake from a buggy client or network glitch holds a connection
65// slot, which can prevent legitimate clients from reconnecting. Also hardens
66// against the less likely case of intentional connection slot exhaustion.
67static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 15000;
68
69static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
70
71static const char *const TAG = "api.connection";
72#ifdef USE_CAMERA
73static const int CAMERA_STOP_STREAM = 5000;
74#endif
75
76#ifdef USE_DEVICES
77// Helper macro for entity command handlers - gets entity by key and device_id, returns if not found, and creates call
78// object
79#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
80 entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \
81 if ((entity_var) == nullptr) \
82 return; \
83 auto call = (entity_var)->make_call();
84
85// Helper macro for entity command handlers that don't use make_call() - gets entity by key and device_id and returns if
86// not found
87#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
88 entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \
89 if ((entity_var) == nullptr) \
90 return;
91#else // No device support, use simpler macros
92// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call
93// object
94#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
95 entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
96 if ((entity_var) == nullptr) \
97 return; \
98 auto call = (entity_var)->make_call();
99
100// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if
101// not found
102#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
103 entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
104 if ((entity_var) == nullptr) \
105 return;
106#endif // USE_DEVICES
107
108APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) : parent_(parent) {
109#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
110 auto &noise_ctx = parent->get_noise_ctx();
111 if (noise_ctx.has_psk()) {
112 this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx)};
113 } else {
114 this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
115 }
116#elif defined(USE_API_PLAINTEXT)
117 this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
118#elif defined(USE_API_NOISE)
119 this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
120#else
121#error "No frame helper defined"
122#endif
123#ifdef USE_CAMERA
124 if (camera::Camera::instance() != nullptr) {
125 this->image_reader_ = std::unique_ptr<camera::CameraImageReader>{camera::Camera::instance()->create_image_reader()};
126 }
127#endif
128}
129
130uint32_t APIConnection::get_batch_delay_ms_() const { return this->parent_->get_batch_delay(); }
131
132void APIConnection::start() {
133 this->last_traffic_ = App.get_loop_component_start_time();
134
135 APIError err = this->helper_->init();
136 if (err != APIError::OK) {
137 this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
138 return;
139 }
140 // Initialize client name with peername (IP address) until Hello message provides actual name
141 char peername[socket::SOCKADDR_STR_LEN];
142 this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
143}
144
145APIConnection::~APIConnection() {
146 this->destroy_active_iterator_();
147#ifdef USE_BLUETOOTH_PROXY
148 if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) {
150 }
151#endif
152#ifdef USE_VOICE_ASSISTANT
153 if (voice_assistant::global_voice_assistant->get_api_connection() == this) {
155 }
156#endif
157}
158
159void APIConnection::destroy_active_iterator_() {
160 switch (this->active_iterator_) {
161 case ActiveIterator::LIST_ENTITIES:
162 this->iterator_storage_.list_entities.~ListEntitiesIterator();
163 break;
164 case ActiveIterator::INITIAL_STATE:
165 this->iterator_storage_.initial_state.~InitialStateIterator();
166 break;
167 case ActiveIterator::NONE:
168 break;
169 }
170 this->active_iterator_ = ActiveIterator::NONE;
171}
172
173void APIConnection::begin_iterator_(ActiveIterator type) {
174 this->destroy_active_iterator_();
175 this->active_iterator_ = type;
176 if (type == ActiveIterator::LIST_ENTITIES) {
177 new (&this->iterator_storage_.list_entities) ListEntitiesIterator(this);
178 this->iterator_storage_.list_entities.begin();
179 } else {
180 new (&this->iterator_storage_.initial_state) InitialStateIterator(this);
181 this->iterator_storage_.initial_state.begin();
182 }
183}
184
185void APIConnection::loop() {
186 if (this->flags_.next_close) {
187 // requested a disconnect - don't close socket here, let APIServer::loop() do it
188 // so getpeername() still works for the disconnect trigger
189 this->flags_.remove = true;
190 return;
191 }
192
193 APIError err = this->helper_->loop();
194 if (err != APIError::OK) {
195 this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
196 return;
197 }
198
199 const uint32_t now = App.get_loop_component_start_time();
200 // Check if socket has data ready before attempting to read
201 if (this->helper_->is_socket_ready()) {
202 // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
203 for (uint8_t message_count = 0; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
204 ReadPacketBuffer buffer;
205 err = this->helper_->read_packet(&buffer);
206 if (err == APIError::WOULD_BLOCK) {
207 // No more data available
208 break;
209 } else if (err != APIError::OK) {
210 this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
211 return;
212 } else {
213 // Only update last_traffic_ after authentication to ensure the
214 // handshake timeout is an absolute deadline from connection start.
215 // Pre-auth messages (e.g. PingRequest) must not reset the timer.
216 if (this->is_authenticated()) {
217 this->last_traffic_ = now;
218 }
219 // read a packet
220 this->read_message(buffer.data_len, buffer.type, buffer.data);
221 if (this->flags_.remove)
222 return;
223 }
224 }
225 }
226
227 // Process deferred batch if scheduled and timer has expired
228 if (this->flags_.batch_scheduled && now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) {
229 this->process_batch_();
230 }
231
232 if (this->active_iterator_ != ActiveIterator::NONE) {
233 this->process_active_iterator_();
234 }
235
236 // Disconnect clients that haven't completed the handshake in time.
237 // Stale half-open connections from buggy clients or network issues can
238 // accumulate and block legitimate clients from reconnecting.
239 if (!this->is_authenticated() && now - this->last_traffic_ > HANDSHAKE_TIMEOUT_MS) {
240 this->on_fatal_error();
241 this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("handshake timeout; disconnecting"));
242 return;
243 }
244
245 // Keepalive: only call into the cold path when enough time has elapsed.
246 // When sent_ping is true, last_traffic_ hasn't been updated so this
247 // condition is already satisfied — covers both send-ping and disconnect cases.
248 if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) {
249 this->check_keepalive_(now);
250 }
251
252#ifdef USE_API_HOMEASSISTANT_STATES
253 if (state_subs_at_ >= 0) {
254 this->process_state_subscriptions_();
255 }
256#endif
257
258#ifdef USE_CAMERA
259 // Process camera last - state updates are higher priority
260 // (missing a frame is fine, missing a state update is not)
261 this->try_send_camera_image_();
262#endif
263}
264
265void APIConnection::check_keepalive_(uint32_t now) {
266 // Caller guarantees: now - last_traffic_ > KEEPALIVE_TIMEOUT_MS
267 if (this->flags_.sent_ping) {
268 // Disconnect if not responded within 2.5*keepalive
269 if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
270 on_fatal_error();
271 this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting"));
272 }
273 } else if (!this->flags_.remove) {
274 // Only send ping if we're not disconnecting
275 ESP_LOGVV(TAG, "Sending keepalive PING");
276 PingRequest req;
277 this->flags_.sent_ping = this->send_message(req, PingRequest::MESSAGE_TYPE);
278 if (!this->flags_.sent_ping) {
279 // If we can't send the ping request directly (tx_buffer full),
280 // schedule it at the front of the batch so it will be sent with priority
281 ESP_LOGW(TAG, "Buffer full, ping queued");
282 this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE);
283 this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings
284 }
285 }
286}
287
288void APIConnection::process_active_iterator_() {
289 // Caller ensures active_iterator_ != NONE
290 if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) {
291 if (this->iterator_storage_.list_entities.completed()) {
292 this->destroy_active_iterator_();
293 if (this->flags_.state_subscription) {
294 this->begin_iterator_(ActiveIterator::INITIAL_STATE);
295 }
296 } else {
297 this->process_iterator_batch_(this->iterator_storage_.list_entities);
298 }
299 } else { // INITIAL_STATE
300 if (this->iterator_storage_.initial_state.completed()) {
301 this->destroy_active_iterator_();
302 // Process any remaining batched messages immediately
303 if (!this->deferred_batch_.empty()) {
304 this->process_batch_();
305 }
306 // Now that everything is sent, enable immediate sending for future state changes
307 this->flags_.should_try_send_immediately = true;
308 // Release excess memory from buffers that grew during initial sync
309 this->deferred_batch_.release_buffer();
310 this->helper_->release_buffers();
311 } else {
312 this->process_iterator_batch_(this->iterator_storage_.initial_state);
313 }
314 }
315}
316
317void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
318 size_t initial_size = this->deferred_batch_.size();
319 size_t max_batch = this->get_max_batch_size_();
320 while (!iterator.completed() && (this->deferred_batch_.size() - initial_size) < max_batch) {
321 iterator.advance();
322 }
323
324 // If the batch is full, process it immediately
325 // Note: iterator.advance() already calls schedule_batch_() via schedule_message_()
326 if (this->deferred_batch_.size() >= max_batch) {
327 this->process_batch_();
328 }
329}
330
331bool APIConnection::send_disconnect_response_() {
332 // remote initiated disconnect_client
333 // don't close yet, we still need to send the disconnect response
334 // close will happen on next loop
335 this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected"));
336 this->flags_.next_close = true;
338 return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
339}
340void APIConnection::on_disconnect_response() {
341 // Don't close socket here, let APIServer::loop() do it
342 // so getpeername() still works for the disconnect trigger
343 this->flags_.remove = true;
344}
345
346// Encodes a message to the buffer and returns the total number of bytes used,
347// including header and footer overhead. Returns 0 if the message doesn't fit.
348uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint8_t message_type, APIConnection *conn,
349 uint32_t remaining_size) {
350#ifdef HAS_PROTO_MESSAGE_DUMP
351 // If in log-only mode, just log and return
352 if (conn->flags_.log_only_mode) {
353 DumpBuffer dump_buf;
354 conn->log_send_message_(msg.message_name(), msg.dump_to(dump_buf));
355 return 1; // Return non-zero to indicate "success" for logging
356 }
357#endif
358
359 // Calculate size
360 uint32_t calculated_size = msg.calculated_size();
361
362 // Cache frame sizes to avoid repeated virtual calls
363 const uint8_t header_padding = conn->helper_->frame_header_padding();
364 const uint8_t footer_size = conn->helper_->frame_footer_size();
365
366 // Calculate total size with padding for buffer allocation
367 size_t total_calculated_size = calculated_size + header_padding + footer_size;
368
369 // Check if it fits
370 if (total_calculated_size > remaining_size) {
371 return 0; // Doesn't fit
372 }
373
374 // Get buffer size after allocation (which includes header padding)
375 std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref();
376
377 if (conn->flags_.batch_first_message) {
378 // First message - buffer already prepared by caller, just clear flag
379 conn->flags_.batch_first_message = false;
380 } else {
381 // Batch message second or later
382 // Add padding for previous message footer + this message header
383 size_t current_size = shared_buf.size();
384 shared_buf.reserve(current_size + total_calculated_size);
385 shared_buf.resize(current_size + footer_size + header_padding);
386 }
387
388 // Pre-resize buffer to include payload, then encode through raw pointer
389 size_t write_start = shared_buf.size();
390 shared_buf.resize(write_start + calculated_size);
391 ProtoWriteBuffer buffer{&shared_buf, write_start};
392 msg.encode(buffer);
393
394 // Return total size (header + payload + footer)
395 return static_cast<uint16_t>(header_padding + calculated_size + footer_size);
396}
397
398#ifdef USE_BINARY_SENSOR
399bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) {
400 return this->send_message_smart_(binary_sensor, BinarySensorStateResponse::MESSAGE_TYPE,
401 BinarySensorStateResponse::ESTIMATED_SIZE);
402}
403
404uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
405 auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity);
407 resp.state = binary_sensor->state;
408 resp.missing_state = !binary_sensor->has_state();
409 return fill_and_encode_entity_state(binary_sensor, resp, BinarySensorStateResponse::MESSAGE_TYPE, conn,
410 remaining_size);
411}
412
413uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
414 auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity);
416 msg.device_class = binary_sensor->get_device_class_ref();
417 msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
418 return fill_and_encode_entity_info(binary_sensor, msg, ListEntitiesBinarySensorResponse::MESSAGE_TYPE, conn,
419 remaining_size);
420}
421#endif
422
423#ifdef USE_COVER
424bool APIConnection::send_cover_state(cover::Cover *cover) {
425 return this->send_message_smart_(cover, CoverStateResponse::MESSAGE_TYPE, CoverStateResponse::ESTIMATED_SIZE);
426}
427uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
428 auto *cover = static_cast<cover::Cover *>(entity);
430 auto traits = cover->get_traits();
431 msg.position = cover->position;
432 if (traits.get_supports_tilt())
433 msg.tilt = cover->tilt;
434 msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation);
435 return fill_and_encode_entity_state(cover, msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size);
436}
437uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
438 auto *cover = static_cast<cover::Cover *>(entity);
440 auto traits = cover->get_traits();
441 msg.assumed_state = traits.get_is_assumed_state();
442 msg.supports_position = traits.get_supports_position();
443 msg.supports_tilt = traits.get_supports_tilt();
444 msg.supports_stop = traits.get_supports_stop();
445 msg.device_class = cover->get_device_class_ref();
446 return fill_and_encode_entity_info(cover, msg, ListEntitiesCoverResponse::MESSAGE_TYPE, conn, remaining_size);
447}
448void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
449 ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
450 if (msg.has_position)
451 call.set_position(msg.position);
452 if (msg.has_tilt)
453 call.set_tilt(msg.tilt);
454 if (msg.stop)
455 call.set_command_stop();
456 call.perform();
457}
458#endif
459
460#ifdef USE_FAN
461bool APIConnection::send_fan_state(fan::Fan *fan) {
462 return this->send_message_smart_(fan, FanStateResponse::MESSAGE_TYPE, FanStateResponse::ESTIMATED_SIZE);
463}
464uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
465 auto *fan = static_cast<fan::Fan *>(entity);
467 auto traits = fan->get_traits();
468 msg.state = fan->state;
469 if (traits.supports_oscillation())
470 msg.oscillating = fan->oscillating;
471 if (traits.supports_speed()) {
472 msg.speed_level = fan->speed;
473 }
474 if (traits.supports_direction())
475 msg.direction = static_cast<enums::FanDirection>(fan->direction);
476 if (traits.supports_preset_modes() && fan->has_preset_mode())
477 msg.preset_mode = fan->get_preset_mode();
478 return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size);
479}
480uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
481 auto *fan = static_cast<fan::Fan *>(entity);
483 auto traits = fan->get_traits();
484 msg.supports_oscillation = traits.supports_oscillation();
485 msg.supports_speed = traits.supports_speed();
486 msg.supports_direction = traits.supports_direction();
487 msg.supported_speed_count = traits.supported_speed_count();
488 msg.supported_preset_modes = &traits.supported_preset_modes();
489 return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size);
490}
491void APIConnection::on_fan_command_request(const FanCommandRequest &msg) {
492 ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
493 if (msg.has_state)
494 call.set_state(msg.state);
495 if (msg.has_oscillating)
496 call.set_oscillating(msg.oscillating);
497 if (msg.has_speed_level) {
498 // Prefer level
499 call.set_speed(msg.speed_level);
500 }
501 if (msg.has_direction)
502 call.set_direction(static_cast<fan::FanDirection>(msg.direction));
503 if (msg.has_preset_mode)
504 call.set_preset_mode(msg.preset_mode.c_str(), msg.preset_mode.size());
505 call.perform();
506}
507#endif
508
509#ifdef USE_LIGHT
510bool APIConnection::send_light_state(light::LightState *light) {
511 return this->send_message_smart_(light, LightStateResponse::MESSAGE_TYPE, LightStateResponse::ESTIMATED_SIZE);
512}
513uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
514 auto *light = static_cast<light::LightState *>(entity);
516 auto values = light->remote_values;
517 auto color_mode = values.get_color_mode();
518 resp.state = values.is_on();
519 resp.color_mode = static_cast<enums::ColorMode>(color_mode);
520 resp.brightness = values.get_brightness();
521 resp.color_brightness = values.get_color_brightness();
522 resp.red = values.get_red();
523 resp.green = values.get_green();
524 resp.blue = values.get_blue();
525 resp.white = values.get_white();
526 resp.color_temperature = values.get_color_temperature();
527 resp.cold_white = values.get_cold_white();
528 resp.warm_white = values.get_warm_white();
529 if (light->supports_effects()) {
530 resp.effect = light->get_effect_name();
531 }
532 return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size);
533}
534uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
535 auto *light = static_cast<light::LightState *>(entity);
537 auto traits = light->get_traits();
538 auto supported_modes = traits.get_supported_color_modes();
539 // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
540 msg.supported_color_modes = &supported_modes;
541 if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
542 traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
543 msg.min_mireds = traits.get_min_mireds();
544 msg.max_mireds = traits.get_max_mireds();
545 }
546 FixedVector<const char *> effects_list;
547 if (light->supports_effects()) {
548 auto &light_effects = light->get_effects();
549 effects_list.init(light_effects.size() + 1);
550 effects_list.push_back("None");
551 for (auto *effect : light_effects) {
552 // c_str() is safe as effect names are null-terminated strings from codegen
553 effects_list.push_back(effect->get_name().c_str());
554 }
555 }
556 msg.effects = &effects_list;
557 return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size);
558}
559void APIConnection::on_light_command_request(const LightCommandRequest &msg) {
560 ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
561 if (msg.has_state)
562 call.set_state(msg.state);
563 if (msg.has_brightness)
564 call.set_brightness(msg.brightness);
565 if (msg.has_color_mode)
566 call.set_color_mode(static_cast<light::ColorMode>(msg.color_mode));
567 if (msg.has_color_brightness)
568 call.set_color_brightness(msg.color_brightness);
569 if (msg.has_rgb) {
570 call.set_red(msg.red);
571 call.set_green(msg.green);
572 call.set_blue(msg.blue);
573 }
574 if (msg.has_white)
575 call.set_white(msg.white);
576 if (msg.has_color_temperature)
577 call.set_color_temperature(msg.color_temperature);
578 if (msg.has_cold_white)
579 call.set_cold_white(msg.cold_white);
580 if (msg.has_warm_white)
581 call.set_warm_white(msg.warm_white);
582 if (msg.has_transition_length)
583 call.set_transition_length(msg.transition_length);
584 if (msg.has_flash_length)
585 call.set_flash_length(msg.flash_length);
586 if (msg.has_effect)
587 call.set_effect(msg.effect.c_str(), msg.effect.size());
588 call.perform();
589}
590#endif
591
592#ifdef USE_SENSOR
593bool APIConnection::send_sensor_state(sensor::Sensor *sensor) {
594 return this->send_message_smart_(sensor, SensorStateResponse::MESSAGE_TYPE, SensorStateResponse::ESTIMATED_SIZE);
595}
596
597uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
598 auto *sensor = static_cast<sensor::Sensor *>(entity);
600 resp.state = sensor->state;
601 resp.missing_state = !sensor->has_state();
602 return fill_and_encode_entity_state(sensor, resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size);
603}
604
605uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
606 auto *sensor = static_cast<sensor::Sensor *>(entity);
608 msg.unit_of_measurement = sensor->get_unit_of_measurement_ref();
609 msg.accuracy_decimals = sensor->get_accuracy_decimals();
610 msg.force_update = sensor->get_force_update();
611 msg.device_class = sensor->get_device_class_ref();
612 msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
613 return fill_and_encode_entity_info(sensor, msg, ListEntitiesSensorResponse::MESSAGE_TYPE, conn, remaining_size);
614}
615#endif
616
617#ifdef USE_SWITCH
618bool APIConnection::send_switch_state(switch_::Switch *a_switch) {
619 return this->send_message_smart_(a_switch, SwitchStateResponse::MESSAGE_TYPE, SwitchStateResponse::ESTIMATED_SIZE);
620}
621
622uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
623 auto *a_switch = static_cast<switch_::Switch *>(entity);
625 resp.state = a_switch->state;
626 return fill_and_encode_entity_state(a_switch, resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size);
627}
628
629uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
630 auto *a_switch = static_cast<switch_::Switch *>(entity);
632 msg.assumed_state = a_switch->assumed_state();
633 msg.device_class = a_switch->get_device_class_ref();
634 return fill_and_encode_entity_info(a_switch, msg, ListEntitiesSwitchResponse::MESSAGE_TYPE, conn, remaining_size);
635}
636void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
637 ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
638
639 if (msg.state) {
640 a_switch->turn_on();
641 } else {
642 a_switch->turn_off();
643 }
644}
645#endif
646
647#ifdef USE_TEXT_SENSOR
648bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) {
649 return this->send_message_smart_(text_sensor, TextSensorStateResponse::MESSAGE_TYPE,
650 TextSensorStateResponse::ESTIMATED_SIZE);
651}
652
653uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
654 auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
656 resp.state = StringRef(text_sensor->state);
657 resp.missing_state = !text_sensor->has_state();
658 return fill_and_encode_entity_state(text_sensor, resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size);
659}
660uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
661 auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
663 msg.device_class = text_sensor->get_device_class_ref();
664 return fill_and_encode_entity_info(text_sensor, msg, ListEntitiesTextSensorResponse::MESSAGE_TYPE, conn,
665 remaining_size);
666}
667#endif
668
669#ifdef USE_CLIMATE
670bool APIConnection::send_climate_state(climate::Climate *climate) {
671 return this->send_message_smart_(climate, ClimateStateResponse::MESSAGE_TYPE, ClimateStateResponse::ESTIMATED_SIZE);
672}
673uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
674 auto *climate = static_cast<climate::Climate *>(entity);
676 auto traits = climate->get_traits();
677 resp.mode = static_cast<enums::ClimateMode>(climate->mode);
678 resp.action = static_cast<enums::ClimateAction>(climate->action);
679 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE))
680 resp.current_temperature = climate->current_temperature;
681 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
683 resp.target_temperature_low = climate->target_temperature_low;
684 resp.target_temperature_high = climate->target_temperature_high;
685 } else {
686 resp.target_temperature = climate->target_temperature;
687 }
688 if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
689 resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
690 if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) {
691 resp.custom_fan_mode = climate->get_custom_fan_mode();
692 }
693 if (traits.get_supports_presets() && climate->preset.has_value()) {
694 resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
695 }
696 if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) {
697 resp.custom_preset = climate->get_custom_preset();
698 }
699 if (traits.get_supports_swing_modes())
700 resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
701 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY))
702 resp.current_humidity = climate->current_humidity;
703 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY))
704 resp.target_humidity = climate->target_humidity;
705 return fill_and_encode_entity_state(climate, resp, ClimateStateResponse::MESSAGE_TYPE, conn, remaining_size);
706}
707uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
708 auto *climate = static_cast<climate::Climate *>(entity);
710 auto traits = climate->get_traits();
711 // Flags set for backward compatibility, deprecated in 2025.11.0
714 msg.supports_two_point_target_temperature = traits.has_feature_flags(
717 msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
718 // Current feature flags and other supported parameters
719 msg.feature_flags = traits.get_feature_flags();
720 msg.supported_modes = &traits.get_supported_modes();
721 msg.visual_min_temperature = traits.get_visual_min_temperature();
722 msg.visual_max_temperature = traits.get_visual_max_temperature();
723 msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
724 msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
725 msg.visual_min_humidity = traits.get_visual_min_humidity();
726 msg.visual_max_humidity = traits.get_visual_max_humidity();
727 msg.supported_fan_modes = &traits.get_supported_fan_modes();
728 msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes();
729 msg.supported_presets = &traits.get_supported_presets();
730 msg.supported_custom_presets = &traits.get_supported_custom_presets();
731 msg.supported_swing_modes = &traits.get_supported_swing_modes();
732 return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size);
733}
734void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
735 ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
736 if (msg.has_mode)
737 call.set_mode(static_cast<climate::ClimateMode>(msg.mode));
739 call.set_target_temperature(msg.target_temperature);
741 call.set_target_temperature_low(msg.target_temperature_low);
743 call.set_target_temperature_high(msg.target_temperature_high);
744 if (msg.has_target_humidity)
745 call.set_target_humidity(msg.target_humidity);
746 if (msg.has_fan_mode)
747 call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
748 if (msg.has_custom_fan_mode)
749 call.set_fan_mode(msg.custom_fan_mode.c_str(), msg.custom_fan_mode.size());
750 if (msg.has_preset)
751 call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
752 if (msg.has_custom_preset)
753 call.set_preset(msg.custom_preset.c_str(), msg.custom_preset.size());
754 if (msg.has_swing_mode)
755 call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
756 call.perform();
757}
758#endif
759
760#ifdef USE_NUMBER
761bool APIConnection::send_number_state(number::Number *number) {
762 return this->send_message_smart_(number, NumberStateResponse::MESSAGE_TYPE, NumberStateResponse::ESTIMATED_SIZE);
763}
764
765uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
766 auto *number = static_cast<number::Number *>(entity);
768 resp.state = number->state;
769 resp.missing_state = !number->has_state();
770 return fill_and_encode_entity_state(number, resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size);
771}
772
773uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
774 auto *number = static_cast<number::Number *>(entity);
776 msg.unit_of_measurement = number->traits.get_unit_of_measurement_ref();
777 msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode());
778 msg.device_class = number->traits.get_device_class_ref();
779 msg.min_value = number->traits.get_min_value();
780 msg.max_value = number->traits.get_max_value();
781 msg.step = number->traits.get_step();
782 return fill_and_encode_entity_info(number, msg, ListEntitiesNumberResponse::MESSAGE_TYPE, conn, remaining_size);
783}
784void APIConnection::on_number_command_request(const NumberCommandRequest &msg) {
785 ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
786 call.set_value(msg.state);
787 call.perform();
788}
789#endif
790
791#ifdef USE_DATETIME_DATE
792bool APIConnection::send_date_state(datetime::DateEntity *date) {
793 return this->send_message_smart_(date, DateStateResponse::MESSAGE_TYPE, DateStateResponse::ESTIMATED_SIZE);
794}
795uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
796 auto *date = static_cast<datetime::DateEntity *>(entity);
798 resp.missing_state = !date->has_state();
799 resp.year = date->year;
800 resp.month = date->month;
801 resp.day = date->day;
802 return fill_and_encode_entity_state(date, resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size);
803}
804uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
805 auto *date = static_cast<datetime::DateEntity *>(entity);
807 return fill_and_encode_entity_info(date, msg, ListEntitiesDateResponse::MESSAGE_TYPE, conn, remaining_size);
808}
809void APIConnection::on_date_command_request(const DateCommandRequest &msg) {
810 ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
811 call.set_date(msg.year, msg.month, msg.day);
812 call.perform();
813}
814#endif
815
816#ifdef USE_DATETIME_TIME
817bool APIConnection::send_time_state(datetime::TimeEntity *time) {
818 return this->send_message_smart_(time, TimeStateResponse::MESSAGE_TYPE, TimeStateResponse::ESTIMATED_SIZE);
819}
820uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
821 auto *time = static_cast<datetime::TimeEntity *>(entity);
823 resp.missing_state = !time->has_state();
824 resp.hour = time->hour;
825 resp.minute = time->minute;
826 resp.second = time->second;
827 return fill_and_encode_entity_state(time, resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size);
828}
829uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
830 auto *time = static_cast<datetime::TimeEntity *>(entity);
832 return fill_and_encode_entity_info(time, msg, ListEntitiesTimeResponse::MESSAGE_TYPE, conn, remaining_size);
833}
834void APIConnection::on_time_command_request(const TimeCommandRequest &msg) {
835 ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
836 call.set_time(msg.hour, msg.minute, msg.second);
837 call.perform();
838}
839#endif
840
841#ifdef USE_DATETIME_DATETIME
842bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
843 return this->send_message_smart_(datetime, DateTimeStateResponse::MESSAGE_TYPE,
844 DateTimeStateResponse::ESTIMATED_SIZE);
845}
846uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
847 auto *datetime = static_cast<datetime::DateTimeEntity *>(entity);
849 resp.missing_state = !datetime->has_state();
850 if (datetime->has_state()) {
851 ESPTime state = datetime->state_as_esptime();
852 resp.epoch_seconds = state.timestamp;
853 }
854 return fill_and_encode_entity_state(datetime, resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size);
855}
856uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
857 auto *datetime = static_cast<datetime::DateTimeEntity *>(entity);
859 return fill_and_encode_entity_info(datetime, msg, ListEntitiesDateTimeResponse::MESSAGE_TYPE, conn, remaining_size);
860}
861void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
862 ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
863 call.set_datetime(msg.epoch_seconds);
864 call.perform();
865}
866#endif
867
868#ifdef USE_TEXT
869bool APIConnection::send_text_state(text::Text *text) {
870 return this->send_message_smart_(text, TextStateResponse::MESSAGE_TYPE, TextStateResponse::ESTIMATED_SIZE);
871}
872
873uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
874 auto *text = static_cast<text::Text *>(entity);
876 resp.state = StringRef(text->state);
877 resp.missing_state = !text->has_state();
878 return fill_and_encode_entity_state(text, resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size);
879}
880
881uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
882 auto *text = static_cast<text::Text *>(entity);
884 msg.mode = static_cast<enums::TextMode>(text->traits.get_mode());
885 msg.min_length = text->traits.get_min_length();
886 msg.max_length = text->traits.get_max_length();
887 msg.pattern = text->traits.get_pattern_ref();
888 return fill_and_encode_entity_info(text, msg, ListEntitiesTextResponse::MESSAGE_TYPE, conn, remaining_size);
889}
890void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
891 ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
892 call.set_value(msg.state.c_str(), msg.state.size());
893 call.perform();
894}
895#endif
896
897#ifdef USE_SELECT
898bool APIConnection::send_select_state(select::Select *select) {
899 return this->send_message_smart_(select, SelectStateResponse::MESSAGE_TYPE, SelectStateResponse::ESTIMATED_SIZE);
900}
901
902uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
903 auto *select = static_cast<select::Select *>(entity);
905 resp.state = select->current_option();
906 resp.missing_state = !select->has_state();
907 return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size);
908}
909
910uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
911 auto *select = static_cast<select::Select *>(entity);
913 msg.options = &select->traits.get_options();
914 return fill_and_encode_entity_info(select, msg, ListEntitiesSelectResponse::MESSAGE_TYPE, conn, remaining_size);
915}
916void APIConnection::on_select_command_request(const SelectCommandRequest &msg) {
917 ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
918 call.set_option(msg.state.c_str(), msg.state.size());
919 call.perform();
920}
921#endif
922
923#ifdef USE_BUTTON
924uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
925 auto *button = static_cast<button::Button *>(entity);
927 msg.device_class = button->get_device_class_ref();
928 return fill_and_encode_entity_info(button, msg, ListEntitiesButtonResponse::MESSAGE_TYPE, conn, remaining_size);
929}
931 ENTITY_COMMAND_GET(button::Button, button, button)
932 button->press();
933}
934#endif
935
936#ifdef USE_LOCK
937bool APIConnection::send_lock_state(lock::Lock *a_lock) {
938 return this->send_message_smart_(a_lock, LockStateResponse::MESSAGE_TYPE, LockStateResponse::ESTIMATED_SIZE);
939}
940
941uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
942 auto *a_lock = static_cast<lock::Lock *>(entity);
944 resp.state = static_cast<enums::LockState>(a_lock->state);
945 return fill_and_encode_entity_state(a_lock, resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size);
946}
947
948uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
949 auto *a_lock = static_cast<lock::Lock *>(entity);
951 msg.assumed_state = a_lock->traits.get_assumed_state();
952 msg.supports_open = a_lock->traits.get_supports_open();
953 msg.requires_code = a_lock->traits.get_requires_code();
954 return fill_and_encode_entity_info(a_lock, msg, ListEntitiesLockResponse::MESSAGE_TYPE, conn, remaining_size);
955}
956void APIConnection::on_lock_command_request(const LockCommandRequest &msg) {
957 ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
958
959 switch (msg.command) {
960 case enums::LOCK_UNLOCK:
961 a_lock->unlock();
962 break;
963 case enums::LOCK_LOCK:
964 a_lock->lock();
965 break;
966 case enums::LOCK_OPEN:
967 a_lock->open();
968 break;
969 }
970}
971#endif
972
973#ifdef USE_VALVE
974bool APIConnection::send_valve_state(valve::Valve *valve) {
975 return this->send_message_smart_(valve, ValveStateResponse::MESSAGE_TYPE, ValveStateResponse::ESTIMATED_SIZE);
976}
977uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
978 auto *valve = static_cast<valve::Valve *>(entity);
980 resp.position = valve->position;
981 resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation);
982 return fill_and_encode_entity_state(valve, resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size);
983}
984uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
985 auto *valve = static_cast<valve::Valve *>(entity);
987 auto traits = valve->get_traits();
988 msg.device_class = valve->get_device_class_ref();
989 msg.assumed_state = traits.get_is_assumed_state();
990 msg.supports_position = traits.get_supports_position();
991 msg.supports_stop = traits.get_supports_stop();
992 return fill_and_encode_entity_info(valve, msg, ListEntitiesValveResponse::MESSAGE_TYPE, conn, remaining_size);
993}
994void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) {
995 ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
996 if (msg.has_position)
997 call.set_position(msg.position);
998 if (msg.stop)
999 call.set_command_stop();
1000 call.perform();
1001}
1002#endif
1003
1004#ifdef USE_MEDIA_PLAYER
1005bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
1006 return this->send_message_smart_(media_player, MediaPlayerStateResponse::MESSAGE_TYPE,
1007 MediaPlayerStateResponse::ESTIMATED_SIZE);
1008}
1009uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1010 auto *media_player = static_cast<media_player::MediaPlayer *>(entity);
1014 : media_player->state;
1015 resp.state = static_cast<enums::MediaPlayerState>(report_state);
1016 resp.volume = media_player->volume;
1017 resp.muted = media_player->is_muted();
1018 return fill_and_encode_entity_state(media_player, resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size);
1019}
1020uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1021 auto *media_player = static_cast<media_player::MediaPlayer *>(entity);
1023 auto traits = media_player->get_traits();
1024 msg.supports_pause = traits.get_supports_pause();
1025 msg.feature_flags = traits.get_feature_flags();
1026 for (auto &supported_format : traits.get_supported_formats()) {
1027 msg.supported_formats.emplace_back();
1028 auto &media_format = msg.supported_formats.back();
1029 media_format.format = StringRef(supported_format.format);
1030 media_format.sample_rate = supported_format.sample_rate;
1031 media_format.num_channels = supported_format.num_channels;
1032 media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
1033 media_format.sample_bytes = supported_format.sample_bytes;
1034 }
1035 return fill_and_encode_entity_info(media_player, msg, ListEntitiesMediaPlayerResponse::MESSAGE_TYPE, conn,
1036 remaining_size);
1037}
1038void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
1039 ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
1040 if (msg.has_command) {
1041 call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
1042 }
1043 if (msg.has_volume) {
1044 call.set_volume(msg.volume);
1045 }
1046 if (msg.has_media_url) {
1047 call.set_media_url(msg.media_url);
1048 }
1049 if (msg.has_announcement) {
1050 call.set_announcement(msg.announcement);
1051 }
1052 call.perform();
1053}
1054#endif
1055
1056#ifdef USE_CAMERA
1057void APIConnection::try_send_camera_image_() {
1058 if (!this->image_reader_)
1059 return;
1060
1061 // Send as many chunks as possible without blocking
1062 while (this->image_reader_->available()) {
1063 if (!this->helper_->can_write_without_blocking())
1064 return;
1065
1066 uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
1067 bool done = this->image_reader_->available() == to_send;
1068
1071 msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
1072 msg.done = done;
1073#ifdef USE_DEVICES
1075#endif
1076
1077 if (!this->send_message_impl(msg, CameraImageResponse::MESSAGE_TYPE)) {
1078 return; // Send failed, try again later
1079 }
1080 this->image_reader_->consume_data(to_send);
1081 if (done) {
1082 this->image_reader_->return_image();
1083 return;
1084 }
1085 }
1086}
1087void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) {
1088 if (!this->flags_.state_subscription)
1089 return;
1090 if (!this->image_reader_)
1091 return;
1092 if (this->image_reader_->available())
1093 return;
1094 if (image->was_requested_by(esphome::camera::API_REQUESTER) || image->was_requested_by(esphome::camera::IDLE)) {
1095 this->image_reader_->set_image(std::move(image));
1096 // Try to send immediately to reduce latency
1097 this->try_send_camera_image_();
1098 }
1099}
1100uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1101 auto *camera = static_cast<camera::Camera *>(entity);
1103 return fill_and_encode_entity_info(camera, msg, ListEntitiesCameraResponse::MESSAGE_TYPE, conn, remaining_size);
1104}
1105void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
1106 if (camera::Camera::instance() == nullptr)
1107 return;
1108
1109 if (msg.single)
1111 if (msg.stream) {
1113
1114 App.scheduler.set_timeout(this->parent_, "api_camera_stop_stream", CAMERA_STOP_STREAM,
1116 }
1117}
1118#endif
1119
1120#ifdef USE_HOMEASSISTANT_TIME
1121void APIConnection::on_get_time_response(const GetTimeResponse &value) {
1124#ifdef USE_TIME_TIMEZONE
1125 if (!value.timezone.empty()) {
1126 // Check if the sender provided pre-parsed timezone data.
1127 // If std_offset is non-zero or DST rules are present, the parsed data was populated.
1128 // For UTC (all zeros), string parsing produces the same result, so the fallback is equivalent.
1129 const auto &pt = value.parsed_timezone;
1130 if (pt.std_offset_seconds != 0 || pt.dst_start.type != enums::DST_RULE_TYPE_NONE) {
1132 tz.std_offset_seconds = pt.std_offset_seconds;
1133 tz.dst_offset_seconds = pt.dst_offset_seconds;
1134 tz.dst_start.time_seconds = pt.dst_start.time_seconds;
1135 tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
1136 tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
1137 tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
1138 tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
1139 tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
1140 tz.dst_end.time_seconds = pt.dst_end.time_seconds;
1141 tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
1142 tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
1143 tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
1144 tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
1145 tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
1147 } else {
1149 }
1150 }
1151#endif
1152 }
1153}
1154#endif
1155
1156#ifdef USE_BLUETOOTH_PROXY
1157void APIConnection::on_subscribe_bluetooth_le_advertisements_request(
1160}
1161void APIConnection::on_unsubscribe_bluetooth_le_advertisements_request() {
1163}
1164void APIConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
1166}
1167void APIConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
1169}
1170void APIConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
1172}
1173void APIConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
1175}
1176void APIConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
1178}
1179void APIConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
1181}
1182
1183void APIConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
1185}
1186
1187bool APIConnection::send_subscribe_bluetooth_connections_free_response_() {
1189 return true;
1190}
1191void APIConnection::on_subscribe_bluetooth_connections_free_request() {
1192 if (!this->send_subscribe_bluetooth_connections_free_response_()) {
1193 this->on_fatal_error();
1194 }
1195}
1196
1197void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
1199 msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
1200}
1201#endif
1202
1203#ifdef USE_VOICE_ASSISTANT
1204bool APIConnection::check_voice_assistant_api_connection_() const {
1205 return voice_assistant::global_voice_assistant != nullptr &&
1207}
1208
1209void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
1212 }
1213}
1214void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
1215 if (!this->check_voice_assistant_api_connection_()) {
1216 return;
1217 }
1218
1219 if (msg.error) {
1221 return;
1222 }
1223 if (msg.port == 0) {
1224 // Use API Audio
1226 } else {
1227 struct sockaddr_storage storage;
1228 socklen_t len = sizeof(storage);
1229 this->helper_->getpeername((struct sockaddr *) &storage, &len);
1231 }
1232};
1233void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) {
1234 if (this->check_voice_assistant_api_connection_()) {
1236 }
1237}
1238void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) {
1239 if (this->check_voice_assistant_api_connection_()) {
1241 }
1242};
1243void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) {
1244 if (this->check_voice_assistant_api_connection_()) {
1246 }
1247};
1248
1249void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) {
1250 if (this->check_voice_assistant_api_connection_()) {
1252 }
1253}
1254
1255bool APIConnection::send_voice_assistant_get_configuration_response_(const VoiceAssistantConfigurationRequest &msg) {
1257 if (!this->check_voice_assistant_api_connection_()) {
1258 return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
1259 }
1260
1262 for (auto &wake_word : config.available_wake_words) {
1263 resp.available_wake_words.emplace_back();
1264 auto &resp_wake_word = resp.available_wake_words.back();
1265 resp_wake_word.id = StringRef(wake_word.id);
1266 resp_wake_word.wake_word = StringRef(wake_word.wake_word);
1267 for (const auto &lang : wake_word.trained_languages) {
1268 resp_wake_word.trained_languages.push_back(lang);
1269 }
1270 }
1271
1272 // Filter external wake words
1273 for (auto &wake_word : msg.external_wake_words) {
1274 if (wake_word.model_type != "micro") {
1275 // microWakeWord only
1276 continue;
1277 }
1278
1279 resp.available_wake_words.emplace_back();
1280 auto &resp_wake_word = resp.available_wake_words.back();
1281 resp_wake_word.id = StringRef(wake_word.id);
1282 resp_wake_word.wake_word = StringRef(wake_word.wake_word);
1283 for (const auto &lang : wake_word.trained_languages) {
1284 resp_wake_word.trained_languages.push_back(lang);
1285 }
1286 }
1287
1288 resp.active_wake_words = &config.active_wake_words;
1289 resp.max_active_wake_words = config.max_active_wake_words;
1290 return this->send_message(resp, VoiceAssistantConfigurationResponse::MESSAGE_TYPE);
1291}
1292void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
1293 if (!this->send_voice_assistant_get_configuration_response_(msg)) {
1294 this->on_fatal_error();
1295 }
1296}
1297
1298void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
1299 if (this->check_voice_assistant_api_connection_()) {
1301 }
1302}
1303#endif
1304
1305#ifdef USE_ZWAVE_PROXY
1306void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
1308}
1309
1310void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
1312}
1313#endif
1314
1315#ifdef USE_ALARM_CONTROL_PANEL
1316bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
1317 return this->send_message_smart_(a_alarm_control_panel, AlarmControlPanelStateResponse::MESSAGE_TYPE,
1318 AlarmControlPanelStateResponse::ESTIMATED_SIZE);
1319}
1320uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn,
1321 uint32_t remaining_size) {
1322 auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity);
1324 resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state());
1325 return fill_and_encode_entity_state(a_alarm_control_panel, resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn,
1326 remaining_size);
1327}
1328uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn,
1329 uint32_t remaining_size) {
1330 auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity);
1332 msg.supported_features = a_alarm_control_panel->get_supported_features();
1333 msg.requires_code = a_alarm_control_panel->get_requires_code();
1334 msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm();
1335 return fill_and_encode_entity_info(a_alarm_control_panel, msg, ListEntitiesAlarmControlPanelResponse::MESSAGE_TYPE,
1336 conn, remaining_size);
1337}
1338void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
1339 ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
1340 switch (msg.command) {
1341 case enums::ALARM_CONTROL_PANEL_DISARM:
1342 call.disarm();
1343 break;
1344 case enums::ALARM_CONTROL_PANEL_ARM_AWAY:
1345 call.arm_away();
1346 break;
1347 case enums::ALARM_CONTROL_PANEL_ARM_HOME:
1348 call.arm_home();
1349 break;
1350 case enums::ALARM_CONTROL_PANEL_ARM_NIGHT:
1351 call.arm_night();
1352 break;
1353 case enums::ALARM_CONTROL_PANEL_ARM_VACATION:
1354 call.arm_vacation();
1355 break;
1356 case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS:
1357 call.arm_custom_bypass();
1358 break;
1359 case enums::ALARM_CONTROL_PANEL_TRIGGER:
1360 call.pending();
1361 break;
1362 }
1363 call.set_code(msg.code.c_str(), msg.code.size());
1364 call.perform();
1365}
1366#endif
1367
1368#ifdef USE_WATER_HEATER
1369bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_heater) {
1370 return this->send_message_smart_(water_heater, WaterHeaterStateResponse::MESSAGE_TYPE,
1371 WaterHeaterStateResponse::ESTIMATED_SIZE);
1372}
1373uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1374 auto *wh = static_cast<water_heater::WaterHeater *>(entity);
1376 resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode());
1377 resp.current_temperature = wh->get_current_temperature();
1378 resp.target_temperature = wh->get_target_temperature();
1379 resp.target_temperature_low = wh->get_target_temperature_low();
1380 resp.target_temperature_high = wh->get_target_temperature_high();
1381 resp.state = wh->get_state();
1382
1383 return fill_and_encode_entity_state(wh, resp, WaterHeaterStateResponse::MESSAGE_TYPE, conn, remaining_size);
1384}
1385uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1386 auto *wh = static_cast<water_heater::WaterHeater *>(entity);
1388 auto traits = wh->get_traits();
1389 msg.min_temperature = traits.get_min_temperature();
1390 msg.max_temperature = traits.get_max_temperature();
1391 msg.target_temperature_step = traits.get_target_temperature_step();
1392 msg.supported_modes = &traits.get_supported_modes();
1393 msg.supported_features = traits.get_feature_flags();
1394 return fill_and_encode_entity_info(wh, msg, ListEntitiesWaterHeaterResponse::MESSAGE_TYPE, conn, remaining_size);
1395}
1396
1397void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
1398 ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
1399 if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
1400 call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
1401 if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE)
1402 call.set_target_temperature(msg.target_temperature);
1403 if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW)
1404 call.set_target_temperature_low(msg.target_temperature_low);
1405 if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
1406 call.set_target_temperature_high(msg.target_temperature_high);
1407 if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) ||
1408 (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
1409 call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
1410 }
1411 if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_ON_STATE) ||
1412 (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
1413 call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
1414 }
1415 call.perform();
1416}
1417#endif
1418
1419#ifdef USE_EVENT
1420// Event is a special case - unlike other entities with simple state fields,
1421// events store their state in a member accessed via obj->get_last_event_type()
1422void APIConnection::send_event(event::Event *event) {
1423 this->send_message_smart_(event, EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE,
1424 event->get_last_event_type_index());
1425}
1426uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
1427 uint32_t remaining_size) {
1428 EventResponse resp;
1429 resp.event_type = event_type;
1430 return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size);
1431}
1432
1433uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1434 auto *event = static_cast<event::Event *>(entity);
1436 msg.device_class = event->get_device_class_ref();
1437 msg.event_types = &event->get_event_types();
1438 return fill_and_encode_entity_info(event, msg, ListEntitiesEventResponse::MESSAGE_TYPE, conn, remaining_size);
1439}
1440#endif
1441
1442#ifdef USE_IR_RF
1443void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
1444 // TODO: When RF is implemented, add a field to the message to distinguish IR vs RF
1445 // and dispatch to the appropriate entity type based on that field.
1446#ifdef USE_INFRARED
1447 ENTITY_COMMAND_MAKE_CALL(infrared::Infrared, infrared, infrared)
1448 call.set_carrier_frequency(msg.carrier_frequency);
1449 call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
1450 call.set_repeat_count(msg.repeat_count);
1451 call.perform();
1452#endif
1453}
1454
1455void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) {
1456 this->send_message(msg, InfraredRFReceiveEvent::MESSAGE_TYPE);
1457}
1458#endif
1459
1460#ifdef USE_INFRARED
1461uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1462 auto *infrared = static_cast<infrared::Infrared *>(entity);
1464 msg.capabilities = infrared->get_capability_flags();
1465 return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size);
1466}
1467#endif
1468
1469#ifdef USE_UPDATE
1470bool APIConnection::send_update_state(update::UpdateEntity *update) {
1471 return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE);
1472}
1473uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1474 auto *update = static_cast<update::UpdateEntity *>(entity);
1476 resp.missing_state = !update->has_state();
1477 if (update->has_state()) {
1479 if (update->update_info.has_progress) {
1480 resp.has_progress = true;
1481 resp.progress = update->update_info.progress;
1482 }
1483 resp.current_version = StringRef(update->update_info.current_version);
1484 resp.latest_version = StringRef(update->update_info.latest_version);
1485 resp.title = StringRef(update->update_info.title);
1486 resp.release_summary = StringRef(update->update_info.summary);
1487 resp.release_url = StringRef(update->update_info.release_url);
1488 }
1489 return fill_and_encode_entity_state(update, resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size);
1490}
1491uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1492 auto *update = static_cast<update::UpdateEntity *>(entity);
1494 msg.device_class = update->get_device_class_ref();
1495 return fill_and_encode_entity_info(update, msg, ListEntitiesUpdateResponse::MESSAGE_TYPE, conn, remaining_size);
1496}
1497void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) {
1498 ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
1499
1500 switch (msg.command) {
1501 case enums::UPDATE_COMMAND_UPDATE:
1502 update->perform();
1503 break;
1504 case enums::UPDATE_COMMAND_CHECK:
1505 update->check();
1506 break;
1507 case enums::UPDATE_COMMAND_NONE:
1508 ESP_LOGE(TAG, "UPDATE_COMMAND_NONE not handled; confirm command is correct");
1509 break;
1510 default:
1511 ESP_LOGW(TAG, "Unknown update command: %" PRIu32, msg.command);
1512 break;
1513 }
1514}
1515#endif
1516
1517bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) {
1519 msg.level = static_cast<enums::LogLevel>(level);
1520 msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len);
1521 return this->send_message_impl(msg, SubscribeLogsResponse::MESSAGE_TYPE);
1522}
1523
1524void APIConnection::complete_authentication_() {
1525 // Early return if already authenticated
1526 if (this->flags_.connection_state == static_cast<uint8_t>(ConnectionState::AUTHENTICATED)) {
1527 return;
1528 }
1529
1530 this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
1531 // Reset traffic timer so keepalive starts from authentication, not connection start
1532 this->last_traffic_ = App.get_loop_component_start_time();
1533 this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
1534#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
1535 {
1536 char peername[socket::SOCKADDR_STR_LEN];
1537 this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
1538 std::string(this->helper_->get_peername_to(peername)));
1539 }
1540#endif
1541#ifdef USE_HOMEASSISTANT_TIME
1543 this->send_time_request();
1544 }
1545#endif
1546#ifdef USE_ZWAVE_PROXY
1547 if (zwave_proxy::global_zwave_proxy != nullptr) {
1549 }
1550#endif
1551}
1552
1553bool APIConnection::send_hello_response_(const HelloRequest &msg) {
1554 // Copy client name with truncation if needed (set_client_name handles truncation)
1555 this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
1556 this->client_api_version_major_ = msg.api_version_major;
1557 this->client_api_version_minor_ = msg.api_version_minor;
1558 char peername[socket::SOCKADDR_STR_LEN];
1559 ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu16 ".%" PRIu16, this->helper_->get_client_name(),
1560 this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
1561
1562 // TODO: Remove before 2026.8.0 (one version after get_object_id backward compat removal)
1563 if (!this->client_supports_api_version(1, 14)) {
1564 ESP_LOGW(TAG, "'%s' using outdated API %" PRIu16 ".%" PRIu16 ", update to 1.14+", this->helper_->get_client_name(),
1565 this->client_api_version_major_, this->client_api_version_minor_);
1566 }
1567
1568 HelloResponse resp;
1569 resp.api_version_major = 1;
1570 resp.api_version_minor = 14;
1571 // Send only the version string - the client only logs this for debugging and doesn't use it otherwise
1572 resp.server_info = ESPHOME_VERSION_REF;
1573 resp.name = StringRef(App.get_name());
1574
1575 // Auto-authenticate - password auth was removed in ESPHome 2026.1.0
1576 this->complete_authentication_();
1577
1578 return this->send_message(resp, HelloResponse::MESSAGE_TYPE);
1579}
1580
1581bool APIConnection::send_ping_response_() {
1582 PingResponse resp;
1583 return this->send_message(resp, PingResponse::MESSAGE_TYPE);
1584}
1585
1586bool APIConnection::send_device_info_response_() {
1587 DeviceInfoResponse resp{};
1588 resp.name = StringRef(App.get_name());
1589 resp.friendly_name = StringRef(App.get_friendly_name());
1590#ifdef USE_AREAS
1591 resp.suggested_area = StringRef(App.get_area());
1592#endif
1593 // Stack buffer for MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
1594 char mac_address[18];
1595 uint8_t mac[6];
1597 format_mac_addr_upper(mac, mac_address);
1598 resp.mac_address = StringRef(mac_address);
1599
1600 resp.esphome_version = ESPHOME_VERSION_REF;
1601
1602 // Stack buffer for build time string
1603 char build_time_str[Application::BUILD_TIME_STR_SIZE];
1604 App.get_build_time_string(build_time_str);
1605 resp.compilation_time = StringRef(build_time_str);
1606
1607 // Manufacturer string - define once, handle ESP8266 PROGMEM separately
1608#if defined(USE_ESP8266) || defined(USE_ESP32)
1609#define ESPHOME_MANUFACTURER "Espressif"
1610#elif defined(USE_RP2040)
1611#define ESPHOME_MANUFACTURER "Raspberry Pi"
1612#elif defined(USE_BK72XX)
1613#define ESPHOME_MANUFACTURER "Beken"
1614#elif defined(USE_LN882X)
1615#define ESPHOME_MANUFACTURER "Lightning"
1616#elif defined(USE_NRF52)
1617#define ESPHOME_MANUFACTURER "Nordic Semiconductor"
1618#elif defined(USE_RTL87XX)
1619#define ESPHOME_MANUFACTURER "Realtek"
1620#elif defined(USE_HOST)
1621#define ESPHOME_MANUFACTURER "Host"
1622#endif
1623
1624#ifdef USE_ESP8266
1625 // ESP8266 requires PROGMEM for flash storage, copy to stack for memcpy compatibility
1626 static const char MANUFACTURER_PROGMEM[] PROGMEM = ESPHOME_MANUFACTURER;
1627 char manufacturer_buf[sizeof(MANUFACTURER_PROGMEM)];
1628 memcpy_P(manufacturer_buf, MANUFACTURER_PROGMEM, sizeof(MANUFACTURER_PROGMEM));
1629 resp.manufacturer = StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1);
1630#else
1631 static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
1632 resp.manufacturer = MANUFACTURER;
1633#endif
1634#undef ESPHOME_MANUFACTURER
1635
1636#ifdef USE_ESP8266
1637 static const char MODEL_PROGMEM[] PROGMEM = ESPHOME_BOARD;
1638 char model_buf[sizeof(MODEL_PROGMEM)];
1639 memcpy_P(model_buf, MODEL_PROGMEM, sizeof(MODEL_PROGMEM));
1640 resp.model = StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1);
1641#else
1642 static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
1643 resp.model = MODEL;
1644#endif
1645#ifdef USE_DEEP_SLEEP
1646 resp.has_deep_sleep = deep_sleep::global_has_deep_sleep;
1647#endif
1648#ifdef ESPHOME_PROJECT_NAME
1649#ifdef USE_ESP8266
1650 static const char PROJECT_NAME_PROGMEM[] PROGMEM = ESPHOME_PROJECT_NAME;
1651 static const char PROJECT_VERSION_PROGMEM[] PROGMEM = ESPHOME_PROJECT_VERSION;
1652 char project_name_buf[sizeof(PROJECT_NAME_PROGMEM)];
1653 char project_version_buf[sizeof(PROJECT_VERSION_PROGMEM)];
1654 memcpy_P(project_name_buf, PROJECT_NAME_PROGMEM, sizeof(PROJECT_NAME_PROGMEM));
1655 memcpy_P(project_version_buf, PROJECT_VERSION_PROGMEM, sizeof(PROJECT_VERSION_PROGMEM));
1656 resp.project_name = StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1);
1657 resp.project_version = StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1);
1658#else
1659 static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
1660 static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
1661 resp.project_name = PROJECT_NAME;
1662 resp.project_version = PROJECT_VERSION;
1663#endif
1664#endif
1665#ifdef USE_WEBSERVER
1666 resp.webserver_port = USE_WEBSERVER_PORT;
1667#endif
1668#ifdef USE_BLUETOOTH_PROXY
1669 resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags();
1670 // Stack buffer for Bluetooth MAC address (XX:XX:XX:XX:XX:XX\0 = 18 bytes)
1671 char bluetooth_mac[18];
1673 resp.bluetooth_mac_address = StringRef(bluetooth_mac);
1674#endif
1675#ifdef USE_VOICE_ASSISTANT
1676 resp.voice_assistant_feature_flags = voice_assistant::global_voice_assistant->get_feature_flags();
1677#endif
1678#ifdef USE_ZWAVE_PROXY
1679 resp.zwave_proxy_feature_flags = zwave_proxy::global_zwave_proxy->get_feature_flags();
1680 resp.zwave_home_id = zwave_proxy::global_zwave_proxy->get_home_id();
1681#endif
1682#ifdef USE_API_NOISE
1683 resp.api_encryption_supported = true;
1684#endif
1685#ifdef USE_DEVICES
1686 size_t device_index = 0;
1687 for (auto const &device : App.get_devices()) {
1688 if (device_index >= ESPHOME_DEVICE_COUNT)
1689 break;
1690 auto &device_info = resp.devices[device_index++];
1691 device_info.device_id = device->get_device_id();
1692 device_info.name = StringRef(device->get_name());
1693 device_info.area_id = device->get_area_id();
1694 }
1695#endif
1696#ifdef USE_AREAS
1697 size_t area_index = 0;
1698 for (auto const &area : App.get_areas()) {
1699 if (area_index >= ESPHOME_AREA_COUNT)
1700 break;
1701 auto &area_info = resp.areas[area_index++];
1702 area_info.area_id = area->get_area_id();
1703 area_info.name = StringRef(area->get_name());
1704 }
1705#endif
1706
1707 return this->send_message(resp, DeviceInfoResponse::MESSAGE_TYPE);
1708}
1709void APIConnection::on_hello_request(const HelloRequest &msg) {
1710 if (!this->send_hello_response_(msg)) {
1711 this->on_fatal_error();
1712 }
1713}
1714void APIConnection::on_disconnect_request() {
1715 if (!this->send_disconnect_response_()) {
1716 this->on_fatal_error();
1717 }
1718}
1719void APIConnection::on_ping_request() {
1720 if (!this->send_ping_response_()) {
1721 this->on_fatal_error();
1722 }
1723}
1724void APIConnection::on_device_info_request() {
1725 if (!this->send_device_info_response_()) {
1726 this->on_fatal_error();
1727 }
1728}
1729
1730#ifdef USE_API_HOMEASSISTANT_STATES
1731void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
1732 // Skip if entity_id is empty (invalid message)
1733 if (msg.entity_id.empty()) {
1734 return;
1735 }
1736
1737 // Null-terminate state in-place for safe c_str() usage (e.g., parse_number in callbacks).
1738 // Safe: decode is complete, byte after string data was already consumed during parse,
1739 // and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte in rx_buf_.
1740 // const_cast is safe: msg references mutable rx_buf_ data; the const& handler
1741 // signature is a generated protobuf pattern, not a true immutability contract.
1742 if (!msg.state.empty()) {
1743 const_cast<char *>(msg.state.c_str())[msg.state.size()] = '\0';
1744 }
1745
1746 for (auto &it : this->parent_->get_state_subs()) {
1747 if (msg.entity_id != it.entity_id) {
1748 continue;
1749 }
1750
1751 // If subscriber has attribute filter (non-null), message attribute must match it;
1752 // if subscriber has no filter (nullptr), message must have no attribute.
1753 if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) {
1754 continue;
1755 }
1756
1757 it.callback(msg.state);
1758 }
1759}
1760#endif
1761#ifdef USE_API_USER_DEFINED_ACTIONS
1762void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
1763 // Null-terminate string args in-place for safe c_str() usage in YAML service triggers.
1764 // Safe: full ExecuteServiceRequest decode is complete, all bytes in rx_buf_ consumed,
1765 // and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte for the last field.
1766 // const_cast is safe: msg references mutable rx_buf_ data; the const& handler
1767 // signature is a generated protobuf pattern, not a true immutability contract.
1768 for (auto &arg : const_cast<ExecuteServiceRequest &>(msg).args) {
1769 if (!arg.string_.empty()) {
1770 const_cast<char *>(arg.string_.c_str())[arg.string_.size()] = '\0';
1771 }
1772 }
1773 bool found = false;
1774#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
1775 // Register the call and get a unique server-generated action_call_id
1776 // This avoids collisions when multiple clients use the same call_id
1777 uint32_t action_call_id = 0;
1778 if (msg.call_id != 0) {
1779 action_call_id = this->parent_->register_active_action_call(msg.call_id, this);
1780 }
1781 // Use the overload that passes action_call_id separately (avoids copying msg)
1782 for (auto *service : this->parent_->get_user_services()) {
1783 if (service->execute_service(msg, action_call_id)) {
1784 found = true;
1785 }
1786 }
1787#else
1788 for (auto *service : this->parent_->get_user_services()) {
1789 if (service->execute_service(msg)) {
1790 found = true;
1791 }
1792 }
1793#endif
1794 if (!found) {
1795 ESP_LOGV(TAG, "Could not find service");
1796 }
1797 // Note: For services with supports_response != none, the call is unregistered
1798 // by an automatically appended APIUnregisterServiceCallAction at the end of
1799 // the action list. This ensures async actions (delays, waits) complete first.
1800}
1801#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
1802void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message) {
1804 resp.call_id = call_id;
1805 resp.success = success;
1806 resp.error_message = error_message;
1807 this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
1808}
1809#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
1810void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message,
1811 const uint8_t *response_data, size_t response_data_len) {
1813 resp.call_id = call_id;
1814 resp.success = success;
1815 resp.error_message = error_message;
1816 resp.response_data = response_data;
1817 resp.response_data_len = response_data_len;
1818 this->send_message(resp, ExecuteServiceResponse::MESSAGE_TYPE);
1819}
1820#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
1821#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
1822#endif
1823
1824#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
1825void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
1826#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
1827 if (msg.response_data_len > 0) {
1828 this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
1829 msg.response_data_len);
1830 } else
1831#endif
1832 {
1833 this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
1834 }
1835};
1836#endif
1837#ifdef USE_API_NOISE
1838bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) {
1840 resp.success = false;
1841
1842 psk_t psk{};
1843 if (msg.key_len == 0) {
1844 if (this->parent_->clear_noise_psk(true)) {
1845 resp.success = true;
1846 } else {
1847 ESP_LOGW(TAG, "Failed to clear encryption key");
1848 }
1849 } else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
1850 ESP_LOGW(TAG, "Invalid encryption key length");
1851 } else if (!this->parent_->save_noise_psk(psk, true)) {
1852 ESP_LOGW(TAG, "Failed to save encryption key");
1853 } else {
1854 resp.success = true;
1855 }
1856
1857 return this->send_message(resp, NoiseEncryptionSetKeyResponse::MESSAGE_TYPE);
1858}
1859void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
1860 if (!this->send_noise_encryption_set_key_response_(msg)) {
1861 this->on_fatal_error();
1862 }
1863}
1864#endif
1865#ifdef USE_API_HOMEASSISTANT_STATES
1866void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; }
1867#endif
1868bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
1869 if (this->flags_.remove)
1870 return false;
1871 if (this->helper_->can_write_without_blocking())
1872 return true;
1873 delay(0);
1874 APIError err = this->helper_->loop();
1875 if (err != APIError::OK) {
1876 this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
1877 return false;
1878 }
1879 if (this->helper_->can_write_without_blocking())
1880 return true;
1881 if (log_out_of_space) {
1882 ESP_LOGV(TAG, "Cannot send message because of TCP buffer space");
1883 }
1884 return false;
1885}
1886bool APIConnection::send_message_impl(const ProtoMessage &msg, uint8_t message_type) {
1887 uint32_t payload_size = msg.calculated_size();
1888 std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref();
1889 this->prepare_first_message_buffer(shared_buf, payload_size);
1890 size_t write_start = shared_buf.size();
1891 shared_buf.resize(write_start + payload_size);
1892 ProtoWriteBuffer buffer{&shared_buf, write_start};
1893 msg.encode(buffer);
1894 return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
1895}
1896bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
1897 const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
1898
1899 if (!this->try_to_clear_buffer(!is_log_message)) {
1900 return false;
1901 }
1902
1903 // Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
1904 this->helper_->set_nodelay_for_message(is_log_message);
1905
1906 APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
1907 if (err == APIError::WOULD_BLOCK)
1908 return false;
1909 if (err != APIError::OK) {
1910 this->fatal_error_with_log_(LOG_STR("Packet write failed"), err);
1911 return false;
1912 }
1913 // Do not set last_traffic_ on send
1914 return true;
1915}
1916void APIConnection::on_no_setup_connection() {
1917 this->on_fatal_error();
1918 this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
1919}
1920void APIConnection::on_fatal_error() {
1921 // Don't close socket here - keep it open so getpeername() works for logging
1922 // Socket will be closed when client is removed from the list in APIServer::loop()
1923 this->flags_.remove = true;
1924}
1925
1926void __attribute__((flatten)) APIConnection::DeferredBatch::push_item(const BatchItem &item) { items.push_back(item); }
1927
1928void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
1929 uint8_t aux_data_index) {
1930 // Check if we already have a message of this type for this entity
1931 // This provides deduplication per entity/message_type combination
1932 // O(n) but optimized for RAM and not performance.
1933 // Skip deduplication for events - they are edge-triggered, every occurrence matters
1934#ifdef USE_EVENT
1935 if (message_type != EventResponse::MESSAGE_TYPE)
1936#endif
1937 {
1938 for (const auto &item : items) {
1939 if (item.entity == entity && item.message_type == message_type)
1940 return; // Already queued
1941 }
1942 }
1943 // No existing item found (or event), add new one
1944 this->push_item({entity, message_type, estimated_size, aux_data_index});
1945}
1946
1947void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
1948 // Add high priority message and swap to front
1949 // This avoids expensive vector::insert which shifts all elements
1950 // Note: We only ever have one high-priority message at a time (ping OR disconnect)
1951 // If we're disconnecting, pings are blocked, so this simple swap is sufficient
1952 this->push_item({entity, message_type, estimated_size, AUX_DATA_UNUSED});
1953 if (items.size() > 1) {
1954 // Swap the new high-priority item to the front
1955 std::swap(items.front(), items.back());
1956 }
1957}
1958
1959bool APIConnection::send_message_smart_(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
1960 uint8_t aux_data_index) {
1961 if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
1962 auto &shared_buf = this->parent_->get_shared_buffer_ref();
1963 this->prepare_first_message_buffer(shared_buf, estimated_size);
1964 DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index};
1965 if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) &&
1966 this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type)) {
1967#ifdef HAS_PROTO_MESSAGE_DUMP
1968 this->log_batch_item_(item);
1969#endif
1970 return true;
1971 }
1972 }
1973 return this->schedule_message_(entity, message_type, estimated_size, aux_data_index);
1974}
1975
1976bool APIConnection::schedule_batch_() {
1977 if (!this->flags_.batch_scheduled) {
1978 this->flags_.batch_scheduled = true;
1979 this->deferred_batch_.batch_start_time = App.get_loop_component_start_time();
1980 }
1981 return true;
1982}
1983
1984void APIConnection::process_batch_() {
1985 if (this->deferred_batch_.empty()) {
1986 this->flags_.batch_scheduled = false;
1987 return;
1988 }
1989
1990 // Try to clear buffer first
1991 if (!this->try_to_clear_buffer(true)) {
1992 // Can't write now, we'll try again later
1993 return;
1994 }
1995
1996 // Get shared buffer reference once to avoid multiple calls
1997 auto &shared_buf = this->parent_->get_shared_buffer_ref();
1998 size_t num_items = this->deferred_batch_.size();
1999
2000 // Cache these values to avoid repeated virtual calls
2001 const uint8_t header_padding = this->helper_->frame_header_padding();
2002 const uint8_t footer_size = this->helper_->frame_footer_size();
2003
2004 // Pre-calculate exact buffer size needed based on message types
2005 uint32_t total_estimated_size = num_items * (header_padding + footer_size);
2006 for (size_t i = 0; i < num_items; i++) {
2007 total_estimated_size += this->deferred_batch_[i].estimated_size;
2008 }
2009 // Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
2010 if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
2011 total_estimated_size = MAX_BATCH_PACKET_SIZE;
2012 }
2013
2014 this->prepare_first_message_buffer(shared_buf, header_padding, total_estimated_size);
2015
2016 // Fast path for single message - buffer already allocated above
2017 if (num_items == 1) {
2018 const auto &item = this->deferred_batch_[0];
2019 // Let dispatch_message_ calculate size and encode if it fits
2020 uint16_t payload_size = this->dispatch_message_(item, std::numeric_limits<uint16_t>::max(), true);
2021
2022 if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&shared_buf}, item.message_type)) {
2023#ifdef HAS_PROTO_MESSAGE_DUMP
2024 // Log message after send attempt for VV debugging
2025 this->log_batch_item_(item);
2026#endif
2027 this->clear_batch_();
2028 } else if (payload_size == 0) {
2029 // Message too large to fit in available space
2030 ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
2031 this->clear_batch_();
2032 }
2033 return;
2034 }
2035
2036 // Multi-message path — heavy stack frame isolated in separate noinline function
2037 this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
2038}
2039
2040// Separated from process_batch_() so the single-message fast path gets a minimal
2041// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
2042void APIConnection::process_batch_multi_(std::vector<uint8_t> &shared_buf, size_t num_items, uint8_t header_padding,
2043 uint8_t footer_size) {
2044 // Ensure MessageInfo remains trivially destructible for our placement new approach
2045 static_assert(std::is_trivially_destructible<MessageInfo>::value,
2046 "MessageInfo must remain trivially destructible with this placement-new approach");
2047
2048 const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
2049 const uint8_t frame_overhead = header_padding + footer_size;
2050
2051 // Stack-allocated array for message info
2052 alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
2053 MessageInfo *message_info = reinterpret_cast<MessageInfo *>(message_info_storage);
2054 size_t items_processed = 0;
2055 uint16_t remaining_size = std::numeric_limits<uint16_t>::max();
2056 // Track where each message's header padding begins in the buffer
2057 // For plaintext: this is where the 6-byte header padding starts
2058 // For noise: this is where the 7-byte header padding starts
2059 // The actual message data follows after the header padding
2060 uint32_t current_offset = 0;
2061
2062 // Process items and encode directly to buffer (up to our limit)
2063 for (size_t i = 0; i < messages_to_process; i++) {
2064 const auto &item = this->deferred_batch_[i];
2065 // Try to encode message via dispatch
2066 // The dispatch function calculates overhead to determine if the message fits
2067 uint16_t payload_size = this->dispatch_message_(item, remaining_size, i == 0);
2068
2069 if (payload_size == 0) {
2070 // Message won't fit, stop processing
2071 break;
2072 }
2073
2074 // Message was encoded successfully
2075 // payload_size is header_padding + actual payload size + footer_size
2076 uint16_t proto_payload_size = payload_size - frame_overhead;
2077 // Use placement new to construct MessageInfo in pre-allocated stack array
2078 // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
2079 // Explicit destruction is not needed because MessageInfo is trivially destructible,
2080 // as ensured by the static_assert in its definition.
2081 new (&message_info[items_processed++]) MessageInfo(item.message_type, current_offset, proto_payload_size);
2082 // After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation
2083 if (items_processed == 1) {
2084 remaining_size = MAX_BATCH_PACKET_SIZE;
2085 }
2086 remaining_size -= payload_size;
2087 // Calculate where the next message's header padding will start
2088 // Current buffer size + footer space for this message
2089 current_offset = shared_buf.size() + footer_size;
2090 }
2091
2092 if (items_processed > 0) {
2093 // Add footer space for the last message (for Noise protocol MAC)
2094 if (footer_size > 0) {
2095 shared_buf.resize(shared_buf.size() + footer_size);
2096 }
2097
2098 // Send all collected messages
2099 APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
2100 std::span<const MessageInfo>(message_info, items_processed));
2101 if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
2102 this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
2103 }
2104
2105#ifdef HAS_PROTO_MESSAGE_DUMP
2106 // Log messages after send attempt for VV debugging
2107 // It's safe to use the buffer for logging at this point regardless of send result
2108 for (size_t i = 0; i < items_processed; i++) {
2109 const auto &item = this->deferred_batch_[i];
2110 this->log_batch_item_(item);
2111 }
2112#endif
2113
2114 // Partial batch — remove processed items and reschedule
2115 if (items_processed < this->deferred_batch_.size()) {
2116 this->deferred_batch_.remove_front(items_processed);
2117 this->schedule_batch_();
2118 return;
2119 }
2120 }
2121
2122 // All items processed (or none could be processed)
2123 this->clear_batch_();
2124}
2125
2126// Dispatch message encoding based on message_type
2127// Switch assigns function pointer, single call site for smaller code size
2128uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size,
2129 bool batch_first) {
2130 this->flags_.batch_first_message = batch_first;
2131#ifdef USE_EVENT
2132 // Events need aux_data_index to look up event type from entity
2133 if (item.message_type == EventResponse::MESSAGE_TYPE) {
2134 // Skip if aux_data_index is invalid (should never happen in normal operation)
2135 if (item.aux_data_index == DeferredBatch::AUX_DATA_UNUSED)
2136 return 0;
2137 auto *event = static_cast<event::Event *>(item.entity);
2138 return try_send_event_response(event, StringRef::from_maybe_nullptr(event->get_event_type(item.aux_data_index)),
2139 this, remaining_size);
2140 }
2141#endif
2142
2143 // All other message types use function pointer lookup via switch
2144 MessageCreatorPtr func = nullptr;
2145
2146// Macros to reduce repetitive switch cases
2147#define CASE_STATE_INFO(entity_name, StateResp, InfoResp) \
2148 case StateResp::MESSAGE_TYPE: \
2149 func = &try_send_##entity_name##_state; \
2150 break; \
2151 case InfoResp::MESSAGE_TYPE: \
2152 func = &try_send_##entity_name##_info; \
2153 break;
2154#define CASE_INFO_ONLY(entity_name, InfoResp) \
2155 case InfoResp::MESSAGE_TYPE: \
2156 func = &try_send_##entity_name##_info; \
2157 break;
2158
2159 switch (item.message_type) {
2160#ifdef USE_BINARY_SENSOR
2161 CASE_STATE_INFO(binary_sensor, BinarySensorStateResponse, ListEntitiesBinarySensorResponse)
2162#endif
2163#ifdef USE_COVER
2164 CASE_STATE_INFO(cover, CoverStateResponse, ListEntitiesCoverResponse)
2165#endif
2166#ifdef USE_FAN
2167 CASE_STATE_INFO(fan, FanStateResponse, ListEntitiesFanResponse)
2168#endif
2169#ifdef USE_LIGHT
2170 CASE_STATE_INFO(light, LightStateResponse, ListEntitiesLightResponse)
2171#endif
2172#ifdef USE_SENSOR
2173 CASE_STATE_INFO(sensor, SensorStateResponse, ListEntitiesSensorResponse)
2174#endif
2175#ifdef USE_SWITCH
2176 CASE_STATE_INFO(switch, SwitchStateResponse, ListEntitiesSwitchResponse)
2177#endif
2178#ifdef USE_BUTTON
2179 CASE_INFO_ONLY(button, ListEntitiesButtonResponse)
2180#endif
2181#ifdef USE_TEXT_SENSOR
2182 CASE_STATE_INFO(text_sensor, TextSensorStateResponse, ListEntitiesTextSensorResponse)
2183#endif
2184#ifdef USE_CLIMATE
2185 CASE_STATE_INFO(climate, ClimateStateResponse, ListEntitiesClimateResponse)
2186#endif
2187#ifdef USE_NUMBER
2188 CASE_STATE_INFO(number, NumberStateResponse, ListEntitiesNumberResponse)
2189#endif
2190#ifdef USE_DATETIME_DATE
2191 CASE_STATE_INFO(date, DateStateResponse, ListEntitiesDateResponse)
2192#endif
2193#ifdef USE_DATETIME_TIME
2194 CASE_STATE_INFO(time, TimeStateResponse, ListEntitiesTimeResponse)
2195#endif
2196#ifdef USE_DATETIME_DATETIME
2197 CASE_STATE_INFO(datetime, DateTimeStateResponse, ListEntitiesDateTimeResponse)
2198#endif
2199#ifdef USE_TEXT
2200 CASE_STATE_INFO(text, TextStateResponse, ListEntitiesTextResponse)
2201#endif
2202#ifdef USE_SELECT
2203 CASE_STATE_INFO(select, SelectStateResponse, ListEntitiesSelectResponse)
2204#endif
2205#ifdef USE_LOCK
2206 CASE_STATE_INFO(lock, LockStateResponse, ListEntitiesLockResponse)
2207#endif
2208#ifdef USE_VALVE
2209 CASE_STATE_INFO(valve, ValveStateResponse, ListEntitiesValveResponse)
2210#endif
2211#ifdef USE_MEDIA_PLAYER
2212 CASE_STATE_INFO(media_player, MediaPlayerStateResponse, ListEntitiesMediaPlayerResponse)
2213#endif
2214#ifdef USE_ALARM_CONTROL_PANEL
2215 CASE_STATE_INFO(alarm_control_panel, AlarmControlPanelStateResponse, ListEntitiesAlarmControlPanelResponse)
2216#endif
2217#ifdef USE_WATER_HEATER
2218 CASE_STATE_INFO(water_heater, WaterHeaterStateResponse, ListEntitiesWaterHeaterResponse)
2219#endif
2220#ifdef USE_CAMERA
2221 CASE_INFO_ONLY(camera, ListEntitiesCameraResponse)
2222#endif
2223#ifdef USE_INFRARED
2224 CASE_INFO_ONLY(infrared, ListEntitiesInfraredResponse)
2225#endif
2226#ifdef USE_EVENT
2227 CASE_INFO_ONLY(event, ListEntitiesEventResponse)
2228#endif
2229#ifdef USE_UPDATE
2230 CASE_STATE_INFO(update, UpdateStateResponse, ListEntitiesUpdateResponse)
2231#endif
2232 // Special messages (not entity state/info)
2233 case ListEntitiesDoneResponse::MESSAGE_TYPE:
2234 func = &try_send_list_info_done;
2235 break;
2236 case DisconnectRequest::MESSAGE_TYPE:
2237 func = &try_send_disconnect_request;
2238 break;
2239 case PingRequest::MESSAGE_TYPE:
2240 func = &try_send_ping_request;
2241 break;
2242 default:
2243 return 0;
2244 }
2245
2246#undef CASE_STATE_INFO
2247#undef CASE_INFO_ONLY
2248
2249 return func(item.entity, this, remaining_size);
2250}
2251
2252uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
2254 return encode_message_to_buffer(resp, ListEntitiesDoneResponse::MESSAGE_TYPE, conn, remaining_size);
2255}
2256
2257uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
2259 return encode_message_to_buffer(req, DisconnectRequest::MESSAGE_TYPE, conn, remaining_size);
2260}
2261
2262uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
2263 PingRequest req;
2264 return encode_message_to_buffer(req, PingRequest::MESSAGE_TYPE, conn, remaining_size);
2265}
2266
2267#ifdef USE_API_HOMEASSISTANT_STATES
2268void APIConnection::process_state_subscriptions_() {
2269 const auto &subs = this->parent_->get_state_subs();
2270 if (this->state_subs_at_ >= static_cast<int>(subs.size())) {
2271 this->state_subs_at_ = -1;
2272 return;
2273 }
2274
2275 const auto &it = subs[this->state_subs_at_];
2277 resp.entity_id = StringRef(it.entity_id);
2278
2279 // Avoid string copy by using the const char* pointer if it exists
2280 resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef("");
2281
2282 resp.once = it.once;
2283 if (this->send_message(resp, SubscribeHomeAssistantStateResponse::MESSAGE_TYPE)) {
2284 this->state_subs_at_++;
2285 }
2286}
2287#endif // USE_API_HOMEASSISTANT_STATES
2288
2289void APIConnection::log_client_(int level, const LogString *message) {
2290 char peername[socket::SOCKADDR_STR_LEN];
2291 esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
2292 this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
2293}
2294
2295void APIConnection::log_warning_(const LogString *message, APIError err) {
2296 char peername[socket::SOCKADDR_STR_LEN];
2297 ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
2298 LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
2299}
2300
2301} // namespace esphome::api
2302#endif
const std::string & get_friendly_name() const
Get the friendly name of this Application set by pre_setup().
const auto & get_areas()
static constexpr size_t BUILD_TIME_STR_SIZE
Size of buffer required for build time string (including null terminator)
void get_build_time_string(std::span< char, BUILD_TIME_STR_SIZE > buffer)
Copy the build time string into the provided buffer Buffer must be BUILD_TIME_STR_SIZE bytes (compile...
const char * get_area() const
Get the area of this Application set by pre_setup().
const auto & get_devices()
const std::string & get_name() const
Get the name of this Application set by pre_setup().
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
void begin(bool include_internal=false)
ESPDEPRECATED("object_id mangles names and all object_id methods are planned for removal " "(see https://github.com/esphome/backlog/issues/76). " "Now is the time to stop using object_id. If still needed, use get_object_id_to() " "which will remain available longer. get_object_id() will be removed in 2026.7.0", "2025.12.0") std uint32_t get_object_id_hash()
uint32_t get_device_id() const
Fixed-capacity vector - allocates once at runtime, never reallocates This avoids std::vector template...
Definition helpers.h:299
void push_back(const T &value)
Add element without bounds checking Caller must ensure sufficient capacity was allocated via init() S...
Definition helpers.h:416
void init(size_t n)
Definition helpers.h:389
StringRef is a reference to a string owned by something else.
Definition string_ref.h:26
constexpr const char * c_str() const
Definition string_ref.h:73
constexpr bool empty() const
Definition string_ref.h:76
constexpr size_type size() const
Definition string_ref.h:74
static constexpr StringRef from_lit(const CharT(&s)[N])
Definition string_ref.h:50
static StringRef from_maybe_nullptr(const char *s)
Definition string_ref.h:53
struct esphome::api::APIConnection::APIFlags flags_
std::unique_ptr< APIFrameHelper > helper_
APIConnection(std::unique_ptr< socket::Socket > socket, APIServer *parent)
uint16_t(*)(EntityBase *, APIConnection *, uint32_t remaining_size) MessageCreatorPtr
void on_button_command_request(const ButtonCommandRequest &msg) override
void log_send_message_(const char *name, const char *dump)
APINoiseContext & get_noise_ctx()
Definition api_server.h:74
std::vector< uint8_t > & get_shared_buffer_ref()
Definition api_server.h:68
enums::AlarmControlPanelStateCommand command
Definition api_pb2.h:2585
enums::AlarmControlPanelState state
Definition api_pb2.h:2569
enums::BluetoothScannerMode mode
Definition api_pb2.h:2285
void set_data(const uint8_t *data, size_t len)
Definition api_pb2.h:1300
enums::ClimateSwingMode swing_mode
Definition api_pb2.h:1410
enums::ClimateFanMode fan_mode
Definition api_pb2.h:1408
enums::ClimatePreset preset
Definition api_pb2.h:1414
enums::ClimateFanMode fan_mode
Definition api_pb2.h:1377
enums::ClimateSwingMode swing_mode
Definition api_pb2.h:1378
enums::ClimateAction action
Definition api_pb2.h:1376
enums::ClimatePreset preset
Definition api_pb2.h:1380
enums::CoverOperation current_operation
Definition api_pb2.h:623
Fixed-size buffer for message dumps - avoids heap allocation.
Definition proto.h:388
enums::FanDirection direction
Definition api_pb2.h:706
enums::FanDirection direction
Definition api_pb2.h:683
ParsedTimezone parsed_timezone
Definition api_pb2.h:1163
enums::ColorMode color_mode
Definition api_pb2.h:750
const std::vector< const char * > * supported_custom_presets
Definition api_pb2.h:1349
const climate::ClimateSwingModeMask * supported_swing_modes
Definition api_pb2.h:1346
const std::vector< const char * > * supported_custom_fan_modes
Definition api_pb2.h:1347
const climate::ClimatePresetMask * supported_presets
Definition api_pb2.h:1348
const climate::ClimateFanModeMask * supported_fan_modes
Definition api_pb2.h:1345
const climate::ClimateModeMask * supported_modes
Definition api_pb2.h:1340
const FixedVector< const char * > * event_types
Definition api_pb2.h:2769
const std::vector< const char * > * supported_preset_modes
Definition api_pb2.h:665
const FixedVector< const char * > * effects
Definition api_pb2.h:732
const light::ColorModeMask * supported_color_modes
Definition api_pb2.h:729
std::vector< MediaPlayerSupportedFormat > supported_formats
Definition api_pb2.h:1774
const FixedVector< const char * > * options
Definition api_pb2.h:1557
enums::SensorStateClass state_class
Definition api_pb2.h:823
const water_heater::WaterHeaterModeMask * supported_modes
Definition api_pb2.h:1440
enums::LockCommand command
Definition api_pb2.h:1704
enums::MediaPlayerCommand command
Definition api_pb2.h:1810
enums::MediaPlayerState state
Definition api_pb2.h:1791
uint32_t calculated_size() const
Definition proto.h:927
virtual const char * message_name() const
Definition proto.h:463
virtual void encode(ProtoWriteBuffer &buffer) const
Definition proto.h:456
virtual const char * dump_to(DumpBuffer &out) const =0
void set_message(const uint8_t *data, size_t len)
Definition api_pb2.h:962
enums::UpdateCommand command
Definition api_pb2.h:2949
enums::ValveOperation current_operation
Definition api_pb2.h:2823
std::vector< VoiceAssistantExternalWakeWord > external_wake_words
Definition api_pb2.h:2501
std::vector< VoiceAssistantWakeWord > available_wake_words
Definition api_pb2.h:2516
const std::vector< std::string > * active_wake_words
Definition api_pb2.h:2517
std::vector< std::string > active_wake_words
Definition api_pb2.h:2534
enums::ZWaveProxyRequestType type
Definition api_pb2.h:2985
Base class for all binary_sensor-type classes.
void bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg)
void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg)
void bluetooth_device_request(const api::BluetoothDeviceRequest &msg)
void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg)
void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags)
void unsubscribe_api_connection(api::APIConnection *api_connection)
void get_bluetooth_mac_address_pretty(std::span< char, 18 > output)
void bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg)
void bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg)
void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg)
Base class for all buttons.
Definition button.h:25
Abstract camera base class.
Definition camera.h:115
virtual CameraImageReader * create_image_reader()=0
Returns a new camera image reader that keeps track of the JPEG data in the camera image.
virtual void start_stream(CameraRequester requester)=0
virtual void stop_stream(CameraRequester requester)=0
virtual void request_image(CameraRequester requester)=0
static Camera * instance()
The singleton instance of the camera implementation.
Definition camera.cpp:19
ClimateDevice - This is the base class for all climate integrations.
Definition climate.h:186
Base class for all cover devices.
Definition cover.h:110
uint8_t get_last_event_type_index() const
Return index of last triggered event type, or max uint8_t if no event triggered yet.
Definition event.h:54
Infrared - Base class for infrared remote control implementations.
Definition infrared.h:110
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition light_state.h:93
Base class for all locks.
Definition lock.h:110
Base-class for all numbers.
Definition number.h:29
Base-class for all selects.
Definition select.h:29
Base-class for all sensors.
Definition sensor.h:47
Base class for all switches.
Definition switch.h:38
Base-class for all text inputs.
Definition text.h:21
void set_timezone(const char *tz)
Set the time zone from a POSIX TZ string.
Base class for all valve devices.
Definition valve.h:104
void on_timer_event(const api::VoiceAssistantTimerEventResponse &msg)
void on_audio(const api::VoiceAssistantAudio &msg)
void client_subscription(api::APIConnection *client, bool subscribe)
void on_event(const api::VoiceAssistantEventResponse &msg)
void on_announce(const api::VoiceAssistantAnnounceRequest &msg)
api::APIConnection * get_api_connection() const
void on_set_configuration(const std::vector< std::string > &active_wake_words)
void zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type)
void send_frame(const uint8_t *data, size_t length)
uint32_t get_feature_flags() const
Definition zwave_proxy.h:59
void api_connection_authenticated(api::APIConnection *conn)
struct @65::@66 __attribute__
const char * message
Definition component.cpp:38
uint16_t type
bool state
Definition fan.h:2
uint32_t socklen_t
Definition headers.h:97
const LogString * api_error_to_logstr(APIError err)
std::array< uint8_t, 32 > psk_t
BluetoothProxy * global_bluetooth_proxy
@ CLIMATE_SUPPORTS_CURRENT_HUMIDITY
@ CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE
@ CLIMATE_SUPPORTS_CURRENT_TEMPERATURE
@ CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE
ClimatePreset
Enum for all preset modes NOTE: If adding values, update ClimatePresetMask in climate_traits....
ClimateSwingMode
Enum for all modes a climate swing can be in NOTE: If adding values, update ClimateSwingModeMask in c...
ClimateMode
Enum for all modes a climate device can be in.
ClimateFanMode
NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value.
FanDirection
Simple enum to represent the direction of a fan.
Definition fan.h:21
HomeassistantTime * global_homeassistant_time
ColorMode
Color modes are a combination of color capabilities that can be used at the same time.
Definition color_mode.h:49
@ COLOR_TEMPERATURE
Color temperature can be controlled.
@ COLD_WARM_WHITE
Brightness of cold and warm white output can be controlled.
void set_global_tz(const ParsedTimezone &tz)
Set the global timezone used by epoch_to_local_tm() when called without a timezone.
Definition posix_tz.cpp:14
DSTRuleType
Type of DST transition rule.
Definition posix_tz.h:11
VoiceAssistant * global_voice_assistant
@ WATER_HEATER_STATE_ON
Water heater is on (not in standby)
@ WATER_HEATER_STATE_AWAY
Away/vacation mode is currently active.
ZWaveProxy * global_zwave_proxy
void HOT esp_log_printf_(int level, const char *tag, int line, const char *format,...)
Definition log.cpp:11
std::string size_t len
Definition helpers.h:817
void get_mac_address_raw(uint8_t *mac)
Get the device MAC address as raw bytes, written into the provided byte array (6 bytes).
Definition helpers.cpp:73
void HOT delay(uint32_t ms)
Definition core.cpp:27
Application App
Global storage of Application pointer - only one Application can exist.
size_t base64_decode(const std::string &encoded_string, uint8_t *buf, size_t buf_len)
Definition helpers.cpp:593
char * format_mac_addr_upper(const uint8_t *mac, char *output)
Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators)
Definition helpers.h:1170
A more user-friendly version of struct tm from time.h.
Definition time.h:21
uint16_t day
Day of year (for JULIAN_NO_LEAP and DAY_OF_YEAR)
Definition posix_tz.h:21
DSTRuleType type
Type of rule.
Definition posix_tz.h:22
uint8_t week
Week 1-5, 5 = last (for MONTH_WEEK_DAY)
Definition posix_tz.h:24
int32_t time_seconds
Seconds after midnight (default 7200 = 2:00 AM)
Definition posix_tz.h:20
uint8_t day_of_week
Day 0-6, 0 = Sunday (for MONTH_WEEK_DAY)
Definition posix_tz.h:25
uint8_t month
Month 1-12 (for MONTH_WEEK_DAY)
Definition posix_tz.h:23
Parsed POSIX timezone information (packed for 32-bit: 32 bytes)
Definition posix_tz.h:29
DSTRule dst_end
When DST ends.
Definition posix_tz.h:33
DSTRule dst_start
When DST starts.
Definition posix_tz.h:32
int32_t dst_offset_seconds
DST offset from UTC in seconds.
Definition posix_tz.h:31
int32_t std_offset_seconds
Standard time offset from UTC in seconds (positive = west)
Definition posix_tz.h:30
uint32_t payload_size()
const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM
Definition web_server.h:28