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