Merge remote-tracking branch 'upstream/dev' into integration

This commit is contained in:
J. Nick Koston
2026-01-12 15:52:36 -10:00
51 changed files with 1018 additions and 127 deletions
+12 -11
View File
@@ -295,7 +295,9 @@ This document provides essential context for AI models interacting with this pro
**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. For this reason, ESPHome treats runtime heap allocation in hot paths as a 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.
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:**
@@ -326,15 +328,15 @@ This document provides essential context for AI models interacting with this pro
std::array<uint8_t, 256> 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<ServiceRecord> services;
services.reserve(5); // Still includes reallocation machinery
// Good - compile-time fixed size, stack allocated, no reallocation machinery
StaticVector<ServiceRecord, MAX_SERVICES> 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<ServiceRecord, MAX_SERVICES> 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.
@@ -376,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.
+1
View File
@@ -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
+48
View File
@@ -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<int32_t>"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods
}
+32
View File
@@ -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<infrared::Infrared *>(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,
+9
View File
@@ -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);
+93
View File
@@ -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<uint32_t>(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<uint32_t>(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
+65
View File
@@ -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<int32_t> *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
+44
View File
@@ -2293,6 +2293,50 @@ void ZWaveProxyRequest::dump_to(std::string &out) const {
dump_bytes_field(out, "data", this->data, this->data_len);
}
#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<enums::EntityCategory>(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
@@ -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
+11
View File
@@ -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;
};
+15
View File
@@ -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<int32_t> *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
+3
View File
@@ -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<int32_t> *timings);
#endif
bool is_connected(bool state_subscription_only = false) const;
+3
View File
@@ -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
+3
View File
@@ -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
+3
View File
@@ -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
@@ -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";
@@ -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<esp_deepsleep_gpio_wake_up_mode_t>(level));
}
+10
View File
@@ -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:
+14 -1
View File
@@ -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;
+7
View File
@@ -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<uint32_t> tx_queue_len_{};
optional<uint32_t> rx_queue_len_{};
+76
View File
@@ -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)
+138
View File
@@ -0,0 +1,138 @@
#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<int32_t> &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
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)
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());
}
// 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
+130
View File
@@ -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 <vector>
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<int32_t> &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<uint32_t> &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<int32_t> &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<uint32_t> carrier_frequency_;
// Vector-based timings (for lambdas/automations)
const std::vector<int32_t> *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
-5
View File
@@ -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,
+42 -6
View File
@@ -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"""
&reg1 {{
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)
@@ -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_();
}
@@ -1011,7 +1011,7 @@ void PrometheusHandler::climate_row_(AsyncResponseStream *stream, climate::Clima
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 = "mininum_temperature";
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
+35 -15
View File
@@ -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<float>(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<float>(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<float>(speed) * 10.0f);
target.speed->publish_state(valid ? static_cast<float>(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<float>(x), static_cast<float>(y));
target.distance->publish_state(distance);
if (valid) {
target.distance->publish_state(std::hypot(static_cast<float>(x), static_cast<float>(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<float>(x), static_cast<float>(y)) * 180.0f / M_PI;
target.angle->publish_state(angle);
} else {
target.angle->publish_state(NAN);
}
}
}
@@ -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
}
}
@@ -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]))
+7 -4
View File
@@ -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;
}
+3 -5
View File
@@ -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};
};
@@ -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,9 @@ void TuyaTextSensor::setup() {
this->publish_state(datapoint.value_string);
break;
case TuyaDatapointType::RAW: {
// Text sensor state is limited to 255 bytes, use 256 byte buffer
char hex_buf[256];
const char *formatted = format_hex_pretty_to(hex_buf, sizeof(hex_buf), datapoint.value_raw);
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;
@@ -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
@@ -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
@@ -202,7 +202,7 @@ void WebServer::handle_index_request(AsyncWebServerRequest *request) {
stream.print("<option></option>");
for (auto const &option : select->traits.get_options()) {
stream.print("<option>");
stream.print(option.c_str());
stream.print(option);
stream.print("</option>");
}
stream.print("</select>");
+17 -12
View File
@@ -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]
+10 -1
View File
@@ -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([""])
@@ -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)}))"
+15
View File
@@ -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_heater::WaterHeater *, ESPHOME_ENTITY_WATER_HEATER_COUNT> water_heaters_{};
#endif
#ifdef USE_INFRARED
StaticVector<infrared::Infrared *, ESPHOME_ENTITY_INFRARED_COUNT> infrareds_{};
#endif
#ifdef USE_UPDATE
StaticVector<update::UpdateEntity *, ESPHOME_ENTITY_UPDATE_COUNT> updates_{};
#endif
+6
View File
@@ -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);
+12
View File
@@ -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
+4 -1
View File
@@ -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
+3
View File
@@ -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,
+44 -29
View File
@@ -532,8 +532,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.
/// @deprecated Allocates heap memory and is unused. Removed in 2026.7.0.
ESPDEPRECATED("Allocates heap memory and is unused. Removed in 2026.7.0.", "2026.1.0")
/// @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
@@ -545,15 +544,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.
/// @deprecated Allocates heap memory and is unused. Removed in 2026.7.0.
ESPDEPRECATED("Allocates heap memory and is unused. Removed in 2026.7.0.", "2026.1.0")
/// @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).
/// @deprecated Allocates heap memory and is unused in C++. Removed in 2026.7.0.
ESPDEPRECATED("Allocates heap memory and is unused in C++. Removed in 2026.7.0.", "2026.1.0")
/// @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.
@@ -776,6 +773,16 @@ inline char *format_hex_to(char (&buffer)[N], T val) {
return format_hex_to(buffer, reinterpret_cast<const uint8_t *>(&val), sizeof(T));
}
/// Format std::vector<uint8_t> as lowercase hex to buffer.
template<size_t N> inline char *format_hex_to(char (&buffer)[N], const std::vector<uint8_t> &data) {
return format_hex_to(buffer, data.data(), data.size());
}
/// Format std::array<uint8_t, M> as lowercase hex to buffer.
template<size_t N, size_t M> inline char *format_hex_to(char (&buffer)[N], const std::array<uint8_t, M> &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; }
@@ -825,6 +832,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<uint8_t> as uppercase hex with separator to buffer.
template<size_t N>
inline char *format_hex_pretty_to(char (&buffer)[N], const std::vector<uint8_t> &data, char separator = ':') {
return format_hex_pretty_to(buffer, data.data(), data.size(), separator);
}
/// Format std::array<uint8_t, M> as uppercase hex with separator to buffer.
template<size_t N, size_t M>
inline char *format_hex_pretty_to(char (&buffer)[N], const std::array<uint8_t, M> &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; }
@@ -858,8 +877,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)
@@ -868,34 +887,31 @@ 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.
/// @deprecated Allocates heap memory. Use format_mac_addr_upper() with a stack buffer instead. Removed in 2026.7.0.
ESPDEPRECATED("Allocates heap memory. Use format_mac_addr_upper() with stack buffer. Removed in 2026.7.0.", "2026.1.0")
/// @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.
/// @deprecated Allocates heap memory. Use format_hex_to() with a stack buffer instead. Removed in 2026.7.0.
ESPDEPRECATED("Allocates heap memory. Use format_hex_to() with stack buffer. Removed in 2026.7.0.", "2026.1.0")
/// @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.
/// @deprecated Allocates heap memory. Use format_hex_to() with a stack buffer instead. Removed in 2026.7.0.
ESPDEPRECATED("Allocates heap memory. Use format_hex_to() with stack buffer. Removed in 2026.7.0.", "2026.1.0")
/// @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<uint8_t> &data);
/// Format an unsigned integer in lowercased hex, starting with the most significant byte.
/// @deprecated Allocates heap memory. Use format_hex_to() with a stack buffer instead. Removed in 2026.7.0.
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0>
ESPDEPRECATED("Allocates heap memory. Use format_hex_to() with stack buffer. Removed in 2026.7.0.", "2026.1.0")
std::string format_hex(T val) {
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
/// Causes heap fragmentation on long-running devices.
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex(T val) {
val = convert_big_endian(val);
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
return format_hex(reinterpret_cast<uint8_t *>(&val), sizeof(T));
#pragma GCC diagnostic pop
}
/// @deprecated Allocates heap memory. Use format_hex_to() with a stack buffer instead. Removed in 2026.7.0.
template<std::size_t N>
ESPDEPRECATED("Allocates heap memory. Use format_hex_to() with stack buffer. Removed in 2026.7.0.", "2026.1.0")
std::string format_hex(const std::array<uint8_t, N> &data) {
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
/// 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::size_t N> std::string format_hex(const std::array<uint8_t, N> &data) {
return format_hex(data.data(), data.size());
#pragma GCC diagnostic pop
}
@@ -1342,14 +1358,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.
/// @deprecated Allocates heap memory. Use get_mac_address_into_buffer() instead. Removed in 2026.7.0.
ESPDEPRECATED("Allocates heap memory. Use get_mac_address_into_buffer() instead. Removed in 2026.7.0.", "2026.1.0")
/// @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.
/// @deprecated Allocates heap memory. Use get_mac_address_pretty_into_buffer() instead. Removed in 2026.7.0.
ESPDEPRECATED("Allocates heap memory. Use get_mac_address_pretty_into_buffer() instead. Removed in 2026.7.0.",
"2026.1.0")
/// @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.
+1 -1
View File
@@ -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
+46
View File
@@ -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"],
+1
View File
@@ -18,6 +18,7 @@ canbus:
tx_pin: ${tx_pin}
can_id: 4
bit_rate: 50kbps
mode: NORMAL
on_frame:
- can_id: 500
then:
@@ -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:
@@ -19,3 +19,5 @@ nrf52:
reg0:
voltage: 2.1V
uicr_erase: true
framework:
version: "2.6.1-7"
+3
View File
@@ -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
+1
View File
@@ -37,3 +37,4 @@ datetime:
event:
update:
water_heater:
infrared: