mirror of
https://github.com/esphome/esphome.git
synced 2026-01-21 02:19:10 -07:00
Compare commits
1 Commits
integratio
...
idf_no_hea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ec6c37c01 |
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from collections.abc import Callable
|
||||
import json
|
||||
import sys
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@@ -439,28 +438,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Export analysis results as JSON."""
|
||||
data = {
|
||||
"components": {
|
||||
name: {
|
||||
"text": mem.text_size,
|
||||
"rodata": mem.rodata_size,
|
||||
"data": mem.data_size,
|
||||
"bss": mem.bss_size,
|
||||
"flash_total": mem.flash_total,
|
||||
"ram_total": mem.ram_total,
|
||||
"symbol_count": mem.symbol_count,
|
||||
}
|
||||
for name, mem in self.components.items()
|
||||
},
|
||||
"totals": {
|
||||
"flash": sum(c.flash_total for c in self.components.values()),
|
||||
"ram": sum(c.ram_total for c in self.components.values()),
|
||||
},
|
||||
}
|
||||
return json.dumps(data, indent=2)
|
||||
|
||||
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
|
||||
"""Dump uncategorized symbols for analysis."""
|
||||
# Sort by size descending
|
||||
|
||||
@@ -67,29 +67,52 @@ void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback)
|
||||
this->ready_callback_.add(std::move(callback));
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(),
|
||||
const char *code) {
|
||||
void AlarmControlPanel::arm_away(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
(call.*arm_method)();
|
||||
if (code != nullptr)
|
||||
call.set_code(code);
|
||||
call.arm_away();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_away(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_away, code); }
|
||||
|
||||
void AlarmControlPanel::arm_home(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_home, code); }
|
||||
|
||||
void AlarmControlPanel::arm_night(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::arm_night, code); }
|
||||
|
||||
void AlarmControlPanel::arm_vacation(const char *code) {
|
||||
this->arm_with_code_(&AlarmControlPanelCall::arm_vacation, code);
|
||||
void AlarmControlPanel::arm_home(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.arm_home();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_custom_bypass(const char *code) {
|
||||
this->arm_with_code_(&AlarmControlPanelCall::arm_custom_bypass, code);
|
||||
void AlarmControlPanel::arm_night(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.arm_night();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
}
|
||||
|
||||
void AlarmControlPanel::disarm(const char *code) { this->arm_with_code_(&AlarmControlPanelCall::disarm, code); }
|
||||
void AlarmControlPanel::arm_vacation(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.arm_vacation();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
}
|
||||
|
||||
void AlarmControlPanel::arm_custom_bypass(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.arm_custom_bypass();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
}
|
||||
|
||||
void AlarmControlPanel::disarm(optional<std::string> code) {
|
||||
auto call = this->make_call();
|
||||
call.disarm();
|
||||
if (code.has_value())
|
||||
call.set_code(code.value());
|
||||
call.perform();
|
||||
}
|
||||
|
||||
} // namespace esphome::alarm_control_panel
|
||||
|
||||
@@ -76,53 +76,37 @@ class AlarmControlPanel : public EntityBase {
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void arm_away(const char *code = nullptr);
|
||||
void arm_away(const optional<std::string> &code) {
|
||||
this->arm_away(code.has_value() ? code.value().c_str() : nullptr);
|
||||
}
|
||||
void arm_away(optional<std::string> code = nullopt);
|
||||
|
||||
/** arm the alarm in home mode
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void arm_home(const char *code = nullptr);
|
||||
void arm_home(const optional<std::string> &code) {
|
||||
this->arm_home(code.has_value() ? code.value().c_str() : nullptr);
|
||||
}
|
||||
void arm_home(optional<std::string> code = nullopt);
|
||||
|
||||
/** arm the alarm in night mode
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void arm_night(const char *code = nullptr);
|
||||
void arm_night(const optional<std::string> &code) {
|
||||
this->arm_night(code.has_value() ? code.value().c_str() : nullptr);
|
||||
}
|
||||
void arm_night(optional<std::string> code = nullopt);
|
||||
|
||||
/** arm the alarm in vacation mode
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void arm_vacation(const char *code = nullptr);
|
||||
void arm_vacation(const optional<std::string> &code) {
|
||||
this->arm_vacation(code.has_value() ? code.value().c_str() : nullptr);
|
||||
}
|
||||
void arm_vacation(optional<std::string> code = nullopt);
|
||||
|
||||
/** arm the alarm in custom bypass mode
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void arm_custom_bypass(const char *code = nullptr);
|
||||
void arm_custom_bypass(const optional<std::string> &code) {
|
||||
this->arm_custom_bypass(code.has_value() ? code.value().c_str() : nullptr);
|
||||
}
|
||||
void arm_custom_bypass(optional<std::string> code = nullopt);
|
||||
|
||||
/** disarm the alarm
|
||||
*
|
||||
* @param code The code
|
||||
*/
|
||||
void disarm(const char *code = nullptr);
|
||||
void disarm(const optional<std::string> &code) { this->disarm(code.has_value() ? code.value().c_str() : nullptr); }
|
||||
void disarm(optional<std::string> code = nullopt);
|
||||
|
||||
/** Get the state
|
||||
*
|
||||
@@ -134,8 +118,6 @@ class AlarmControlPanel : public EntityBase {
|
||||
|
||||
protected:
|
||||
friend AlarmControlPanelCall;
|
||||
// Helper to reduce code duplication for arm/disarm methods
|
||||
void arm_with_code_(AlarmControlPanelCall &(AlarmControlPanelCall::*arm_method)(), const char *code);
|
||||
// in order to store last panel state in flash
|
||||
ESPPreferenceObject pref_;
|
||||
// current state
|
||||
|
||||
@@ -10,10 +10,8 @@ static const char *const TAG = "alarm_control_panel";
|
||||
|
||||
AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {}
|
||||
|
||||
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const char *code) {
|
||||
if (code != nullptr) {
|
||||
this->code_ = std::string(code);
|
||||
}
|
||||
AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) {
|
||||
this->code_ = code;
|
||||
return *this;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ class AlarmControlPanelCall {
|
||||
public:
|
||||
AlarmControlPanelCall(AlarmControlPanel *parent);
|
||||
|
||||
AlarmControlPanelCall &set_code(const char *code);
|
||||
AlarmControlPanelCall &set_code(const std::string &code) { return this->set_code(code.c_str()); }
|
||||
AlarmControlPanelCall &set_code(const std::string &code);
|
||||
AlarmControlPanelCall &arm_away();
|
||||
AlarmControlPanelCall &arm_home();
|
||||
AlarmControlPanelCall &arm_night();
|
||||
|
||||
@@ -66,7 +66,15 @@ template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, code)
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->arm_away(this->code_.optional_value(x...)); }
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->alarm_control_panel_->make_call();
|
||||
auto code = this->code_.optional_value(x...);
|
||||
if (code.has_value()) {
|
||||
call.set_code(code.value());
|
||||
}
|
||||
call.arm_away();
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
@@ -78,7 +86,15 @@ template<typename... Ts> class ArmHomeAction : public Action<Ts...> {
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, code)
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->arm_home(this->code_.optional_value(x...)); }
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->alarm_control_panel_->make_call();
|
||||
auto code = this->code_.optional_value(x...);
|
||||
if (code.has_value()) {
|
||||
call.set_code(code.value());
|
||||
}
|
||||
call.arm_home();
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
@@ -90,7 +106,15 @@ template<typename... Ts> class ArmNightAction : public Action<Ts...> {
|
||||
|
||||
TEMPLATABLE_VALUE(std::string, code)
|
||||
|
||||
void play(const Ts &...x) override { this->alarm_control_panel_->arm_night(this->code_.optional_value(x...)); }
|
||||
void play(const Ts &...x) override {
|
||||
auto call = this->alarm_control_panel_->make_call();
|
||||
auto code = this->code_.optional_value(x...);
|
||||
if (code.has_value()) {
|
||||
call.set_code(code.value());
|
||||
}
|
||||
call.arm_night();
|
||||
call.perform();
|
||||
}
|
||||
|
||||
protected:
|
||||
AlarmControlPanel *alarm_control_panel_;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "bedjet_hub.h"
|
||||
#include "bedjet_child.h"
|
||||
#include "bedjet_const.h"
|
||||
#include "esphome/components/esp32_ble/ble_uuid.h"
|
||||
#include "esphome/core/application.h"
|
||||
#include <cinttypes>
|
||||
|
||||
|
||||
@@ -63,13 +63,11 @@ def validate_auto_clear(value):
|
||||
return cv.boolean(value)
|
||||
|
||||
|
||||
def basic_display_schema(default_update_interval: str = "1s") -> cv.Schema:
|
||||
"""Create a basic display schema with configurable default update interval."""
|
||||
return cv.Schema(
|
||||
{
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
).extend(cv.polling_component_schema(default_update_interval))
|
||||
BASIC_DISPLAY_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Exclusive(CONF_LAMBDA, CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
).extend(cv.polling_component_schema("1s"))
|
||||
|
||||
|
||||
def _validate_test_card(config):
|
||||
@@ -83,41 +81,34 @@ def _validate_test_card(config):
|
||||
return config
|
||||
|
||||
|
||||
def full_display_schema(default_update_interval: str = "1s") -> cv.Schema:
|
||||
"""Create a full display schema with configurable default update interval."""
|
||||
schema = basic_display_schema(default_update_interval).extend(
|
||||
{
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.GenerateID(): cv.declare_id(DisplayPage),
|
||||
cv.Required(CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
),
|
||||
cv.Length(min=1),
|
||||
),
|
||||
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
|
||||
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
|
||||
{
|
||||
cv.Optional(CONF_ROTATION): validate_rotation,
|
||||
cv.Exclusive(CONF_PAGES, CONF_LAMBDA): cv.All(
|
||||
cv.ensure_list(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayOnPageChangeTrigger
|
||||
),
|
||||
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
|
||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||
cv.GenerateID(): cv.declare_id(DisplayPage),
|
||||
cv.Required(CONF_LAMBDA): cv.lambda_,
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
||||
): validate_auto_clear,
|
||||
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
||||
}
|
||||
)
|
||||
schema.add_extra(_validate_test_card)
|
||||
return schema
|
||||
|
||||
|
||||
BASIC_DISPLAY_SCHEMA = basic_display_schema("1s")
|
||||
FULL_DISPLAY_SCHEMA = full_display_schema("1s")
|
||||
cv.Length(min=1),
|
||||
),
|
||||
cv.Optional(CONF_ON_PAGE_CHANGE): automation.validate_automation(
|
||||
{
|
||||
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
|
||||
DisplayOnPageChangeTrigger
|
||||
),
|
||||
cv.Optional(CONF_FROM): cv.use_id(DisplayPage),
|
||||
cv.Optional(CONF_TO): cv.use_id(DisplayPage),
|
||||
}
|
||||
),
|
||||
cv.Optional(
|
||||
CONF_AUTO_CLEAR_ENABLED, default=CONF_UNSPECIFIED
|
||||
): validate_auto_clear,
|
||||
cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
|
||||
}
|
||||
)
|
||||
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
|
||||
|
||||
|
||||
async def setup_display_core_(var, config):
|
||||
|
||||
@@ -31,7 +31,6 @@ from esphome.const import (
|
||||
CONF_TRANSFORM,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_WIDTH,
|
||||
SCHEDULER_DONT_RUN,
|
||||
)
|
||||
from esphome.cpp_generator import RawExpression
|
||||
from esphome.final_validate import full_config
|
||||
@@ -73,10 +72,12 @@ TRANSFORM_OPTIONS = {CONF_MIRROR_X, CONF_MIRROR_Y, CONF_SWAP_XY}
|
||||
def model_schema(config):
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
class_name = epaper_spi_ns.class_(model.class_name, EPaperBase)
|
||||
minimum_update_interval = update_interval(
|
||||
model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s")
|
||||
)
|
||||
cv_dimensions = cv.Optional if model.get_default(CONF_WIDTH) else cv.Required
|
||||
return (
|
||||
display.full_display_schema("60s")
|
||||
.extend(
|
||||
display.FULL_DISPLAY_SCHEMA.extend(
|
||||
spi.spi_device_schema(
|
||||
cs_pin_required=False,
|
||||
default_mode="MODE0",
|
||||
@@ -93,6 +94,9 @@ def model_schema(config):
|
||||
{
|
||||
cv.Optional(CONF_ROTATION, default=0): validate_rotation,
|
||||
cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True),
|
||||
cv.Optional(CONF_UPDATE_INTERVAL, default=cv.UNDEFINED): cv.All(
|
||||
update_interval, cv.Range(min=minimum_update_interval)
|
||||
),
|
||||
cv.Optional(CONF_TRANSFORM): cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_MIRROR_X): cv.boolean,
|
||||
@@ -146,22 +150,15 @@ def _final_validate(config):
|
||||
global_config = full_config.get()
|
||||
from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN
|
||||
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
if (
|
||||
CONF_LAMBDA not in config
|
||||
and CONF_PAGES not in config
|
||||
and LVGL_DOMAIN not in global_config
|
||||
):
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
|
||||
interval = config[CONF_UPDATE_INTERVAL]
|
||||
if interval != SCHEDULER_DONT_RUN:
|
||||
model = MODELS[config[CONF_MODEL]]
|
||||
minimum = update_interval(model.get_default(CONF_MINIMUM_UPDATE_INTERVAL, "1s"))
|
||||
if interval < minimum:
|
||||
raise cv.Invalid(
|
||||
f"update_interval must be at least {minimum} for {model.name}, got {interval}"
|
||||
)
|
||||
if CONF_LAMBDA not in config and CONF_PAGES not in config:
|
||||
if LVGL_DOMAIN in global_config:
|
||||
if CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("never")
|
||||
else:
|
||||
# If no drawing methods are configured, and LVGL is not enabled, show a test card
|
||||
config[CONF_SHOW_TEST_CARD] = True
|
||||
elif CONF_UPDATE_INTERVAL not in config:
|
||||
config[CONF_UPDATE_INTERVAL] = update_interval("1min")
|
||||
return config
|
||||
|
||||
|
||||
|
||||
@@ -663,9 +663,6 @@ CONF_LOOP_TASK_STACK_SIZE = "loop_task_stack_size"
|
||||
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
|
||||
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
|
||||
|
||||
# Ring buffer IRAM requirement tracking
|
||||
KEY_RINGBUF_IN_IRAM = "ringbuf_in_iram"
|
||||
|
||||
|
||||
def require_vfs_select() -> None:
|
||||
"""Mark that VFS select support is required by a component.
|
||||
@@ -685,17 +682,6 @@ def require_vfs_dir() -> None:
|
||||
CORE.data[KEY_VFS_DIR_REQUIRED] = True
|
||||
|
||||
|
||||
def enable_ringbuf_in_iram() -> None:
|
||||
"""Keep ring buffer functions in IRAM instead of moving them to flash.
|
||||
|
||||
Call this from components that use esphome/core/ring_buffer.cpp and need
|
||||
the ring buffer functions to remain in IRAM for performance reasons
|
||||
(e.g., voice assistants, audio components).
|
||||
This prevents CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH from being enabled.
|
||||
"""
|
||||
CORE.data[KEY_RINGBUF_IN_IRAM] = True
|
||||
|
||||
|
||||
def _parse_idf_component(value: str) -> ConfigType:
|
||||
"""Parse IDF component shorthand syntax like 'owner/component^version'"""
|
||||
# Match operator followed by version-like string (digit or *)
|
||||
@@ -1119,18 +1105,14 @@ async def to_code(config):
|
||||
add_idf_sdkconfig_option("CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH", True)
|
||||
|
||||
# Place ring buffer functions into flash instead of IRAM by default
|
||||
# This saves IRAM but may impact performance for audio/voice components.
|
||||
# Components that need ring buffer in IRAM call enable_ringbuf_in_iram().
|
||||
# Users can also set ringbuf_in_iram: true to force IRAM placement.
|
||||
# In ESP-IDF 6.0 flash placement becomes the default.
|
||||
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM] or CORE.data.get(
|
||||
KEY_RINGBUF_IN_IRAM, False
|
||||
):
|
||||
# User config or component requires ring buffer in IRAM for performance
|
||||
# This saves IRAM. In ESP-IDF 6.0 flash placement becomes the default.
|
||||
# Users can set ringbuf_in_iram: true as an escape hatch if they encounter issues.
|
||||
if conf[CONF_ADVANCED][CONF_RINGBUF_IN_IRAM]:
|
||||
# User requests ring buffer in IRAM
|
||||
# IDF 6.0+: will need CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH=n
|
||||
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_ISR_FUNCTIONS_INTO_FLASH", False)
|
||||
else:
|
||||
# No component needs it - place in flash to save IRAM
|
||||
# Place in flash to save IRAM (default)
|
||||
add_idf_sdkconfig_option("CONFIG_RINGBUF_PLACE_FUNCTIONS_INTO_FLASH", True)
|
||||
|
||||
# Place heap functions into flash to save IRAM (~4-6KB savings)
|
||||
|
||||
@@ -85,6 +85,7 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
||||
break;
|
||||
}
|
||||
gpio_set_intr_type(this->get_pin_num(), idf_type);
|
||||
gpio_intr_enable(this->get_pin_num());
|
||||
if (!isr_service_installed) {
|
||||
auto res = gpio_install_isr_service(ESP_INTR_FLAG_LEVEL3);
|
||||
if (res != ESP_OK) {
|
||||
@@ -94,7 +95,6 @@ void ESP32InternalGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpi
|
||||
isr_service_installed = true;
|
||||
}
|
||||
gpio_isr_handler_add(this->get_pin_num(), func, arg);
|
||||
gpio_intr_enable(this->get_pin_num());
|
||||
}
|
||||
|
||||
size_t ESP32InternalGPIOPin::dump_summary(char *buffer, size_t len) const {
|
||||
|
||||
@@ -19,7 +19,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
size_t len;
|
||||
|
||||
void set_data(const uint8_t *src, size_t size) {
|
||||
if (!this->data || this->len != size) {
|
||||
this->data = std::make_unique<uint8_t[]>(size);
|
||||
this->len = size;
|
||||
}
|
||||
memcpy(this->data.get(), src, size);
|
||||
}
|
||||
};
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -32,14 +41,14 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
obj.set_data(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -47,11 +56,11 @@ class ESP32PreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
if (obj.len != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
memcpy(data, obj.data.get(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -127,10 +136,10 @@ class ESP32Preferences : public ESPPreferences {
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if NVS data %s has changed", key_str);
|
||||
if (this->is_changed_(this->nvs_handle, save, key_str)) {
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
esp_err_t err = nvs_set_blob(this->nvs_handle, key_str, save.data.get(), save.len);
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.data.size(), esp_err_to_name(err));
|
||||
ESP_LOGV(TAG, "nvs_set_blob('%s', len=%zu) failed: %s", key_str, save.len, esp_err_to_name(err));
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -138,7 +147,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
ESP_LOGV(TAG, "NVS data not changed skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
@@ -169,7 +178,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
return true;
|
||||
}
|
||||
// Check size first before allocating memory
|
||||
if (actual_len != to_save.data.size()) {
|
||||
if (actual_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
@@ -179,7 +188,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
return true;
|
||||
}
|
||||
return memcmp(to_save.data.data(), stored_data.get(), to_save.data.size()) != 0;
|
||||
return memcmp(to_save.data.get(), stored_data.get(), to_save.len) != 0;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
||||
@@ -98,10 +98,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
this->advertising_init_();
|
||||
this->advertising_->set_manufacturer_data(data);
|
||||
this->advertising_start();
|
||||
|
||||
@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
|
||||
void advertising_start();
|
||||
void advertising_set_service_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
|
||||
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
|
||||
void advertising_add_service_uuid(ESPBTUUID uuid);
|
||||
|
||||
@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
|
||||
this->set_manufacturer_data(std::span<const uint8_t>(data));
|
||||
}
|
||||
|
||||
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
|
||||
delete[] this->advertising_data_.p_manufacturer_data;
|
||||
this->advertising_data_.p_manufacturer_data = nullptr;
|
||||
this->advertising_data_.manufacturer_len = data.size();
|
||||
|
||||
@@ -37,7 +37,6 @@ class BLEAdvertising {
|
||||
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
|
||||
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
|
||||
void set_manufacturer_data(const std::vector<uint8_t> &data);
|
||||
void set_manufacturer_data(std::span<const uint8_t> data);
|
||||
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
|
||||
void set_service_data(const std::vector<uint8_t> &data);
|
||||
void set_service_data(std::span<const uint8_t> data);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "esp32_ble_beacon.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
|
||||
#ifdef USE_ESP32
|
||||
|
||||
|
||||
@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
|
||||
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
||||
// Convert span to vector for trigger - copy is necessary because:
|
||||
// 1. Trigger stores the data for use in automation actions that execute later
|
||||
// 2. The span is only valid during this callback (points to temporary BLE stack data)
|
||||
// 3. User lambdas in automations need persistent data they can access asynchronously
|
||||
// Convert span to vector for trigger
|
||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||
});
|
||||
return on_write_trigger;
|
||||
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
|
||||
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
|
||||
new Trigger<std::vector<uint8_t>, uint16_t>();
|
||||
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
|
||||
// Convert span to vector for trigger - copy is necessary because:
|
||||
// 1. Trigger stores the data for use in automation actions that execute later
|
||||
// 2. The span is only valid during this callback (points to temporary BLE stack data)
|
||||
// 3. User lambdas in automations need persistent data they can access asynchronously
|
||||
// Convert span to vector for trigger
|
||||
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
|
||||
});
|
||||
return on_write_trigger;
|
||||
|
||||
@@ -12,6 +12,7 @@ extern "C" {
|
||||
#include "preferences.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
@@ -142,8 +143,16 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
return false;
|
||||
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
||||
uint32_t *buffer = buffer_alloc.get();
|
||||
uint32_t stack_buffer[PREF_BUFFER_WORDS];
|
||||
std::unique_ptr<uint32_t[]> heap_buffer;
|
||||
uint32_t *buffer;
|
||||
|
||||
if (buffer_size <= PREF_BUFFER_WORDS) {
|
||||
buffer = stack_buffer;
|
||||
} else {
|
||||
heap_buffer = make_unique<uint32_t[]>(buffer_size);
|
||||
buffer = heap_buffer.get();
|
||||
}
|
||||
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
||||
|
||||
memcpy(buffer, data, len);
|
||||
@@ -158,8 +167,16 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
return false;
|
||||
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
||||
uint32_t *buffer = buffer_alloc.get();
|
||||
uint32_t stack_buffer[PREF_BUFFER_WORDS];
|
||||
std::unique_ptr<uint32_t[]> heap_buffer;
|
||||
uint32_t *buffer;
|
||||
|
||||
if (buffer_size <= PREF_BUFFER_WORDS) {
|
||||
buffer = stack_buffer;
|
||||
} else {
|
||||
heap_buffer = make_unique<uint32_t[]>(buffer_size);
|
||||
buffer = heap_buffer.get();
|
||||
}
|
||||
|
||||
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
||||
: load_from_rtc(this->offset, buffer, buffer_size);
|
||||
|
||||
@@ -802,8 +802,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
|
||||
/*
|
||||
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
|
||||
@@ -820,10 +820,8 @@ void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
|
||||
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
|
||||
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
|
||||
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
|
||||
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
|
||||
#endif
|
||||
}
|
||||
}
|
||||
#endif // USE_ETHERNET_KSZ8081
|
||||
|
||||
@@ -71,7 +71,7 @@ void FanCall::validate_() {
|
||||
auto traits = this->parent_.get_traits();
|
||||
|
||||
if (this->speed_.has_value()) {
|
||||
this->speed_ = clamp(*this->speed_, 1, static_cast<int>(traits.supported_speed_count()));
|
||||
this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count());
|
||||
|
||||
// https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes
|
||||
// "Manually setting a speed must disable any set preset mode"
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace fan {
|
||||
class FanTraits {
|
||||
public:
|
||||
FanTraits() = default;
|
||||
FanTraits(bool oscillation, bool speed, bool direction, uint8_t speed_count)
|
||||
FanTraits(bool oscillation, bool speed, bool direction, int speed_count)
|
||||
: oscillation_(oscillation), speed_(speed), direction_(direction), speed_count_(speed_count) {}
|
||||
|
||||
/// Return if this fan supports oscillation.
|
||||
@@ -23,9 +23,9 @@ class FanTraits {
|
||||
/// Set whether this fan supports speed levels.
|
||||
void set_speed(bool speed) { this->speed_ = speed; }
|
||||
/// Return how many speed levels the fan has
|
||||
uint8_t supported_speed_count() const { return this->speed_count_; }
|
||||
int supported_speed_count() const { return this->speed_count_; }
|
||||
/// Set how many speed levels this fan has.
|
||||
void set_supported_speed_count(uint8_t speed_count) { this->speed_count_ = speed_count; }
|
||||
void set_supported_speed_count(int speed_count) { this->speed_count_ = speed_count; }
|
||||
/// Return if this fan supports changing direction
|
||||
bool supports_direction() const { return this->direction_; }
|
||||
/// Set whether this fan supports changing direction
|
||||
@@ -64,7 +64,7 @@ class FanTraits {
|
||||
bool oscillation_{false};
|
||||
bool speed_{false};
|
||||
bool direction_{false};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ from esphome.const import (
|
||||
CONF_INITIAL_VALUE,
|
||||
CONF_RESTORE_VALUE,
|
||||
CONF_TYPE,
|
||||
CONF_UPDATE_INTERVAL,
|
||||
CONF_VALUE,
|
||||
)
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
@@ -14,37 +13,25 @@ from esphome.core import CoroPriority, coroutine_with_priority
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
globals_ns = cg.esphome_ns.namespace("globals")
|
||||
GlobalsComponent = globals_ns.class_("GlobalsComponent", cg.Component)
|
||||
RestoringGlobalsComponent = globals_ns.class_(
|
||||
"RestoringGlobalsComponent", cg.PollingComponent
|
||||
)
|
||||
RestoringGlobalsComponent = globals_ns.class_("RestoringGlobalsComponent", cg.Component)
|
||||
RestoringGlobalStringComponent = globals_ns.class_(
|
||||
"RestoringGlobalStringComponent", cg.PollingComponent
|
||||
"RestoringGlobalStringComponent", cg.Component
|
||||
)
|
||||
GlobalVarSetAction = globals_ns.class_("GlobalVarSetAction", automation.Action)
|
||||
|
||||
CONF_MAX_RESTORE_DATA_LENGTH = "max_restore_data_length"
|
||||
|
||||
|
||||
def validate_update_interval(config):
|
||||
if CONF_UPDATE_INTERVAL in config and not config.get(CONF_RESTORE_VALUE, False):
|
||||
raise cv.Invalid("update_interval requires restore_value to be true")
|
||||
return config
|
||||
|
||||
|
||||
MULTI_CONF = True
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
|
||||
cv.Required(CONF_TYPE): cv.string_strict,
|
||||
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
|
||||
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
|
||||
cv.Optional(CONF_UPDATE_INTERVAL): cv.update_interval,
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA),
|
||||
validate_update_interval,
|
||||
)
|
||||
CONFIG_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_ID): cv.declare_id(GlobalsComponent),
|
||||
cv.Required(CONF_TYPE): cv.string_strict,
|
||||
cv.Optional(CONF_INITIAL_VALUE): cv.string_strict,
|
||||
cv.Optional(CONF_RESTORE_VALUE, default=False): cv.boolean,
|
||||
cv.Optional(CONF_MAX_RESTORE_DATA_LENGTH): cv.int_range(0, 254),
|
||||
}
|
||||
).extend(cv.COMPONENT_SCHEMA)
|
||||
|
||||
|
||||
# Run with low priority so that namespaces are registered first
|
||||
@@ -78,8 +65,6 @@ async def to_code(config):
|
||||
value = value.encode()
|
||||
hash_ = int(hashlib.md5(value).hexdigest()[:8], 16)
|
||||
cg.add(glob.set_name_hash(hash_))
|
||||
if CONF_UPDATE_INTERVAL in config:
|
||||
cg.add(glob.set_update_interval(config[CONF_UPDATE_INTERVAL]))
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include <cstring>
|
||||
|
||||
namespace esphome::globals {
|
||||
namespace esphome {
|
||||
namespace globals {
|
||||
|
||||
template<typename T> class GlobalsComponent : public Component {
|
||||
public:
|
||||
@@ -23,14 +24,13 @@ template<typename T> class GlobalsComponent : public Component {
|
||||
T value_{};
|
||||
};
|
||||
|
||||
template<typename T> class RestoringGlobalsComponent : public PollingComponent {
|
||||
template<typename T> class RestoringGlobalsComponent : public Component {
|
||||
public:
|
||||
using value_type = T;
|
||||
explicit RestoringGlobalsComponent() : PollingComponent(1000) {}
|
||||
explicit RestoringGlobalsComponent(T initial_value) : PollingComponent(1000), value_(initial_value) {}
|
||||
explicit RestoringGlobalsComponent() = default;
|
||||
explicit RestoringGlobalsComponent(T initial_value) : value_(initial_value) {}
|
||||
explicit RestoringGlobalsComponent(
|
||||
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
|
||||
: PollingComponent(1000) {
|
||||
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
|
||||
memcpy(this->value_, initial_value.data(), sizeof(T));
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ template<typename T> class RestoringGlobalsComponent : public PollingComponent {
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void update() override { store_value_(); }
|
||||
void loop() override { store_value_(); }
|
||||
|
||||
void on_shutdown() override { store_value_(); }
|
||||
|
||||
@@ -66,14 +66,13 @@ template<typename T> class RestoringGlobalsComponent : public PollingComponent {
|
||||
};
|
||||
|
||||
// Use with string or subclasses of strings
|
||||
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public PollingComponent {
|
||||
template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public Component {
|
||||
public:
|
||||
using value_type = T;
|
||||
explicit RestoringGlobalStringComponent() : PollingComponent(1000) {}
|
||||
explicit RestoringGlobalStringComponent(T initial_value) : PollingComponent(1000) { this->value_ = initial_value; }
|
||||
explicit RestoringGlobalStringComponent() = default;
|
||||
explicit RestoringGlobalStringComponent(T initial_value) { this->value_ = initial_value; }
|
||||
explicit RestoringGlobalStringComponent(
|
||||
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value)
|
||||
: PollingComponent(1000) {
|
||||
std::array<typename std::remove_extent<T>::type, std::extent<T>::value> initial_value) {
|
||||
memcpy(this->value_, initial_value.data(), sizeof(T));
|
||||
}
|
||||
|
||||
@@ -91,7 +90,7 @@ template<typename T, uint8_t SZ> class RestoringGlobalStringComponent : public P
|
||||
|
||||
float get_setup_priority() const override { return setup_priority::HARDWARE; }
|
||||
|
||||
void update() override { store_value_(); }
|
||||
void loop() override { store_value_(); }
|
||||
|
||||
void on_shutdown() override { store_value_(); }
|
||||
|
||||
@@ -145,4 +144,5 @@ template<typename T> T &id(GlobalsComponent<T> *value) { return value->value();
|
||||
template<typename T> T &id(RestoringGlobalsComponent<T> *value) { return value->value(); }
|
||||
template<typename T, uint8_t SZ> T &id(RestoringGlobalStringComponent<T, SZ> *value) { return value->value(); }
|
||||
|
||||
} // namespace esphome::globals
|
||||
} // namespace globals
|
||||
} // namespace esphome
|
||||
|
||||
@@ -39,7 +39,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_DECAY_MODE, default="SLOW"): cv.enum(
|
||||
DECAY_MODE_OPTIONS, upper=True
|
||||
),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
|
||||
cv.Optional(CONF_ENABLE_PIN): cv.use_id(output.FloatOutput),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ enum DecayMode {
|
||||
|
||||
class HBridgeFan : public Component, public fan::Fan {
|
||||
public:
|
||||
HBridgeFan(uint8_t speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
|
||||
HBridgeFan(int speed_count, DecayMode decay_mode) : speed_count_(speed_count), decay_mode_(decay_mode) {}
|
||||
|
||||
void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; }
|
||||
void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; }
|
||||
@@ -33,7 +33,7 @@ class HBridgeFan : public Component, public fan::Fan {
|
||||
output::FloatOutput *pin_b_;
|
||||
output::FloatOutput *enable_{nullptr};
|
||||
output::BinaryOutput *oscillating_{nullptr};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
DecayMode decay_mode_{DECAY_MODE_SLOW};
|
||||
fan::FanTraits traits_;
|
||||
std::vector<const char *> preset_modes_{};
|
||||
|
||||
@@ -119,7 +119,7 @@ void IDFI2CBus::dump_config() {
|
||||
if (s.second) {
|
||||
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
from esphome import pins
|
||||
import esphome.codegen as cg
|
||||
from esphome.components.esp32 import (
|
||||
add_idf_sdkconfig_option,
|
||||
enable_ringbuf_in_iram,
|
||||
get_esp32_variant,
|
||||
)
|
||||
from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32,
|
||||
VARIANT_ESP32C3,
|
||||
VARIANT_ESP32C5,
|
||||
@@ -15,6 +10,8 @@ from esphome.components.esp32.const import (
|
||||
VARIANT_ESP32P4,
|
||||
VARIANT_ESP32S2,
|
||||
VARIANT_ESP32S3,
|
||||
add_idf_sdkconfig_option,
|
||||
get_esp32_variant,
|
||||
)
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import CONF_BITS_PER_SAMPLE, CONF_CHANNEL, CONF_ID, CONF_SAMPLE_RATE
|
||||
@@ -281,9 +278,6 @@ async def to_code(config):
|
||||
# Helps avoid callbacks being skipped due to processor load
|
||||
add_idf_sdkconfig_option("CONFIG_I2S_ISR_IRAM_SAFE", True)
|
||||
|
||||
# Keep ring buffer functions in IRAM for audio performance
|
||||
enable_ringbuf_in_iram()
|
||||
|
||||
cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN]))
|
||||
if CONF_I2S_BCLK_PIN in config:
|
||||
cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN]))
|
||||
|
||||
@@ -11,7 +11,7 @@ static const char *const TAG = "kuntze";
|
||||
static const uint8_t CMD_READ_REG = 0x03;
|
||||
static const uint16_t REGISTER[] = {4136, 4160, 4680, 6000, 4688, 4728, 5832};
|
||||
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4 bytes, plus byte count = 5 bytes)
|
||||
// Maximum bytes to log for Modbus responses (2 registers = 4, plus count = 5)
|
||||
static constexpr size_t KUNTZE_MAX_LOG_BYTES = 8;
|
||||
|
||||
void Kuntze::on_modbus_data(const std::vector<uint8_t> &data) {
|
||||
|
||||
@@ -18,7 +18,16 @@ static constexpr size_t KEY_BUFFER_SIZE = 12;
|
||||
|
||||
struct NVSData {
|
||||
uint32_t key;
|
||||
SmallInlineBuffer<8> data; // Most prefs fit in 8 bytes (covers fan, cover, select, etc.)
|
||||
std::unique_ptr<uint8_t[]> data;
|
||||
size_t len;
|
||||
|
||||
void set_data(const uint8_t *src, size_t size) {
|
||||
if (!this->data || this->len != size) {
|
||||
this->data = std::make_unique<uint8_t[]>(size);
|
||||
this->len = size;
|
||||
}
|
||||
memcpy(this->data.get(), src, size);
|
||||
}
|
||||
};
|
||||
|
||||
static std::vector<NVSData> s_pending_save; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
@@ -33,14 +42,14 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and update that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
obj.data.set(data, len);
|
||||
obj.set_data(data, len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
NVSData save{};
|
||||
save.key = this->key;
|
||||
save.data.set(data, len);
|
||||
s_pending_save.push_back(std::move(save));
|
||||
save.set_data(data, len);
|
||||
s_pending_save.emplace_back(std::move(save));
|
||||
ESP_LOGVV(TAG, "s_pending_save: key: %" PRIu32 ", len: %zu", this->key, len);
|
||||
return true;
|
||||
}
|
||||
@@ -49,11 +58,11 @@ class LibreTinyPreferenceBackend : public ESPPreferenceBackend {
|
||||
// try find in pending saves and load from that
|
||||
for (auto &obj : s_pending_save) {
|
||||
if (obj.key == this->key) {
|
||||
if (obj.data.size() != len) {
|
||||
if (obj.len != len) {
|
||||
// size mismatch
|
||||
return false;
|
||||
}
|
||||
memcpy(data, obj.data.data(), len);
|
||||
memcpy(data, obj.data.get(), len);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -117,11 +126,11 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
snprintf(key_str, sizeof(key_str), "%" PRIu32, save.key);
|
||||
ESP_LOGVV(TAG, "Checking if FDB data %s has changed", key_str);
|
||||
if (this->is_changed_(&this->db, save, key_str)) {
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.data.size());
|
||||
fdb_blob_make(&this->blob, save.data.data(), save.data.size());
|
||||
ESP_LOGV(TAG, "sync: key: %s, len: %zu", key_str, save.len);
|
||||
fdb_blob_make(&this->blob, save.data.get(), save.len);
|
||||
fdb_err_t err = fdb_kv_set_blob(&this->db, key_str, &this->blob);
|
||||
if (err != FDB_NO_ERR) {
|
||||
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.data.size(), err);
|
||||
ESP_LOGV(TAG, "fdb_kv_set_blob('%s', len=%zu) failed: %d", key_str, save.len, err);
|
||||
failed++;
|
||||
last_err = err;
|
||||
last_key = save.key;
|
||||
@@ -129,7 +138,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
written++;
|
||||
} else {
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.data.size());
|
||||
ESP_LOGD(TAG, "FDB data not changed; skipping %" PRIu32 " len=%zu", save.key, save.len);
|
||||
cached++;
|
||||
}
|
||||
s_pending_save.erase(s_pending_save.begin() + i);
|
||||
@@ -153,7 +162,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
|
||||
// Check size first - if different, data has changed
|
||||
if (kv.value_len != to_save.data.size()) {
|
||||
if (kv.value_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -167,7 +176,7 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
}
|
||||
|
||||
// Compare the actual data
|
||||
return memcmp(to_save.data.data(), stored_data.get(), kv.value_len) != 0;
|
||||
return memcmp(to_save.data.get(), stored_data.get(), kv.value_len) != 0;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
|
||||
@@ -234,7 +234,6 @@ class Logger : public Component {
|
||||
#endif
|
||||
|
||||
protected:
|
||||
void write_msg_(const char *msg, size_t len);
|
||||
// RAII guard for recursion flags - sets flag on construction, clears on destruction
|
||||
class RecursionGuard {
|
||||
public:
|
||||
@@ -261,6 +260,7 @@ class Logger : public Component {
|
||||
#endif
|
||||
#endif
|
||||
void process_messages_();
|
||||
void write_msg_(const char *msg, size_t len);
|
||||
|
||||
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
|
||||
// It's the caller's responsibility to initialize buffer_at (typically to 0)
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#ifdef USE_ESP8266
|
||||
#include "logger.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
|
||||
Serial.swap();
|
||||
}
|
||||
Serial.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
Serial1.setDebugOutput(ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
uart_set_debug(UART_NO);
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART0_SWAP:
|
||||
default:
|
||||
return LOG_STR("UART0_SWAP");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif
|
||||
@@ -1,22 +0,0 @@
|
||||
#if defined(USE_HOST)
|
||||
#include "logger.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
time_t rawtime;
|
||||
struct tm *timeinfo;
|
||||
char buffer[80];
|
||||
|
||||
time(&rawtime);
|
||||
timeinfo = localtime(&rawtime);
|
||||
strftime(buffer, sizeof buffer, "[%H:%M:%S]", timeinfo);
|
||||
fputs(buffer, stdout);
|
||||
puts(msg);
|
||||
}
|
||||
|
||||
void Logger::pre_setup() { global_logger = this; }
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -1,70 +0,0 @@
|
||||
#ifdef USE_LIBRETINY
|
||||
#include "logger.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
#if LT_HW_UART0
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial0;
|
||||
Serial0.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART1
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
#if LT_HW_UART2
|
||||
case UART_SELECTION_UART2:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
#endif
|
||||
default:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
if (this->uart_ != UART_SELECTION_DEFAULT) {
|
||||
ESP_LOGW(TAG, " The chosen logger UART port is not available on this board."
|
||||
"The default port was used instead.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// change lt_log() port to match default Serial
|
||||
if (this->uart_ == UART_SELECTION_DEFAULT) {
|
||||
this->uart_ = (UARTSelection) (LT_UART_DEFAULT_SERIAL + 1);
|
||||
lt_log_set_port(LT_UART_DEFAULT_SERIAL);
|
||||
} else {
|
||||
lt_log_set_port(this->uart_ - 1);
|
||||
}
|
||||
}
|
||||
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_DEFAULT:
|
||||
return LOG_STR("DEFAULT");
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
case UART_SELECTION_UART2:
|
||||
default:
|
||||
return LOG_STR("UART2");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif // USE_LIBRETINY
|
||||
@@ -1,48 +0,0 @@
|
||||
#ifdef USE_RP2040
|
||||
#include "logger.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
this->hw_serial_ = &Serial1;
|
||||
Serial1.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
this->hw_serial_ = &Serial2;
|
||||
Serial2.begin(this->baud_rate_);
|
||||
break;
|
||||
case UART_SELECTION_USB_CDC:
|
||||
this->hw_serial_ = &Serial;
|
||||
Serial.begin(this->baud_rate_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
#endif // USE_RP2040
|
||||
@@ -1,96 +0,0 @@
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include "logger.h"
|
||||
|
||||
#include <zephyr/device.h>
|
||||
#include <zephyr/drivers/uart.h>
|
||||
#include <zephyr/usb/usb_device.h>
|
||||
|
||||
namespace esphome::logger {
|
||||
|
||||
static const char *const TAG = "logger";
|
||||
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
void Logger::loop() {
|
||||
if (this->uart_ != UART_SELECTION_USB_CDC || nullptr == this->uart_dev_) {
|
||||
return;
|
||||
}
|
||||
static bool opened = false;
|
||||
uint32_t dtr = 0;
|
||||
uart_line_ctrl_get(this->uart_dev_, UART_LINE_CTRL_DTR, &dtr);
|
||||
|
||||
/* Poll if the DTR flag was set, optional */
|
||||
if (opened == dtr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!opened) {
|
||||
App.schedule_dump_config();
|
||||
}
|
||||
opened = !opened;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Logger::pre_setup() {
|
||||
if (this->baud_rate_ > 0) {
|
||||
static const struct device *uart_dev = nullptr;
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart0));
|
||||
break;
|
||||
case UART_SELECTION_UART1:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(uart1));
|
||||
break;
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
uart_dev = DEVICE_DT_GET_OR_NULL(DT_NODELABEL(cdc_acm_uart0));
|
||||
if (device_is_ready(uart_dev)) {
|
||||
usb_enable(nullptr);
|
||||
}
|
||||
break;
|
||||
#endif
|
||||
}
|
||||
if (!device_is_ready(uart_dev)) {
|
||||
ESP_LOGE(TAG, "%s is not ready.", LOG_STR_ARG(get_uart_selection_()));
|
||||
} else {
|
||||
this->uart_dev_ = uart_dev;
|
||||
}
|
||||
}
|
||||
global_logger = this;
|
||||
ESP_LOGI(TAG, "Log initialized");
|
||||
}
|
||||
|
||||
void HOT Logger::write_msg_(const char *msg) {
|
||||
#ifdef CONFIG_PRINTK
|
||||
printk("%s\n", msg);
|
||||
#endif
|
||||
if (nullptr == this->uart_dev_) {
|
||||
return;
|
||||
}
|
||||
while (*msg) {
|
||||
uart_poll_out(this->uart_dev_, *msg);
|
||||
++msg;
|
||||
}
|
||||
uart_poll_out(this->uart_dev_, '\n');
|
||||
}
|
||||
|
||||
const LogString *Logger::get_uart_selection_() {
|
||||
switch (this->uart_) {
|
||||
case UART_SELECTION_UART0:
|
||||
return LOG_STR("UART0");
|
||||
case UART_SELECTION_UART1:
|
||||
return LOG_STR("UART1");
|
||||
#ifdef USE_LOGGER_USB_CDC
|
||||
case UART_SELECTION_USB_CDC:
|
||||
return LOG_STR("USB_CDC");
|
||||
#endif
|
||||
default:
|
||||
return LOG_STR("UNKNOWN");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace esphome::logger
|
||||
|
||||
#endif
|
||||
@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
|
||||
this->update_reg_(pin, false, iodir);
|
||||
}
|
||||
}
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
|
||||
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
|
||||
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
|
||||
if (this->is_failed())
|
||||
return false;
|
||||
|
||||
@@ -448,9 +448,6 @@ async def to_code(config):
|
||||
# The inference task queues detection events that need immediate processing
|
||||
socket.require_wake_loop_threadsafe()
|
||||
|
||||
# Keep ring buffer functions in IRAM for audio performance
|
||||
esp32.enable_ringbuf_in_iram()
|
||||
|
||||
mic_source = await microphone.microphone_source_to_code(config[CONF_MICROPHONE])
|
||||
cg.add(var.set_microphone_source(mic_source))
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ bool CustomMQTTDevice::publish(const std::string &topic, float value, int8_t num
|
||||
}
|
||||
bool CustomMQTTDevice::publish(const std::string &topic, int value) {
|
||||
char buffer[24];
|
||||
size_t len = buf_append_printf(buffer, sizeof(buffer), 0, "%d", value);
|
||||
int len = snprintf(buffer, sizeof(buffer), "%d", value);
|
||||
return global_mqtt_client->publish(topic, buffer, len);
|
||||
}
|
||||
bool CustomMQTTDevice::publish_json(const std::string &topic, const json::json_build_t &f, uint8_t qos, bool retain) {
|
||||
|
||||
@@ -43,7 +43,7 @@ void MQTTAlarmControlPanelComponent::setup() {
|
||||
|
||||
void MQTTAlarmControlPanelComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT alarm_control_panel '%s':", this->alarm_control_panel_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Supported Features: %" PRIu32 "\n"
|
||||
" Requires Code to Disarm: %s\n"
|
||||
|
||||
@@ -19,7 +19,7 @@ void MQTTBinarySensorComponent::setup() {
|
||||
|
||||
void MQTTBinarySensorComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Binary Sensor '%s':", this->binary_sensor_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, false);
|
||||
LOG_MQTT_COMPONENT(true, false)
|
||||
}
|
||||
MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor *binary_sensor)
|
||||
: binary_sensor_(binary_sensor) {
|
||||
|
||||
@@ -98,17 +98,7 @@ void MQTTClientComponent::send_device_info_() {
|
||||
uint8_t index = 0;
|
||||
for (auto &ip : network::get_ip_addresses()) {
|
||||
if (ip.is_set()) {
|
||||
char key[8]; // "ip" + up to 3 digits + null
|
||||
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
|
||||
if (index == 0) {
|
||||
key[0] = 'i';
|
||||
key[1] = 'p';
|
||||
key[2] = '\0';
|
||||
} else {
|
||||
buf_append_printf(key, sizeof(key), 0, "ip%u", index);
|
||||
}
|
||||
ip.str_to(ip_buf);
|
||||
root[key] = ip_buf;
|
||||
root["ip" + (index == 0 ? "" : esphome::to_string(index))] = ip.str();
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,23 +27,20 @@ inline char *append_char(char *p, char c) {
|
||||
// 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, MQTT_SUFFIX_MAX_LEN, and MQTT_DEFAULT_TOPIC_MAX_LEN are in mqtt_component.h.
|
||||
// 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;
|
||||
|
||||
// Function implementation of LOG_MQTT_COMPONENT macro to reduce code size
|
||||
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic) {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
if (state_topic)
|
||||
ESP_LOGCONFIG(tag, " State Topic: '%s'", obj->get_state_topic_to_(buf).c_str());
|
||||
if (command_topic)
|
||||
ESP_LOGCONFIG(tag, " Command Topic: '%s'", obj->get_command_topic_to_(buf).c_str());
|
||||
}
|
||||
|
||||
void MQTTComponent::set_qos(uint8_t qos) { this->qos_ = qos; }
|
||||
|
||||
void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; }
|
||||
@@ -72,18 +69,19 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove
|
||||
return std::string(buf, p - buf);
|
||||
}
|
||||
|
||||
StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
|
||||
size_t suffix_len) const {
|
||||
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
|
||||
const std::string &topic_prefix = global_mqtt_client->get_topic_prefix();
|
||||
if (topic_prefix.empty()) {
|
||||
return StringRef(); // Empty topic_prefix means no default topic
|
||||
// If the topic_prefix is null, the default topic should be null
|
||||
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 *p = buf.data();
|
||||
char buf[DEFAULT_TOPIC_MAX_LEN];
|
||||
char *p = buf;
|
||||
|
||||
p = append_str(p, topic_prefix.data(), topic_prefix.size());
|
||||
p = append_char(p, '/');
|
||||
@@ -91,44 +89,21 @@ StringRef MQTTComponent::get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_
|
||||
p = append_char(p, '/');
|
||||
p = append_str(p, object_id.c_str(), object_id.size());
|
||||
p = append_char(p, '/');
|
||||
p = append_str(p, suffix, suffix_len);
|
||||
*p = '\0';
|
||||
p = append_str(p, suffix.data(), suffix.size());
|
||||
|
||||
return StringRef(buf.data(), p - buf.data());
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_default_topic_for_(const std::string &suffix) const {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
StringRef ref = this->get_default_topic_for_to_(buf, suffix.data(), suffix.size());
|
||||
return std::string(ref.c_str(), ref.size());
|
||||
}
|
||||
|
||||
StringRef MQTTComponent::get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
|
||||
if (this->custom_state_topic_.has_value()) {
|
||||
// Returns ref to existing data for static/value, uses buf only for lambda case
|
||||
return this->custom_state_topic_.ref_or_copy_to(buf.data(), buf.size());
|
||||
}
|
||||
return this->get_default_topic_for_to_(buf, "state", 5);
|
||||
}
|
||||
|
||||
StringRef MQTTComponent::get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const {
|
||||
if (this->custom_command_topic_.has_value()) {
|
||||
// Returns ref to existing data for static/value, uses buf only for lambda case
|
||||
return this->custom_command_topic_.ref_or_copy_to(buf.data(), buf.size());
|
||||
}
|
||||
return this->get_default_topic_for_to_(buf, "command", 7);
|
||||
return std::string(buf, p - buf);
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_state_topic_() const {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
StringRef ref = this->get_state_topic_to_(buf);
|
||||
return std::string(ref.c_str(), ref.size());
|
||||
if (this->custom_state_topic_.has_value())
|
||||
return this->custom_state_topic_.value();
|
||||
return this->get_default_topic_for_("state");
|
||||
}
|
||||
|
||||
std::string MQTTComponent::get_command_topic_() const {
|
||||
char buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
StringRef ref = this->get_command_topic_to_(buf);
|
||||
return std::string(ref.c_str(), ref.size());
|
||||
if (this->custom_command_topic_.has_value())
|
||||
return this->custom_command_topic_.value();
|
||||
return this->get_default_topic_for_("command");
|
||||
}
|
||||
|
||||
bool MQTTComponent::publish(const std::string &topic, const std::string &payload) {
|
||||
@@ -193,14 +168,10 @@ bool MQTTComponent::send_discovery_() {
|
||||
break;
|
||||
}
|
||||
|
||||
if (config.state_topic) {
|
||||
char state_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
root[MQTT_STATE_TOPIC] = this->get_state_topic_to_(state_topic_buf);
|
||||
}
|
||||
if (config.command_topic) {
|
||||
char command_topic_buf[MQTT_DEFAULT_TOPIC_MAX_LEN];
|
||||
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_to_(command_topic_buf);
|
||||
}
|
||||
if (config.state_topic)
|
||||
root[MQTT_STATE_TOPIC] = this->get_state_topic_();
|
||||
if (config.command_topic)
|
||||
root[MQTT_COMMAND_TOPIC] = this->get_command_topic_();
|
||||
if (this->command_retain_)
|
||||
root[MQTT_COMMAND_RETAIN] = true;
|
||||
|
||||
@@ -219,37 +190,27 @@ bool MQTTComponent::send_discovery_() {
|
||||
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];
|
||||
buf_append_printf(friendly_name_hash, sizeof(friendly_name_hash), 0, "%08" PRIx32,
|
||||
fnv1_hash(this->friendly_name_()));
|
||||
snprintf(friendly_name_hash, sizeof(friendly_name_hash), "%08" PRIx32, fnv1_hash(this->friendly_name_()));
|
||||
// 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);
|
||||
buf_append_printf(unique_id, sizeof(unique_id), 0, "%s-%s-%s", mac_buf, this->component_type(),
|
||||
friendly_name_hash);
|
||||
snprintf(unique_id, sizeof(unique_id), "%s-%s-%s", mac_buf, this->component_type(), friendly_name_hash);
|
||||
root[MQTT_UNIQUE_ID] = unique_id;
|
||||
} else {
|
||||
// default to almost-unique ID. It's a hack but the only way to get that
|
||||
// gorgeous device registry view.
|
||||
// "ESP" (3) + component_type (max 20) + object_id (max 128) + null
|
||||
char unique_id_buf[3 + MQTT_COMPONENT_TYPE_MAX_LEN + OBJECT_ID_MAX_LEN + 1];
|
||||
buf_append_printf(unique_id_buf, sizeof(unique_id_buf), 0, "ESP%s%s", this->component_type(),
|
||||
object_id.c_str());
|
||||
root[MQTT_UNIQUE_ID] = unique_id_buf;
|
||||
root[MQTT_UNIQUE_ID] = "ESP" + std::string(this->component_type()) + object_id.c_str();
|
||||
}
|
||||
|
||||
const std::string &node_name = App.get_name();
|
||||
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR) {
|
||||
// node_name (max 31) + "_" (1) + object_id (max 128) + null
|
||||
char object_id_full[ESPHOME_DEVICE_NAME_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1];
|
||||
buf_append_printf(object_id_full, sizeof(object_id_full), 0, "%s_%s", node_name.c_str(), object_id.c_str());
|
||||
root[MQTT_OBJECT_ID] = object_id_full;
|
||||
}
|
||||
if (discovery_info.object_id_generator == MQTT_DEVICE_NAME_OBJECT_ID_GENERATOR)
|
||||
root[MQTT_OBJECT_ID] = node_name + "_" + object_id.c_str();
|
||||
|
||||
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;
|
||||
const char *node_area = App.get_area();
|
||||
std::string node_area = App.get_area();
|
||||
|
||||
JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>();
|
||||
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||
@@ -260,29 +221,18 @@ bool MQTTComponent::send_discovery_() {
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = ESPHOME_PROJECT_VERSION " (ESPHome " ESPHOME_VERSION ")";
|
||||
const char *model = std::strchr(ESPHOME_PROJECT_NAME, '.');
|
||||
device_info[MQTT_DEVICE_MODEL] = model == nullptr ? ESPHOME_BOARD : model + 1;
|
||||
if (model == nullptr) {
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = ESPHOME_PROJECT_NAME;
|
||||
} else {
|
||||
// Extract manufacturer (part before '.') using stack buffer to avoid heap allocation
|
||||
// memcpy is used instead of strncpy since we know the exact length and strncpy
|
||||
// would still require manual null-termination
|
||||
char manufacturer[sizeof(ESPHOME_PROJECT_NAME)];
|
||||
size_t len = model - ESPHOME_PROJECT_NAME;
|
||||
memcpy(manufacturer, ESPHOME_PROJECT_NAME, len);
|
||||
manufacturer[len] = '\0';
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = manufacturer;
|
||||
}
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] =
|
||||
model == nullptr ? ESPHOME_PROJECT_NAME : std::string(ESPHOME_PROJECT_NAME, model - ESPHOME_PROJECT_NAME);
|
||||
#else
|
||||
static const char ver_fmt[] PROGMEM = ESPHOME_VERSION " (config hash 0x%08" PRIx32 ")";
|
||||
// Buffer sized for format string expansion: ~4 bytes net growth from format specifier to 8 hex digits, plus
|
||||
// safety margin
|
||||
char version_buf[sizeof(ver_fmt) + 8];
|
||||
#ifdef USE_ESP8266
|
||||
snprintf_P(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
|
||||
char fmt_buf[sizeof(ver_fmt)];
|
||||
strcpy_P(fmt_buf, ver_fmt);
|
||||
const char *fmt = fmt_buf;
|
||||
#else
|
||||
snprintf(version_buf, sizeof(version_buf), ver_fmt, App.get_config_hash());
|
||||
const char *fmt = ver_fmt;
|
||||
#endif
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = version_buf;
|
||||
device_info[MQTT_DEVICE_SW_VERSION] = str_sprintf(fmt, App.get_config_hash());
|
||||
device_info[MQTT_DEVICE_MODEL] = ESPHOME_BOARD;
|
||||
#if defined(USE_ESP8266) || defined(USE_ESP32)
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = "Espressif";
|
||||
@@ -296,7 +246,7 @@ bool MQTTComponent::send_discovery_() {
|
||||
device_info[MQTT_DEVICE_MANUFACTURER] = "Host";
|
||||
#endif
|
||||
#endif
|
||||
if (node_area[0] != '\0') {
|
||||
if (!node_area.empty()) {
|
||||
device_info[MQTT_DEVICE_SUGGESTED_AREA] = node_area;
|
||||
}
|
||||
|
||||
@@ -338,9 +288,7 @@ void MQTTComponent::set_availability(std::string topic, std::string payload_avai
|
||||
}
|
||||
void MQTTComponent::disable_availability() { this->set_availability("", "", ""); }
|
||||
void MQTTComponent::call_setup() {
|
||||
// Cache is_internal result once during setup - topics don't change after this
|
||||
this->is_internal_ = this->compute_is_internal_();
|
||||
if (this->is_internal_)
|
||||
if (this->is_internal())
|
||||
return;
|
||||
|
||||
this->setup();
|
||||
@@ -392,28 +340,26 @@ StringRef MQTTComponent::get_default_object_id_to_(std::span<char, OBJECT_ID_MAX
|
||||
}
|
||||
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::compute_is_internal_() {
|
||||
bool MQTTComponent::is_internal() {
|
||||
if (this->custom_state_topic_.has_value()) {
|
||||
// If the custom state_topic is empty, return true as it is internal and should not publish
|
||||
// If the custom state_topic is null, return true as it is internal and should not publish
|
||||
// else, return false, as it is explicitly set to a topic, so it is not internal and should publish
|
||||
// Using is_empty() avoids heap allocation for non-lambda cases
|
||||
return this->custom_state_topic_.is_empty();
|
||||
return this->get_state_topic_().empty();
|
||||
}
|
||||
|
||||
if (this->custom_command_topic_.has_value()) {
|
||||
// If the custom command_topic is empty, return true as it is internal and should not publish
|
||||
// If the custom command_topic is null, return true as it is internal and should not publish
|
||||
// else, return false, as it is explicitly set to a topic, so it is not internal and should publish
|
||||
// Using is_empty() avoids heap allocation for non-lambda cases
|
||||
return this->custom_command_topic_.is_empty();
|
||||
return this->get_command_topic_().empty();
|
||||
}
|
||||
|
||||
// No custom topics have been set - check topic_prefix directly to avoid allocation
|
||||
if (global_mqtt_client->get_topic_prefix().empty()) {
|
||||
// If the default topic prefix is empty, then the component, by default, is internal and should not publish
|
||||
// No custom topics have been set
|
||||
if (this->get_default_topic_for_("").empty()) {
|
||||
// If the default topic prefix is null, then the component, by default, is internal and should not publish
|
||||
return true;
|
||||
}
|
||||
|
||||
// Use ESPHome's component internal state if topic_prefix is not empty with no custom state_topic or command_topic
|
||||
// Use ESPHome's component internal state if topic_prefix is not null with no custom state_topic or command_topic
|
||||
return this->get_entity()->is_internal();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,22 +20,17 @@ struct SendDiscoveryConfig {
|
||||
bool command_topic{true}; ///< If the command topic should be included. Default to true.
|
||||
};
|
||||
|
||||
// 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.
|
||||
// This ensures the stack buffers are always large enough.
|
||||
// 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;
|
||||
static constexpr size_t MQTT_TOPIC_PREFIX_MAX_LEN = 64; // Validated in Python: cv.Length(max=64)
|
||||
// Stack buffer size - safe because all inputs are length-validated at config time
|
||||
// Format: prefix + "/" + type + "/" + object_id + "/" + suffix + null
|
||||
static constexpr size_t MQTT_DEFAULT_TOPIC_MAX_LEN =
|
||||
MQTT_TOPIC_PREFIX_MAX_LEN + 1 + MQTT_COMPONENT_TYPE_MAX_LEN + 1 + OBJECT_ID_MAX_LEN + 1 + MQTT_SUFFIX_MAX_LEN + 1;
|
||||
|
||||
class MQTTComponent; // Forward declaration
|
||||
void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
|
||||
|
||||
#define LOG_MQTT_COMPONENT(state_topic, command_topic) log_mqtt_component(TAG, this, state_topic, command_topic)
|
||||
#define LOG_MQTT_COMPONENT(state_topic, command_topic) \
|
||||
if (state_topic) { \
|
||||
ESP_LOGCONFIG(TAG, " State Topic: '%s'", this->get_state_topic_().c_str()); \
|
||||
} \
|
||||
if (command_topic) { \
|
||||
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")
|
||||
@@ -79,8 +74,6 @@ void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, b
|
||||
* a clean separation.
|
||||
*/
|
||||
class MQTTComponent : public Component {
|
||||
friend void log_mqtt_component(const char *tag, MQTTComponent *obj, bool state_topic, bool command_topic);
|
||||
|
||||
public:
|
||||
/// Constructs a MQTTComponent.
|
||||
explicit MQTTComponent();
|
||||
@@ -95,8 +88,7 @@ class MQTTComponent : public Component {
|
||||
|
||||
virtual bool send_initial_state() = 0;
|
||||
|
||||
/// Returns cached is_internal result (computed once during setup).
|
||||
bool is_internal() const { return this->is_internal_; }
|
||||
virtual bool is_internal();
|
||||
|
||||
/// Set QOS for state messages.
|
||||
void set_qos(uint8_t qos);
|
||||
@@ -187,16 +179,7 @@ class MQTTComponent : public Component {
|
||||
/// Helper method to get the discovery topic for this component.
|
||||
std::string get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const;
|
||||
|
||||
/** Get this components state/command/... topic into a buffer.
|
||||
*
|
||||
* @param buf The buffer to write to (must be exactly MQTT_DEFAULT_TOPIC_MAX_LEN).
|
||||
* @param suffix The suffix/key such as "state" or "command".
|
||||
* @return StringRef pointing to the buffer with the topic.
|
||||
*/
|
||||
StringRef get_default_topic_for_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf, const char *suffix,
|
||||
size_t suffix_len) const;
|
||||
|
||||
/** Get this components state/command/... topic (allocates std::string).
|
||||
/** Get this components state/command/... topic.
|
||||
*
|
||||
* @param suffix The suffix/key such as "state" or "command".
|
||||
* @return The full topic.
|
||||
@@ -217,20 +200,10 @@ class MQTTComponent : public Component {
|
||||
/// Get whether the underlying Entity is disabled by default
|
||||
bool is_disabled_by_default_() const;
|
||||
|
||||
/// Get the MQTT state topic into a buffer (no heap allocation for non-lambda custom topics).
|
||||
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
|
||||
/// @return StringRef pointing to the topic in the buffer.
|
||||
StringRef get_state_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
|
||||
|
||||
/// Get the MQTT command topic into a buffer (no heap allocation for non-lambda custom topics).
|
||||
/// @param buf Buffer of exactly MQTT_DEFAULT_TOPIC_MAX_LEN bytes.
|
||||
/// @return StringRef pointing to the topic in the buffer.
|
||||
StringRef get_command_topic_to_(std::span<char, MQTT_DEFAULT_TOPIC_MAX_LEN> buf) const;
|
||||
|
||||
/// Get the MQTT topic that new states will be shared to (allocates std::string).
|
||||
/// Get the MQTT topic that new states will be shared to.
|
||||
std::string get_state_topic_() const;
|
||||
|
||||
/// Get the MQTT topic for listening to commands (allocates std::string).
|
||||
/// Get the MQTT topic for listening to commands.
|
||||
std::string get_command_topic_() const;
|
||||
|
||||
bool is_connected_() const;
|
||||
@@ -248,18 +221,12 @@ class MQTTComponent : public Component {
|
||||
|
||||
std::unique_ptr<Availability> availability_;
|
||||
|
||||
// Packed bitfields - QoS values are 0-2, bools are flags
|
||||
uint8_t qos_ : 2 {0};
|
||||
uint8_t subscribe_qos_ : 2 {0};
|
||||
bool command_retain_ : 1 {false};
|
||||
bool retain_ : 1 {true};
|
||||
bool discovery_enabled_ : 1 {true};
|
||||
bool resend_state_ : 1 {false};
|
||||
bool is_internal_ : 1 {false}; ///< Cached result of compute_is_internal_(), set during setup
|
||||
|
||||
/// Compute is_internal status based on topics and entity state.
|
||||
/// Called once during setup to cache the result.
|
||||
bool compute_is_internal_();
|
||||
bool command_retain_{false};
|
||||
bool retain_{true};
|
||||
uint8_t qos_{0};
|
||||
uint8_t subscribe_qos_{0};
|
||||
bool discovery_enabled_{true};
|
||||
bool resend_state_{false};
|
||||
};
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -51,7 +51,7 @@ void MQTTCoverComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT cover '%s':", this->cover_->get_name().c_str());
|
||||
auto traits = this->cover_->get_traits();
|
||||
bool has_command_topic = traits.get_supports_position() || !traits.get_supports_tilt();
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic);
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic)
|
||||
if (traits.get_supports_position()) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Position State Topic: '%s'\n"
|
||||
|
||||
@@ -36,7 +36,7 @@ void MQTTDateComponent::setup() {
|
||||
|
||||
void MQTTDateComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Date '%s':", this->date_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTDateComponent, "date")
|
||||
|
||||
@@ -47,7 +47,7 @@ void MQTTDateTimeComponent::setup() {
|
||||
|
||||
void MQTTDateTimeComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT DateTime '%s':", this->datetime_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTDateTimeComponent, "datetime")
|
||||
|
||||
@@ -175,7 +175,7 @@ bool MQTTFanComponent::publish_state() {
|
||||
auto traits = this->state_->get_traits();
|
||||
if (traits.supports_speed()) {
|
||||
char buf[12];
|
||||
size_t len = buf_append_printf(buf, sizeof(buf), 0, "%d", this->state_->speed);
|
||||
int len = snprintf(buf, sizeof(buf), "%d", this->state_->speed);
|
||||
bool success = this->publish(this->get_speed_level_state_topic(), buf, len);
|
||||
failed = failed || !success;
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery
|
||||
bool MQTTJSONLightComponent::send_initial_state() { return this->publish_state_(); }
|
||||
void MQTTJSONLightComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Light '%s':", this->state_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
}
|
||||
|
||||
} // namespace esphome::mqtt
|
||||
|
||||
@@ -30,7 +30,7 @@ void MQTTNumberComponent::setup() {
|
||||
|
||||
void MQTTNumberComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Number '%s':", this->number_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, false);
|
||||
LOG_MQTT_COMPONENT(true, false)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTNumberComponent, "number")
|
||||
@@ -75,7 +75,7 @@ bool MQTTNumberComponent::send_initial_state() {
|
||||
}
|
||||
bool MQTTNumberComponent::publish_state(float value) {
|
||||
char buffer[64];
|
||||
buf_append_printf(buffer, sizeof(buffer), 0, "%f", value);
|
||||
snprintf(buffer, sizeof(buffer), "%f", value);
|
||||
return this->publish(this->get_state_topic_(), buffer);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ void MQTTSelectComponent::setup() {
|
||||
|
||||
void MQTTSelectComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Select '%s':", this->select_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, false);
|
||||
LOG_MQTT_COMPONENT(true, false)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTSelectComponent, "select")
|
||||
|
||||
@@ -28,7 +28,7 @@ void MQTTSensorComponent::dump_config() {
|
||||
if (this->get_expire_after() > 0) {
|
||||
ESP_LOGCONFIG(TAG, " Expire After: %" PRIu32 "s", this->get_expire_after() / 1000);
|
||||
}
|
||||
LOG_MQTT_COMPONENT(true, false);
|
||||
LOG_MQTT_COMPONENT(true, false)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTSensorComponent, "sensor")
|
||||
|
||||
@@ -26,7 +26,7 @@ void MQTTTextComponent::setup() {
|
||||
|
||||
void MQTTTextComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT text '%s':", this->text_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTTextComponent, "text")
|
||||
|
||||
@@ -36,7 +36,7 @@ void MQTTTimeComponent::setup() {
|
||||
|
||||
void MQTTTimeComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT Time '%s':", this->time_->get_name().c_str());
|
||||
LOG_MQTT_COMPONENT(true, true);
|
||||
LOG_MQTT_COMPONENT(true, true)
|
||||
}
|
||||
|
||||
MQTT_COMPONENT_TYPE(MQTTTimeComponent, "time")
|
||||
|
||||
@@ -39,7 +39,7 @@ void MQTTValveComponent::dump_config() {
|
||||
ESP_LOGCONFIG(TAG, "MQTT valve '%s':", this->valve_->get_name().c_str());
|
||||
auto traits = this->valve_->get_traits();
|
||||
bool has_command_topic = traits.get_supports_position();
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic);
|
||||
LOG_MQTT_COMPONENT(true, has_command_topic)
|
||||
if (traits.get_supports_position()) {
|
||||
ESP_LOGCONFIG(TAG,
|
||||
" Position State Topic: '%s'\n"
|
||||
|
||||
@@ -80,21 +80,23 @@ class Select : public EntityBase {
|
||||
|
||||
void add_on_state_callback(std::function<void(size_t)> &&callback);
|
||||
|
||||
/** Set the value of the select by index, this is an optional virtual method.
|
||||
*
|
||||
* This method is called by the SelectCall when the index is already known.
|
||||
* Default implementation converts to string and calls control().
|
||||
* Override this to work directly with indices and avoid string conversions.
|
||||
*
|
||||
* @param index The index as validated by the SelectCall.
|
||||
*/
|
||||
virtual void control(size_t index) { this->control(this->option_at(index)); }
|
||||
|
||||
protected:
|
||||
friend class SelectCall;
|
||||
|
||||
size_t active_index_{0};
|
||||
|
||||
/** Set the value of the select by index, this is an optional virtual method.
|
||||
*
|
||||
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
|
||||
* Overriding this index-based version is PREFERRED as it avoids string conversions.
|
||||
*
|
||||
* This method is called by the SelectCall when the index is already known.
|
||||
* Default implementation converts to string and calls control(const std::string&).
|
||||
*
|
||||
* @param index The index as validated by the SelectCall.
|
||||
*/
|
||||
virtual void control(size_t index) { this->control(this->option_at(index)); }
|
||||
|
||||
/** Set the value of the select, this is a virtual method that each select integration can implement.
|
||||
*
|
||||
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
|
||||
|
||||
@@ -25,7 +25,7 @@ CONFIG_SCHEMA = (
|
||||
cv.Optional(CONF_SPEED): cv.invalid(
|
||||
"Configuring individual speeds is deprecated."
|
||||
),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=100): cv.int_range(min=1),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace speed {
|
||||
|
||||
class SpeedFan : public Component, public fan::Fan {
|
||||
public:
|
||||
SpeedFan(uint8_t speed_count) : speed_count_(speed_count) {}
|
||||
SpeedFan(int speed_count) : speed_count_(speed_count) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void set_output(output::FloatOutput *output) { this->output_ = output; }
|
||||
@@ -26,7 +26,7 @@ class SpeedFan : public Component, public fan::Fan {
|
||||
output::FloatOutput *output_;
|
||||
output::BinaryOutput *oscillating_{nullptr};
|
||||
output::BinaryOutput *direction_{nullptr};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
fan::FanTraits traits_;
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ CONFIG_SCHEMA = (
|
||||
{
|
||||
cv.Optional(CONF_HAS_DIRECTION, default=False): cv.boolean,
|
||||
cv.Optional(CONF_HAS_OSCILLATING, default=False): cv.boolean,
|
||||
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SPEED_COUNT): cv.int_range(min=1),
|
||||
cv.Optional(CONF_PRESET_MODES): validate_preset_modes,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -12,7 +12,7 @@ class TemplateFan final : public Component, public fan::Fan {
|
||||
void dump_config() override;
|
||||
void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; }
|
||||
void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; }
|
||||
void set_speed_count(uint8_t count) { this->speed_count_ = count; }
|
||||
void set_speed_count(int count) { this->speed_count_ = count; }
|
||||
void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; }
|
||||
fan::FanTraits get_traits() override { return this->traits_; }
|
||||
|
||||
@@ -21,7 +21,7 @@ class TemplateFan final : public Component, public fan::Fan {
|
||||
|
||||
bool has_oscillating_{false};
|
||||
bool has_direction_{false};
|
||||
uint8_t speed_count_{0};
|
||||
int speed_count_{0};
|
||||
fan::FanTraits traits_;
|
||||
std::vector<const char *> preset_modes_{};
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ CONFIG_SCHEMA = cv.All(
|
||||
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_SWITCH_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
|
||||
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=255),
|
||||
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
|
||||
}
|
||||
)
|
||||
.extend(cv.COMPONENT_SCHEMA),
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace tuya {
|
||||
|
||||
class TuyaFan : public Component, public fan::Fan {
|
||||
public:
|
||||
TuyaFan(Tuya *parent, uint8_t speed_count) : parent_(parent), speed_count_(speed_count) {}
|
||||
TuyaFan(Tuya *parent, int speed_count) : parent_(parent), speed_count_(speed_count) {}
|
||||
void setup() override;
|
||||
void dump_config() override;
|
||||
void set_speed_id(uint8_t speed_id) { this->speed_id_ = speed_id; }
|
||||
@@ -27,7 +27,7 @@ class TuyaFan : public Component, public fan::Fan {
|
||||
optional<uint8_t> switch_id_{};
|
||||
optional<uint8_t> oscillation_id_{};
|
||||
optional<uint8_t> direction_id_{};
|
||||
uint8_t speed_count_{};
|
||||
int speed_count_{};
|
||||
TuyaDatapointType speed_type_{};
|
||||
TuyaDatapointType oscillation_type_{};
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
|
||||
namespace esphome {
|
||||
namespace tx20 {
|
||||
@@ -45,24 +45,25 @@ std::string Tx20Component::get_wind_cardinal_direction() const { return this->wi
|
||||
void Tx20Component::decode_and_publish_() {
|
||||
ESP_LOGVV(TAG, "Decode Tx20");
|
||||
|
||||
std::array<bool, MAX_BUFFER_SIZE> bit_buffer{};
|
||||
size_t bit_pos = 0;
|
||||
std::string string_buffer;
|
||||
std::string string_buffer_2;
|
||||
std::vector<bool> bit_buffer;
|
||||
bool current_bit = true;
|
||||
// Cap at MAX_BUFFER_SIZE to prevent out-of-bounds access (buffer_index can exceed MAX_BUFFER_SIZE in ISR)
|
||||
const int max_buffer_index = std::min(static_cast<int>(this->store_.buffer_index), static_cast<int>(MAX_BUFFER_SIZE));
|
||||
|
||||
for (int i = 1; i <= max_buffer_index; i++) {
|
||||
for (int i = 1; i <= this->store_.buffer_index; i++) {
|
||||
string_buffer_2 += to_string(this->store_.buffer[i]) + ", ";
|
||||
uint8_t repeat = this->store_.buffer[i] / TX20_BIT_TIME;
|
||||
// ignore segments at the end that were too short
|
||||
for (uint8_t j = 0; j < repeat && bit_pos < MAX_BUFFER_SIZE; j++) {
|
||||
bit_buffer[bit_pos++] = current_bit;
|
||||
}
|
||||
string_buffer.append(repeat, current_bit ? '1' : '0');
|
||||
bit_buffer.insert(bit_buffer.end(), repeat, current_bit);
|
||||
current_bit = !current_bit;
|
||||
}
|
||||
current_bit = !current_bit;
|
||||
size_t bits_before_padding = bit_pos;
|
||||
while (bit_pos < MAX_BUFFER_SIZE) {
|
||||
bit_buffer[bit_pos++] = current_bit;
|
||||
if (string_buffer.length() < MAX_BUFFER_SIZE) {
|
||||
uint8_t remain = MAX_BUFFER_SIZE - string_buffer.length();
|
||||
string_buffer_2 += to_string(remain) + ", ";
|
||||
string_buffer.append(remain, current_bit ? '1' : '0');
|
||||
bit_buffer.insert(bit_buffer.end(), remain, current_bit);
|
||||
}
|
||||
|
||||
uint8_t tx20_sa = 0;
|
||||
@@ -107,24 +108,8 @@ void Tx20Component::decode_and_publish_() {
|
||||
// 2. Check received checksum matches calculated checksum
|
||||
// 3. Check that Wind Direction matches Wind Direction (Inverted)
|
||||
// 4. Check that Wind Speed matches Wind Speed (Inverted)
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||
// Build debug strings from completed data
|
||||
char debug_buf[320]; // buffer values: max 42 entries * 7 chars each
|
||||
size_t debug_pos = 0;
|
||||
for (int i = 1; i <= max_buffer_index; i++) {
|
||||
debug_pos = buf_append_printf(debug_buf, sizeof(debug_buf), debug_pos, "%u, ", this->store_.buffer[i]);
|
||||
}
|
||||
if (bits_before_padding < MAX_BUFFER_SIZE) {
|
||||
buf_append_printf(debug_buf, sizeof(debug_buf), debug_pos, "%zu, ", MAX_BUFFER_SIZE - bits_before_padding);
|
||||
}
|
||||
char bits_buf[MAX_BUFFER_SIZE + 1];
|
||||
for (size_t i = 0; i < MAX_BUFFER_SIZE; i++) {
|
||||
bits_buf[i] = bit_buffer[i] ? '1' : '0';
|
||||
}
|
||||
bits_buf[MAX_BUFFER_SIZE] = '\0';
|
||||
ESP_LOGVV(TAG, "BUFFER %s", debug_buf);
|
||||
ESP_LOGVV(TAG, "Decoded bits %s", bits_buf);
|
||||
#endif
|
||||
ESP_LOGVV(TAG, "BUFFER %s", string_buffer_2.c_str());
|
||||
ESP_LOGVV(TAG, "Decoded bits %s", string_buffer.c_str());
|
||||
|
||||
if (tx20_sa == 4) {
|
||||
if (chk == tx20_sd) {
|
||||
|
||||
@@ -401,9 +401,8 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
|
||||
void AsyncResponseStream::print(float value) {
|
||||
// Use stack buffer to avoid temporary string allocation
|
||||
// Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
|
||||
constexpr size_t float_buf_size = 32;
|
||||
char buf[float_buf_size];
|
||||
int len = snprintf(buf, float_buf_size, "%f", value);
|
||||
char buf[32];
|
||||
int len = snprintf(buf, sizeof(buf), "%f", value);
|
||||
this->content_.append(buf, len);
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,6 @@
|
||||
#include "esphome/components/esp32_improv/esp32_improv_component.h"
|
||||
#endif
|
||||
|
||||
#ifdef USE_IMPROV_SERIAL
|
||||
#include "esphome/components/improv_serial/improv_serial_component.h"
|
||||
#endif
|
||||
|
||||
namespace esphome::wifi {
|
||||
|
||||
static const char *const TAG = "wifi";
|
||||
@@ -369,75 +365,6 @@ bool WiFiComponent::ssid_was_seen_in_scan_(const std::string &ssid) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WiFiComponent::needs_full_scan_results_() const {
|
||||
// Components that require full scan results (for example, scan result listeners)
|
||||
// are expected to call request_wifi_scan_results(), which sets keep_scan_results_.
|
||||
if (this->keep_scan_results_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
// Captive portal needs full results when active (showing network list to user)
|
||||
if (captive_portal::global_captive_portal != nullptr && captive_portal::global_captive_portal->is_active()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IMPROV_SERIAL
|
||||
// Improv serial needs results during provisioning (before connected)
|
||||
if (improv_serial::global_improv_serial_component != nullptr && !this->is_connected()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef USE_IMPROV
|
||||
// BLE improv also needs results during provisioning
|
||||
if (esp32_improv::global_improv_component != nullptr && esp32_improv::global_improv_component->is_active()) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t *bssid) const {
|
||||
// Hidden networks in scan results have empty SSIDs - skip them
|
||||
if (ssid[0] == '\0') {
|
||||
return false;
|
||||
}
|
||||
for (const auto &sta : this->sta_) {
|
||||
// Skip hidden network configs (they don't appear in normal scans)
|
||||
if (sta.get_hidden()) {
|
||||
continue;
|
||||
}
|
||||
// For BSSID-only configs (empty SSID), match by BSSID
|
||||
if (sta.get_ssid().empty()) {
|
||||
if (sta.has_bssid() && std::memcmp(sta.get_bssid().data(), bssid, 6) == 0) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Match by SSID
|
||||
if (sta.get_ssid() == ssid) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
// Skip logging during roaming scans to avoid log buffer overflow
|
||||
// (roaming scans typically find many networks but only care about same-SSID APs)
|
||||
if (this->roaming_state_ == RoamingState::SCANNING) {
|
||||
return;
|
||||
}
|
||||
char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
format_mac_addr_upper(bssid, bssid_s);
|
||||
ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") " %ddB Ch:%u", ssid, bssid_s, rssi, channel);
|
||||
#endif
|
||||
}
|
||||
|
||||
int8_t WiFiComponent::find_next_hidden_sta_(int8_t start_index) {
|
||||
// Find next SSID to try in RETRY_HIDDEN phase.
|
||||
//
|
||||
@@ -726,12 +653,8 @@ void WiFiComponent::loop() {
|
||||
ESP_LOGI(TAG, "Starting fallback AP");
|
||||
this->setup_ap_config_();
|
||||
#ifdef USE_CAPTIVE_PORTAL
|
||||
if (captive_portal::global_captive_portal != nullptr) {
|
||||
// Reset so we force one full scan after captive portal starts
|
||||
// (previous scans were filtered because captive portal wasn't active yet)
|
||||
this->has_completed_scan_after_captive_portal_start_ = false;
|
||||
if (captive_portal::global_captive_portal != nullptr)
|
||||
captive_portal::global_captive_portal->start();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -1248,7 +1171,7 @@ template<typename VectorType> static void insertion_sort_scan_results(VectorType
|
||||
// has overhead from UART transmission, so combining INFO+DEBUG into one line halves
|
||||
// the blocking time. Do NOT split this into separate ESP_LOGI/ESP_LOGD calls.
|
||||
__attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res) {
|
||||
char bssid_s[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
|
||||
char bssid_s[18];
|
||||
auto bssid = res.get_bssid();
|
||||
format_mac_addr_upper(bssid.data(), bssid_s);
|
||||
|
||||
@@ -1264,6 +1187,18 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
|
||||
#endif
|
||||
}
|
||||
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
// Helper function to log non-matching scan results at verbose level
|
||||
__attribute__((noinline)) static void log_scan_result_non_matching(const WiFiScanResult &res) {
|
||||
char bssid_s[18];
|
||||
auto bssid = res.get_bssid();
|
||||
format_mac_addr_upper(bssid.data(), bssid_s);
|
||||
|
||||
ESP_LOGV(TAG, "- " LOG_SECRET("'%s'") " " LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), bssid_s,
|
||||
LOG_STR_ARG(get_signal_bars(res.get_rssi())));
|
||||
}
|
||||
#endif
|
||||
|
||||
void WiFiComponent::check_scanning_finished() {
|
||||
if (!this->scan_done_) {
|
||||
if (millis() - this->action_started_ > WIFI_SCAN_TIMEOUT_MS) {
|
||||
@@ -1273,8 +1208,6 @@ void WiFiComponent::check_scanning_finished() {
|
||||
return;
|
||||
}
|
||||
this->scan_done_ = false;
|
||||
this->has_completed_scan_after_captive_portal_start_ =
|
||||
true; // Track that we've done a scan since captive portal started
|
||||
this->retry_hidden_mode_ = RetryHiddenMode::SCAN_BASED;
|
||||
|
||||
if (this->scan_result_.empty()) {
|
||||
@@ -1302,12 +1235,21 @@ void WiFiComponent::check_scanning_finished() {
|
||||
// Sort scan results using insertion sort for better memory efficiency
|
||||
insertion_sort_scan_results(this->scan_result_);
|
||||
|
||||
// Log matching networks (non-matching already logged at VERBOSE in scan callback)
|
||||
size_t non_matching_count = 0;
|
||||
for (auto &res : this->scan_result_) {
|
||||
if (res.get_matches()) {
|
||||
log_scan_result(res);
|
||||
} else {
|
||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
|
||||
log_scan_result_non_matching(res);
|
||||
#else
|
||||
non_matching_count++;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
if (non_matching_count > 0) {
|
||||
ESP_LOGD(TAG, "- %zu non-matching (VERBOSE to show)", non_matching_count);
|
||||
}
|
||||
|
||||
// SYNCHRONIZATION POINT: Establish link between scan_result_[0] and selected_sta_index_
|
||||
// After sorting, scan_result_[0] contains the best network. Now find which sta_[i] config
|
||||
@@ -1571,10 +1513,7 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
||||
if (this->went_through_explicit_hidden_phase_()) {
|
||||
return WiFiRetryPhase::EXPLICIT_HIDDEN;
|
||||
}
|
||||
// Skip scanning when captive portal/improv is active to avoid disrupting AP,
|
||||
// BUT only if we've already completed at least one scan AFTER the portal started.
|
||||
// When captive portal first starts, scan results may be filtered/stale, so we need
|
||||
// to do one full scan to populate available networks for the captive portal UI.
|
||||
// Skip scanning when captive portal/improv is active to avoid disrupting AP.
|
||||
//
|
||||
// WHY SCANNING DISRUPTS AP MODE:
|
||||
// WiFi scanning requires the radio to leave the AP's channel and hop through
|
||||
@@ -1591,16 +1530,7 @@ WiFiRetryPhase WiFiComponent::determine_next_phase_() {
|
||||
//
|
||||
// This allows users to configure WiFi via captive portal while the device keeps
|
||||
// attempting to connect to all configured networks in sequence.
|
||||
// Captive portal needs scan results to show available networks.
|
||||
// If captive portal is active, only skip scanning if we've done a scan after it started.
|
||||
// If only improv is active (no captive portal), skip scanning since improv doesn't need results.
|
||||
if (this->is_captive_portal_active_()) {
|
||||
if (this->has_completed_scan_after_captive_portal_start_) {
|
||||
return WiFiRetryPhase::RETRY_HIDDEN;
|
||||
}
|
||||
// Need to scan for captive portal
|
||||
} else if (this->is_esp32_improv_active_()) {
|
||||
// Improv doesn't need scan results
|
||||
if (this->is_captive_portal_active_() || this->is_esp32_improv_active_()) {
|
||||
return WiFiRetryPhase::RETRY_HIDDEN;
|
||||
}
|
||||
return WiFiRetryPhase::SCAN_CONNECTING;
|
||||
@@ -1805,7 +1735,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
|
||||
(old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
|
||||
this->set_sta_priority(failed_bssid.value(), new_priority);
|
||||
}
|
||||
|
||||
char bssid_s[18];
|
||||
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
|
||||
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d",
|
||||
@@ -2150,7 +2079,7 @@ void WiFiComponent::clear_roaming_state_() {
|
||||
|
||||
void WiFiComponent::release_scan_results_() {
|
||||
if (!this->keep_scan_results_) {
|
||||
#if defined(USE_RP2040) || defined(USE_ESP32)
|
||||
#ifdef USE_RP2040
|
||||
// std::vector - use swap trick since shrink_to_fit is non-binding
|
||||
decltype(this->scan_result_)().swap(this->scan_result_);
|
||||
#else
|
||||
|
||||
@@ -161,12 +161,9 @@ struct EAPAuth {
|
||||
|
||||
using bssid_t = std::array<uint8_t, 6>;
|
||||
|
||||
/// Initial reserve size for filtered scan results (typical: 1-3 matching networks per SSID)
|
||||
static constexpr size_t WIFI_SCAN_RESULT_FILTERED_RESERVE = 8;
|
||||
|
||||
// Use std::vector for RP2040 (callback-based) and ESP32 (destructive scan API)
|
||||
// Use FixedVector for ESP8266 and LibreTiny where two-pass exact allocation is possible
|
||||
#if defined(USE_RP2040) || defined(USE_ESP32)
|
||||
// Use std::vector for RP2040 since scan count is unknown (callback-based)
|
||||
// Use FixedVector for other platforms where count is queried first
|
||||
#ifdef USE_RP2040
|
||||
template<typename T> using wifi_scan_vector_t = std::vector<T>;
|
||||
#else
|
||||
template<typename T> using wifi_scan_vector_t = FixedVector<T>;
|
||||
@@ -542,13 +539,6 @@ class WiFiComponent : public Component {
|
||||
/// Check if an SSID was seen in the most recent scan results
|
||||
/// Used to skip hidden mode for SSIDs we know are visible
|
||||
bool ssid_was_seen_in_scan_(const std::string &ssid) const;
|
||||
/// Check if full scan results are needed (captive portal active, improv, listeners)
|
||||
bool needs_full_scan_results_() const;
|
||||
/// Check if network matches any configured network (for scan result filtering)
|
||||
/// Matches by SSID when configured, or by BSSID for BSSID-only configs
|
||||
bool matches_configured_network_(const char *ssid, const uint8_t *bssid) const;
|
||||
/// Log a discarded scan result at VERBOSE level (skipped during roaming scans to avoid log overflow)
|
||||
void log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel);
|
||||
/// Find next SSID that wasn't in scan results (might be hidden)
|
||||
/// Returns index of next potentially hidden SSID, or -1 if none found
|
||||
/// @param start_index Start searching from index after this (-1 to start from beginning)
|
||||
@@ -720,8 +710,6 @@ class WiFiComponent : public Component {
|
||||
bool enable_on_boot_{true};
|
||||
bool got_ipv4_address_{false};
|
||||
bool keep_scan_results_{false};
|
||||
bool has_completed_scan_after_captive_portal_start_{
|
||||
false}; // Tracks if we've completed a scan after captive portal started
|
||||
RetryHiddenMode retry_hidden_mode_{RetryHiddenMode::BLIND_RETRY};
|
||||
bool skip_cooldown_next_cycle_{false};
|
||||
bool post_connect_roaming_{true}; // Enabled by default
|
||||
|
||||
@@ -756,35 +756,20 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Count the number of results first
|
||||
auto *head = reinterpret_cast<bss_info *>(arg);
|
||||
bool needs_full = this->needs_full_scan_results_();
|
||||
|
||||
// First pass: count matching networks (linked list is non-destructive)
|
||||
size_t total = 0;
|
||||
size_t count = 0;
|
||||
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
||||
total++;
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
|
||||
count++;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
this->scan_result_.init(count); // Exact allocation
|
||||
|
||||
// Second pass: store matching networks
|
||||
this->scan_result_.init(count);
|
||||
for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(it->ssid);
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, it->bssid)) {
|
||||
this->scan_result_.emplace_back(
|
||||
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
||||
std::string(ssid_cstr, it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN, it->is_hidden != 0);
|
||||
} else {
|
||||
this->log_discarded_scan_result_(ssid_cstr, it->bssid, it->rssi, it->channel);
|
||||
}
|
||||
this->scan_result_.emplace_back(
|
||||
bssid_t{it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
|
||||
std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi, it->authmode != AUTH_OPEN,
|
||||
it->is_hidden != 0);
|
||||
}
|
||||
ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", total, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
this->scan_done_ = true;
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : global_wifi_component->scan_results_listeners_) {
|
||||
|
||||
@@ -827,14 +827,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
|
||||
uint16_t number = it.number;
|
||||
bool needs_full = this->needs_full_scan_results_();
|
||||
|
||||
// Smart reserve: full capacity if needed, small reserve otherwise
|
||||
if (needs_full) {
|
||||
this->scan_result_.reserve(number);
|
||||
} else {
|
||||
this->scan_result_.reserve(WIFI_SCAN_RESULT_FILTERED_RESERVE);
|
||||
}
|
||||
scan_result_.init(number);
|
||||
|
||||
// Process one record at a time to avoid large buffer allocation
|
||||
wifi_ap_record_t record;
|
||||
@@ -845,23 +838,12 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
|
||||
break;
|
||||
}
|
||||
|
||||
// Check C string first - avoid std::string construction for non-matching networks
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(record.ssid);
|
||||
|
||||
// Only construct std::string and store if needed
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, record.bssid)) {
|
||||
bssid_t bssid;
|
||||
std::copy(record.bssid, record.bssid + 6, bssid.begin());
|
||||
std::string ssid(ssid_cstr);
|
||||
this->scan_result_.emplace_back(bssid, std::move(ssid), record.primary, record.rssi,
|
||||
record.authmode != WIFI_AUTH_OPEN, ssid_cstr[0] == '\0');
|
||||
} else {
|
||||
this->log_discarded_scan_result_(ssid_cstr, record.bssid, record.rssi, record.primary);
|
||||
}
|
||||
bssid_t bssid;
|
||||
std::copy(record.bssid, record.bssid + 6, bssid.begin());
|
||||
std::string ssid(reinterpret_cast<const char *>(record.ssid));
|
||||
scan_result_.emplace_back(bssid, ssid, record.primary, record.rssi, record.authmode != WIFI_AUTH_OPEN,
|
||||
ssid.empty());
|
||||
}
|
||||
ESP_LOGV(TAG, "Scan complete: %u found, %zu stored%s", number, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(this->scan_result_);
|
||||
|
||||
@@ -664,39 +664,18 @@ void WiFiComponent::wifi_scan_done_callback_() {
|
||||
if (num < 0)
|
||||
return;
|
||||
|
||||
bool needs_full = this->needs_full_scan_results_();
|
||||
|
||||
// Access scan results directly via WiFi.scan struct to avoid Arduino String allocations
|
||||
// WiFi.scan is public in LibreTiny for WiFiEvents & WiFiScan static handlers
|
||||
auto *scan = WiFi.scan;
|
||||
|
||||
// First pass: count matching networks
|
||||
size_t count = 0;
|
||||
this->scan_result_.init(static_cast<unsigned int>(num));
|
||||
for (int i = 0; i < num; i++) {
|
||||
const char *ssid_cstr = scan->ap[i].ssid;
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
String ssid = WiFi.SSID(i);
|
||||
wifi_auth_mode_t authmode = WiFi.encryptionType(i);
|
||||
int32_t rssi = WiFi.RSSI(i);
|
||||
uint8_t *bssid = WiFi.BSSID(i);
|
||||
int32_t channel = WiFi.channel(i);
|
||||
|
||||
this->scan_result_.init(count); // Exact allocation
|
||||
|
||||
// Second pass: store matching networks
|
||||
for (int i = 0; i < num; i++) {
|
||||
const char *ssid_cstr = scan->ap[i].ssid;
|
||||
if (needs_full || this->matches_configured_network_(ssid_cstr, scan->ap[i].bssid.addr)) {
|
||||
auto &ap = scan->ap[i];
|
||||
this->scan_result_.emplace_back(bssid_t{ap.bssid.addr[0], ap.bssid.addr[1], ap.bssid.addr[2], ap.bssid.addr[3],
|
||||
ap.bssid.addr[4], ap.bssid.addr[5]},
|
||||
std::string(ssid_cstr), ap.channel, ap.rssi, ap.auth != WIFI_AUTH_OPEN,
|
||||
ssid_cstr[0] == '\0');
|
||||
} else {
|
||||
auto &ap = scan->ap[i];
|
||||
this->log_discarded_scan_result_(ssid_cstr, ap.bssid.addr, ap.rssi, ap.channel);
|
||||
}
|
||||
this->scan_result_.emplace_back(bssid_t{bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]},
|
||||
std::string(ssid.c_str()), channel, rssi, authmode != WIFI_AUTH_OPEN,
|
||||
ssid.length() == 0);
|
||||
}
|
||||
ESP_LOGV(TAG, "Scan complete: %d found, %zu stored%s", num, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
WiFi.scanDelete();
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
|
||||
@@ -21,7 +21,6 @@ static const char *const TAG = "wifi_pico_w";
|
||||
// Track previous state for detecting changes
|
||||
static bool s_sta_was_connected = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static bool s_sta_had_ip = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
static size_t s_scan_result_count = 0; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
|
||||
if (sta.has_value()) {
|
||||
@@ -138,20 +137,10 @@ int WiFiComponent::s_wifi_scan_result(void *env, const cyw43_ev_scan_result_t *r
|
||||
}
|
||||
|
||||
void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *result) {
|
||||
s_scan_result_count++;
|
||||
const char *ssid_cstr = reinterpret_cast<const char *>(result->ssid);
|
||||
|
||||
// Skip networks that don't match any configured network (unless full results needed)
|
||||
if (!this->needs_full_scan_results_() && !this->matches_configured_network_(ssid_cstr, result->bssid)) {
|
||||
this->log_discarded_scan_result_(ssid_cstr, result->bssid, result->rssi, result->channel);
|
||||
return;
|
||||
}
|
||||
|
||||
bssid_t bssid;
|
||||
std::copy(result->bssid, result->bssid + 6, bssid.begin());
|
||||
std::string ssid(ssid_cstr);
|
||||
WiFiScanResult res(bssid, std::move(ssid), result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN,
|
||||
ssid_cstr[0] == '\0');
|
||||
std::string ssid(reinterpret_cast<const char *>(result->ssid));
|
||||
WiFiScanResult res(bssid, ssid, result->channel, result->rssi, result->auth_mode != CYW43_AUTH_OPEN, ssid.empty());
|
||||
if (std::find(this->scan_result_.begin(), this->scan_result_.end(), res) == this->scan_result_.end()) {
|
||||
this->scan_result_.push_back(res);
|
||||
}
|
||||
@@ -160,7 +149,6 @@ void WiFiComponent::wifi_scan_result(void *env, const cyw43_ev_scan_result_t *re
|
||||
bool WiFiComponent::wifi_scan_start_(bool passive) {
|
||||
this->scan_result_.clear();
|
||||
this->scan_done_ = false;
|
||||
s_scan_result_count = 0;
|
||||
cyw43_wifi_scan_options_t scan_options = {0};
|
||||
scan_options.scan_type = passive ? 1 : 0;
|
||||
int err = cyw43_wifi_scan(&cyw43_state, &scan_options, nullptr, &s_wifi_scan_result);
|
||||
@@ -256,9 +244,7 @@ void WiFiComponent::wifi_loop_() {
|
||||
// Handle scan completion
|
||||
if (this->state_ == WIFI_COMPONENT_STATE_STA_SCANNING && !cyw43_wifi_scan_active(&cyw43_state)) {
|
||||
this->scan_done_ = true;
|
||||
bool needs_full = this->needs_full_scan_results_();
|
||||
ESP_LOGV(TAG, "Scan complete: %zu found, %zu stored%s", s_scan_result_count, this->scan_result_.size(),
|
||||
needs_full ? "" : " (filtered)");
|
||||
ESP_LOGV(TAG, "Scan done");
|
||||
#ifdef USE_WIFI_SCAN_RESULTS_LISTENERS
|
||||
for (auto *listener : this->scan_results_listeners_) {
|
||||
listener->on_wifi_scan_results(this->scan_result_);
|
||||
|
||||
@@ -762,15 +762,6 @@ class EsphomeCore:
|
||||
def relative_piolibdeps_path(self, *path: str | Path) -> Path:
|
||||
return self.relative_build_path(".piolibdeps", *path)
|
||||
|
||||
@property
|
||||
def platformio_cache_dir(self) -> str:
|
||||
"""Get the PlatformIO cache directory path."""
|
||||
# Check if running in Docker/HA addon with custom cache dir
|
||||
if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip():
|
||||
return cache_dir
|
||||
# Default PlatformIO cache location
|
||||
return os.path.expanduser("~/.platformio/.cache")
|
||||
|
||||
@property
|
||||
def firmware_bin(self) -> Path:
|
||||
# Check if using native ESP-IDF build (--native-idf)
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include "esphome/core/defines.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/string_ref.h"
|
||||
#include <concepts>
|
||||
#include <functional>
|
||||
#include <utility>
|
||||
@@ -191,55 +190,15 @@ template<typename T, typename... X> class TemplatableValue {
|
||||
/// Get the static string pointer (only valid if is_static_string() returns true)
|
||||
const char *get_static_string() const { return this->static_str_; }
|
||||
|
||||
/// Check if the string value is empty without allocating (for std::string specialization).
|
||||
/// For NONE, returns true. For STATIC_STRING/VALUE, checks without allocation.
|
||||
/// For LAMBDA/STATELESS_LAMBDA, must call value() which may allocate.
|
||||
bool is_empty() const requires std::same_as<T, std::string> {
|
||||
switch (this->type_) {
|
||||
case NONE:
|
||||
return true;
|
||||
case STATIC_STRING:
|
||||
return this->static_str_ == nullptr || this->static_str_[0] == '\0';
|
||||
case VALUE:
|
||||
return this->value_->empty();
|
||||
default: // LAMBDA/STATELESS_LAMBDA - must call value()
|
||||
return this->value().empty();
|
||||
}
|
||||
}
|
||||
protected:
|
||||
enum : uint8_t {
|
||||
NONE,
|
||||
VALUE,
|
||||
LAMBDA,
|
||||
STATELESS_LAMBDA,
|
||||
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
|
||||
} type_;
|
||||
|
||||
/// Get a StringRef to the string value without heap allocation when possible.
|
||||
/// For STATIC_STRING/VALUE, returns reference to existing data (no allocation).
|
||||
/// For LAMBDA/STATELESS_LAMBDA, calls value(), copies to provided buffer, returns ref to buffer.
|
||||
/// @param lambda_buf Buffer used only for lambda case (must remain valid while StringRef is used).
|
||||
/// @param lambda_buf_size Size of the buffer.
|
||||
/// @return StringRef pointing to the string data.
|
||||
StringRef ref_or_copy_to(char *lambda_buf, size_t lambda_buf_size) const requires std::same_as<T, std::string> {
|
||||
switch (this->type_) {
|
||||
case NONE:
|
||||
return StringRef();
|
||||
case STATIC_STRING:
|
||||
if (this->static_str_ == nullptr)
|
||||
return StringRef();
|
||||
return StringRef(this->static_str_, strlen(this->static_str_));
|
||||
case VALUE:
|
||||
return StringRef(this->value_->data(), this->value_->size());
|
||||
default: { // LAMBDA/STATELESS_LAMBDA - must call value() and copy
|
||||
std::string result = this->value();
|
||||
size_t copy_len = std::min(result.size(), lambda_buf_size - 1);
|
||||
memcpy(lambda_buf, result.data(), copy_len);
|
||||
lambda_buf[copy_len] = '\0';
|
||||
return StringRef(lambda_buf, copy_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected : enum : uint8_t {
|
||||
NONE,
|
||||
VALUE,
|
||||
LAMBDA,
|
||||
STATELESS_LAMBDA,
|
||||
STATIC_STRING, // For const char* when T is std::string - avoids heap allocation
|
||||
} type_;
|
||||
// For std::string, use heap pointer to minimize union size (4 bytes vs 12+).
|
||||
// For other types, store value inline as before.
|
||||
using ValueStorage = std::conditional_t<USE_HEAP_STORAGE, T *, T>;
|
||||
|
||||
@@ -347,10 +347,7 @@ std::string format_hex(const uint8_t *data, size_t length) {
|
||||
format_hex_to(&ret[0], length * 2 + 1, data, length);
|
||||
return ret;
|
||||
}
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
std::string format_hex(const std::vector<uint8_t> &data) { return format_hex(data.data(), data.size()); }
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
char *format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator) {
|
||||
return format_hex_internal(buffer, buffer_size, data, length, separator, 'A');
|
||||
@@ -519,8 +516,10 @@ int8_t step_to_accuracy_decimals(float step) {
|
||||
return str.length() - dot_pos - 1;
|
||||
}
|
||||
|
||||
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms
|
||||
static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
|
||||
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
"abcdefghijklmnopqrstuvwxyz"
|
||||
"0123456789+/";
|
||||
|
||||
// Helper function to find the index of a base64/base64url character in the lookup table.
|
||||
// Returns the character's position (0-63) if found, or 0 if not found.
|
||||
|
||||
@@ -132,78 +132,6 @@ template<typename T> class ConstVector {
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
/// Small buffer optimization - stores data inline when small, heap-allocates for large data
|
||||
/// This avoids heap fragmentation for common small allocations while supporting arbitrary sizes.
|
||||
/// Memory management is encapsulated - callers just use set() and data().
|
||||
template<size_t InlineSize = 8> class SmallInlineBuffer {
|
||||
public:
|
||||
SmallInlineBuffer() = default;
|
||||
~SmallInlineBuffer() {
|
||||
if (!this->is_inline_())
|
||||
delete[] this->heap_;
|
||||
}
|
||||
|
||||
// Move constructor
|
||||
SmallInlineBuffer(SmallInlineBuffer &&other) noexcept : len_(other.len_) {
|
||||
if (other.is_inline_()) {
|
||||
memcpy(this->inline_, other.inline_, this->len_);
|
||||
} else {
|
||||
this->heap_ = other.heap_;
|
||||
other.heap_ = nullptr;
|
||||
}
|
||||
other.len_ = 0;
|
||||
}
|
||||
|
||||
// Move assignment
|
||||
SmallInlineBuffer &operator=(SmallInlineBuffer &&other) noexcept {
|
||||
if (this != &other) {
|
||||
if (!this->is_inline_())
|
||||
delete[] this->heap_;
|
||||
this->len_ = other.len_;
|
||||
if (other.is_inline_()) {
|
||||
memcpy(this->inline_, other.inline_, this->len_);
|
||||
} else {
|
||||
this->heap_ = other.heap_;
|
||||
other.heap_ = nullptr;
|
||||
}
|
||||
other.len_ = 0;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
// Disable copy (would need deep copy of heap data)
|
||||
SmallInlineBuffer(const SmallInlineBuffer &) = delete;
|
||||
SmallInlineBuffer &operator=(const SmallInlineBuffer &) = delete;
|
||||
|
||||
/// Set buffer contents, allocating heap if needed
|
||||
void set(const uint8_t *src, size_t size) {
|
||||
// Free existing heap allocation if switching from heap to inline or different heap size
|
||||
if (!this->is_inline_() && (size <= InlineSize || size != this->len_)) {
|
||||
delete[] this->heap_;
|
||||
this->heap_ = nullptr; // Defensive: prevent use-after-free if logic changes
|
||||
}
|
||||
// Allocate new heap buffer if needed
|
||||
if (size > InlineSize && (this->is_inline_() || size != this->len_)) {
|
||||
this->heap_ = new uint8_t[size]; // NOLINT(cppcoreguidelines-owning-memory)
|
||||
}
|
||||
this->len_ = size;
|
||||
memcpy(this->data(), src, size);
|
||||
}
|
||||
|
||||
uint8_t *data() { return this->is_inline_() ? this->inline_ : this->heap_; }
|
||||
const uint8_t *data() const { return this->is_inline_() ? this->inline_ : this->heap_; }
|
||||
size_t size() const { return this->len_; }
|
||||
|
||||
protected:
|
||||
bool is_inline_() const { return this->len_ <= InlineSize; }
|
||||
|
||||
size_t len_{0};
|
||||
union {
|
||||
uint8_t inline_[InlineSize]{}; // Zero-init ensures clean initial state
|
||||
uint8_t *heap_;
|
||||
};
|
||||
};
|
||||
|
||||
/// Minimal static vector - saves memory by avoiding std::vector overhead
|
||||
template<typename T, size_t N> class StaticVector {
|
||||
public:
|
||||
@@ -238,9 +166,6 @@ template<typename T, size_t N> class StaticVector {
|
||||
size_t size() const { return count_; }
|
||||
bool empty() const { return count_ == 0; }
|
||||
|
||||
// Direct access to size counter for efficient in-place construction
|
||||
size_t &count() { return count_; }
|
||||
|
||||
// Direct access to underlying data
|
||||
T *data() { return data_.data(); }
|
||||
const T *data() const { return data_.data(); }
|
||||
@@ -647,10 +572,6 @@ template<typename T> constexpr T convert_little_endian(T val) {
|
||||
bool str_equals_case_insensitive(const std::string &a, const std::string &b);
|
||||
/// Compare StringRefs for equality in case-insensitive manner.
|
||||
bool str_equals_case_insensitive(StringRef a, StringRef b);
|
||||
/// Compare C strings for equality in case-insensitive manner (no heap allocation).
|
||||
inline bool str_equals_case_insensitive(const char *a, const char *b) { return strcasecmp(a, b) == 0; }
|
||||
inline bool str_equals_case_insensitive(const std::string &a, const char *b) { return strcasecmp(a.c_str(), b) == 0; }
|
||||
inline bool str_equals_case_insensitive(const char *a, const std::string &b) { return strcasecmp(a, b.c_str()) == 0; }
|
||||
|
||||
/// Check whether a string starts with a value.
|
||||
bool str_startswith(const std::string &str, const std::string &start);
|
||||
@@ -728,11 +649,9 @@ inline uint32_t fnv1_hash_object_id(const char *str, size_t len) {
|
||||
}
|
||||
|
||||
/// snprintf-like function returning std::string of maximum length \p len (excluding null terminator).
|
||||
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
|
||||
std::string __attribute__((format(printf, 1, 3))) str_snprintf(const char *fmt, size_t len, ...);
|
||||
|
||||
/// sprintf-like function returning std::string.
|
||||
/// @warning Allocates heap memory. Use snprintf() with a stack buffer instead.
|
||||
std::string __attribute__((format(printf, 1, 2))) str_sprintf(const char *fmt, ...);
|
||||
|
||||
#ifdef USE_ESP8266
|
||||
@@ -1105,17 +1024,13 @@ std::string format_hex(const std::vector<uint8_t> &data);
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
template<typename T, enable_if_t<std::is_unsigned<T>::value, int> = 0> std::string format_hex(T val) {
|
||||
val = convert_big_endian(val);
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
|
||||
return format_hex(reinterpret_cast<uint8_t *>(&val), sizeof(T));
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
/// Format the std::array \p data in lowercased hex.
|
||||
/// @warning Allocates heap memory. Use format_hex_to() with a stack buffer instead.
|
||||
/// Causes heap fragmentation on long-running devices.
|
||||
template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &data) {
|
||||
return format_hex(data.data(), data.size());
|
||||
#pragma GCC diagnostic pop
|
||||
}
|
||||
|
||||
/** Format a byte array in pretty-printed, human-readable hex format.
|
||||
|
||||
@@ -118,9 +118,10 @@ class Scheduler {
|
||||
} name_;
|
||||
uint32_t interval;
|
||||
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
|
||||
// with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit
|
||||
// for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter
|
||||
// supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
|
||||
// with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
|
||||
// This is intentionally limited to 48 bits, not stored as a full 64-bit value.
|
||||
// With 49.7 days per 32-bit rollover, the 16-bit counter supports
|
||||
// 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
|
||||
// even when devices run for months. Split into two fields for better memory
|
||||
// alignment on 32-bit systems.
|
||||
uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)
|
||||
|
||||
@@ -171,16 +171,7 @@ def run_compile(config, verbose):
|
||||
args = []
|
||||
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
|
||||
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
|
||||
result = run_platformio_cli_run(config, verbose, *args)
|
||||
|
||||
# Run memory analysis if enabled
|
||||
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
|
||||
try:
|
||||
analyze_memory_usage(config)
|
||||
except Exception as e:
|
||||
_LOGGER.warning("Failed to analyze memory usage: %s", e)
|
||||
|
||||
return result
|
||||
return run_platformio_cli_run(config, verbose, *args)
|
||||
|
||||
|
||||
def _run_idedata(config):
|
||||
@@ -434,74 +425,3 @@ class IDEData:
|
||||
def defines(self) -> list[str]:
|
||||
"""Return the list of preprocessor defines from idedata."""
|
||||
return self.raw.get("defines", [])
|
||||
|
||||
|
||||
def analyze_memory_usage(config: dict[str, Any]) -> None:
|
||||
"""Analyze memory usage by component after compilation."""
|
||||
# Lazy import to avoid overhead when not needed
|
||||
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
|
||||
from esphome.analyze_memory.helpers import get_esphome_components
|
||||
|
||||
idedata = get_idedata(config)
|
||||
|
||||
# Get paths to tools
|
||||
elf_path = idedata.firmware_elf_path
|
||||
objdump_path = idedata.objdump_path
|
||||
readelf_path = idedata.readelf_path
|
||||
|
||||
# Debug logging
|
||||
_LOGGER.debug("ELF path from idedata: %s", elf_path)
|
||||
|
||||
# Check if file exists
|
||||
if not Path(elf_path).exists():
|
||||
# Try alternate path
|
||||
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
|
||||
if alt_path.exists():
|
||||
elf_path = str(alt_path)
|
||||
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
|
||||
else:
|
||||
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
|
||||
return
|
||||
|
||||
# Extract external components from config
|
||||
external_components = set()
|
||||
|
||||
# Get the list of built-in ESPHome components
|
||||
builtin_components = get_esphome_components()
|
||||
|
||||
# Special non-component keys that appear in configs
|
||||
NON_COMPONENT_KEYS = {
|
||||
CONF_ESPHOME,
|
||||
"substitutions",
|
||||
"packages",
|
||||
"globals",
|
||||
"<<",
|
||||
}
|
||||
|
||||
# Check all top-level keys in config
|
||||
for key in config:
|
||||
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
|
||||
# This is an external component
|
||||
external_components.add(key)
|
||||
|
||||
_LOGGER.debug("Detected external components: %s", external_components)
|
||||
|
||||
# Create analyzer and run analysis
|
||||
analyzer = MemoryAnalyzerCLI(
|
||||
elf_path, objdump_path, readelf_path, external_components
|
||||
)
|
||||
analyzer.analyze()
|
||||
|
||||
# Generate and print report
|
||||
report = analyzer.generate_report()
|
||||
_LOGGER.info("\n%s", report)
|
||||
|
||||
# Optionally save to file
|
||||
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
|
||||
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
|
||||
if report_file.suffix == ".json":
|
||||
report_file.write_text(analyzer.to_json())
|
||||
_LOGGER.info("Memory report saved to %s", report_file)
|
||||
else:
|
||||
report_file.write_text(report)
|
||||
_LOGGER.info("Memory report saved to %s", report_file)
|
||||
|
||||
@@ -692,8 +692,6 @@ HEAP_ALLOCATING_HELPERS = {
|
||||
"str_truncate": "removal (function is unused)",
|
||||
"str_upper_case": "removal (function is unused)",
|
||||
"str_snake_case": "removal (function is unused)",
|
||||
"str_sprintf": "snprintf() with a stack buffer",
|
||||
"str_snprintf": "snprintf() with a stack buffer",
|
||||
}
|
||||
|
||||
|
||||
@@ -712,9 +710,7 @@ HEAP_ALLOCATING_HELPERS = {
|
||||
r"str_sanitize(?!_)|"
|
||||
r"str_truncate|"
|
||||
r"str_upper_case|"
|
||||
r"str_snake_case|"
|
||||
r"str_sprintf|"
|
||||
r"str_snprintf"
|
||||
r"str_snake_case"
|
||||
r")\s*\(" + CPP_RE_EOL,
|
||||
include=cpp_include,
|
||||
exclude=[
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
esphome:
|
||||
name: test-user-services-union
|
||||
friendly_name: Test User Services Union Storage
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
wifi:
|
||||
ssid: "test"
|
||||
password: "password"
|
||||
|
||||
api:
|
||||
actions:
|
||||
# Test service with no arguments
|
||||
- action: test_no_args
|
||||
then:
|
||||
- logger.log: "No args service called"
|
||||
|
||||
# Test service with one argument
|
||||
- action: test_one_arg
|
||||
variables:
|
||||
value: int
|
||||
then:
|
||||
- logger.log:
|
||||
format: "One arg service: %d"
|
||||
args: [value]
|
||||
|
||||
# Test service with multiple arguments of different types
|
||||
- action: test_multi_args
|
||||
variables:
|
||||
int_val: int
|
||||
float_val: float
|
||||
str_val: string
|
||||
bool_val: bool
|
||||
then:
|
||||
- logger.log:
|
||||
format: "Multi args: %d, %.2f, %s, %d"
|
||||
args: [int_val, float_val, str_val.c_str(), bool_val]
|
||||
|
||||
# Test service with max typical arguments
|
||||
- action: test_many_args
|
||||
variables:
|
||||
arg1: int
|
||||
arg2: int
|
||||
arg3: int
|
||||
arg4: string
|
||||
arg5: float
|
||||
then:
|
||||
- logger.log: "Many args service called"
|
||||
|
||||
binary_sensor:
|
||||
- platform: template
|
||||
name: "Test Binary Sensor"
|
||||
id: test_sensor
|
||||
@@ -453,7 +453,6 @@ def test_clean_build(
|
||||
mock_core.relative_pioenvs_path.return_value = pioenvs_dir
|
||||
mock_core.relative_piolibdeps_path.return_value = piolibdeps_dir
|
||||
mock_core.relative_build_path.return_value = dependencies_lock
|
||||
mock_core.platformio_cache_dir = str(platformio_cache_dir)
|
||||
|
||||
# Verify all exist before
|
||||
assert pioenvs_dir.exists()
|
||||
|
||||
Reference in New Issue
Block a user