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