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