mirror of
https://github.com/esphome/esphome.git
synced 2026-09-18 18:48:39 +00:00
Merge remote-tracking branch 'upstream/dev' into integration
This commit is contained in:
+12
-11
@@ -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.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>");
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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)}))"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -37,3 +37,4 @@ datetime:
|
||||
event:
|
||||
update:
|
||||
water_heater:
|
||||
infrared:
|
||||
|
||||
Reference in New Issue
Block a user