ESPHome 2026.10.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#include "api_connection_buffer.h" // for the APIServer-dependent APIConnection inlines
4#ifdef USE_API_NOISE
6#endif
7#ifdef USE_API_PLAINTEXT
9#endif
10#ifdef USE_API_USER_DEFINED_ACTIONS
11#include "user_services.h"
12#endif
13#include <cerrno>
14#include <cinttypes>
15#include <functional>
16#include <limits>
17#include <new>
18#include <utility>
19#ifdef USE_ESP8266
20#include <pgmspace.h>
21#endif
25#include "esphome/core/hal.h"
27#include "esphome/core/log.h"
29#ifdef USE_PROVISIONING
31#endif
32
33#ifdef USE_DEEP_SLEEP
35#endif
36#ifdef USE_HOMEASSISTANT_TIME
38#endif
39#ifdef USE_BLUETOOTH_PROXY
41#endif
42#ifdef USE_CLIMATE
44#endif
45#ifdef USE_VOICE_ASSISTANT
47#endif
48#ifdef USE_ZWAVE_PROXY
50#endif
51#ifdef USE_WATER_HEATER
53#endif
54#ifdef USE_INFRARED
56#endif
57#ifdef USE_RADIO_FREQUENCY
59#endif
60
61namespace esphome::api {
62
63// Maximum messages to read per loop iteration to prevent starving other components.
64// This is a balance between API responsiveness and allowing other components to run.
65// Since each message could contain multiple protobuf messages when using packet batching,
66// this limits the number of messages processed, not the number of TCP packets.
67static constexpr uint8_t MAX_MESSAGES_PER_LOOP = 10;
68static constexpr uint8_t MAX_PING_RETRIES = 60;
69static constexpr uint16_t PING_RETRY_INTERVAL = 1000;
70static constexpr uint32_t KEEPALIVE_DISCONNECT_TIMEOUT = (KEEPALIVE_TIMEOUT_MS * 5) / 2;
71// Timeout for completing the handshake (Noise transport + HelloRequest).
72// A stalled handshake from a buggy client or network glitch holds a connection
73// slot, which can prevent legitimate clients from reconnecting. Also hardens
74// against the less likely case of intentional connection slot exhaustion.
75//
76// 60s is intentionally high: on ESP8266 with power_save_mode: LIGHT and weak
77// WiFi (-70 dBm+), TCP retransmissions push real-world handshake times to
78// 28-30s. See https://github.com/esphome/esphome/issues/14999
79static constexpr uint32_t HANDSHAKE_TIMEOUT_MS = 60000;
80
81static constexpr auto ESPHOME_VERSION_REF = StringRef::from_lit(ESPHOME_VERSION);
82
83// Cross-validate C++ constants against proto max_data_length annotations in api.proto
84static_assert(MAC_ADDRESS_PRETTY_BUFFER_SIZE - 1 == 17,
85 "Update max_data_length for mac_address/bluetooth_mac_address in api.proto");
86static_assert(Application::BUILD_TIME_STR_SIZE - 1 == 25, "Update max_data_length for compilation_time in api.proto");
87static_assert(sizeof(ESPHOME_VERSION) - 1 <= 32, "Update max_data_length for esphome_version in api.proto");
88static_assert(ESPHOME_DEVICE_NAME_MAX_LEN <= 31, "Update max_data_length for name in api.proto");
89static_assert(ESPHOME_FRIENDLY_NAME_MAX_LEN <= 120, "Update max_data_length for friendly_name in api.proto");
90
91static const char *const TAG = "api.connection";
92
93#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
94void log_dropped_message(const char *tag, int line, const LogString *what) {
95 esp_log_printf_(ESPHOME_LOG_LEVEL_WARN, tag, line, ESPHOME_LOG_FORMAT("%s dropped, TCP buffer full"),
96 LOG_STR_ARG(what));
97}
98#endif
99#ifdef USE_CAMERA
100static const int CAMERA_STOP_STREAM = 5000;
101#endif
102
103#ifdef USE_DEVICES
104// Helper macro for entity command handlers - gets entity by key and device_id, returns if not found, and creates call
105// object
106#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
107 entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \
108 if ((entity_var) == nullptr) \
109 return; \
110 auto call = (entity_var)->make_call();
111
112// Helper macro for entity command handlers that don't use make_call() - gets entity by key and device_id and returns if
113// not found
114#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
115 entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id); \
116 if ((entity_var) == nullptr) \
117 return;
118
119// Helper macro for multi-entity dispatch: looks up an entity by key and device_id without early return or make_call().
120// Use when multiple entity types must be checked in sequence (at most one will match).
121#define ENTITY_COMMAND_LOOKUP(entity_type, entity_var, getter_name) \
122 entity_type *entity_var = App.get_##getter_name##_by_key(msg.key, msg.device_id)
123
124#else // No device support, use simpler macros
125// Helper macro for entity command handlers - gets entity by key, returns if not found, and creates call
126// object
127#define ENTITY_COMMAND_MAKE_CALL(entity_type, entity_var, getter_name) \
128 entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
129 if ((entity_var) == nullptr) \
130 return; \
131 auto call = (entity_var)->make_call();
132
133// Helper macro for entity command handlers that don't use make_call() - gets entity by key and returns if
134// not found
135#define ENTITY_COMMAND_GET(entity_type, entity_var, getter_name) \
136 entity_type *entity_var = App.get_##getter_name##_by_key(msg.key); \
137 if ((entity_var) == nullptr) \
138 return;
139
140// Helper macro for multi-entity dispatch: looks up an entity by key without early return or make_call().
141// Use when multiple entity types must be checked in sequence (at most one will match).
142#define ENTITY_COMMAND_LOOKUP(entity_type, entity_var, getter_name) \
143 entity_type *entity_var = App.get_##getter_name##_by_key(msg.key)
144
145#endif // USE_DEVICES
146
147APIConnection::APIConnection(std::unique_ptr<socket::Socket> sock, APIServer *parent) : parent_(parent) {
148#if defined(USE_API_PLAINTEXT) && defined(USE_API_NOISE)
149 auto &noise_ctx = parent->get_noise_ctx();
150 if (noise_ctx.has_psk()) {
151 this->helper_ = std::unique_ptr<APIFrameHelper>{new APINoiseFrameHelper(std::move(sock), noise_ctx)};
152 } else {
153 this->helper_ = std::unique_ptr<APIFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
154 }
155#elif defined(USE_API_PLAINTEXT)
156 this->helper_ = std::unique_ptr<APIPlaintextFrameHelper>{new APIPlaintextFrameHelper(std::move(sock))};
157#elif defined(USE_API_NOISE)
158 this->helper_ =
159 std::unique_ptr<APINoiseFrameHelper>{new APINoiseFrameHelper(std::move(sock), parent->get_noise_ctx())};
160#else
161#error "No frame helper defined"
162#endif
163}
164
165void APIConnection::start() {
166 this->last_traffic_ = App.get_loop_component_start_time();
167
168 APIError err = this->helper_->init();
169 if (err != APIError::OK) {
170 this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
171 return;
172 }
173 // Initialize client name with peername (IP address) until Hello message provides actual name
174 char peername[socket::SOCKADDR_STR_LEN];
175 this->helper_->set_client_name(this->helper_->get_peername_to(peername), strlen(peername));
176}
177
178APIConnection::~APIConnection() {
179 this->destroy_active_iterator_();
180#ifdef USE_BLUETOOTH_PROXY
181 if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) {
183 }
184#endif
185#ifdef USE_VOICE_ASSISTANT
186 if (voice_assistant::global_voice_assistant->get_api_connection() == this) {
188 }
189#endif
190#ifdef USE_ZWAVE_PROXY
191 if (zwave_proxy::global_zwave_proxy != nullptr && zwave_proxy::global_zwave_proxy->get_api_connection() == this) {
192 zwave_proxy::global_zwave_proxy->zwave_proxy_request(this, enums::ZWAVE_PROXY_REQUEST_TYPE_UNSUBSCRIBE);
193 }
194#endif
195#ifdef USE_SERIAL_PROXY
196 for (auto *proxy : App.get_serial_proxies()) {
197 if (proxy->get_api_connection() == this) {
198 proxy->serial_proxy_request(this, enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE);
199 }
200 }
201#endif
202}
203
204#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
205void APIConnection::upgrade_helper_to_noise_() {
206 // The client opened with a Noise hello while this device has no encryption
207 // key set. Replace the plaintext helper with a Noise helper so the key can
208 // be provisioned over an encrypted channel: the noise context PSK is all
209 // zeros when unprovisioned, and NNpsk0 still runs a fresh ephemeral X25519
210 // exchange, so a passive listener cannot read the session. A publicly known
211 // PSK authenticates nobody; this protects against sniffing only.
212 auto *plaintext = static_cast<APIPlaintextFrameHelper *>(this->helper_.get());
213 uint8_t header[3];
214 uint8_t header_len = plaintext->get_consumed_header(header);
215 auto *noise = new APINoiseFrameHelper(plaintext->release_socket_for_switch(), this->parent_->get_noise_ctx());
216 // Carry over the peername-based client name (Hello has not arrived yet)
217 const char *name = plaintext->get_client_name();
218 noise->set_client_name(name, strlen(name));
219 this->helper_.reset(noise); // destroys the plaintext helper
220 APIError err = noise->init_from_handoff(header, header_len);
221 if (err != APIError::OK) {
222 this->fatal_error_with_log_(LOG_STR("Noise handoff failed"), err);
223 }
224}
225#endif // USE_API_NOISE && USE_API_PLAINTEXT
226
227void APIConnection::destroy_active_iterator_() {
228 switch (this->active_iterator_) {
229 case ActiveIterator::LIST_ENTITIES:
230 this->iterator_storage_.list_entities.~ListEntitiesIterator();
231 break;
232 case ActiveIterator::INITIAL_STATE:
233 this->iterator_storage_.initial_state.~InitialStateIterator();
234 break;
235 case ActiveIterator::NONE:
236 break;
237 }
238 this->active_iterator_ = ActiveIterator::NONE;
239}
240
241void APIConnection::begin_iterator_(ActiveIterator type) {
242 this->destroy_active_iterator_();
243 this->active_iterator_ = type;
244 if (type == ActiveIterator::LIST_ENTITIES) {
245 new (&this->iterator_storage_.list_entities) ListEntitiesIterator(this);
246 this->iterator_storage_.list_entities.begin();
247 } else {
248 new (&this->iterator_storage_.initial_state) InitialStateIterator(this);
249 this->iterator_storage_.initial_state.begin();
250 }
251}
252
253void APIConnection::loop() {
254 if (this->flags_.next_close) {
255 // requested a disconnect - don't close socket here, let APIServer::loop() do it
256 // so getpeername() still works for the disconnect trigger
257 this->flags_.remove = true;
258 return;
259 }
260
261 APIError err = this->helper_->loop();
262 if (err != APIError::OK) {
263 this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
264 return;
265 }
266
268 // Check if socket has data ready before attempting to read.
269 // Also try reading if we hit the message limit last time — LWIP's rcvevent
270 // (used by is_socket_ready) tracks pbuf dequeues, not bytes. When multiple
271 // messages share a TCP segment, the last message's data stays in LWIP's
272 // lastdata cache after rcvevent hits 0, making is_socket_ready() return false
273 // even though data remains.
274 if (this->helper_->is_socket_ready() || this->flags_.may_have_remaining_data) {
275 this->flags_.may_have_remaining_data = false;
276 // Read up to MAX_MESSAGES_PER_LOOP messages per loop to improve throughput
277 uint8_t message_count = 0;
278 for (; message_count < MAX_MESSAGES_PER_LOOP; message_count++) {
279 ReadPacketBuffer buffer;
280 err = this->helper_->read_packet(&buffer);
281 if (err == APIError::WOULD_BLOCK) {
282 // No more data available
283 break;
284 } else if (err != APIError::OK) {
285#if defined(USE_API_NOISE) && defined(USE_API_PLAINTEXT)
286 // Checked inside the error branch to keep the hot err == OK path
287 // free of it; this can only fire on the first bytes of a plaintext
288 // helper on an unprovisioned device
289 if (err == APIError::PROTOCOL_SWITCH_TO_NOISE) {
290 this->upgrade_helper_to_noise_();
291 return;
292 }
293#endif
294 this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
295 return;
296 } else {
297 // Only update last_traffic_ after authentication to ensure the
298 // handshake timeout is an absolute deadline from connection start.
299 // Pre-auth messages (e.g. PingRequest) must not reset the timer.
300 if (this->is_authenticated()) {
301 this->last_traffic_ = now;
302 }
303 // read a packet
304 this->read_message_(buffer.data_len, buffer.type, buffer.data);
305 if (this->flags_.remove)
306 return;
307 }
308 }
309 // If we hit the limit, there may be more data remaining in LWIP's
310 // lastdata cache that rcvevent doesn't account for.
311 if (message_count == MAX_MESSAGES_PER_LOOP) {
312 this->flags_.may_have_remaining_data = true;
313 }
314 }
315
316 // Process deferred batch if scheduled and timer has expired
317 if (this->flags_.batch_scheduled && now - this->deferred_batch_.batch_start_time >= this->get_batch_delay_ms_()) {
318 this->process_batch_();
319 }
320
321 if (this->active_iterator_ != ActiveIterator::NONE) {
322 this->process_active_iterator_();
323 }
324
325 // Disconnect clients that haven't completed the handshake in time.
326 // Stale half-open connections from buggy clients or network issues can
327 // accumulate and block legitimate clients from reconnecting.
328 if (!this->is_authenticated() && now - this->last_traffic_ > HANDSHAKE_TIMEOUT_MS) {
329 this->on_fatal_error();
330 this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("handshake timeout; disconnecting"));
331 return;
332 }
333
334 // Keepalive: only call into the cold path when enough time has elapsed.
335 // When sent_ping is true, last_traffic_ hasn't been updated so this
336 // condition is already satisfied — covers both send-ping and disconnect cases.
337 if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS) {
338 this->check_keepalive_(now);
339 }
340
341#ifdef USE_API_HOMEASSISTANT_STATES
342 if (state_subs_at_ >= 0) {
343 this->process_state_subscriptions_();
344 }
345#endif
346
347#ifdef USE_CAMERA
348 // Process camera last - state updates are higher priority
349 // (missing a frame is fine, missing a state update is not)
350 this->try_send_camera_image_();
351#endif
352}
353
354void APIConnection::check_keepalive_(uint32_t now) {
355 // Caller guarantees: now - last_traffic_ > KEEPALIVE_TIMEOUT_MS
356 if (this->flags_.sent_ping) {
357 // Disconnect if not responded within 2.5*keepalive
358 if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
359 on_fatal_error();
360 this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("is unresponsive; disconnecting"));
361 }
362 } else if (!this->flags_.remove) {
363 // Only send ping if we're not disconnecting
364 ESP_LOGVV(TAG, "Sending keepalive PING");
365 PingRequest req;
366 this->flags_.sent_ping = this->send_message(req);
367 if (!this->flags_.sent_ping) {
368 // If we can't send the ping request directly (tx_buffer full),
369 // schedule it at the front of the batch so it will be sent with priority
370 ESP_LOGW(TAG, "Buffer full, ping queued");
371 this->schedule_message_front_(nullptr, PingRequest::MESSAGE_TYPE, PingRequest::ESTIMATED_SIZE);
372 this->flags_.sent_ping = true; // Mark as sent to avoid scheduling multiple pings
373 }
374 }
375}
376
377void APIConnection::process_active_iterator_() {
378 // Caller ensures active_iterator_ != NONE
379 if (this->active_iterator_ == ActiveIterator::LIST_ENTITIES) {
380 if (this->iterator_storage_.list_entities.completed()) {
381 this->destroy_active_iterator_();
382 if (this->flags_.state_subscription) {
383 this->begin_iterator_(ActiveIterator::INITIAL_STATE);
384 } else {
385 this->finalize_iterator_sync_();
386 }
387 } else {
388 this->process_iterator_batch_(this->iterator_storage_.list_entities);
389 }
390 } else { // INITIAL_STATE
391 if (this->iterator_storage_.initial_state.completed()) {
392 this->destroy_active_iterator_();
393 this->finalize_iterator_sync_();
394 } else {
395 this->process_iterator_batch_(this->iterator_storage_.initial_state);
396 }
397 }
398}
399
400void APIConnection::finalize_iterator_sync_() {
401 // Flush any remaining batched messages immediately so clients
402 // receive completion responses (e.g. ListEntitiesDoneResponse)
403 // without waiting for the batch timer.
404 if (!this->deferred_batch_.empty()) {
405 this->process_batch_();
406 }
407 // Enable immediate sending for future state changes
408 this->flags_.should_try_send_immediately = true;
409 // Release excess memory from buffers that grew during initial sync
410 this->deferred_batch_.release_buffer();
411 this->helper_->release_buffers();
412}
413
414void APIConnection::process_iterator_batch_(ComponentIterator &iterator) {
415 // Budget by remaining batch capacity so a pass cannot overfill the batch;
416 // stops early on a refused send and resumes next loop pass
417 size_t batch_size = this->deferred_batch_.size();
418 if (batch_size < MAX_INITIAL_BATCH_SIZE)
419 iterator.try_advance(MAX_INITIAL_BATCH_SIZE - batch_size);
420
421 // Flush immediately once enough is queued (not guaranteed every pass);
422 // partial batches go out via the batch timer or finalize_iterator_sync_()
423 if (this->deferred_batch_.size() >= MAX_INITIAL_BATCH_SIZE) {
424 this->process_batch_();
425 }
426}
427
428bool APIConnection::send_disconnect_response_() {
429 // remote initiated disconnect_client
430 // don't close yet, we still need to send the disconnect response
431 // close will happen on next loop
432 this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("disconnected"));
433 this->flags_.next_close = true;
435 return this->send_message(resp);
436}
437void APIConnection::on_disconnect_response() {
438 // Don't close socket here, let APIServer::loop() do it
439 // so getpeername() still works for the disconnect trigger
440 this->flags_.remove = true;
441}
442
443uint16_t APIConnection::fill_and_encode_entity_state(EntityBase *entity, StateResponseProtoMessage &msg,
444 CalculateSizeFn size_fn, MessageEncodeFn encode_fn,
445 APIConnection *conn, uint32_t remaining_size) {
446 msg.key = entity->get_object_id_hash();
447#ifdef USE_DEVICES
448 msg.device_id = entity->get_device_id();
449#endif
450 return encode_to_buffer(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
451}
452
453uint16_t APIConnection::fill_and_encode_entity_info(EntityBase *entity, InfoResponseProtoMessage &msg,
454 CalculateSizeFn size_fn, MessageEncodeFn encode_fn,
455 APIConnection *conn, uint32_t remaining_size) {
456 // Set common fields that are shared by all entity types
457 msg.key = entity->get_object_id_hash();
458
459 if (entity->has_own_name()) {
460 msg.name = entity->get_name();
461 }
462
463 // Set common EntityBase properties
464#ifdef USE_ENTITY_ICON
465 char icon_buf[MAX_ICON_LENGTH];
466 msg.icon = StringRef(entity->get_icon_to(icon_buf));
467#endif
469 msg.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category());
470#ifdef USE_DEVICES
471 msg.device_id = entity->get_device_id();
472#endif
473 return encode_to_buffer_slow(size_fn(&msg), encode_fn, &msg, conn, remaining_size);
474}
475
476uint16_t APIConnection::fill_and_encode_entity_info_with_device_class(EntityBase *entity, InfoResponseProtoMessage &msg,
477 StringRef &device_class_field,
478 CalculateSizeFn size_fn,
479 MessageEncodeFn encode_fn, APIConnection *conn,
480 uint32_t remaining_size) {
481 char dc_buf[MAX_DEVICE_CLASS_LENGTH];
482 device_class_field = StringRef(entity->get_device_class_to(dc_buf));
483 return fill_and_encode_entity_info(entity, msg, size_fn, encode_fn, conn, remaining_size);
484}
485
486#ifdef USE_BINARY_SENSOR
487bool APIConnection::send_binary_sensor_state(binary_sensor::BinarySensor *binary_sensor) {
488 return this->send_message_smart_(binary_sensor, BinarySensorStateResponse::MESSAGE_TYPE,
489 BinarySensorStateResponse::ESTIMATED_SIZE);
490}
491
492uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
493 auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity);
495 resp.state = binary_sensor->state;
496 resp.missing_state = !binary_sensor->has_state();
497 return fill_and_encode_entity_state(binary_sensor, resp, conn, remaining_size);
498}
499
500uint16_t APIConnection::try_send_binary_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
501 auto *binary_sensor = static_cast<binary_sensor::BinarySensor *>(entity);
503 msg.is_status_binary_sensor = binary_sensor->is_status_binary_sensor();
504 return fill_and_encode_entity_info_with_device_class(binary_sensor, msg, msg.device_class, conn, remaining_size);
505}
506#endif
507
508#ifdef USE_COVER
509bool APIConnection::send_cover_state(cover::Cover *cover) {
510 return this->send_message_smart_(cover, CoverStateResponse::MESSAGE_TYPE, CoverStateResponse::ESTIMATED_SIZE);
511}
512uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
513 auto *cover = static_cast<cover::Cover *>(entity);
515 auto traits = cover->get_traits();
516 msg.position = cover->position;
517 if (traits.get_supports_tilt())
518 msg.tilt = cover->tilt;
519 msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation);
520 return fill_and_encode_entity_state(cover, msg, conn, remaining_size);
521}
522uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
523 auto *cover = static_cast<cover::Cover *>(entity);
525 auto traits = cover->get_traits();
526 msg.assumed_state = traits.get_is_assumed_state();
527 msg.supports_position = traits.get_supports_position();
528 msg.supports_tilt = traits.get_supports_tilt();
529 msg.supports_stop = traits.get_supports_stop();
530 return fill_and_encode_entity_info_with_device_class(cover, msg, msg.device_class, conn, remaining_size);
531}
532void APIConnection::on_cover_command_request(const CoverCommandRequest &msg) {
533 ENTITY_COMMAND_MAKE_CALL(cover::Cover, cover, cover)
534 if (msg.has_position)
535 call.set_position(msg.position);
536 if (msg.has_tilt)
537 call.set_tilt(msg.tilt);
538 if (msg.stop)
539 call.set_command_stop();
540 call.perform();
541}
542#endif
543
544#ifdef USE_FAN
545bool APIConnection::send_fan_state(fan::Fan *fan) {
546 return this->send_message_smart_(fan, FanStateResponse::MESSAGE_TYPE, FanStateResponse::ESTIMATED_SIZE);
547}
548uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
549 auto *fan = static_cast<fan::Fan *>(entity);
551 auto traits = fan->get_traits();
552 msg.state = fan->state;
553 if (traits.supports_oscillation())
554 msg.oscillating = fan->oscillating;
555 if (traits.supports_speed()) {
556 msg.speed_level = fan->speed;
557 }
558 if (traits.supports_direction())
559 msg.direction = static_cast<enums::FanDirection>(fan->direction);
560 if (traits.supports_preset_modes() && fan->has_preset_mode())
561 msg.preset_mode = fan->get_preset_mode();
562 return fill_and_encode_entity_state(fan, msg, conn, remaining_size);
563}
564uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
565 auto *fan = static_cast<fan::Fan *>(entity);
567 auto traits = fan->get_traits();
568 msg.supports_oscillation = traits.supports_oscillation();
569 msg.supports_speed = traits.supports_speed();
570 msg.supports_direction = traits.supports_direction();
571 msg.supported_speed_count = traits.supported_speed_count();
572 msg.supported_preset_modes = &traits.supported_preset_modes();
573 return fill_and_encode_entity_info(fan, msg, conn, remaining_size);
574}
575void APIConnection::on_fan_command_request(const FanCommandRequest &msg) {
576 ENTITY_COMMAND_MAKE_CALL(fan::Fan, fan, fan)
577 if (msg.has_state)
578 call.set_state(msg.state);
579 if (msg.has_oscillating)
580 call.set_oscillating(msg.oscillating);
581 if (msg.has_speed_level) {
582 // Prefer level
583 call.set_speed(msg.speed_level);
584 }
585 if (msg.has_direction)
586 call.set_direction(static_cast<fan::FanDirection>(msg.direction));
587 if (msg.has_preset_mode)
588 call.set_preset_mode(msg.preset_mode.c_str(), msg.preset_mode.size());
589 call.perform();
590}
591#endif
592
593#ifdef USE_LIGHT
594bool APIConnection::send_light_state(light::LightState *light) {
595 return this->send_message_smart_(light, LightStateResponse::MESSAGE_TYPE, LightStateResponse::ESTIMATED_SIZE);
596}
597uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
598 auto *light = static_cast<light::LightState *>(entity);
600 auto values = light->remote_values;
601 auto color_mode = values.get_color_mode();
602 resp.state = values.is_on();
603 resp.color_mode = static_cast<enums::ColorMode>(color_mode);
604 resp.brightness = values.get_brightness();
605 resp.color_brightness = values.get_color_brightness();
606 resp.red = values.get_red();
607 resp.green = values.get_green();
608 resp.blue = values.get_blue();
609 resp.white = values.get_white();
610 resp.color_temperature = values.get_color_temperature();
611 resp.cold_white = values.get_cold_white();
612 resp.warm_white = values.get_warm_white();
613 if (light->supports_effects()) {
614 resp.effect = light->get_effect_name();
615 }
616 return fill_and_encode_entity_state(light, resp, conn, remaining_size);
617}
618uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
619 auto *light = static_cast<light::LightState *>(entity);
621 auto traits = light->get_traits();
622 auto supported_modes = traits.get_supported_color_modes();
623 // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
624 msg.supported_color_modes = &supported_modes;
625 if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
626 traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
627 msg.min_mireds = traits.get_min_mireds();
628 msg.max_mireds = traits.get_max_mireds();
629 }
630 FixedVector<const char *> effects_list;
631 if (light->supports_effects()) {
632 auto &light_effects = light->get_effects();
633 effects_list.init(light_effects.size() + 1);
634 effects_list.push_back("None");
635 for (auto *effect : light_effects) {
636 // c_str() is safe as effect names are null-terminated strings from codegen
637 effects_list.push_back(effect->get_name().c_str());
638 }
639 }
640 msg.effects = &effects_list;
641 return fill_and_encode_entity_info(light, msg, conn, remaining_size);
642}
643void APIConnection::on_light_command_request(const LightCommandRequest &msg) {
644 ENTITY_COMMAND_MAKE_CALL(light::LightState, light, light)
645 if (msg.has_state)
646 call.set_state(msg.state);
647 if (msg.has_brightness)
648 call.set_brightness(msg.brightness);
649 if (msg.has_color_mode)
650 call.set_color_mode(static_cast<light::ColorMode>(msg.color_mode));
651 if (msg.has_color_brightness)
652 call.set_color_brightness(msg.color_brightness);
653 if (msg.has_rgb) {
654 call.set_red(msg.red);
655 call.set_green(msg.green);
656 call.set_blue(msg.blue);
657 }
658 if (msg.has_white)
659 call.set_white(msg.white);
660 if (msg.has_color_temperature)
661 call.set_color_temperature(msg.color_temperature);
662 if (msg.has_cold_white)
663 call.set_cold_white(msg.cold_white);
664 if (msg.has_warm_white)
665 call.set_warm_white(msg.warm_white);
666 if (msg.has_transition_length)
667 call.set_transition_length(msg.transition_length);
668 if (msg.has_flash_length)
669 call.set_flash_length(msg.flash_length);
670 if (msg.has_effect)
671 call.set_effect(msg.effect.c_str(), msg.effect.size());
672 call.perform();
673}
674#endif
675
676#ifdef USE_SENSOR
677bool APIConnection::send_sensor_state(sensor::Sensor *sensor) {
678 return this->send_message_smart_(sensor, SensorStateResponse::MESSAGE_TYPE, SensorStateResponse::ESTIMATED_SIZE);
679}
680
681uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
682 auto *sensor = static_cast<sensor::Sensor *>(entity);
684 resp.state = sensor->state;
685 resp.missing_state = !sensor->has_state();
686 return fill_and_encode_entity_state(sensor, resp, conn, remaining_size);
687}
688
689uint16_t APIConnection::try_send_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
690 auto *sensor = static_cast<sensor::Sensor *>(entity);
692 msg.unit_of_measurement = sensor->get_unit_of_measurement_ref();
693 msg.accuracy_decimals = sensor->get_accuracy_decimals();
694 msg.force_update = sensor->get_force_update();
695 msg.state_class = static_cast<enums::SensorStateClass>(sensor->get_state_class());
696 return fill_and_encode_entity_info_with_device_class(sensor, msg, msg.device_class, conn, remaining_size);
697}
698#endif
699
700#ifdef USE_SWITCH
701bool APIConnection::send_switch_state(switch_::Switch *a_switch) {
702 return this->send_message_smart_(a_switch, SwitchStateResponse::MESSAGE_TYPE, SwitchStateResponse::ESTIMATED_SIZE);
703}
704
705uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
706 auto *a_switch = static_cast<switch_::Switch *>(entity);
708 resp.state = a_switch->state;
709 return fill_and_encode_entity_state(a_switch, resp, conn, remaining_size);
710}
711
712uint16_t APIConnection::try_send_switch_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
713 auto *a_switch = static_cast<switch_::Switch *>(entity);
715 msg.assumed_state = a_switch->assumed_state();
716 return fill_and_encode_entity_info_with_device_class(a_switch, msg, msg.device_class, conn, remaining_size);
717}
718void APIConnection::on_switch_command_request(const SwitchCommandRequest &msg) {
719 ENTITY_COMMAND_GET(switch_::Switch, a_switch, switch)
720
721 if (msg.state) {
722 a_switch->turn_on();
723 } else {
724 a_switch->turn_off();
725 }
726}
727#endif
728
729#ifdef USE_TEXT_SENSOR
730bool APIConnection::send_text_sensor_state(text_sensor::TextSensor *text_sensor) {
731 return this->send_message_smart_(text_sensor, TextSensorStateResponse::MESSAGE_TYPE,
732 TextSensorStateResponse::ESTIMATED_SIZE);
733}
734
735uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
736 auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
738 resp.state = StringRef(text_sensor->state);
739 resp.missing_state = !text_sensor->has_state();
740 return fill_and_encode_entity_state(text_sensor, resp, conn, remaining_size);
741}
742uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
743 auto *text_sensor = static_cast<text_sensor::TextSensor *>(entity);
745 return fill_and_encode_entity_info_with_device_class(text_sensor, msg, msg.device_class, conn, remaining_size);
746}
747#endif
748
749#ifdef USE_CLIMATE
750bool APIConnection::send_climate_state(climate::Climate *climate) {
751 return this->send_message_smart_(climate, ClimateStateResponse::MESSAGE_TYPE, ClimateStateResponse::ESTIMATED_SIZE);
752}
753uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
754 auto *climate = static_cast<climate::Climate *>(entity);
756 auto traits = climate->get_traits();
757 resp.mode = static_cast<enums::ClimateMode>(climate->mode);
758 resp.action = static_cast<enums::ClimateAction>(climate->action);
759 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE))
760 resp.current_temperature = climate->current_temperature;
761 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
763 resp.target_temperature_low = climate->target_temperature_low;
764 resp.target_temperature_high = climate->target_temperature_high;
765 } else {
766 resp.target_temperature = climate->target_temperature;
767 }
768 if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
769 resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
770 if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) {
771 resp.custom_fan_mode = climate->get_custom_fan_mode();
772 }
773 if (traits.get_supports_presets() && climate->preset.has_value()) {
774 resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
775 }
776 if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) {
777 resp.custom_preset = climate->get_custom_preset();
778 }
779 if (traits.get_supports_swing_modes())
780 resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
781 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY))
782 resp.current_humidity = climate->current_humidity;
783 if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY))
784 resp.target_humidity = climate->target_humidity;
785 return fill_and_encode_entity_state(climate, resp, conn, remaining_size);
786}
787uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
788 auto *climate = static_cast<climate::Climate *>(entity);
790 auto traits = climate->get_traits();
791 // Flags set for backward compatibility, deprecated in 2025.11.0
794 msg.supports_two_point_target_temperature = traits.has_feature_flags(
797 msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
798 // Current feature flags and other supported parameters
799 msg.feature_flags = traits.get_feature_flags();
800 msg.temperature_unit = static_cast<enums::TemperatureUnit>(traits.get_temperature_unit());
801 msg.supported_modes = &traits.get_supported_modes();
802 msg.visual_min_temperature = traits.get_visual_min_temperature();
803 msg.visual_max_temperature = traits.get_visual_max_temperature();
804 msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
805 msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
806 msg.visual_min_humidity = traits.get_visual_min_humidity();
807 msg.visual_max_humidity = traits.get_visual_max_humidity();
808 msg.supported_fan_modes = &traits.get_supported_fan_modes();
809 msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes();
810 msg.supported_presets = &traits.get_supported_presets();
811 msg.supported_custom_presets = &traits.get_supported_custom_presets();
812 msg.supported_swing_modes = &traits.get_supported_swing_modes();
813 return fill_and_encode_entity_info(climate, msg, conn, remaining_size);
814}
815void APIConnection::on_climate_command_request(const ClimateCommandRequest &msg) {
816 ENTITY_COMMAND_MAKE_CALL(climate::Climate, climate, climate)
817 if (msg.has_mode)
818 call.set_mode(static_cast<climate::ClimateMode>(msg.mode));
820 call.set_target_temperature(msg.target_temperature);
822 call.set_target_temperature_low(msg.target_temperature_low);
824 call.set_target_temperature_high(msg.target_temperature_high);
825 if (msg.has_target_humidity)
826 call.set_target_humidity(msg.target_humidity);
827 if (msg.has_fan_mode)
828 call.set_fan_mode(static_cast<climate::ClimateFanMode>(msg.fan_mode));
829 if (msg.has_custom_fan_mode)
830 call.set_fan_mode(msg.custom_fan_mode.c_str(), msg.custom_fan_mode.size());
831 if (msg.has_preset)
832 call.set_preset(static_cast<climate::ClimatePreset>(msg.preset));
833 if (msg.has_custom_preset)
834 call.set_preset(msg.custom_preset.c_str(), msg.custom_preset.size());
835 if (msg.has_swing_mode)
836 call.set_swing_mode(static_cast<climate::ClimateSwingMode>(msg.swing_mode));
837 call.perform();
838}
839#endif
840
841#ifdef USE_NUMBER
842bool APIConnection::send_number_state(number::Number *number) {
843 return this->send_message_smart_(number, NumberStateResponse::MESSAGE_TYPE, NumberStateResponse::ESTIMATED_SIZE);
844}
845
846uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
847 auto *number = static_cast<number::Number *>(entity);
849 resp.state = number->state;
850 resp.missing_state = !number->has_state();
851 return fill_and_encode_entity_state(number, resp, conn, remaining_size);
852}
853
854uint16_t APIConnection::try_send_number_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
855 auto *number = static_cast<number::Number *>(entity);
857 msg.unit_of_measurement = number->get_unit_of_measurement_ref();
858 msg.mode = static_cast<enums::NumberMode>(number->traits.get_mode());
859 msg.min_value = number->traits.get_min_value();
860 msg.max_value = number->traits.get_max_value();
861 msg.step = number->traits.get_step();
862 return fill_and_encode_entity_info_with_device_class(number, msg, msg.device_class, conn, remaining_size);
863}
864void APIConnection::on_number_command_request(const NumberCommandRequest &msg) {
865 ENTITY_COMMAND_MAKE_CALL(number::Number, number, number)
866 call.set_value(msg.state);
867 call.perform();
868}
869#endif
870
871#ifdef USE_DATETIME_DATE
872bool APIConnection::send_date_state(datetime::DateEntity *date) {
873 return this->send_message_smart_(date, DateStateResponse::MESSAGE_TYPE, DateStateResponse::ESTIMATED_SIZE);
874}
875uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
876 auto *date = static_cast<datetime::DateEntity *>(entity);
878 resp.missing_state = !date->has_state();
879 resp.year = date->year;
880 resp.month = date->month;
881 resp.day = date->day;
882 return fill_and_encode_entity_state(date, resp, conn, remaining_size);
883}
884uint16_t APIConnection::try_send_date_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
885 auto *date = static_cast<datetime::DateEntity *>(entity);
887 return fill_and_encode_entity_info(date, msg, conn, remaining_size);
888}
889void APIConnection::on_date_command_request(const DateCommandRequest &msg) {
890 ENTITY_COMMAND_MAKE_CALL(datetime::DateEntity, date, date)
891 call.set_date(msg.year, msg.month, msg.day);
892 call.perform();
893}
894#endif
895
896#ifdef USE_DATETIME_TIME
897bool APIConnection::send_time_state(datetime::TimeEntity *time) {
898 return this->send_message_smart_(time, TimeStateResponse::MESSAGE_TYPE, TimeStateResponse::ESTIMATED_SIZE);
899}
900uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
901 auto *time = static_cast<datetime::TimeEntity *>(entity);
903 resp.missing_state = !time->has_state();
904 resp.hour = time->hour;
905 resp.minute = time->minute;
906 resp.second = time->second;
907 return fill_and_encode_entity_state(time, resp, conn, remaining_size);
908}
909uint16_t APIConnection::try_send_time_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
910 auto *time = static_cast<datetime::TimeEntity *>(entity);
912 return fill_and_encode_entity_info(time, msg, conn, remaining_size);
913}
914void APIConnection::on_time_command_request(const TimeCommandRequest &msg) {
915 ENTITY_COMMAND_MAKE_CALL(datetime::TimeEntity, time, time)
916 call.set_time(msg.hour, msg.minute, msg.second);
917 call.perform();
918}
919#endif
920
921#ifdef USE_DATETIME_DATETIME
922bool APIConnection::send_datetime_state(datetime::DateTimeEntity *datetime) {
923 return this->send_message_smart_(datetime, DateTimeStateResponse::MESSAGE_TYPE,
924 DateTimeStateResponse::ESTIMATED_SIZE);
925}
926uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
927 auto *datetime = static_cast<datetime::DateTimeEntity *>(entity);
929 resp.missing_state = !datetime->has_state();
930 if (datetime->has_state()) {
931 ESPTime state = datetime->state_as_esptime();
932 resp.epoch_seconds = state.timestamp;
933 }
934 return fill_and_encode_entity_state(datetime, resp, conn, remaining_size);
935}
936uint16_t APIConnection::try_send_datetime_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
937 auto *datetime = static_cast<datetime::DateTimeEntity *>(entity);
939 return fill_and_encode_entity_info(datetime, msg, conn, remaining_size);
940}
941void APIConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) {
942 ENTITY_COMMAND_MAKE_CALL(datetime::DateTimeEntity, datetime, datetime)
943 call.set_datetime(msg.epoch_seconds);
944 call.perform();
945}
946#endif
947
948#ifdef USE_TEXT
949bool APIConnection::send_text_state(text::Text *text) {
950 return this->send_message_smart_(text, TextStateResponse::MESSAGE_TYPE, TextStateResponse::ESTIMATED_SIZE);
951}
952
953uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
954 auto *text = static_cast<text::Text *>(entity);
956 resp.state = StringRef(text->state);
957 resp.missing_state = !text->has_state();
958 return fill_and_encode_entity_state(text, resp, conn, remaining_size);
959}
960
961uint16_t APIConnection::try_send_text_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
962 auto *text = static_cast<text::Text *>(entity);
964 msg.mode = static_cast<enums::TextMode>(text->traits.get_mode());
965 msg.min_length = text->traits.get_min_length();
966 msg.max_length = text->traits.get_max_length();
967 msg.pattern = text->traits.get_pattern_ref();
968 return fill_and_encode_entity_info(text, msg, conn, remaining_size);
969}
970void APIConnection::on_text_command_request(const TextCommandRequest &msg) {
971 ENTITY_COMMAND_MAKE_CALL(text::Text, text, text)
972 call.set_value(msg.state.c_str(), msg.state.size());
973 call.perform();
974}
975#endif
976
977#ifdef USE_SELECT
978bool APIConnection::send_select_state(select::Select *select) {
979 return this->send_message_smart_(select, SelectStateResponse::MESSAGE_TYPE, SelectStateResponse::ESTIMATED_SIZE);
980}
981
982uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
983 auto *select = static_cast<select::Select *>(entity);
985 resp.state = select->current_option();
986 resp.missing_state = !select->has_state();
987 return fill_and_encode_entity_state(select, resp, conn, remaining_size);
988}
989
990uint16_t APIConnection::try_send_select_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
991 auto *select = static_cast<select::Select *>(entity);
993 msg.options = &select->traits.get_options();
994 return fill_and_encode_entity_info(select, msg, conn, remaining_size);
995}
996void APIConnection::on_select_command_request(const SelectCommandRequest &msg) {
997 ENTITY_COMMAND_MAKE_CALL(select::Select, select, select)
998 call.set_option(msg.state.c_str(), msg.state.size());
999 call.perform();
1000}
1001#endif
1002
1003#ifdef USE_BUTTON
1004uint16_t APIConnection::try_send_button_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1005 auto *button = static_cast<button::Button *>(entity);
1007 return fill_and_encode_entity_info_with_device_class(button, msg, msg.device_class, conn, remaining_size);
1008}
1010 ENTITY_COMMAND_GET(button::Button, button, button)
1011 button->press();
1012}
1013#endif
1014
1015#ifdef USE_LOCK
1016bool APIConnection::send_lock_state(lock::Lock *a_lock) {
1017 return this->send_message_smart_(a_lock, LockStateResponse::MESSAGE_TYPE, LockStateResponse::ESTIMATED_SIZE);
1018}
1019
1020uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1021 auto *a_lock = static_cast<lock::Lock *>(entity);
1022 LockStateResponse resp;
1023 resp.state = static_cast<enums::LockState>(a_lock->state);
1024 return fill_and_encode_entity_state(a_lock, resp, conn, remaining_size);
1025}
1026
1027uint16_t APIConnection::try_send_lock_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1028 auto *a_lock = static_cast<lock::Lock *>(entity);
1030 msg.assumed_state = a_lock->traits.get_assumed_state();
1031 msg.supports_open = a_lock->traits.get_supports_open();
1032 msg.requires_code = a_lock->traits.get_requires_code();
1033 return fill_and_encode_entity_info(a_lock, msg, conn, remaining_size);
1034}
1035void APIConnection::on_lock_command_request(const LockCommandRequest &msg) {
1036 ENTITY_COMMAND_GET(lock::Lock, a_lock, lock)
1037
1038 switch (msg.command) {
1039 case enums::LOCK_UNLOCK:
1040 a_lock->unlock();
1041 break;
1042 case enums::LOCK_LOCK:
1043 a_lock->lock();
1044 break;
1045 case enums::LOCK_OPEN:
1046 a_lock->open();
1047 break;
1048 }
1049}
1050#endif
1051
1052#ifdef USE_VALVE
1053bool APIConnection::send_valve_state(valve::Valve *valve) {
1054 return this->send_message_smart_(valve, ValveStateResponse::MESSAGE_TYPE, ValveStateResponse::ESTIMATED_SIZE);
1055}
1056uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1057 auto *valve = static_cast<valve::Valve *>(entity);
1058 ValveStateResponse resp;
1059 resp.position = valve->position;
1060 resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation);
1061 return fill_and_encode_entity_state(valve, resp, conn, remaining_size);
1062}
1063uint16_t APIConnection::try_send_valve_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1064 auto *valve = static_cast<valve::Valve *>(entity);
1066 auto traits = valve->get_traits();
1067 msg.assumed_state = traits.get_is_assumed_state();
1068 msg.supports_position = traits.get_supports_position();
1069 msg.supports_stop = traits.get_supports_stop();
1070 return fill_and_encode_entity_info_with_device_class(valve, msg, msg.device_class, conn, remaining_size);
1071}
1072void APIConnection::on_valve_command_request(const ValveCommandRequest &msg) {
1073 ENTITY_COMMAND_MAKE_CALL(valve::Valve, valve, valve)
1074 if (msg.has_position)
1075 call.set_position(msg.position);
1076 if (msg.stop)
1077 call.set_command_stop();
1078 call.perform();
1079}
1080#endif
1081
1082#ifdef USE_MEDIA_PLAYER
1083bool APIConnection::send_media_player_state(media_player::MediaPlayer *media_player) {
1084 return this->send_message_smart_(media_player, MediaPlayerStateResponse::MESSAGE_TYPE,
1085 MediaPlayerStateResponse::ESTIMATED_SIZE);
1086}
1087uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1088 auto *media_player = static_cast<media_player::MediaPlayer *>(entity);
1092 : media_player->state;
1093 resp.state = static_cast<enums::MediaPlayerState>(report_state);
1094 resp.volume = media_player->volume;
1095 resp.muted = media_player->is_muted();
1096 return fill_and_encode_entity_state(media_player, resp, conn, remaining_size);
1097}
1098uint16_t APIConnection::try_send_media_player_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1099 auto *media_player = static_cast<media_player::MediaPlayer *>(entity);
1101 auto traits = media_player->get_traits();
1102 msg.feature_flags = traits.get_feature_flags();
1103 for (auto &supported_format : traits.get_supported_formats()) {
1104 msg.supported_formats.emplace_back();
1105 auto &media_format = msg.supported_formats.back();
1106 media_format.format = StringRef(supported_format.format);
1107 media_format.sample_rate = supported_format.sample_rate;
1108 media_format.num_channels = supported_format.num_channels;
1109 media_format.purpose = static_cast<enums::MediaPlayerFormatPurpose>(supported_format.purpose);
1110 media_format.sample_bytes = supported_format.sample_bytes;
1111 }
1112 return fill_and_encode_entity_info(media_player, msg, conn, remaining_size);
1113}
1114void APIConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) {
1115 ENTITY_COMMAND_MAKE_CALL(media_player::MediaPlayer, media_player, media_player)
1116 if (msg.has_command) {
1117 call.set_command(static_cast<media_player::MediaPlayerCommand>(msg.command));
1118 }
1119 if (msg.has_volume) {
1120 call.set_volume(msg.volume);
1121 }
1122 if (msg.has_media_url) {
1123 call.set_media_url(msg.media_url);
1124 }
1125 if (msg.has_announcement) {
1126 call.set_announcement(msg.announcement);
1127 }
1128 call.perform();
1129}
1130#endif
1131
1132#ifdef USE_CAMERA
1133void APIConnection::try_send_camera_image_() {
1134 if (!this->image_reader_)
1135 return;
1136
1137 const auto *cam = camera::Camera::instance();
1138 // Send as many chunks as possible without blocking
1139 while (this->image_reader_->available()) {
1140 if (!this->helper_->can_write_without_blocking())
1141 return;
1142
1143 uint32_t to_send = std::min((size_t) MAX_BATCH_PACKET_SIZE, this->image_reader_->available());
1144 bool done = this->image_reader_->available() == to_send;
1145
1147 msg.key = cam->get_object_id_hash();
1148 msg.set_data(this->image_reader_->peek_data_buffer(), to_send);
1149 msg.done = done;
1150#ifdef USE_DEVICES
1151 msg.device_id = cam->get_device_id();
1152#endif
1153
1154 if (!this->send_message(msg)) {
1155 return; // Send failed, try again later
1156 }
1157 this->image_reader_->consume_data(to_send);
1158 if (done) {
1159 this->image_reader_->return_image();
1160 return;
1161 }
1162 }
1163}
1164void APIConnection::set_camera_state(std::shared_ptr<camera::CameraImage> image) {
1165 if (!this->flags_.state_subscription)
1166 return;
1167 if (this->image_reader_ && this->image_reader_->available())
1168 return;
1169 if (!image->was_requested_by(esphome::camera::API_REQUESTER) && !image->was_requested_by(esphome::camera::IDLE))
1170 return;
1171 if (!this->image_reader_) {
1172 // Created on the first image this connection will send, so connections
1173 // that never receive one never pay for a reader. Only a registered
1174 // camera's listener can reach this, so instance() is non-null here.
1175 this->image_reader_ = std::unique_ptr<camera::CameraImageReader>{camera::Camera::instance()->create_image_reader()};
1176 }
1177 this->image_reader_->set_image(std::move(image));
1178 // Try to send immediately to reduce latency
1179 this->try_send_camera_image_();
1180}
1181uint16_t APIConnection::try_send_camera_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1182 auto *camera = static_cast<camera::Camera *>(entity);
1184 return fill_and_encode_entity_info(camera, msg, conn, remaining_size);
1185}
1186void APIConnection::on_camera_image_request(const CameraImageRequest &msg) {
1187 if (camera::Camera::instance() == nullptr)
1188 return;
1189
1190 if (msg.single)
1192 if (msg.stream) {
1194
1195 App.scheduler.set_timeout(this->parent_, "api_camera_stop_stream", CAMERA_STOP_STREAM,
1197 }
1198}
1199#endif
1200
1201#ifdef USE_HOMEASSISTANT_TIME
1202void APIConnection::on_get_time_response(const GetTimeResponse &value) {
1205#if defined(USE_HOMEASSISTANT_TIMEZONE) && defined(USE_TIME_TIMEZONE)
1206 // Apply only if the sender provided pre-parsed timezone data (Home Assistant 2026.3.0
1207 // and newer); field presence distinguishes a genuine all-zero UTC timezone from an
1208 // absent field. Older clients send only the deprecated timezone string, which is no
1209 // longer decoded; for them the device keeps its codegen-configured timezone.
1210 if (value.has_parsed_timezone) {
1211 const auto &pt = value.parsed_timezone;
1213 tz.std_offset_seconds = pt.std_offset_seconds;
1214 tz.dst_offset_seconds = pt.dst_offset_seconds;
1215 tz.dst_start.time_seconds = pt.dst_start.time_seconds;
1216 tz.dst_start.day = static_cast<uint16_t>(pt.dst_start.day);
1217 tz.dst_start.type = static_cast<time::DSTRuleType>(pt.dst_start.type);
1218 tz.dst_start.month = static_cast<uint8_t>(pt.dst_start.month);
1219 tz.dst_start.week = static_cast<uint8_t>(pt.dst_start.week);
1220 tz.dst_start.day_of_week = static_cast<uint8_t>(pt.dst_start.day_of_week);
1221 tz.dst_end.time_seconds = pt.dst_end.time_seconds;
1222 tz.dst_end.day = static_cast<uint16_t>(pt.dst_end.day);
1223 tz.dst_end.type = static_cast<time::DSTRuleType>(pt.dst_end.type);
1224 tz.dst_end.month = static_cast<uint8_t>(pt.dst_end.month);
1225 tz.dst_end.week = static_cast<uint8_t>(pt.dst_end.week);
1226 tz.dst_end.day_of_week = static_cast<uint8_t>(pt.dst_end.day_of_week);
1228 }
1229#endif
1230 }
1231}
1232#endif
1233
1234#ifdef USE_BLUETOOTH_PROXY
1235void APIConnection::on_subscribe_bluetooth_le_advertisements_request(
1238}
1239void APIConnection::on_unsubscribe_bluetooth_le_advertisements_request() {
1241}
1242#ifdef USE_BLUETOOTH_PROXY_CONNECTIONS
1243void APIConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) {
1245}
1246void APIConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) {
1248}
1249void APIConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) {
1251}
1252void APIConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) {
1254}
1255void APIConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) {
1257}
1258void APIConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) {
1260}
1261
1262void APIConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) {
1264}
1265
1266bool APIConnection::send_subscribe_bluetooth_connections_free_response_() {
1268 return true;
1269}
1270void APIConnection::on_subscribe_bluetooth_connections_free_request() {
1271 if (!this->send_subscribe_bluetooth_connections_free_response_()) {
1272 this->on_fatal_error();
1273 }
1274}
1275
1276void APIConnection::on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) {
1278}
1279#endif
1280
1281void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) {
1283 msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE);
1284}
1285#endif
1286
1287#ifdef USE_VOICE_ASSISTANT
1288bool APIConnection::check_voice_assistant_api_connection_() const {
1289 return voice_assistant::global_voice_assistant != nullptr &&
1291}
1292
1293void APIConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) {
1296 }
1297}
1298void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) {
1299 if (!this->check_voice_assistant_api_connection_()) {
1300 return;
1301 }
1302
1303 if (msg.error) {
1305 return;
1306 }
1307 if (msg.port == 0) {
1308 // Use API Audio
1310 } else {
1311 struct sockaddr_storage storage;
1312 socklen_t len = sizeof(storage);
1313 this->helper_->getpeername((struct sockaddr *) &storage, &len);
1315 }
1316};
1317void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) {
1318 if (this->check_voice_assistant_api_connection_()) {
1320 }
1321}
1322void APIConnection::on_voice_assistant_audio(const VoiceAssistantAudio &msg) {
1323 if (this->check_voice_assistant_api_connection_()) {
1325 }
1326};
1327void APIConnection::on_voice_assistant_timer_event_response(const VoiceAssistantTimerEventResponse &msg) {
1328 if (this->check_voice_assistant_api_connection_()) {
1330 }
1331};
1332
1333void APIConnection::on_voice_assistant_announce_request(const VoiceAssistantAnnounceRequest &msg) {
1334 if (this->check_voice_assistant_api_connection_()) {
1336 }
1337}
1338
1339bool APIConnection::send_voice_assistant_get_configuration_response_(
1340 const VoiceAssistantConfigurationRequest & /*msg*/) {
1342 if (!this->check_voice_assistant_api_connection_()) {
1343 // send_message encodes synchronously, so this stack local outlives the encode
1344 const std::vector<std::string> empty_wake_words;
1345 resp.active_wake_words = &empty_wake_words;
1346 return this->send_message(resp);
1347 }
1348
1350 for (auto &wake_word : config.available_wake_words) {
1351 resp.available_wake_words.emplace_back();
1352 auto &resp_wake_word = resp.available_wake_words.back();
1353 resp_wake_word.id = StringRef(wake_word.id);
1354 resp_wake_word.wake_word = StringRef(wake_word.wake_word);
1355 for (const auto &lang : wake_word.trained_languages) {
1356 resp_wake_word.trained_languages.push_back(lang);
1357 }
1358 }
1359
1360 resp.active_wake_words = &config.active_wake_words;
1361 resp.max_active_wake_words = config.max_active_wake_words;
1362 return this->send_message(resp);
1363}
1364void APIConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) {
1365 if (!this->send_voice_assistant_get_configuration_response_(msg)) {
1366 this->on_fatal_error();
1367 }
1368}
1369
1370void APIConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) {
1371 if (this->check_voice_assistant_api_connection_()) {
1373 }
1374}
1375#endif
1376
1377#ifdef USE_ZWAVE_PROXY
1378void APIConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) {
1380}
1381
1382void APIConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) {
1384 resp.type = msg.type;
1386 if (!this->send_message(resp)) {
1387 API_LOG_MSG_DROPPED(TAG, "Z-Wave proxy response");
1388 }
1389}
1390#endif
1391
1392#ifdef USE_ALARM_CONTROL_PANEL
1393bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) {
1394 return this->send_message_smart_(a_alarm_control_panel, AlarmControlPanelStateResponse::MESSAGE_TYPE,
1395 AlarmControlPanelStateResponse::ESTIMATED_SIZE);
1396}
1397uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, APIConnection *conn,
1398 uint32_t remaining_size) {
1399 auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity);
1401 resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state());
1402 return fill_and_encode_entity_state(a_alarm_control_panel, resp, conn, remaining_size);
1403}
1404uint16_t APIConnection::try_send_alarm_control_panel_info(EntityBase *entity, APIConnection *conn,
1405 uint32_t remaining_size) {
1406 auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity);
1408 msg.supported_features = a_alarm_control_panel->get_supported_features();
1409 msg.requires_code = a_alarm_control_panel->get_requires_code();
1410 msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm();
1411 return fill_and_encode_entity_info(a_alarm_control_panel, msg, conn, remaining_size);
1412}
1413void APIConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) {
1414 ENTITY_COMMAND_MAKE_CALL(alarm_control_panel::AlarmControlPanel, a_alarm_control_panel, alarm_control_panel)
1415 switch (msg.command) {
1416 case enums::ALARM_CONTROL_PANEL_DISARM:
1417 call.disarm();
1418 break;
1419 case enums::ALARM_CONTROL_PANEL_ARM_AWAY:
1420 call.arm_away();
1421 break;
1422 case enums::ALARM_CONTROL_PANEL_ARM_HOME:
1423 call.arm_home();
1424 break;
1425 case enums::ALARM_CONTROL_PANEL_ARM_NIGHT:
1426 call.arm_night();
1427 break;
1428 case enums::ALARM_CONTROL_PANEL_ARM_VACATION:
1429 call.arm_vacation();
1430 break;
1431 case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS:
1432 call.arm_custom_bypass();
1433 break;
1434 case enums::ALARM_CONTROL_PANEL_TRIGGER:
1435 call.pending();
1436 break;
1437 }
1438 call.set_code(msg.code.c_str(), msg.code.size());
1439 call.perform();
1440}
1441#endif
1442
1443#ifdef USE_WATER_HEATER
1444bool APIConnection::send_water_heater_state(water_heater::WaterHeater *water_heater) {
1445 return this->send_message_smart_(water_heater, WaterHeaterStateResponse::MESSAGE_TYPE,
1446 WaterHeaterStateResponse::ESTIMATED_SIZE);
1447}
1448uint16_t APIConnection::try_send_water_heater_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1449 auto *wh = static_cast<water_heater::WaterHeater *>(entity);
1451 resp.mode = static_cast<enums::WaterHeaterMode>(wh->get_mode());
1452 resp.current_temperature = wh->get_current_temperature();
1453 resp.target_temperature = wh->get_target_temperature();
1454 resp.target_temperature_low = wh->get_target_temperature_low();
1455 resp.target_temperature_high = wh->get_target_temperature_high();
1456 resp.state = wh->get_state();
1457
1458 return fill_and_encode_entity_state(wh, resp, conn, remaining_size);
1459}
1460uint16_t APIConnection::try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1461 auto *wh = static_cast<water_heater::WaterHeater *>(entity);
1463 auto traits = wh->get_traits();
1464 msg.min_temperature = traits.get_min_temperature();
1465 msg.max_temperature = traits.get_max_temperature();
1466 msg.target_temperature_step = traits.get_target_temperature_step();
1467 msg.supported_modes = &traits.get_supported_modes();
1468 msg.supported_features = traits.get_feature_flags();
1469 msg.temperature_unit = static_cast<enums::TemperatureUnit>(traits.get_temperature_unit());
1470 return fill_and_encode_entity_info(wh, msg, conn, remaining_size);
1471}
1472
1473void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequest &msg) {
1474 ENTITY_COMMAND_MAKE_CALL(water_heater::WaterHeater, water_heater, water_heater)
1475 if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_MODE)
1476 call.set_mode(static_cast<water_heater::WaterHeaterMode>(msg.mode));
1477 if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE)
1478 call.set_target_temperature(msg.target_temperature);
1479 if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW)
1480 call.set_target_temperature_low(msg.target_temperature_low);
1481 if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH)
1482 call.set_target_temperature_high(msg.target_temperature_high);
1483 if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) ||
1484 (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
1485 call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0);
1486 }
1487 if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_ON_STATE) ||
1488 (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) {
1489 call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0);
1490 }
1491 call.perform();
1492}
1493#endif
1494
1495#ifdef USE_EVENT
1496// Event is a special case - unlike other entities with simple state fields,
1497// events store their state in a member accessed via obj->get_last_event_type()
1498void APIConnection::send_event(event::Event *event) {
1499 this->send_message_smart_(event, EventResponse::MESSAGE_TYPE, EventResponse::ESTIMATED_SIZE,
1500 event->get_last_event_type_index());
1501}
1502uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
1503 uint32_t remaining_size) {
1504 EventResponse resp;
1505 resp.event_type = event_type;
1506 return fill_and_encode_entity_state(event, resp, conn, remaining_size);
1507}
1508
1509uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1510 auto *event = static_cast<event::Event *>(entity);
1512 msg.event_types = &event->get_event_types();
1513 return fill_and_encode_entity_info_with_device_class(event, msg, msg.device_class, conn, remaining_size);
1514}
1515#endif
1516
1517#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
1518void APIConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) {
1519 // Dispatch by key: infrared entities are checked first, then radio frequency entities.
1520 // The key is unique across all entity instances on a device, so at most one lookup will succeed.
1521#ifdef USE_INFRARED
1522 ENTITY_COMMAND_LOOKUP(infrared::Infrared, infrared, infrared);
1523 if (infrared != nullptr) {
1524 auto call = infrared->make_call();
1525 call.set_carrier_frequency(msg.carrier_frequency);
1526 call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
1527 call.set_repeat_count(msg.repeat_count);
1528 call.perform();
1529 return;
1530 }
1531#endif
1532#ifdef USE_RADIO_FREQUENCY
1533 ENTITY_COMMAND_LOOKUP(radio_frequency::RadioFrequency, radio_frequency, radio_frequency);
1534 if (radio_frequency != nullptr) {
1535 auto call = radio_frequency->make_call();
1536 call.set_frequency(msg.carrier_frequency);
1537 call.set_modulation(static_cast<radio_frequency::RadioFrequencyModulation>(msg.modulation));
1538 call.set_repeat_count(msg.repeat_count);
1539 call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_);
1540 call.perform();
1541 }
1542#endif
1543}
1544#endif
1545
1546#if defined(USE_IR_RF) || defined(USE_RADIO_FREQUENCY)
1547void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) {
1548 if (!this->send_message(msg)) {
1549 // V: fires per decoded frame with no subscription gate, so a warning
1550 // would flood the congested link it reports on.
1551 ESP_LOGV(TAG, "IR/RF event dropped, TCP buffer full");
1552 }
1553}
1554#endif
1555
1556#ifdef USE_SERIAL_PROXY
1557static enums::SerialProxyStatus serial_proxy_result_to_status(serial_proxy::SerialProxyResult result) {
1558 switch (result) {
1560 return enums::SERIAL_PROXY_STATUS_OK;
1562 return enums::SERIAL_PROXY_STATUS_ASSUMED_SUCCESS;
1564 return enums::SERIAL_PROXY_STATUS_PORT_IN_USE;
1566 return enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
1568 return enums::SERIAL_PROXY_STATUS_TIMEOUT;
1570 return enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
1572 return enums::SERIAL_PROXY_STATUS_ERROR;
1573 }
1574 return enums::SERIAL_PROXY_STATUS_ERROR; // Unreachable; all enum values handled above
1575}
1576
1577static void send_serial_proxy_ack(APIConnection *conn, uint32_t instance, enums::SerialProxyRequestType type,
1578 enums::SerialProxyStatus status) {
1579 SerialProxyRequestResponse resp{};
1580 resp.instance = instance;
1581 resp.type = type;
1582 resp.status = status;
1583 if (!conn->send_message(resp)) {
1584 API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
1585 }
1586}
1587
1588void APIConnection::on_serial_proxy_configure_request(const SerialProxyConfigureRequest &msg) {
1589 auto &proxies = App.get_serial_proxies();
1590 if (msg.instance >= proxies.size()) {
1591 ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range (max %" PRIu32 ")", msg.instance,
1592 static_cast<uint32_t>(proxies.size()));
1593 send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE,
1594 enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
1595 return;
1596 }
1597 serial_proxy::SerialProxyResult result = proxies[msg.instance]->configure(
1598 this, msg.baudrate, msg.flow_control, static_cast<uint8_t>(msg.parity), msg.stop_bits, msg.data_size);
1599 send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE,
1600 serial_proxy_result_to_status(result));
1601}
1602
1603void APIConnection::on_serial_proxy_write_request(const SerialProxyWriteRequest &msg) {
1604 auto &proxies = App.get_serial_proxies();
1605 if (msg.instance >= proxies.size()) {
1606 ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
1607 return;
1608 }
1609 proxies[msg.instance]->write_from_client(this, msg.data, msg.data_len);
1610}
1611
1612void APIConnection::on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &msg) {
1613 auto &proxies = App.get_serial_proxies();
1614 if (msg.instance >= proxies.size()) {
1615 ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
1616 send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS,
1617 enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
1618 return;
1619 }
1620 serial_proxy::SerialProxyResult result = proxies[msg.instance]->set_modem_pins(this, msg.line_states);
1621 send_serial_proxy_ack(this, msg.instance, enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS,
1622 serial_proxy_result_to_status(result));
1623}
1624
1625void APIConnection::on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &msg) {
1626 auto &proxies = App.get_serial_proxies();
1628 resp.instance = msg.instance;
1629 if (msg.instance >= proxies.size()) {
1630 ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
1631 // Pre-1.16 clients do not read the status field and would take this error
1632 // for a successful "both pins deasserted" answer; let them time out as before
1633 if (!this->client_supports_api_version(1, 16)) {
1634 return;
1635 }
1636 resp.status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
1637 } else {
1638 resp.line_states = proxies[msg.instance]->get_modem_pins();
1639 }
1640 if (!this->send_message(resp)) {
1641 API_LOG_MSG_DROPPED(TAG, "Serial proxy response");
1642 }
1643}
1644
1645void APIConnection::on_serial_proxy_request(const SerialProxyRequest &msg) {
1646 auto &proxies = App.get_serial_proxies();
1647 if (msg.instance >= proxies.size()) {
1648 ESP_LOGW(TAG, "Serial proxy instance %" PRIu32 " out of range", msg.instance);
1649 send_serial_proxy_ack(this, msg.instance, msg.type, enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT);
1650 return;
1651 }
1652 auto *proxy = proxies[msg.instance];
1654 switch (msg.type) {
1655 case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE:
1656 case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE:
1657 status = serial_proxy_result_to_status(proxy->serial_proxy_request(this, msg.type));
1658 break;
1659 case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH:
1660 status = serial_proxy_result_to_status(proxy->flush_port(this));
1661 break;
1662 case enums::SERIAL_PROXY_REQUEST_TYPE_CONFIGURE:
1663 case enums::SERIAL_PROXY_REQUEST_TYPE_SET_MODEM_PINS:
1664 // Response-only discriminators; never valid in a request
1665 ESP_LOGW(TAG, "Response-only serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
1666 status = enums::SERIAL_PROXY_STATUS_INVALID_ARGUMENT;
1667 break;
1668 default:
1669 ESP_LOGW(TAG, "Unknown serial proxy request type: %" PRIu32, static_cast<uint32_t>(msg.type));
1670 status = enums::SERIAL_PROXY_STATUS_NOT_SUPPORTED;
1671 break;
1672 }
1673 send_serial_proxy_ack(this, msg.instance, msg.type, status);
1674}
1675
1676void APIConnection::send_serial_proxy_data(const SerialProxyDataReceived &msg) {
1677 if (!this->send_message(msg)) {
1678 ESP_LOGV(TAG, "Serial proxy data dropped, TCP buffer full");
1679 }
1680}
1681#endif
1682
1683#ifdef USE_INFRARED
1684uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1685 auto *infrared = static_cast<infrared::Infrared *>(entity);
1687 msg.capabilities = infrared->get_capability_flags();
1688 msg.receiver_frequency = infrared->get_traits().get_receiver_frequency_hz();
1689 return fill_and_encode_entity_info(infrared, msg, conn, remaining_size);
1690}
1691#endif
1692
1693#ifdef USE_RADIO_FREQUENCY
1694uint16_t APIConnection::try_send_radio_frequency_info(EntityBase *entity, APIConnection *conn,
1695 uint32_t remaining_size) {
1696 auto *rf = static_cast<radio_frequency::RadioFrequency *>(entity);
1698 msg.capabilities = rf->get_capability_flags();
1699 msg.frequency_min = rf->get_traits().get_frequency_min_hz();
1700 msg.frequency_max = rf->get_traits().get_frequency_max_hz();
1701 msg.supported_modulations = rf->get_traits().get_supported_modulations();
1702 return fill_and_encode_entity_info(rf, msg, conn, remaining_size);
1703}
1704#endif
1705
1706#ifdef USE_UPDATE
1707bool APIConnection::send_update_state(update::UpdateEntity *update) {
1708 return this->send_message_smart_(update, UpdateStateResponse::MESSAGE_TYPE, UpdateStateResponse::ESTIMATED_SIZE);
1709}
1710uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1711 auto *update = static_cast<update::UpdateEntity *>(entity);
1713 resp.missing_state = !update->has_state();
1714 if (update->has_state()) {
1716 if (update->update_info.has_progress) {
1717 resp.has_progress = true;
1718 resp.progress = update->update_info.progress;
1719 }
1720 resp.current_version = StringRef(update->update_info.current_version);
1721 resp.latest_version = StringRef(update->update_info.latest_version);
1722 resp.title = StringRef(update->update_info.title);
1723 resp.release_summary = StringRef(update->update_info.summary);
1724 resp.release_url = StringRef(update->update_info.release_url);
1725 }
1726 return fill_and_encode_entity_state(update, resp, conn, remaining_size);
1727}
1728uint16_t APIConnection::try_send_update_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
1729 auto *update = static_cast<update::UpdateEntity *>(entity);
1731 return fill_and_encode_entity_info_with_device_class(update, msg, msg.device_class, conn, remaining_size);
1732}
1733void APIConnection::on_update_command_request(const UpdateCommandRequest &msg) {
1734 ENTITY_COMMAND_GET(update::UpdateEntity, update, update)
1735
1736 switch (msg.command) {
1737 case enums::UPDATE_COMMAND_UPDATE:
1738 update->perform();
1739 break;
1740 case enums::UPDATE_COMMAND_CHECK:
1741 update->check();
1742 break;
1743 case enums::UPDATE_COMMAND_NONE:
1744 ESP_LOGE(TAG, "UPDATE_COMMAND_NONE not handled; confirm command is correct");
1745 break;
1746 default:
1747 ESP_LOGW(TAG, "Unknown update command: %" PRIu32, msg.command);
1748 break;
1749 }
1750}
1751#endif
1752
1753bool APIConnection::try_send_log_message(int level, const char *tag, const char *line, size_t message_len) {
1755 msg.level = static_cast<enums::LogLevel>(level);
1756 msg.set_message(reinterpret_cast<const uint8_t *>(line), message_len);
1757 return this->send_message(msg);
1758}
1759
1760void APIConnection::complete_authentication_() {
1761 // Early return if already authenticated
1762 if (this->flags_.connection_state == static_cast<uint8_t>(ConnectionState::AUTHENTICATED)) {
1763 return;
1764 }
1765
1766 this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
1767 // Reset traffic timer so keepalive starts from authentication, not connection start
1768 this->last_traffic_ = App.get_loop_component_start_time();
1769 this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("connected"));
1770#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
1771 {
1772 char peername[socket::SOCKADDR_STR_LEN];
1773 this->parent_->get_client_connected_trigger()->trigger(std::string(this->helper_->get_client_name()),
1774 std::string(this->helper_->get_peername_to(peername)));
1775 }
1776#endif
1777#ifdef USE_HOMEASSISTANT_TIME
1779 this->send_time_request();
1780 }
1781#endif
1782#ifdef USE_ZWAVE_PROXY
1783 if (zwave_proxy::global_zwave_proxy != nullptr) {
1785 }
1786#endif
1787}
1788
1789bool APIConnection::send_hello_response_(const HelloRequest &msg) {
1790 // Copy client name with truncation if needed (set_client_name handles truncation)
1791 this->helper_->set_client_name(msg.client_info.c_str(), msg.client_info.size());
1792 this->client_api_version_major_ =
1793 static_cast<uint8_t>(std::min<uint32_t>(msg.api_version_major, std::numeric_limits<uint8_t>::max()));
1794 this->client_api_version_minor_ =
1795 static_cast<uint8_t>(std::min<uint32_t>(msg.api_version_minor, std::numeric_limits<uint8_t>::max()));
1796 char peername[socket::SOCKADDR_STR_LEN];
1797 ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %u.%u", this->helper_->get_client_name(),
1798 this->helper_->get_peername_to(peername), this->client_api_version_major_, this->client_api_version_minor_);
1799
1800 HelloResponse resp;
1801 resp.api_version_major = 1;
1802 resp.api_version_minor = 16;
1803 // Send only the version string - the client only logs this for debugging and doesn't use it otherwise
1804 resp.server_info = ESPHOME_VERSION_REF;
1805 resp.name = StringRef(App.get_name());
1806
1807#ifdef USE_PROVISIONING
1809 // The provisioning window has closed without the device being provisioned.
1810 // Acknowledge the hello so the client can read the server name, then request
1811 // disconnect with the reason. Authentication is intentionally not completed.
1812 this->log_client_(ESPHOME_LOG_LEVEL_WARN, LOG_STR("Provisioning closed; rejecting connection"));
1813 if (!this->send_message(resp)) {
1814 API_LOG_MSG_DROPPED(TAG, "Hello response");
1815 }
1817 req.reason = enums::DISCONNECT_REASON_PROVISIONING_CLOSED;
1818 return this->send_message(req);
1819 }
1820#endif
1821
1822 // Auto-authenticate - password auth was removed in ESPHome 2026.1.0
1823 this->complete_authentication_();
1824
1825 return this->send_message(resp);
1826}
1827
1828bool APIConnection::send_ping_response_() {
1829 PingResponse resp;
1830 return this->send_message(resp);
1831}
1832
1833bool APIConnection::send_device_info_response_() {
1834 DeviceInfoResponse resp;
1835 resp.name = StringRef(App.get_name());
1837#ifdef USE_AREAS
1839#endif
1840 char mac_address[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1841 uint8_t mac[MAC_ADDRESS_SIZE];
1843 format_mac_addr_upper(mac, mac_address);
1844 resp.mac_address = StringRef(mac_address);
1845
1846 resp.esphome_version = ESPHOME_VERSION_REF;
1847
1848 // Stack buffer for build time string
1849 char build_time_str[Application::BUILD_TIME_STR_SIZE];
1850 App.get_build_time_string(build_time_str);
1851 resp.compilation_time = StringRef(build_time_str);
1852
1853 // Manufacturer string - define once, handle ESP8266 PROGMEM separately
1854#if defined(USE_ESP8266) || defined(USE_ESP32)
1855#define ESPHOME_MANUFACTURER "Espressif"
1856#elif defined(USE_RP2)
1857#define ESPHOME_MANUFACTURER "Raspberry Pi"
1858#elif defined(USE_BK72XX)
1859#define ESPHOME_MANUFACTURER "Beken"
1860#elif defined(USE_LN882X)
1861#define ESPHOME_MANUFACTURER "Lightning"
1862#elif defined(USE_NRF52)
1863#define ESPHOME_MANUFACTURER "Nordic Semiconductor"
1864#elif defined(USE_RTL87XX)
1865#define ESPHOME_MANUFACTURER "Realtek"
1866#elif defined(USE_HOST)
1867#define ESPHOME_MANUFACTURER "Host"
1868#endif
1869
1870#ifdef USE_ESP8266
1871 // ESP8266 requires PROGMEM for flash storage, copy to stack for memcpy compatibility
1872 static const char MANUFACTURER_PROGMEM[] PROGMEM = ESPHOME_MANUFACTURER;
1873 char manufacturer_buf[sizeof(MANUFACTURER_PROGMEM)];
1874 memcpy_P(manufacturer_buf, MANUFACTURER_PROGMEM, sizeof(MANUFACTURER_PROGMEM));
1875 resp.manufacturer = StringRef(manufacturer_buf, sizeof(MANUFACTURER_PROGMEM) - 1);
1876#else
1877 static constexpr auto MANUFACTURER = StringRef::from_lit(ESPHOME_MANUFACTURER);
1878 resp.manufacturer = MANUFACTURER;
1879#endif
1880 static_assert(sizeof(ESPHOME_MANUFACTURER) - 1 <= 20, "Update max_data_length for manufacturer in api.proto");
1881#undef ESPHOME_MANUFACTURER
1882
1883#ifdef USE_ESP8266
1884 static const char MODEL_PROGMEM[] PROGMEM = ESPHOME_BOARD;
1885 char model_buf[sizeof(MODEL_PROGMEM)];
1886 memcpy_P(model_buf, MODEL_PROGMEM, sizeof(MODEL_PROGMEM));
1887 resp.model = StringRef(model_buf, sizeof(MODEL_PROGMEM) - 1);
1888#else
1889 static constexpr auto MODEL = StringRef::from_lit(ESPHOME_BOARD);
1890 resp.model = MODEL;
1891#endif
1892#ifdef USE_DEEP_SLEEP
1894#endif
1895#ifdef ESPHOME_PROJECT_NAME
1896#ifdef USE_ESP8266
1897 static const char PROJECT_NAME_PROGMEM[] PROGMEM = ESPHOME_PROJECT_NAME;
1898 static const char PROJECT_VERSION_PROGMEM[] PROGMEM = ESPHOME_PROJECT_VERSION;
1899 char project_name_buf[sizeof(PROJECT_NAME_PROGMEM)];
1900 char project_version_buf[sizeof(PROJECT_VERSION_PROGMEM)];
1901 memcpy_P(project_name_buf, PROJECT_NAME_PROGMEM, sizeof(PROJECT_NAME_PROGMEM));
1902 memcpy_P(project_version_buf, PROJECT_VERSION_PROGMEM, sizeof(PROJECT_VERSION_PROGMEM));
1903 resp.project_name = StringRef(project_name_buf, sizeof(PROJECT_NAME_PROGMEM) - 1);
1904 resp.project_version = StringRef(project_version_buf, sizeof(PROJECT_VERSION_PROGMEM) - 1);
1905#else
1906 static constexpr auto PROJECT_NAME = StringRef::from_lit(ESPHOME_PROJECT_NAME);
1907 static constexpr auto PROJECT_VERSION = StringRef::from_lit(ESPHOME_PROJECT_VERSION);
1908 resp.project_name = PROJECT_NAME;
1909 resp.project_version = PROJECT_VERSION;
1910#endif
1911#endif
1912#ifdef USE_WEBSERVER
1913 resp.webserver_port = USE_WEBSERVER_PORT;
1914#endif
1915#ifdef USE_BLUETOOTH_PROXY
1917 char bluetooth_mac[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1919 resp.bluetooth_mac_address = StringRef(bluetooth_mac);
1920#endif
1921#ifdef USE_VOICE_ASSISTANT
1923#endif
1924#ifdef USE_ZWAVE_PROXY
1927#endif
1928#ifdef USE_SERIAL_PROXY
1929 size_t serial_proxy_index = 0;
1930 for (auto const &proxy : App.get_serial_proxies()) {
1931 if (serial_proxy_index >= SERIAL_PROXY_COUNT)
1932 break;
1933 auto &info = resp.serial_proxies[serial_proxy_index++];
1934 info.name = StringRef(proxy->get_name());
1935 info.port_type = proxy->get_port_type();
1936 info.configured_line_states = proxy->get_configured_modem_pins();
1937 }
1938#endif
1939#ifdef USE_API_NOISE
1940 resp.api_encryption_supported = true;
1941#ifndef USE_API_NOISE_PSK_FROM_YAML
1942 // No key from YAML: while no key is set, the key can be provisioned over a
1943 // zero-PSK Noise connection. Gated on the YAML define (not the plaintext
1944 // one) so this advertisement survives the plaintext removal in 2027.2.0.
1945 resp.api_encryption_provisionable = !this->parent_->get_noise_ctx().has_psk();
1946#endif
1947#endif
1948#ifdef USE_DEVICES
1949 size_t device_index = 0;
1950 for (auto const &device : App.get_devices()) {
1951 if (device_index >= ESPHOME_DEVICE_COUNT)
1952 break;
1953 auto &device_info = resp.devices[device_index++];
1954 device_info.device_id = device->get_device_id();
1955 device_info.name = StringRef(device->get_name());
1956 device_info.area_id = device->get_area_id();
1957 }
1958#endif
1959#ifdef USE_AREAS
1960 size_t area_index = 0;
1961 for (auto const &area : App.get_areas()) {
1962 if (area_index >= ESPHOME_AREA_COUNT)
1963 break;
1964 auto &area_info = resp.areas[area_index++];
1965 area_info.area_id = area->get_area_id();
1966 area_info.name = StringRef(area->get_name());
1967 }
1968#endif
1969
1970 return this->send_message(resp);
1971}
1972bool APIConnection::send_device_capabilities_response_() {
1973 // These are the same values DeviceInfoResponse still reports for older clients. Keep the blocks
1974 // below in sync with send_device_info_response_() until those copies are removed.
1976#ifdef USE_BLUETOOTH_PROXY
1978 char bluetooth_mac[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
1980 resp.bluetooth_proxy.mac_address = StringRef(bluetooth_mac);
1981#endif
1982#ifdef USE_VOICE_ASSISTANT
1984#endif
1985#ifdef USE_ZWAVE_PROXY
1988#endif
1989#ifdef USE_SERIAL_PROXY
1990 size_t serial_proxy_index = 0;
1991 for (auto const &proxy : App.get_serial_proxies()) {
1992 if (serial_proxy_index >= SERIAL_PROXY_COUNT)
1993 break;
1994 auto &info = resp.serial_proxies[serial_proxy_index++];
1995 info.name = StringRef(proxy->get_name());
1996 info.port_type = proxy->get_port_type();
1997 info.configured_line_states = proxy->get_configured_modem_pins();
1998 }
1999#endif
2000 return this->send_message(resp);
2001}
2002void APIConnection::on_hello_request(const HelloRequest &msg) {
2003 if (!this->send_hello_response_(msg)) {
2004 this->on_fatal_error();
2005 }
2006}
2007void APIConnection::on_disconnect_request(const DisconnectRequest & /*msg*/) {
2008 // The reason is informational when a client disconnects us; we always ack and close.
2009 if (!this->send_disconnect_response_()) {
2010 this->on_fatal_error();
2011 }
2012}
2013void APIConnection::on_ping_request() {
2014 if (!this->send_ping_response_()) {
2015 this->on_fatal_error();
2016 }
2017}
2018void APIConnection::on_device_info_request() {
2019 if (!this->send_device_info_response_()) {
2020 this->on_fatal_error();
2021 }
2022}
2023void APIConnection::on_device_capabilities_request() {
2024 if (!this->send_device_capabilities_response_()) {
2025 this->on_fatal_error();
2026 }
2027}
2028
2029#ifdef USE_API_HOMEASSISTANT_STATES
2030void APIConnection::on_home_assistant_state_response(const HomeAssistantStateResponse &msg) {
2031 // Skip if entity_id is empty (invalid message)
2032 if (msg.entity_id.empty()) {
2033 return;
2034 }
2035
2036 // Null-terminate state in-place for safe c_str() usage (e.g., parse_number in callbacks).
2037 // Safe: decode is complete, byte after string data was already consumed during parse,
2038 // and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte in rx_buf_.
2039 // const_cast is safe: msg references mutable rx_buf_ data; the const& handler
2040 // signature is a generated protobuf pattern, not a true immutability contract.
2041 if (!msg.state.empty()) {
2042 const_cast<char *>(msg.state.c_str())[msg.state.size()] = '\0';
2043 }
2044
2045 for (auto &it : this->parent_->get_state_subs()) {
2046 if (msg.entity_id != it.entity_id) {
2047 continue;
2048 }
2049
2050 // If subscriber has attribute filter (non-null), message attribute must match it;
2051 // if subscriber has no filter (nullptr), message must have no attribute.
2052 if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) {
2053 continue;
2054 }
2055
2056 it.callback(msg.state);
2057 }
2058}
2059#endif
2060#ifdef USE_API_USER_DEFINED_ACTIONS
2061void APIConnection::on_execute_service_request(const ExecuteServiceRequest &msg) {
2062 // Null-terminate string args in-place for safe c_str() usage in YAML service triggers.
2063 // Safe: full ExecuteServiceRequest decode is complete, all bytes in rx_buf_ consumed,
2064 // and frame helpers reserve RX_BUF_NULL_TERMINATOR extra byte for the last field.
2065 // const_cast is safe: msg references mutable rx_buf_ data; the const& handler
2066 // signature is a generated protobuf pattern, not a true immutability contract.
2067 for (auto &arg : const_cast<ExecuteServiceRequest &>(msg).args) {
2068 if (!arg.string_.empty()) {
2069 const_cast<char *>(arg.string_.c_str())[arg.string_.size()] = '\0';
2070 }
2071 }
2072 bool found = false;
2073#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
2074 // Register the call and get a unique server-generated action_call_id
2075 // This avoids collisions when multiple clients use the same call_id
2076 uint32_t action_call_id = 0;
2077 if (msg.call_id != 0) {
2078 action_call_id = this->parent_->register_active_action_call(msg.call_id, this);
2079 }
2080 // Use the overload that passes action_call_id separately (avoids copying msg)
2081 for (auto *service : this->parent_->get_user_services()) {
2082 if (service->execute_service(msg, action_call_id)) {
2083 found = true;
2084 }
2085 }
2086#else
2087 for (auto *service : this->parent_->get_user_services()) {
2088 if (service->execute_service(msg)) {
2089 found = true;
2090 }
2091 }
2092#endif
2093 if (!found) {
2094 ESP_LOGV(TAG, "Could not find service");
2095 }
2096 // Note: For services with supports_response != none, the call is unregistered
2097 // by an automatically appended APIUnregisterServiceCallAction at the end of
2098 // the action list. This ensures async actions (delays, waits) complete first.
2099}
2100#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
2101void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message) {
2103 resp.call_id = call_id;
2104 resp.success = success;
2105 resp.error_message = error_message;
2106 if (!this->send_message(resp)) {
2107 API_LOG_MSG_DROPPED(TAG, "Action response");
2108 }
2109}
2110#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
2111void APIConnection::send_execute_service_response(uint32_t call_id, bool success, StringRef error_message,
2112 const uint8_t *response_data, size_t response_data_len) {
2114 resp.call_id = call_id;
2115 resp.success = success;
2116 resp.error_message = error_message;
2117 resp.response_data = response_data;
2118 resp.response_data_len = response_data_len;
2119 if (!this->send_message(resp)) {
2120 API_LOG_MSG_DROPPED(TAG, "Action response");
2121 }
2122}
2123#endif // USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
2124#endif // USE_API_USER_DEFINED_ACTION_RESPONSES
2125#endif
2126
2127#ifdef USE_API_HOMEASSISTANT_SERVICES
2128bool APIConnection::send_homeassistant_action(const HomeassistantActionRequest &call) {
2129 if (!this->flags_.service_call_subscription)
2130 return false;
2131 if (!this->send_message(call)) {
2132 API_LOG_MSG_DROPPED(TAG, "Action request");
2133 }
2134 return true;
2135}
2136#endif // USE_API_HOMEASSISTANT_SERVICES
2137
2138#ifdef USE_HOMEASSISTANT_TIME
2139void APIConnection::send_time_request() {
2140 GetTimeRequest req;
2141 if (!this->send_message(req)) {
2142 API_LOG_MSG_DROPPED(TAG, "Time request");
2143 }
2144}
2145#endif // USE_HOMEASSISTANT_TIME
2146
2147#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
2148void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
2149#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
2150 if (msg.response_data_len > 0) {
2151 this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
2152 msg.response_data_len);
2153 } else
2154#endif
2155 {
2156 this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
2157 }
2158};
2159#endif
2160#ifdef USE_API_NOISE
2161bool APIConnection::send_noise_encryption_set_key_response_(const NoiseEncryptionSetKeyRequest &msg) {
2163 resp.success = false;
2164#ifdef USE_API_NOISE_PSK_FROM_YAML
2165 // A yaml key cannot be changed at runtime, so no decode or save path is built
2166 ESP_LOGW(TAG, "Key set in YAML");
2167#else
2168#ifdef USE_PROVISIONING
2169 // Refuse to set a key once the provisioning window has closed (defense in depth;
2170 // such connections are already rejected at hello).
2172 ESP_LOGW(TAG, "Provisioning closed; rejecting key set");
2173 return this->send_message(resp);
2174 }
2175#endif
2176
2177 noise::psk_t psk{};
2178 if (msg.key_len == 0) {
2179 if (this->parent_->clear_noise_psk(true)) {
2180 resp.success = true;
2181 } else {
2182 ESP_LOGW(TAG, "Failed to clear encryption key");
2183 }
2184 } else if (base64_decode(msg.key, msg.key_len, psk.data(), psk.size()) != psk.size()) {
2185 ESP_LOGW(TAG, "Invalid encryption key length");
2186 } else if (noise::NoiseContext::is_all_zeros(psk)) {
2187 // Accepting the reserved provisioning PSK would report success without
2188 // enabling encryption (or silently clear an existing key)
2189 ESP_LOGW(TAG, "Rejecting all-zero encryption key");
2190 } else if (!this->parent_->save_noise_psk(psk, true)) {
2191 ESP_LOGW(TAG, "Failed to save encryption key");
2192 } else {
2193 resp.success = true;
2194#ifdef USE_API_PLAINTEXT
2195 if (this->helper_->frame_footer_size() == 0) {
2196 // Plaintext transport has no frame footer; Noise always has the MAC footer.
2197 // Remove after 2027.2.0 together with plaintext support on keyless devices.
2198 ESP_LOGW(TAG, "Key received over plaintext; deprecated, will be removed in 2027.2.0");
2199 }
2200#endif
2201 }
2202#endif // USE_API_NOISE_PSK_FROM_YAML
2203
2204 return this->send_message(resp);
2205}
2206void APIConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) {
2207 if (!this->send_noise_encryption_set_key_response_(msg)) {
2208 this->on_fatal_error();
2209 }
2210}
2211#endif
2212#ifdef USE_API_HOMEASSISTANT_STATES
2213void APIConnection::on_subscribe_home_assistant_states_request() { state_subs_at_ = 0; }
2214#endif
2215bool APIConnection::try_to_clear_buffer_slow_(bool log_out_of_space) {
2216 delay(0);
2217 APIError err = this->helper_->loop();
2218 if (err != APIError::OK) {
2219 this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
2220 return false;
2221 }
2222 if (this->helper_->can_write_without_blocking())
2223 return true;
2224 if (log_out_of_space) {
2225 // VV: refusals are either reported by the sending call site (naming what
2226 // was lost) or retried without loss (the deferred batch), so this generic
2227 // line only duplicates them.
2228 ESP_LOGVV(TAG, "Cannot send message because of TCP buffer space");
2229 }
2230 return false;
2231}
2232bool APIConnection::send_message_(uint32_t payload_size, uint16_t message_type, MessageEncodeFn encode_fn,
2233 const void *msg) {
2234#ifdef HAS_PROTO_MESSAGE_DUMP
2235 // Skip dump for log messages (recursive logging risk) and camera frames (high-frequency noise)
2236 if (message_type != SubscribeLogsResponse::MESSAGE_TYPE
2237#ifdef USE_CAMERA
2238 && message_type != CameraImageResponse::MESSAGE_TYPE
2239#endif
2240 ) {
2241 auto *proto_msg = static_cast<const ProtoMessage *>(msg);
2242 DumpBuffer dump_buf;
2243 this->log_send_message_(proto_msg->message_name(), proto_msg->dump_to(dump_buf));
2244 }
2245#endif
2246 if (!this->prepare_first_message_buffer(payload_size)) [[unlikely]] {
2247 this->fatal_out_of_memory_();
2248 return false;
2249 }
2250 auto &shared_buf = this->parent_->get_shared_buffer_ref();
2251 size_t write_start = shared_buf.size();
2252#ifdef ESPHOME_DEBUG_API
2253 assert(shared_buf.capacity() >= write_start + payload_size);
2254#endif
2255 // Capacity reserved above, cannot fail
2256 (void) shared_buf.resize(write_start + payload_size);
2257 ProtoWriteBuffer buffer{&shared_buf, write_start};
2258 encode_fn(msg, buffer PROTO_ENCODE_DEBUG_INIT(&shared_buf));
2259 return this->send_buffer(ProtoWriteBuffer{&shared_buf}, message_type);
2260}
2261// encode_to_buffer is defined inline in api_connection.h (ESPHOME_ALWAYS_INLINE)
2262
2263// Noinline version for cold paths — single shared copy
2264uint16_t APIConnection::encode_to_buffer_slow(uint32_t calculated_size, MessageEncodeFn encode_fn, const void *msg,
2265 APIConnection *conn, uint32_t remaining_size) {
2266 return encode_to_buffer(calculated_size, encode_fn, msg, conn, remaining_size);
2267}
2268bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) {
2269 const bool is_log_message = (message_type == SubscribeLogsResponse::MESSAGE_TYPE);
2270
2271 if (!this->try_to_clear_buffer(!is_log_message)) {
2272 return false;
2273 }
2274
2275 // Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details
2276 this->helper_->set_nodelay_for_message(is_log_message);
2277
2278 APIError err = this->helper_->write_protobuf_packet(message_type, buffer);
2279 if (err == APIError::WOULD_BLOCK)
2280 return false;
2281 if (err != APIError::OK) {
2282 this->fatal_error_with_log_(LOG_STR("Packet write failed"), err);
2283 return false;
2284 }
2285 // Do not set last_traffic_ on send
2286 return true;
2287}
2288void APIConnection::on_no_setup_connection() {
2289 this->on_fatal_error();
2290 this->log_client_(ESPHOME_LOG_LEVEL_DEBUG, LOG_STR("no connection setup"));
2291}
2292void APIConnection::fatal_out_of_memory_() {
2293 this->fatal_error_with_log_(LOG_STR("Out of memory"), APIError::OUT_OF_MEMORY);
2294}
2295void APIConnection::on_fatal_error() {
2296 // Don't close socket here - keep it open so getpeername() works for logging
2297 // Socket will be closed when client is removed from the list in APIServer::loop()
2298 this->flags_.remove = true;
2299}
2300
2301bool APIConnection::schedule_message_front_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size) {
2302 this->deferred_batch_.add_item_front(entity, message_type, estimated_size);
2303 return this->schedule_batch_();
2304}
2305
2306bool APIConnection::send_message_smart_(EntityBase *entity, uint16_t message_type, uint8_t estimated_size,
2307 uint8_t aux_data_index) {
2308 if (this->should_send_immediately_(message_type) && this->helper_->can_write_without_blocking()) {
2309 // No local for the shared buffer here: keeping it live across
2310 // dispatch_message_ costs a register and spills message_type into the
2311 // batching path's dedup loop (measured on x86 GCC -Os)
2312 if (!this->prepare_first_message_buffer(estimated_size)) [[unlikely]] {
2313 this->fatal_out_of_memory_();
2314 return false;
2315 }
2316 DeferredBatch::BatchItem item{entity, message_type, estimated_size, aux_data_index};
2317 if (this->dispatch_message_(item, MAX_BATCH_PACKET_SIZE, true) &&
2318 this->send_buffer(ProtoWriteBuffer{&this->parent_->get_shared_buffer_ref()}, message_type)) {
2319#ifdef HAS_PROTO_MESSAGE_DUMP
2320 this->log_batch_item_(item);
2321#endif
2322 return true;
2323 }
2324 // An OOM during the immediate attempt marks the connection for removal;
2325 // don't queue more work (schedule_message_'s push_back may allocate again)
2326 if (this->flags_.remove) [[unlikely]]
2327 return false;
2328 }
2329 return this->schedule_message_(entity, message_type, estimated_size, aux_data_index);
2330}
2331
2332bool APIConnection::schedule_batch_() {
2333 if (!this->flags_.batch_scheduled) {
2334 this->flags_.batch_scheduled = true;
2335 this->deferred_batch_.batch_start_time = App.get_loop_component_start_time();
2336 }
2337 return true;
2338}
2339
2340void APIConnection::process_batch_() {
2341 if (this->deferred_batch_.empty()) {
2342 this->flags_.batch_scheduled = false;
2343 return;
2344 }
2345
2346 // Ensure TCP_NODELAY is on before draining overflow and writing batch data.
2347 // Log messages enable Nagle (NODELAY off) to coalesce small packets.
2348 // If Nagle is still on when we try to drain, LWIP holds data in the
2349 // Nagle buffer, the TCP send buffer stays full, and the overflow
2350 // buffer can never drain — blocking the batch write indefinitely.
2351 this->helper_->set_nodelay_for_message(false);
2352
2353 // Try to clear buffer first
2354 if (!this->try_to_clear_buffer(true)) {
2355 // Can't write now, we'll try again later
2356 return;
2357 }
2358
2359 // Get shared buffer reference once to avoid multiple calls
2360 auto &shared_buf = this->parent_->get_shared_buffer_ref();
2361 size_t num_items = this->deferred_batch_.size();
2362
2363 // Cache these values to avoid repeated virtual calls
2364 const uint8_t header_padding = this->helper_->frame_header_padding();
2365 const uint8_t footer_size = this->helper_->frame_footer_size();
2366
2367 // Pre-calculate exact buffer size needed based on message types
2368 uint32_t total_estimated_size = num_items * (header_padding + footer_size);
2369 for (size_t i = 0; i < num_items; i++) {
2370 total_estimated_size += this->deferred_batch_[i].estimated_size;
2371 }
2372 // Clamp to MAX_BATCH_PACKET_SIZE — we won't send more than that per batch
2373 if (total_estimated_size > MAX_BATCH_PACKET_SIZE) {
2374 total_estimated_size = MAX_BATCH_PACKET_SIZE;
2375 }
2376
2377 if (!this->prepare_first_message_buffer(header_padding, total_estimated_size)) [[unlikely]] {
2378 this->fatal_out_of_memory_();
2379 this->clear_batch_();
2380 return;
2381 }
2382
2383 // Fast path for single message - buffer already allocated above
2384 if (num_items == 1) {
2385 const auto &item = this->deferred_batch_[0];
2386 // Let dispatch_message_ calculate size and encode if it fits
2387 uint16_t payload_size = this->dispatch_message_(item, std::numeric_limits<uint16_t>::max(), true);
2388
2389 if (payload_size > 0 && this->send_buffer(ProtoWriteBuffer{&shared_buf}, item.message_type)) {
2390#ifdef HAS_PROTO_MESSAGE_DUMP
2391 // Log message after send attempt for VV debugging
2392 this->log_batch_item_(item);
2393#endif
2394 this->clear_batch_();
2395 } else if (payload_size == 0) {
2396 // payload_size == 0 with remove set means encoding hit OOM and the
2397 // connection is being dropped; warn only for a genuinely oversized message
2398 if (!this->flags_.remove) {
2399 ESP_LOGW(TAG, "Message too large to send: type=%u", item.message_type);
2400 }
2401 this->clear_batch_();
2402 }
2403 return;
2404 }
2405
2406 // Multi-message path — heavy stack frame isolated in separate noinline function
2407 this->process_batch_multi_(shared_buf, num_items, header_padding, footer_size);
2408}
2409
2410// Separated from process_batch_() so the single-message fast path gets a minimal
2411// stack frame without the MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo) array.
2412void APIConnection::process_batch_multi_(APIBuffer &shared_buf, size_t num_items, uint8_t header_padding,
2413 uint8_t footer_size) {
2414 // Ensure MessageInfo remains trivially destructible for our placement new approach
2415 static_assert(std::is_trivially_destructible<MessageInfo>::value,
2416 "MessageInfo must remain trivially destructible with this placement-new approach");
2417
2418 const size_t messages_to_process = std::min(num_items, MAX_MESSAGES_PER_BATCH);
2419
2420 // Stack-allocated array for message info
2421 alignas(MessageInfo) char message_info_storage[MAX_MESSAGES_PER_BATCH * sizeof(MessageInfo)];
2422 MessageInfo *message_info = reinterpret_cast<MessageInfo *>(message_info_storage);
2423 size_t items_processed = 0;
2424 uint16_t remaining_size = std::numeric_limits<uint16_t>::max();
2425 // Track where each message's header begins in the buffer
2426 // First message: offset 0 (max padding, may have unused leading bytes)
2427 // Subsequent messages: offset points to exact header start (no gaps)
2428 uint32_t current_offset = 0;
2429
2430 // Process items and encode directly to buffer (up to our limit)
2431 for (size_t i = 0; i < messages_to_process; i++) {
2432 const auto &item = this->deferred_batch_[i];
2433 // Try to encode message via dispatch
2434 // The dispatch function calculates overhead to determine if the message fits
2435 uint16_t payload_size = this->dispatch_message_(item, remaining_size, i == 0);
2436
2437 if (payload_size == 0) {
2438 // Message won't fit, stop processing
2439 break;
2440 }
2441
2442 // Message was encoded successfully
2443 // payload_size = header_size + proto_payload_size + footer_size
2444 uint16_t proto_payload_size = payload_size - this->batch_header_size_ - footer_size;
2445 // Use placement new to construct MessageInfo in pre-allocated stack array
2446 // This avoids default-constructing all MAX_MESSAGES_PER_BATCH elements
2447 // Explicit destruction is not needed because MessageInfo is trivially destructible,
2448 // as ensured by the static_assert in its definition.
2449 new (&message_info[items_processed++])
2450 MessageInfo(item.message_type, current_offset, proto_payload_size, this->batch_header_size_);
2451 // After first message, set remaining size to MAX_BATCH_PACKET_SIZE to avoid fragmentation
2452 if (items_processed == 1) {
2453 remaining_size = MAX_BATCH_PACKET_SIZE;
2454 }
2455 remaining_size -= payload_size;
2456 // Calculate where the next message's header padding will start
2457 // Current buffer size + footer space for this message
2458 current_offset = shared_buf.size() + footer_size;
2459 }
2460
2461 if (items_processed > 0) {
2462 // Add footer space for the last message (for Noise protocol MAC)
2463 if (footer_size > 0 && !shared_buf.resize(shared_buf.size() + footer_size)) [[unlikely]] {
2464 this->fatal_out_of_memory_();
2465 this->clear_batch_();
2466 return;
2467 }
2468
2469 // Send all collected messages
2470 APIError err = this->helper_->write_protobuf_messages(ProtoWriteBuffer{&shared_buf},
2471 std::span<const MessageInfo>(message_info, items_processed));
2472 if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
2473 this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
2474 }
2475
2476#ifdef HAS_PROTO_MESSAGE_DUMP
2477 // Log messages after send attempt for VV debugging
2478 // It's safe to use the buffer for logging at this point regardless of send result
2479 for (size_t i = 0; i < items_processed; i++) {
2480 const auto &item = this->deferred_batch_[i];
2481 this->log_batch_item_(item);
2482 }
2483#endif
2484
2485 // Partial batch — remove processed items and reschedule
2486 if (items_processed < this->deferred_batch_.size()) {
2487 this->deferred_batch_.remove_front(items_processed);
2488 this->schedule_batch_();
2489 return;
2490 }
2491 }
2492
2493 // All items processed (or none could be processed)
2494 this->clear_batch_();
2495}
2496
2497// Dispatch message encoding based on message_type
2498// Switch assigns function pointer, single call site for smaller code size
2499uint16_t APIConnection::dispatch_message_(const DeferredBatch::BatchItem &item, uint32_t remaining_size,
2500 bool batch_first) {
2501 this->flags_.batch_first_message = batch_first;
2502 this->batch_message_type_ = item.message_type;
2503#ifdef USE_EVENT
2504 // Events need aux_data_index to look up event type from entity
2505 if (item.message_type == EventResponse::MESSAGE_TYPE) {
2506 // Skip if aux_data_index is invalid (should never happen in normal operation)
2507 if (item.aux_data_index == DeferredBatch::AUX_DATA_UNUSED)
2508 return 0;
2509 auto *event = static_cast<event::Event *>(item.entity);
2510 return try_send_event_response(event, StringRef::from_maybe_nullptr(event->get_event_type(item.aux_data_index)),
2511 this, remaining_size);
2512 }
2513#endif
2514
2515 // All other message types use function pointer lookup via switch
2516 MessageCreatorPtr func = nullptr;
2517
2518// Macros to reduce repetitive switch cases
2519#define CASE_STATE_INFO(entity_name, StateResp, InfoResp) \
2520 case StateResp::MESSAGE_TYPE: \
2521 func = &try_send_##entity_name##_state; \
2522 break; \
2523 case InfoResp::MESSAGE_TYPE: \
2524 func = &try_send_##entity_name##_info; \
2525 break;
2526#define CASE_INFO_ONLY(entity_name, InfoResp) \
2527 case InfoResp::MESSAGE_TYPE: \
2528 func = &try_send_##entity_name##_info; \
2529 break;
2530
2531 switch (item.message_type) {
2532#ifdef USE_BINARY_SENSOR
2533 CASE_STATE_INFO(binary_sensor, BinarySensorStateResponse, ListEntitiesBinarySensorResponse)
2534#endif
2535#ifdef USE_COVER
2536 CASE_STATE_INFO(cover, CoverStateResponse, ListEntitiesCoverResponse)
2537#endif
2538#ifdef USE_FAN
2539 CASE_STATE_INFO(fan, FanStateResponse, ListEntitiesFanResponse)
2540#endif
2541#ifdef USE_LIGHT
2542 CASE_STATE_INFO(light, LightStateResponse, ListEntitiesLightResponse)
2543#endif
2544#ifdef USE_SENSOR
2545 CASE_STATE_INFO(sensor, SensorStateResponse, ListEntitiesSensorResponse)
2546#endif
2547#ifdef USE_SWITCH
2548 CASE_STATE_INFO(switch, SwitchStateResponse, ListEntitiesSwitchResponse)
2549#endif
2550#ifdef USE_BUTTON
2551 CASE_INFO_ONLY(button, ListEntitiesButtonResponse)
2552#endif
2553#ifdef USE_TEXT_SENSOR
2554 CASE_STATE_INFO(text_sensor, TextSensorStateResponse, ListEntitiesTextSensorResponse)
2555#endif
2556#ifdef USE_CLIMATE
2557 CASE_STATE_INFO(climate, ClimateStateResponse, ListEntitiesClimateResponse)
2558#endif
2559#ifdef USE_NUMBER
2560 CASE_STATE_INFO(number, NumberStateResponse, ListEntitiesNumberResponse)
2561#endif
2562#ifdef USE_DATETIME_DATE
2563 CASE_STATE_INFO(date, DateStateResponse, ListEntitiesDateResponse)
2564#endif
2565#ifdef USE_DATETIME_TIME
2566 CASE_STATE_INFO(time, TimeStateResponse, ListEntitiesTimeResponse)
2567#endif
2568#ifdef USE_DATETIME_DATETIME
2569 CASE_STATE_INFO(datetime, DateTimeStateResponse, ListEntitiesDateTimeResponse)
2570#endif
2571#ifdef USE_TEXT
2572 CASE_STATE_INFO(text, TextStateResponse, ListEntitiesTextResponse)
2573#endif
2574#ifdef USE_SELECT
2575 CASE_STATE_INFO(select, SelectStateResponse, ListEntitiesSelectResponse)
2576#endif
2577#ifdef USE_LOCK
2579#endif
2580#ifdef USE_VALVE
2581 CASE_STATE_INFO(valve, ValveStateResponse, ListEntitiesValveResponse)
2582#endif
2583#ifdef USE_MEDIA_PLAYER
2584 CASE_STATE_INFO(media_player, MediaPlayerStateResponse, ListEntitiesMediaPlayerResponse)
2585#endif
2586#ifdef USE_ALARM_CONTROL_PANEL
2587 CASE_STATE_INFO(alarm_control_panel, AlarmControlPanelStateResponse, ListEntitiesAlarmControlPanelResponse)
2588#endif
2589#ifdef USE_WATER_HEATER
2590 CASE_STATE_INFO(water_heater, WaterHeaterStateResponse, ListEntitiesWaterHeaterResponse)
2591#endif
2592#ifdef USE_CAMERA
2593 CASE_INFO_ONLY(camera, ListEntitiesCameraResponse)
2594#endif
2595#ifdef USE_INFRARED
2596 CASE_INFO_ONLY(infrared, ListEntitiesInfraredResponse)
2597#endif
2598#ifdef USE_RADIO_FREQUENCY
2599 CASE_INFO_ONLY(radio_frequency, ListEntitiesRadioFrequencyResponse)
2600#endif
2601#ifdef USE_EVENT
2602 CASE_INFO_ONLY(event, ListEntitiesEventResponse)
2603#endif
2604#ifdef USE_UPDATE
2605 CASE_STATE_INFO(update, UpdateStateResponse, ListEntitiesUpdateResponse)
2606#endif
2607 // Special messages (not entity state/info)
2608 case ListEntitiesDoneResponse::MESSAGE_TYPE:
2609 func = &try_send_list_info_done;
2610 break;
2611 case DisconnectRequest::MESSAGE_TYPE:
2612 func = &try_send_disconnect_request;
2613 break;
2614 case PingRequest::MESSAGE_TYPE:
2615 func = &try_send_ping_request;
2616 break;
2617 default:
2618 return 0;
2619 }
2620
2621#undef CASE_STATE_INFO
2622#undef CASE_INFO_ONLY
2623
2624 return func(item.entity, this, remaining_size);
2625}
2626
2627uint16_t APIConnection::try_send_list_info_done(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
2629 return encode_message_to_buffer(resp, conn, remaining_size);
2630}
2631
2632uint16_t APIConnection::try_send_disconnect_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
2634 return encode_message_to_buffer(req, conn, remaining_size);
2635}
2636
2637uint16_t APIConnection::try_send_ping_request(EntityBase *entity, APIConnection *conn, uint32_t remaining_size) {
2638 PingRequest req;
2639 return encode_message_to_buffer(req, conn, remaining_size);
2640}
2641
2642#ifdef USE_API_HOMEASSISTANT_STATES
2643void APIConnection::process_state_subscriptions_() {
2644 const auto &subs = this->parent_->get_state_subs();
2645 if (this->state_subs_at_ >= static_cast<int>(subs.size())) {
2646 this->state_subs_at_ = -1;
2647 return;
2648 }
2649
2650 const auto &it = subs[this->state_subs_at_];
2652 resp.entity_id = StringRef(it.entity_id);
2653
2654 // Avoid string copy by using the const char* pointer if it exists
2655 resp.attribute = it.attribute != nullptr ? StringRef(it.attribute) : StringRef("");
2656
2657 resp.once = it.once;
2658 if (this->send_message(resp)) {
2659 this->state_subs_at_++;
2660 }
2661}
2662#endif // USE_API_HOMEASSISTANT_STATES
2663
2664void APIConnection::log_client_(int level, const LogString *message) {
2665 char peername[socket::SOCKADDR_STR_LEN];
2666 esp_log_printf_(level, TAG, __LINE__, ESPHOME_LOG_FORMAT("%s (%s): %s"), this->helper_->get_client_name(),
2667 this->helper_->get_peername_to(peername), LOG_STR_ARG(message));
2668}
2669
2670void APIConnection::log_warning_(const LogString *message, APIError err) {
2671 char peername[socket::SOCKADDR_STR_LEN];
2672 ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->helper_->get_client_name(), this->helper_->get_peername_to(peername),
2673 LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
2674}
2675
2676} // namespace esphome::api
2677#endif
uint8_t status
Definition bl0942.h:8
const StringRef & get_name() const
Get the 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)
const StringRef & get_friendly_name() const
Get the friendly name of this Application set by pre_setup().
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()
auto & get_serial_proxies() const
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)
ESPHOME_ALWAYS_INLINE void try_advance(size_t max_steps)
Run up to max_steps iteration steps; stops early when iteration completes or a callback refuses (that...
const char * get_device_class_to(std::span< char, MAX_DEVICE_CLASS_LENGTH > buffer) const
bool has_own_name() const
Definition entity_base.h:74
const StringRef & get_name() const
Definition entity_base.h:71
const char * get_icon_to(std::span< char, MAX_ICON_LENGTH > buffer) const
uint32_t get_object_id_hash() const
Definition entity_base.h:77
uint32_t get_device_id() const
bool is_disabled_by_default() const
EntityCategory get_entity_category() const
Fixed-capacity vector - allocates once at runtime, never reallocates This avoids std::vector template...
Definition helpers.h:545
void push_back(const T &value)
Add element without bounds checking Caller must ensure sufficient capacity was allocated via init() S...
Definition helpers.h:662
void init(size_t n)
Definition helpers.h:635
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
Byte buffer that skips zero-initialization on resize().
Definition api_buffer.h:26
size_t size() const
Definition api_buffer.h:44
bool resize(size_t n) ESPHOME_ALWAYS_INLINE
Returns false if allocation fails; the buffer is left unchanged. No zero-fill.
Definition api_buffer.h:32
void on_button_command_request(const ButtonCommandRequest &msg)
uint8_t *(*)(const void *, ProtoWriteBuffer &PROTO_ENCODE_DEBUG_PARAM) MessageEncodeFn
APIConnection(std::unique_ptr< socket::Socket > socket, APIServer *parent)
uint16_t(*)(EntityBase *, APIConnection *, uint32_t remaining_size) MessageCreatorPtr
uint32_t(*)(const void *) CalculateSizeFn
uint8_t get_consumed_header(uint8_t out[3]) const
noise::NoiseContext & get_noise_ctx()
Definition api_server.h:87
enums::AlarmControlPanelStateCommand command
Definition api_pb2.h:2742
enums::AlarmControlPanelState state
Definition api_pb2.h:2726
enums::BluetoothScannerMode mode
Definition api_pb2.h:2440
void set_data(const uint8_t *data, size_t len)
Definition api_pb2.h:1452
enums::ClimateSwingMode swing_mode
Definition api_pb2.h:1563
enums::ClimateFanMode fan_mode
Definition api_pb2.h:1561
enums::ClimatePreset preset
Definition api_pb2.h:1567
enums::ClimateFanMode fan_mode
Definition api_pb2.h:1530
enums::ClimateSwingMode swing_mode
Definition api_pb2.h:1531
enums::ClimateAction action
Definition api_pb2.h:1529
enums::ClimatePreset preset
Definition api_pb2.h:1533
enums::CoverOperation current_operation
Definition api_pb2.h:766
std::array< SerialProxyInfo, SERIAL_PROXY_COUNT > serial_proxies
Definition api_pb2.h:677
VoiceAssistantCapabilities voice_assistant
Definition api_pb2.h:671
ZWaveProxyCapabilities zwave_proxy
Definition api_pb2.h:674
BluetoothProxyCapabilities bluetooth_proxy
Definition api_pb2.h:668
std::array< AreaInfo, ESPHOME_AREA_COUNT > areas
Definition api_pb2.h:594
std::array< SerialProxyInfo, SERIAL_PROXY_COUNT > serial_proxies
Definition api_pb2.h:606
std::array< DeviceInfo, ESPHOME_DEVICE_COUNT > devices
Definition api_pb2.h:591
enums::DisconnectReason reason
Definition api_pb2.h:456
Fixed-size buffer for message dumps - avoids heap allocation.
Definition proto.h:543
enums::FanDirection direction
Definition api_pb2.h:849
enums::FanDirection direction
Definition api_pb2.h:826
ParsedTimezone parsed_timezone
Definition api_pb2.h:1305
enums::EntityCategory entity_category
Definition api_pb2.h:382
enums::ColorMode color_mode
Definition api_pb2.h:893
const std::vector< const char * > * supported_custom_presets
Definition api_pb2.h:1501
const climate::ClimateSwingModeMask * supported_swing_modes
Definition api_pb2.h:1498
enums::TemperatureUnit temperature_unit
Definition api_pb2.h:1508
const std::vector< const char * > * supported_custom_fan_modes
Definition api_pb2.h:1499
const climate::ClimatePresetMask * supported_presets
Definition api_pb2.h:1500
const climate::ClimateFanModeMask * supported_fan_modes
Definition api_pb2.h:1497
const climate::ClimateModeMask * supported_modes
Definition api_pb2.h:1492
const FixedVector< const char * > * event_types
Definition api_pb2.h:2926
const std::vector< const char * > * supported_preset_modes
Definition api_pb2.h:808
const FixedVector< const char * > * effects
Definition api_pb2.h:875
const light::ColorModeMask * supported_color_modes
Definition api_pb2.h:872
std::vector< MediaPlayerSupportedFormat > supported_formats
Definition api_pb2.h:1927
const FixedVector< const char * > * options
Definition api_pb2.h:1711
enums::SensorStateClass state_class
Definition api_pb2.h:966
const water_heater::WaterHeaterModeMask * supported_modes
Definition api_pb2.h:1593
enums::LockCommand command
Definition api_pb2.h:1858
enums::MediaPlayerCommand command
Definition api_pb2.h:1963
enums::MediaPlayerState state
Definition api_pb2.h:1944
enums::SerialProxyParity parity
Definition api_pb2.h:3272
enums::SerialProxyRequestType type
Definition api_pb2.h:3379
void set_message(const uint8_t *data, size_t len)
Definition api_pb2.h:1105
enums::UpdateCommand command
Definition api_pb2.h:3106
enums::ValveOperation current_operation
Definition api_pb2.h:2980
std::vector< VoiceAssistantWakeWord > available_wake_words
Definition api_pb2.h:2673
const std::vector< std::string > * active_wake_words
Definition api_pb2.h:2674
std::vector< std::string > active_wake_words
Definition api_pb2.h:2691
enums::ZWaveProxyRequestType type
Definition api_pb2.h:3142
enums::ZWaveProxyRequestType type
Definition api_pb2.h:3162
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 get_bluetooth_mac_address_pretty(std::span< char, MAC_ADDRESS_PRETTY_BUFFER_SIZE > output)
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 bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg)
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:18
ClimateDevice - This is the base class for all climate integrations.
Definition climate.h:187
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:53
Infrared - Base class for infrared remote control implementations.
Definition infrared.h:114
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:112
static bool is_all_zeros(const psk_t &psk)
Definition noise.h:19
Base-class for all numbers.
Definition number.h:29
RadioFrequency - Base class for radio frequency implementations.
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
Base class for all valve devices.
Definition valve.h:103
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)
uint32_t get_feature_flags() const
Definition zwave_proxy.h:67
void send_frame(api::APIConnection *api_connection, const uint8_t *data, size_t length)
void api_connection_authenticated(api::APIConnection *conn)
api::enums::ZWaveProxyStatus zwave_proxy_request(api::APIConnection *api_connection, api::enums::ZWaveProxyRequestType type)
const LogString * message
Definition component.cpp:35
uint16_t type
bool state
Definition fan.h:2
uint32_t socklen_t
Definition headers.h:99
const LogString * api_error_to_logstr(APIError err)
void log_dropped_message(const char *tag, int line, const LogString *what)
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:20
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.
std::array< uint8_t, 32 > psk_t
Definition noise.h:11
ProvisioningManager * global_provisioning_manager
RadioFrequencyModulation
Modulation types supported by radio frequency implementations.
SerialProxyResult
Result of a client-initiated operation; mapped to api::enums::SerialProxyStatus by the API layer.
@ SERIAL_PROXY_RESULT_TIMEOUT
Timed out before TX completed.
@ SERIAL_PROXY_RESULT_ERROR
Driver or hardware error.
@ SERIAL_PROXY_RESULT_NOT_SUPPORTED
Requested feature is not available on this instance.
@ SERIAL_PROXY_RESULT_PORT_IN_USE
Denied: another live client holds the port.
@ SERIAL_PROXY_RESULT_OK
Operation completed or request accepted.
@ SERIAL_PROXY_RESULT_INVALID_ARGUMENT
A parameter value is out of range.
@ SERIAL_PROXY_RESULT_ASSUMED_SUCCESS
Platform cannot confirm TX drain; success assumed.
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:21
const void size_t len
Definition hal.h:64
std::vector< uint8_t > base64_decode(const std::string &encoded_string)
Decode a base64 string to a byte vector.
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:87
void HOT delay(uint32_t ms)
Definition hal.cpp:85
Application App
Global storage of Application pointer - only one Application can exist.
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:1505
static void uint32_t
A more user-friendly version of struct tm from time.h.
Definition time.h:23
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()
SemaphoreHandle_t lock
const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM
Definition web_server.h:28