Compare commits

..

10 Commits

Author SHA1 Message Date
J. Nick Koston
1071254f18 Merge branch 'dev' into max6956_gpio_cache_banks 2026-01-07 13:45:12 -10:00
J. Nick Koston
dd616872a1 Merge branch 'dev' into max6956_gpio_cache_banks 2025-10-29 00:39:06 -05:00
J. Nick Koston
0272b53a17 cleanup 2025-09-04 16:08:20 -05:00
J. Nick Koston
2b2fb5958d cleanup 2025-09-04 16:07:50 -05:00
J. Nick Koston
3f6a490cf4 fixes 2025-09-04 15:55:41 -05:00
J. Nick Koston
b424c16dde fixes 2025-09-04 15:55:34 -05:00
J. Nick Koston
dd861ab9b6 align 2025-09-04 15:51:28 -05:00
J. Nick Koston
938a3d8188 align 2025-09-04 15:49:10 -05:00
J. Nick Koston
bdc299789f fix 2025-09-04 15:39:29 -05:00
J. Nick Koston
6e2c3f4981 [max6956] Migrate to CachedGpioExpander to reduce I2C bus usage 2025-09-04 15:35:26 -05:00
240 changed files with 10076 additions and 13081 deletions

View File

@@ -293,12 +293,6 @@ This document provides essential context for AI models interacting with this pro
* **Configuration Design:** Aim for simplicity with sensible defaults, while allowing for advanced customization.
* **Embedded Systems Optimization:** ESPHome targets resource-constrained microcontrollers. Be mindful of flash size and RAM usage.
**Why Heap Allocation Matters:**
ESP devices run for months with small heaps shared between Wi-Fi, BLE, LWIP, and application code. Over time, repeated allocations of different sizes fragment the heap. Failures happen when the largest contiguous block shrinks, even if total free heap is still large. We have seen field crashes caused by this.
**Heap allocation after `setup()` should be avoided unless absolutely unavoidable.** Every allocation/deallocation cycle contributes to fragmentation. ESPHome treats runtime heap allocation as a long-term reliability bug, not a performance issue. Helpers that hide allocation (`std::string`, `std::to_string`, string-returning helpers) are being deprecated and replaced with buffer and view based APIs.
**STL Container Guidelines:**
ESPHome runs on embedded systems with limited resources. Choose containers carefully:
@@ -328,15 +322,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 compile-time fixed size with `push_back()` interface (no dynamic allocation).
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.
```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, no dynamic allocation
StaticVector<ServiceRecord, MAX_SERVICES> services;
services.push_back(record1);
// 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
```
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.
@@ -378,21 +372,22 @@ 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. **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:
5. **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`)
**Prioritize optimization effort for:**
**When to optimize:**
- Core components (API, network, logger)
- Widely-used components (mdns, wifi, ble)
- Components causing flash size complaints
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`).
**When not to optimize:**
- Single-use niche components
- Code where readability matters more than bytes
- Already using appropriate containers
* **State Management:** Use `CORE.data` for component state that needs to persist during configuration generation. Avoid module-level mutable globals.

View File

@@ -1 +1 @@
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65
191a0e6ab5842d153dd77a2023bc5742f9d4333c334de8d81b57f2b8d4d4b65e

View File

@@ -11,7 +11,7 @@ ci:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.14.11
rev: v0.14.10
hooks:
# Run the linter.
- id: ruff

View File

@@ -249,7 +249,6 @@ 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

View File

@@ -1,3 +1,5 @@
#ifdef USE_ARDUINO
#include "ac_dimmer.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
@@ -7,12 +9,12 @@
#ifdef USE_ESP8266
#include <core_esp8266_waveform.h>
#endif
#ifdef USE_ESP32
#include "hw_timer_esp_idf.h"
#ifdef USE_ESP32_FRAMEWORK_ARDUINO
#include <esp32-hal-timer.h>
#endif
namespace esphome::ac_dimmer {
namespace esphome {
namespace ac_dimmer {
static const char *const TAG = "ac_dimmer";
@@ -25,14 +27,7 @@ static AcDimmerDataStore *all_dimmers[32]; // NOLINT(cppcoreguidelines-avoid-no
/// However other factors like gate driver propagation time
/// are also considered and a really low value is not important
/// See also: https://github.com/esphome/issues/issues/1632
static constexpr uint32_t GATE_ENABLE_TIME = 50;
#ifdef USE_ESP32
/// Timer frequency in Hz (1 MHz = 1µs resolution)
static constexpr uint32_t TIMER_FREQUENCY_HZ = 1000000;
/// Timer interrupt interval in microseconds
static constexpr uint64_t TIMER_INTERVAL_US = 50;
#endif
static const uint32_t GATE_ENABLE_TIME = 50;
/// Function called from timer interrupt
/// Input is current time in microseconds (micros())
@@ -159,7 +154,7 @@ void IRAM_ATTR HOT AcDimmerDataStore::s_gpio_intr(AcDimmerDataStore *store) {
#ifdef USE_ESP32
// ESP32 implementation, uses basically the same code but needs to wrap
// timer_interrupt() function to auto-reschedule
static HWTimer *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
static hw_timer_t *dimmer_timer = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void IRAM_ATTR HOT AcDimmerDataStore::s_timer_intr() { timer_interrupt(); }
#endif
@@ -199,15 +194,15 @@ void AcDimmer::setup() {
setTimer1Callback(&timer_interrupt);
#endif
#ifdef USE_ESP32
dimmer_timer = timer_begin(TIMER_FREQUENCY_HZ);
timer_attach_interrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// timer frequency of 1mhz
dimmer_timer = timerBegin(1000000);
timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr);
// For ESP32, we can't use dynamic interval calculation because the timerX functions
// are not callable from ISR (placed in flash storage).
// Here we just use an interrupt firing every 50 µs.
timer_alarm(dimmer_timer, TIMER_INTERVAL_US, true, 0);
timerAlarm(dimmer_timer, 50, true, 0);
#endif
}
void AcDimmer::write_state(float state) {
state = std::acos(1 - (2 * state)) / std::numbers::pi; // RMS power compensation
auto new_value = static_cast<uint16_t>(roundf(state * 65535));
@@ -215,7 +210,6 @@ void AcDimmer::write_state(float state) {
this->store_.init_cycle = this->init_with_half_cycle_;
this->store_.value = new_value;
}
void AcDimmer::dump_config() {
ESP_LOGCONFIG(TAG,
"AcDimmer:\n"
@@ -236,4 +230,7 @@ void AcDimmer::dump_config() {
ESP_LOGV(TAG, " Estimated Frequency: %.3fHz", 1e6f / this->store_.cycle_time_us / 2);
}
} // namespace esphome::ac_dimmer
} // namespace ac_dimmer
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,10 +1,13 @@
#pragma once
#ifdef USE_ARDUINO
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/output/float_output.h"
namespace esphome::ac_dimmer {
namespace esphome {
namespace ac_dimmer {
enum DimMethod { DIM_METHOD_LEADING_PULSE = 0, DIM_METHOD_LEADING, DIM_METHOD_TRAILING };
@@ -61,4 +64,7 @@ class AcDimmer : public output::FloatOutput, public Component {
DimMethod method_;
};
} // namespace esphome::ac_dimmer
} // namespace ac_dimmer
} // namespace esphome
#endif // USE_ARDUINO

View File

@@ -1,152 +0,0 @@
#ifdef USE_ESP32
#include "hw_timer_esp_idf.h"
#include "freertos/FreeRTOS.h"
#include "esphome/core/log.h"
#include "driver/gptimer.h"
#include "esp_clk_tree.h"
#include "soc/clk_tree_defs.h"
static const char *const TAG = "hw_timer_esp_idf";
namespace esphome::ac_dimmer {
// GPTimer divider constraints from ESP-IDF documentation
static constexpr uint32_t GPTIMER_DIVIDER_MIN = 2;
static constexpr uint32_t GPTIMER_DIVIDER_MAX = 65536;
using voidFuncPtr = void (*)();
using voidFuncPtrArg = void (*)(void *);
struct InterruptConfigT {
voidFuncPtr fn{nullptr};
void *arg{nullptr};
};
struct HWTimer {
gptimer_handle_t timer_handle{nullptr};
InterruptConfigT interrupt_handle{};
bool timer_started{false};
};
HWTimer *timer_begin(uint32_t frequency) {
esp_err_t err = ESP_OK;
uint32_t counter_src_hz = 0;
uint32_t divider = 0;
soc_module_clk_t clk;
for (auto clk_candidate : SOC_GPTIMER_CLKS) {
clk = clk_candidate;
esp_clk_tree_src_get_freq_hz(clk, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &counter_src_hz);
divider = counter_src_hz / frequency;
if ((divider >= GPTIMER_DIVIDER_MIN) && (divider <= GPTIMER_DIVIDER_MAX)) {
break;
} else {
divider = 0;
}
}
if (divider == 0) {
ESP_LOGE(TAG, "Resolution not possible; aborting");
return nullptr;
}
gptimer_config_t config = {
.clk_src = static_cast<gptimer_clock_source_t>(clk),
.direction = GPTIMER_COUNT_UP,
.resolution_hz = frequency,
.flags = {.intr_shared = true},
};
HWTimer *timer = new HWTimer();
err = gptimer_new_timer(&config, &timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer creation failed; error %d", err);
delete timer;
return nullptr;
}
err = gptimer_enable(timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer enable failed; error %d", err);
gptimer_del_timer(timer->timer_handle);
delete timer;
return nullptr;
}
err = gptimer_start(timer->timer_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "GPTimer start failed; error %d", err);
gptimer_disable(timer->timer_handle);
gptimer_del_timer(timer->timer_handle);
delete timer;
return nullptr;
}
timer->timer_started = true;
return timer;
}
bool IRAM_ATTR timer_fn_wrapper(gptimer_handle_t timer, const gptimer_alarm_event_data_t *edata, void *args) {
auto *isr = static_cast<InterruptConfigT *>(args);
if (isr->fn) {
if (isr->arg) {
reinterpret_cast<voidFuncPtrArg>(isr->fn)(isr->arg);
} else {
isr->fn();
}
}
// Return false to indicate that no higher-priority task was woken and no context switch is requested.
return false;
}
static void timer_attach_interrupt_functional_arg(HWTimer *timer, void (*user_func)(void *), void *arg) {
if (timer == nullptr) {
ESP_LOGE(TAG, "Timer handle is nullptr");
return;
}
gptimer_event_callbacks_t cbs = {
.on_alarm = timer_fn_wrapper,
};
timer->interrupt_handle.fn = reinterpret_cast<voidFuncPtr>(user_func);
timer->interrupt_handle.arg = arg;
if (timer->timer_started) {
gptimer_stop(timer->timer_handle);
}
gptimer_disable(timer->timer_handle);
esp_err_t err = gptimer_register_event_callbacks(timer->timer_handle, &cbs, &timer->interrupt_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Timer Attach Interrupt failed; error %d", err);
}
gptimer_enable(timer->timer_handle);
if (timer->timer_started) {
gptimer_start(timer->timer_handle);
}
}
void timer_attach_interrupt(HWTimer *timer, voidFuncPtr user_func) {
timer_attach_interrupt_functional_arg(timer, reinterpret_cast<voidFuncPtrArg>(user_func), nullptr);
}
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count) {
if (timer == nullptr) {
ESP_LOGE(TAG, "Timer handle is nullptr");
return;
}
gptimer_alarm_config_t alarm_cfg = {
.alarm_count = alarm_value,
.reload_count = reload_count,
.flags = {.auto_reload_on_alarm = autoreload},
};
esp_err_t err = gptimer_set_alarm_action(timer->timer_handle, &alarm_cfg);
if (err != ESP_OK) {
ESP_LOGE(TAG, "Timer Alarm Write failed; error %d", err);
}
}
} // namespace esphome::ac_dimmer
#endif

View File

@@ -1,17 +0,0 @@
#pragma once
#ifdef USE_ESP32
#include "driver/gptimer_types.h"
namespace esphome::ac_dimmer {
struct HWTimer;
HWTimer *timer_begin(uint32_t frequency);
void timer_attach_interrupt(HWTimer *timer, void (*user_func)());
void timer_alarm(HWTimer *timer, uint64_t alarm_value, bool autoreload, uint64_t reload_count);
} // namespace esphome::ac_dimmer
#endif

View File

@@ -32,6 +32,7 @@ CONFIG_SCHEMA = cv.All(
),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_with_arduino,
)

View File

@@ -66,8 +66,6 @@ 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) {}
}
@@ -765,7 +763,7 @@ message SubscribeHomeassistantServicesRequest {
message HomeassistantServiceMap {
string key = 1;
string value = 2;
string value = 2 [(no_zero_copy) = true];
}
message HomeassistantActionRequest {
@@ -781,7 +779,7 @@ message HomeassistantActionRequest {
bool is_event = 5;
uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
string response_template = 8 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
}
// Message sent by Home Assistant to ESPHome with service call response data
@@ -2439,49 +2437,3 @@ 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
}

View File

@@ -46,9 +46,6 @@
#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 {
@@ -446,7 +443,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
if (traits.supports_direction())
msg.direction = static_cast<enums::FanDirection>(fan->direction);
if (traits.supports_preset_modes() && fan->has_preset_mode())
msg.preset_mode = fan->get_preset_mode();
msg.preset_mode = StringRef(fan->get_preset_mode());
return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
@@ -502,7 +499,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
resp.cold_white = values.get_cold_white();
resp.warm_white = values.get_warm_white();
if (light->supports_effects()) {
resp.effect = light->get_effect_name();
resp.effect = light->get_effect_name_ref();
}
return fill_and_encode_entity_state(light, resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -525,8 +522,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
effects_list.init(light_effects.size() + 1);
effects_list.push_back("None");
for (auto *effect : light_effects) {
// c_str() is safe as effect names are null-terminated strings from codegen
effects_list.push_back(effect->get_name().c_str());
effects_list.push_back(effect->get_name());
}
}
msg.effects = &effects_list;
@@ -679,13 +675,13 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection
if (traits.get_supports_fan_modes() && climate->fan_mode.has_value())
resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value());
if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) {
resp.custom_fan_mode = climate->get_custom_fan_mode();
resp.custom_fan_mode = StringRef(climate->get_custom_fan_mode());
}
if (traits.get_supports_presets() && climate->preset.has_value()) {
resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value());
}
if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) {
resp.custom_preset = climate->get_custom_preset();
resp.custom_preset = StringRef(climate->get_custom_preset());
}
if (traits.get_supports_swing_modes())
resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode);
@@ -918,7 +914,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection
bool is_single) {
auto *select = static_cast<select::Select *>(entity);
SelectStateResponse resp;
resp.state = select->current_option();
resp.state = StringRef(select->current_option());
resp.missing_state = !select->has_state();
return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -1418,15 +1414,14 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ
#endif
#ifdef USE_EVENT
void APIConnection::send_event(event::Event *event, StringRef event_type) {
// get_last_event_type() returns StringRef pointing to null-terminated string literals from codegen
this->send_message_smart_(event, MessageCreator(event_type.c_str()), EventResponse::MESSAGE_TYPE,
void APIConnection::send_event(event::Event *event, const char *event_type) {
this->send_message_smart_(event, MessageCreator(event_type), EventResponse::MESSAGE_TYPE,
EventResponse::ESTIMATED_SIZE);
}
uint16_t APIConnection::try_send_event_response(event::Event *event, StringRef event_type, APIConnection *conn,
uint16_t APIConnection::try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single) {
EventResponse resp;
resp.event_type = event_type;
resp.event_type = StringRef(event_type);
return fill_and_encode_entity_state(event, resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
}
@@ -1441,35 +1436,6 @@ 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,
@@ -2089,7 +2055,7 @@ uint16_t APIConnection::MessageCreator::operator()(EntityBase *entity, APIConnec
// Special case: EventResponse uses const char * pointer
if (message_type == EventResponse::MESSAGE_TYPE) {
auto *e = static_cast<event::Event *>(entity);
return APIConnection::try_send_event_response(e, StringRef(data_.const_char_ptr), conn, remaining_size, is_single);
return APIConnection::try_send_event_response(e, data_.const_char_ptr, conn, remaining_size, is_single);
}
#endif

View File

@@ -172,13 +172,8 @@ 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);
void send_event(event::Event *event, const char *event_type);
#endif
#ifdef USE_UPDATE
@@ -473,12 +468,8 @@ 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,
static uint16_t try_send_event_response(event::Event *event, const char *event_type, APIConnection *conn,
uint32_t remaining_size, bool is_single);
static uint16_t try_send_event_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, bool is_single);
#endif

View File

@@ -27,6 +27,7 @@ extend google.protobuf.MessageOptions {
extend google.protobuf.FieldOptions {
optional string field_ifdef = 1042;
optional uint32 fixed_array_size = 50007;
optional bool no_zero_copy = 50008 [default=false];
optional bool fixed_array_skip_zero = 50009 [default=false];
optional string fixed_array_size_define = 50010;
optional string fixed_array_with_length_define = 50011;
@@ -79,15 +80,4 @@ extend google.protobuf.FieldOptions {
// Example: [(container_pointer_no_template) = "light::ColorModeMask"]
// generates: const light::ColorModeMask *supported_color_modes{};
optional string container_pointer_no_template = 50014;
// packed_buffer: Expose raw packed buffer instead of decoding into container
// When set on a packed repeated field, the generated code stores a pointer
// to the raw protobuf buffer instead of decoding values. This enables
// zero-copy passthrough when the consumer can decode on-demand.
// The field must be a packed repeated field (packed=true).
// Generates three fields:
// - const uint8_t *<field>_data_{nullptr};
// - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false];
}

View File

@@ -3347,98 +3347,5 @@ 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

View File

@@ -1053,7 +1053,7 @@ class SubscribeHomeassistantServicesRequest final : public ProtoMessage {
class HomeassistantServiceMap final : public ProtoMessage {
public:
StringRef key{};
StringRef value{};
std::string value{};
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
@@ -1081,7 +1081,7 @@ class HomeassistantActionRequest final : public ProtoMessage {
bool wants_response{false};
#endif
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
StringRef response_template{};
std::string response_template{};
#endif
void encode(ProtoWriteBuffer buffer) const override;
void calculate_size(ProtoSize &size) const override;
@@ -3049,70 +3049,5 @@ 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

View File

@@ -100,16 +100,6 @@ template<typename T> static void dump_field(std::string &out, const char *field_
out.append("\n");
}
// Helper for bytes fields - uses stack buffer to avoid heap allocation
// Buffer sized for 160 bytes of data (480 chars with separators) to fit typical log buffer
static void dump_bytes_field(std::string &out, const char *field_name, const uint8_t *data, size_t len,
int indent = 2) {
char hex_buf[format_hex_pretty_size(160)];
append_field_prefix(out, field_name, indent);
format_hex_pretty_to(hex_buf, data, len);
append_with_newline(out, hex_buf);
}
template<> const char *proto_enum_to_string<enums::EntityCategory>(enums::EntityCategory value) {
switch (value) {
case enums::ENTITY_CATEGORY_NONE:
@@ -1137,12 +1127,16 @@ void SubscribeLogsRequest::dump_to(std::string &out) const {
void SubscribeLogsResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "SubscribeLogsResponse");
dump_field(out, "level", static_cast<enums::LogLevel>(this->level));
dump_bytes_field(out, "message", this->message_ptr_, this->message_len_);
out.append(" message: ");
out.append(format_hex_pretty(this->message_ptr_, this->message_len_));
out.append("\n");
}
#ifdef USE_API_NOISE
void NoiseEncryptionSetKeyRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "NoiseEncryptionSetKeyRequest");
dump_bytes_field(out, "key", this->key, this->key_len);
out.append(" key: ");
out.append(format_hex_pretty(this->key, this->key_len));
out.append("\n");
}
void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "NoiseEncryptionSetKeyResponse");
@@ -1195,7 +1189,9 @@ void HomeassistantActionResponse::dump_to(std::string &out) const {
dump_field(out, "success", this->success);
dump_field(out, "error_message", this->error_message);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
dump_bytes_field(out, "response_data", this->response_data, this->response_data_len);
out.append(" response_data: ");
out.append(format_hex_pretty(this->response_data, this->response_data_len));
out.append("\n");
#endif
}
#endif
@@ -1282,7 +1278,9 @@ void ExecuteServiceResponse::dump_to(std::string &out) const {
dump_field(out, "success", this->success);
dump_field(out, "error_message", this->error_message);
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES_JSON
dump_bytes_field(out, "response_data", this->response_data, this->response_data_len);
out.append(" response_data: ");
out.append(format_hex_pretty(this->response_data, this->response_data_len));
out.append("\n");
#endif
}
#endif
@@ -1304,7 +1302,9 @@ void ListEntitiesCameraResponse::dump_to(std::string &out) const {
void CameraImageResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "CameraImageResponse");
dump_field(out, "key", this->key);
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
out.append(" data: ");
out.append(format_hex_pretty(this->data_ptr_, this->data_len_));
out.append("\n");
dump_field(out, "done", this->done);
#ifdef USE_DEVICES
dump_field(out, "device_id", this->device_id);
@@ -1705,7 +1705,9 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
dump_field(out, "address", this->address);
dump_field(out, "rssi", this->rssi);
dump_field(out, "address_type", this->address_type);
dump_bytes_field(out, "data", this->data, this->data_len);
out.append(" data: ");
out.append(format_hex_pretty(this->data, this->data_len));
out.append("\n");
}
void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse");
@@ -1790,14 +1792,18 @@ void BluetoothGATTReadResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTReadResponse");
dump_field(out, "address", this->address);
dump_field(out, "handle", this->handle);
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
out.append(" data: ");
out.append(format_hex_pretty(this->data_ptr_, this->data_len_));
out.append("\n");
}
void BluetoothGATTWriteRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTWriteRequest");
dump_field(out, "address", this->address);
dump_field(out, "handle", this->handle);
dump_field(out, "response", this->response);
dump_bytes_field(out, "data", this->data, this->data_len);
out.append(" data: ");
out.append(format_hex_pretty(this->data, this->data_len));
out.append("\n");
}
void BluetoothGATTReadDescriptorRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTReadDescriptorRequest");
@@ -1808,7 +1814,9 @@ void BluetoothGATTWriteDescriptorRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTWriteDescriptorRequest");
dump_field(out, "address", this->address);
dump_field(out, "handle", this->handle);
dump_bytes_field(out, "data", this->data, this->data_len);
out.append(" data: ");
out.append(format_hex_pretty(this->data, this->data_len));
out.append("\n");
}
void BluetoothGATTNotifyRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTNotifyRequest");
@@ -1820,7 +1828,9 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "BluetoothGATTNotifyDataResponse");
dump_field(out, "address", this->address);
dump_field(out, "handle", this->handle);
dump_bytes_field(out, "data", this->data_ptr_, this->data_len_);
out.append(" data: ");
out.append(format_hex_pretty(this->data_ptr_, this->data_len_));
out.append("\n");
}
void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const {
out.append("SubscribeBluetoothConnectionsFreeRequest {}");
@@ -1924,7 +1934,9 @@ void VoiceAssistantEventResponse::dump_to(std::string &out) const {
}
void VoiceAssistantAudio::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "VoiceAssistantAudio");
dump_bytes_field(out, "data", this->data, this->data_len);
out.append(" data: ");
out.append(format_hex_pretty(this->data, this->data_len));
out.append("\n");
dump_field(out, "end", this->end);
}
void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const {
@@ -2285,56 +2297,16 @@ void UpdateCommandRequest::dump_to(std::string &out) const {
#ifdef USE_ZWAVE_PROXY
void ZWaveProxyFrame::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ZWaveProxyFrame");
dump_bytes_field(out, "data", this->data, this->data_len);
out.append(" data: ");
out.append(format_hex_pretty(this->data, this->data_len));
out.append("\n");
}
void ZWaveProxyRequest::dump_to(std::string &out) const {
MessageDumpHelper helper(out, "ZWaveProxyRequest");
dump_field(out, "type", static_cast<enums::ZWaveProxyRequestType>(this->type));
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);
}
out.append(" data: ");
out.append(format_hex_pretty(this->data, this->data_len));
out.append("\n");
}
#endif

View File

@@ -621,17 +621,6 @@ 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;
@@ -830,11 +819,6 @@ 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

View File

@@ -217,11 +217,6 @@ 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;
};
@@ -352,9 +347,6 @@ 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;
@@ -481,9 +473,6 @@ 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;
};

View File

@@ -186,17 +186,14 @@ void APIServer::loop() {
}
// Rare case: handle disconnection
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
this->client_disconnected_trigger_->trigger(std::string(client->get_name()), std::string(client->get_peername()));
#endif
#ifdef USE_API_USER_DEFINED_ACTION_RESPONSES
this->unregister_active_action_calls_for_connection(client.get());
#endif
ESP_LOGV(TAG, "Remove connection %s", client->get_name());
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Save client info before removal for the trigger
std::string client_name(client->get_name());
std::string client_peername(client->get_peername());
#endif
// Swap with the last element and pop (avoids expensive vector shifts)
if (client_index < this->clients_.size() - 1) {
std::swap(this->clients_[client_index], this->clients_.back());
@@ -208,11 +205,6 @@ void APIServer::loop() {
this->status_set_warning();
this->last_connected_ = App.get_loop_component_start_time();
}
#ifdef USE_API_CLIENT_DISCONNECTED_TRIGGER
// Fire trigger after client is removed so api.connected reflects the true state
this->client_disconnected_trigger_->trigger(client_name, client_peername);
#endif
// Don't increment client_index since we need to process the swapped element
}
}
@@ -347,21 +339,6 @@ 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
@@ -639,14 +616,6 @@ bool APIServer::teardown() {
#define USE_API_ACTION_CALL_TIMEOUT_MS 30000 // NOLINT
#endif
// SSO-friendly action call key - hex format guarantees max 11 chars ("ac_ffffffff")
// which fits in any std::string SSO buffer (typically 12-15 bytes)
static inline std::string make_action_call_key(uint32_t id) {
char buf[12];
size_t len = snprintf(buf, sizeof(buf), "ac_%x", id);
return std::string(buf, len);
}
uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConnection *conn) {
uint32_t action_call_id = this->next_action_call_id_++;
// Handle wraparound (skip 0 as it means "no call")
@@ -656,17 +625,18 @@ uint32_t APIServer::register_active_action_call(uint32_t client_call_id, APIConn
this->active_action_calls_.push_back({action_call_id, client_call_id, conn});
// Schedule automatic cleanup after timeout (client will have given up by then)
this->set_timeout(make_action_call_key(action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS, [this, action_call_id]() {
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
this->unregister_active_action_call(action_call_id);
});
this->set_timeout(str_sprintf("action_call_%u", action_call_id), USE_API_ACTION_CALL_TIMEOUT_MS,
[this, action_call_id]() {
ESP_LOGD(TAG, "Action call %u timed out", action_call_id);
this->unregister_active_action_call(action_call_id);
});
return action_call_id;
}
void APIServer::unregister_active_action_call(uint32_t action_call_id) {
// Cancel the timeout for this action call
this->cancel_timeout(make_action_call_key(action_call_id));
this->cancel_timeout(str_sprintf("action_call_%u", action_call_id));
// Swap-and-pop is more efficient than remove_if for unordered vectors
for (size_t i = 0; i < this->active_action_calls_.size(); i++) {
@@ -683,7 +653,7 @@ void APIServer::unregister_active_action_calls_for_connection(APIConnection *con
for (size_t i = 0; i < this->active_action_calls_.size();) {
if (this->active_action_calls_[i].connection == conn) {
// Cancel the timeout for this action call
this->cancel_timeout(make_action_call_key(this->active_action_calls_[i].action_call_id));
this->cancel_timeout(str_sprintf("action_call_%u", this->active_action_calls_[i].action_call_id));
std::swap(this->active_action_calls_[i], this->active_action_calls_.back());
this->active_action_calls_.pop_back();

View File

@@ -185,9 +185,6 @@ 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;

View File

@@ -265,7 +265,7 @@ class CustomAPIDevice {
for (auto &it : data) {
auto &kv = resp.data.emplace_back();
kv.key = StringRef(it.first);
kv.value = StringRef(it.second); // data map lives until send completes
kv.value = it.second; // value is std::string (no_zero_copy), assign directly
}
global_api_server->send_homeassistant_action(resp);
}
@@ -308,7 +308,7 @@ class CustomAPIDevice {
for (auto &it : data) {
auto &kv = resp.data.emplace_back();
kv.key = StringRef(it.first);
kv.value = StringRef(it.second); // data map lives until send completes
kv.value = it.second; // value is std::string (no_zero_copy), assign directly
}
global_api_server->send_homeassistant_action(resp);
}

View File

@@ -149,21 +149,11 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
std::string service_value = this->service_.value(x...);
resp.service = StringRef(service_value);
resp.is_event = this->flags_.is_event;
// Local storage for lambda-evaluated strings - lives until after send
FixedVector<std::string> data_storage;
FixedVector<std::string> data_template_storage;
FixedVector<std::string> variables_storage;
this->populate_service_map(resp.data, this->data_, data_storage, x...);
this->populate_service_map(resp.data_template, this->data_template_, data_template_storage, x...);
this->populate_service_map(resp.variables, this->variables_, variables_storage, x...);
this->populate_service_map(resp.data, this->data_, x...);
this->populate_service_map(resp.data_template, this->data_template_, x...);
this->populate_service_map(resp.variables, this->variables_, x...);
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
// IMPORTANT: Declare at outer scope so it lives until send_homeassistant_action returns.
std::string response_template_value;
#endif
if (this->flags_.wants_status) {
// Generate a unique call ID for this service call
static uint32_t call_id_counter = 1;
@@ -174,8 +164,8 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
resp.wants_response = true;
// Set response template if provided
if (this->flags_.has_response_template) {
response_template_value = this->response_template_.value(x...);
resp.response_template = StringRef(response_template_value);
std::string response_template_value = this->response_template_.value(x...);
resp.response_template = response_template_value;
}
}
#endif
@@ -215,31 +205,12 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
}
template<typename VectorType, typename SourceType>
static void populate_service_map(VectorType &dest, SourceType &source, FixedVector<std::string> &value_storage,
Ts... x) {
static void populate_service_map(VectorType &dest, SourceType &source, Ts... x) {
dest.init(source.size());
// Count non-static strings to allocate exact storage needed
size_t lambda_count = 0;
for (const auto &it : source) {
if (!it.value.is_static_string()) {
lambda_count++;
}
}
value_storage.init(lambda_count);
for (auto &it : source) {
auto &kv = dest.emplace_back();
kv.key = StringRef(it.key);
if (it.value.is_static_string()) {
// Static string from YAML - zero allocation
kv.value = StringRef(it.value.get_static_string());
} else {
// Lambda evaluation - store result, reference it
value_storage.push_back(it.value.value(x...));
kv.value = StringRef(value_storage.back());
}
kv.value = it.value.value(x...);
}
}

View File

@@ -76,9 +76,6 @@ 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

View File

@@ -85,9 +85,6 @@ 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

View File

@@ -39,24 +39,6 @@ inline constexpr int64_t decode_zigzag64(uint64_t value) {
return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1);
}
/// Count number of varints in a packed buffer
inline uint16_t count_packed_varints(const uint8_t *data, size_t len) {
uint16_t count = 0;
while (len > 0) {
// Skip varint bytes until we find one without continuation bit
while (len > 0 && (*data & 0x80)) {
data++;
len--;
}
if (len > 0) {
data++;
len--;
count++;
}
}
return count;
}
/*
* StringRef Ownership Model for API Protocol Messages
* ===================================================
@@ -198,10 +180,9 @@ class ProtoVarInt {
uint64_t value_;
};
// Forward declarations for decode_to_message, encode_message and encode_packed_sint32
class ProtoDecodableMessage;
// Forward declaration for decode_to_message and encode_to_writer
class ProtoMessage;
class ProtoSize;
class ProtoDecodableMessage;
class ProtoLengthDelimited {
public:
@@ -353,8 +334,6 @@ class ProtoWriteBuffer {
void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
this->encode_uint64(field_id, encode_zigzag64(value), force);
}
/// Encode a packed repeated sint32 field (zero-copy from vector)
void encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values);
void encode_message(uint32_t field_id, const ProtoMessage &value);
std::vector<uint8_t> *get_buffer() const { return buffer_; }
@@ -362,6 +341,9 @@ class ProtoWriteBuffer {
std::vector<uint8_t> *buffer_;
};
// Forward declaration
class ProtoSize;
class ProtoMessage {
public:
virtual ~ProtoMessage() = default;
@@ -810,43 +792,8 @@ class ProtoSize {
}
}
}
/**
* @brief Calculate size of a packed repeated sint32 field
*/
inline void add_packed_sint32(uint32_t field_id_size, const std::vector<int32_t> &values) {
if (values.empty())
return;
size_t packed_size = 0;
for (int value : values) {
packed_size += varint(encode_zigzag32(value));
}
// field_id + length varint + packed data
total_size_ += field_id_size + varint(static_cast<uint32_t>(packed_size)) + static_cast<uint32_t>(packed_size);
}
};
// Implementation of encode_packed_sint32 - must be after ProtoSize is defined
inline void ProtoWriteBuffer::encode_packed_sint32(uint32_t field_id, const std::vector<int32_t> &values) {
if (values.empty())
return;
// Calculate packed size
size_t packed_size = 0;
for (int value : values) {
packed_size += ProtoSize::varint(encode_zigzag32(value));
}
// Write tag (LENGTH_DELIMITED) + length + all zigzag-encoded values
this->encode_field_raw(field_id, WIRE_TYPE_LENGTH_DELIMITED);
this->encode_varint_raw(packed_size);
for (int value : values) {
this->encode_varint_raw(encode_zigzag32(value));
}
}
// Implementation of encode_message - must be after ProtoMessage is defined
inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const ProtoMessage &value) {
this->encode_field_raw(field_id, 2); // type 2: Length-delimited message

View File

@@ -79,9 +79,6 @@ 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

View File

@@ -6,7 +6,7 @@ namespace esphome::aqi {
class AbstractAQICalculator {
public:
virtual uint16_t get_aqi(float pm2_5_value, float pm10_0_value) = 0;
virtual uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) = 0;
};
} // namespace esphome::aqi

View File

@@ -1,7 +1,6 @@
#pragma once
#include <cmath>
#include <limits>
#include <climits>
#include "abstract_aqi_calculator.h"
// https://document.airnow.gov/technical-assistance-document-for-the-reporting-of-daily-air-quailty.pdf
@@ -10,11 +9,11 @@ namespace esphome::aqi {
class AQICalculator : public AbstractAQICalculator {
public:
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
int pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
int pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index;
}
protected:
@@ -22,28 +21,25 @@ class AQICalculator : public AbstractAQICalculator {
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}};
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f},
{35.5f, 55.4f}, {55.5f, 125.4f},
{125.5f, 225.4f}, {225.5f, std::numeric_limits<float>::max()}};
static constexpr int PM2_5_GRID[NUM_LEVELS][2] = {{0, 9}, {10, 35}, {36, 55}, {56, 125}, {126, 225}, {226, INT_MAX}};
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f},
{155.0f, 254.0f}, {255.0f, 354.0f},
{355.0f, 424.0f}, {425.0f, std::numeric_limits<float>::max()}};
static constexpr int PM10_0_GRID[NUM_LEVELS][2] = {{0, 54}, {55, 154}, {155, 254},
{255, 354}, {355, 424}, {425, INT_MAX}};
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
static int calculate_index(uint16_t value, const int array[NUM_LEVELS][2]) {
int grid_index = get_grid_index(value, array);
if (grid_index == -1) {
return -1.0f;
return -1;
}
float aqi_lo = INDEX_GRID[grid_index][0];
float aqi_hi = INDEX_GRID[grid_index][1];
float conc_lo = array[grid_index][0];
float conc_hi = array[grid_index][1];
int aqi_lo = INDEX_GRID[grid_index][0];
int aqi_hi = INDEX_GRID[grid_index][1];
int conc_lo = array[grid_index][0];
int conc_hi = array[grid_index][1];
return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
}
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
static int get_grid_index(uint16_t value, const int array[NUM_LEVELS][2]) {
for (int i = 0; i < NUM_LEVELS; i++) {
if (value >= array[i][0] && value <= array[i][1]) {
return i;

View File

@@ -44,7 +44,8 @@ void AQISensor::calculate_aqi_() {
return;
}
uint16_t aqi = calculator->get_aqi(this->pm_2_5_value_, this->pm_10_0_value_);
uint16_t aqi =
calculator->get_aqi(static_cast<uint16_t>(this->pm_2_5_value_), static_cast<uint16_t>(this->pm_10_0_value_));
this->publish_state(aqi);
}

View File

@@ -1,18 +1,16 @@
#pragma once
#include <cmath>
#include <limits>
#include "abstract_aqi_calculator.h"
namespace esphome::aqi {
class CAQICalculator : public AbstractAQICalculator {
public:
uint16_t get_aqi(float pm2_5_value, float pm10_0_value) override {
float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
uint16_t get_aqi(uint16_t pm2_5_value, uint16_t pm10_0_value) override {
int pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID);
int pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID);
return static_cast<uint16_t>(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index));
return (pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index;
}
protected:
@@ -20,27 +18,25 @@ class CAQICalculator : public AbstractAQICalculator {
static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}};
static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {
{0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits<float>::max()}};
static constexpr int PM2_5_GRID[NUM_LEVELS][2] = {{0, 15}, {16, 30}, {31, 55}, {56, 110}, {111, 400}};
static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {
{0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits<float>::max()}};
static constexpr int PM10_0_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 90}, {91, 180}, {181, 400}};
static float calculate_index(float value, const float array[NUM_LEVELS][2]) {
static int calculate_index(uint16_t value, const int array[NUM_LEVELS][2]) {
int grid_index = get_grid_index(value, array);
if (grid_index == -1) {
return -1.0f;
return -1;
}
float aqi_lo = INDEX_GRID[grid_index][0];
float aqi_hi = INDEX_GRID[grid_index][1];
float conc_lo = array[grid_index][0];
float conc_hi = array[grid_index][1];
int aqi_lo = INDEX_GRID[grid_index][0];
int aqi_hi = INDEX_GRID[grid_index][1];
int conc_lo = array[grid_index][0];
int conc_hi = array[grid_index][1];
return (value - conc_lo) * (aqi_hi - aqi_lo) / (conc_hi - conc_lo) + aqi_lo;
}
static int get_grid_index(float value, const float array[NUM_LEVELS][2]) {
static int get_grid_index(uint16_t value, const int array[NUM_LEVELS][2]) {
for (int i = 0; i < NUM_LEVELS; i++) {
if (value >= array[i][0] && value <= array[i][1]) {
return i;

View File

@@ -164,21 +164,21 @@ void BedJetClimate::control(const ClimateCall &call) {
return;
}
} else if (call.has_custom_preset()) {
auto preset = call.get_custom_preset();
const char *preset = call.get_custom_preset();
bool result;
if (preset == "M1") {
if (strcmp(preset, "M1") == 0) {
result = this->parent_->button_memory1();
} else if (preset == "M2") {
} else if (strcmp(preset, "M2") == 0) {
result = this->parent_->button_memory2();
} else if (preset == "M3") {
} else if (strcmp(preset, "M3") == 0) {
result = this->parent_->button_memory3();
} else if (preset == "LTD HT") {
} else if (strcmp(preset, "LTD HT") == 0) {
result = this->parent_->button_heat();
} else if (preset == "EXT HT") {
} else if (strcmp(preset, "EXT HT") == 0) {
result = this->parent_->button_ext_heat();
} else {
ESP_LOGW(TAG, "Unsupported preset: %.*s", (int) preset.size(), preset.c_str());
ESP_LOGW(TAG, "Unsupported preset: %s", preset);
return;
}
@@ -208,11 +208,10 @@ void BedJetClimate::control(const ClimateCall &call) {
this->set_fan_mode_(fan_mode);
}
} else if (call.has_custom_fan_mode()) {
auto fan_mode = call.get_custom_fan_mode();
auto fan_index = bedjet_fan_speed_to_step(fan_mode.c_str());
const char *fan_mode = call.get_custom_fan_mode();
auto fan_index = bedjet_fan_speed_to_step(fan_mode);
if (fan_index <= 19) {
ESP_LOGV(TAG, "[%s] Converted fan mode %.*s to bedjet fan step %d", this->get_name().c_str(),
(int) fan_mode.size(), fan_mode.c_str(), fan_index);
ESP_LOGV(TAG, "[%s] Converted fan mode %s to bedjet fan step %d", this->get_name().c_str(), fan_mode, fan_index);
bool result = this->parent_->set_fan_index(fan_index);
if (result) {
this->set_custom_fan_mode_(fan_mode);

View File

@@ -7,83 +7,83 @@ namespace esphome::captive_portal {
#ifdef USE_CAPTIVE_PORTAL_GZIP
const uint8_t INDEX_GZ[] PROGMEM = {
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x95, 0x16, 0x6b, 0x8f, 0xdb, 0x36, 0xf2, 0x7b, 0x7e,
0x05, 0x8f, 0x49, 0xbb, 0x52, 0xb3, 0x7a, 0x7a, 0xed, 0x6c, 0x24, 0x51, 0x45, 0x9a, 0xbb, 0xa2, 0x05, 0x9a, 0x36,
0xc0, 0x6e, 0x73, 0x1f, 0x82, 0x00, 0x4b, 0x53, 0x23, 0x8b, 0x31, 0x45, 0xea, 0x48, 0xca, 0x8f, 0x18, 0xbe, 0xdf,
0x7e, 0xa0, 0x24, 0x7b, 0x9d, 0x45, 0x73, 0xb8, 0xb3, 0x60, 0x61, 0x38, 0xef, 0x19, 0xcd, 0x83, 0xc5, 0xdf, 0x2a,
0xc5, 0xec, 0xbe, 0x03, 0xd4, 0xd8, 0x56, 0x94, 0x85, 0x7b, 0x23, 0x41, 0xe5, 0x8a, 0x80, 0x2c, 0x8b, 0x06, 0x68,
0x55, 0x16, 0x2d, 0x58, 0x8a, 0x58, 0x43, 0xb5, 0x01, 0x4b, 0xfe, 0xbc, 0xff, 0x39, 0xb8, 0x2d, 0x0b, 0xc1, 0xe5,
0x1a, 0x69, 0x10, 0x84, 0x33, 0x25, 0x51, 0xa3, 0xa1, 0x26, 0x15, 0xb5, 0x34, 0xe3, 0x2d, 0x5d, 0xc1, 0x24, 0x22,
0x69, 0x0b, 0x64, 0xc3, 0x61, 0xdb, 0x29, 0x6d, 0x11, 0x53, 0xd2, 0x82, 0xb4, 0x04, 0x6f, 0x79, 0x65, 0x1b, 0x52,
0xc1, 0x86, 0x33, 0x08, 0x86, 0xc3, 0x35, 0x97, 0xdc, 0x72, 0x2a, 0x02, 0xc3, 0xa8, 0x00, 0x92, 0x5c, 0xf7, 0x06,
0xf4, 0x70, 0xa0, 0x4b, 0x01, 0x44, 0x2a, 0x5c, 0x16, 0x86, 0x69, 0xde, 0x59, 0xe4, 0x5c, 0x25, 0xad, 0xaa, 0x7a,
0x01, 0x65, 0x14, 0x51, 0x63, 0xc0, 0x9a, 0x88, 0xcb, 0x0a, 0x76, 0xe1, 0x32, 0x66, 0x2c, 0x86, 0xdb, 0xdb, 0xf0,
0xb3, 0x79, 0x56, 0x29, 0xd6, 0xb7, 0x20, 0x6d, 0x28, 0x14, 0xa3, 0x96, 0x2b, 0x19, 0x1a, 0xa0, 0x9a, 0x35, 0x84,
0x10, 0xfc, 0xa3, 0xa1, 0x1b, 0xc0, 0xdf, 0x7f, 0xef, 0x9d, 0x99, 0x56, 0x60, 0xff, 0x21, 0xc0, 0x81, 0xe6, 0xa7,
0xfd, 0x3d, 0x5d, 0xfd, 0x4e, 0x5b, 0xf0, 0x30, 0x35, 0xbc, 0x02, 0xec, 0x7f, 0x8c, 0x3f, 0x85, 0xc6, 0xee, 0x05,
0x84, 0x15, 0x37, 0x9d, 0xa0, 0x7b, 0x82, 0x97, 0x42, 0xb1, 0x35, 0xf6, 0xf3, 0xba, 0x97, 0xcc, 0x29, 0x47, 0xc6,
0x03, 0xff, 0x20, 0xc0, 0x22, 0x4b, 0xde, 0x51, 0xdb, 0x84, 0x2d, 0xdd, 0x79, 0x23, 0xc0, 0xa5, 0x97, 0xfe, 0xe0,
0xc1, 0xcb, 0x24, 0x8e, 0xfd, 0xeb, 0xe1, 0x15, 0xfb, 0x51, 0x12, 0xc7, 0xb9, 0x06, 0xdb, 0x6b, 0x89, 0xa8, 0xf7,
0x50, 0x74, 0xd4, 0x36, 0xa8, 0x22, 0xf8, 0x5d, 0x92, 0xa2, 0xe4, 0x75, 0x98, 0xce, 0x7f, 0x0b, 0x5f, 0xa1, 0x9b,
0x30, 0x9d, 0xb3, 0x57, 0xc1, 0x1c, 0x25, 0x37, 0xc1, 0x1c, 0xa5, 0x69, 0x38, 0x47, 0xf1, 0x17, 0x8c, 0x6a, 0x2e,
0x04, 0xc1, 0x52, 0x49, 0xc0, 0xc8, 0x58, 0xad, 0xd6, 0x40, 0x30, 0xeb, 0xb5, 0x06, 0x69, 0xdf, 0x2a, 0xa1, 0x34,
0x8e, 0xca, 0x67, 0xff, 0x97, 0x42, 0xab, 0xa9, 0x34, 0xb5, 0xd2, 0x2d, 0xc1, 0x43, 0xf6, 0xbd, 0x17, 0x07, 0x7b,
0x44, 0xee, 0xe5, 0x5f, 0x10, 0x03, 0xa5, 0xf9, 0x8a, 0x4b, 0x82, 0x9d, 0xc6, 0x5b, 0x1c, 0x95, 0x0f, 0xfe, 0xf1,
0x1c, 0x3d, 0x75, 0xd1, 0x4f, 0xf1, 0x28, 0xef, 0xe3, 0x43, 0x61, 0x36, 0x2b, 0xb4, 0x6b, 0x85, 0x34, 0x04, 0x37,
0xd6, 0x76, 0x59, 0x14, 0x6d, 0xb7, 0xdb, 0x70, 0x3b, 0x0b, 0x95, 0x5e, 0x45, 0x69, 0x1c, 0xc7, 0x91, 0xd9, 0xac,
0x30, 0x1a, 0x0b, 0x01, 0xa7, 0x37, 0x18, 0x35, 0xc0, 0x57, 0x8d, 0x1d, 0xe0, 0xf2, 0xc5, 0x01, 0x8e, 0x85, 0xe3,
0x28, 0x1f, 0x3e, 0x5d, 0x58, 0xe1, 0x17, 0x56, 0xe0, 0x47, 0xea, 0xe1, 0x53, 0x98, 0x57, 0x43, 0x98, 0xaf, 0x68,
0x8a, 0x52, 0x14, 0x0f, 0x4f, 0x1a, 0x38, 0x78, 0x3a, 0x05, 0x4f, 0x4e, 0xe8, 0xe2, 0xe4, 0xa0, 0x76, 0x11, 0xbc,
0x3e, 0xcb, 0x26, 0x0e, 0xb3, 0x49, 0xe2, 0x47, 0x84, 0x13, 0xf8, 0x65, 0x71, 0x79, 0x0e, 0xd2, 0x0f, 0x97, 0x0c,
0xce, 0x5a, 0x93, 0x7c, 0x58, 0xd0, 0x39, 0x9a, 0x4f, 0x98, 0x79, 0xe0, 0xe0, 0xf3, 0x09, 0xcd, 0x37, 0x69, 0x93,
0xb4, 0xc1, 0x22, 0x98, 0xd3, 0x19, 0x9a, 0x4d, 0x8e, 0xcc, 0xd0, 0x6c, 0x93, 0x36, 0x8b, 0x0f, 0x8b, 0x4b, 0x5c,
0x30, 0xfb, 0x72, 0x15, 0x95, 0xd8, 0xcf, 0x30, 0x7e, 0x8c, 0x5c, 0x5d, 0x46, 0x1e, 0x7e, 0x56, 0x5c, 0x7a, 0x18,
0xfb, 0xc7, 0x1a, 0x2c, 0x6b, 0x3c, 0x1c, 0x31, 0x25, 0x6b, 0xbe, 0x0a, 0x3f, 0x1b, 0x25, 0xb1, 0x1f, 0xda, 0x06,
0xa4, 0x77, 0x12, 0x75, 0x82, 0x30, 0x50, 0xbc, 0xa7, 0x14, 0xeb, 0x1f, 0xce, 0xf5, 0x6f, 0xb9, 0x15, 0x40, 0x6c,
0xe8, 0x1a, 0xf6, 0xfa, 0x8c, 0x5d, 0xaa, 0x6a, 0xff, 0x8d, 0xd6, 0x68, 0x92, 0xb1, 0x2f, 0xb8, 0x94, 0xa0, 0xef,
0x61, 0x67, 0x09, 0x7e, 0xf7, 0xe6, 0x2d, 0x7a, 0x53, 0x55, 0x1a, 0x8c, 0xc9, 0x10, 0x7e, 0x69, 0xc3, 0x96, 0xb2,
0xff, 0x5d, 0x57, 0xf2, 0x95, 0xae, 0x7f, 0xf2, 0x9f, 0x39, 0xfa, 0x1d, 0xec, 0x56, 0xe9, 0xf5, 0xa4, 0xcd, 0xb9,
0x96, 0xbb, 0x0e, 0xd3, 0xc4, 0x86, 0xb4, 0x33, 0xa1, 0x11, 0x9c, 0x81, 0x97, 0xf8, 0x61, 0x4b, 0xbb, 0xc7, 0xa8,
0xe4, 0x29, 0x51, 0x0f, 0x45, 0xc5, 0x37, 0x88, 0x09, 0x6a, 0x0c, 0xc1, 0x72, 0x54, 0x85, 0xd1, 0x33, 0x34, 0xfc,
0x94, 0x64, 0x82, 0xb3, 0x35, 0xc1, 0x7f, 0x31, 0x01, 0x7e, 0xda, 0xff, 0x5a, 0x79, 0x57, 0xc6, 0xf0, 0xea, 0xca,
0x0f, 0x37, 0x54, 0xf4, 0x80, 0x08, 0xb2, 0x0d, 0x37, 0x8f, 0x0e, 0xe6, 0xdf, 0x14, 0xeb, 0xcc, 0xfa, 0xca, 0x0f,
0x6b, 0xc5, 0x7a, 0xe3, 0xf9, 0xb8, 0x9c, 0xcc, 0x15, 0x74, 0x1c, 0x90, 0xf8, 0x39, 0x7e, 0xe2, 0x51, 0x20, 0xa0,
0xb6, 0x67, 0x3e, 0x84, 0x5e, 0x1c, 0x8c, 0x27, 0x43, 0x6d, 0x0c, 0xf7, 0x8f, 0x67, 0x64, 0x61, 0x3a, 0x2a, 0x9f,
0x0a, 0x3a, 0x07, 0x5d, 0xab, 0xc8, 0xd0, 0x41, 0xae, 0x5f, 0x3a, 0x2a, 0xcf, 0x06, 0x23, 0x7a, 0x02, 0x5f, 0x1c,
0xb8, 0x27, 0xdd, 0x14, 0x5c, 0x9f, 0x35, 0x16, 0x51, 0xc5, 0x37, 0xe5, 0xc3, 0xd1, 0x7f, 0x8c, 0xe3, 0x5f, 0x3d,
0xe8, 0xfd, 0x1d, 0x08, 0x60, 0x56, 0x69, 0x0f, 0x3f, 0x97, 0x60, 0xb1, 0x3f, 0x06, 0xfc, 0xcb, 0xfd, 0xbb, 0xdf,
0x88, 0xf2, 0xb4, 0x7f, 0xfd, 0x2d, 0x6e, 0xb7, 0x0a, 0x3e, 0x6a, 0x10, 0xff, 0x26, 0x57, 0x6e, 0x19, 0x5c, 0x7d,
0xc2, 0x7e, 0x38, 0xc4, 0xfb, 0xf0, 0xb8, 0x11, 0x5c, 0x3b, 0xbf, 0xdc, 0xb5, 0xe2, 0xda, 0x45, 0x18, 0x2c, 0xe6,
0xfe, 0xf1, 0xe1, 0xe8, 0x1f, 0xfd, 0xbc, 0x88, 0xc6, 0xb9, 0x5e, 0x16, 0xc3, 0x88, 0x2d, 0x7f, 0x38, 0x2c, 0xd5,
0x2e, 0x30, 0xfc, 0x0b, 0x97, 0xab, 0x8c, 0xcb, 0x06, 0x34, 0xb7, 0xc7, 0x8a, 0x6f, 0xae, 0xb9, 0xec, 0x7a, 0x7b,
0xe8, 0x68, 0x55, 0x39, 0xca, 0xbc, 0xdb, 0xe5, 0xb5, 0x92, 0xd6, 0x71, 0x42, 0x96, 0x40, 0x7b, 0x1c, 0xe9, 0xc3,
0x44, 0xc9, 0x5e, 0xcf, 0xbf, 0x3b, 0xba, 0x82, 0x3b, 0x58, 0xd8, 0xd9, 0x80, 0x0a, 0xbe, 0x92, 0x19, 0x03, 0x69,
0x41, 0x8f, 0x42, 0x35, 0x6d, 0xb9, 0xd8, 0x67, 0x86, 0x4a, 0x13, 0x18, 0xd0, 0xbc, 0x3e, 0x2e, 0x7b, 0x6b, 0x95,
0x3c, 0x2c, 0x95, 0xae, 0x40, 0x67, 0x71, 0x3e, 0x02, 0x81, 0xa6, 0x15, 0xef, 0x4d, 0x16, 0xce, 0x34, 0xb4, 0xf9,
0x92, 0xb2, 0xf5, 0x4a, 0xab, 0x5e, 0x56, 0x01, 0x73, 0x93, 0x36, 0x7b, 0x9e, 0xd4, 0x74, 0x06, 0x2c, 0x9f, 0x4e,
0x75, 0x5d, 0xe7, 0x82, 0x4b, 0x08, 0xc6, 0x59, 0x96, 0xa5, 0xe1, 0x8d, 0x13, 0xbb, 0x70, 0x33, 0x4c, 0x1d, 0x62,
0xf4, 0x31, 0x89, 0xe3, 0xef, 0xf2, 0x53, 0x38, 0x71, 0xce, 0x7a, 0x6d, 0x94, 0xce, 0x3a, 0xc5, 0x9d, 0x9b, 0xc7,
0x96, 0x72, 0x79, 0xe9, 0xbd, 0x2b, 0x93, 0x7c, 0x5a, 0x3f, 0x19, 0x97, 0x83, 0x99, 0x61, 0x09, 0xe5, 0x2d, 0x97,
0xe3, 0x0e, 0xcd, 0xd2, 0x45, 0xdc, 0xed, 0x8e, 0xe1, 0x54, 0x20, 0x87, 0x13, 0x77, 0x2d, 0x60, 0x97, 0x7f, 0xee,
0x8d, 0xe5, 0xf5, 0x3e, 0x98, 0x76, 0x70, 0x66, 0x3a, 0xca, 0x20, 0x58, 0x82, 0xdd, 0x02, 0xc8, 0x7c, 0xb0, 0x11,
0x70, 0x0b, 0xad, 0x99, 0xf2, 0x74, 0x56, 0x33, 0x14, 0xe8, 0xd7, 0xba, 0xfe, 0x1b, 0xb7, 0xab, 0xc5, 0x43, 0x4b,
0xf5, 0x8a, 0xcb, 0x60, 0xa9, 0xac, 0x55, 0x6d, 0x16, 0xbc, 0xea, 0x76, 0xf9, 0x84, 0x72, 0xca, 0xb2, 0xc4, 0xb9,
0x39, 0xec, 0xd6, 0x53, 0xbe, 0x93, 0x6e, 0x87, 0x8c, 0x12, 0xbc, 0x9a, 0xf8, 0x06, 0x16, 0x14, 0x9f, 0xd3, 0x93,
0xcc, 0xbb, 0x1d, 0x72, 0xb8, 0x53, 0xaa, 0x6f, 0xea, 0x5b, 0x9a, 0xc4, 0x7f, 0xf1, 0x45, 0xaa, 0xba, 0x4e, 0x97,
0xf5, 0x39, 0x53, 0x6e, 0x4d, 0xba, 0xd6, 0x18, 0x4a, 0xab, 0x88, 0xc6, 0xdb, 0x8c, 0xab, 0x8c, 0xb2, 0x70, 0x19,
0x2e, 0x8b, 0x26, 0x41, 0xbc, 0x22, 0x2d, 0x65, 0xe5, 0xc5, 0xf8, 0x2a, 0xa2, 0x26, 0x39, 0x91, 0x9a, 0xa4, 0xfc,
0x6a, 0x18, 0x8d, 0xb4, 0xc1, 0xfb, 0xf2, 0xad, 0x92, 0x12, 0x98, 0xe5, 0x72, 0x85, 0xac, 0x42, 0x53, 0x0a, 0xc2,
0x30, 0x2c, 0x96, 0xba, 0x7c, 0x2f, 0x80, 0x1a, 0x40, 0x5b, 0xca, 0x6d, 0x58, 0x44, 0x23, 0xff, 0xd8, 0xc7, 0xbc,
0x22, 0x12, 0x6c, 0x39, 0x35, 0x6c, 0xd1, 0xcc, 0x46, 0x03, 0x77, 0x60, 0x9d, 0x26, 0x67, 0x60, 0x56, 0x16, 0x6e,
0xe5, 0x22, 0x3a, 0x8c, 0x34, 0x12, 0x6d, 0x79, 0xcd, 0xdd, 0x95, 0xa5, 0x2c, 0x86, 0x22, 0x77, 0x1a, 0x5c, 0x9e,
0xc7, 0xeb, 0xd5, 0x00, 0x09, 0x90, 0x2b, 0xdb, 0x90, 0x59, 0x8a, 0x3a, 0x41, 0x19, 0x34, 0x4a, 0x54, 0xa0, 0xc9,
0xdd, 0xdd, 0xaf, 0x7f, 0x2f, 0x9d, 0x33, 0x8f, 0x72, 0x9d, 0x59, 0x8f, 0x62, 0x0e, 0x98, 0xa4, 0x16, 0x37, 0xe3,
0xa5, 0xaa, 0xa3, 0xc6, 0x6c, 0x95, 0xae, 0xbe, 0xd2, 0xf1, 0x7e, 0x42, 0x8e, 0x7a, 0x86, 0xff, 0xd0, 0x2a, 0xe5,
0x1d, 0xdd, 0x40, 0x11, 0x4d, 0x87, 0x22, 0x72, 0x0e, 0x8f, 0xf4, 0x66, 0xe2, 0x6b, 0x92, 0xf2, 0x8f, 0xfb, 0x37,
0xe8, 0xcf, 0xae, 0xa2, 0x16, 0xc6, 0xb4, 0x0d, 0x51, 0xb5, 0x60, 0x1b, 0x55, 0x91, 0xf7, 0x7f, 0xdc, 0xdd, 0x9f,
0x23, 0xec, 0x07, 0x26, 0x04, 0x92, 0x8d, 0xd7, 0xbb, 0x5e, 0x58, 0xde, 0x51, 0x6d, 0x07, 0xb5, 0x81, 0x9b, 0x22,
0xa7, 0x18, 0x06, 0x7a, 0xcd, 0x05, 0x8c, 0x61, 0x8c, 0x82, 0x25, 0x3a, 0x79, 0x75, 0xb2, 0xf6, 0xc4, 0xaf, 0x68,
0xfc, 0xda, 0xd1, 0xf8, 0xe9, 0xa3, 0xe1, 0xa6, 0xfb, 0x1f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00};
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13, 0x95, 0x56, 0xeb, 0x6f, 0xd4, 0x38, 0x10, 0xff, 0xce,
0x5f, 0xe1, 0x33, 0x8f, 0x26, 0xd0, 0x3c, 0xb7, 0xdb, 0x96, 0x6c, 0x12, 0x04, 0xdc, 0x21, 0x90, 0x28, 0x20, 0xb5,
0x70, 0x1f, 0x10, 0x52, 0xbd, 0xc9, 0x64, 0x63, 0x9a, 0x38, 0x39, 0xdb, 0xfb, 0x62, 0xb5, 0xf7, 0xb7, 0xdf, 0x38,
0xc9, 0x6e, 0xb7, 0x15, 0x9c, 0xee, 0x5a, 0x35, 0x1d, 0xdb, 0xf3, 0xf8, 0xcd, 0x78, 0x1e, 0x8e, 0x7f, 0xcb, 0x9b,
0x4c, 0xaf, 0x5b, 0x20, 0xa5, 0xae, 0xab, 0x34, 0x36, 0x5f, 0x52, 0x31, 0x31, 0x4b, 0x40, 0xe0, 0x0a, 0x58, 0x9e,
0xc6, 0x35, 0x68, 0x46, 0xb2, 0x92, 0x49, 0x05, 0x3a, 0xf9, 0x7c, 0xf5, 0xc6, 0x39, 0x4f, 0xe3, 0x8a, 0x8b, 0x1b,
0x22, 0xa1, 0x4a, 0x78, 0xd6, 0x08, 0x52, 0x4a, 0x28, 0x92, 0x9c, 0x69, 0x16, 0xf1, 0x9a, 0xcd, 0x60, 0x10, 0x11,
0xac, 0x86, 0x64, 0xc1, 0x61, 0xd9, 0x36, 0x52, 0x13, 0xe4, 0xd3, 0x20, 0x74, 0x42, 0x97, 0x3c, 0xd7, 0x65, 0x92,
0xc3, 0x82, 0x67, 0xe0, 0x74, 0x8b, 0x63, 0x2e, 0xb8, 0xe6, 0xac, 0x72, 0x54, 0xc6, 0x2a, 0x48, 0x82, 0xe3, 0xb9,
0x02, 0xd9, 0x2d, 0xd8, 0x14, 0xd7, 0xa2, 0xa1, 0x69, 0xac, 0x32, 0xc9, 0x5b, 0x4d, 0x0c, 0xd4, 0xa4, 0x6e, 0xf2,
0x79, 0x05, 0xa9, 0xe7, 0x31, 0x85, 0x90, 0x94, 0xc7, 0x45, 0x0e, 0x2b, 0x77, 0xea, 0x67, 0x99, 0x0f, 0xe7, 0xe7,
0xee, 0x77, 0xf5, 0x00, 0x9d, 0x9a, 0xd7, 0x68, 0xcd, 0xad, 0x9a, 0x8c, 0x69, 0xde, 0x08, 0x57, 0x01, 0x93, 0x59,
0x99, 0x24, 0x09, 0x7d, 0xa1, 0xd8, 0x02, 0xe8, 0x93, 0x27, 0xd6, 0x9e, 0x69, 0x06, 0xfa, 0x8f, 0x0a, 0x0c, 0xa9,
0x5e, 0xad, 0xaf, 0xd8, 0xec, 0x03, 0x02, 0xb7, 0x28, 0x53, 0x3c, 0x07, 0x6a, 0x7f, 0xf5, 0xbf, 0xb9, 0x4a, 0xaf,
0x2b, 0x70, 0x73, 0xae, 0xda, 0x8a, 0xad, 0x13, 0x3a, 0x45, 0xad, 0x37, 0xd4, 0x9e, 0x14, 0x73, 0x91, 0x19, 0xe5,
0x44, 0x59, 0x60, 0x6f, 0x2a, 0x40, 0x78, 0xc9, 0x05, 0xd3, 0xa5, 0x5b, 0xb3, 0x95, 0xd5, 0x13, 0x5c, 0x58, 0xe1,
0x53, 0x0b, 0x9e, 0x05, 0xbe, 0x6f, 0x1f, 0x77, 0x1f, 0xdf, 0xf6, 0xf0, 0xff, 0x44, 0x82, 0x9e, 0x4b, 0x41, 0x98,
0x75, 0x1d, 0xb7, 0xc8, 0x49, 0xf2, 0x84, 0x5e, 0x04, 0x21, 0x09, 0x9e, 0xbb, 0xe1, 0xf8, 0xbd, 0x7b, 0x46, 0x4e,
0xf0, 0x7f, 0x76, 0xe6, 0x8c, 0x49, 0x70, 0x82, 0x9f, 0x30, 0x74, 0xc7, 0xc4, 0xff, 0x41, 0x49, 0xc1, 0xab, 0x2a,
0xa1, 0xa2, 0x11, 0x40, 0x89, 0xd2, 0xb2, 0xb9, 0x81, 0x84, 0x66, 0x73, 0x29, 0x11, 0xfb, 0xeb, 0xa6, 0x6a, 0x24,
0xf5, 0xd2, 0x07, 0xff, 0x4b, 0xa1, 0x96, 0x4c, 0xa8, 0xa2, 0x91, 0x75, 0x42, 0xbb, 0xe8, 0x5b, 0x8f, 0x36, 0x7a,
0x4b, 0xcc, 0xc7, 0x3e, 0x38, 0x74, 0x1a, 0xc9, 0x67, 0x5c, 0x24, 0xd4, 0x68, 0x3c, 0x47, 0x23, 0xd7, 0xf6, 0x76,
0xef, 0x3d, 0x33, 0xde, 0x0f, 0xfe, 0x34, 0xd6, 0xd7, 0xeb, 0x58, 0x2d, 0x66, 0x64, 0x55, 0x57, 0x42, 0x25, 0xb4,
0xd4, 0xba, 0x8d, 0x3c, 0x6f, 0xb9, 0x5c, 0xba, 0xcb, 0x91, 0xdb, 0xc8, 0x99, 0x17, 0xfa, 0xbe, 0xef, 0x21, 0x07,
0x25, 0x7d, 0x22, 0xd0, 0xf0, 0x84, 0x92, 0x12, 0xf8, 0xac, 0xd4, 0x1d, 0x9d, 0x3e, 0xda, 0xc0, 0x36, 0x36, 0x1c,
0xe9, 0xf5, 0xb7, 0x03, 0x2b, 0xfc, 0xc0, 0x0a, 0xbc, 0x60, 0x16, 0xdd, 0xb9, 0x79, 0xd4, 0xb9, 0x79, 0xc6, 0x42,
0x12, 0x12, 0xbf, 0xfb, 0x0d, 0x1d, 0x43, 0x0f, 0x2b, 0xe7, 0xde, 0x8a, 0x1c, 0xac, 0x0c, 0x55, 0x9f, 0x3a, 0xcf,
0xf7, 0xb2, 0x81, 0xd9, 0x59, 0x04, 0xfe, 0xed, 0x86, 0x11, 0x78, 0x7b, 0x7a, 0xb8, 0x76, 0xc2, 0x2f, 0x87, 0x0c,
0xc6, 0x5a, 0x19, 0x7c, 0x39, 0x65, 0x63, 0x32, 0x1e, 0x76, 0xc6, 0x8e, 0xa1, 0xf7, 0x2b, 0x32, 0x5e, 0x20, 0x47,
0xed, 0x9c, 0x3a, 0x63, 0x36, 0x22, 0xa3, 0x01, 0x08, 0x52, 0xb8, 0x7d, 0x8a, 0x82, 0x07, 0x7b, 0xce, 0xe8, 0xc7,
0x91, 0x97, 0x52, 0x3b, 0xa2, 0xf4, 0xd6, 0xf3, 0xe6, 0xd0, 0x73, 0xf7, 0x7b, 0x83, 0x39, 0x45, 0x29, 0x46, 0x06,
0x74, 0x56, 0x5a, 0xd4, 0xc3, 0xc2, 0x2a, 0xf8, 0x0c, 0xb3, 0xbe, 0x11, 0xd4, 0x76, 0x75, 0x09, 0xc2, 0xda, 0x89,
0x1a, 0x41, 0xe8, 0x4e, 0xac, 0xfb, 0x27, 0xda, 0xde, 0xec, 0xf3, 0x5f, 0x73, 0x8d, 0x65, 0xa6, 0x5d, 0x53, 0xb0,
0xc7, 0xfb, 0xdd, 0x69, 0x93, 0xaf, 0x7f, 0x51, 0x1a, 0x65, 0xd0, 0xd7, 0x05, 0x17, 0x02, 0xe4, 0x15, 0xac, 0xf0,
0xe6, 0x2e, 0x5e, 0xbe, 0x26, 0x2f, 0xf3, 0x5c, 0x82, 0x52, 0x11, 0xa1, 0xcf, 0x34, 0xd6, 0x40, 0xf6, 0xdf, 0x75,
0x05, 0x77, 0x74, 0xfd, 0xc9, 0xdf, 0x70, 0xf2, 0x01, 0xf4, 0xb2, 0x91, 0x37, 0x83, 0x36, 0x03, 0x6d, 0x62, 0x2a,
0x4c, 0x22, 0x4e, 0xd6, 0x2a, 0x57, 0x55, 0xd8, 0x3e, 0xac, 0xc0, 0x46, 0x3b, 0xed, 0xad, 0x57, 0x62, 0x17, 0xa8,
0xeb, 0x38, 0xe7, 0x0b, 0x92, 0x55, 0xd8, 0x21, 0xb0, 0x5c, 0x7a, 0x55, 0x94, 0x3c, 0x20, 0xdd, 0x4f, 0x23, 0x32,
0x94, 0xbe, 0x49, 0xe8, 0x4f, 0x3a, 0xc0, 0xab, 0xf5, 0xbb, 0xdc, 0x3a, 0x52, 0x58, 0xfb, 0x47, 0xb6, 0xbb, 0x60,
0xd5, 0x1c, 0x48, 0x42, 0x74, 0xc9, 0xd5, 0x2d, 0xc0, 0xc9, 0x2f, 0xc5, 0x5a, 0x75, 0x83, 0x52, 0x05, 0x1e, 0x2b,
0xcb, 0xa6, 0xe9, 0x60, 0x2e, 0x66, 0x7d, 0x83, 0xa4, 0x0f, 0xe9, 0x3d, 0x44, 0x4e, 0x05, 0x85, 0xde, 0xf3, 0x11,
0x2c, 0x3b, 0x65, 0x09, 0x57, 0xa2, 0x75, 0x7b, 0xbb, 0xdf, 0x8c, 0x55, 0xcb, 0xc4, 0x7d, 0x41, 0x03, 0xd0, 0x94,
0x0a, 0x36, 0x36, 0xa4, 0x4c, 0xbd, 0x20, 0xd3, 0xde, 0xa0, 0xc7, 0x76, 0xe4, 0xa3, 0x0d, 0x47, 0x8d, 0xa6, 0x5f,
0xed, 0x35, 0xc6, 0x1e, 0x86, 0x26, 0xbd, 0xde, 0xda, 0xb7, 0x7e, 0xfc, 0x35, 0x07, 0xb9, 0xbe, 0x84, 0x0a, 0x32,
0xdd, 0x48, 0x8b, 0x3e, 0x44, 0x2b, 0x98, 0x4a, 0x9d, 0xc3, 0x6f, 0xaf, 0x2e, 0xde, 0x27, 0x8d, 0x25, 0xed, 0xe3,
0x5f, 0x71, 0x9b, 0x51, 0xf0, 0x15, 0x47, 0xc1, 0xdf, 0xc9, 0x91, 0x19, 0x06, 0x47, 0xdf, 0x50, 0xb4, 0xf3, 0xf7,
0xfa, 0x76, 0x22, 0x98, 0x72, 0x7e, 0x86, 0x2d, 0xe1, 0xd8, 0x78, 0xe8, 0x9c, 0x8e, 0xed, 0x2d, 0xda, 0x47, 0x04,
0x88, 0xbb, 0xeb, 0xeb, 0xd8, 0xdf, 0x4d, 0x8b, 0x4d, 0x9f, 0x6e, 0xa6, 0xcd, 0xca, 0x51, 0xfc, 0x07, 0x17, 0xb3,
0x88, 0x8b, 0x12, 0x24, 0xd7, 0x5b, 0x84, 0x8b, 0x13, 0xa2, 0x9d, 0xeb, 0x4d, 0xcb, 0xf2, 0xdc, 0x9c, 0x8c, 0xdb,
0xd5, 0xa4, 0xc0, 0x79, 0x62, 0x38, 0x21, 0x0a, 0xa0, 0xde, 0xf6, 0xe7, 0x5d, 0x47, 0x89, 0x9e, 0x8f, 0x1f, 0x6f,
0x4d, 0xc2, 0x6d, 0x34, 0x5e, 0x96, 0xc3, 0x2a, 0x3e, 0x13, 0x51, 0x86, 0xc0, 0x41, 0xf6, 0x42, 0x05, 0xab, 0x79,
0xb5, 0x8e, 0x14, 0xf6, 0x36, 0x07, 0x07, 0x0d, 0x2f, 0xb6, 0xd3, 0xb9, 0xd6, 0x8d, 0x40, 0xdb, 0x32, 0x07, 0x19,
0xf9, 0x93, 0x9e, 0x70, 0x24, 0xcb, 0xf9, 0x5c, 0x45, 0xee, 0x48, 0x42, 0x3d, 0x99, 0xb2, 0xec, 0x66, 0x26, 0x9b,
0xb9, 0xc8, 0x9d, 0xcc, 0x74, 0xda, 0xe8, 0x61, 0x50, 0xb0, 0x11, 0x64, 0x93, 0x61, 0x55, 0x14, 0xc5, 0x04, 0x43,
0x01, 0x4e, 0xdf, 0xcb, 0xa2, 0xd0, 0x3d, 0x31, 0x62, 0x07, 0x30, 0xdd, 0xd0, 0x6c, 0xf4, 0x18, 0x71, 0x04, 0x3c,
0x9e, 0xec, 0xdc, 0xf1, 0x27, 0xd8, 0xc2, 0x15, 0x2a, 0x69, 0xb1, 0xb6, 0x11, 0xe6, 0xb6, 0x66, 0x5c, 0x1c, 0xa2,
0x37, 0x69, 0x32, 0x19, 0xc6, 0x0f, 0x86, 0xa5, 0x33, 0xd3, 0x0d, 0xa1, 0x09, 0x0e, 0x98, 0x7e, 0x86, 0x46, 0xe1,
0xa9, 0xdf, 0xae, 0xb6, 0xee, 0x90, 0x20, 0x9b, 0x1d, 0x77, 0x51, 0xc1, 0x6a, 0xf2, 0x7d, 0xae, 0x34, 0x2f, 0xd6,
0xce, 0x30, 0x83, 0x23, 0x4c, 0x16, 0x9c, 0xbd, 0x53, 0x64, 0x05, 0x10, 0x93, 0xce, 0x86, 0xc3, 0x35, 0xd4, 0x6a,
0x88, 0xd3, 0x5e, 0x4d, 0x97, 0xa0, 0x77, 0x75, 0xfd, 0x1b, 0xb7, 0xc9, 0xc5, 0x4d, 0xcd, 0x24, 0x8e, 0x0a, 0x67,
0xda, 0x60, 0x4c, 0xeb, 0xc8, 0x39, 0xc3, 0xbb, 0x1a, 0xb6, 0x8c, 0x32, 0xf4, 0x1c, 0x61, 0x76, 0xb3, 0x75, 0x17,
0xef, 0xa0, 0x5d, 0x11, 0xd5, 0x54, 0x3c, 0x1f, 0xf8, 0x3a, 0x16, 0xe2, 0xef, 0xc3, 0x13, 0xe0, 0x75, 0x13, 0xb3,
0xb7, 0x0b, 0xf5, 0x49, 0x71, 0xce, 0x02, 0xff, 0x27, 0x37, 0x92, 0x17, 0x45, 0x38, 0x2d, 0xf6, 0x91, 0x32, 0x63,
0xd2, 0x94, 0x46, 0x97, 0x5a, 0xb1, 0xd7, 0xbf, 0x66, 0x4c, 0x66, 0xe0, 0x03, 0x05, 0x23, 0x8c, 0xef, 0x9b, 0x80,
0xf0, 0x3c, 0xc1, 0x4e, 0x95, 0x1e, 0xb4, 0x2f, 0x64, 0x0c, 0x76, 0x47, 0x48, 0xdd, 0x69, 0x46, 0xfd, 0x59, 0x87,
0x3e, 0x7d, 0xdd, 0x60, 0x7d, 0x60, 0xdb, 0x11, 0x33, 0xa2, 0x1b, 0x32, 0x84, 0xc0, 0x75, 0xdd, 0x78, 0x2a, 0xd3,
0x4f, 0x15, 0x30, 0x05, 0x64, 0xc9, 0xb8, 0x76, 0xb1, 0x1a, 0x3b, 0xfe, 0xbe, 0x8e, 0x51, 0x29, 0xb2, 0xa6, 0x43,
0xc1, 0xc6, 0xe5, 0xa8, 0x37, 0x70, 0x09, 0xda, 0x68, 0x32, 0x06, 0x46, 0x69, 0x6c, 0x46, 0x2e, 0x61, 0x5d, 0x4b,
0x4b, 0xbc, 0x25, 0x2f, 0xb8, 0x79, 0xb2, 0xa4, 0x71, 0x97, 0xe4, 0x46, 0x83, 0x89, 0x73, 0xff, 0xbc, 0xea, 0xa8,
0x0a, 0xc4, 0x0c, 0x27, 0xe9, 0x28, 0x24, 0xe8, 0x76, 0x06, 0x65, 0x53, 0x61, 0x58, 0x93, 0xcb, 0xcb, 0x77, 0xbf,
0xa7, 0x06, 0xcc, 0xad, 0x1c, 0xf6, 0xa7, 0x5e, 0xcc, 0x10, 0x83, 0xd4, 0xe9, 0x49, 0xff, 0xa8, 0x6a, 0xb1, 0xbf,
0xa0, 0x07, 0xf9, 0x1d, 0x1d, 0x9f, 0x86, 0xcd, 0x5e, 0x4f, 0xf7, 0xd7, 0x95, 0x4a, 0x7a, 0x89, 0x80, 0x62, 0x6f,
0x58, 0xc4, 0x9e, 0x01, 0xdc, 0x9f, 0x97, 0x03, 0x1f, 0xc6, 0xe9, 0xe3, 0xd5, 0x4b, 0xf2, 0xb9, 0xc5, 0x26, 0x00,
0x7d, 0xd8, 0x3a, 0xaf, 0xf0, 0x65, 0x58, 0x36, 0x79, 0xf2, 0xe9, 0xe3, 0xe5, 0xd5, 0xde, 0xc3, 0x79, 0xc7, 0x44,
0x40, 0x64, 0xfd, 0xf3, 0x6e, 0x5e, 0x69, 0xde, 0x32, 0xa9, 0x3b, 0xb5, 0x8e, 0xe9, 0x22, 0x3b, 0x1f, 0xba, 0x73,
0x7c, 0x03, 0x41, 0xef, 0x46, 0x2f, 0x98, 0x92, 0x1d, 0xaa, 0x9d, 0xb5, 0x7b, 0xb8, 0xbc, 0xfe, 0xb6, 0xbd, 0xfe,
0xea, 0xbd, 0xee, 0xa5, 0xfb, 0x0f, 0x53, 0x58, 0x46, 0xb2, 0xf9, 0x0a, 0x00, 0x00};
#else // Brotli (default, smaller)
const uint8_t INDEX_BR[] PROGMEM = {

View File

@@ -93,9 +93,7 @@ bool CH422GComponent::read_inputs_() {
bool CH422GComponent::write_reg_(uint8_t reg, uint8_t value) {
auto err = this->bus_->write_readv(reg, &value, 1, nullptr, 0);
if (err != i2c::ERROR_OK) {
char buf[64];
snprintf(buf, sizeof(buf), "write failed for register 0x%X, error %d", reg, err);
this->status_set_warning(buf);
this->status_set_warning(str_sprintf("write failed for register 0x%X, error %d", reg, err).c_str());
return false;
}
this->status_clear_warning();
@@ -106,9 +104,7 @@ uint8_t CH422GComponent::read_reg_(uint8_t reg) {
uint8_t value;
auto err = this->bus_->write_readv(reg, nullptr, 0, &value, 1);
if (err != i2c::ERROR_OK) {
char buf[64];
snprintf(buf, sizeof(buf), "read failed for register 0x%X, error %d", reg, err);
this->status_set_warning(buf);
this->status_set_warning(str_sprintf("read failed for register 0x%X, error %d", reg, err).c_str());
return 0;
}
this->status_clear_warning();

View File

@@ -682,19 +682,19 @@ bool Climate::set_fan_mode_(ClimateFanMode mode) {
return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode);
}
bool Climate::set_custom_fan_mode_(const char *mode, size_t len) {
bool Climate::set_custom_fan_mode_(const char *mode) {
auto traits = this->get_traits();
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode,
traits.find_custom_fan_mode_(mode, len), this->has_custom_fan_mode());
return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode),
this->has_custom_fan_mode());
}
void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; }
bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); }
bool Climate::set_custom_preset_(const char *preset, size_t len) {
bool Climate::set_custom_preset_(const char *preset) {
auto traits = this->get_traits();
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset, len),
return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset),
this->has_custom_preset());
}

View File

@@ -5,7 +5,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/preferences.h"
#include "esphome/core/string_ref.h"
#include "climate_mode.h"
#include "climate_traits.h"
@@ -111,8 +110,8 @@ class ClimateCall {
const optional<ClimateFanMode> &get_fan_mode() const;
const optional<ClimateSwingMode> &get_swing_mode() const;
const optional<ClimatePreset> &get_preset() const;
StringRef get_custom_fan_mode() const { return StringRef::from_maybe_nullptr(this->custom_fan_mode_); }
StringRef get_custom_preset() const { return StringRef::from_maybe_nullptr(this->custom_preset_); }
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
const char *get_custom_preset() const { return this->custom_preset_; }
bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; }
bool has_custom_preset() const { return this->custom_preset_ != nullptr; }
@@ -267,11 +266,11 @@ class Climate : public EntityBase {
/// The active swing mode of the climate device.
ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
/// Get the active custom fan mode (read-only access). Returns StringRef.
StringRef get_custom_fan_mode() const { return StringRef::from_maybe_nullptr(this->custom_fan_mode_); }
/// Get the active custom fan mode (read-only access).
const char *get_custom_fan_mode() const { return this->custom_fan_mode_; }
/// Get the active custom preset (read-only access). Returns StringRef.
StringRef get_custom_preset() const { return StringRef::from_maybe_nullptr(this->custom_preset_); }
/// Get the active custom preset (read-only access).
const char *get_custom_preset() const { return this->custom_preset_; }
protected:
friend ClimateCall;
@@ -281,9 +280,7 @@ class Climate : public EntityBase {
bool set_fan_mode_(ClimateFanMode mode);
/// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed.
bool set_custom_fan_mode_(const char *mode) { return this->set_custom_fan_mode_(mode, strlen(mode)); }
bool set_custom_fan_mode_(const char *mode, size_t len);
bool set_custom_fan_mode_(StringRef mode) { return this->set_custom_fan_mode_(mode.c_str(), mode.size()); }
bool set_custom_fan_mode_(const char *mode);
/// Clear custom fan mode.
void clear_custom_fan_mode_();
@@ -291,9 +288,7 @@ class Climate : public EntityBase {
bool set_preset_(ClimatePreset preset);
/// Set custom preset. Reset primary preset. Return true if preset has been changed.
bool set_custom_preset_(const char *preset) { return this->set_custom_preset_(preset, strlen(preset)); }
bool set_custom_preset_(const char *preset, size_t len);
bool set_custom_preset_(StringRef preset) { return this->set_custom_preset_(preset.c_str(), preset.size()); }
bool set_custom_preset_(const char *preset);
/// Clear custom preset.
void clear_custom_preset_();

View File

@@ -8,24 +8,20 @@ static const char *const TAG = "copy.fan";
void CopyFan::setup() {
source_->add_on_state_callback([this]() {
this->copy_state_from_source_();
this->state = source_->state;
this->oscillating = source_->oscillating;
this->speed = source_->speed;
this->direction = source_->direction;
this->set_preset_mode_(source_->get_preset_mode());
this->publish_state();
});
this->copy_state_from_source_();
this->publish_state();
}
void CopyFan::copy_state_from_source_() {
this->state = source_->state;
this->oscillating = source_->oscillating;
this->speed = source_->speed;
this->direction = source_->direction;
if (source_->has_preset_mode()) {
this->set_preset_mode_(source_->get_preset_mode());
} else {
this->clear_preset_mode_();
}
this->set_preset_mode_(source_->get_preset_mode());
this->publish_state();
}
void CopyFan::dump_config() { LOG_FAN("", "Copy Fan", this); }

View File

@@ -16,7 +16,7 @@ class CopyFan : public fan::Fan, public Component {
protected:
void control(const fan::FanCall &call) override;
void copy_state_from_source_();
;
fan::Fan *source_;
};

View File

@@ -322,8 +322,6 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
return "Unspecified";
};
char mac_pretty[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
get_mac_address_pretty_into_buffer(mac_pretty);
ESP_LOGD(TAG,
"Code page size: %u, code size: %u, device id: 0x%08x%08x\n"
"Encryption root: 0x%08x%08x%08x%08x, Identity Root: 0x%08x%08x%08x%08x\n"
@@ -332,10 +330,10 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
"RAM: %ukB, Flash: %ukB, production test: %sdone",
NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE, NRF_FICR->DEVICEID[1], NRF_FICR->DEVICEID[0], NRF_FICR->ER[0],
NRF_FICR->ER[1], NRF_FICR->ER[2], NRF_FICR->ER[3], NRF_FICR->IR[0], NRF_FICR->IR[1], NRF_FICR->IR[2],
NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), mac_pretty, NRF_FICR->INFO.PART,
NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF, NRF_FICR->INFO.VARIANT >> 8 & 0xFF,
NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE), NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH,
(NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
NRF_FICR->IR[3], (NRF_FICR->DEVICEADDRTYPE & 0x1 ? "Random" : "Public"), get_mac_address_pretty().c_str(),
NRF_FICR->INFO.PART, NRF_FICR->INFO.VARIANT >> 24 & 0xFF, NRF_FICR->INFO.VARIANT >> 16 & 0xFF,
NRF_FICR->INFO.VARIANT >> 8 & 0xFF, NRF_FICR->INFO.VARIANT & 0xFF, package(NRF_FICR->INFO.PACKAGE),
NRF_FICR->INFO.RAM, NRF_FICR->INFO.FLASH, (NRF_FICR->PRODTEST[0] == 0xBB42319F ? "" : "not "));
bool n_reset_enabled = NRF_UICR->PSELRESET[0] == NRF_UICR->PSELRESET[1] &&
(NRF_UICR->PSELRESET[0] & UICR_PSELRESET_CONNECT_Msk) == UICR_PSELRESET_CONNECT_Connected
<< UICR_PSELRESET_CONNECT_Pos;

View File

@@ -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 RTC pins (esp_deep_sleep_enable_gpio_wakeup)
// - GPIO wakeup: GPIO wakeup for non-RTC pins (esp_deep_sleep_enable_gpio_wakeup)
static const char *const TAG = "deep_sleep";
@@ -127,14 +127,22 @@ 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());
// Make sure GPIO is in input mode, not all RTC GPIO pins are input by default
gpio_set_direction(gpio_pin, GPIO_MODE_INPUT);
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
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));
}

View File

@@ -42,8 +42,7 @@ std::string MenuItemSelect::get_value_text() const {
result = this->value_getter_.value()(this);
} else {
if (this->select_var_ != nullptr) {
auto option = this->select_var_->current_option();
result.assign(option.c_str(), option.size());
result = this->select_var_->current_option();
}
}

View File

@@ -82,9 +82,8 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
return;
}
auto effect_name = light_effect->get_name();
ESP_LOGD(TAG, "Registering '%.*s' for universes %d-%d.", (int) effect_name.size(), effect_name.c_str(),
light_effect->get_first_universe(), light_effect->get_last_universe());
ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
light_effect->get_last_universe());
light_effects_.push_back(light_effect);
@@ -99,9 +98,8 @@ void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
return;
}
auto effect_name = light_effect->get_name();
ESP_LOGD(TAG, "Unregistering '%.*s' for universes %d-%d.", (int) effect_name.size(), effect_name.c_str(),
light_effect->get_first_universe(), light_effect->get_last_universe());
ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
light_effect->get_last_universe());
// Swap with last element and pop for O(1) removal (order doesn't matter)
*it = light_effects_.back();

View File

@@ -58,9 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1));
auto *input_data = packet.values + 1;
auto effect_name = get_name();
ESP_LOGV(TAG, "Applying data for '%.*s' on %d universe, for %" PRId32 "-%d.", (int) effect_name.size(),
effect_name.c_str(), universe, output_offset, output_end);
ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset,
output_end);
switch (channels_) {
case E131_MONO:

View File

@@ -19,7 +19,6 @@ 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,
@@ -34,13 +33,6 @@ 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)
@@ -103,7 +95,6 @@ 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,
@@ -126,7 +117,6 @@ 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:

View File

@@ -75,15 +75,8 @@ 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);
TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL);
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();
@@ -118,12 +111,6 @@ 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;

View File

@@ -10,16 +10,10 @@
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) {
@@ -34,7 +28,6 @@ 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_{};

View File

@@ -93,9 +93,9 @@ async def to_code(config):
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
os.environ["ESP_IDF_VERSION"] = f"{framework_ver.major}.{framework_ver.minor}"
if framework_ver >= cv.Version(5, 5, 0):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.4")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.4")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.9.3")
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.2.2")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.7.0")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")

View File

@@ -4,24 +4,18 @@ from typing import Any
import esphome.codegen as cg
from esphome.components import esp32, update
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_PATH, CONF_SOURCE, CONF_TYPE
from esphome.core import CORE, ID, HexInt
from esphome.const import CONF_PATH, CONF_RAW_DATA_ID
from esphome.core import CORE, HexInt
CODEOWNERS = ["@swoboda1337"]
AUTO_LOAD = ["sha256", "watchdog", "json"]
AUTO_LOAD = ["sha256", "watchdog"]
DEPENDENCIES = ["esp32_hosted"]
CONF_SHA256 = "sha256"
CONF_HTTP_REQUEST_ID = "http_request_id"
TYPE_EMBEDDED = "embedded"
TYPE_HTTP = "http"
esp32_hosted_ns = cg.esphome_ns.namespace("esp32_hosted")
http_request_ns = cg.esphome_ns.namespace("http_request")
HttpRequestComponent = http_request_ns.class_("HttpRequestComponent", cg.Component)
Esp32HostedUpdate = esp32_hosted_ns.class_(
"Esp32HostedUpdate", update.UpdateEntity, cg.PollingComponent
"Esp32HostedUpdate", update.UpdateEntity, cg.Component
)
@@ -36,29 +30,12 @@ def _validate_sha256(value: Any) -> str:
return value
BASE_SCHEMA = update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
cv.polling_component_schema("6h")
)
EMBEDDED_SCHEMA = BASE_SCHEMA.extend(
{
cv.Required(CONF_PATH): cv.file_,
cv.Required(CONF_SHA256): _validate_sha256,
}
)
HTTP_SCHEMA = BASE_SCHEMA.extend(
{
cv.GenerateID(CONF_HTTP_REQUEST_ID): cv.use_id(HttpRequestComponent),
cv.Required(CONF_SOURCE): cv.url,
}
)
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
update.update_schema(Esp32HostedUpdate, device_class="firmware").extend(
{
TYPE_EMBEDDED: EMBEDDED_SCHEMA,
TYPE_HTTP: HTTP_SCHEMA,
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
cv.Required(CONF_PATH): cv.file_,
cv.Required(CONF_SHA256): _validate_sha256,
}
),
esp32.only_on_variant(
@@ -71,9 +48,6 @@ CONFIG_SCHEMA = cv.All(
def _validate_firmware(config: dict[str, Any]) -> None:
if config[CONF_TYPE] != TYPE_EMBEDDED:
return
path = CORE.relative_config_path(config[CONF_PATH])
with open(path, "rb") as f:
firmware_data = f.read()
@@ -91,22 +65,14 @@ FINAL_VALIDATE_SCHEMA = _validate_firmware
async def to_code(config: dict[str, Any]) -> None:
var = await update.new_update(config)
if config[CONF_TYPE] == TYPE_EMBEDDED:
path = config[CONF_PATH]
with open(CORE.relative_config_path(path), "rb") as f:
firmware_data = f.read()
rhs = [HexInt(x) for x in firmware_data]
arr_id = ID(f"{config[CONF_ID]}_data", is_declaration=True, type=cg.uint8)
prog_arr = cg.progmem_array(arr_id, rhs)
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
cg.add(var.set_firmware_data(prog_arr))
cg.add(var.set_firmware_size(len(firmware_data)))
else:
http_request_var = await cg.get_variable(config[CONF_HTTP_REQUEST_ID])
cg.add(var.set_http_request_parent(http_request_var))
cg.add(var.set_source_url(config[CONF_SOURCE]))
cg.add_define("USE_ESP32_HOSTED_HTTP_UPDATE")
path = config[CONF_PATH]
with open(CORE.relative_config_path(path), "rb") as f:
firmware_data = f.read()
rhs = [HexInt(x) for x in firmware_data]
prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs)
sha256_bytes = bytes.fromhex(config[CONF_SHA256])
cg.add(var.set_firmware_sha256([HexInt(b) for b in sha256_bytes]))
cg.add(var.set_firmware_data(prog_arr))
cg.add(var.set_firmware_size(len(firmware_data)))
await cg.register_component(var, config)

View File

@@ -7,12 +7,6 @@
#include <esp_image_format.h>
#include <esp_app_desc.h>
#include <esp_hosted.h>
#include <esp_hosted_host_fw_ver.h>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/json/json_util.h"
#include "esphome/components/network/util.h"
#endif
extern "C" {
#include <esp_hosted_ota.h>
@@ -22,50 +16,18 @@ namespace esphome::esp32_hosted {
static const char *const TAG = "esp32_hosted.update";
// Older coprocessor firmware versions have a 1500-byte limit per RPC call
// older coprocessor firmware versions have a 1500-byte limit per RPC call
constexpr size_t CHUNK_SIZE = 1500;
// Compile-time version string from esp_hosted_host_fw_ver.h macros
#define STRINGIFY_(x) #x
#define STRINGIFY(x) STRINGIFY_(x)
static const char *const ESP_HOSTED_VERSION_STR = STRINGIFY(ESP_HOSTED_VERSION_MAJOR_1) "." STRINGIFY(
ESP_HOSTED_VERSION_MINOR_1) "." STRINGIFY(ESP_HOSTED_VERSION_PATCH_1);
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// Parse version string "major.minor.patch" into components
// Returns true if parsing succeeded
static bool parse_version(const std::string &version_str, int &major, int &minor, int &patch) {
major = minor = patch = 0;
if (sscanf(version_str.c_str(), "%d.%d.%d", &major, &minor, &patch) >= 2) {
return true;
}
return false;
}
// Compare two versions, returns:
// -1 if v1 < v2
// 0 if v1 == v2
// 1 if v1 > v2
static int compare_versions(int major1, int minor1, int patch1, int major2, int minor2, int patch2) {
if (major1 != major2)
return major1 < major2 ? -1 : 1;
if (minor1 != minor2)
return minor1 < minor2 ? -1 : 1;
if (patch1 != patch2)
return patch1 < patch2 ? -1 : 1;
return 0;
}
#endif
void Esp32HostedUpdate::setup() {
this->update_info_.title = "ESP32 Hosted Coprocessor";
// if wifi is not present, connect to the coprocessor
#ifndef USE_WIFI
// If WiFi is not present, connect to the coprocessor
esp_hosted_connect_to_slave(); // NOLINT
#endif
// Get coprocessor version
// get coprocessor version
esp_hosted_coprocessor_fwver_t ver_info;
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
@@ -74,8 +36,7 @@ void Esp32HostedUpdate::setup() {
}
ESP_LOGD(TAG, "Coprocessor version: %s", this->update_info_.current_version.c_str());
#ifndef USE_ESP32_HOSTED_HTTP_UPDATE
// Embedded mode: get image version from embedded firmware
// get image version
const int app_desc_offset = sizeof(esp_image_header_t) + sizeof(esp_image_segment_header_t);
if (this->firmware_size_ >= app_desc_offset + sizeof(esp_app_desc_t)) {
esp_app_desc_t *app_desc = (esp_app_desc_t *) (this->firmware_data_ + app_desc_offset);
@@ -103,272 +64,58 @@ void Esp32HostedUpdate::setup() {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
// Publish state
// publish state
this->status_clear_error();
this->publish_state();
#else
// HTTP mode: retry initial check every 10s until network is ready (max 6 attempts)
// Only if update interval is > 1 minute to avoid redundant checks
if (this->get_update_interval() > 60000) {
this->set_retry("initial_check", 10000, 6, [this](uint8_t) {
if (!network::is_connected()) {
return RetryResult::RETRY;
}
this->check();
return RetryResult::DONE;
});
}
#endif
}
void Esp32HostedUpdate::dump_config() {
ESP_LOGCONFIG(TAG,
"ESP32 Hosted Update:\n"
" Host Library Version: %s\n"
" Coprocessor Version: %s\n"
" Latest Version: %s",
ESP_HOSTED_VERSION_STR, this->update_info_.current_version.c_str(),
this->update_info_.latest_version.c_str());
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
ESP_LOGCONFIG(TAG,
" Mode: HTTP\n"
" Source URL: %s",
this->source_url_.c_str());
#else
ESP_LOGCONFIG(TAG,
" Mode: Embedded\n"
" Firmware Size: %zu bytes",
" Current Version: %s\n"
" Latest Version: %s\n"
" Latest Size: %zu bytes",
this->update_info_.current_version.c_str(), this->update_info_.latest_version.c_str(),
this->firmware_size_);
#endif
}
void Esp32HostedUpdate::check() {
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
if (!network::is_connected()) {
ESP_LOGD(TAG, "Network not connected, skipping update check");
void Esp32HostedUpdate::perform(bool force) {
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
ESP_LOGW(TAG, "Update not available");
return;
}
if (!this->fetch_manifest_()) {
return;
}
// Compare versions
if (this->update_info_.latest_version.empty() ||
this->update_info_.latest_version == this->update_info_.current_version) {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
} else {
this->state_ = update::UPDATE_STATE_AVAILABLE;
}
this->update_info_.has_progress = false;
this->update_info_.progress = 0.0f;
this->status_clear_error();
this->publish_state();
#endif
}
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
bool Esp32HostedUpdate::fetch_manifest_() {
ESP_LOGD(TAG, "Fetching manifest");
auto container = this->http_request_parent_->get(this->source_url_);
if (container == nullptr || container->status_code != 200) {
ESP_LOGE(TAG, "Failed to fetch manifest from %s", this->source_url_.c_str());
this->status_set_error(LOG_STR("Failed to fetch manifest"));
return false;
}
// Read manifest JSON into string (manifest is small, ~1KB max)
std::string json_str;
json_str.reserve(container->content_length);
uint8_t buf[256];
while (container->get_bytes_read() < container->content_length) {
int read = container->read(buf, sizeof(buf));
if (read > 0) {
json_str.append(reinterpret_cast<char *>(buf), read);
}
yield();
}
container->end();
// Parse JSON manifest
// Format: {"versions": [{"version": "2.7.0", "url": "...", "sha256": "..."}]}
// Only consider versions <= host library version to avoid compatibility issues
bool valid = json::parse_json(json_str, [this](JsonObject root) -> bool {
if (!root["versions"].is<JsonArray>()) {
ESP_LOGE(TAG, "Manifest does not contain 'versions' array");
return false;
}
JsonArray versions = root["versions"].as<JsonArray>();
if (versions.size() == 0) {
ESP_LOGE(TAG, "Manifest 'versions' array is empty");
return false;
}
// Find the highest version that is compatible with the host library
// (version <= host version to avoid upgrading coprocessor ahead of host)
int best_major = -1, best_minor = -1, best_patch = -1;
std::string best_version, best_url, best_sha256;
for (JsonObject entry : versions) {
if (!entry["version"].is<const char *>() || !entry["url"].is<const char *>() ||
!entry["sha256"].is<const char *>()) {
continue; // Skip malformed entries
}
std::string ver_str = entry["version"].as<std::string>();
int major, minor, patch;
if (!parse_version(ver_str, major, minor, patch)) {
ESP_LOGW(TAG, "Failed to parse version: %s", ver_str.c_str());
continue;
}
// Check if this version is compatible (not newer than host)
if (compare_versions(major, minor, patch, ESP_HOSTED_VERSION_MAJOR_1, ESP_HOSTED_VERSION_MINOR_1,
ESP_HOSTED_VERSION_PATCH_1) > 0) {
continue;
}
// Check if this is better than our current best
if (best_major < 0 || compare_versions(major, minor, patch, best_major, best_minor, best_patch) > 0) {
best_major = major;
best_minor = minor;
best_patch = patch;
best_version = ver_str;
best_url = entry["url"].as<std::string>();
best_sha256 = entry["sha256"].as<std::string>();
}
}
if (best_major < 0) {
ESP_LOGW(TAG, "No compatible firmware version found (host is %s)", ESP_HOSTED_VERSION_STR);
return false;
}
this->update_info_.latest_version = best_version;
this->firmware_url_ = best_url;
// Parse SHA256 hex string to bytes
if (!parse_hex(best_sha256, this->firmware_sha256_.data(), 32)) {
ESP_LOGE(TAG, "Invalid SHA256: %s", best_sha256.c_str());
return false;
}
ESP_LOGD(TAG, "Best compatible version: %s", this->update_info_.latest_version.c_str());
return true;
});
if (!valid) {
ESP_LOGE(TAG, "Failed to parse manifest JSON");
this->status_set_error(LOG_STR("Failed to parse manifest"));
return false;
}
return true;
}
bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
ESP_LOGI(TAG, "Downloading firmware");
auto container = this->http_request_parent_->get(this->firmware_url_);
if (container == nullptr || container->status_code != 200) {
ESP_LOGE(TAG, "Failed to fetch firmware");
this->status_set_error(LOG_STR("Failed to fetch firmware"));
return false;
}
size_t total_size = container->content_length;
ESP_LOGI(TAG, "Firmware size: %zu bytes", total_size);
// Begin OTA on coprocessor
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
container->end();
this->status_set_error(LOG_STR("Failed to begin OTA"));
return false;
}
// Stream firmware to coprocessor while computing SHA256
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
hasher.init();
uint8_t buffer[CHUNK_SIZE];
while (container->get_bytes_read() < total_size) {
int read = container->read(buffer, sizeof(buffer));
// Feed watchdog and give other tasks a chance to run
App.feed_wdt();
yield();
// Exit loop if no data available (stream closed or end of data)
if (read <= 0) {
if (read < 0) {
ESP_LOGE(TAG, "Stream closed with error");
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Download failed"));
return false;
}
// read == 0: no more data available, exit loop
break;
}
hasher.add(buffer, read);
err = esp_hosted_slave_ota_write(buffer, read); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
container->end();
this->status_set_error(LOG_STR("Failed to write OTA data"));
return false;
}
}
container->end();
// Verify SHA256
hasher.calculate();
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
ESP_LOGE(TAG, "SHA256 mismatch");
esp_hosted_slave_ota_end(); // NOLINT
this->status_set_error(LOG_STR("SHA256 verification failed"));
return false;
}
ESP_LOGI(TAG, "SHA256 verified successfully");
return true;
}
#else
bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
if (this->firmware_data_ == nullptr || this->firmware_size_ == 0) {
ESP_LOGE(TAG, "No firmware data available");
this->status_set_error(LOG_STR("No firmware data available"));
return false;
return;
}
// Verify SHA256 before writing
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
// ESP32-S3 hardware SHA acceleration requires 32-byte DMA alignment (IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
hasher.init();
hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate();
if (!hasher.equals_bytes(this->firmware_sha256_.data())) {
ESP_LOGE(TAG, "SHA256 mismatch");
this->status_set_error(LOG_STR("SHA256 verification failed"));
return false;
this->publish_state();
return;
}
ESP_LOGI(TAG, "Starting OTA update (%zu bytes)", this->firmware_size_);
watchdog::WatchdogManager watchdog(20000);
update::UpdateState prev_state = this->state_;
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = false;
this->publish_state();
esp_err_t err = esp_hosted_slave_ota_begin(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to begin OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error(LOG_STR("Failed to begin OTA"));
return false;
this->publish_state();
return;
}
uint8_t chunk[CHUNK_SIZE];
@@ -381,68 +128,42 @@ bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to write OTA data: %s", esp_err_to_name(err));
esp_hosted_slave_ota_end(); // NOLINT
this->state_ = prev_state;
this->status_set_error(LOG_STR("Failed to write OTA data"));
return false;
this->publish_state();
return;
}
data_ptr += chunk_size;
remaining -= chunk_size;
App.feed_wdt();
}
return true;
}
#endif
void Esp32HostedUpdate::perform(bool force) {
if (this->state_ != update::UPDATE_STATE_AVAILABLE && !force) {
ESP_LOGW(TAG, "Update not available");
return;
}
update::UpdateState prev_state = this->state_;
this->state_ = update::UPDATE_STATE_INSTALLING;
this->update_info_.has_progress = false;
this->publish_state();
watchdog::WatchdogManager watchdog(60000);
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
if (!this->stream_firmware_to_coprocessor_())
#else
if (!this->write_embedded_firmware_to_coprocessor_())
#endif
{
this->state_ = prev_state;
this->publish_state();
return;
}
// End OTA and activate new firmware
esp_err_t end_err = esp_hosted_slave_ota_end(); // NOLINT
if (end_err != ESP_OK) {
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(end_err));
err = esp_hosted_slave_ota_end(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to end OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error(LOG_STR("Failed to end OTA"));
this->publish_state();
return;
}
esp_err_t activate_err = esp_hosted_slave_ota_activate(); // NOLINT
if (activate_err != ESP_OK) {
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(activate_err));
// activate new firmware
err = esp_hosted_slave_ota_activate(); // NOLINT
if (err != ESP_OK) {
ESP_LOGE(TAG, "Failed to activate OTA: %s", esp_err_to_name(err));
this->state_ = prev_state;
this->status_set_error(LOG_STR("Failed to activate OTA"));
this->publish_state();
return;
}
// Update state
// update state
ESP_LOGI(TAG, "OTA update successful");
this->state_ = update::UPDATE_STATE_NO_UPDATE;
this->status_clear_error();
this->publish_state();
// Schedule a restart to ensure everything is in sync
// schedule a restart to ensure everything is in sync
ESP_LOGI(TAG, "Restarting in 1 second");
this->set_timeout(1000, []() { App.safe_reboot(); });
}

View File

@@ -5,55 +5,26 @@
#include "esphome/core/component.h"
#include "esphome/components/update/update_entity.h"
#include <array>
#include <string>
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
#include "esphome/components/http_request/http_request.h"
#endif
namespace esphome::esp32_hosted {
class Esp32HostedUpdate : public update::UpdateEntity, public PollingComponent {
class Esp32HostedUpdate : public update::UpdateEntity, public Component {
public:
void setup() override;
void dump_config() override;
float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
void update() override { this->check(); } // PollingComponent - delegates to check()
void perform(bool force) override;
void check() override;
void check() override {}
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// HTTP mode setters
void set_source_url(const std::string &url) { this->source_url_ = url; }
void set_http_request_parent(http_request::HttpRequestComponent *parent) { this->http_request_parent_ = parent; }
#else
// Embedded mode setters
void set_firmware_data(const uint8_t *data) { this->firmware_data_ = data; }
void set_firmware_size(size_t size) { this->firmware_size_ = size; }
void set_firmware_sha256(const std::array<uint8_t, 32> &sha256) { this->firmware_sha256_ = sha256; }
#endif
protected:
#ifdef USE_ESP32_HOSTED_HTTP_UPDATE
// HTTP mode members
http_request::HttpRequestComponent *http_request_parent_{nullptr};
std::string source_url_;
std::string firmware_url_;
// HTTP mode helpers
bool fetch_manifest_();
bool stream_firmware_to_coprocessor_();
#else
// Embedded mode members
const uint8_t *firmware_data_{nullptr};
size_t firmware_size_{0};
// Embedded mode helper
bool write_embedded_firmware_to_coprocessor_();
#endif
std::array<uint8_t, 32> firmware_sha256_{};
std::array<uint8_t, 32> firmware_sha256_;
};
} // namespace esphome::esp32_hosted

View File

@@ -7,7 +7,6 @@
#include "esphome/core/component.h"
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
#include "esphome/core/string_ref.h"
namespace esphome {
namespace event {
@@ -45,11 +44,8 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
/// Return the event types supported by this event.
const FixedVector<const char *> &get_event_types() const { return this->types_; }
/// Return the last triggered event type, or empty StringRef if no event triggered yet.
StringRef get_last_event_type() const { return StringRef::from_maybe_nullptr(this->last_event_type_); }
/// Check if an event has been triggered.
bool has_event() const { return this->last_event_type_ != nullptr; }
/// Return the last triggered event type (pointer to string in types_), or nullptr if no event triggered yet.
const char *get_last_event_type() const { return this->last_event_type_; }
void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);

View File

@@ -212,18 +212,19 @@ class FanPresetSetTrigger : public Trigger<std::string> {
public:
FanPresetSetTrigger(Fan *state) {
state->add_on_state_callback([this, state]() {
auto preset_mode = state->get_preset_mode();
const auto *preset_mode = state->get_preset_mode();
auto should_trigger = preset_mode != this->last_preset_mode_;
this->last_preset_mode_ = preset_mode;
if (should_trigger) {
this->trigger(std::string(preset_mode));
// Trigger with empty string when nullptr to maintain backward compatibility
this->trigger(preset_mode != nullptr ? preset_mode : "");
}
});
this->last_preset_mode_ = state->get_preset_mode();
}
protected:
StringRef last_preset_mode_{};
const char *last_preset_mode_{nullptr};
};
} // namespace fan

View File

@@ -61,7 +61,7 @@ void FanCall::perform() {
if (this->direction_.has_value()) {
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(*this->direction_)));
}
if (this->preset_mode_ != nullptr) {
if (this->has_preset_mode()) {
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
}
this->parent_.control(*this);
@@ -83,7 +83,7 @@ void FanCall::validate_() {
*this->binary_state_
// ..,and no preset mode will be active...
&& !this->has_preset_mode() &&
!this->parent_.has_preset_mode()
this->parent_.get_preset_mode() == nullptr
// ...and neither current nor new speed is available...
&& traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) {
// ...set speed to 100%
@@ -154,16 +154,16 @@ const char *Fan::find_preset_mode_(const char *preset_mode, size_t len) {
return this->get_traits().find_preset_mode(preset_mode, len);
}
bool Fan::set_preset_mode_(const char *preset_mode, size_t len) {
if (preset_mode == nullptr || len == 0) {
// Treat nullptr or empty string as clearing the preset mode (no valid preset is "")
bool Fan::set_preset_mode_(const char *preset_mode) {
if (preset_mode == nullptr) {
// Treat nullptr as clearing the preset mode
if (this->preset_mode_ == nullptr) {
return false; // No change
}
this->clear_preset_mode_();
return true;
}
const char *validated = this->find_preset_mode_(preset_mode, len);
const char *validated = this->find_preset_mode_(preset_mode);
if (validated == nullptr || this->preset_mode_ == validated) {
return false; // Preset mode not supported or no change
}
@@ -171,31 +171,10 @@ bool Fan::set_preset_mode_(const char *preset_mode, size_t len) {
return true;
}
bool Fan::set_preset_mode_(const char *preset_mode) {
return this->set_preset_mode_(preset_mode, preset_mode ? strlen(preset_mode) : 0);
}
bool Fan::set_preset_mode_(const std::string &preset_mode) {
return this->set_preset_mode_(preset_mode.data(), preset_mode.size());
}
bool Fan::set_preset_mode_(StringRef preset_mode) {
// Safe: find_preset_mode_ only uses the input for comparison and returns
// a pointer from traits, so the input StringRef's lifetime doesn't matter.
return this->set_preset_mode_(preset_mode.c_str(), preset_mode.size());
}
bool Fan::set_preset_mode_(const std::string &preset_mode) { return this->set_preset_mode_(preset_mode.c_str()); }
void Fan::clear_preset_mode_() { this->preset_mode_ = nullptr; }
void Fan::apply_preset_mode_(const FanCall &call) {
if (call.has_preset_mode()) {
this->set_preset_mode_(call.get_preset_mode());
} else if (call.get_speed().has_value()) {
// Manually setting speed clears preset (per Home Assistant convention)
this->clear_preset_mode_();
}
}
void Fan::add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); }
void Fan::publish_state() {
auto traits = this->get_traits();
@@ -213,8 +192,9 @@ void Fan::publish_state() {
if (traits.supports_direction()) {
ESP_LOGD(TAG, " Direction: %s", LOG_STR_ARG(fan_direction_to_string(this->direction)));
}
if (this->preset_mode_ != nullptr) {
ESP_LOGD(TAG, " Preset Mode: %s", this->preset_mode_);
const char *preset = this->get_preset_mode();
if (preset != nullptr) {
ESP_LOGD(TAG, " Preset Mode: %s", preset);
}
this->state_callback_.call();
#if defined(USE_FAN) && defined(USE_CONTROLLER_REGISTRY)
@@ -269,11 +249,12 @@ void Fan::save_state_() {
state.speed = this->speed;
state.direction = this->direction;
if (this->has_preset_mode()) {
const char *preset = this->get_preset_mode();
if (preset != nullptr) {
const auto &preset_modes = traits.supported_preset_modes();
// Find index of current preset mode (pointer comparison is safe since preset is from traits)
for (size_t i = 0; i < preset_modes.size(); i++) {
if (preset_modes[i] == this->preset_mode_) {
if (preset_modes[i] == preset) {
state.preset_mode = i;
break;
}

View File

@@ -5,7 +5,6 @@
#include "esphome/core/log.h"
#include "esphome/core/optional.h"
#include "esphome/core/preferences.h"
#include "esphome/core/string_ref.h"
#include "fan_traits.h"
namespace esphome {
@@ -129,11 +128,8 @@ class Fan : public EntityBase {
/// Set the restore mode of this fan.
void set_restore_mode(FanRestoreMode restore_mode) { this->restore_mode_ = restore_mode; }
/// Get the current preset mode.
/// Returns a StringRef of the string stored in traits, or empty ref if not set.
/// The returned ref points to string literals from codegen (static storage).
/// Traits are set once at startup and valid for the lifetime of the program.
StringRef get_preset_mode() const { return StringRef::from_maybe_nullptr(this->preset_mode_); }
/// Get the current preset mode (returns pointer to string stored in traits, or nullptr if not set)
const char *get_preset_mode() const { return this->preset_mode_; }
/// Check if a preset mode is currently active
bool has_preset_mode() const { return this->preset_mode_ != nullptr; }
@@ -150,15 +146,11 @@ class Fan : public EntityBase {
void dump_traits_(const char *tag, const char *prefix);
/// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
/// Passing nullptr or empty string clears the preset mode.
bool set_preset_mode_(const char *preset_mode, size_t len);
bool set_preset_mode_(const char *preset_mode);
/// Set the preset mode (finds and stores pointer from traits). Returns true if changed.
bool set_preset_mode_(const std::string &preset_mode);
bool set_preset_mode_(StringRef preset_mode);
/// Clear the preset mode
void clear_preset_mode_();
/// Apply preset mode from a FanCall (handles speed-clears-preset convention)
void apply_preset_mode_(const FanCall &call);
/// Find and return the matching preset mode pointer from traits, or nullptr if not found.
const char *find_preset_mode_(const char *preset_mode);
const char *find_preset_mode_(const char *preset_mode, size_t len);

View File

@@ -232,19 +232,17 @@ void GraphLegend::init(Graph *g) {
ESP_LOGI(TAGL, " %s %d %d", txtstr.c_str(), fw, fh);
if (this->values_ != VALUE_POSITION_TYPE_NONE) {
char valstr[VALUE_ACCURACY_MAX_LEN];
std::string valstr =
value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
if (this->units_) {
value_accuracy_with_uom_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals(),
trace->sensor_->get_unit_of_measurement_ref());
} else {
value_accuracy_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
valstr += trace->sensor_->get_unit_of_measurement_ref();
}
this->font_value_->measure(valstr, &fw, &fos, &fbl, &fh);
this->font_value_->measure(valstr.c_str(), &fw, &fos, &fbl, &fh);
if (fw > valw)
valw = fw;
if (fh > valh)
valh = fh;
ESP_LOGI(TAGL, " %s %d %d", valstr, fw, fh);
ESP_LOGI(TAGL, " %s %d %d", valstr.c_str(), fw, fh);
}
}
// Add extra margin
@@ -370,15 +368,13 @@ void Graph::draw_legend(display::Display *buff, uint16_t x_offset, uint16_t y_of
if (legend_->values_ != VALUE_POSITION_TYPE_NONE) {
int xv = x + legend_->xv_;
int yv = y + legend_->yv_;
char valstr[VALUE_ACCURACY_MAX_LEN];
std::string valstr =
value_accuracy_to_string(trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
if (legend_->units_) {
value_accuracy_with_uom_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals(),
trace->sensor_->get_unit_of_measurement_ref());
} else {
value_accuracy_to_buf(valstr, trace->sensor_->get_state(), trace->sensor_->get_accuracy_decimals());
valstr += trace->sensor_->get_unit_of_measurement_ref();
}
buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr);
ESP_LOGV(TAG, " value: %s", valstr);
buff->printf(xv, yv, legend_->font_value_, trace->get_line_color(), TextAlign::TOP_CENTER, "%s", valstr.c_str());
ESP_LOGV(TAG, " value: %s", valstr.c_str());
}
x += legend_->xs_;
y += legend_->ys_;

View File

@@ -57,7 +57,7 @@ void HBridgeFan::control(const fan::FanCall &call) {
this->oscillating = *call.get_oscillating();
if (call.get_direction().has_value())
this->direction = *call.get_direction();
this->apply_preset_mode_(call);
this->set_preset_mode_(call.get_preset_mode());
this->write_state_();
this->publish_state();

View File

@@ -91,14 +91,11 @@ void HomeassistantNumber::control(float value) {
resp.data.init(2);
auto &entity_id = resp.data.emplace_back();
entity_id.key = ENTITY_ID_KEY;
entity_id.value = StringRef(this->entity_id_);
entity_id.value = this->entity_id_;
auto &entity_value = resp.data.emplace_back();
entity_value.key = VALUE_KEY;
// Stack buffer - no heap allocation; %g produces shortest representation
char value_buf[16];
snprintf(value_buf, sizeof(value_buf), "%g", value);
entity_value.value = StringRef(value_buf);
entity_value.value = to_string(value);
api::global_api_server->send_homeassistant_action(resp);
}

View File

@@ -55,7 +55,7 @@ void HomeassistantSwitch::write_state(bool state) {
resp.data.init(1);
auto &entity_id_kv = resp.data.emplace_back();
entity_id_kv.key = ENTITY_ID_KEY;
entity_id_kv.value = StringRef(this->entity_id_);
entity_id_kv.value = this->entity_id_;
api::global_api_server->send_homeassistant_action(resp);
}

View File

@@ -550,12 +550,12 @@ async def to_code(config: ConfigType) -> None:
ref="0.2.2",
)
# Set compile-time configuration via build flags (so external library sees them)
# Set compile-time configuration via defines
if CONF_BIT_DEPTH in config:
cg.add_build_flag(f"-DHUB75_BIT_DEPTH={config[CONF_BIT_DEPTH]}")
cg.add_define("HUB75_BIT_DEPTH", config[CONF_BIT_DEPTH])
if CONF_GAMMA_CORRECT in config:
cg.add_build_flag(f"-DHUB75_GAMMA_MODE={config[CONF_GAMMA_CORRECT].enum_value}")
cg.add_define("HUB75_GAMMA_MODE", config[CONF_GAMMA_CORRECT])
# Await all pin expressions
pin_expressions = {

View File

@@ -1,7 +1,6 @@
from __future__ import annotations
import contextlib
from dataclasses import dataclass
import hashlib
import io
import logging
@@ -38,21 +37,11 @@ image_ns = cg.esphome_ns.namespace("image")
ImageType = image_ns.enum("ImageType")
@dataclass(frozen=True)
class ImageMetaData:
width: int
height: int
image_type: str
transparency: str
CONF_OPAQUE = "opaque"
CONF_CHROMA_KEY = "chroma_key"
CONF_ALPHA_CHANNEL = "alpha_channel"
CONF_INVERT_ALPHA = "invert_alpha"
CONF_IMAGES = "images"
KEY_METADATA = "metadata"
TRANSPARENCY_TYPES = (
CONF_OPAQUE,
@@ -734,38 +723,10 @@ async def write_image(config, all_frames=False):
return prog_arr, width, height, image_type, trans_value, frame_count
async def _image_to_code(entry):
"""
Convert a single image entry to code and return its metadata.
:param entry: The config entry for the image.
:return: An ImageMetaData object
"""
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
cg.new_Pvariable(entry[CONF_ID], prog_arr, width, height, image_type, trans_value)
return ImageMetaData(
width,
height,
entry[CONF_TYPE],
entry[CONF_TRANSPARENCY],
)
async def to_code(config):
cg.add_define("USE_IMAGE")
# By now the config will be a simple list.
# Use a subkey to allow for other data in the future
CORE.data[DOMAIN] = {
KEY_METADATA: {
entry[CONF_ID].id: await _image_to_code(entry) for entry in config
}
}
def get_all_image_metadata() -> dict[str, ImageMetaData]:
"""Get all image metadata."""
return CORE.data.get(DOMAIN, {}).get(KEY_METADATA, {})
def get_image_metadata(image_id: str) -> ImageMetaData | None:
"""Get image metadata by ID for use by other components."""
return get_all_image_metadata().get(image_id)
# By now the config should be a simple list.
for entry in config:
prog_arr, width, height, image_type, trans_value, _ = await write_image(entry)
cg.new_Pvariable(
entry[CONF_ID], prog_arr, width, height, image_type, trans_value
)

View File

@@ -267,10 +267,8 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command
if (std::find(networks.begin(), networks.end(), ssid) != networks.end())
continue;
// Send each ssid separately to avoid overflowing the buffer
char rssi_buf[5]; // int8_t: -128 to 127, max 4 chars + null
*int8_to_str(rssi_buf, scan.get_rssi()) = '\0';
std::vector<uint8_t> data =
improv::build_rpc_response(improv::GET_WIFI_NETWORKS, {ssid, rssi_buf, YESNO(scan.get_with_auth())}, false);
std::vector<uint8_t> data = improv::build_rpc_response(
improv::GET_WIFI_NETWORKS, {ssid, str_sprintf("%d", scan.get_rssi()), YESNO(scan.get_with_auth())}, false);
this->send_response_(data);
networks.push_back(ssid);
}

View File

@@ -1,76 +0,0 @@
"""
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)

View File

@@ -1,138 +0,0 @@
#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

View File

@@ -1,130 +0,0 @@
#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

View File

@@ -183,14 +183,11 @@ uint8_t Lc709203f::get_register_(uint8_t register_to_read, uint16_t *register_va
return_code = this->read_register(register_to_read, &read_buffer[3], 3);
if (return_code != i2c::NO_ERROR) {
// Error on the i2c bus
char buf[64];
snprintf(buf, sizeof(buf), "Error code %d when reading from register 0x%02X", return_code, register_to_read);
this->status_set_warning(buf);
this->status_set_warning(
str_sprintf("Error code %d when reading from register 0x%02X", return_code, register_to_read).c_str());
} else if (crc8(read_buffer, 5, 0x00, 0x07, true) != read_buffer[5]) {
// I2C indicated OK, but the CRC of the data does not matcth.
char buf[64];
snprintf(buf, sizeof(buf), "CRC error reading from register 0x%02X", register_to_read);
this->status_set_warning(buf);
this->status_set_warning(str_sprintf("CRC error reading from register 0x%02X", register_to_read).c_str());
} else {
*register_value = ((uint16_t) read_buffer[4] << 8) | (uint16_t) read_buffer[3];
return i2c::NO_ERROR;
@@ -228,9 +225,8 @@ uint8_t Lc709203f::set_register_(uint8_t register_to_set, uint16_t value_to_set)
if (return_code == i2c::NO_ERROR) {
return return_code;
} else {
char buf[64];
snprintf(buf, sizeof(buf), "Error code %d when writing to register 0x%02X", return_code, register_to_set);
this->status_set_warning(buf);
this->status_set_warning(
str_sprintf("Error code %d when writing to register 0x%02X", return_code, register_to_set).c_str());
}
}

View File

@@ -442,8 +442,7 @@ bool LD2410Component::handle_ack_data_() {
ESP_LOGV(TAG, "Baud rate change");
#ifdef USE_SELECT
if (this->baud_rate_select_ != nullptr) {
auto baud = this->baud_rate_select_->current_option();
ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str());
ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option());
}
#endif
break;
@@ -767,10 +766,10 @@ void LD2410Component::set_light_out_control() {
#endif
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str());
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option());
}
if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) {
this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str());
this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option());
}
#endif
this->set_config_mode_(true);

View File

@@ -486,8 +486,7 @@ bool LD2412Component::handle_ack_data_() {
ESP_LOGV(TAG, "Baud rate change");
#ifdef USE_SELECT
if (this->baud_rate_select_ != nullptr) {
auto baud = this->baud_rate_select_->current_option();
ESP_LOGW(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str());
ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option());
}
#endif
break;
@@ -791,7 +790,7 @@ void LD2412Component::set_basic_config() {
1, TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0,
#endif
#ifdef USE_SELECT
find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option().c_str()),
find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()),
#else
0x01, // Default value if not using select
#endif
@@ -845,7 +844,7 @@ void LD2412Component::set_light_out_control() {
#endif
#ifdef USE_SELECT
if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option().c_str());
this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option());
}
#endif
uint8_t value[2] = {this->light_function_, this->light_threshold_};

View File

@@ -637,8 +637,7 @@ bool LD2450Component::handle_ack_data_() {
ESP_LOGV(TAG, "Baud rate change");
#ifdef USE_SELECT
if (this->baud_rate_select_ != nullptr) {
auto baud = this->baud_rate_select_->current_option();
ESP_LOGE(TAG, "Change baud rate to %.*s and reinstall", (int) baud.size(), baud.c_str());
ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option());
}
#endif
break;
@@ -719,8 +718,7 @@ bool LD2450Component::handle_ack_data_() {
this->publish_zone_type();
#ifdef USE_SELECT
if (this->zone_type_select_ != nullptr) {
auto zone = this->zone_type_select_->current_option();
ESP_LOGV(TAG, "Change zone type to: %.*s", (int) zone.size(), zone.c_str());
ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->current_option());
}
#endif
if (this->buffer_data_[10] == 0x00) {

View File

@@ -32,7 +32,6 @@ from .const import (
CONF_SDK_SILENT,
CONF_UART_PORT,
FAMILIES,
FAMILY_BK7231N,
FAMILY_COMPONENT,
FAMILY_FRIENDLY,
KEY_BOARD,
@@ -51,22 +50,6 @@ CODEOWNERS = ["@kuba2k2"]
AUTO_LOAD = ["preferences"]
IS_TARGET_PLATFORM = True
# BK7231N SDK options to disable unused features.
# Disabling BLE saves ~21KB RAM and ~200KB Flash because BLE init code is
# called unconditionally by the SDK. ESPHome doesn't use BLE on LibreTiny.
#
# This only works on BK7231N (BLE 5.x). Other BK72XX chips using BLE 4.2
# (BK7231T, BK7231Q, BK7251; BK7252 boards use the BK7251 family) have a bug
# where the BLE library still links and references undefined symbols when
# CFG_SUPPORT_BLE=0.
#
# Other options like CFG_TX_EVM_TEST, CFG_RX_SENSITIVITY_TEST, CFG_SUPPORT_BKREG,
# CFG_SUPPORT_OTA_HTTP, and CFG_USE_SPI_SLAVE were evaluated but provide no # NOLINT
# measurable benefit - the linker already strips unreferenced code via -gc-sections.
_BK7231N_SYS_CONFIG_OPTIONS = [
"CFG_SUPPORT_BLE=0",
]
def _detect_variant(value):
if KEY_LIBRETINY not in CORE.data:
@@ -191,9 +174,9 @@ def _notify_old_style(config):
# The dev and latest branches will be at *least* this version, which is what matters.
ARDUINO_VERSIONS = {
"dev": (cv.Version(1, 9, 2), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (cv.Version(1, 9, 2), "libretiny"),
"recommended": (cv.Version(1, 9, 2), None),
"dev": (cv.Version(1, 9, 1), "https://github.com/libretiny-eu/libretiny.git"),
"latest": (cv.Version(1, 9, 1), "libretiny"),
"recommended": (cv.Version(1, 9, 1), None),
}
@@ -363,10 +346,4 @@ async def component_to_code(config):
cg.add_platformio_option("custom_fw_name", "esphome")
cg.add_platformio_option("custom_fw_version", __version__)
# Apply chip-specific SDK options to save RAM/Flash
if config[CONF_FAMILY] == FAMILY_BK7231N:
cg.add_platformio_option(
"custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS
)
await cg.register_component(var, config)

View File

@@ -1,5 +1,4 @@
#include <cinttypes>
#include "light_call.h"
#include "light_state.h"
#include "esphome/core/log.h"
@@ -154,15 +153,15 @@ void LightCall::perform() {
} else if (this->has_effect_()) {
// EFFECT
StringRef effect_s;
const char *effect_s;
if (this->effect_ == 0u) {
effect_s = StringRef::from_lit("None");
effect_s = "None";
} else {
effect_s = this->parent_->effects_[this->effect_ - 1]->get_name();
}
if (publish) {
ESP_LOGD(TAG, " Effect: '%.*s'", (int) effect_s.size(), effect_s.c_str());
ESP_LOGD(TAG, " Effect: '%s'", effect_s);
}
this->parent_->start_effect_(this->effect_);
@@ -512,9 +511,11 @@ LightCall &LightCall::set_effect(const char *effect, size_t len) {
}
bool found = false;
StringRef effect_ref(effect, len);
for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) {
if (str_equals_case_insensitive(effect_ref, this->parent_->effects_[i]->get_name())) {
LightEffect *e = this->parent_->effects_[i];
const char *name = e->get_name();
if (strncasecmp(effect, name, len) == 0 && name[len] == '\0') {
this->set_effect(i + 1);
found = true;
break;

View File

@@ -1,28 +0,0 @@
#include "light_color_values.h"
#include <cmath>
namespace esphome::light {
LightColorValues LightColorValues::lerp(const LightColorValues &start, const LightColorValues &end, float completion) {
// Directly interpolate the raw values to avoid getter/setter overhead.
// This is safe because:
// - All LightColorValues have their values clamped when set via the setters
// - std::lerp guarantees output is in the same range as inputs
// - Therefore the output doesn't need clamping, so we can skip the setters
LightColorValues v;
v.color_mode_ = end.color_mode_;
v.state_ = std::lerp(start.state_, end.state_, completion);
v.brightness_ = std::lerp(start.brightness_, end.brightness_, completion);
v.color_brightness_ = std::lerp(start.color_brightness_, end.color_brightness_, completion);
v.red_ = std::lerp(start.red_, end.red_, completion);
v.green_ = std::lerp(start.green_, end.green_, completion);
v.blue_ = std::lerp(start.blue_, end.blue_, completion);
v.white_ = std::lerp(start.white_, end.white_, completion);
v.color_temperature_ = std::lerp(start.color_temperature_, end.color_temperature_, completion);
v.cold_white_ = std::lerp(start.cold_white_, end.cold_white_, completion);
v.warm_white_ = std::lerp(start.warm_white_, end.warm_white_, completion);
return v;
}
} // namespace esphome::light

View File

@@ -82,7 +82,26 @@ class LightColorValues {
* @param completion The completion value. 0 -> start, 1 -> end.
* @return The linearly interpolated LightColorValues.
*/
static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion);
static LightColorValues lerp(const LightColorValues &start, const LightColorValues &end, float completion) {
// Directly interpolate the raw values to avoid getter/setter overhead.
// This is safe because:
// - All LightColorValues have their values clamped when set via the setters
// - std::lerp guarantees output is in the same range as inputs
// - Therefore the output doesn't need clamping, so we can skip the setters
LightColorValues v;
v.color_mode_ = end.color_mode_;
v.state_ = std::lerp(start.state_, end.state_, completion);
v.brightness_ = std::lerp(start.brightness_, end.brightness_, completion);
v.color_brightness_ = std::lerp(start.color_brightness_, end.color_brightness_, completion);
v.red_ = std::lerp(start.red_, end.red_, completion);
v.green_ = std::lerp(start.green_, end.green_, completion);
v.blue_ = std::lerp(start.blue_, end.blue_, completion);
v.white_ = std::lerp(start.white_, end.white_, completion);
v.color_temperature_ = std::lerp(start.color_temperature_, end.color_temperature_, completion);
v.cold_white_ = std::lerp(start.cold_white_, end.cold_white_, completion);
v.warm_white_ = std::lerp(start.warm_white_, end.warm_white_, completion);
return v;
}
/** Normalize the color (RGB/W) component.
*

View File

@@ -1,7 +1,6 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/core/string_ref.h"
namespace esphome::light {
@@ -24,9 +23,9 @@ class LightEffect {
/**
* Returns the name of this effect.
* The underlying data is valid for the lifetime of the program (static string from codegen).
* The returned pointer is valid for the lifetime of the program and must not be freed.
*/
StringRef get_name() const { return StringRef(this->name_); }
const char *get_name() const { return this->name_; }
/// Internal method called by the LightState when this light effect is registered in it.
virtual void init() {}

View File

@@ -36,7 +36,7 @@ static const char *get_color_mode_json_str(ColorMode mode) {
void LightJSONSchema::dump_json(LightState &state, JsonObject root) {
// NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
if (state.supports_effects()) {
root[ESPHOME_F("effect")] = state.get_effect_name().c_str();
root[ESPHOME_F("effect")] = state.get_effect_name_ref();
root[ESPHOME_F("effect_index")] = state.get_current_effect_index();
root[ESPHOME_F("effect_count")] = state.get_effect_count();
}

View File

@@ -162,12 +162,20 @@ void LightState::publish_state() {
LightOutput *LightState::get_output() const { return this->output_; }
static constexpr const char *EFFECT_NONE = "None";
static constexpr auto EFFECT_NONE_REF = StringRef::from_lit("None");
StringRef LightState::get_effect_name() {
std::string LightState::get_effect_name() {
if (this->active_effect_index_ > 0) {
return this->effects_[this->active_effect_index_ - 1]->get_name();
}
return EFFECT_NONE;
}
StringRef LightState::get_effect_name_ref() {
if (this->active_effect_index_ > 0) {
return StringRef(this->effects_[this->active_effect_index_ - 1]->get_name());
}
return EFFECT_NONE_REF;
}

View File

@@ -140,7 +140,9 @@ class LightState : public EntityBase, public Component {
LightOutput *get_output() const;
/// Return the name of the current effect, or if no effect is active "None".
StringRef get_effect_name();
std::string get_effect_name();
/// Return the name of the current effect as StringRef (for API usage)
StringRef get_effect_name_ref();
/** Add a listener for remote values changes.
* Listener is notified when the light's remote values change (state, brightness, color, etc.)
@@ -189,11 +191,11 @@ class LightState : public EntityBase, public Component {
/// Get effect index by name. Returns 0 if effect not found.
uint32_t get_effect_index(const std::string &effect_name) const {
if (str_equals_case_insensitive(effect_name, "none")) {
if (strcasecmp(effect_name.c_str(), "none") == 0) {
return 0;
}
for (size_t i = 0; i < this->effects_.size(); i++) {
if (str_equals_case_insensitive(effect_name, this->effects_[i]->get_name())) {
if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name()) == 0) {
return i + 1; // Effects are 1-indexed in active_effect_index_
}
}
@@ -216,7 +218,7 @@ class LightState : public EntityBase, public Component {
if (index > this->effects_.size()) {
return ""; // Invalid index
}
return std::string(this->effects_[index - 1]->get_name());
return this->effects_[index - 1]->get_name();
}
/// The result of all the current_values_as_* methods have gamma correction applied.

View File

@@ -197,8 +197,8 @@ void Logger::init_log_buffer(size_t total_buffer_size) {
this->log_buffer_ = esphome::make_unique<logger::TaskLogBufferLibreTiny>(total_buffer_size);
#endif
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Start with loop disabled when using task buffer (unless using USB CDC on ESP32)
#ifdef USE_ESP32
// Start with loop disabled when using task buffer (unless using USB CDC)
// The loop will be enabled automatically when messages arrive
this->disable_loop_when_buffer_empty_();
#endif
@@ -247,7 +247,7 @@ void Logger::process_messages_() {
}
#endif
}
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
#ifdef USE_ESP32
else {
// No messages to process, disable loop if appropriate
// This reduces overhead when there's no async logging activity

View File

@@ -609,8 +609,8 @@ class Logger : public Component {
this->write_body_to_buffer_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN, buffer, buffer_at, buffer_size);
}
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// Disable loop when task buffer is empty (with USB CDC check on ESP32)
#ifdef USE_ESP32
// Disable loop when task buffer is empty (with USB CDC check)
inline void disable_loop_when_buffer_empty_() {
// Thread safety note: This is safe even if another task calls enable_loop_soon_any_context()
// concurrently. If that happens between our check and disable_loop(), the enable request

View File

@@ -16,6 +16,7 @@ from esphome.const import (
CONF_REPEAT,
CONF_TRIGGER_ID,
CONF_TYPE,
DEVICE_CLASS_DISTANCE,
DEVICE_CLASS_ILLUMINANCE,
ICON_BRIGHTNESS_5,
ICON_BRIGHTNESS_6,
@@ -168,6 +169,7 @@ 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,
@@ -177,6 +179,7 @@ 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,
@@ -186,6 +189,7 @@ 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,
@@ -194,6 +198,7 @@ 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,

View File

@@ -15,6 +15,7 @@ from esphome.const import (
CODEOWNERS = ["@looping40"]
DEPENDENCIES = ["i2c"]
AUTO_LOAD = ["gpio_expander"]
MULTI_CONF = True
CONF_BRIGHTNESS_MODE = "brightness_mode"

View File

@@ -40,14 +40,59 @@ void MAX6956::setup() {
ESP_LOGD(TAG, "setup reg[0x%.2X]=0x%.2X", MAX6956_CONFIGURATION, configuration);
}
bool MAX6956::digital_read(uint8_t pin) {
uint8_t reg_addr = MAX6956_1PORT_VALUE_START + pin;
uint8_t value = 0;
this->read_reg_(reg_addr, &value);
return (value & MASK_1PORT_VALUE);
void MAX6956::loop() {
// Invalidate cache at the start of each loop
this->reset_pin_cache_();
}
void MAX6956::digital_write(uint8_t pin, bool value) {
bool MAX6956::digital_read_hw(uint8_t pin) {
// MAX6956 pins start at MAX6956_MIN
if (pin < MAX6956_MIN || pin > MAX6956_MAX) {
return false;
}
// Calculate bank index based on the base class view (no offset adjustment)
uint8_t bank_index = pin / MAX6956_BANK_SIZE;
static const uint8_t BANK_REGS[4] = {
MAX6956_4PORTS_4_7, // Bank 0: 4 ports 4-7 (bits D0-D3, D4-D7 read as 0)
MAX6956_8PORTS_8_15, // Bank 1: 8 ports 8-15 (bits D0-D7)
MAX6956_8PORTS_16_23, // Bank 2: 8 ports 16-23 (bits D0-D7)
MAX6956_8PORTS_24_31, // Bank 3: 8 ports 24-31 (bits D0-D7)
};
// Read the appropriate register
uint8_t value = 0;
if (!this->read_reg_(BANK_REGS[bank_index], &value)) {
return false;
}
// Store in cache with proper alignment
if (bank_index == 0) {
// Special case for bank 0: pins 4-7 are in bits D0-D3, shift them to bits 4-7
this->input_banks_[0] = value << MAX6956_BANK0_SHIFT;
} else {
// Banks 1-3 map directly
this->input_banks_[bank_index] = value;
}
return true;
}
bool MAX6956::digital_read_cache(uint8_t pin) {
// MAX6956 pins start at MAX6956_MIN
if (pin < MAX6956_MIN || pin > MAX6956_MAX) {
return false;
}
// Use the base class's view of banks (no offset adjustment)
uint8_t bank_index = pin / MAX6956_BANK_SIZE;
uint8_t bit_position = pin % MAX6956_BANK_SIZE;
return (this->input_banks_[bank_index] & (1 << bit_position)) != 0;
}
void MAX6956::digital_write_hw(uint8_t pin, bool value) {
uint8_t reg_addr = MAX6956_1PORT_VALUE_START + pin;
this->write_reg_(reg_addr, value);
}

View File

@@ -3,6 +3,7 @@
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/components/i2c/i2c.h"
#include "esphome/components/gpio_expander/cached_gpio.h"
namespace esphome {
namespace max6956 {
@@ -21,29 +22,39 @@ enum MAX6956GPIORange : uint8_t {
MAX6956_MAX = 31,
};
/// Bank configuration for MAX6956
static constexpr uint8_t MAX6956_BANK_SIZE = 8;
static constexpr uint8_t MAX6956_TOTAL_PINS = 32; // Includes pins 0-3 (unused) for cache alignment
static constexpr uint8_t MAX6956_BANK0_SHIFT = 4;
enum MAX6956GPIORegisters {
MAX6956_GLOBAL_CURRENT = 0x02,
MAX6956_CONFIGURATION = 0x04,
MAX6956_TRANSITION_DETECT_MASK = 0x06,
MAX6956_DISPLAY_TEST = 0x07,
MAX6956_PORT_CONFIG_START = 0x09, // Port Configuration P7, P6, P5, P4
MAX6956_CURRENT_START = 0x12, // Current054
MAX6956_1PORT_VALUE_START = 0x20, // Port 0 only (virtual port, no action)
MAX6956_8PORTS_VALUE_START = 0x44, // 8 ports 4-11 (data bits D0-D7)
MAX6956_PORT_CONFIG_START = 0x09, // Port Configuration P7, P6, P5, P4
MAX6956_CURRENT_START = 0x12, // Current054
MAX6956_1PORT_VALUE_START = 0x20, // Port 0 only (virtual port, no action)
// 8-port bulk read registers aligned with base class banks
MAX6956_4PORTS_4_7 = 0x40, // 4 ports 4-7 (bits D0-D3, D4-D7 read as 0)
MAX6956_8PORTS_8_15 = 0x48, // 8 ports 8-15 (bits D0-D7)
MAX6956_8PORTS_16_23 = 0x50, // 8 ports 16-23 (bits D0-D7)
MAX6956_8PORTS_24_31 = 0x58, // 8 ports 24-31 (bits D0-D7)
};
enum MAX6956GPIOFlag { FLAG_LED = 0x20 };
enum MAX6956CURRENTMODE { GLOBAL = 0x00, SEGMENT = 0x01 };
class MAX6956 : public Component, public i2c::I2CDevice {
class MAX6956 : public Component,
public i2c::I2CDevice,
public gpio_expander::CachedGpioExpander<uint8_t, MAX6956_TOTAL_PINS> {
public:
MAX6956() = default;
void setup() override;
void loop() override;
bool digital_read(uint8_t pin);
void digital_write(uint8_t pin, bool value);
void pin_mode(uint8_t pin, gpio::Flags flags);
void pin_mode(uint8_t pin, max6956::MAX6956GPIOFlag flags);
@@ -58,6 +69,11 @@ class MAX6956 : public Component, public i2c::I2CDevice {
void write_brightness_global();
void write_brightness_mode();
// CachedGpioExpander implementation
bool digital_read_hw(uint8_t pin) override;
bool digital_read_cache(uint8_t pin) override;
void digital_write_hw(uint8_t pin, bool value) override;
protected:
// read a given register
bool read_reg_(uint8_t reg, uint8_t *value);
@@ -66,6 +82,13 @@ class MAX6956 : public Component, public i2c::I2CDevice {
max6956::MAX6956CURRENTMODE brightness_mode_;
uint8_t global_brightness_;
// Cache for the 4 banks of 8 pins each (aligned with base class view)
// Bank 0: bits 0-7 (bits 0-3 unused as MAX6956 pins start at 4, bits 4-7 = pins 4-7)
// Bank 1: bits 8-15 (pins 8-15)
// Bank 2: bits 16-23 (pins 16-23)
// Bank 3: bits 24-31 (pins 24-31)
uint8_t input_banks_[4] = {0, 0, 0, 0};
private:
int8_t prev_bright_[28] = {0};
};

View File

@@ -65,14 +65,12 @@ void AirConditioner::control(const ClimateCall &call) {
if (call.get_preset().has_value()) {
ctrl.preset = Converters::to_midea_preset(call.get_preset().value());
} else if (call.has_custom_preset()) {
// get_custom_preset() returns StringRef pointing to null-terminated string literals from codegen
ctrl.preset = Converters::to_midea_preset(call.get_custom_preset().c_str());
ctrl.preset = Converters::to_midea_preset(call.get_custom_preset());
}
if (call.get_fan_mode().has_value()) {
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_fan_mode().value());
} else if (call.has_custom_fan_mode()) {
// get_custom_fan_mode() returns StringRef pointing to null-terminated string literals from codegen
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode().c_str());
ctrl.fanMode = Converters::to_midea_fan_mode(call.get_custom_fan_mode());
}
this->base_.control(ctrl);
}

View File

@@ -77,13 +77,6 @@ CONF_DISCOVER_IP = "discover_ip"
CONF_IDF_SEND_ASYNC = "idf_send_async"
CONF_WAIT_FOR_CONNECTION = "wait_for_connection"
# Max lengths for stack-based topic building.
# These values are used in cv.Length() validators below to ensure the C++ code
# in mqtt_component.cpp can safely use fixed-size stack buffers without overflow.
# If you change these, update the corresponding constants in mqtt_component.cpp.
TOPIC_PREFIX_MAX_LEN = 64 # Default is device name, typically short
DISCOVERY_PREFIX_MAX_LEN = 64 # Default is "homeassistant" (13 chars)
def validate_message_just_topic(value):
value = cv.publish_topic(value)
@@ -113,7 +106,6 @@ MQTT_MESSAGE_SCHEMA = cv.Any(
mqtt_ns = cg.esphome_ns.namespace("mqtt")
MQTTMessage = mqtt_ns.struct("MQTTMessage")
MQTTClientDisconnectReason = mqtt_ns.enum("MQTTClientDisconnectReason")
MQTTClientComponent = mqtt_ns.class_("MQTTClientComponent", cg.Component)
MQTTPublishAction = mqtt_ns.class_("MQTTPublishAction", automation.Action)
MQTTPublishJsonAction = mqtt_ns.class_("MQTTPublishJsonAction", automation.Action)
@@ -125,11 +117,9 @@ MQTTMessageTrigger = mqtt_ns.class_(
MQTTJsonMessageTrigger = mqtt_ns.class_(
"MQTTJsonMessageTrigger", automation.Trigger.template(cg.JsonObjectConst)
)
MQTTConnectTrigger = mqtt_ns.class_(
"MQTTConnectTrigger", automation.Trigger.template(cg.bool_)
)
MQTTConnectTrigger = mqtt_ns.class_("MQTTConnectTrigger", automation.Trigger.template())
MQTTDisconnectTrigger = mqtt_ns.class_(
"MQTTDisconnectTrigger", automation.Trigger.template(MQTTClientDisconnectReason)
"MQTTDisconnectTrigger", automation.Trigger.template()
)
MQTTComponent = mqtt_ns.class_("MQTTComponent", cg.Component)
MQTTConnectedCondition = mqtt_ns.class_("MQTTConnectedCondition", Condition)
@@ -263,9 +253,9 @@ CONFIG_SCHEMA = cv.All(
),
cv.Optional(CONF_DISCOVERY_RETAIN, default=True): cv.boolean,
cv.Optional(CONF_DISCOVER_IP, default=True): cv.boolean,
cv.Optional(CONF_DISCOVERY_PREFIX, default="homeassistant"): cv.All(
cv.publish_topic, cv.Length(max=DISCOVERY_PREFIX_MAX_LEN)
),
cv.Optional(
CONF_DISCOVERY_PREFIX, default="homeassistant"
): cv.publish_topic,
cv.Optional(CONF_DISCOVERY_UNIQUE_ID_GENERATOR, default="legacy"): cv.enum(
MQTT_DISCOVERY_UNIQUE_ID_GENERATOR_OPTIONS
),
@@ -276,9 +266,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_BIRTH_MESSAGE): MQTT_MESSAGE_SCHEMA,
cv.Optional(CONF_WILL_MESSAGE): MQTT_MESSAGE_SCHEMA,
cv.Optional(CONF_SHUTDOWN_MESSAGE): MQTT_MESSAGE_SCHEMA,
cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.All(
cv.publish_topic, cv.Length(max=TOPIC_PREFIX_MAX_LEN)
),
cv.Optional(CONF_TOPIC_PREFIX, default=lambda: CORE.name): cv.publish_topic,
cv.Optional(CONF_LOG_TOPIC): cv.Any(
None,
MQTT_MESSAGE_BASE.extend(
@@ -478,15 +466,11 @@ async def to_code(config):
for conf in config.get(CONF_ON_CONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(cg.bool_, "session_present")], conf
)
await automation.build_automation(trigger, [], conf)
for conf in config.get(CONF_ON_DISCONNECT, []):
trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
await automation.build_automation(
trigger, [(MQTTClientDisconnectReason, "reason")], conf
)
await automation.build_automation(trigger, [], conf)
cg.add(var.set_publish_nan_as_none(config[CONF_PUBLISH_NAN_AS_NONE]))

View File

@@ -12,9 +12,8 @@ bool CustomMQTTDevice::publish(const std::string &topic, const std::string &payl
return global_mqtt_client->publish(topic, payload, qos, retain);
}
bool CustomMQTTDevice::publish(const std::string &topic, float value, int8_t number_decimals) {
char buf[VALUE_ACCURACY_MAX_LEN];
value_accuracy_to_buf(buf, value, number_decimals);
return this->publish(topic, buf);
auto str = value_accuracy_to_string(value, number_decimals);
return this->publish(topic, str);
}
bool CustomMQTTDevice::publish(const std::string &topic, int value) {
char buffer[24];

View File

@@ -79,7 +79,7 @@ void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendD
root[MQTT_CODE_ARM_REQUIRED] = this->alarm_control_panel_->get_requires_code_to_arm();
}
MQTT_COMPONENT_TYPE(MQTTAlarmControlPanelComponent, "alarm_control_panel")
std::string MQTTAlarmControlPanelComponent::component_type() const { return "alarm_control_panel"; }
const EntityBase *MQTTAlarmControlPanelComponent::get_entity() const { return this->alarm_control_panel_; }
bool MQTTAlarmControlPanelComponent::send_initial_state() { return this->publish_state(); }

View File

@@ -25,7 +25,7 @@ class MQTTAlarmControlPanelComponent : public mqtt::MQTTComponent {
void dump_config() override;
protected:
const char *component_type() const override;
std::string component_type() const override;
const EntityBase *get_entity() const override;
alarm_control_panel::AlarmControlPanel *alarm_control_panel_;

View File

@@ -10,7 +10,7 @@ namespace esphome::mqtt {
static const char *const TAG = "mqtt.binary_sensor";
MQTT_COMPONENT_TYPE(MQTTBinarySensorComponent, "binary_sensor")
std::string MQTTBinarySensorComponent::component_type() const { return "binary_sensor"; }
const EntityBase *MQTTBinarySensorComponent::get_entity() const { return this->binary_sensor_; }
void MQTTBinarySensorComponent::setup() {

View File

@@ -29,7 +29,7 @@ class MQTTBinarySensorComponent : public mqtt::MQTTComponent {
bool publish_state(bool state);
protected:
const char *component_type() const override;
std::string component_type() const override;
const EntityBase *get_entity() const override;
binary_sensor::BinarySensor *binary_sensor_;

View File

@@ -39,7 +39,7 @@ void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon
// NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
}
MQTT_COMPONENT_TYPE(MQTTButtonComponent, "button")
std::string MQTTButtonComponent::component_type() const { return "button"; }
const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; }
} // namespace esphome::mqtt

View File

@@ -26,7 +26,7 @@ class MQTTButtonComponent : public mqtt::MQTTComponent {
protected:
/// "button" component type.
const char *component_type() const override;
std::string component_type() const override;
const EntityBase *get_entity() const override;
button::Button *button_;

View File

@@ -28,9 +28,8 @@ static const char *const TAG = "mqtt";
MQTTClientComponent::MQTTClientComponent() {
global_mqtt_client = this;
char mac_addr[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac_addr);
this->credentials_.client_id = make_name_with_suffix(App.get_name(), '-', mac_addr, MAC_ADDRESS_BUFFER_SIZE - 1);
const std::string mac_addr = get_mac_address();
this->credentials_.client_id = make_name_with_suffix(App.get_name(), '-', mac_addr.c_str(), mac_addr.size());
}
// Connection
@@ -103,9 +102,7 @@ void MQTTClientComponent::send_device_info_() {
root[ESPHOME_F("port")] = api::global_api_server->get_port();
#endif
root[ESPHOME_F("version")] = ESPHOME_VERSION;
char mac_buf[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac_buf);
root[ESPHOME_F("mac")] = mac_buf;
root[ESPHOME_F("mac")] = get_mac_address();
#ifdef USE_ESP8266
root[ESPHOME_F("platform")] = ESPHOME_F("ESP8266");

View File

@@ -378,17 +378,17 @@ class MQTTJsonMessageTrigger : public Trigger<JsonObjectConst> {
}
};
class MQTTConnectTrigger : public Trigger<bool> {
class MQTTConnectTrigger : public Trigger<> {
public:
explicit MQTTConnectTrigger(MQTTClientComponent *&client) {
client->set_on_connect([this](bool session_present) { this->trigger(session_present); });
client->set_on_connect([this](bool session_present) { this->trigger(); });
}
};
class MQTTDisconnectTrigger : public Trigger<MQTTClientDisconnectReason> {
class MQTTDisconnectTrigger : public Trigger<> {
public:
explicit MQTTDisconnectTrigger(MQTTClientComponent *&client) {
client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(reason); });
client->set_on_disconnect([this](MQTTClientDisconnectReason reason) { this->trigger(); });
}
};

View File

@@ -254,7 +254,7 @@ void MQTTClimateComponent::setup() {
}
MQTTClimateComponent::MQTTClimateComponent(Climate *device) : device_(device) {}
bool MQTTClimateComponent::send_initial_state() { return this->publish_state_(); }
MQTT_COMPONENT_TYPE(MQTTClimateComponent, "climate")
std::string MQTTClimateComponent::component_type() const { return "climate"; }
const EntityBase *MQTTClimateComponent::get_entity() const { return this->device_; }
bool MQTTClimateComponent::publish_state_() {
@@ -291,36 +291,35 @@ bool MQTTClimateComponent::publish_state_() {
success = false;
int8_t target_accuracy = traits.get_target_temperature_accuracy_decimals();
int8_t current_accuracy = traits.get_current_temperature_accuracy_decimals();
char payload[VALUE_ACCURACY_MAX_LEN];
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE) &&
!std::isnan(this->device_->current_temperature)) {
value_accuracy_to_buf(payload, this->device_->current_temperature, current_accuracy);
std::string payload = value_accuracy_to_string(this->device_->current_temperature, current_accuracy);
if (!this->publish(this->get_current_temperature_state_topic(), payload))
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
value_accuracy_to_buf(payload, this->device_->target_temperature_low, target_accuracy);
std::string payload = value_accuracy_to_string(this->device_->target_temperature_low, target_accuracy);
if (!this->publish(this->get_target_temperature_low_state_topic(), payload))
success = false;
value_accuracy_to_buf(payload, this->device_->target_temperature_high, target_accuracy);
payload = value_accuracy_to_string(this->device_->target_temperature_high, target_accuracy);
if (!this->publish(this->get_target_temperature_high_state_topic(), payload))
success = false;
} else {
value_accuracy_to_buf(payload, this->device_->target_temperature, target_accuracy);
std::string payload = value_accuracy_to_string(this->device_->target_temperature, target_accuracy);
if (!this->publish(this->get_target_temperature_state_topic(), payload))
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY) &&
!std::isnan(this->device_->current_humidity)) {
value_accuracy_to_buf(payload, this->device_->current_humidity, 0);
std::string payload = value_accuracy_to_string(this->device_->current_humidity, 0);
if (!this->publish(this->get_current_humidity_state_topic(), payload))
success = false;
}
if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY) &&
!std::isnan(this->device_->target_humidity)) {
value_accuracy_to_buf(payload, this->device_->target_humidity, 0);
std::string payload = value_accuracy_to_string(this->device_->target_humidity, 0);
if (!this->publish(this->get_target_humidity_state_topic(), payload))
success = false;
}
@@ -358,7 +357,7 @@ bool MQTTClimateComponent::publish_state_() {
}
}
if (this->device_->has_custom_preset())
payload = this->device_->get_custom_preset().c_str();
payload = this->device_->get_custom_preset();
if (!this->publish(this->get_preset_state_topic(), payload))
success = false;
}
@@ -430,7 +429,7 @@ bool MQTTClimateComponent::publish_state_() {
}
}
if (this->device_->has_custom_fan_mode())
payload = this->device_->get_custom_fan_mode().c_str();
payload = this->device_->get_custom_fan_mode();
if (!this->publish(this->get_fan_mode_state_topic(), payload))
success = false;
}

View File

@@ -15,7 +15,7 @@ class MQTTClimateComponent : public mqtt::MQTTComponent {
MQTTClimateComponent(climate::Climate *device);
void send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) override;
bool send_initial_state() override;
const char *component_type() const override;
std::string component_type() const override;
void setup() override;
MQTT_COMPONENT_CUSTOM_TOPIC(current_temperature, state)

View File

@@ -13,34 +13,6 @@ namespace esphome::mqtt {
static const char *const TAG = "mqtt.component";
// Helper functions for building topic strings on stack
inline char *append_str(char *p, const char *s, size_t len) {
memcpy(p, s, len);
return p + len;
}
inline char *append_char(char *p, char c) {
*p = c;
return p + 1;
}
// Max lengths for stack-based topic building.
// These limits are enforced at Python config validation time in mqtt/__init__.py
// using cv.Length() validators for topic_prefix and discovery_prefix.
// MQTT_COMPONENT_TYPE_MAX_LEN and MQTT_SUFFIX_MAX_LEN are defined in mqtt_component.h.
// ESPHOME_DEVICE_NAME_MAX_LEN and OBJECT_ID_MAX_LEN are defined in entity_base.h.
// This ensures the stack buffers below are always large enough.
static constexpr size_t TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
static constexpr size_t DISCOVERY_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
// Stack buffer sizes - safe because all inputs are length-validated at config time
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
static constexpr size_t DEFAULT_TOPIC_MAX_LEN =
TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
// Format: prefix + "/" + type + "/" + name + "/" + object_id + "/config" + null
static constexpr size_t DISCOVERY_TOPIC_MAX_LEN = DISCOVERY_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 +
ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 7 + 1;
void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
@@ -49,23 +21,8 @@ void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; }
std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const {
std::string sanitized_name = str_sanitize(App.get_name());
const char *comp_type = this->component_type();
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
char buf[DISCOVERY_TOPIC_MAX_LEN];
char *p = buf;
p = append_str(p, discovery_info.prefix.data(), discovery_info.prefix.size());
p = append_char(p, '/');
p = append_str(p, comp_type, strlen(comp_type));
p = append_char(p, '/');
p = append_str(p, sanitized_name.data(), sanitized_name.size());
p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size());
p = append_str(p, "/config", 7);
return std::string(buf, p - buf);
return discovery_info.prefix + "/" + this->component_type() + "/" + sanitized_name + "/" +
this->get_default_object_id_() + "/config";
}
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
@@ -75,22 +32,7 @@ std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) con
return "";
}
const char *comp_type = this->component_type();
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
char buf[DEFAULT_TOPIC_MAX_LEN];
char *p = buf;
p = append_str(p, topic_prefix.data(), topic_prefix.size());
p = append_char(p, '/');
p = append_str(p, comp_type, strlen(comp_type));
p = append_char(p, '/');
p = append_str(p, object_id.c_str(), object_id.size());
p = append_char(p, '/');
p = append_str(p, suffix.data(), suffix.size());
return std::string(buf, p - buf);
return topic_prefix + "/" + this->component_type() + "/" + this->get_default_object_id_() + "/" + suffix;
}
std::string MQTTComponent::get_state_topic_() const {
@@ -181,36 +123,27 @@ bool MQTTComponent::send_discovery_() {
}
const MQTTDiscoveryInfo &discovery_info = global_mqtt_client->get_discovery_info();
char object_id_buf[OBJECT_ID_MAX_LEN];
StringRef object_id = this->get_default_object_id_to_(object_id_buf);
if (discovery_info.unique_id_generator == MQTT_MAC_ADDRESS_UNIQUE_ID_GENERATOR) {
char friendly_name_hash[9];
sprintf(friendly_name_hash, "%08" PRIx32, fnv1_hash(this->friendly_name_()));
friendly_name_hash[8] = 0; // ensure the hash-string ends with null
// Format: mac-component_type-hash (e.g. "aabbccddeeff-sensor-12345678")
// MAC (12) + "-" (1) + domain (max 20) + "-" (1) + hash (8) + null (1) = 43
char unique_id[MAC_ADDRESS_BUFFER_SIZE + ESPHOME_DOMAIN_MAX_LEN + 11];
char mac_buf[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac_buf);
snprintf(unique_id, sizeof(unique_id), "%s-%s-%s", mac_buf, this->component_type(), friendly_name_hash);
root[MQTT_UNIQUE_ID] = unique_id;
root[MQTT_UNIQUE_ID] = get_mac_address() + "-" + this->component_type() + "-" + friendly_name_hash;
} else {
// default to almost-unique ID. It's a hack but the only way to get that
// gorgeous device registry view.
root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str();
root[MQTT_UNIQUE_ID] = "ESP" + this->component_type() + this->get_default_object_id_();
}
const std::string &node_name = App.get_name();
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str();
root[MQTT_OBJECT_ID] = node_name + "_" + this->get_default_object_id_();
const std::string &friendly_name_ref = App.get_friendly_name();
const std::string &node_friendly_name = friendly_name_ref.empty() ? node_name : friendly_name_ref;
std::string node_area = App.get_area();
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
char mac[MAC_ADDRESS_BUFFER_SIZE];
get_mac_address_into_buffer(mac);
const auto mac = get_mac_address();
device_info[MQTT_DEVICE_IDENTIFIERS] = mac;
device_info[MQTT_DEVICE_NAME] = node_friendly_name;
#ifdef ESPHOME_PROJECT_NAME
@@ -261,6 +194,10 @@ bool MQTTComponent::is_discovery_enabled() const {
return this->discovery_enabled_ && global_mqtt_client->is_discovery_enabled();
}
std::string MQTTComponent::get_default_object_id_() const {
return str_sanitize(str_snake_case(this->friendly_name_()));
}
void MQTTComponent::subscribe(const std::string &topic, mqtt_callback_t callback, uint8_t qos) {
global_mqtt_client->subscribe(topic, std::move(callback), qos);
}
@@ -343,9 +280,6 @@ bool MQTTComponent::is_connected_() const { return global_mqtt_client->is_connec
// Pull these properties from EntityBase if not overridden
std::string MQTTComponent::friendly_name_() const { return this->get_entity()->get_name(); }
StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const {
return this->get_entity()->get_object_id_to(buf);
}
StringRef MQTTComponent::get_icon_ref_() const { return this->get_entity()->get_icon_ref(); }
bool MQTTComponent::is_disabled_by_default_() const { return this->get_entity()->is_disabled_by_default(); }
bool MQTTComponent::is_internal() {

View File

@@ -19,10 +19,6 @@ struct SendDiscoveryConfig {
bool command_topic{true}; ///< If the command topic should be included. Default to true.
};
// Max lengths for stack-based topic building (must match mqtt_component.cpp)
static constexpr size_t MQTT_COMPONENT_TYPE_MAX_LEN = 20;
static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
#define LOG_MQTT_COMPONENT(state_topic, command_topic) \
if (state_topic) { \
ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \
@@ -31,18 +27,7 @@ static constexpr size_t MQTT_SUFFIX_MAX_LEN = 32;
ESP_LOGCONFIG(TAG, " Command Topic: '%s'", this->get_command_topic_().c_str()); \
}
// Macro to define component_type() with compile-time length verification
// Usage: MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
#define MQTT_COMPONENT_TYPE(class_name, type_str) \
const char *class_name::component_type() const { return type_str; } \
static_assert(sizeof(type_str) - 1 <= MQTT_COMPONENT_TYPE_MAX_LEN, \
#class_name "::component_type() exceeds MQTT_COMPONENT_TYPE_MAX_LEN");
// Macro to define custom topic getter/setter with compile-time suffix length verification
#define MQTT_COMPONENT_CUSTOM_TOPIC_(name, type) \
static_assert(sizeof(#name "/" #type) - 1 <= MQTT_SUFFIX_MAX_LEN, \
"topic suffix " #name "/" #type " exceeds MQTT_SUFFIX_MAX_LEN"); \
\
protected: \
std::string custom_##name##_##type##_topic_{}; \
\
@@ -107,7 +92,7 @@ class MQTTComponent : public Component {
void set_subscribe_qos(uint8_t qos);
/// Override this method to return the component type (e.g. "light", "sensor", ...)
virtual const char *component_type() const = 0;
virtual std::string component_type() const = 0;
/// Set a custom state topic. Set to "" for default behavior.
void set_custom_state_topic(const char *custom_state_topic);
@@ -200,8 +185,8 @@ class MQTTComponent : public Component {
// ========== INTERNAL METHODS ==========
// (In most use cases you won't need these)
/// Get the object ID for this MQTT component, writing to the provided buffer.
StringRef get_default_object_id_to_(std::span<char, OBJECT_ID_MAX_LEN> buf) const;
/// Generate the Home Assistant MQTT discovery object id by automatically transforming the friendly name.
std::string get_default_object_id_() const;
StringRef custom_state_topic_{};
StringRef custom_command_topic_{};

View File

@@ -90,7 +90,7 @@ void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConf
}
}
MQTT_COMPONENT_TYPE(MQTTCoverComponent, "cover")
std::string MQTTCoverComponent::component_type() const { return "cover"; }
const EntityBase *MQTTCoverComponent::get_entity() const { return this->cover_; }
bool MQTTCoverComponent::send_initial_state() { return this->publish_state(); }
@@ -98,14 +98,12 @@ bool MQTTCoverComponent::publish_state() {
auto traits = this->cover_->get_traits();
bool success = true;
if (traits.get_supports_position()) {
char pos[VALUE_ACCURACY_MAX_LEN];
value_accuracy_to_buf(pos, roundf(this->cover_->position * 100), 0);
std::string pos = value_accuracy_to_string(roundf(this->cover_->position * 100), 0);
if (!this->publish(this->get_position_state_topic(), pos))
success = false;
}
if (traits.get_supports_tilt()) {
char pos[VALUE_ACCURACY_MAX_LEN];
value_accuracy_to_buf(pos, roundf(this->cover_->tilt * 100), 0);
std::string pos = value_accuracy_to_string(roundf(this->cover_->tilt * 100), 0);
if (!this->publish(this->get_tilt_state_topic(), pos))
success = false;
}

View File

@@ -29,7 +29,7 @@ class MQTTCoverComponent : public mqtt::MQTTComponent {
void dump_config() override;
protected:
const char *component_type() const override;
std::string component_type() const override;
const EntityBase *get_entity() const override;
cover::Cover *cover_;

View File

@@ -39,7 +39,7 @@ void MQTTDateComponent::dump_config() {
LOG_MQTT_COMPONENT(true, true)
}
MQTT_COMPONENT_TYPE(MQTTDateComponent, "date")
std::string MQTTDateComponent::component_type() const { return "date"; }
const EntityBase *MQTTDateComponent::get_entity() const { return this->date_; }
void MQTTDateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) {

View File

@@ -31,7 +31,7 @@ class MQTTDateComponent : public mqtt::MQTTComponent {
bool publish_state(uint16_t year, uint8_t month, uint8_t day);
protected:
const char *component_type() const override;
std::string component_type() const override;
const EntityBase *get_entity() const override;
datetime::DateEntity *date_;

Some files were not shown because too many files have changed in this diff Show More