From 595217786cc889b431b974728defbfe780e612d9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 11 Jan 2026 18:47:57 -1000 Subject: [PATCH 01/20] [tuya][rc522][remote_base] Migrate format_hex_pretty() to stack-based alternatives (#13158) --- esphome/components/rc522/rc522.cpp | 5 ++++- esphome/components/remote_base/midea_protocol.h | 2 ++ esphome/components/tuya/text_sensor/tuya_text_sensor.cpp | 9 ++++++--- esphome/core/entity_base.h | 3 +++ 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/esphome/components/rc522/rc522.cpp b/esphome/components/rc522/rc522.cpp index 8f8740c9252..470c50109e6 100644 --- a/esphome/components/rc522/rc522.cpp +++ b/esphome/components/rc522/rc522.cpp @@ -492,7 +492,10 @@ bool RC522BinarySensor::process(std::vector &data) { this->found_ = result; return result; } -void RC522Trigger::process(std::vector &data) { this->trigger(format_hex_pretty(data, '-', false)); } +void RC522Trigger::process(std::vector &data) { + char uid_buf[format_hex_pretty_size(RC522_MAX_UID_SIZE)]; + this->trigger(format_hex_pretty_to(uid_buf, data.data(), data.size(), '-')); +} } // namespace rc522 } // namespace esphome diff --git a/esphome/components/remote_base/midea_protocol.h b/esphome/components/remote_base/midea_protocol.h index 0a5de8e9df5..c3030d565ee 100644 --- a/esphome/components/remote_base/midea_protocol.h +++ b/esphome/components/remote_base/midea_protocol.h @@ -29,6 +29,8 @@ class MideaData { bool is_valid() const { return this->data_[OFFSET_CS] == this->calc_cs_(); } void finalize() { this->data_[OFFSET_CS] = this->calc_cs_(); } bool is_compliment(const MideaData &rhs) const; + /// @deprecated Allocates heap memory. Use to_str() instead. Removed in 2026.7.0. + ESPDEPRECATED("Allocates heap memory. Use to_str() instead. Removed in 2026.7.0.", "2026.1.0") std::string to_string() const { return format_hex_pretty(this->data_.data(), this->data_.size()); } /// Buffer size for to_str(): 6 bytes = "AA.BB.CC.DD.EE.FF\0" static constexpr size_t TO_STR_BUFFER_SIZE = format_hex_pretty_size(6); diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp index 3c492d609d6..36b6d630ae3 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -1,4 +1,5 @@ #include "tuya_text_sensor.h" +#include "esphome/core/entity_base.h" #include "esphome/core/log.h" namespace esphome { @@ -14,9 +15,11 @@ void TuyaTextSensor::setup() { this->publish_state(datapoint.value_string); break; case TuyaDatapointType::RAW: { - std::string data = format_hex_pretty(datapoint.value_raw); - ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, data.c_str()); - this->publish_state(data); + char hex_buf[MAX_STATE_LEN + 1]; + const char *formatted = + format_hex_pretty_to(hex_buf, sizeof(hex_buf), datapoint.value_raw.data(), datapoint.value_raw.size()); + ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, formatted); + this->publish_state(formatted); break; } case TuyaDatapointType::ENUM: { diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 5f75872a0f7..f91bd9b20c6 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -26,6 +26,9 @@ static constexpr size_t ESPHOME_DOMAIN_MAX_LEN = 20; // Maximum size for object_id buffer (friendly_name + null + margin) static constexpr size_t OBJECT_ID_MAX_LEN = 128; +// Maximum state length that Home Assistant will accept without raising ValueError +static constexpr size_t MAX_STATE_LEN = 255; + enum EntityCategory : uint8_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, From 83eebdf15da04184c2fa1bd3d34aaab541739c6f Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Sun, 11 Jan 2026 23:01:23 -0600 Subject: [PATCH 02/20] [infrared] Implement experimental API/Core/component for new component/entity type (#13129) Co-authored-by: J. Nick Koston Co-authored-by: J. Nick Koston --- CODEOWNERS | 1 + esphome/components/api/api.proto | 48 ++++++ esphome/components/api/api_connection.cpp | 32 ++++ esphome/components/api/api_connection.h | 9 ++ esphome/components/api/api_pb2.cpp | 93 +++++++++++ esphome/components/api/api_pb2.h | 65 ++++++++ esphome/components/api/api_pb2_dump.cpp | 44 +++++ esphome/components/api/api_pb2_service.cpp | 16 ++ esphome/components/api/api_pb2_service.h | 11 ++ esphome/components/api/api_server.cpp | 15 ++ esphome/components/api/api_server.h | 3 + esphome/components/api/list_entities.cpp | 3 + esphome/components/api/list_entities.h | 3 + esphome/components/api/subscribe_state.h | 3 + esphome/components/infrared/__init__.py | 76 +++++++++ esphome/components/infrared/infrared.cpp | 150 ++++++++++++++++++ esphome/components/infrared/infrared.h | 130 +++++++++++++++ .../components/web_server/list_entities.cpp | 7 + esphome/components/web_server/list_entities.h | 3 + esphome/core/application.h | 15 ++ esphome/core/component_iterator.cpp | 6 + esphome/core/component_iterator.h | 12 ++ esphome/core/defines.h | 5 +- tests/components/web_server/common.yaml | 1 + 24 files changed, 750 insertions(+), 1 deletion(-) create mode 100644 esphome/components/infrared/__init__.py create mode 100644 esphome/components/infrared/infrared.cpp create mode 100644 esphome/components/infrared/infrared.h diff --git a/CODEOWNERS b/CODEOWNERS index bdcc86ef0c4..48318ee0646 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -249,6 +249,7 @@ esphome/components/ina260/* @mreditor97 esphome/components/ina2xx_base/* @latonita esphome/components/ina2xx_i2c/* @latonita esphome/components/ina2xx_spi/* @latonita +esphome/components/infrared/* @kbx81 esphome/components/inkbird_ibsth1_mini/* @fkirill esphome/components/inkplate/* @jesserockz @JosipKuci esphome/components/integration/* @OttoWinter diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d6384456d55..597da25883a 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -66,6 +66,8 @@ service APIConnection { rpc zwave_proxy_frame(ZWaveProxyFrame) returns (void) {} rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {} + + rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {} } @@ -2437,3 +2439,49 @@ message ZWaveProxyRequest { ZWaveProxyRequestType type = 1; bytes data = 2; } + +// ==================== INFRARED ==================== +// Note: Feature and capability flag enums are defined in +// esphome/components/infrared/infrared.h + +// Listing of infrared instances +message ListEntitiesInfraredResponse { + option (id) = 135; + option (base_class) = "InfoResponseProtoMessage"; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_INFRARED"; + + string object_id = 1; + fixed32 key = 2; + string name = 3; + string icon = 4 [(field_ifdef) = "USE_ENTITY_ICON"]; + bool disabled_by_default = 5; + EntityCategory entity_category = 6; + uint32 device_id = 7 [(field_ifdef) = "USE_DEVICES"]; + uint32 capabilities = 8; // Bitfield of InfraredCapabilityFlags +} + +// Command to transmit infrared/RF data using raw timings +message InfraredRFTransmitRawTimingsRequest { + option (id) = 136; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_IR_RF"; + + uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; + fixed32 key = 2; // Key identifying the transmitter instance + uint32 carrier_frequency = 3; // Carrier frequency in Hz + uint32 repeat_count = 4; // Number of times to transmit (1 = once, 2 = twice, etc.) + repeated sint32 timings = 5 [packed = true, (packed_buffer) = true]; // Raw timings in microseconds (zigzag-encoded): positive = mark (LED/TX on), negative = space (LED/TX off) +} + +// Event message for received infrared/RF data +message InfraredRFReceiveEvent { + option (id) = 137; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_IR_RF"; + option (no_delay) = true; + + uint32 device_id = 1 [(field_ifdef) = "USE_DEVICES"]; + fixed32 key = 2; // Key identifying the receiver instance + repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods +} diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index d6f0d84550c..65f8c1a8cc6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -46,6 +46,9 @@ #ifdef USE_WATER_HEATER #include "esphome/components/water_heater/water_heater.h" #endif +#ifdef USE_INFRARED +#include "esphome/components/infrared/infrared.h" +#endif namespace esphome::api { @@ -1438,6 +1441,35 @@ uint16_t APIConnection::try_send_event_info(EntityBase *entity, APIConnection *c } #endif +#ifdef USE_IR_RF +void APIConnection::infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) { + // TODO: When RF is implemented, add a field to the message to distinguish IR vs RF + // and dispatch to the appropriate entity type based on that field. +#ifdef USE_INFRARED + ENTITY_COMMAND_MAKE_CALL(infrared::Infrared, infrared, infrared) + call.set_carrier_frequency(msg.carrier_frequency); + call.set_raw_timings_packed(msg.timings_data_, msg.timings_length_, msg.timings_count_); + call.set_repeat_count(msg.repeat_count); + call.perform(); +#endif +} + +void APIConnection::send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg) { + this->send_message(msg, InfraredRFReceiveEvent::MESSAGE_TYPE); +} +#endif + +#ifdef USE_INFRARED +uint16_t APIConnection::try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single) { + auto *infrared = static_cast(entity); + ListEntitiesInfraredResponse msg; + msg.capabilities = infrared->get_capability_flags(); + return fill_and_encode_entity_info(infrared, msg, ListEntitiesInfraredResponse::MESSAGE_TYPE, conn, remaining_size, + is_single); +} +#endif + #ifdef USE_UPDATE bool APIConnection::send_update_state(update::UpdateEntity *update) { return this->send_message_smart_(update, &APIConnection::try_send_update_state, UpdateStateResponse::MESSAGE_TYPE, diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 0289b3d2ff5..b3d072ff69c 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -172,6 +172,11 @@ class APIConnection final : public APIServerConnection { void on_water_heater_command_request(const WaterHeaterCommandRequest &msg) override; #endif +#ifdef USE_IR_RF + void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) override; + void send_infrared_rf_receive_event(const InfraredRFReceiveEvent &msg); +#endif + #ifdef USE_EVENT void send_event(event::Event *event, StringRef event_type); #endif @@ -468,6 +473,10 @@ class APIConnection final : public APIServerConnection { static uint16_t try_send_water_heater_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single); #endif +#ifdef USE_INFRARED + static uint16_t try_send_infrared_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, + bool is_single); +#endif #ifdef USE_EVENT static uint16_t try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn, uint32_t remaining_size, bool is_single); diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index 03a6639b5e5..743f51dac77 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -3347,5 +3347,98 @@ void ZWaveProxyRequest::calculate_size(ProtoSize &size) const { size.add_length(1, this->data_len); } #endif +#ifdef USE_INFRARED +void ListEntitiesInfraredResponse::encode(ProtoWriteBuffer buffer) const { + buffer.encode_string(1, this->object_id); + buffer.encode_fixed32(2, this->key); + buffer.encode_string(3, this->name); +#ifdef USE_ENTITY_ICON + buffer.encode_string(4, this->icon); +#endif + buffer.encode_bool(5, this->disabled_by_default); + buffer.encode_uint32(6, static_cast(this->entity_category)); +#ifdef USE_DEVICES + buffer.encode_uint32(7, this->device_id); +#endif + buffer.encode_uint32(8, this->capabilities); +} +void ListEntitiesInfraredResponse::calculate_size(ProtoSize &size) const { + size.add_length(1, this->object_id.size()); + size.add_fixed32(1, this->key); + size.add_length(1, this->name.size()); +#ifdef USE_ENTITY_ICON + size.add_length(1, this->icon.size()); +#endif + size.add_bool(1, this->disabled_by_default); + size.add_uint32(1, static_cast(this->entity_category)); +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif + size.add_uint32(1, this->capabilities); +} +#endif +#ifdef USE_IR_RF +bool InfraredRFTransmitRawTimingsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { +#ifdef USE_DEVICES + case 1: + this->device_id = value.as_uint32(); + break; +#endif + case 3: + this->carrier_frequency = value.as_uint32(); + break; + case 4: + this->repeat_count = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool InfraredRFTransmitRawTimingsRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 5: { + this->timings_data_ = value.data(); + this->timings_length_ = value.size(); + this->timings_count_ = count_packed_varints(value.data(), value.size()); + break; + } + default: + return false; + } + return true; +} +bool InfraredRFTransmitRawTimingsRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { + switch (field_id) { + case 2: + this->key = value.as_fixed32(); + break; + default: + return false; + } + return true; +} +void InfraredRFReceiveEvent::encode(ProtoWriteBuffer buffer) const { +#ifdef USE_DEVICES + buffer.encode_uint32(1, this->device_id); +#endif + buffer.encode_fixed32(2, this->key); + for (const auto &it : *this->timings) { + buffer.encode_sint32(3, it, true); + } +} +void InfraredRFReceiveEvent::calculate_size(ProtoSize &size) const { +#ifdef USE_DEVICES + size.add_uint32(1, this->device_id); +#endif + size.add_fixed32(1, this->key); + if (!this->timings->empty()) { + for (const auto &it : *this->timings) { + size.add_sint32_force(1, it); + } + } +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index e21b8596ca6..0ab38b8b85f 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -3049,5 +3049,70 @@ class ZWaveProxyRequest final : public ProtoDecodableMessage { bool decode_varint(uint32_t field_id, ProtoVarInt value) override; }; #endif +#ifdef USE_INFRARED +class ListEntitiesInfraredResponse final : public InfoResponseProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 135; + static constexpr uint8_t ESTIMATED_SIZE = 44; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "list_entities_infrared_response"; } +#endif + uint32_t capabilities{0}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(ProtoSize &size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: +}; +#endif +#ifdef USE_IR_RF +class InfraredRFTransmitRawTimingsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 136; + static constexpr uint8_t ESTIMATED_SIZE = 220; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "infrared_rf_transmit_raw_timings_request"; } +#endif +#ifdef USE_DEVICES + uint32_t device_id{0}; +#endif + uint32_t key{0}; + uint32_t carrier_frequency{0}; + uint32_t repeat_count{0}; + const uint8_t *timings_data_{nullptr}; + uint16_t timings_length_{0}; + uint16_t timings_count_{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: + bool decode_32bit(uint32_t field_id, Proto32Bit value) override; + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class InfraredRFReceiveEvent final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 137; + static constexpr uint8_t ESTIMATED_SIZE = 17; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "infrared_rf_receive_event"; } +#endif +#ifdef USE_DEVICES + uint32_t device_id{0}; +#endif + uint32_t key{0}; + const std::vector *timings{}; + void encode(ProtoWriteBuffer buffer) const override; + void calculate_size(ProtoSize &size) const override; +#ifdef HAS_PROTO_MESSAGE_DUMP + void dump_to(std::string &out) const override; +#endif + + protected: +}; +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 160a9a93c9d..8e4d55d11b5 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -2309,6 +2309,50 @@ void ZWaveProxyRequest::dump_to(std::string &out) const { out.append("\n"); } #endif +#ifdef USE_INFRARED +void ListEntitiesInfraredResponse::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "ListEntitiesInfraredResponse"); + dump_field(out, "object_id", this->object_id); + dump_field(out, "key", this->key); + dump_field(out, "name", this->name); +#ifdef USE_ENTITY_ICON + dump_field(out, "icon", this->icon); +#endif + dump_field(out, "disabled_by_default", this->disabled_by_default); + dump_field(out, "entity_category", static_cast(this->entity_category)); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif + dump_field(out, "capabilities", this->capabilities); +} +#endif +#ifdef USE_IR_RF +void InfraredRFTransmitRawTimingsRequest::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "InfraredRFTransmitRawTimingsRequest"); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif + dump_field(out, "key", this->key); + dump_field(out, "carrier_frequency", this->carrier_frequency); + dump_field(out, "repeat_count", this->repeat_count); + out.append(" timings: "); + out.append("packed buffer ["); + out.append(std::to_string(this->timings_count_)); + out.append(" values, "); + out.append(std::to_string(this->timings_length_)); + out.append(" bytes]\n"); +} +void InfraredRFReceiveEvent::dump_to(std::string &out) const { + MessageDumpHelper helper(out, "InfraredRFReceiveEvent"); +#ifdef USE_DEVICES + dump_field(out, "device_id", this->device_id); +#endif + dump_field(out, "key", this->key); + for (const auto &it : *this->timings) { + dump_field(out, "timings", it, 4); + } +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index c9bf638ad74..576b8024430 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -621,6 +621,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_water_heater_command_request(msg); break; } +#endif +#ifdef USE_IR_RF + case InfraredRFTransmitRawTimingsRequest::MESSAGE_TYPE: { + InfraredRFTransmitRawTimingsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + ESP_LOGVV(TAG, "on_infrared_rf_transmit_raw_timings_request: %s", msg.dump().c_str()); +#endif + this->on_infrared_rf_transmit_raw_timings_request(msg); + break; + } #endif default: break; @@ -819,6 +830,11 @@ void APIServerConnection::on_z_wave_proxy_frame(const ZWaveProxyFrame &msg) { th #ifdef USE_ZWAVE_PROXY void APIServerConnection::on_z_wave_proxy_request(const ZWaveProxyRequest &msg) { this->zwave_proxy_request(msg); } #endif +#ifdef USE_IR_RF +void APIServerConnection::on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) { + this->infrared_rf_transmit_raw_timings(msg); +} +#endif void APIServerConnection::read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) { // Check authentication/connection requirements for messages diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index e2a23827dc9..4bd6a7b6a40 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -217,6 +217,11 @@ class APIServerConnectionBase : public ProtoService { #ifdef USE_ZWAVE_PROXY virtual void on_z_wave_proxy_request(const ZWaveProxyRequest &value){}; #endif + +#ifdef USE_IR_RF + virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; +#endif + protected: void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; @@ -347,6 +352,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_ZWAVE_PROXY virtual void zwave_proxy_request(const ZWaveProxyRequest &msg) = 0; +#endif +#ifdef USE_IR_RF + virtual void infrared_rf_transmit_raw_timings(const InfraredRFTransmitRawTimingsRequest &msg) = 0; #endif protected: void on_hello_request(const HelloRequest &msg) override; @@ -473,6 +481,9 @@ class APIServerConnection : public APIServerConnectionBase { #endif #ifdef USE_ZWAVE_PROXY void on_z_wave_proxy_request(const ZWaveProxyRequest &msg) override; +#endif +#ifdef USE_IR_RF + void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &msg) override; #endif void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 336672f50b9..949262098ff 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -347,6 +347,21 @@ void APIServer::on_zwave_proxy_request(const esphome::api::ProtoMessage &msg) { } #endif +#ifdef USE_IR_RF +void APIServer::send_infrared_rf_receive_event([[maybe_unused]] uint32_t device_id, uint32_t key, + const std::vector *timings) { + InfraredRFReceiveEvent resp{}; +#ifdef USE_DEVICES + resp.device_id = device_id; +#endif + resp.key = key; + resp.timings = timings; + + for (auto &c : this->clients_) + c->send_infrared_rf_receive_event(resp); +} +#endif + #ifdef USE_ALARM_CONTROL_PANEL API_DISPATCH_UPDATE(alarm_control_panel::AlarmControlPanel, alarm_control_panel) #endif diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index f5b57f994af..93421ef801f 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -185,6 +185,9 @@ class APIServer : public Component, #ifdef USE_ZWAVE_PROXY void on_zwave_proxy_request(const esphome::api::ProtoMessage &msg); #endif +#ifdef USE_IR_RF + void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector *timings); +#endif bool is_connected(bool state_subscription_only = false) const; diff --git a/esphome/components/api/list_entities.cpp b/esphome/components/api/list_entities.cpp index 2470899c93e..fe43a47c3b7 100644 --- a/esphome/components/api/list_entities.cpp +++ b/esphome/components/api/list_entities.cpp @@ -76,6 +76,9 @@ LIST_ENTITIES_HANDLER(alarm_control_panel, alarm_control_panel::AlarmControlPane #ifdef USE_WATER_HEATER LIST_ENTITIES_HANDLER(water_heater, water_heater::WaterHeater, ListEntitiesWaterHeaterResponse) #endif +#ifdef USE_INFRARED +LIST_ENTITIES_HANDLER(infrared, infrared::Infrared, ListEntitiesInfraredResponse) +#endif #ifdef USE_EVENT LIST_ENTITIES_HANDLER(event, event::Event, ListEntitiesEventResponse) #endif diff --git a/esphome/components/api/list_entities.h b/esphome/components/api/list_entities.h index 04e6525eb0a..912aab72b29 100644 --- a/esphome/components/api/list_entities.h +++ b/esphome/components/api/list_entities.h @@ -85,6 +85,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_WATER_HEATER bool on_water_heater(water_heater::WaterHeater *entity) override; #endif +#ifdef USE_INFRARED + bool on_infrared(infrared::Infrared *entity) override; +#endif #ifdef USE_EVENT bool on_event(event::Event *entity) override; #endif diff --git a/esphome/components/api/subscribe_state.h b/esphome/components/api/subscribe_state.h index 9230000acef..3c9f33835a5 100644 --- a/esphome/components/api/subscribe_state.h +++ b/esphome/components/api/subscribe_state.h @@ -79,6 +79,9 @@ class InitialStateIterator : public ComponentIterator { #ifdef USE_WATER_HEATER bool on_water_heater(water_heater::WaterHeater *entity) override; #endif +#ifdef USE_INFRARED + bool on_infrared(infrared::Infrared *infrared) override { return true; }; +#endif #ifdef USE_EVENT bool on_event(event::Event *event) override { return true; }; #endif diff --git a/esphome/components/infrared/__init__.py b/esphome/components/infrared/__init__.py new file mode 100644 index 00000000000..5c759d6fd9c --- /dev/null +++ b/esphome/components/infrared/__init__.py @@ -0,0 +1,76 @@ +""" +Infrared component for ESPHome. + +WARNING: This component is EXPERIMENTAL. The API (both Python configuration +and C++ interfaces) may change at any time without following the normal +breaking changes policy. Use at your own risk. + +Once the API is considered stable, this warning will be removed. +""" + +import esphome.codegen as cg +import esphome.config_validation as cv +from esphome.const import CONF_ID +from esphome.core import CORE, coroutine_with_priority +from esphome.core.entity_helpers import setup_entity +from esphome.coroutine import CoroPriority +from esphome.types import ConfigType + +CODEOWNERS = ["@kbx81"] +AUTO_LOAD = ["remote_base"] + +IS_PLATFORM_COMPONENT = True + +infrared_ns = cg.esphome_ns.namespace("infrared") +Infrared = infrared_ns.class_("Infrared", cg.EntityBase, cg.Component) +InfraredCall = infrared_ns.class_("InfraredCall") +InfraredTraits = infrared_ns.class_("InfraredTraits") + +CONF_INFRARED_ID = "infrared_id" +CONF_SUPPORTS_TRANSMITTER = "supports_transmitter" +CONF_SUPPORTS_RECEIVER = "supports_receiver" + + +def infrared_schema(class_: type[cg.MockObjClass]) -> cv.Schema: + """Create a schema for an infrared platform. + + :param class_: The infrared class to use for this schema. + :return: An extended schema for infrared configuration. + """ + entity_schema = cv.ENTITY_BASE_SCHEMA.extend(cv.COMPONENT_SCHEMA) + return entity_schema.extend( + { + cv.GenerateID(): cv.declare_id(class_), + } + ) + + +async def setup_infrared_core_(var: cg.Pvariable, config: ConfigType) -> None: + """Set up core infrared configuration.""" + await setup_entity(var, config, "infrared") + + +async def register_infrared(var: cg.Pvariable, config: ConfigType) -> None: + """Register an infrared device with the core.""" + cg.add_define("USE_IR_RF") + await cg.register_component(var, config) + await setup_infrared_core_(var, config) + cg.add(cg.App.register_infrared(var)) + CORE.register_platform_component("infrared", var) + + +async def new_infrared(config: ConfigType, *args) -> cg.Pvariable: + """Create a new Infrared instance. + + :param config: Configuration dictionary. + :param args: Additional arguments to pass to new_Pvariable. + :return: The created Infrared instance. + """ + var = cg.new_Pvariable(config[CONF_ID], *args) + await register_infrared(var, config) + return var + + +@coroutine_with_priority(CoroPriority.CORE) +async def to_code(config: ConfigType) -> None: + cg.add_global(infrared_ns.using) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp new file mode 100644 index 00000000000..384ff431a54 --- /dev/null +++ b/esphome/components/infrared/infrared.cpp @@ -0,0 +1,150 @@ +#include "infrared.h" +#include "esphome/core/log.h" + +#ifdef USE_API +#include "esphome/components/api/api_server.h" +#endif + +namespace esphome::infrared { + +static const char *const TAG = "infrared"; + +// ========== InfraredCall ========== + +InfraredCall &InfraredCall::set_carrier_frequency(uint32_t frequency) { + this->carrier_frequency_ = frequency; + return *this; +} + +InfraredCall &InfraredCall::set_raw_timings(const std::vector &timings) { + this->raw_timings_ = &timings; + this->packed_data_ = nullptr; // Clear packed if vector is set + return *this; +} + +InfraredCall &InfraredCall::set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count) { + this->packed_data_ = data; + this->packed_length_ = length; + this->packed_count_ = count; + this->raw_timings_ = nullptr; // Clear vector if packed is set + return *this; +} + +InfraredCall &InfraredCall::set_repeat_count(uint32_t count) { + this->repeat_count_ = count; + return *this; +} + +void InfraredCall::perform() { + if (this->parent_ != nullptr) { + this->parent_->control(*this); + } +} + +// ========== Infrared ========== + +void Infrared::setup() { + // Set up traits based on configuration + this->traits_.set_supports_transmitter(this->has_transmitter()); + this->traits_.set_supports_receiver(this->has_receiver()); + + // Register as listener for received IR data + if (this->receiver_ != nullptr) { + this->receiver_->register_listener(this); + } +} + +void Infrared::dump_config() { + ESP_LOGCONFIG(TAG, + "Infrared '%s'\n" + " Supports Transmitter: %s\n" + " Supports Receiver: %s", + this->get_name().c_str(), YESNO(this->traits_.get_supports_transmitter()), + YESNO(this->traits_.get_supports_receiver())); +} + +InfraredCall Infrared::make_call() { return InfraredCall(this); } + +void Infrared::control(const InfraredCall &call) { + if (this->transmitter_ == nullptr) { + ESP_LOGW(TAG, "No transmitter configured"); + return; + } + + if (!call.has_raw_timings()) { + ESP_LOGE(TAG, "No raw timings provided"); + return; + } + + // Create transmit data object + auto transmit_call = this->transmitter_->transmit(); + auto *transmit_data = transmit_call.get_data(); + + // Set carrier frequency + if (call.get_carrier_frequency().has_value()) { + transmit_data->set_carrier_frequency(call.get_carrier_frequency().value()); + } + + // Set timings based on format + if (call.is_packed()) { + // Zero-copy from packed protobuf data + ESP_LOGD(TAG, "Transmitting raw timings: timing_count=%u, repeat_count=%u", call.get_packed_count(), + call.get_repeat_count()); + transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(), + call.get_packed_count()); + } else { + // From vector (lambdas/automations) + const auto &timings = call.get_raw_timings(); + if (timings.empty()) { + ESP_LOGE(TAG, "Raw timings array is empty"); + return; + } + ESP_LOGD(TAG, "Transmitting raw timings: timing_count=%zu, repeat_count=%u", timings.size(), + call.get_repeat_count()); + // Timings format: positive values = mark (LED on), negative values = space (LED off) + for (const auto &timing : timings) { + if (timing > 0) { + transmit_data->mark(static_cast(timing)); + } else { + transmit_data->space(static_cast(-timing)); + } + } + } + + // Set repeat count + if (call.get_repeat_count() > 0) { + transmit_call.set_send_times(call.get_repeat_count()); + } + + // Perform transmission + transmit_call.perform(); +} + +uint32_t Infrared::get_capability_flags() const { + uint32_t flags = 0; + + // Add transmit/receive capability based on traits + if (this->traits_.get_supports_transmitter()) + flags |= InfraredCapability::CAPABILITY_TRANSMITTER; + if (this->traits_.get_supports_receiver()) + flags |= InfraredCapability::CAPABILITY_RECEIVER; + + return flags; +} + +bool Infrared::on_receive(remote_base::RemoteReceiveData data) { + // Forward received IR data to API server +#if defined(USE_API) && defined(USE_IR_RF) + if (api::global_api_server != nullptr) { +#ifdef USE_DEVICES + uint32_t device_id = this->get_device_id(); +#else + uint32_t device_id = 0; +#endif + api::global_api_server->send_infrared_rf_receive_event(device_id, this->get_object_id_hash(), &data.get_raw_data()); + } +#endif + return false; // Don't consume the event, allow other listeners to process it +} + +} // namespace esphome::infrared diff --git a/esphome/components/infrared/infrared.h b/esphome/components/infrared/infrared.h new file mode 100644 index 00000000000..3a891301f4d --- /dev/null +++ b/esphome/components/infrared/infrared.h @@ -0,0 +1,130 @@ +#pragma once + +// WARNING: This component is EXPERIMENTAL. The API may change at any time +// without following the normal breaking changes policy. Use at your own risk. +// Once the API is considered stable, this warning will be removed. + +#include "esphome/core/component.h" +#include "esphome/core/entity_base.h" +#include "esphome/components/remote_base/remote_base.h" + +#include + +namespace esphome::infrared { + +/// Capability flags for individual infrared instances +enum InfraredCapability : uint32_t { + CAPABILITY_TRANSMITTER = 1 << 0, // Can transmit signals + CAPABILITY_RECEIVER = 1 << 1, // Can receive signals +}; + +/// Forward declarations +class Infrared; + +/// InfraredCall - Builder pattern for transmitting infrared signals +class InfraredCall { + public: + explicit InfraredCall(Infrared *parent) : parent_(parent) {} + + /// Set the carrier frequency in Hz + InfraredCall &set_carrier_frequency(uint32_t frequency); + /// Set the raw timings (positive = mark, negative = space) + /// Note: The timings vector must outlive the InfraredCall (zero-copy reference) + InfraredCall &set_raw_timings(const std::vector &timings); + /// Set the raw timings from packed protobuf sint32 data (zero-copy from wire) + /// Note: The data must outlive the InfraredCall + InfraredCall &set_raw_timings_packed(const uint8_t *data, uint16_t length, uint16_t count); + /// Set the number of times to repeat transmission (1 = transmit once, 2 = transmit twice, etc.) + InfraredCall &set_repeat_count(uint32_t count); + + /// Perform the transmission + void perform(); + + /// Get the carrier frequency + const optional &get_carrier_frequency() const { return this->carrier_frequency_; } + /// Get the raw timings (only valid if set via set_raw_timings, not packed) + const std::vector &get_raw_timings() const { return *this->raw_timings_; } + /// Check if raw timings have been set (either vector or packed) + bool has_raw_timings() const { return this->raw_timings_ != nullptr || this->packed_data_ != nullptr; } + /// Check if using packed data format + bool is_packed() const { return this->packed_data_ != nullptr; } + /// Get packed data (only valid if set via set_raw_timings_packed) + const uint8_t *get_packed_data() const { return this->packed_data_; } + uint16_t get_packed_length() const { return this->packed_length_; } + uint16_t get_packed_count() const { return this->packed_count_; } + /// Get the repeat count + uint32_t get_repeat_count() const { return this->repeat_count_; } + + protected: + uint32_t repeat_count_{1}; + Infrared *parent_; + optional carrier_frequency_; + // Vector-based timings (for lambdas/automations) + const std::vector *raw_timings_{nullptr}; + // Packed protobuf timings (for API zero-copy) + const uint8_t *packed_data_{nullptr}; + uint16_t packed_length_{0}; + uint16_t packed_count_{0}; +}; + +/// InfraredTraits - Describes the capabilities of an infrared implementation +class InfraredTraits { + public: + bool get_supports_transmitter() const { return this->supports_transmitter_; } + void set_supports_transmitter(bool supports) { this->supports_transmitter_ = supports; } + + bool get_supports_receiver() const { return this->supports_receiver_; } + void set_supports_receiver(bool supports) { this->supports_receiver_ = supports; } + + protected: + bool supports_transmitter_{false}; + bool supports_receiver_{false}; +}; + +/// Infrared - Base class for infrared remote control implementations +class Infrared : public Component, public EntityBase, public remote_base::RemoteReceiverListener { + public: + Infrared() = default; + + void setup() override; + void dump_config() override; + float get_setup_priority() const override { return setup_priority::AFTER_CONNECTION; } + + /// Set the remote receiver component + void set_receiver(remote_base::RemoteReceiverBase *receiver) { this->receiver_ = receiver; } + /// Set the remote transmitter component + void set_transmitter(remote_base::RemoteTransmitterBase *transmitter) { this->transmitter_ = transmitter; } + + /// Check if this infrared has a transmitter configured + bool has_transmitter() const { return this->transmitter_ != nullptr; } + /// Check if this infrared has a receiver configured + bool has_receiver() const { return this->receiver_ != nullptr; } + + /// Get the traits for this infrared implementation + InfraredTraits &get_traits() { return this->traits_; } + const InfraredTraits &get_traits() const { return this->traits_; } + + /// Create a call object for transmitting + InfraredCall make_call(); + + /// Get capability flags for this infrared instance + uint32_t get_capability_flags() const; + + /// Called when IR data is received (from RemoteReceiverListener) + bool on_receive(remote_base::RemoteReceiveData data) override; + + protected: + friend class InfraredCall; + + /// Perform the actual transmission (called by InfraredCall) + virtual void control(const InfraredCall &call); + + // Underlying hardware components + remote_base::RemoteReceiverBase *receiver_{nullptr}; + remote_base::RemoteTransmitterBase *transmitter_{nullptr}; + + // Traits describing capabilities + InfraredTraits traits_; +}; + +} // namespace esphome::infrared diff --git a/esphome/components/web_server/list_entities.cpp b/esphome/components/web_server/list_entities.cpp index 1e852f6a961..0af95213261 100644 --- a/esphome/components/web_server/list_entities.cpp +++ b/esphome/components/web_server/list_entities.cpp @@ -141,6 +141,13 @@ bool ListEntitiesIterator::on_water_heater(water_heater::WaterHeater *obj) { } #endif +#ifdef USE_INFRARED +bool ListEntitiesIterator::on_infrared(infrared::Infrared *obj) { + // Infrared web_server support not yet implemented - this stub acknowledges the entity + return true; +} +#endif + #ifdef USE_EVENT bool ListEntitiesIterator::on_event(event::Event *obj) { // Null event type, since we are just iterating over entities diff --git a/esphome/components/web_server/list_entities.h b/esphome/components/web_server/list_entities.h index 56fd91a8c62..d0a4fa27256 100644 --- a/esphome/components/web_server/list_entities.h +++ b/esphome/components/web_server/list_entities.h @@ -82,6 +82,9 @@ class ListEntitiesIterator : public ComponentIterator { #ifdef USE_WATER_HEATER bool on_water_heater(water_heater::WaterHeater *obj) override; #endif +#ifdef USE_INFRARED + bool on_infrared(infrared::Infrared *obj) override; +#endif #ifdef USE_EVENT bool on_event(event::Event *obj) override; #endif diff --git a/esphome/core/application.h b/esphome/core/application.h index 13461b3ebd6..592bf809f1d 100644 --- a/esphome/core/application.h +++ b/esphome/core/application.h @@ -91,6 +91,9 @@ #ifdef USE_WATER_HEATER #include "esphome/components/water_heater/water_heater.h" #endif +#ifdef USE_INFRARED +#include "esphome/components/infrared/infrared.h" +#endif #ifdef USE_EVENT #include "esphome/components/event/event.h" #endif @@ -223,6 +226,10 @@ class Application { void register_water_heater(water_heater::WaterHeater *water_heater) { this->water_heaters_.push_back(water_heater); } #endif +#ifdef USE_INFRARED + void register_infrared(infrared::Infrared *infrared) { this->infrareds_.push_back(infrared); } +#endif + #ifdef USE_EVENT void register_event(event::Event *event) { this->events_.push_back(event); } #endif @@ -457,6 +464,11 @@ class Application { GET_ENTITY_METHOD(water_heater::WaterHeater, water_heater, water_heaters) #endif +#ifdef USE_INFRARED + auto &get_infrareds() const { return this->infrareds_; } + GET_ENTITY_METHOD(infrared::Infrared, infrared, infrareds) +#endif + #ifdef USE_EVENT auto &get_events() const { return this->events_; } GET_ENTITY_METHOD(event::Event, event, events) @@ -656,6 +668,9 @@ class Application { #ifdef USE_WATER_HEATER StaticVector water_heaters_{}; #endif +#ifdef USE_INFRARED + StaticVector infrareds_{}; +#endif #ifdef USE_UPDATE StaticVector updates_{}; #endif diff --git a/esphome/core/component_iterator.cpp b/esphome/core/component_iterator.cpp index 4015d8ec604..ff76b2b81bf 100644 --- a/esphome/core/component_iterator.cpp +++ b/esphome/core/component_iterator.cpp @@ -169,6 +169,12 @@ void ComponentIterator::advance() { break; #endif +#ifdef USE_INFRARED + case IteratorState::INFRARED: + this->process_platform_item_(App.get_infrareds(), &ComponentIterator::on_infrared); + break; +#endif + #ifdef USE_EVENT case IteratorState::EVENT: this->process_platform_item_(App.get_events(), &ComponentIterator::on_event); diff --git a/esphome/core/component_iterator.h b/esphome/core/component_iterator.h index 37d19606015..e13d81a8e4f 100644 --- a/esphome/core/component_iterator.h +++ b/esphome/core/component_iterator.h @@ -16,6 +16,12 @@ class UserServiceDescriptor; } // namespace api #endif +#ifdef USE_INFRARED +namespace infrared { +class Infrared; +} // namespace infrared +#endif + class ComponentIterator { public: void begin(bool include_internal = false); @@ -87,6 +93,9 @@ class ComponentIterator { #ifdef USE_WATER_HEATER virtual bool on_water_heater(water_heater::WaterHeater *water_heater) = 0; #endif +#ifdef USE_INFRARED + virtual bool on_infrared(infrared::Infrared *infrared) = 0; +#endif #ifdef USE_EVENT virtual bool on_event(event::Event *event) = 0; #endif @@ -167,6 +176,9 @@ class ComponentIterator { #ifdef USE_WATER_HEATER WATER_HEATER, #endif +#ifdef USE_INFRARED + INFRARED, +#endif #ifdef USE_EVENT EVENT, #endif diff --git a/esphome/core/defines.h b/esphome/core/defines.h index ed5f152e9f2..633b0c6c5e4 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -49,6 +49,8 @@ #define USE_HTTP_REQUEST_OTA_WATCHDOG_TIMEOUT 8000 // NOLINT #define USE_IMAGE #define USE_IMPROV_SERIAL_NEXT_URL +#define USE_INFRARED +#define USE_IR_RF #define USE_JSON #define USE_LIGHT #define USE_LOCK @@ -321,9 +323,9 @@ // Default counts for static analysis #define CONTROLLER_REGISTRY_MAX 2 +#define ESPHOME_AREA_COUNT 10 #define ESPHOME_COMPONENT_COUNT 50 #define ESPHOME_DEVICE_COUNT 10 -#define ESPHOME_AREA_COUNT 10 #define ESPHOME_ENTITY_ALARM_CONTROL_PANEL_COUNT 1 #define ESPHOME_ENTITY_BINARY_SENSOR_COUNT 1 #define ESPHOME_ENTITY_BUTTON_COUNT 1 @@ -333,6 +335,7 @@ #define ESPHOME_ENTITY_DATETIME_COUNT 1 #define ESPHOME_ENTITY_EVENT_COUNT 1 #define ESPHOME_ENTITY_FAN_COUNT 1 +#define ESPHOME_ENTITY_INFRARED_COUNT 1 #define ESPHOME_ENTITY_LIGHT_COUNT 1 #define ESPHOME_ENTITY_LOCK_COUNT 1 #define ESPHOME_ENTITY_MEDIA_PLAYER_COUNT 1 diff --git a/tests/components/web_server/common.yaml b/tests/components/web_server/common.yaml index 82307c189c7..35a605484c1 100644 --- a/tests/components/web_server/common.yaml +++ b/tests/components/web_server/common.yaml @@ -37,3 +37,4 @@ datetime: event: update: water_heater: +infrared: From 29cef3bc5d8f8cc01da2a1db17f936649667790d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 11 Jan 2026 19:26:40 -1000 Subject: [PATCH 03/20] Bump aioesphomeapi from 43.12.0 to 43.13.0 (#13160) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d3bb5b5dc50..9994148cf6d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.18 # When updating platformio, also update /docker/Dockerfile esptool==5.1.0 click==8.1.7 esphome-dashboard==20260110.0 -aioesphomeapi==43.12.0 +aioesphomeapi==43.13.0 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 6c68ebe86e8c6a0f717ea7ed3b4056c16fb98ab1 Mon Sep 17 00:00:00 2001 From: Jas Strong Date: Mon, 12 Jan 2026 06:25:43 -0800 Subject: [PATCH 04/20] [rd03d] Filter targets with sentinel speed values (#13146) Co-authored-by: jas --- esphome/components/rd03d/rd03d.cpp | 50 +++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index 44e479c153f..d9b0b59fe9f 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -21,6 +21,11 @@ static constexpr uint8_t CMD_FRAME_FOOTER[] = {0x04, 0x03, 0x02, 0x01}; static constexpr uint16_t CMD_SINGLE_TARGET = 0x0080; static constexpr uint16_t CMD_MULTI_TARGET = 0x0090; +// Speed sentinel values (cm/s) - radar outputs these when no valid Doppler measurement +// FMCW radars detect motion via Doppler shift; targets with these speeds are likely noise +static constexpr int16_t SPEED_SENTINEL_248 = 248; +static constexpr int16_t SPEED_SENTINEL_256 = 256; + // Decode coordinate/speed value from RD-03D format // Per datasheet: MSB=1 means positive, MSB=0 means negative static constexpr int16_t decode_value(uint8_t low_byte, uint8_t high_byte) { @@ -31,6 +36,13 @@ static constexpr int16_t decode_value(uint8_t low_byte, uint8_t high_byte) { return value; } +// Check if speed value indicates a valid Doppler measurement +// Zero, ±248, or ±256 cm/s are sentinel values from the radar firmware +static constexpr bool is_speed_valid(int16_t speed) { + int16_t abs_speed = speed < 0 ? -speed : speed; + return speed != 0 && abs_speed != SPEED_SENTINEL_248 && abs_speed != SPEED_SENTINEL_256; +} + void RD03DComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up RD-03D..."); this->set_timeout(SETUP_TIMEOUT_MS, [this]() { this->apply_config_(); }); @@ -136,8 +148,12 @@ void RD03DComponent::process_frame_() { int16_t speed = decode_value(speed_low, speed_high); uint16_t resolution = (res_high << 8) | res_low; - // Check if target is present (non-zero coordinates) - bool target_present = (x != 0 || y != 0); + // Check if target is present + // Requires non-zero coordinates AND valid speed (not a sentinel value) + // FMCW radars detect motion via Doppler; sentinel speed indicates no real target + bool has_position = (x != 0 || y != 0); + bool has_valid_speed = is_speed_valid(speed); + bool target_present = has_position && has_valid_speed; if (target_present) { target_count++; } @@ -169,20 +185,21 @@ void RD03DComponent::process_frame_() { #ifdef USE_SENSOR void RD03DComponent::publish_target_(uint8_t target_num, int16_t x, int16_t y, int16_t speed, uint16_t resolution) { TargetSensor &target = this->targets_[target_num]; + bool valid = is_speed_valid(speed); - // Publish X coordinate (mm) + // Publish X coordinate (mm) - NaN if target invalid if (target.x != nullptr) { - target.x->publish_state(x); + target.x->publish_state(valid ? static_cast(x) : NAN); } - // Publish Y coordinate (mm) + // Publish Y coordinate (mm) - NaN if target invalid if (target.y != nullptr) { - target.y->publish_state(y); + target.y->publish_state(valid ? static_cast(y) : NAN); } - // Publish speed (convert from cm/s to mm/s) + // Publish speed (convert from cm/s to mm/s) - NaN if target invalid if (target.speed != nullptr) { - target.speed->publish_state(static_cast(speed) * 10.0f); + target.speed->publish_state(valid ? static_cast(speed) * 10.0f : NAN); } // Publish resolution (mm) @@ -190,20 +207,23 @@ void RD03DComponent::publish_target_(uint8_t target_num, int16_t x, int16_t y, i target.resolution->publish_state(resolution); } - // Calculate and publish distance (mm) + // Calculate and publish distance (mm) - NaN if target invalid if (target.distance != nullptr) { - float distance = std::hypot(static_cast(x), static_cast(y)); - target.distance->publish_state(distance); + if (valid) { + target.distance->publish_state(std::hypot(static_cast(x), static_cast(y))); + } else { + target.distance->publish_state(NAN); + } } - // Calculate and publish angle (degrees) + // Calculate and publish angle (degrees) - NaN if target invalid // Angle is measured from the Y axis (radar forward direction) if (target.angle != nullptr) { - if (x == 0 && y == 0) { - target.angle->publish_state(0); - } else { + if (valid) { float angle = std::atan2(static_cast(x), static_cast(y)) * 180.0f / M_PI; target.angle->publish_state(angle); + } else { + target.angle->publish_state(NAN); } } } From 353daa97d0f6f9468bbfba238c4f79493ad59992 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 12 Jan 2026 15:45:15 +0100 Subject: [PATCH 05/20] [nrf52,zigbee] Warning if spaces in description (#13114) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/zigbee/__init__.py | 11 ++++++++++- esphome/components/zigbee/zigbee_zephyr.py | 2 ++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/esphome/components/zigbee/__init__.py b/esphome/components/zigbee/__init__.py index cf37e890c46..e3631649974 100644 --- a/esphome/components/zigbee/__init__.py +++ b/esphome/components/zigbee/__init__.py @@ -1,3 +1,4 @@ +import logging from typing import Any from esphome import automation, core @@ -6,7 +7,7 @@ from esphome.components.nrf52.boards import BOOTLOADER_CONFIG, Section from esphome.components.zephyr import zephyr_add_pm_static, zephyr_data from esphome.components.zephyr.const import KEY_BOOTLOADER import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_INTERNAL +from esphome.const import CONF_ID, CONF_INTERNAL, CONF_NAME from esphome.core import CORE from esphome.types import ConfigType @@ -24,6 +25,8 @@ from .const_zephyr import ( ) from .zigbee_zephyr import zephyr_binary_sensor, zephyr_sensor +_LOGGER = logging.getLogger(__name__) + CODEOWNERS = ["@tomaszduda23"] @@ -107,6 +110,12 @@ async def setup_sensor(entity: cg.MockObj, config: ConfigType) -> None: def consume_endpoint(config: ConfigType) -> ConfigType: if not config.get(CONF_ZIGBEE_ID) or config.get(CONF_INTERNAL): return config + if " " in config[CONF_NAME]: + _LOGGER.warning( + "Spaces in '%s' work with ZHA but not Zigbee2MQTT. For Zigbee2MQTT use '%s'", + config[CONF_NAME], + config[CONF_NAME].replace(" ", "_"), + ) data: dict[str, Any] = CORE.data.setdefault(KEY_ZIGBEE, {}) slots: list[str] = data.setdefault(KEY_EP_NUMBER, []) slots.extend([""]) diff --git a/esphome/components/zigbee/zigbee_zephyr.py b/esphome/components/zigbee/zigbee_zephyr.py index d8a2716603c..71ea0da6a7b 100644 --- a/esphome/components/zigbee/zigbee_zephyr.py +++ b/esphome/components/zigbee/zigbee_zephyr.py @@ -212,6 +212,8 @@ def zigbee_assign(target: cg.MockObj, expression: cg.RawExpression | int) -> str def zigbee_set_string(target: cg.MockObj, value: str) -> str: """Set a ZCL string value and return the target name (arrays decay to pointers).""" + # Zigbee supports only ASCII + value = value.encode("ascii", "ignore").decode() cg.add( cg.RawExpression( f"ZB_ZCL_SET_STRING_VAL({target}, {cg.safe_exp(value)}, ZB_ZCL_STRING_CONST_SIZE({cg.safe_exp(value)}))" From 7ea6bcef88c55631aff114bad0214d250c7cb3a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jan 2026 07:37:58 -1000 Subject: [PATCH 06/20] [api] Use stack buffer for bytes field dumping in proto message logs (#13162) --- esphome/components/api/api_pb2_dump.cpp | 62 +++++++++---------------- script/api_protobuf/api_protobuf.py | 52 ++++++++++++++++----- 2 files changed, 64 insertions(+), 50 deletions(-) diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 8e4d55d11b5..9550ecbcdd7 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -100,6 +100,16 @@ template static void dump_field(std::string &out, const char *field_ out.append("\n"); } +// Helper for bytes fields - uses stack buffer to avoid heap allocation +// Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer +static void dump_bytes_field(std::string &out, const char *field_name, const uint8_t *data, size_t len, + int indent = 2) { + char hex_buf[format_hex_pretty_size(160)]; + append_field_prefix(out, field_name, indent); + format_hex_pretty_to(hex_buf, data, len); + append_with_newline(out, hex_buf); +} + template<> const char *proto_enum_to_string(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: @@ -1127,16 +1137,12 @@ void SubscribeLogsRequest::dump_to(std::string &out) const { void SubscribeLogsResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "SubscribeLogsResponse"); dump_field(out, "level", static_cast(this->level)); - out.append(" message: "); - out.append(format_hex_pretty(this->message_ptr_, this->message_len_)); - out.append("\n"); + dump_bytes_field(out, "message", this->message_ptr_, this->message_len_); } #ifdef USE_API_NOISE void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest"); - out.append(" key: "); - out.append(format_hex_pretty(this->key, this->key_len)); - out.append("\n"); + dump_bytes_field(out, "key", this->key, this->key_len); } void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "NoiseEncryptionSetKeyResponse"); @@ -1189,9 +1195,7 @@ void HomeassistantActionResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); dump_field(out, "error_message", this->error_message); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON - out.append(" response_data: "); - out.append(format_hex_pretty(this->response_data, this->response_data_len)); - out.append("\n"); + dump_bytes_field(out, "response_data", this->response_data, this->response_data_len); #endif } #endif @@ -1278,9 +1282,7 @@ void ExecuteServiceResponse::dump_to(std::string &out) const { dump_field(out, "success", this->success); dump_field(out, "error_message", this->error_message); #ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON - out.append(" response_data: "); - out.append(format_hex_pretty(this->response_data, this->response_data_len)); - out.append("\n"); + dump_bytes_field(out, "response_data", this->response_data, this->response_data_len); #endif } #endif @@ -1302,9 +1304,7 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const { void CameraImageResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "CameraImageResponse"); dump_field(out, "key", this->key); - out.append(" data: "); - out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); - out.append("\n"); + dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); dump_field(out, "done", this->done); #ifdef USE_DEVICES dump_field(out, "device_id", this->device_id); @@ -1705,9 +1705,7 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const { dump_field(out, "address", this->address); dump_field(out, "rssi", this->rssi); dump_field(out, "address_type", this->address_type); - out.append(" data: "); - out.append(format_hex_pretty(this->data, this->data_len)); - out.append("\n"); + dump_bytes_field(out, "data", this->data, this->data_len); } void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse"); @@ -1792,18 +1790,14 @@ void BluetoothGATTReadResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothGATTReadResponse"); dump_field(out, "address", this->address); dump_field(out, "handle", this->handle); - out.append(" data: "); - out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); - out.append("\n"); + dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); } void BluetoothGATTWriteRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothGATTWriteRequest"); dump_field(out, "address", this->address); dump_field(out, "handle", this->handle); dump_field(out, "response", this->response); - out.append(" data: "); - out.append(format_hex_pretty(this->data, this->data_len)); - out.append("\n"); + dump_bytes_field(out, "data", this->data, this->data_len); } void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothGATTReadDescriptorRequest"); @@ -1814,9 +1808,7 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothGATTWriteDescriptorRequest"); dump_field(out, "address", this->address); dump_field(out, "handle", this->handle); - out.append(" data: "); - out.append(format_hex_pretty(this->data, this->data_len)); - out.append("\n"); + dump_bytes_field(out, "data", this->data, this->data_len); } void BluetoothGATTNotifyRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothGATTNotifyRequest"); @@ -1828,9 +1820,7 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { MessageDumpHelper helper(out, "BluetoothGATTNotifyDataResponse"); dump_field(out, "address", this->address); dump_field(out, "handle", this->handle); - out.append(" data: "); - out.append(format_hex_pretty(this->data_ptr_, this->data_len_)); - out.append("\n"); + dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); } void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const { out.append("SubscribeBluetoothConnectionsFreeRequest {}"); @@ -1934,9 +1924,7 @@ void VoiceAssistantEventResponse::dump_to(std::string &out) const { } void VoiceAssistantAudio::dump_to(std::string &out) const { MessageDumpHelper helper(out, "VoiceAssistantAudio"); - out.append(" data: "); - out.append(format_hex_pretty(this->data, this->data_len)); - out.append("\n"); + dump_bytes_field(out, "data", this->data, this->data_len); dump_field(out, "end", this->end); } void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const { @@ -2297,16 +2285,12 @@ void UpdateCommandRequest::dump_to(std::string &out) const { #ifdef USE_ZWAVE_PROXY void ZWaveProxyFrame::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ZWaveProxyFrame"); - out.append(" data: "); - out.append(format_hex_pretty(this->data, this->data_len)); - out.append("\n"); + dump_bytes_field(out, "data", this->data, this->data_len); } void ZWaveProxyRequest::dump_to(std::string &out) const { MessageDumpHelper helper(out, "ZWaveProxyRequest"); dump_field(out, "type", static_cast(this->type)); - out.append(" data: "); - out.append(format_hex_pretty(this->data, this->data_len)); - out.append("\n"); + dump_bytes_field(out, "data", this->data, this->data_len); } #endif #ifdef USE_INFRARED diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 118c87356ee..a10a9121869 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -786,10 +786,32 @@ class BytesType(TypeInfo): @property def dump_content(self) -> str: - o = f'out.append(" {self.name}: ");\n' - o += self.dump(f"this->{self.field_name}") + "\n" - o += 'out.append("\\n");' - return o + # For SOURCE_CLIENT only, always use std::string + if not self._needs_encode: + return ( + f'dump_bytes_field(out, "{self.name}", ' + f"reinterpret_cast(this->{self.field_name}.data()), " + f"this->{self.field_name}.size());" + ) + + # For SOURCE_SERVER, always use pointer/length + if not self._needs_decode: + return ( + f'dump_bytes_field(out, "{self.name}", ' + f"this->{self.field_name}_ptr_, this->{self.field_name}_len_);" + ) + + # For SOURCE_BOTH, check if pointer is set (sending) or use string (received) + return ( + f"if (this->{self.field_name}_ptr_ != nullptr) {{\n" + f' dump_bytes_field(out, "{self.name}", ' + f"this->{self.field_name}_ptr_, this->{self.field_name}_len_);\n" + f"}} else {{\n" + f' dump_bytes_field(out, "{self.name}", ' + f"reinterpret_cast(this->{self.field_name}.data()), " + f"this->{self.field_name}.size());\n" + f"}}" + ) def get_size_calculation(self, name: str, force: bool = False) -> str: return f"size.add_length({self.calculate_field_id_size()}, this->{self.field_name}_len_);" @@ -862,9 +884,8 @@ class PointerToBytesBufferType(PointerToBufferTypeBase): @property def dump_content(self) -> str: return ( - f'out.append(" {self.name}: ");\n' - + f"out.append({self.dump(self.field_name)});\n" - + 'out.append("\\n");' + f'dump_bytes_field(out, "{self.name}", ' + f"this->{self.field_name}, this->{self.field_name}_len);" ) def get_size_calculation(self, name: str, force: bool = False) -> str: @@ -1062,10 +1083,10 @@ class FixedArrayBytesType(TypeInfo): @property def dump_content(self) -> str: - o = f'out.append(" {self.name}: ");\n' - o += f"out.append(format_hex_pretty(this->{self.field_name}, this->{self.field_name}_len));\n" - o += 'out.append("\\n");' - return o + return ( + f'dump_bytes_field(out, "{self.name}", ' + f"this->{self.field_name}, this->{self.field_name}_len);" + ) def get_size_calculation(self, name: str, force: bool = False) -> str: # Use the actual length stored in the _len field @@ -2658,6 +2679,15 @@ static void dump_field(std::string &out, const char *field_name, T value, int in out.append("\\n"); } +// Helper for bytes fields - uses stack buffer to avoid heap allocation +// Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer +static void dump_bytes_field(std::string &out, const char *field_name, const uint8_t *data, size_t len, int indent = 2) { + char hex_buf[format_hex_pretty_size(160)]; + append_field_prefix(out, field_name, indent); + format_hex_pretty_to(hex_buf, data, len); + append_with_newline(out, hex_buf); +} + """ content += "namespace enums {\n\n" From 8cccfa5369535059fd7e05aebaa58fcabe07a0d7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jan 2026 07:38:20 -1000 Subject: [PATCH 07/20] [mqtt][prometheus][graph] Migrate value_accuracy_to_string() to stack-based alternative (#13159) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/graph/graph.cpp | 24 +++++----- .../components/mqtt/custom_mqtt_device.cpp | 5 ++- esphome/components/mqtt/mqtt_climate.cpp | 13 +++--- esphome/components/mqtt/mqtt_cover.cpp | 6 ++- esphome/components/mqtt/mqtt_sensor.cpp | 4 +- esphome/components/mqtt/mqtt_valve.cpp | 3 +- .../prometheus/prometheus_handler.cpp | 45 ++++++++++--------- .../prometheus/prometheus_handler.h | 2 +- esphome/core/helpers.h | 3 +- 9 files changed, 60 insertions(+), 45 deletions(-) diff --git a/esphome/components/graph/graph.cpp b/esphome/components/graph/graph.cpp index e3b9119108b..c43cd07fe08 100644 --- a/esphome/components/graph/graph.cpp +++ b/esphome/components/graph/graph.cpp @@ -232,17 +232,19 @@ void GraphLegend::init(Graph *g) { ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh); if (this->values_ != VALUE_POSITION_TYPE_NONE) { - std::string valstr = - value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); + char valstr[VALUE_ACCURACY_MAX_LEN]; if (this->units_) { - valstr += trace->sensor_->get_unit_of_measurement_ref(); + value_accuracy_with_uom_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals(), + trace->sensor_->get_unit_of_measurement_ref()); + } else { + value_accuracy_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); } - this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh); + this->font_value_->measure(valstr, &fw, &fos, &fbl, &fh); if (fw > valw) valw = fw; if (fh > valh) valh = fh; - ESP_LOGI(TAGL, " %s %d %d", valstr.c_str(), fw, fh); + ESP_LOGI(TAGL, " %s %d %d", valstr, fw, fh); } } // Add extra margin @@ -368,13 +370,15 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of if (legend_->values_ != VALUE_POSITION_TYPE_NONE) { int xv = x + legend_->xv_; int yv = y + legend_->yv_; - std::string valstr = - value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); + char valstr[VALUE_ACCURACY_MAX_LEN]; if (legend_->units_) { - valstr += trace->sensor_->get_unit_of_measurement_ref(); + value_accuracy_with_uom_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals(), + trace->sensor_->get_unit_of_measurement_ref()); + } else { + value_accuracy_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals()); } - buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str()); - ESP_LOGV(TAG, " value: %s", valstr.c_str()); + buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr); + ESP_LOGV(TAG, " value: %s", valstr); } x += legend_->xs_; y += legend_->ys_; diff --git a/esphome/components/mqtt/custom_mqtt_device.cpp b/esphome/components/mqtt/custom_mqtt_device.cpp index 25a8a820663..c900e3861d3 100644 --- a/esphome/components/mqtt/custom_mqtt_device.cpp +++ b/esphome/components/mqtt/custom_mqtt_device.cpp @@ -12,8 +12,9 @@ bool CustomMQTTDevice::publish(const std::string &topic, const std::string &payl return global_mqtt_client->publish(topic, payload, qos, retain); } bool CustomMQTTDevice::publish(const std::string &topic, float value, int8_t number_decimals) { - auto str = value_accuracy_to_string(value, number_decimals); - return this->publish(topic, str); + char buf[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(buf, value, number_decimals); + return this->publish(topic, buf); } bool CustomMQTTDevice::publish(const std::string &topic, int value) { char buffer[24]; diff --git a/esphome/components/mqtt/mqtt_climate.cpp b/esphome/components/mqtt/mqtt_climate.cpp index 625fb715a70..c7e086115b4 100644 --- a/esphome/components/mqtt/mqtt_climate.cpp +++ b/esphome/components/mqtt/mqtt_climate.cpp @@ -291,35 +291,36 @@ bool MQTTClimateComponent::publish_state_() { success = false; int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); + char payload[VALUE_ACCURACY_MAX_LEN]; if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) && !std::isnan(this->device_->current_temperature)) { - std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy); + value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy); if (!this->publish(this->get_current_temperature_state_topic(), payload)) success = false; } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { - std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy); + value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy); if (!this->publish(this->get_target_temperature_low_state_topic(), payload)) success = false; - payload = value_accuracy_to_string(this->device_->target_temperature_high, target_accuracy); + value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy); if (!this->publish(this->get_target_temperature_high_state_topic(), payload)) success = false; } else { - std::string payload = value_accuracy_to_string(this->device_->target_temperature, target_accuracy); + value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy); if (!this->publish(this->get_target_temperature_state_topic(), payload)) success = false; } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) && !std::isnan(this->device_->current_humidity)) { - std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0); + value_accuracy_to_buf(payload, this->device_->current_humidity, 0); if (!this->publish(this->get_current_humidity_state_topic(), payload)) success = false; } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) && !std::isnan(this->device_->target_humidity)) { - std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0); + value_accuracy_to_buf(payload, this->device_->target_humidity, 0); if (!this->publish(this->get_target_humidity_state_topic(), payload)) success = false; } diff --git a/esphome/components/mqtt/mqtt_cover.cpp b/esphome/components/mqtt/mqtt_cover.cpp index 45050274850..2164b5ca441 100644 --- a/esphome/components/mqtt/mqtt_cover.cpp +++ b/esphome/components/mqtt/mqtt_cover.cpp @@ -98,12 +98,14 @@ bool MQTTCoverComponent::publish_state() { auto traits = this->cover_->get_traits(); bool success = true; if (traits.get_supports_position()) { - std::string pos = value_accuracy_to_string(roundf(this->cover_->position * 100), 0); + char pos[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0); if (!this->publish(this->get_position_state_topic(), pos)) success = false; } if (traits.get_supports_tilt()) { - std::string pos = value_accuracy_to_string(roundf(this->cover_->tilt * 100), 0); + char pos[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0); if (!this->publish(this->get_tilt_state_topic(), pos)) success = false; } diff --git a/esphome/components/mqtt/mqtt_sensor.cpp b/esphome/components/mqtt/mqtt_sensor.cpp index 14eb160e728..cfe6923a5f1 100644 --- a/esphome/components/mqtt/mqtt_sensor.cpp +++ b/esphome/components/mqtt/mqtt_sensor.cpp @@ -82,7 +82,9 @@ bool MQTTSensorComponent::publish_state(float value) { if (mqtt::global_mqtt_client->is_publish_nan_as_none() && std::isnan(value)) return this->publish(this->get_state_topic_(), "None"); int8_t accuracy = this->sensor_->get_accuracy_decimals(); - return this->publish(this->get_state_topic_(), value_accuracy_to_string(value, accuracy)); + char buf[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(buf, value, accuracy); + return this->publish(this->get_state_topic_(), buf); } } // namespace esphome::mqtt diff --git a/esphome/components/mqtt/mqtt_valve.cpp b/esphome/components/mqtt/mqtt_valve.cpp index a4c893f84b5..b4cc367bc53 100644 --- a/esphome/components/mqtt/mqtt_valve.cpp +++ b/esphome/components/mqtt/mqtt_valve.cpp @@ -73,7 +73,8 @@ bool MQTTValveComponent::publish_state() { auto traits = this->valve_->get_traits(); bool success = true; if (traits.get_supports_position()) { - std::string pos = value_accuracy_to_string(roundf(this->valve_->position * 100), 0); + char pos[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(pos, roundf(this->valve_->position * 100), 0); if (!this->publish(this->get_position_state_topic(), pos)) success = false; } diff --git a/esphome/components/prometheus/prometheus_handler.cpp b/esphome/components/prometheus/prometheus_handler.cpp index dd577a4dbcc..e2639a22983 100644 --- a/esphome/components/prometheus/prometheus_handler.cpp +++ b/esphome/components/prometheus/prometheus_handler.cpp @@ -194,7 +194,9 @@ void PrometheusHandler::sensor_row_(AsyncResponseStream *stream, sensor::Sensor stream->print(ESPHOME_F("\",unit=\"")); stream->print(obj->get_unit_of_measurement_ref().c_str()); stream->print(ESPHOME_F("\"} ")); - stream->print(value_accuracy_to_string(obj->state, obj->get_accuracy_decimals()).c_str()); + char value_buf[VALUE_ACCURACY_MAX_LEN]; + value_accuracy_to_buf(value_buf, obj->state, obj->get_accuracy_decimals()); + stream->print(value_buf); stream->print(ESPHOME_F("\n")); } else { // Invalid state @@ -954,7 +956,7 @@ void PrometheusHandler::climate_setting_row_(AsyncResponseStream *stream, climat void PrometheusHandler::climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &category, - std::string &climate_value) { + const char *climate_value) { stream->print(ESPHOME_F("esphome_climate_value{id=\"")); stream->print(relabel_id_(obj).c_str()); add_area_label_(stream, area); @@ -965,7 +967,7 @@ void PrometheusHandler::climate_value_row_(AsyncResponseStream *stream, climate: stream->print(ESPHOME_F("\",category=\"")); stream->print(category.c_str()); stream->print(ESPHOME_F("\"} ")); - stream->print(climate_value.c_str()); + stream->print(climate_value); stream->print(ESPHOME_F("\n")); } @@ -1003,14 +1005,15 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima // Now see if traits is supported int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals(); int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals(); + char value_buf[VALUE_ACCURACY_MAX_LEN]; // max temp std::string max_temp = "maximum_temperature"; - auto max_temp_value = value_accuracy_to_string(traits.get_visual_max_temperature(), target_accuracy); - climate_value_row_(stream, obj, area, node, friendly_name, max_temp, max_temp_value); - // max temp - std::string min_temp = "mininum_temperature"; - auto min_temp_value = value_accuracy_to_string(traits.get_visual_min_temperature(), target_accuracy); - climate_value_row_(stream, obj, area, node, friendly_name, min_temp, min_temp_value); + value_accuracy_to_buf(value_buf, traits.get_visual_max_temperature(), target_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, max_temp, value_buf); + // min temp + std::string min_temp = "minimum_temperature"; + value_accuracy_to_buf(value_buf, traits.get_visual_min_temperature(), target_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, min_temp, value_buf); // now check optional traits if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) { std::string current_temp = "current_temperature"; @@ -1018,8 +1021,8 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, true); any_failures = true; } else { - auto current_temp_value = value_accuracy_to_string(obj->current_temperature, current_accuracy); - climate_value_row_(stream, obj, area, node, friendly_name, current_temp, current_temp_value); + value_accuracy_to_buf(value_buf, obj->current_temperature, current_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, current_temp, value_buf); climate_failed_row_(stream, obj, area, node, friendly_name, current_temp, false); } } @@ -1029,8 +1032,8 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, true); any_failures = true; } else { - auto current_humidity_value = value_accuracy_to_string(obj->current_humidity, 0); - climate_value_row_(stream, obj, area, node, friendly_name, current_humidity, current_humidity_value); + value_accuracy_to_buf(value_buf, obj->current_humidity, 0); + climate_value_row_(stream, obj, area, node, friendly_name, current_humidity, value_buf); climate_failed_row_(stream, obj, area, node, friendly_name, current_humidity, false); } } @@ -1040,23 +1043,23 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, true); any_failures = true; } else { - auto target_humidity_value = value_accuracy_to_string(obj->target_humidity, 0); - climate_value_row_(stream, obj, area, node, friendly_name, target_humidity, target_humidity_value); + value_accuracy_to_buf(value_buf, obj->target_humidity, 0); + climate_value_row_(stream, obj, area, node, friendly_name, target_humidity, value_buf); climate_failed_row_(stream, obj, area, node, friendly_name, target_humidity, false); } } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) { std::string target_temp_low = "target_temperature_low"; - auto target_temp_low_value = value_accuracy_to_string(obj->target_temperature_low, target_accuracy); - climate_value_row_(stream, obj, area, node, friendly_name, target_temp_low, target_temp_low_value); + value_accuracy_to_buf(value_buf, obj->target_temperature_low, target_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, target_temp_low, value_buf); std::string target_temp_high = "target_temperature_high"; - auto target_temp_high_value = value_accuracy_to_string(obj->target_temperature_high, target_accuracy); - climate_value_row_(stream, obj, area, node, friendly_name, target_temp_high, target_temp_high_value); + value_accuracy_to_buf(value_buf, obj->target_temperature_high, target_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, target_temp_high, value_buf); } else { std::string target_temp = "target_temperature"; - auto target_temp_value = value_accuracy_to_string(obj->target_temperature, target_accuracy); - climate_value_row_(stream, obj, area, node, friendly_name, target_temp, target_temp_value); + value_accuracy_to_buf(value_buf, obj->target_temperature, target_accuracy); + climate_value_row_(stream, obj, area, node, friendly_name, target_temp, value_buf); } if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) { std::string climate_trait_category = "action"; diff --git a/esphome/components/prometheus/prometheus_handler.h b/esphome/components/prometheus/prometheus_handler.h index 24243c8c98d..fc48ad67e3c 100644 --- a/esphome/components/prometheus/prometheus_handler.h +++ b/esphome/components/prometheus/prometheus_handler.h @@ -207,7 +207,7 @@ class PrometheusHandler : public AsyncWebHandler, public Component { void climate_setting_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, std::string &friendly_name, std::string &setting, const LogString *setting_value); void climate_value_row_(AsyncResponseStream *stream, climate::Climate *obj, std::string &area, std::string &node, - std::string &friendly_name, std::string &category, std::string &climate_value); + std::string &friendly_name, std::string &category, const char *climate_value); #endif web_server_base::WebServerBase *base_; diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index bf559d2bc61..396a58464f0 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1027,7 +1027,8 @@ enum ParseOnOffState : uint8_t { /// Parse a string that contains either on, off or toggle. ParseOnOffState parse_on_off(const char *str, const char *on = nullptr, const char *off = nullptr); -/// Create a string from a value and an accuracy in decimals. +/// @deprecated Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0. +ESPDEPRECATED("Allocates heap memory. Use value_accuracy_to_buf() instead. Removed in 2026.7.0.", "2026.1.0") std::string value_accuracy_to_string(float value, int8_t accuracy_decimals); /// Maximum buffer size for value_accuracy formatting (float ~15 chars + space + UOM ~40 chars + null) From 7f0e4eaa84ada41d72af40b5bbf0564c60a945fb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jan 2026 07:38:39 -1000 Subject: [PATCH 08/20] [nfc] Use stack-based hex formatting in pn7150/pn7160 components (#13163) --- esphome/components/nfc/automation.cpp | 6 +- .../nfc/binary_sensor/nfc_binary_sensor.cpp | 4 +- esphome/components/nfc/nfc.cpp | 11 +++ esphome/components/nfc/nfc.h | 14 ++++ esphome/components/pn532/pn532.cpp | 3 +- .../components/pn532/pn532_mifare_classic.cpp | 3 +- .../pn532/pn532_mifare_ultralight.cpp | 3 +- esphome/components/pn7150/pn7150.cpp | 63 +++++++++++------ .../pn7150/pn7150_mifare_classic.cpp | 22 +++--- .../pn7150/pn7150_mifare_ultralight.cpp | 3 +- esphome/components/pn7160/pn7160.cpp | 70 ++++++++++++------- .../pn7160/pn7160_mifare_classic.cpp | 22 +++--- .../pn7160/pn7160_mifare_ultralight.cpp | 3 +- 13 files changed, 154 insertions(+), 73 deletions(-) diff --git a/esphome/components/nfc/automation.cpp b/esphome/components/nfc/automation.cpp index ff00340df0c..e2956e4c123 100644 --- a/esphome/components/nfc/automation.cpp +++ b/esphome/components/nfc/automation.cpp @@ -1,9 +1,13 @@ #include "automation.h" +#include "nfc.h" namespace esphome { namespace nfc { -void NfcOnTagTrigger::process(const std::unique_ptr &tag) { this->trigger(format_uid(tag->get_uid()), *tag); } +void NfcOnTagTrigger::process(const std::unique_ptr &tag) { + char uid_buf[FORMAT_UID_BUFFER_SIZE]; + this->trigger(std::string(format_uid_to(uid_buf, tag->get_uid())), *tag); +} } // namespace nfc } // namespace esphome diff --git a/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp index bc19fa72138..b62b243cc68 100644 --- a/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp +++ b/esphome/components/nfc/binary_sensor/nfc_binary_sensor.cpp @@ -1,4 +1,5 @@ #include "nfc_binary_sensor.h" +#include "../nfc.h" #include "../nfc_helpers.h" #include "esphome/core/log.h" @@ -24,7 +25,8 @@ void NfcTagBinarySensor::dump_config() { return; } if (!this->uid_.empty()) { - ESP_LOGCONFIG(TAG, " Tag UID: %s", format_bytes(this->uid_).c_str()); + char uid_buf[FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGCONFIG(TAG, " Tag UID: %s", format_bytes_to(uid_buf, this->uid_)); } } diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index d3a24816934..82e86b936a6 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -8,9 +8,20 @@ namespace nfc { static const char *const TAG = "nfc"; +char *format_uid_to(char *buffer, const std::vector &uid) { + return format_hex_pretty_to(buffer, FORMAT_UID_BUFFER_SIZE, uid.data(), uid.size(), '-'); +} + +char *format_bytes_to(char *buffer, const std::vector &bytes) { + return format_hex_pretty_to(buffer, FORMAT_BYTES_BUFFER_SIZE, bytes.data(), bytes.size(), ' '); +} + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" std::string format_uid(const std::vector &uid) { return format_hex_pretty(uid, '-', false); } std::string format_bytes(const std::vector &bytes) { return format_hex_pretty(bytes, ' ', false); } +#pragma GCC diagnostic pop uint8_t guess_tag_type(uint8_t uid_length) { if (uid_length == 4) { diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index 9879cfdb03e..6568c60a858 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -53,7 +53,21 @@ static const uint8_t DEFAULT_KEY[6] = {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}; static const uint8_t NDEF_KEY[6] = {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7}; static const uint8_t MAD_KEY[6] = {0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}; +/// Max UID size is 10 bytes, formatted as "XX-XX-XX-XX-XX-XX-XX-XX-XX-XX\0" = 30 chars +static constexpr size_t FORMAT_UID_BUFFER_SIZE = 30; +/// Format UID to buffer with '-' separator (e.g., "04-11-22-33"). Returns buffer for inline use. +char *format_uid_to(char *buffer, const std::vector &uid); + +/// Buffer size for format_bytes_to (64 bytes max = 192 chars with space separator) +static constexpr size_t FORMAT_BYTES_BUFFER_SIZE = 192; +/// Format bytes to buffer with ' ' separator (e.g., "04 11 22 33"). Returns buffer for inline use. +char *format_bytes_to(char *buffer, const std::vector &bytes); + +// Remove before 2026.6.0 +ESPDEPRECATED("Use format_uid_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0") std::string format_uid(const std::vector &uid); +// Remove before 2026.6.0 +ESPDEPRECATED("Use format_bytes_to() with stack buffer instead. Removed in 2026.6.0", "2025.12.0") std::string format_bytes(const std::vector &bytes); uint8_t guess_tag_type(uint8_t uid_length); diff --git a/esphome/components/pn532/pn532.cpp b/esphome/components/pn532/pn532.cpp index d5e892a5763..8f0c5581d4b 100644 --- a/esphome/components/pn532/pn532.cpp +++ b/esphome/components/pn532/pn532.cpp @@ -197,7 +197,8 @@ void PN532::loop() { trigger->process(tag); if (report) { - ESP_LOGD(TAG, "Found new tag '%s'", nfc::format_uid(nfcid).c_str()); + char uid_buf[nfc::FORMAT_UID_BUFFER_SIZE]; + ESP_LOGD(TAG, "Found new tag '%s'", nfc::format_uid_to(uid_buf, nfcid)); if (tag->has_ndef_message()) { const auto &message = tag->get_ndef_message(); const auto &records = message->get_records(); diff --git a/esphome/components/pn532/pn532_mifare_classic.cpp b/esphome/components/pn532/pn532_mifare_classic.cpp index 943f8c55192..28ab22e160e 100644 --- a/esphome/components/pn532/pn532_mifare_classic.cpp +++ b/esphome/components/pn532/pn532_mifare_classic.cpp @@ -77,7 +77,8 @@ bool PN532::read_mifare_classic_block_(uint8_t block_num, std::vector & } data.erase(data.begin()); - ESP_LOGVV(TAG, " Block %d: %s", block_num, nfc::format_bytes(data).c_str()); + char data_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGVV(TAG, " Block %d: %s", block_num, nfc::format_bytes_to(data_buf, data)); return true; } diff --git a/esphome/components/pn532/pn532_mifare_ultralight.cpp b/esphome/components/pn532/pn532_mifare_ultralight.cpp index f823829a6cc..0221ba31c5c 100644 --- a/esphome/components/pn532/pn532_mifare_ultralight.cpp +++ b/esphome/components/pn532/pn532_mifare_ultralight.cpp @@ -71,7 +71,8 @@ bool PN532::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_bytes } } - ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + char data_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes_to(data_buf, data)); return true; } diff --git a/esphome/components/pn7150/pn7150.cpp b/esphome/components/pn7150/pn7150.cpp index f6ddcb07672..e1ba3761d45 100644 --- a/esphome/components/pn7150/pn7150.cpp +++ b/esphome/components/pn7150/pn7150.cpp @@ -203,7 +203,8 @@ uint8_t PN7150::set_test_mode(const TestMode test_mode, const std::vectortag_listeners_) { listener->tag_off(*this->discovered_endpoint_[tag_index].tag); } - ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); + char uid_buf[nfc::FORMAT_UID_BUFFER_SIZE]; + ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid_to(uid_buf, this->discovered_endpoint_[tag_index].tag->get_uid())); this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); } } @@ -772,26 +777,33 @@ void PN7150::process_message_() { ESP_LOGV(TAG, "Unimplemented NCI Core OID received: 0x%02X", rx.get_oid()); } } else { - ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes(rx.get_message()).c_str()); + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes_to(buf, rx.get_message())); } break; - case nfc::NCI_PKT_MT_CTRL_RESPONSE: + case nfc::NCI_PKT_MT_CTRL_RESPONSE: { + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; ESP_LOGV(TAG, "Unimplemented GID: 0x%02X OID: 0x%02X Full response: %s", rx.get_gid(), rx.get_oid(), - nfc::format_bytes(rx.get_message()).c_str()); + nfc::format_bytes_to(buf, rx.get_message())); break; + } - case nfc::NCI_PKT_MT_CTRL_COMMAND: - ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes(rx.get_message()).c_str()); + case nfc::NCI_PKT_MT_CTRL_COMMAND: { + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes_to(buf, rx.get_message())); break; + } case nfc::NCI_PKT_MT_DATA: this->process_data_message_(rx); break; - default: - ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes(rx.get_message()).c_str()); + default: { + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes_to(buf, rx.get_message())); break; + } } } @@ -872,8 +884,9 @@ void PN7150::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoi case EP_READ: default: if (!working_endpoint.trig_called) { + char uid_buf[nfc::FORMAT_UID_BUFFER_SIZE]; ESP_LOGI(TAG, "Read tag type %s with UID %s", working_endpoint.tag->get_tag_type().c_str(), - nfc::format_uid(working_endpoint.tag->get_uid()).c_str()); + nfc::format_uid_to(uid_buf, working_endpoint.tag->get_uid())); if (this->read_endpoint_data_(*working_endpoint.tag) != nfc::STATUS_OK) { ESP_LOGW(TAG, " Unable to read NDEF record(s)"); } else if (working_endpoint.tag->has_ndef_message()) { @@ -964,7 +977,8 @@ void PN7150::process_rf_deactivate_oid_(nfc::NciMessage &rx) { } void PN7150::process_data_message_(nfc::NciMessage &rx) { - ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes_to(buf, rx.get_message())); std::vector ndef_response; this->card_emu_t4t_get_response_(rx.get_message(), ndef_response); @@ -978,7 +992,7 @@ void PN7150::process_data_message_(nfc::NciMessage &rx) { uint8_t(ndef_response_size & 0x00FF)}; tx_msg.insert(tx_msg.end(), ndef_response.begin(), ndef_response.end()); nfc::NciMessage tx(tx_msg); - ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes(tx.get_message()).c_str()); + ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes_to(buf, tx.get_message())); if (this->transceive_(tx, rx, NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Sending reply for card emulation failed"); } @@ -1031,7 +1045,8 @@ void PN7150::card_emu_t4t_get_response_(std::vector &response, std::vec uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; - ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes(ndef_message).c_str()); + char ndef_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes_to(ndef_buf, ndef_message)); if (length <= (ndef_msg_size + offset + 2)) { if (offset == 0) { @@ -1070,7 +1085,8 @@ void PN7150::card_emu_t4t_get_response_(std::vector &response, std::vec ndef_msg_written.insert(ndef_msg_written.end(), response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5, response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5 + length); - ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes(ndef_msg_written).c_str()); + char ndef_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes_to(ndef_buf, ndef_msg_written)); ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); } } @@ -1079,6 +1095,7 @@ void PN7150::card_emu_t4t_get_response_(std::vector &response, std::vec uint8_t PN7150::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint16_t timeout, const bool expect_notification) { uint8_t retries = NFCC_MAX_COMM_FAILS; + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; while (retries) { // first, send the message we need to send @@ -1086,7 +1103,7 @@ uint8_t PN7150::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint ESP_LOGE(TAG, "Error sending message"); return nfc::STATUS_FAILED; } - ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes(tx.get_message()).c_str()); + ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes_to(buf, tx.get_message())); // next, the NFCC should send back a response if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { ESP_LOGW(TAG, "Error receiving message"); @@ -1098,24 +1115,24 @@ uint8_t PN7150::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint break; } } - ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes_to(buf, rx.get_message())); // validate the response based on the message type that was sent (command vs. data) if (!tx.message_type_is(nfc::NCI_PKT_MT_DATA)) { // for commands, the GID and OID should match and the status should be OK if ((rx.get_gid() != tx.get_gid()) || (rx.get_oid()) != tx.get_oid()) { - ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes_to(buf, rx.get_message())); return nfc::STATUS_FAILED; } if (!rx.simple_status_response_is(nfc::STATUS_OK)) { - ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes_to(buf, rx.get_message())); } return rx.get_simple_status_response(); } else { // when requesting data from the endpoint, the first response is from the NFCC; we must validate this, first if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.gid_is(nfc::NCI_CORE_GID)) || (!rx.oid_is(nfc::NCI_CORE_CONN_CREDITS_OID)) || (!rx.message_length_is(3))) { - ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes_to(buf, rx.get_message())); return nfc::STATUS_FAILED; } @@ -1125,7 +1142,7 @@ uint8_t PN7150::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint ESP_LOGE(TAG, "Error receiving data from endpoint"); return nfc::STATUS_FAILED; } - ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes_to(buf, rx.get_message())); } return nfc::STATUS_OK; diff --git a/esphome/components/pn7150/pn7150_mifare_classic.cpp b/esphome/components/pn7150/pn7150_mifare_classic.cpp index 0443929f693..dee81b610a3 100644 --- a/esphome/components/pn7150/pn7150_mifare_classic.cpp +++ b/esphome/components/pn7150/pn7150_mifare_classic.cpp @@ -70,7 +70,8 @@ uint8_t PN7150::read_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Timeout reading tag data"); return nfc::STATUS_FAILED; @@ -79,13 +80,13 @@ uint8_t PN7150::read_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed"); return nfc::STATUS_FAILED; @@ -119,7 +121,7 @@ uint8_t PN7150::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, c if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) || (rx.get_message()[4] != nfc::STATUS_OK)) { ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num); - ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes_to(buf, rx.get_message())); return nfc::STATUS_FAILED; } @@ -238,7 +240,8 @@ uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed"); return nfc::STATUS_FAILED; @@ -247,7 +250,7 @@ uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write"); return nfc::STATUS_FAILED; @@ -256,7 +259,7 @@ uint8_t PN7150::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed"); return nfc::STATUS_FAILED; diff --git a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp index b107f6f79e0..ac15475bad3 100644 --- a/esphome/components/pn7150/pn7150_mifare_ultralight.cpp +++ b/esphome/components/pn7150/pn7150_mifare_ultralight.cpp @@ -72,7 +72,8 @@ uint8_t PN7150::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_b } } - ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes_to(buf, data)); return nfc::STATUS_OK; } diff --git a/esphome/components/pn7160/pn7160.cpp b/esphome/components/pn7160/pn7160.cpp index 8c8028b04a6..1a38dce5fd9 100644 --- a/esphome/components/pn7160/pn7160.cpp +++ b/esphome/components/pn7160/pn7160.cpp @@ -215,7 +215,8 @@ uint8_t PN7160::set_test_mode(const TestMode test_mode, const std::vector features(rx.get_message().begin() + 4, rx.get_message().begin() + 8); + char feat_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; ESP_LOGD(TAG, "Hardware version: %u\n" "ROM code version: %u\n" "FLASH major version: %u\n" "FLASH minor version: %u\n" "Features: %s", - hw_version, rom_code_version, flash_major_version, flash_minor_version, nfc::format_bytes(features).c_str()); + hw_version, rom_code_version, flash_major_version, flash_minor_version, + nfc::format_bytes_to(feat_buf, features)); return rx.get_simple_status_response(); } @@ -599,7 +606,8 @@ void PN7160::erase_tag_(const uint8_t tag_index) { for (auto *listener : this->tag_listeners_) { listener->tag_off(*this->discovered_endpoint_[tag_index].tag); } - ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid(this->discovered_endpoint_[tag_index].tag->get_uid()).c_str()); + char uid_buf[nfc::FORMAT_UID_BUFFER_SIZE]; + ESP_LOGI(TAG, "Tag %s removed", nfc::format_uid_to(uid_buf, this->discovered_endpoint_[tag_index].tag->get_uid())); this->discovered_endpoint_.erase(this->discovered_endpoint_.begin() + tag_index); } } @@ -796,26 +804,33 @@ void PN7160::process_message_() { ESP_LOGV(TAG, "Unimplemented NCI Core OID received: 0x%02X", rx.get_oid()); } } else { - ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes(rx.get_message()).c_str()); + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGV(TAG, "Unimplemented notification: %s", nfc::format_bytes_to(buf, rx.get_message())); } break; - case nfc::NCI_PKT_MT_CTRL_RESPONSE: + case nfc::NCI_PKT_MT_CTRL_RESPONSE: { + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; ESP_LOGV(TAG, "Unimplemented GID: 0x%02X OID: 0x%02X Full response: %s", rx.get_gid(), rx.get_oid(), - nfc::format_bytes(rx.get_message()).c_str()); + nfc::format_bytes_to(buf, rx.get_message())); break; + } - case nfc::NCI_PKT_MT_CTRL_COMMAND: - ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes(rx.get_message()).c_str()); + case nfc::NCI_PKT_MT_CTRL_COMMAND: { + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGV(TAG, "Unimplemented command: %s", nfc::format_bytes_to(buf, rx.get_message())); break; + } case nfc::NCI_PKT_MT_DATA: this->process_data_message_(rx); break; - default: - ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes(rx.get_message()).c_str()); + default: { + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGV(TAG, "Unimplemented message type: %s", nfc::format_bytes_to(buf, rx.get_message())); break; + } } } @@ -896,8 +911,9 @@ void PN7160::process_rf_intf_activated_oid_(nfc::NciMessage &rx) { // an endpoi case EP_READ: default: if (!working_endpoint.trig_called) { + char uid_buf[nfc::FORMAT_UID_BUFFER_SIZE]; ESP_LOGI(TAG, "Read tag type %s with UID %s", working_endpoint.tag->get_tag_type().c_str(), - nfc::format_uid(working_endpoint.tag->get_uid()).c_str()); + nfc::format_uid_to(uid_buf, working_endpoint.tag->get_uid())); if (this->read_endpoint_data_(*working_endpoint.tag) != nfc::STATUS_OK) { ESP_LOGW(TAG, " Unable to read NDEF record(s)"); } else if (working_endpoint.tag->has_ndef_message()) { @@ -988,7 +1004,8 @@ void PN7160::process_rf_deactivate_oid_(nfc::NciMessage &rx) { } void PN7160::process_data_message_(nfc::NciMessage &rx) { - ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGVV(TAG, "Received data message: %s", nfc::format_bytes_to(buf, rx.get_message())); std::vector ndef_response; this->card_emu_t4t_get_response_(rx.get_message(), ndef_response); @@ -1002,7 +1019,7 @@ void PN7160::process_data_message_(nfc::NciMessage &rx) { uint8_t(ndef_response_size & 0x00FF)}; tx_msg.insert(tx_msg.end(), ndef_response.begin(), ndef_response.end()); nfc::NciMessage tx(tx_msg); - ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes(tx.get_message()).c_str()); + ESP_LOGVV(TAG, "Sending data message: %s", nfc::format_bytes_to(buf, tx.get_message())); if (this->transceive_(tx, rx, NFCC_DEFAULT_TIMEOUT, false) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Sending reply for card emulation failed"); } @@ -1055,7 +1072,8 @@ void PN7160::card_emu_t4t_get_response_(std::vector &response, std::vec uint16_t offset = (response[nfc::NCI_PKT_HEADER_SIZE + 2] << 8) + response[nfc::NCI_PKT_HEADER_SIZE + 3]; uint8_t length = response[nfc::NCI_PKT_HEADER_SIZE + 4]; - ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes(ndef_message).c_str()); + char ndef_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGVV(TAG, "Encoded NDEF message: %s", nfc::format_bytes_to(ndef_buf, ndef_message)); if (length <= (ndef_msg_size + offset + 2)) { if (offset == 0) { @@ -1094,7 +1112,8 @@ void PN7160::card_emu_t4t_get_response_(std::vector &response, std::vec ndef_msg_written.insert(ndef_msg_written.end(), response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5, response.begin() + nfc::NCI_PKT_HEADER_SIZE + 5 + length); - ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes(ndef_msg_written).c_str()); + char write_buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGD(TAG, "Received %u-byte NDEF message: %s", length, nfc::format_bytes_to(write_buf, ndef_msg_written)); ndef_response.insert(ndef_response.end(), std::begin(CARD_EMU_T4T_OK), std::end(CARD_EMU_T4T_OK)); } } @@ -1103,6 +1122,7 @@ void PN7160::card_emu_t4t_get_response_(std::vector &response, std::vec uint8_t PN7160::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint16_t timeout, const bool expect_notification) { uint8_t retries = NFCC_MAX_COMM_FAILS; + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; while (retries) { // first, send the message we need to send @@ -1110,7 +1130,7 @@ uint8_t PN7160::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint ESP_LOGE(TAG, "Error sending message"); return nfc::STATUS_FAILED; } - ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes(tx.get_message()).c_str()); + ESP_LOGVV(TAG, "Wrote: %s", nfc::format_bytes_to(buf, tx.get_message())); // next, the NFCC should send back a response if (this->read_nfcc(rx, timeout) != nfc::STATUS_OK) { ESP_LOGW(TAG, "Error receiving message"); @@ -1122,24 +1142,24 @@ uint8_t PN7160::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint break; } } - ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes_to(buf, rx.get_message())); // validate the response based on the message type that was sent (command vs. data) if (!tx.message_type_is(nfc::NCI_PKT_MT_DATA)) { // for commands, the GID and OID should match and the status should be OK if ((rx.get_gid() != tx.get_gid()) || (rx.get_oid()) != tx.get_oid()) { - ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGE(TAG, "Incorrect response to command: %s", nfc::format_bytes_to(buf, rx.get_message())); return nfc::STATUS_FAILED; } if (!rx.simple_status_response_is(nfc::STATUS_OK)) { - ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGE(TAG, "Error in response to command: %s", nfc::format_bytes_to(buf, rx.get_message())); } return rx.get_simple_status_response(); } else { // when requesting data from the endpoint, the first response is from the NFCC; we must validate this, first if ((!rx.message_type_is(nfc::NCI_PKT_MT_CTRL_NOTIFICATION)) || (!rx.gid_is(nfc::NCI_CORE_GID)) || (!rx.oid_is(nfc::NCI_CORE_CONN_CREDITS_OID)) || (!rx.message_length_is(3))) { - ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGE(TAG, "Incorrect response to data message: %s", nfc::format_bytes_to(buf, rx.get_message())); return nfc::STATUS_FAILED; } @@ -1149,7 +1169,7 @@ uint8_t PN7160::transceive_(nfc::NciMessage &tx, nfc::NciMessage &rx, const uint ESP_LOGE(TAG, "Error receiving data from endpoint"); return nfc::STATUS_FAILED; } - ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGVV(TAG, "Read: %s", nfc::format_bytes_to(buf, rx.get_message())); } return nfc::STATUS_OK; diff --git a/esphome/components/pn7160/pn7160_mifare_classic.cpp b/esphome/components/pn7160/pn7160_mifare_classic.cpp index fa63cc00d50..57d2042eaa4 100644 --- a/esphome/components/pn7160/pn7160_mifare_classic.cpp +++ b/esphome/components/pn7160/pn7160_mifare_classic.cpp @@ -69,8 +69,9 @@ uint8_t PN7160::read_mifare_classic_tag_(nfc::NfcTag &tag) { uint8_t PN7160::read_mifare_classic_block_(uint8_t block_num, std::vector &data) { nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_READ, block_num}); + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; - ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes(tx.get_message()).c_str()); + ESP_LOGVV(TAG, "Read XCHG_DATA_REQ: %s", nfc::format_bytes_to(buf, tx.get_message())); if (this->transceive_(tx, rx) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Timeout reading tag data"); return nfc::STATUS_FAILED; @@ -79,13 +80,13 @@ uint8_t PN7160::read_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Sending MFC_AUTHENTICATE_REQ failed"); return nfc::STATUS_FAILED; @@ -119,7 +121,7 @@ uint8_t PN7160::auth_mifare_classic_block_(uint8_t block_num, uint8_t key_num, c if ((!rx.message_type_is(nfc::NCI_PKT_MT_DATA)) || (!rx.simple_status_response_is(MFC_AUTHENTICATE_OID)) || (rx.get_message()[4] != nfc::STATUS_OK)) { ESP_LOGE(TAG, "MFC authentication failed - block 0x%02x", block_num); - ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes(rx.get_message()).c_str()); + ESP_LOGVV(TAG, "MFC_AUTHENTICATE_RSP: %s", nfc::format_bytes_to(buf, rx.get_message())); return nfc::STATUS_FAILED; } @@ -237,8 +239,9 @@ uint8_t PN7160::format_mifare_classic_ndef_() { uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vector &write_data) { nfc::NciMessage rx; nfc::NciMessage tx(nfc::NCI_PKT_MT_DATA, {XCHG_DATA_OID, nfc::MIFARE_CMD_WRITE, block_num}); + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; - ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes(tx.get_message()).c_str()); + ESP_LOGVV(TAG, "Write XCHG_DATA_REQ 1: %s", nfc::format_bytes_to(buf, tx.get_message())); if (this->transceive_(tx, rx) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Sending XCHG_DATA_REQ failed"); return nfc::STATUS_FAILED; @@ -247,7 +250,7 @@ uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { ESP_LOGE(TAG, "MFC XCHG_DATA timed out waiting for XCHG_DATA_RSP during block write"); return nfc::STATUS_FAILED; @@ -256,7 +259,7 @@ uint8_t PN7160::write_mifare_classic_block_(uint8_t block_num, std::vectortransceive_(tx, rx, NFCC_TAG_WRITE_TIMEOUT) != nfc::STATUS_OK) { ESP_LOGE(TAG, "Sending halt XCHG_DATA_REQ failed"); return nfc::STATUS_FAILED; diff --git a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp index 65daac494fa..584385f113a 100644 --- a/esphome/components/pn7160/pn7160_mifare_ultralight.cpp +++ b/esphome/components/pn7160/pn7160_mifare_ultralight.cpp @@ -72,7 +72,8 @@ uint8_t PN7160::read_mifare_ultralight_bytes_(uint8_t start_page, uint16_t num_b } } - ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes(data).c_str()); + char buf[nfc::FORMAT_BYTES_BUFFER_SIZE]; + ESP_LOGVV(TAG, "Data read: %s", nfc::format_bytes_to(buf, data)); return nfc::STATUS_OK; } From 7e1cda8f9fb900832217ef12ec6b522623b9e849 Mon Sep 17 00:00:00 2001 From: mikaabra Date: Mon, 12 Jan 2026 18:50:59 +0100 Subject: [PATCH 09/20] [esp32_can] Add listen-only mode to esp32_can component (#13084) Co-authored-by: Claude Opus 4.5 --- esphome/components/esp32_can/canbus.py | 10 ++++++++++ esphome/components/esp32_can/esp32_can.cpp | 15 ++++++++++++++- esphome/components/esp32_can/esp32_can.h | 7 +++++++ tests/components/esp32_can/common.yaml | 1 + tests/components/esp32_can/test.esp32-c6-idf.yaml | 14 +++----------- 5 files changed, 35 insertions(+), 12 deletions(-) diff --git a/esphome/components/esp32_can/canbus.py b/esphome/components/esp32_can/canbus.py index 0899a0dc2b1..0768b355071 100644 --- a/esphome/components/esp32_can/canbus.py +++ b/esphome/components/esp32_can/canbus.py @@ -19,6 +19,7 @@ from esphome.components.esp32 import ( import esphome.config_validation as cv from esphome.const import ( CONF_ID, + CONF_MODE, CONF_RX_PIN, CONF_RX_QUEUE_LEN, CONF_TX_PIN, @@ -33,6 +34,13 @@ CONF_TX_ENQUEUE_TIMEOUT = "tx_enqueue_timeout" esp32_can_ns = cg.esphome_ns.namespace("esp32_can") esp32_can = esp32_can_ns.class_("ESP32Can", CanbusComponent) +# Mode options - consistent with MCP2515 component +CanMode = esp32_can_ns.enum("CanMode") +CAN_MODES = { + "NORMAL": CanMode.CAN_MODE_NORMAL, + "LISTENONLY": CanMode.CAN_MODE_LISTEN_ONLY, +} + # Currently the driver only supports a subset of the bit rates defined in canbus # The supported bit rates differ between ESP32 variants. # See ESP-IDF Programming Guide --> API Reference --> Two-Wire Automotive Interface (TWAI) @@ -95,6 +103,7 @@ CONFIG_SCHEMA = canbus.CANBUS_SCHEMA.extend( cv.Optional(CONF_BIT_RATE, default="125KBPS"): validate_bit_rate, cv.Required(CONF_RX_PIN): pins.internal_gpio_input_pin_number, cv.Required(CONF_TX_PIN): pins.internal_gpio_output_pin_number, + cv.Optional(CONF_MODE, default="NORMAL"): cv.enum(CAN_MODES, upper=True), cv.Optional(CONF_RX_QUEUE_LEN): cv.uint32_t, cv.Optional(CONF_TX_QUEUE_LEN): cv.uint32_t, cv.Optional(CONF_TX_ENQUEUE_TIMEOUT): cv.positive_time_period_milliseconds, @@ -117,6 +126,7 @@ async def to_code(config): cg.add(var.set_rx(config[CONF_RX_PIN])) cg.add(var.set_tx(config[CONF_TX_PIN])) + cg.add(var.set_mode(config[CONF_MODE])) if (rx_queue_len := config.get(CONF_RX_QUEUE_LEN)) is not None: cg.add(var.set_rx_queue_len(rx_queue_len)) if (tx_queue_len := config.get(CONF_TX_QUEUE_LEN)) is not None: diff --git a/esphome/components/esp32_can/esp32_can.cpp b/esphome/components/esp32_can/esp32_can.cpp index d50964187d0..f521b63430f 100644 --- a/esphome/components/esp32_can/esp32_can.cpp +++ b/esphome/components/esp32_can/esp32_can.cpp @@ -75,8 +75,15 @@ bool ESP32Can::setup_internal() { return false; } + // Select TWAI mode based on configuration + twai_mode_t twai_mode = (this->mode_ == CAN_MODE_LISTEN_ONLY) ? TWAI_MODE_LISTEN_ONLY : TWAI_MODE_NORMAL; + + if (this->mode_ == CAN_MODE_LISTEN_ONLY) { + ESP_LOGI(TAG, "CAN bus configured in LISTEN_ONLY mode (passive, no ACKs)"); + } + twai_general_config_t g_config = - TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL); + TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, twai_mode); g_config.controller_id = next_twai_ctrl_num++; if (this->tx_queue_len_.has_value()) { g_config.tx_queue_len = this->tx_queue_len_.value(); @@ -111,6 +118,12 @@ bool ESP32Can::setup_internal() { } canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) { + // In listen-only mode, we cannot transmit + if (this->mode_ == CAN_MODE_LISTEN_ONLY) { + ESP_LOGW(TAG, "Cannot send messages in LISTEN_ONLY mode"); + return canbus::ERROR_FAIL; + } + if (this->twai_handle_ == nullptr) { // not setup yet or setup failed return canbus::ERROR_FAIL; diff --git a/esphome/components/esp32_can/esp32_can.h b/esphome/components/esp32_can/esp32_can.h index dc44aceb368..c3f200271bb 100644 --- a/esphome/components/esp32_can/esp32_can.h +++ b/esphome/components/esp32_can/esp32_can.h @@ -10,10 +10,16 @@ namespace esphome { namespace esp32_can { +enum CanMode : uint8_t { + CAN_MODE_NORMAL = 0, + CAN_MODE_LISTEN_ONLY = 1, +}; + class ESP32Can : public canbus::Canbus { public: void set_rx(int rx) { rx_ = rx; } void set_tx(int tx) { tx_ = tx; } + void set_mode(CanMode mode) { mode_ = mode; } void set_tx_queue_len(uint32_t tx_queue_len) { this->tx_queue_len_ = tx_queue_len; } void set_rx_queue_len(uint32_t rx_queue_len) { this->rx_queue_len_ = rx_queue_len; } void set_tx_enqueue_timeout_ms(uint32_t tx_enqueue_timeout_ms) { @@ -28,6 +34,7 @@ class ESP32Can : public canbus::Canbus { int rx_{-1}; int tx_{-1}; + CanMode mode_{CAN_MODE_NORMAL}; TickType_t tx_enqueue_timeout_ticks_{}; optional tx_queue_len_{}; optional rx_queue_len_{}; diff --git a/tests/components/esp32_can/common.yaml b/tests/components/esp32_can/common.yaml index 4349c470f30..3b9b33c048e 100644 --- a/tests/components/esp32_can/common.yaml +++ b/tests/components/esp32_can/common.yaml @@ -18,6 +18,7 @@ canbus: tx_pin: ${tx_pin} can_id: 4 bit_rate: 50kbps + mode: NORMAL on_frame: - can_id: 500 then: diff --git a/tests/components/esp32_can/test.esp32-c6-idf.yaml b/tests/components/esp32_can/test.esp32-c6-idf.yaml index 6ef730c3786..ac978482fcd 100644 --- a/tests/components/esp32_can/test.esp32-c6-idf.yaml +++ b/tests/components/esp32_can/test.esp32-c6-idf.yaml @@ -12,17 +12,7 @@ esphome: canbus_id: esp32_internal_can can_id: 0x100 data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] - - canbus.send: - # Extended ID explicit - canbus_id: esp32_internal_can_2 - use_extended_id: true - can_id: 0x100 - data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] - - canbus.send: - # Standard ID by default - canbus_id: esp32_internal_can_2 - can_id: 0x100 - data: [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08] + # Note: esp32_internal_can_2 uses LISTENONLY mode, so no send actions canbus: - platform: esp32_can @@ -31,6 +21,7 @@ canbus: tx_pin: GPIO7 can_id: 4 bit_rate: 50kbps + mode: NORMAL on_frame: - can_id: 500 then: @@ -62,6 +53,7 @@ canbus: tx_pin: GPIO9 can_id: 4 bit_rate: 50kbps + mode: LISTENONLY on_frame: - can_id: 500 then: From 0c3433d0568c53122048ebb849c239b89d0451f0 Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Mon, 12 Jan 2026 18:57:58 +0100 Subject: [PATCH 10/20] [deep_sleep] Fix GPIO wakeup comment (#12815) --- esphome/components/deep_sleep/deep_sleep_esp32.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index 833be8e76c0..ea1cd00c5f7 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -26,7 +26,7 @@ namespace deep_sleep { // - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup) // - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup) // - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup) -// - GPIO wakeup: GPIO wakeup for non-RTC pins (esp_deep_sleep_enable_gpio_wakeup) +// - GPIO wakeup: GPIO wakeup for RTC pins (esp_deep_sleep_enable_gpio_wakeup) static const char *const TAG = "deep_sleep"; From 61a89a97d7d2e401d2dffcdac8388c26b94c2d0a Mon Sep 17 00:00:00 2001 From: Jasper van der Neut - Stulen Date: Mon, 12 Jan 2026 19:03:13 +0100 Subject: [PATCH 11/20] [deep_sleep] Fix GPIO wakeup on ESP32-C3/C6 (#12803) --- .../components/deep_sleep/deep_sleep_esp32.cpp | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/esphome/components/deep_sleep/deep_sleep_esp32.cpp b/esphome/components/deep_sleep/deep_sleep_esp32.cpp index ea1cd00c5f7..79c34f627a9 100644 --- a/esphome/components/deep_sleep/deep_sleep_esp32.cpp +++ b/esphome/components/deep_sleep/deep_sleep_esp32.cpp @@ -127,22 +127,14 @@ void DeepSleepComponent::deep_sleep_() { defined(USE_ESP32_VARIANT_ESP32C61) if (this->wakeup_pin_ != nullptr) { const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin()); - if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) { - gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY); - } else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) { - gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY); - } - gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT); - gpio_hold_en(gpio_pin); -#if !SOC_GPIO_SUPPORT_HOLD_SINGLE_IO_IN_DSLP - // Some ESP32 variants support holding a single GPIO during deep sleep without this function - // For those variants, gpio_hold_en() is sufficient to hold the pin state during deep sleep - gpio_deep_sleep_hold_en(); -#endif + // Make sure GPIO is in input mode, not all RTC GPIO pins are input by default + gpio_set_direction(gpio_pin, GPIO_MODE_INPUT); bool level = !this->wakeup_pin_->is_inverted(); if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) { level = !level; } + // Internal pullup/pulldown resistors are enabled automatically, when + // ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS is set (by default it is) esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(), static_cast(level)); } From 71d532a34947c5dbf9ac385ee6a716f8fafd9bb4 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Mon, 12 Jan 2026 19:31:09 +0100 Subject: [PATCH 12/20] [nrf52,sdk] Add framework version support (#12489) --- esphome/components/nrf52/__init__.py | 48 ++++++++++++++++--- esphome/components/zephyr/__init__.py | 29 ++++++----- .../components/nrf52/test.nrf52-adafruit.yaml | 2 + 3 files changed, 61 insertions(+), 18 deletions(-) diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index bf90a41df50..5fb8abddfc8 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -8,6 +8,7 @@ from esphome import pins import esphome.codegen as cg from esphome.components.zephyr import ( copy_files as zephyr_copy_files, + zephyr_add_overlay, zephyr_add_pm_static, zephyr_add_prj_conf, zephyr_data, @@ -26,6 +27,7 @@ from esphome.const import ( CONF_FRAMEWORK, CONF_ID, CONF_RESET_PIN, + CONF_VERSION, CONF_VOLTAGE, KEY_CORE, KEY_FRAMEWORK_VERSION, @@ -59,7 +61,6 @@ def set_core_data(config: ConfigType) -> ConfigType: zephyr_set_core_data(config) CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52 CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR - CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version(2, 6, 1) if config[KEY_BOOTLOADER] in BOOTLOADER_CONFIG: zephyr_add_pm_static(BOOTLOADER_CONFIG[config[KEY_BOOTLOADER]]) @@ -67,6 +68,12 @@ def set_core_data(config: ConfigType) -> ConfigType: return config +def set_framework(config: ConfigType) -> ConfigType: + version = cv.Version.parse(cv.version_number(config[CONF_FRAMEWORK][CONF_VERSION])) + CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = version + return config + + BOOTLOADERS = [ BOOTLOADER_ADAFRUIT, BOOTLOADER_ADAFRUIT_NRF52_SD132, @@ -133,8 +140,14 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_UICR_ERASE, default=False): cv.boolean, } ), + cv.Optional(CONF_FRAMEWORK, default={CONF_VERSION: "2.6.1-7"}): cv.Schema( + { + cv.Required(CONF_VERSION): cv.string_strict, + } + ), } ), + set_framework, ) @@ -173,7 +186,7 @@ async def to_code(config: ConfigType) -> None: cg.add_platformio_option( "platform_packages", [ - "platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-7.zip", + f"platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v{CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}.zip", "platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.17.4-0.zip", ], ) @@ -200,7 +213,17 @@ async def to_code(config: ConfigType) -> None: if dfu_config := config.get(CONF_DFU): CORE.add_job(_dfu_to_code, dfu_config) - zephyr_add_prj_conf("BOARD_ENABLE_DCDC", config[CONF_DCDC]) + framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if framework_ver < cv.Version(2, 9, 2): + zephyr_add_prj_conf("BOARD_ENABLE_DCDC", config[CONF_DCDC]) + else: + zephyr_add_overlay( + f""" + ®1 {{ + regulator-initial-mode = <{"NRF5X_REG_MODE_DCDC" if config[CONF_DCDC] else "NRF5X_REG_MODE_LDO"}>; + }}; + """ + ) if reg0_config := config.get(CONF_REG0): value = VOLTAGE_LEVELS.index(reg0_config[CONF_VOLTAGE]) @@ -209,8 +232,12 @@ async def to_code(config: ConfigType) -> None: cg.add_define("USE_NRF52_UICR_ERASE") # c++ support - zephyr_add_prj_conf("CPLUSPLUS", True) - zephyr_add_prj_conf("LIB_CPLUSPLUS", True) + if framework_ver < cv.Version(2, 9, 2): + zephyr_add_prj_conf("CPLUSPLUS", True) + zephyr_add_prj_conf("LIB_CPLUSPLUS", True) + else: + zephyr_add_prj_conf("CPP", True) + zephyr_add_prj_conf("REQUIRES_FULL_LIBCPP", True) # watchdog zephyr_add_prj_conf("WATCHDOG", True) zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False) @@ -218,7 +245,16 @@ async def to_code(config: ConfigType) -> None: zephyr_add_prj_conf("UART_CONSOLE", False) zephyr_add_prj_conf("CONSOLE", False) # use NFC pins as GPIO - zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) + if framework_ver < cv.Version(2, 9, 2): + zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True) + else: + zephyr_add_overlay( + """ + &uicr { + nfct-pins-as-gpios; + }; + """ + ) @coroutine_with_priority(CoroPriority.DIAGNOSTICS) diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index a91d976e6b3..8e3ae86bbe0 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -3,7 +3,8 @@ import textwrap from typing import TypedDict import esphome.codegen as cg -from esphome.const import CONF_BOARD +import esphome.config_validation as cv +from esphome.const import CONF_BOARD, KEY_CORE, KEY_FRAMEWORK_VERSION from esphome.core import CORE from esphome.helpers import copy_file_if_changed, write_file_if_changed @@ -150,6 +151,9 @@ def _format_prj_conf_val(value: PrjConfValueType) -> str: def zephyr_add_cdc_acm(config, id): + framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] + if CORE.is_nrf52 and framework_ver >= cv.Version(3, 2, 0): + zephyr_add_prj_conf("CONFIG_USB_DEVICE_STACK_NEXT", False) zephyr_add_prj_conf("USB_DEVICE_STACK", True) zephyr_add_prj_conf("USB_CDC_ACM", True) # prevent device to go to susspend, without this communication stop working in python @@ -159,12 +163,12 @@ def zephyr_add_cdc_acm(config, id): zephyr_add_prj_conf("USB_CDC_ACM_LOG_LEVEL_WRN", True) zephyr_add_overlay( f""" -&zephyr_udc0 {{ - cdc_acm_uart{id}: cdc_acm_uart{id} {{ - compatible = "zephyr,cdc-acm-uart"; - }}; -}}; -""" + &zephyr_udc0 {{ + cdc_acm_uart{id}: cdc_acm_uart{id} {{ + compatible = "zephyr,cdc-acm-uart"; + }}; + }}; + """ ) @@ -184,11 +188,12 @@ def copy_files(): if user: zephyr_add_overlay( f""" -/ {{ - zephyr,user {{ - {[f"{key} = {', '.join(value)};" for key, value in user.items()][0]} -}}; -}};""" + / {{ + zephyr,user {{ + {[f"{key} = {', '.join(value)};" for key, value in user.items()][0]} + }}; + }}; + """ ) want_opts = zephyr_data()[KEY_PRJ_CONF] diff --git a/tests/components/nrf52/test.nrf52-adafruit.yaml b/tests/components/nrf52/test.nrf52-adafruit.yaml index 5fa0d6e88fb..0ad31993aed 100644 --- a/tests/components/nrf52/test.nrf52-adafruit.yaml +++ b/tests/components/nrf52/test.nrf52-adafruit.yaml @@ -19,3 +19,5 @@ nrf52: reg0: voltage: 2.1V uicr_erase: true + framework: + version: "2.6.1-7" From 9f9341a7005701d53f6c0cd1b863bf51b2891958 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jan 2026 08:42:10 -1000 Subject: [PATCH 13/20] [web_server] Fix select compilation error in v1 (#13169) --- esphome/components/web_server/web_server_v1.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/web_server/web_server_v1.cpp b/esphome/components/web_server/web_server_v1.cpp index c3fe6f67804..ae4bbfa557b 100644 --- a/esphome/components/web_server/web_server_v1.cpp +++ b/esphome/components/web_server/web_server_v1.cpp @@ -202,7 +202,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) { stream.print(""); for (auto const &option : select->traits.get_options()) { stream.print(""); } stream.print(""); From c50bf45496e8a4c956dd37a2ed78654869cecf34 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:09:54 -0500 Subject: [PATCH 14/20] [ltr_als_ps] Remove incorrect device_class from count sensors (#13167) Co-authored-by: Claude Opus 4.5 --- esphome/components/ltr_als_ps/sensor.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/esphome/components/ltr_als_ps/sensor.py b/esphome/components/ltr_als_ps/sensor.py index 27263d0bffb..0dbcff1bfbb 100644 --- a/esphome/components/ltr_als_ps/sensor.py +++ b/esphome/components/ltr_als_ps/sensor.py @@ -16,7 +16,6 @@ from esphome.const import ( CONF_REPEAT, CONF_TRIGGER_ID, CONF_TYPE, - DEVICE_CLASS_DISTANCE, DEVICE_CLASS_ILLUMINANCE, ICON_BRIGHTNESS_5, ICON_BRIGHTNESS_6, @@ -169,7 +168,6 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_5, accuracy_decimals=0, - device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ), key=CONF_NAME, @@ -179,7 +177,6 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_COUNTS, icon=ICON_BRIGHTNESS_7, accuracy_decimals=0, - device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ), key=CONF_NAME, @@ -189,7 +186,6 @@ CONFIG_SCHEMA = cv.All( unit_of_measurement=UNIT_COUNTS, icon=ICON_PROXIMITY, accuracy_decimals=0, - device_class=DEVICE_CLASS_DISTANCE, state_class=STATE_CLASS_MEASUREMENT, ), key=CONF_NAME, @@ -198,7 +194,6 @@ CONFIG_SCHEMA = cv.All( sensor.sensor_schema( icon=ICON_GAIN, accuracy_decimals=0, - device_class=DEVICE_CLASS_ILLUMINANCE, state_class=STATE_CLASS_MEASUREMENT, ), key=CONF_NAME, From f9ffd134df87dddbb411623bfafbf39348f13b4b Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:10:15 -0500 Subject: [PATCH 15/20] [packet_transport] Fix packet size check to account for round4 padding (#13165) Co-authored-by: Claude Opus 4.5 --- esphome/components/packet_transport/packet_transport.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 4a53ab110b5..cefe9a604e7 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -274,7 +274,7 @@ void PacketTransport::flush_() { void PacketTransport::add_binary_data_(uint8_t key, const char *id, bool data) { auto len = 1 + 1 + 1 + strlen(id); - if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) { + if (round4(this->header_.size()) + round4(this->data_.size() + len) > this->get_max_packet_size()) { this->flush_(); this->init_data_(); } @@ -289,7 +289,7 @@ void PacketTransport::add_data_(uint8_t key, const char *id, float data) { void PacketTransport::add_data_(uint8_t key, const char *id, uint32_t data) { auto len = 4 + 1 + 1 + strlen(id); - if (len + this->header_.size() + this->data_.size() > this->get_max_packet_size()) { + if (round4(this->header_.size()) + round4(this->data_.size() + len) > this->get_max_packet_size()) { this->flush_(); this->init_data_(); } From 81e639a6bad5d92e3789ac1f4261953317ee5a4a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jan 2026 09:35:49 -1000 Subject: [PATCH 16/20] [core] Migrate callers and soft deprecate get_mac_address()/get_mac_address_pretty() (#13157) --- esphome/components/debug/debug_zephyr.cpp | 10 ++++++---- esphome/components/mqtt/mqtt_client.cpp | 9 ++++++--- esphome/components/mqtt/mqtt_component.cpp | 11 +++++++++-- esphome/core/helpers.h | 4 ++++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/esphome/components/debug/debug_zephyr.cpp b/esphome/components/debug/debug_zephyr.cpp index 85880595b60..3f9af03b2be 100644 --- a/esphome/components/debug/debug_zephyr.cpp +++ b/esphome/components/debug/debug_zephyr.cpp @@ -322,6 +322,8 @@ size_t DebugComponent::get_device_info_(std::span return "Unspecified"; }; + char mac_pretty[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + get_mac_address_pretty_into_buffer(mac_pretty); ESP_LOGD(TAG, "Code page size: %u, code size: %u, device id: 0x%08x%08x\n" "Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n" @@ -330,10 +332,10 @@ size_t DebugComponent::get_device_info_(std::span "RAM: %ukB, Flash: %ukB, production test: %sdone", NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0], NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2], - NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), get_mac_address_pretty().c_str(), - NRF_FICR->INFO.PART, NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, - NRF_FICR->INFO.VARIANT >> 8 & 0xFF, NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE), - NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); + NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), mac_pretty, NRF_FICR->INFO.PART, + NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, NRF_FICR->INFO.VARIANT >> 8 & 0xFF, + NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE), NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, + (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not ")); bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] && (NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected << UICR_PSELRESET_CONNECT_Pos; diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 652f55734b2..0ab5b238b54 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -28,8 +28,9 @@ static const char *const TAG = "mqtt"; MQTTClientComponent::MQTTClientComponent() { global_mqtt_client = this; - const std::string mac_addr = get_mac_address(); - this->credentials_.client_id = make_name_with_suffix(App.get_name(), '-', mac_addr.c_str(), mac_addr.size()); + char mac_addr[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(mac_addr); + this->credentials_.client_id = make_name_with_suffix(App.get_name(), '-', mac_addr, MAC_ADDRESS_BUFFER_SIZE - 1); } // Connection @@ -102,7 +103,9 @@ void MQTTClientComponent::send_device_info_() { root[ESPHOME_F("port")] = api::global_api_server->get_port(); #endif root[ESPHOME_F("version")] = ESPHOME_VERSION; - root[ESPHOME_F("mac")] = get_mac_address(); + char mac_buf[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(mac_buf); + root[ESPHOME_F("mac")] = mac_buf; #ifdef USE_ESP8266 root[ESPHOME_F("platform")] = ESPHOME_F("ESP8266"); diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index d838d1789f5..40eb15acddd 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -187,7 +187,13 @@ bool MQTTComponent::send_discovery_() { char friendly_name_hash[9]; sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_())); friendly_name_hash[8] = 0; // ensure the hash-string ends with null - root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash; + // Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678") + // MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43 + char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11]; + char mac_buf[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(mac_buf); + snprintf(unique_id, sizeof(unique_id), "%s-%s-%s", mac_buf, this->component_type(), friendly_name_hash); + root[MQTT_UNIQUE_ID] = unique_id; } else { // default to almost-unique ID. It's a hack but the only way to get that // gorgeous device registry view. @@ -203,7 +209,8 @@ bool MQTTComponent::send_discovery_() { std::string node_area = App.get_area(); JsonObject device_info = root[MQTT_DEVICE].to(); - const auto mac = get_mac_address(); + char mac[MAC_ADDRESS_BUFFER_SIZE]; + get_mac_address_into_buffer(mac); device_info[MQTT_DEVICE_IDENTIFIERS] = mac; device_info[MQTT_DEVICE_NAME] = node_friendly_name; #ifdef ESPHOME_PROJECT_NAME diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 396a58464f0..8847586f0a3 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1306,9 +1306,13 @@ class HighFrequencyLoopRequester { void get_mac_address_raw(uint8_t *mac); // NOLINT(readability-non-const-parameter) /// Get the device MAC address as a string, in lowercase hex notation. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +/// Use get_mac_address_into_buffer() instead. std::string get_mac_address(); /// Get the device MAC address as a string, in colon-separated uppercase hex notation. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. +/// Use get_mac_address_pretty_into_buffer() instead. std::string get_mac_address_pretty(); /// Get the device MAC address into the given buffer, in lowercase hex notation. From 655e2b43cbc7c34f1ed2307d1b0e4cfcee8c7ae7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jan 2026 11:27:42 -1000 Subject: [PATCH 17/20] [infrared] Use set_data() for vector timings in control() (#13171) --- esphome/components/infrared/infrared.cpp | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/esphome/components/infrared/infrared.cpp b/esphome/components/infrared/infrared.cpp index 384ff431a54..5f8d63926a9 100644 --- a/esphome/components/infrared/infrared.cpp +++ b/esphome/components/infrared/infrared.cpp @@ -88,27 +88,15 @@ void Infrared::control(const InfraredCall &call) { // Set timings based on format if (call.is_packed()) { // Zero-copy from packed protobuf data - ESP_LOGD(TAG, "Transmitting raw timings: timing_count=%u, repeat_count=%u", call.get_packed_count(), - call.get_repeat_count()); transmit_data->set_data_from_packed_sint32(call.get_packed_data(), call.get_packed_length(), call.get_packed_count()); + ESP_LOGD(TAG, "Transmitting packed raw timings: count=%u, repeat=%u", call.get_packed_count(), + call.get_repeat_count()); } else { // From vector (lambdas/automations) - const auto &timings = call.get_raw_timings(); - if (timings.empty()) { - ESP_LOGE(TAG, "Raw timings array is empty"); - return; - } - ESP_LOGD(TAG, "Transmitting raw timings: timing_count=%zu, repeat_count=%u", timings.size(), + transmit_data->set_data(call.get_raw_timings()); + ESP_LOGD(TAG, "Transmitting raw timings: count=%zu, repeat=%u", call.get_raw_timings().size(), call.get_repeat_count()); - // Timings format: positive values = mark (LED on), negative values = space (LED off) - for (const auto &timing : timings) { - if (timing > 0) { - transmit_data->mark(static_cast(timing)); - } else { - transmit_data->space(static_cast(-timing)); - } - } } // Set repeat count From 889886909be4858cf58dacd5ee46995fb02540a1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Jan 2026 12:48:54 -1000 Subject: [PATCH 18/20] [core] Soft deprecate heap-allocating string helpers to prevent fragmentation patterns (#13156) --- .ai/instructions.md | 25 ++++++++++++++--------- esphome/core/helpers.h | 40 ++++++++++++++++++++++++++++++++++-- script/ci-custom.py | 46 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 12 deletions(-) diff --git a/.ai/instructions.md b/.ai/instructions.md index 994d517f75e..3c24177827f 100644 --- a/.ai/instructions.md +++ b/.ai/instructions.md @@ -293,6 +293,12 @@ This document provides essential context for AI models interacting with this pro * **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization. * **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage. + **Why Heap Allocation Matters:** + + ESP devices run for months with small heaps shared between Wi-Fi, BLE, LWIP, and application code. Over time, repeated allocations of different sizes fragment the heap. Failures happen when the largest contiguous block shrinks, even if total free heap is still large. We have seen field crashes caused by this. + + **Heap allocation after `setup()` should be avoided unless absolutely unavoidable.** Every allocation/deallocation cycle contributes to fragmentation. ESPHome treats runtime heap allocation as a long-term reliability bug, not a performance issue. Helpers that hide allocation (`std::string`, `std::to_string`, string-returning helpers) are being deprecated and replaced with buffer and view based APIs. + **STL Container Guidelines:** ESPHome runs on embedded systems with limited resources. Choose containers carefully: @@ -322,15 +328,15 @@ This document provides essential context for AI models interacting with this pro std::array buffer; ``` - 2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for fixed-size stack allocation with `push_back()` interface. + 2. **Compile-time-known fixed sizes with vector-like API:** Use `StaticVector` from `esphome/core/helpers.h` for compile-time fixed size with `push_back()` interface (no dynamic allocation). ```cpp // Bad - generates STL realloc code (_M_realloc_insert) std::vector services; services.reserve(5); // Still includes reallocation machinery - // Good - compile-time fixed size, stack allocated, no reallocation machinery - StaticVector services; // Allocates all MAX_SERVICES on stack - services.push_back(record1); // Tracks count but all slots allocated + // Good - compile-time fixed size, no dynamic allocation + StaticVector services; + services.push_back(record1); ``` Use `cg.add_define("MAX_SERVICES", count)` to set the size from Python configuration. Like `std::array` but with vector-like API (`push_back()`, `size()`) and no STL reallocation code. @@ -372,22 +378,21 @@ This document provides essential context for AI models interacting with this pro ``` Linear search on small datasets (1-16 elements) is often faster than hashing/tree overhead, but this depends on lookup frequency and access patterns. For frequent lookups in hot code paths, the O(1) vs O(n) complexity difference may still matter even for small datasets. `std::vector` with simple structs is usually fine—it's the heavy containers (`map`, `set`, `unordered_map`) that should be avoided for small datasets unless profiling shows otherwise. - 5. **Detection:** Look for these patterns in compiler output: + 5. **Avoid `std::deque`:** It allocates in 512-byte blocks regardless of element size, guaranteeing at least 512 bytes of RAM usage immediately. This is a major source of crashes on memory-constrained devices. + + 6. **Detection:** Look for these patterns in compiler output: - Large code sections with STL symbols (vector, map, set) - `alloc`, `realloc`, `dealloc` in symbol names - `_M_realloc_insert`, `_M_default_append` (vector reallocation) - Red-black tree code (`rb_tree`, `_Rb_tree`) - Hash table infrastructure (`unordered_map`, `hash`) - **When to optimize:** + **Prioritize optimization effort for:** - Core components (API, network, logger) - Widely-used components (mdns, wifi, ble) - Components causing flash size complaints - **When not to optimize:** - - Single-use niche components - - Code where readability matters more than bytes - - Already using appropriate containers + Note: Avoiding heap allocation after `setup()` is always required regardless of component type. The prioritization above is about the effort spent on container optimization (e.g., migrating from `std::vector` to `StaticVector`). * **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals. diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 8847586f0a3..2e9c0e6b13b 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -520,6 +520,7 @@ bool str_startswith(const std::string &str, const std::string &start); bool str_endswith(const std::string &str, const std::string &end); /// Truncate a string to a specific length. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. std::string str_truncate(const std::string &str, size_t length); /// Extract the part of the string until either the first occurrence of the specified character, or the end @@ -531,11 +532,13 @@ std::string str_until(const std::string &str, char ch); /// Convert the string to lower case. std::string str_lower_case(const std::string &str); /// Convert the string to upper case. +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. std::string str_upper_case(const std::string &str); /// Convert a single char to snake_case: lowercase and space to underscore. constexpr char to_snake_case_char(char c) { return (c == ' ') ? '_' : (c >= 'A' && c <= 'Z') ? c + ('a' - 'A') : c; } /// Convert the string to snake case (lowercase with underscores). +/// @warning Allocates heap memory. Avoid in new code - causes heap fragmentation on long-running devices. std::string str_snake_case(const std::string &str); /// Sanitize a single char: keep alphanumerics, dashes, underscores; replace others with underscore. @@ -758,6 +761,16 @@ inline char *format_hex_to(char (&buffer)[N], T val) { return format_hex_to(buffer, reinterpret_cast(&val), sizeof(T)); } +/// Format std::vector as lowercase hex to buffer. +template inline char *format_hex_to(char (&buffer)[N], const std::vector &data) { + return format_hex_to(buffer, data.data(), data.size()); +} + +/// Format std::array as lowercase hex to buffer. +template inline char *format_hex_to(char (&buffer)[N], const std::array &data) { + return format_hex_to(buffer, data.data(), data.size()); +} + /// Calculate buffer size needed for format_hex_to: "XXXXXXXX...\0" = bytes * 2 + 1 constexpr size_t format_hex_size(size_t byte_count) { return byte_count * 2 + 1; } @@ -807,6 +820,18 @@ inline char *format_hex_pretty_to(char (&buffer)[N], const uint8_t *data, size_t return format_hex_pretty_to(buffer, N, data, length, separator); } +/// Format std::vector as uppercase hex with separator to buffer. +template +inline char *format_hex_pretty_to(char (&buffer)[N], const std::vector &data, char separator = ':') { + return format_hex_pretty_to(buffer, data.data(), data.size(), separator); +} + +/// Format std::array as uppercase hex with separator to buffer. +template +inline char *format_hex_pretty_to(char (&buffer)[N], const std::array &data, char separator = ':') { + return format_hex_pretty_to(buffer, data.data(), data.size(), separator); +} + /// Calculate buffer size needed for format_hex_pretty_to with uint16_t data: "XXXX:XXXX:...:XXXX\0" constexpr size_t format_hex_pretty_uint16_size(size_t count) { return count * 5; } @@ -840,8 +865,8 @@ static constexpr size_t MAC_ADDRESS_PRETTY_BUFFER_SIZE = format_hex_pretty_size( static constexpr size_t MAC_ADDRESS_BUFFER_SIZE = MAC_ADDRESS_SIZE * 2 + 1; /// Format MAC address as XX:XX:XX:XX:XX:XX (uppercase, colon separators) -inline void format_mac_addr_upper(const uint8_t *mac, char *output) { - format_hex_pretty_to(output, MAC_ADDRESS_PRETTY_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE, ':'); +inline char *format_mac_addr_upper(const uint8_t *mac, char *output) { + return format_hex_pretty_to(output, MAC_ADDRESS_PRETTY_BUFFER_SIZE, mac, MAC_ADDRESS_SIZE, ':'); } /// Format MAC address as xxxxxxxxxxxxxx (lowercase, no separators) @@ -850,16 +875,27 @@ inline void format_mac_addr_lower_no_sep(const uint8_t *mac, char *output) { } /// Format the six-byte array \p mac into a MAC address. +/// @warning Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead. +/// Causes heap fragmentation on long-running devices. std::string format_mac_address_pretty(const uint8_t mac[6]); /// Format the byte array \p data of length \p len in lowercased hex. +/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. +/// Causes heap fragmentation on long-running devices. std::string format_hex(const uint8_t *data, size_t length); /// Format the vector \p data in lowercased hex. +/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. +/// Causes heap fragmentation on long-running devices. std::string format_hex(const std::vector &data); /// Format an unsigned integer in lowercased hex, starting with the most significant byte. +/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. +/// Causes heap fragmentation on long-running devices. template::value, int> = 0> std::string format_hex(T val) { val = convert_big_endian(val); return format_hex(reinterpret_cast(&val), sizeof(T)); } +/// Format the std::array \p data in lowercased hex. +/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead. +/// Causes heap fragmentation on long-running devices. template std::string format_hex(const std::array &data) { return format_hex(data.data(), data.size()); } diff --git a/script/ci-custom.py b/script/ci-custom.py index 77d2ab287d0..1e2d07885e3 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -679,6 +679,52 @@ def lint_trailing_whitespace(fname, match): return "Trailing whitespace detected" +# Heap-allocating helpers that cause fragmentation on long-running embedded devices. +# These return std::string and should be replaced with stack-based alternatives. +HEAP_ALLOCATING_HELPERS = { + "format_hex": "format_hex_to() with a stack buffer", + "format_mac_address_pretty": "format_mac_addr_upper() with a stack buffer", + "get_mac_address": "get_mac_address_into_buffer() with a stack buffer", + "get_mac_address_pretty": "get_mac_address_pretty_into_buffer() with a stack buffer", + "str_truncate": "removal (function is unused)", + "str_upper_case": "removal (function is unused)", + "str_snake_case": "removal (function is unused)", +} + + +@lint_re_check( + # Use negative lookahead to exclude _to/_into_buffer variants + # format_hex(?!_) ensures we don't match format_hex_to, format_hex_pretty_to, etc. + # get_mac_address(?!_) ensures we don't match get_mac_address_into_buffer, etc. + r"[^\w](" + r"format_hex(?!_)|" + r"format_mac_address_pretty|" + r"get_mac_address_pretty(?!_)|" + r"get_mac_address(?!_)|" + r"str_truncate|" + r"str_upper_case|" + r"str_snake_case" + r")\s*\(", + include=cpp_include, + exclude=[ + # The definitions themselves + "esphome/core/helpers.h", + "esphome/core/helpers.cpp", + ], +) +def lint_no_heap_allocating_helpers(fname, match): + func = match.group(1) + replacement = HEAP_ALLOCATING_HELPERS.get(func, "a stack-based alternative") + return ( + f"{highlight(func + '()')} allocates heap memory. On long-running embedded devices, " + f"repeated heap allocations fragment memory over time. Even infrequent allocations " + f"become time bombs - the heap eventually cannot satisfy requests even with free " + f"memory available.\n" + f"Please use {replacement} instead.\n" + f"(If strictly necessary, add `// NOLINT` to the end of the line)" + ) + + @lint_content_find_check( "ESP_LOG", include=["*.h", "*.tcc"], From 54fc10714d72c835bd16ec9f0c638614cbe9bbe8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 12 Jan 2026 18:06:41 -0500 Subject: [PATCH 19/20] [remote_transmitter] Fix ESP8266 timing by using busy loop (#13172) Co-authored-by: Claude --- .../components/remote_transmitter/remote_transmitter.cpp | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/esphome/components/remote_transmitter/remote_transmitter.cpp b/esphome/components/remote_transmitter/remote_transmitter.cpp index 576143bcbcb..f20789fb9f2 100644 --- a/esphome/components/remote_transmitter/remote_transmitter.cpp +++ b/esphome/components/remote_transmitter/remote_transmitter.cpp @@ -40,13 +40,10 @@ void RemoteTransmitterComponent::await_target_time_() { if (this->target_time_ == 0) { this->target_time_ = current_time; } else if ((int32_t) (this->target_time_ - current_time) > 0) { -#if defined(USE_LIBRETINY) || defined(USE_RP2040) - // busy loop is required for libretiny and rp2040 as interrupts are disabled + // busy loop is required as interrupts are disabled and delayMicroseconds() + // may not work correctly in interrupt-disabled contexts on all platforms while ((int32_t) (this->target_time_ - micros()) > 0) ; -#else - delayMicroseconds(this->target_time_ - current_time); -#endif } } From 297f05d60015cd524efba118dfcf8b32aa617e5d Mon Sep 17 00:00:00 2001 From: lullius Date: Tue, 13 Jan 2026 00:08:33 +0100 Subject: [PATCH 20/20] [tuya] add color_type_lowercase option (#13101) Co-authored-by: lullius <> --- esphome/components/tuya/light/__init__.py | 3 +++ esphome/components/tuya/light/tuya_light.cpp | 11 +++++++---- esphome/components/tuya/light/tuya_light.h | 8 +++----- tests/components/tuya/common.yaml | 3 +++ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/esphome/components/tuya/light/__init__.py b/esphome/components/tuya/light/__init__.py index 4d2ccba8b19..bf2d3daf981 100644 --- a/esphome/components/tuya/light/__init__.py +++ b/esphome/components/tuya/light/__init__.py @@ -26,6 +26,7 @@ CONF_RGB_DATAPOINT = "rgb_datapoint" CONF_HSV_DATAPOINT = "hsv_datapoint" CONF_COLOR_DATAPOINT = "color_datapoint" CONF_COLOR_TYPE = "color_type" +CONF_COLOR_TYPE_LOWERCASE = "color_type_lowercase" TuyaColorType = tuya_ns.enum("TuyaColorType") @@ -47,6 +48,7 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t, cv.Inclusive(CONF_COLOR_DATAPOINT, "color"): cv.uint8_t, cv.Inclusive(CONF_COLOR_TYPE, "color"): cv.enum(COLOR_TYPES, upper=True), + cv.Optional(CONF_COLOR_TYPE_LOWERCASE, default=False): cv.boolean, cv.Optional(CONF_COLOR_INTERLOCK, default=False): cv.boolean, cv.Inclusive( CONF_COLOR_TEMPERATURE_DATAPOINT, "color_temperature" @@ -91,6 +93,7 @@ async def to_code(config): if CONF_COLOR_DATAPOINT in config: cg.add(var.set_color_id(config[CONF_COLOR_DATAPOINT])) cg.add(var.set_color_type(config[CONF_COLOR_TYPE])) + cg.add(var.set_color_type_lowercase(config[CONF_COLOR_TYPE_LOWERCASE])) if CONF_COLOR_TEMPERATURE_DATAPOINT in config: cg.add(var.set_color_temperature_id(config[CONF_COLOR_TEMPERATURE_DATAPOINT])) cg.add(var.set_color_temperature_invert(config[CONF_COLOR_TEMPERATURE_INVERT])) diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index 815a089d9f5..c487f9f50bf 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -190,7 +190,8 @@ void TuyaLight::write_state(light::LightState *state) { switch (*this->color_type_) { case TuyaColorType::RGB: { char buffer[7]; - sprintf(buffer, "%02X%02X%02X", int(red * 255), int(green * 255), int(blue * 255)); + const char *format_str = this->color_type_lowercase_ ? "%02x%02x%02x" : "%02X%02X%02X"; + sprintf(buffer, format_str, int(red * 255), int(green * 255), int(blue * 255)); color_value = buffer; break; } @@ -199,7 +200,8 @@ void TuyaLight::write_state(light::LightState *state) { float saturation, value; rgb_to_hsv(red, green, blue, hue, saturation, value); char buffer[13]; - sprintf(buffer, "%04X%04X%04X", hue, int(saturation * 1000), int(value * 1000)); + const char *format_str = this->color_type_lowercase_ ? "%04x%04x%04x" : "%04X%04X%04X"; + sprintf(buffer, format_str, hue, int(saturation * 1000), int(value * 1000)); color_value = buffer; break; } @@ -208,8 +210,9 @@ void TuyaLight::write_state(light::LightState *state) { float saturation, value; rgb_to_hsv(red, green, blue, hue, saturation, value); char buffer[15]; - sprintf(buffer, "%02X%02X%02X%04X%02X%02X", int(red * 255), int(green * 255), int(blue * 255), hue, - int(saturation * 255), int(value * 255)); + const char *format_str = this->color_type_lowercase_ ? "%02x%02x%02x%04x%02x%02x" : "%02X%02X%02X%04X%02X%02X"; + sprintf(buffer, format_str, int(red * 255), int(green * 255), int(blue * 255), hue, int(saturation * 255), + int(value * 255)); color_value = buffer; break; } diff --git a/esphome/components/tuya/light/tuya_light.h b/esphome/components/tuya/light/tuya_light.h index bd9920f18f3..ded94f390af 100644 --- a/esphome/components/tuya/light/tuya_light.h +++ b/esphome/components/tuya/light/tuya_light.h @@ -7,11 +7,7 @@ namespace esphome { namespace tuya { -enum TuyaColorType { - RGB, - HSV, - RGBHSV, -}; +enum TuyaColorType { RGB, HSV, RGBHSV }; class TuyaLight : public Component, public light::LightOutput { public: @@ -28,6 +24,7 @@ class TuyaLight : public Component, public light::LightOutput { void set_color_temperature_invert(bool color_temperature_invert) { this->color_temperature_invert_ = color_temperature_invert; } + void set_color_type_lowercase(bool color_type_lowercase) { this->color_type_lowercase_ = color_type_lowercase; } void set_tuya_parent(Tuya *parent) { this->parent_ = parent; } void set_min_value(uint32_t min_value) { min_value_ = min_value; } void set_max_value(uint32_t max_value) { max_value_ = max_value; } @@ -63,6 +60,7 @@ class TuyaLight : public Component, public light::LightOutput { float cold_white_temperature_; float warm_white_temperature_; bool color_temperature_invert_{false}; + bool color_type_lowercase_{false}; bool color_interlock_{false}; light::LightState *state_{nullptr}; }; diff --git a/tests/components/tuya/common.yaml b/tests/components/tuya/common.yaml index e177b7d056f..9986d398f1d 100644 --- a/tests/components/tuya/common.yaml +++ b/tests/components/tuya/common.yaml @@ -38,11 +38,14 @@ light: dimmer_datapoint: 2 min_value_datapoint: 3 color_temperature_datapoint: 4 + color_datapoint: 5 min_value: 1 max_value: 100 cold_white_color_temperature: 153 mireds warm_white_color_temperature: 500 mireds gamma_correct: 1 + color_type: RGB + color_type_lowercase: true number: - platform: tuya