mirror of
https://github.com/esphome/esphome.git
synced 2026-01-21 02:19:10 -07:00
Compare commits
1 Commits
dev
...
esp8266_pr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
613e7eb902 |
@@ -1 +1 @@
|
||||
d15ae81646ac0ee76b2586716fe697f187281523ee6db566aed26542a9f98d1a
|
||||
d272a88e8ca28ae9340a9a03295a566432a52cb696501908f57764475bf7ca65
|
||||
|
||||
@@ -43,7 +43,6 @@ from esphome.const import (
|
||||
CONF_SUBSTITUTIONS,
|
||||
CONF_TOPIC,
|
||||
ENV_NOGITIGNORE,
|
||||
KEY_NATIVE_IDF,
|
||||
PLATFORM_ESP32,
|
||||
PLATFORM_ESP8266,
|
||||
PLATFORM_RP2040,
|
||||
@@ -117,7 +116,6 @@ class ArgsProtocol(Protocol):
|
||||
configuration: str
|
||||
name: str
|
||||
upload_speed: str | None
|
||||
native_idf: bool
|
||||
|
||||
|
||||
def choose_prompt(options, purpose: str = None):
|
||||
@@ -502,15 +500,12 @@ def wrap_to_code(name, comp):
|
||||
return wrapped
|
||||
|
||||
|
||||
def write_cpp(config: ConfigType, native_idf: bool = False) -> int:
|
||||
def write_cpp(config: ConfigType) -> int:
|
||||
if not get_bool_env(ENV_NOGITIGNORE):
|
||||
writer.write_gitignore()
|
||||
|
||||
# Store native_idf flag so esp32 component can check it
|
||||
CORE.data[KEY_NATIVE_IDF] = native_idf
|
||||
|
||||
generate_cpp_contents(config)
|
||||
return write_cpp_file(native_idf=native_idf)
|
||||
return write_cpp_file()
|
||||
|
||||
|
||||
def generate_cpp_contents(config: ConfigType) -> None:
|
||||
@@ -524,54 +519,32 @@ def generate_cpp_contents(config: ConfigType) -> None:
|
||||
CORE.flush_tasks()
|
||||
|
||||
|
||||
def write_cpp_file(native_idf: bool = False) -> int:
|
||||
def write_cpp_file() -> int:
|
||||
code_s = indent(CORE.cpp_main_section)
|
||||
writer.write_cpp(code_s)
|
||||
|
||||
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
|
||||
from esphome.build_gen import espidf
|
||||
from esphome.build_gen import platformio
|
||||
|
||||
espidf.write_project()
|
||||
else:
|
||||
from esphome.build_gen import platformio
|
||||
|
||||
platformio.write_project()
|
||||
platformio.write_project()
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
|
||||
native_idf = getattr(args, "native_idf", False)
|
||||
from esphome import platformio_api
|
||||
|
||||
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
|
||||
# If you change this format, update the regex in that script as well
|
||||
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
|
||||
|
||||
if native_idf and CORE.is_esp32 and CORE.target_framework == "esp-idf":
|
||||
from esphome import espidf_api
|
||||
|
||||
rc = espidf_api.run_compile(config, CORE.verbose)
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
# Create factory.bin and ota.bin
|
||||
espidf_api.create_factory_bin()
|
||||
espidf_api.create_ota_bin()
|
||||
else:
|
||||
from esphome import platformio_api
|
||||
|
||||
rc = platformio_api.run_compile(config, CORE.verbose)
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
if idedata is None:
|
||||
return 1
|
||||
rc = platformio_api.run_compile(config, CORE.verbose)
|
||||
if rc != 0:
|
||||
return rc
|
||||
|
||||
# Check if firmware was rebuilt and emit build_info + create manifest
|
||||
_check_and_emit_build_info()
|
||||
|
||||
return 0
|
||||
idedata = platformio_api.get_idedata(config)
|
||||
return 0 if idedata is not None else 1
|
||||
|
||||
|
||||
def _check_and_emit_build_info() -> None:
|
||||
@@ -828,8 +801,7 @@ def command_vscode(args: ArgsProtocol) -> int | None:
|
||||
|
||||
|
||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
native_idf = getattr(args, "native_idf", False)
|
||||
exit_code = write_cpp(config, native_idf=native_idf)
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
if args.only_generate:
|
||||
@@ -884,8 +856,7 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
|
||||
|
||||
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
|
||||
native_idf = getattr(args, "native_idf", False)
|
||||
exit_code = write_cpp(config, native_idf=native_idf)
|
||||
exit_code = write_cpp(config)
|
||||
if exit_code != 0:
|
||||
return exit_code
|
||||
exit_code = compile_program(args, config)
|
||||
@@ -1339,11 +1310,6 @@ def parse_args(argv):
|
||||
help="Only generate source code, do not compile.",
|
||||
action="store_true",
|
||||
)
|
||||
parser_compile.add_argument(
|
||||
"--native-idf",
|
||||
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
parser_upload = subparsers.add_parser(
|
||||
"upload",
|
||||
@@ -1425,11 +1391,6 @@ def parse_args(argv):
|
||||
help="Reset the device before starting serial logs.",
|
||||
default=os.getenv("ESPHOME_SERIAL_LOGGING_RESET"),
|
||||
)
|
||||
parser_run.add_argument(
|
||||
"--native-idf",
|
||||
help="Build with native ESP-IDF instead of PlatformIO (ESP32 esp-idf framework only).",
|
||||
action="store_true",
|
||||
)
|
||||
|
||||
parser_clean = subparsers.add_parser(
|
||||
"clean-mqtt",
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
"""ESP-IDF direct build generator for ESPHome."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from esphome.components.esp32 import get_esp32_variant
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import mkdir_p, write_file_if_changed
|
||||
|
||||
|
||||
def get_available_components() -> list[str] | None:
|
||||
"""Get list of available ESP-IDF components from project_description.json.
|
||||
|
||||
Returns only internal ESP-IDF components, excluding external/managed
|
||||
components (from idf_component.yml).
|
||||
"""
|
||||
project_desc = Path(CORE.build_path) / "build" / "project_description.json"
|
||||
if not project_desc.exists():
|
||||
return None
|
||||
|
||||
try:
|
||||
with open(project_desc, encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
component_info = data.get("build_component_info", {})
|
||||
|
||||
result = []
|
||||
for name, info in component_info.items():
|
||||
# Exclude our own src component
|
||||
if name == "src":
|
||||
continue
|
||||
|
||||
# Exclude managed/external components
|
||||
comp_dir = info.get("dir", "")
|
||||
if "managed_components" in comp_dir:
|
||||
continue
|
||||
|
||||
result.append(name)
|
||||
|
||||
return result
|
||||
except (json.JSONDecodeError, OSError):
|
||||
return None
|
||||
|
||||
|
||||
def has_discovered_components() -> bool:
|
||||
"""Check if we have discovered components from a previous configure."""
|
||||
return get_available_components() is not None
|
||||
|
||||
|
||||
def get_project_cmakelists() -> str:
|
||||
"""Generate the top-level CMakeLists.txt for ESP-IDF project."""
|
||||
# Get IDF target from ESP32 variant (e.g., ESP32S3 -> esp32s3)
|
||||
variant = get_esp32_variant()
|
||||
idf_target = variant.lower().replace("-", "")
|
||||
|
||||
return f"""\
|
||||
# Auto-generated by ESPHome
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
set(IDF_TARGET {idf_target})
|
||||
set(EXTRA_COMPONENT_DIRS ${{CMAKE_SOURCE_DIR}}/src)
|
||||
|
||||
include($ENV{{IDF_PATH}}/tools/cmake/project.cmake)
|
||||
project({CORE.name})
|
||||
"""
|
||||
|
||||
|
||||
def get_component_cmakelists(minimal: bool = False) -> str:
|
||||
"""Generate the main component CMakeLists.txt."""
|
||||
idf_requires = [] if minimal else (get_available_components() or [])
|
||||
requires_str = " ".join(idf_requires)
|
||||
|
||||
# Extract compile definitions from build flags (-DXXX -> XXX)
|
||||
compile_defs = [flag[2:] for flag in CORE.build_flags if flag.startswith("-D")]
|
||||
compile_defs_str = "\n ".join(compile_defs) if compile_defs else ""
|
||||
|
||||
# Extract compile options (-W flags, excluding linker flags)
|
||||
compile_opts = [
|
||||
flag
|
||||
for flag in CORE.build_flags
|
||||
if flag.startswith("-W") and not flag.startswith("-Wl,")
|
||||
]
|
||||
compile_opts_str = "\n ".join(compile_opts) if compile_opts else ""
|
||||
|
||||
# Extract linker options (-Wl, flags)
|
||||
link_opts = [flag for flag in CORE.build_flags if flag.startswith("-Wl,")]
|
||||
link_opts_str = "\n ".join(link_opts) if link_opts else ""
|
||||
|
||||
return f"""\
|
||||
# Auto-generated by ESPHome
|
||||
file(GLOB_RECURSE app_sources
|
||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.cpp"
|
||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/*.c"
|
||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.cpp"
|
||||
"${{CMAKE_CURRENT_SOURCE_DIR}}/esphome/*.c"
|
||||
)
|
||||
|
||||
idf_component_register(
|
||||
SRCS ${{app_sources}}
|
||||
INCLUDE_DIRS "." "esphome"
|
||||
REQUIRES {requires_str}
|
||||
)
|
||||
|
||||
# Apply C++ standard
|
||||
target_compile_features(${{COMPONENT_LIB}} PUBLIC cxx_std_20)
|
||||
|
||||
# ESPHome compile definitions
|
||||
target_compile_definitions(${{COMPONENT_LIB}} PUBLIC
|
||||
{compile_defs_str}
|
||||
)
|
||||
|
||||
# ESPHome compile options
|
||||
target_compile_options(${{COMPONENT_LIB}} PUBLIC
|
||||
{compile_opts_str}
|
||||
)
|
||||
|
||||
# ESPHome linker options
|
||||
target_link_options(${{COMPONENT_LIB}} PUBLIC
|
||||
{link_opts_str}
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
def write_project(minimal: bool = False) -> None:
|
||||
"""Write ESP-IDF project files."""
|
||||
mkdir_p(CORE.build_path)
|
||||
mkdir_p(CORE.relative_src_path())
|
||||
|
||||
# Write top-level CMakeLists.txt
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("CMakeLists.txt"),
|
||||
get_project_cmakelists(),
|
||||
)
|
||||
|
||||
# Write component CMakeLists.txt in src/
|
||||
write_file_if_changed(
|
||||
CORE.relative_src_path("CMakeLists.txt"),
|
||||
get_component_cmakelists(minimal=minimal),
|
||||
)
|
||||
@@ -3,7 +3,6 @@
|
||||
#ifdef USE_API_NOISE
|
||||
#include "api_connection.h" // For ClientInfo struct
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/entity_base.h"
|
||||
#include "esphome/core/hal.h"
|
||||
#include "esphome/core/helpers.h"
|
||||
#include "esphome/core/log.h"
|
||||
@@ -257,30 +256,28 @@ APIError APINoiseFrameHelper::state_action_() {
|
||||
}
|
||||
if (state_ == State::SERVER_HELLO) {
|
||||
// send server hello
|
||||
constexpr size_t mac_len = 13; // 12 hex chars + null terminator
|
||||
const std::string &name = App.get_name();
|
||||
char mac[MAC_ADDRESS_BUFFER_SIZE];
|
||||
char mac[mac_len];
|
||||
get_mac_address_into_buffer(mac);
|
||||
|
||||
// Calculate positions and sizes
|
||||
size_t name_len = name.size() + 1; // including null terminator
|
||||
size_t name_offset = 1;
|
||||
size_t mac_offset = name_offset + name_len;
|
||||
size_t total_size = 1 + name_len + MAC_ADDRESS_BUFFER_SIZE;
|
||||
size_t total_size = 1 + name_len + mac_len;
|
||||
|
||||
// 1 (proto) + name (max ESPHOME_DEVICE_NAME_MAX_LEN) + 1 (name null)
|
||||
// + mac (MAC_ADDRESS_BUFFER_SIZE - 1) + 1 (mac null)
|
||||
constexpr size_t max_msg_size = 1 + ESPHOME_DEVICE_NAME_MAX_LEN + 1 + MAC_ADDRESS_BUFFER_SIZE;
|
||||
uint8_t msg[max_msg_size];
|
||||
auto msg = std::make_unique<uint8_t[]>(total_size);
|
||||
|
||||
// chosen proto
|
||||
msg[0] = 0x01;
|
||||
|
||||
// node name, terminated by null byte
|
||||
std::memcpy(msg + name_offset, name.c_str(), name_len);
|
||||
std::memcpy(msg.get() + name_offset, name.c_str(), name_len);
|
||||
// node mac, terminated by null byte
|
||||
std::memcpy(msg + mac_offset, mac, MAC_ADDRESS_BUFFER_SIZE);
|
||||
std::memcpy(msg.get() + mac_offset, mac, mac_len);
|
||||
|
||||
aerr = write_frame_(msg, total_size);
|
||||
aerr = write_frame_(msg.get(), total_size);
|
||||
if (aerr != APIError::OK)
|
||||
return aerr;
|
||||
|
||||
@@ -356,32 +353,35 @@ APIError APINoiseFrameHelper::state_action_() {
|
||||
return APIError::OK;
|
||||
}
|
||||
void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reason) {
|
||||
// Max reject message: "Bad handshake packet len" (24) + 1 (failure byte) = 25 bytes
|
||||
uint8_t data[32];
|
||||
data[0] = 0x01; // failure
|
||||
|
||||
#ifdef USE_STORE_LOG_STR_IN_FLASH
|
||||
// On ESP8266 with flash strings, we need to use PROGMEM-aware functions
|
||||
size_t reason_len = strlen_P(reinterpret_cast<PGM_P>(reason));
|
||||
size_t data_size = reason_len + 1;
|
||||
auto data = std::make_unique<uint8_t[]>(data_size);
|
||||
data[0] = 0x01; // failure
|
||||
|
||||
// Copy error message from PROGMEM
|
||||
if (reason_len > 0) {
|
||||
memcpy_P(data + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||
memcpy_P(data.get() + 1, reinterpret_cast<PGM_P>(reason), reason_len);
|
||||
}
|
||||
#else
|
||||
// Normal memory access
|
||||
const char *reason_str = LOG_STR_ARG(reason);
|
||||
size_t reason_len = strlen(reason_str);
|
||||
size_t data_size = reason_len + 1;
|
||||
auto data = std::make_unique<uint8_t[]>(data_size);
|
||||
data[0] = 0x01; // failure
|
||||
|
||||
// Copy error message in bulk
|
||||
if (reason_len > 0) {
|
||||
// NOLINTNEXTLINE(bugprone-not-null-terminated-result) - binary protocol, not a C string
|
||||
std::memcpy(data + 1, reason_str, reason_len);
|
||||
std::memcpy(data.get() + 1, reason_str, reason_len);
|
||||
}
|
||||
#endif
|
||||
|
||||
size_t data_size = reason_len + 1;
|
||||
|
||||
// temporarily remove failed state
|
||||
auto orig_state = state_;
|
||||
state_ = State::EXPLICIT_REJECT;
|
||||
write_frame_(data, data_size);
|
||||
write_frame_(data.get(), data_size);
|
||||
state_ = orig_state;
|
||||
}
|
||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
|
||||
|
||||
@@ -135,8 +135,8 @@ void BluetoothConnection::loop() {
|
||||
// - For V3_WITH_CACHE: Services are never sent, disable after INIT state
|
||||
// - For V3_WITHOUT_CACHE: Disable only after service discovery is complete
|
||||
// (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
|
||||
if (this->state() != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->send_service_ == DONE_SENDING_SERVICES)) {
|
||||
if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
|
||||
this->send_service_ == DONE_SENDING_SERVICES)) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,13 +152,6 @@ void CC1101Component::setup() {
|
||||
}
|
||||
}
|
||||
|
||||
void CC1101Component::call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) {
|
||||
for (auto &listener : this->listeners_) {
|
||||
listener->on_packet(packet, freq_offset, rssi, lqi);
|
||||
}
|
||||
this->packet_trigger_->trigger(packet, freq_offset, rssi, lqi);
|
||||
}
|
||||
|
||||
void CC1101Component::loop() {
|
||||
if (this->state_.PKT_FORMAT != static_cast<uint8_t>(PacketFormat::PACKET_FORMAT_FIFO) || this->gdo0_pin_ == nullptr ||
|
||||
!this->gdo0_pin_->digital_read()) {
|
||||
@@ -205,7 +198,7 @@ void CC1101Component::loop() {
|
||||
bool crc_ok = (this->state_.LQI & STATUS_CRC_OK_MASK) != 0;
|
||||
uint8_t lqi = this->state_.LQI & STATUS_LQI_MASK;
|
||||
if (this->state_.CRC_EN == 0 || crc_ok) {
|
||||
this->call_listeners_(this->packet_, freq_offset, rssi, lqi);
|
||||
this->packet_trigger_->trigger(this->packet_, freq_offset, rssi, lqi);
|
||||
}
|
||||
|
||||
// Return to rx
|
||||
|
||||
@@ -11,11 +11,6 @@ namespace esphome::cc1101 {
|
||||
|
||||
enum class CC1101Error { NONE = 0, TIMEOUT, PARAMS, CRC_ERROR, FIFO_OVERFLOW, PLL_LOCK };
|
||||
|
||||
class CC1101Listener {
|
||||
public:
|
||||
virtual void on_packet(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi) = 0;
|
||||
};
|
||||
|
||||
class CC1101Component : public Component,
|
||||
public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
|
||||
spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_1MHZ> {
|
||||
@@ -78,7 +73,6 @@ class CC1101Component : public Component,
|
||||
|
||||
// Packet mode operations
|
||||
CC1101Error transmit_packet(const std::vector<uint8_t> &packet);
|
||||
void register_listener(CC1101Listener *listener) { this->listeners_.push_back(listener); }
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *get_packet_trigger() const { return this->packet_trigger_; }
|
||||
|
||||
protected:
|
||||
@@ -95,11 +89,9 @@ class CC1101Component : public Component,
|
||||
InternalGPIOPin *gdo0_pin_{nullptr};
|
||||
|
||||
// Packet handling
|
||||
void call_listeners_(const std::vector<uint8_t> &packet, float freq_offset, float rssi, uint8_t lqi);
|
||||
Trigger<std::vector<uint8_t>, float, float, uint8_t> *packet_trigger_{
|
||||
new Trigger<std::vector<uint8_t>, float, float, uint8_t>()};
|
||||
std::vector<uint8_t> packet_;
|
||||
std::vector<CC1101Listener *> listeners_;
|
||||
|
||||
// Low-level Helpers
|
||||
uint8_t strobe_(Command cmd);
|
||||
|
||||
@@ -106,9 +106,9 @@ DateCall &DateCall::set_date(uint16_t year, uint8_t month, uint8_t day) {
|
||||
|
||||
DateCall &DateCall::set_date(ESPTime time) { return this->set_date(time.year, time.month, time.day_of_month); };
|
||||
|
||||
DateCall &DateCall::set_date(const char *date, size_t len) {
|
||||
DateCall &DateCall::set_date(const std::string &date) {
|
||||
ESPTime val{};
|
||||
if (!ESPTime::strptime(date, len, val)) {
|
||||
if (!ESPTime::strptime(date, val)) {
|
||||
ESP_LOGE(TAG, "Could not convert the date string to an ESPTime object");
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -67,9 +67,7 @@ class DateCall {
|
||||
void perform();
|
||||
DateCall &set_date(uint16_t year, uint8_t month, uint8_t day);
|
||||
DateCall &set_date(ESPTime time);
|
||||
DateCall &set_date(const char *date, size_t len);
|
||||
DateCall &set_date(const char *date) { return this->set_date(date, strlen(date)); }
|
||||
DateCall &set_date(const std::string &date) { return this->set_date(date.c_str(), date.size()); }
|
||||
DateCall &set_date(const std::string &date);
|
||||
|
||||
DateCall &set_year(uint16_t year) {
|
||||
this->year_ = year;
|
||||
|
||||
@@ -163,9 +163,9 @@ DateTimeCall &DateTimeCall::set_datetime(ESPTime datetime) {
|
||||
datetime.second);
|
||||
};
|
||||
|
||||
DateTimeCall &DateTimeCall::set_datetime(const char *datetime, size_t len) {
|
||||
DateTimeCall &DateTimeCall::set_datetime(const std::string &datetime) {
|
||||
ESPTime val{};
|
||||
if (!ESPTime::strptime(datetime, len, val)) {
|
||||
if (!ESPTime::strptime(datetime, val)) {
|
||||
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -71,11 +71,7 @@ class DateTimeCall {
|
||||
void perform();
|
||||
DateTimeCall &set_datetime(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, uint8_t second);
|
||||
DateTimeCall &set_datetime(ESPTime datetime);
|
||||
DateTimeCall &set_datetime(const char *datetime, size_t len);
|
||||
DateTimeCall &set_datetime(const char *datetime) { return this->set_datetime(datetime, strlen(datetime)); }
|
||||
DateTimeCall &set_datetime(const std::string &datetime) {
|
||||
return this->set_datetime(datetime.c_str(), datetime.size());
|
||||
}
|
||||
DateTimeCall &set_datetime(const std::string &datetime);
|
||||
DateTimeCall &set_datetime(time_t epoch_seconds);
|
||||
|
||||
DateTimeCall &set_year(uint16_t year) {
|
||||
|
||||
@@ -74,9 +74,9 @@ TimeCall &TimeCall::set_time(uint8_t hour, uint8_t minute, uint8_t second) {
|
||||
|
||||
TimeCall &TimeCall::set_time(ESPTime time) { return this->set_time(time.hour, time.minute, time.second); };
|
||||
|
||||
TimeCall &TimeCall::set_time(const char *time, size_t len) {
|
||||
TimeCall &TimeCall::set_time(const std::string &time) {
|
||||
ESPTime val{};
|
||||
if (!ESPTime::strptime(time, len, val)) {
|
||||
if (!ESPTime::strptime(time, val)) {
|
||||
ESP_LOGE(TAG, "Could not convert the time string to an ESPTime object");
|
||||
return *this;
|
||||
}
|
||||
|
||||
@@ -69,9 +69,7 @@ class TimeCall {
|
||||
void perform();
|
||||
TimeCall &set_time(uint8_t hour, uint8_t minute, uint8_t second);
|
||||
TimeCall &set_time(ESPTime time);
|
||||
TimeCall &set_time(const char *time, size_t len);
|
||||
TimeCall &set_time(const char *time) { return this->set_time(time, strlen(time)); }
|
||||
TimeCall &set_time(const std::string &time) { return this->set_time(time.c_str(), time.size()); }
|
||||
TimeCall &set_time(const std::string &time);
|
||||
|
||||
TimeCall &set_hour(uint8_t hour) {
|
||||
this->hour_ = hour;
|
||||
|
||||
@@ -3,80 +3,21 @@
|
||||
#include "esphome/core/log.h"
|
||||
#include <Esp.h>
|
||||
|
||||
extern "C" {
|
||||
#include <user_interface.h>
|
||||
|
||||
// Global reset info struct populated by SDK at boot
|
||||
extern struct rst_info resetInfo;
|
||||
|
||||
// Core version - either a string pointer or a version number to format as hex
|
||||
extern uint32_t core_version;
|
||||
extern const char *core_release;
|
||||
}
|
||||
|
||||
namespace esphome {
|
||||
namespace debug {
|
||||
|
||||
static const char *const TAG = "debug";
|
||||
|
||||
// Get reset reason string from reason code (no heap allocation)
|
||||
// Returns LogString* pointing to flash (PROGMEM) on ESP8266
|
||||
static const LogString *get_reset_reason_str(uint32_t reason) {
|
||||
switch (reason) {
|
||||
case REASON_DEFAULT_RST:
|
||||
return LOG_STR("Power On");
|
||||
case REASON_WDT_RST:
|
||||
return LOG_STR("Hardware Watchdog");
|
||||
case REASON_EXCEPTION_RST:
|
||||
return LOG_STR("Exception");
|
||||
case REASON_SOFT_WDT_RST:
|
||||
return LOG_STR("Software Watchdog");
|
||||
case REASON_SOFT_RESTART:
|
||||
return LOG_STR("Software/System restart");
|
||||
case REASON_DEEP_SLEEP_AWAKE:
|
||||
return LOG_STR("Deep-Sleep Wake");
|
||||
case REASON_EXT_SYS_RST:
|
||||
return LOG_STR("External System");
|
||||
default:
|
||||
return LOG_STR("Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// Size for core version hex buffer
|
||||
static constexpr size_t CORE_VERSION_BUFFER_SIZE = 12;
|
||||
|
||||
// Get core version string (no heap allocation)
|
||||
// Returns either core_release directly or formats core_version as hex into provided buffer
|
||||
static const char *get_core_version_str(std::span<char, CORE_VERSION_BUFFER_SIZE> buffer) {
|
||||
if (core_release != nullptr) {
|
||||
return core_release;
|
||||
}
|
||||
snprintf_P(buffer.data(), CORE_VERSION_BUFFER_SIZE, PSTR("%08x"), core_version);
|
||||
return buffer.data();
|
||||
}
|
||||
|
||||
// Size for reset info buffer
|
||||
static constexpr size_t RESET_INFO_BUFFER_SIZE = 200;
|
||||
|
||||
// Get detailed reset info string (no heap allocation)
|
||||
// For watchdog/exception resets, includes detailed exception info
|
||||
static const char *get_reset_info_str(std::span<char, RESET_INFO_BUFFER_SIZE> buffer, uint32_t reason) {
|
||||
if (reason >= REASON_WDT_RST && reason <= REASON_SOFT_WDT_RST) {
|
||||
snprintf_P(buffer.data(), RESET_INFO_BUFFER_SIZE,
|
||||
PSTR("Fatal exception:%d flag:%d (%s) epc1:0x%08x epc2:0x%08x epc3:0x%08x excvaddr:0x%08x depc:0x%08x"),
|
||||
static_cast<int>(resetInfo.exccause), static_cast<int>(reason),
|
||||
LOG_STR_ARG(get_reset_reason_str(reason)), resetInfo.epc1, resetInfo.epc2, resetInfo.epc3,
|
||||
resetInfo.excvaddr, resetInfo.depc);
|
||||
return buffer.data();
|
||||
}
|
||||
return LOG_STR_ARG(get_reset_reason_str(reason));
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
// Copy from flash to provided buffer
|
||||
strncpy_P(buffer.data(), (PGM_P) get_reset_reason_str(resetInfo.reason), RESET_REASON_BUFFER_SIZE - 1);
|
||||
buffer[RESET_REASON_BUFFER_SIZE - 1] = '\0';
|
||||
return buffer.data();
|
||||
char *buf = buffer.data();
|
||||
#if !defined(CLANG_TIDY)
|
||||
String reason = ESP.getResetReason(); // NOLINT
|
||||
snprintf_P(buf, RESET_REASON_BUFFER_SIZE, PSTR("%s"), reason.c_str());
|
||||
return buf;
|
||||
#else
|
||||
buf[0] = '\0';
|
||||
return buf;
|
||||
#endif
|
||||
}
|
||||
|
||||
const char *DebugComponent::get_wakeup_cause_(std::span<char, RESET_REASON_BUFFER_SIZE> buffer) {
|
||||
@@ -92,42 +33,37 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
constexpr size_t size = DEVICE_INFO_BUFFER_SIZE;
|
||||
char *buf = buffer.data();
|
||||
|
||||
const LogString *flash_mode;
|
||||
const char *flash_mode;
|
||||
switch (ESP.getFlashChipMode()) { // NOLINT(readability-static-accessed-through-instance)
|
||||
case FM_QIO:
|
||||
flash_mode = LOG_STR("QIO");
|
||||
flash_mode = "QIO";
|
||||
break;
|
||||
case FM_QOUT:
|
||||
flash_mode = LOG_STR("QOUT");
|
||||
flash_mode = "QOUT";
|
||||
break;
|
||||
case FM_DIO:
|
||||
flash_mode = LOG_STR("DIO");
|
||||
flash_mode = "DIO";
|
||||
break;
|
||||
case FM_DOUT:
|
||||
flash_mode = LOG_STR("DOUT");
|
||||
flash_mode = "DOUT";
|
||||
break;
|
||||
default:
|
||||
flash_mode = LOG_STR("UNKNOWN");
|
||||
flash_mode = "UNKNOWN";
|
||||
}
|
||||
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT(readability-static-accessed-through-instance)
|
||||
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT(readability-static-accessed-through-instance)
|
||||
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed,
|
||||
LOG_STR_ARG(flash_mode));
|
||||
uint32_t flash_size = ESP.getFlashChipSize() / 1024; // NOLINT
|
||||
uint32_t flash_speed = ESP.getFlashChipSpeed() / 1000000; // NOLINT
|
||||
ESP_LOGD(TAG, "Flash Chip: Size=%" PRIu32 "kB Speed=%" PRIu32 "MHz Mode=%s", flash_size, flash_speed, flash_mode);
|
||||
pos = buf_append_printf(buf, size, pos, "|Flash: %" PRIu32 "kB Speed:%" PRIu32 "MHz Mode:%s", flash_size, flash_speed,
|
||||
LOG_STR_ARG(flash_mode));
|
||||
flash_mode);
|
||||
|
||||
#if !defined(CLANG_TIDY)
|
||||
char reason_buffer[RESET_REASON_BUFFER_SIZE];
|
||||
const char *reset_reason = get_reset_reason_(reason_buffer);
|
||||
char core_version_buffer[CORE_VERSION_BUFFER_SIZE];
|
||||
char reset_info_buffer[RESET_INFO_BUFFER_SIZE];
|
||||
// NOLINTBEGIN(readability-static-accessed-through-instance)
|
||||
const char *reset_reason = get_reset_reason_(std::span<char, RESET_REASON_BUFFER_SIZE>(reason_buffer));
|
||||
uint32_t chip_id = ESP.getChipId();
|
||||
uint8_t boot_version = ESP.getBootVersion();
|
||||
uint8_t boot_mode = ESP.getBootMode();
|
||||
uint8_t cpu_freq = ESP.getCpuFreqMHz();
|
||||
uint32_t flash_chip_id = ESP.getFlashChipId();
|
||||
const char *sdk_version = ESP.getSdkVersion();
|
||||
// NOLINTEND(readability-static-accessed-through-instance)
|
||||
|
||||
ESP_LOGD(TAG,
|
||||
"Chip ID: 0x%08" PRIX32 "\n"
|
||||
@@ -138,18 +74,19 @@ size_t DebugComponent::get_device_info_(std::span<char, DEVICE_INFO_BUFFER_SIZE>
|
||||
"Flash Chip ID=0x%08" PRIX32 "\n"
|
||||
"Reset Reason: %s\n"
|
||||
"Reset Info: %s",
|
||||
chip_id, sdk_version, get_core_version_str(core_version_buffer), boot_version, boot_mode, cpu_freq,
|
||||
flash_chip_id, reset_reason, get_reset_info_str(reset_info_buffer, resetInfo.reason));
|
||||
chip_id, ESP.getSdkVersion(), ESP.getCoreVersion().c_str(), boot_version, boot_mode, cpu_freq, flash_chip_id,
|
||||
reset_reason, ESP.getResetInfo().c_str());
|
||||
|
||||
pos = buf_append_printf(buf, size, pos, "|Chip: 0x%08" PRIX32, chip_id);
|
||||
pos = buf_append_printf(buf, size, pos, "|SDK: %s", sdk_version);
|
||||
pos = buf_append_printf(buf, size, pos, "|Core: %s", get_core_version_str(core_version_buffer));
|
||||
pos = buf_append_printf(buf, size, pos, "|SDK: %s", ESP.getSdkVersion());
|
||||
pos = buf_append_printf(buf, size, pos, "|Core: %s", ESP.getCoreVersion().c_str());
|
||||
pos = buf_append_printf(buf, size, pos, "|Boot: %u", boot_version);
|
||||
pos = buf_append_printf(buf, size, pos, "|Mode: %u", boot_mode);
|
||||
pos = buf_append_printf(buf, size, pos, "|CPU: %u", cpu_freq);
|
||||
pos = buf_append_printf(buf, size, pos, "|Flash: 0x%08" PRIX32, flash_chip_id);
|
||||
pos = buf_append_printf(buf, size, pos, "|Reset: %s", reset_reason);
|
||||
pos = buf_append_printf(buf, size, pos, "|%s", get_reset_info_str(reset_info_buffer, resetInfo.reason));
|
||||
pos = buf_append_printf(buf, size, pos, "|%s", ESP.getResetInfo().c_str());
|
||||
#endif
|
||||
|
||||
return pos;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,6 @@ from esphome.const import (
|
||||
KEY_CORE,
|
||||
KEY_FRAMEWORK_VERSION,
|
||||
KEY_NAME,
|
||||
KEY_NATIVE_IDF,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_ESP32,
|
||||
@@ -54,7 +53,6 @@ from .const import ( # noqa
|
||||
KEY_COMPONENTS,
|
||||
KEY_ESP32,
|
||||
KEY_EXTRA_BUILD_FILES,
|
||||
KEY_FLASH_SIZE,
|
||||
KEY_PATH,
|
||||
KEY_REF,
|
||||
KEY_REPO,
|
||||
@@ -201,7 +199,6 @@ def set_core_data(config):
|
||||
)
|
||||
|
||||
CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD]
|
||||
CORE.data[KEY_ESP32][KEY_FLASH_SIZE] = config[CONF_FLASH_SIZE]
|
||||
CORE.data[KEY_ESP32][KEY_VARIANT] = variant
|
||||
CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {}
|
||||
|
||||
@@ -965,54 +962,12 @@ async def _add_yaml_idf_components(components: list[ConfigType]):
|
||||
|
||||
|
||||
async def to_code(config):
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
|
||||
# Check if using native ESP-IDF build (--native-idf)
|
||||
use_platformio = not CORE.data.get(KEY_NATIVE_IDF, False)
|
||||
if use_platformio:
|
||||
# Clear IDF environment variables to avoid conflicts with PlatformIO's ESP-IDF
|
||||
# but keep them when using --native-idf for native ESP-IDF builds
|
||||
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
|
||||
os.environ.pop(clean_var, None)
|
||||
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
||||
cg.add_platformio_option(
|
||||
"board_upload.maximum_size",
|
||||
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
|
||||
)
|
||||
|
||||
if CONF_SOURCE in conf:
|
||||
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
||||
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"pre_build.py",
|
||||
Path(__file__).parent / "pre_build.py.script",
|
||||
)
|
||||
|
||||
add_extra_script(
|
||||
"post",
|
||||
"post_build.py",
|
||||
Path(__file__).parent / "post_build.py.script",
|
||||
)
|
||||
|
||||
# In testing mode, add IRAM fix script to allow linking grouped component tests
|
||||
# Similar to ESP8266's approach but for ESP-IDF
|
||||
if CORE.testing_mode:
|
||||
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"iram_fix.py",
|
||||
Path(__file__).parent / "iram_fix.py.script",
|
||||
)
|
||||
else:
|
||||
cg.add_build_flag("-Wno-error=format")
|
||||
|
||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||
cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
|
||||
cg.add_platformio_option(
|
||||
"board_upload.maximum_size",
|
||||
int(config[CONF_FLASH_SIZE].removesuffix("MB")) * 1024 * 1024,
|
||||
)
|
||||
cg.set_cpp_standard("gnu++20")
|
||||
cg.add_build_flag("-DUSE_ESP32")
|
||||
cg.add_build_flag("-Wl,-z,noexecstack")
|
||||
@@ -1022,49 +977,79 @@ async def to_code(config):
|
||||
cg.add_define("ESPHOME_VARIANT", VARIANT_FRIENDLY[variant])
|
||||
cg.add_define(ThreadModel.MULTI_ATOMICS)
|
||||
|
||||
cg.add_platformio_option("lib_ldf_mode", "off")
|
||||
cg.add_platformio_option("lib_compat_mode", "strict")
|
||||
|
||||
framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
|
||||
|
||||
conf = config[CONF_FRAMEWORK]
|
||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||
if CONF_SOURCE in conf:
|
||||
cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
|
||||
|
||||
if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
|
||||
cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
|
||||
|
||||
for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
|
||||
os.environ.pop(clean_var, None)
|
||||
|
||||
# Set the location of the IDF component manager cache
|
||||
os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
|
||||
CORE.relative_internal_path(".espressif")
|
||||
)
|
||||
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"pre_build.py",
|
||||
Path(__file__).parent / "pre_build.py.script",
|
||||
)
|
||||
|
||||
add_extra_script(
|
||||
"post",
|
||||
"post_build.py",
|
||||
Path(__file__).parent / "post_build.py.script",
|
||||
)
|
||||
|
||||
# In testing mode, add IRAM fix script to allow linking grouped component tests
|
||||
# Similar to ESP8266's approach but for ESP-IDF
|
||||
if CORE.testing_mode:
|
||||
cg.add_build_flag("-DESPHOME_TESTING_MODE")
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"iram_fix.py",
|
||||
Path(__file__).parent / "iram_fix.py.script",
|
||||
)
|
||||
|
||||
if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
|
||||
cg.add_platformio_option("framework", "espidf")
|
||||
cg.add_build_flag("-DUSE_ESP_IDF")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ESP_IDF")
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("framework", "espidf")
|
||||
else:
|
||||
cg.add_platformio_option("framework", "arduino, espidf")
|
||||
cg.add_build_flag("-DUSE_ARDUINO")
|
||||
cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO")
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("framework", "arduino, espidf")
|
||||
|
||||
# Add IDF framework source for Arduino builds to ensure it uses the same version as
|
||||
# the ESP-IDF framework
|
||||
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||
cg.add_platformio_option(
|
||||
"platform_packages",
|
||||
[_format_framework_espidf_version(idf_ver, None)],
|
||||
)
|
||||
|
||||
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
|
||||
if get_esp32_variant() == VARIANT_ESP32S2:
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
|
||||
cg.add_define(
|
||||
"USE_ARDUINO_VERSION_CODE",
|
||||
cg.RawExpression(
|
||||
f"VERSION_CODE({framework_ver.major}, {framework_ver.minor}, {framework_ver.patch})"
|
||||
),
|
||||
)
|
||||
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
|
||||
add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
|
||||
|
||||
# Add IDF framework source for Arduino builds to ensure it uses the same version as
|
||||
# the ESP-IDF framework
|
||||
if (idf_ver := ARDUINO_IDF_VERSION_LOOKUP.get(framework_ver)) is not None:
|
||||
cg.add_platformio_option(
|
||||
"platform_packages", [_format_framework_espidf_version(idf_ver, None)]
|
||||
)
|
||||
|
||||
# ESP32-S2 Arduino: Disable USB Serial on boot to avoid TinyUSB dependency
|
||||
if get_esp32_variant() == VARIANT_ESP32S2:
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=1")
|
||||
cg.add_build_unflag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=0")
|
||||
|
||||
cg.add_build_flag("-Wno-nonnull-compare")
|
||||
|
||||
add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
|
||||
@@ -1211,8 +1196,7 @@ async def to_code(config):
|
||||
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
|
||||
)
|
||||
|
||||
if use_platformio:
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
cg.add_platformio_option("board_build.partitions", "partitions.csv")
|
||||
if CONF_PARTITIONS in config:
|
||||
add_extra_build_file(
|
||||
"partitions.csv", CORE.relative_config_path(config[CONF_PARTITIONS])
|
||||
@@ -1377,16 +1361,19 @@ def copy_files():
|
||||
_write_idf_component_yml()
|
||||
|
||||
if "partitions.csv" not in CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES]:
|
||||
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
|
||||
if CORE.using_arduino:
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
get_arduino_partition_csv(flash_size),
|
||||
get_arduino_partition_csv(
|
||||
CORE.platformio_options.get("board_upload.flash_size")
|
||||
),
|
||||
)
|
||||
else:
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("partitions.csv"),
|
||||
get_idf_partition_csv(flash_size),
|
||||
get_idf_partition_csv(
|
||||
CORE.platformio_options.get("board_upload.flash_size")
|
||||
),
|
||||
)
|
||||
# IDF build scripts look for version string to put in the build.
|
||||
# However, if the build path does not have an initialized git repo,
|
||||
|
||||
@@ -2,7 +2,6 @@ import esphome.codegen as cg
|
||||
|
||||
KEY_ESP32 = "esp32"
|
||||
KEY_BOARD = "board"
|
||||
KEY_FLASH_SIZE = "flash_size"
|
||||
KEY_VARIANT = "variant"
|
||||
KEY_SDKCONFIG_OPTIONS = "sdkconfig_options"
|
||||
KEY_COMPONENTS = "components"
|
||||
|
||||
@@ -181,8 +181,7 @@ class ESP32Preferences : public ESPPreferences {
|
||||
if (actual_len != to_save.len) {
|
||||
return true;
|
||||
}
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
SmallBufferWithHeapFallback<256> stored_data(actual_len);
|
||||
auto stored_data = std::make_unique<uint8_t[]>(actual_len);
|
||||
err = nvs_get_blob(nvs_handle, key_str, stored_data.get(), &actual_len);
|
||||
if (err != 0) {
|
||||
ESP_LOGV(TAG, "nvs_get_blob('%s') failed: %s", key_str, esp_err_to_name(err));
|
||||
|
||||
@@ -50,7 +50,7 @@ void BLEClientBase::loop() {
|
||||
this->set_state(espbt::ClientState::INIT);
|
||||
return;
|
||||
}
|
||||
if (this->state() == espbt::ClientState::INIT) {
|
||||
if (this->state_ == espbt::ClientState::INIT) {
|
||||
auto ret = esp_ble_gattc_app_register(this->app_id);
|
||||
if (ret) {
|
||||
ESP_LOGE(TAG, "gattc app register failed. app_id=%d code=%d", this->app_id, ret);
|
||||
@@ -60,7 +60,7 @@ void BLEClientBase::loop() {
|
||||
}
|
||||
// If idle, we can disable the loop as connect()
|
||||
// will enable it again when a connection is needed.
|
||||
else if (this->state() == espbt::ClientState::IDLE) {
|
||||
else if (this->state_ == espbt::ClientState::IDLE) {
|
||||
this->disable_loop();
|
||||
}
|
||||
}
|
||||
@@ -86,7 +86,7 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
||||
return false;
|
||||
if (this->address_ == 0 || device.address_uint64() != this->address_)
|
||||
return false;
|
||||
if (this->state() != espbt::ClientState::IDLE)
|
||||
if (this->state_ != espbt::ClientState::IDLE)
|
||||
return false;
|
||||
|
||||
this->log_event_("Found device");
|
||||
@@ -102,10 +102,10 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
|
||||
|
||||
void BLEClientBase::connect() {
|
||||
// Prevent duplicate connection attempts
|
||||
if (this->state() == espbt::ClientState::CONNECTING || this->state() == espbt::ClientState::CONNECTED ||
|
||||
this->state() == espbt::ClientState::ESTABLISHED) {
|
||||
if (this->state_ == espbt::ClientState::CONNECTING || this->state_ == espbt::ClientState::CONNECTED ||
|
||||
this->state_ == espbt::ClientState::ESTABLISHED) {
|
||||
ESP_LOGW(TAG, "[%d] [%s] Connection already in progress, state=%s", this->connection_index_, this->address_str_,
|
||||
espbt::client_state_to_string(this->state()));
|
||||
espbt::client_state_to_string(this->state_));
|
||||
return;
|
||||
}
|
||||
ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_, this->remote_addr_type_);
|
||||
@@ -133,12 +133,12 @@ void BLEClientBase::connect() {
|
||||
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
|
||||
|
||||
void BLEClientBase::disconnect() {
|
||||
if (this->state() == espbt::ClientState::IDLE || this->state() == espbt::ClientState::DISCONNECTING) {
|
||||
if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
|
||||
ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_,
|
||||
espbt::client_state_to_string(this->state()));
|
||||
espbt::client_state_to_string(this->state_));
|
||||
return;
|
||||
}
|
||||
if (this->state() == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
|
||||
if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
|
||||
ESP_LOGD(TAG, "[%d] [%s] Disconnect before connected, disconnect scheduled", this->connection_index_,
|
||||
this->address_str_);
|
||||
this->want_disconnect_ = true;
|
||||
@@ -150,7 +150,7 @@ void BLEClientBase::disconnect() {
|
||||
void BLEClientBase::unconditional_disconnect() {
|
||||
// Disconnect without checking the state.
|
||||
ESP_LOGI(TAG, "[%d] [%s] Disconnecting (conn_id: %d).", this->connection_index_, this->address_str_, this->conn_id_);
|
||||
if (this->state() == espbt::ClientState::DISCONNECTING) {
|
||||
if (this->state_ == espbt::ClientState::DISCONNECTING) {
|
||||
this->log_error_("Already disconnecting");
|
||||
return;
|
||||
}
|
||||
@@ -170,7 +170,7 @@ void BLEClientBase::unconditional_disconnect() {
|
||||
this->log_gattc_warning_("esp_ble_gattc_close", err);
|
||||
}
|
||||
|
||||
if (this->state() == espbt::ClientState::DISCOVERED) {
|
||||
if (this->state_ == espbt::ClientState::DISCOVERED) {
|
||||
this->set_address(0);
|
||||
this->set_state(espbt::ClientState::IDLE);
|
||||
} else {
|
||||
@@ -295,18 +295,18 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
// ESP-IDF's BLE stack may send ESP_GATTC_OPEN_EVT after esp_ble_gattc_open() returns an
|
||||
// error, if the error occurred at the BTA/GATT layer. This can result in the event
|
||||
// arriving after we've already transitioned to IDLE state.
|
||||
if (this->state() == espbt::ClientState::IDLE) {
|
||||
if (this->state_ == espbt::ClientState::IDLE) {
|
||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in IDLE state (status=%d), ignoring", this->connection_index_,
|
||||
this->address_str_, param->open.status);
|
||||
break;
|
||||
}
|
||||
|
||||
if (this->state() != espbt::ClientState::CONNECTING) {
|
||||
if (this->state_ != espbt::ClientState::CONNECTING) {
|
||||
// This should not happen but lets log it in case it does
|
||||
// because it means we have a bad assumption about how the
|
||||
// ESP BT stack works.
|
||||
ESP_LOGE(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT in %s state (status=%d)", this->connection_index_,
|
||||
this->address_str_, espbt::client_state_to_string(this->state()), param->open.status);
|
||||
this->address_str_, espbt::client_state_to_string(this->state_), param->open.status);
|
||||
}
|
||||
if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
|
||||
this->log_gattc_warning_("Connection open", param->open.status);
|
||||
@@ -327,7 +327,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
|
||||
// Cached connections already connected with medium parameters, no update needed
|
||||
// only set our state, subclients might have more stuff to do yet.
|
||||
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
// For V3_WITHOUT_CACHE, we already set fast params before connecting
|
||||
@@ -356,7 +356,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
return false;
|
||||
// Check if we were disconnected while waiting for service discovery
|
||||
if (param->disconnect.reason == ESP_GATT_CONN_TERMINATE_PEER_USER &&
|
||||
this->state() == espbt::ClientState::CONNECTED) {
|
||||
this->state_ == espbt::ClientState::CONNECTED) {
|
||||
this->log_warning_("Remote closed during discovery");
|
||||
} else {
|
||||
ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason 0x%02x", this->connection_index_, this->address_str_,
|
||||
@@ -433,7 +433,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
|
||||
#endif
|
||||
}
|
||||
ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_);
|
||||
this->set_state_internal_(espbt::ClientState::ESTABLISHED);
|
||||
this->state_ = espbt::ClientState::ESTABLISHED;
|
||||
break;
|
||||
}
|
||||
case ESP_GATTC_READ_DESCR_EVT: {
|
||||
|
||||
@@ -44,7 +44,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
|
||||
void unconditional_disconnect();
|
||||
void release_services();
|
||||
|
||||
bool connected() { return this->state() == espbt::ClientState::ESTABLISHED; }
|
||||
bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
|
||||
|
||||
void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
|
||||
|
||||
|
||||
@@ -105,13 +105,15 @@ void ESP32BLETracker::loop() {
|
||||
}
|
||||
|
||||
// Check for scan timeout - moved here from scheduler to avoid false reboots
|
||||
// when the loop is blocked. This must run every iteration for safety.
|
||||
// when the loop is blocked
|
||||
if (this->scanner_state_ == ScannerState::RUNNING) {
|
||||
switch (this->scan_timeout_state_) {
|
||||
case ScanTimeoutState::MONITORING: {
|
||||
uint32_t now = App.get_loop_component_start_time();
|
||||
uint32_t timeout_ms = this->scan_duration_ * 2000;
|
||||
// Robust time comparison that handles rollover correctly
|
||||
// This works because unsigned arithmetic wraps around predictably
|
||||
if ((App.get_loop_component_start_time() - this->scan_start_time_) > this->scan_timeout_ms_) {
|
||||
if ((now - this->scan_start_time_) > timeout_ms) {
|
||||
// First time we've seen the timeout exceeded - wait one more loop iteration
|
||||
// This ensures all components have had a chance to process pending events
|
||||
// This is because esp32_ble may not have run yet and called
|
||||
@@ -126,31 +128,13 @@ void ESP32BLETracker::loop() {
|
||||
ESP_LOGE(TAG, "Scan never terminated, rebooting");
|
||||
App.reboot();
|
||||
break;
|
||||
|
||||
case ScanTimeoutState::INACTIVE:
|
||||
// This case should be unreachable - scanner and timeout states are always synchronized
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Fast path: skip expensive client state counting and processing
|
||||
// if no state has changed since last loop iteration.
|
||||
//
|
||||
// How state changes ensure we reach the code below:
|
||||
// - handle_scanner_failure_(): scanner_state_ becomes FAILED via set_scanner_state_(), or
|
||||
// scan_set_param_failed_ requires scanner_state_==RUNNING which can only be reached via
|
||||
// set_scanner_state_(RUNNING) in gap_scan_start_complete_() (scan params are set during
|
||||
// STARTING, not RUNNING, so version is always incremented before this condition is true)
|
||||
// - start_scan_(): scanner_state_ becomes IDLE via set_scanner_state_() in cleanup_scan_state_()
|
||||
// - try_promote_discovered_clients_(): client enters DISCOVERED via set_state(), or
|
||||
// connecting client finishes (state change), or scanner reaches RUNNING/IDLE
|
||||
//
|
||||
// All conditions that affect the logic below are tied to state changes that increment
|
||||
// state_version_, so the fast path is safe.
|
||||
if (this->state_version_ == this->last_processed_version_) {
|
||||
return;
|
||||
}
|
||||
this->last_processed_version_ = this->state_version_;
|
||||
|
||||
// State changed - do full processing
|
||||
ClientStateCounts counts = this->count_client_states_();
|
||||
if (counts != this->client_state_counts_) {
|
||||
this->client_state_counts_ = counts;
|
||||
@@ -158,7 +142,6 @@ void ESP32BLETracker::loop() {
|
||||
this->client_state_counts_.discovered, this->client_state_counts_.disconnecting);
|
||||
}
|
||||
|
||||
// Scanner failure: reached when set_scanner_state_(FAILED) or scan_set_param_failed_ set
|
||||
if (this->scanner_state_ == ScannerState::FAILED ||
|
||||
(this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
|
||||
this->handle_scanner_failure_();
|
||||
@@ -177,8 +160,6 @@ void ESP32BLETracker::loop() {
|
||||
|
||||
*/
|
||||
|
||||
// Start scan: reached when scanner_state_ becomes IDLE (via set_scanner_state_()) and
|
||||
// all clients are idle (their state changes increment version when they finish)
|
||||
if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting && !counts.discovered) {
|
||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
|
||||
this->update_coex_preference_(false);
|
||||
@@ -187,9 +168,8 @@ void ESP32BLETracker::loop() {
|
||||
this->start_scan_(false); // first = false
|
||||
}
|
||||
}
|
||||
// Promote discovered clients: reached when a client's state becomes DISCOVERED (via set_state()),
|
||||
// or when a blocking condition clears (connecting client finishes, scanner reaches RUNNING/IDLE).
|
||||
// All these trigger state_version_ increment, so we'll process and check promotion eligibility.
|
||||
// If there is a discovered client and no connecting
|
||||
// clients, then promote the discovered client to ready to connect.
|
||||
// We check both RUNNING and IDLE states because:
|
||||
// - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
|
||||
// - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
|
||||
@@ -256,7 +236,6 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
// Start timeout monitoring in loop() instead of using scheduler
|
||||
// This prevents false reboots when the loop is blocked
|
||||
this->scan_start_time_ = App.get_loop_component_start_time();
|
||||
this->scan_timeout_ms_ = this->scan_duration_ * 2000;
|
||||
this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
|
||||
|
||||
esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
|
||||
@@ -274,10 +253,6 @@ void ESP32BLETracker::start_scan_(bool first) {
|
||||
void ESP32BLETracker::register_client(ESPBTClient *client) {
|
||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
|
||||
client->app_id = ++this->app_id_;
|
||||
// Give client a pointer to our state_version_ so it can notify us of state changes.
|
||||
// This enables loop() fast-path optimization - we skip expensive work when no state changed.
|
||||
// Safe because ESP32BLETracker (singleton) outlives all registered clients.
|
||||
client->set_tracker_state_version(&this->state_version_);
|
||||
this->clients_.push_back(client);
|
||||
this->recalculate_advertisement_parser_types();
|
||||
#endif
|
||||
@@ -407,7 +382,6 @@ void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
|
||||
|
||||
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
|
||||
this->scanner_state_ = state;
|
||||
this->state_version_++;
|
||||
for (auto *listener : this->scanner_state_listeners_) {
|
||||
listener->on_scanner_state(state);
|
||||
}
|
||||
|
||||
@@ -216,19 +216,6 @@ enum class ConnectionType : uint8_t {
|
||||
V3_WITHOUT_CACHE
|
||||
};
|
||||
|
||||
/// Base class for BLE GATT clients that connect to remote devices.
|
||||
///
|
||||
/// State Change Tracking Design:
|
||||
/// -----------------------------
|
||||
/// ESP32BLETracker::loop() needs to know when client states change to avoid
|
||||
/// expensive polling. Rather than checking all clients every iteration (~7000/min),
|
||||
/// we use a version counter owned by ESP32BLETracker that clients increment on
|
||||
/// state changes. The tracker compares versions to skip work when nothing changed.
|
||||
///
|
||||
/// Ownership: ESP32BLETracker owns state_version_. Clients hold a non-owning
|
||||
/// pointer (tracker_state_version_) set during register_client(). Clients
|
||||
/// increment the counter through this pointer when their state changes.
|
||||
/// The pointer may be null if the client is not registered with a tracker.
|
||||
class ESPBTClient : public ESPBTDeviceListener {
|
||||
public:
|
||||
virtual bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
|
||||
@@ -238,49 +225,26 @@ class ESPBTClient : public ESPBTDeviceListener {
|
||||
virtual void disconnect() = 0;
|
||||
bool disconnect_pending() const { return this->want_disconnect_; }
|
||||
void cancel_pending_disconnect() { this->want_disconnect_ = false; }
|
||||
|
||||
/// Set the client state with IDLE handling (clears want_disconnect_).
|
||||
/// Notifies the tracker of state change for loop optimization.
|
||||
virtual void set_state(ClientState st) {
|
||||
this->set_state_internal_(st);
|
||||
this->state_ = st;
|
||||
if (st == ClientState::IDLE) {
|
||||
this->want_disconnect_ = false;
|
||||
}
|
||||
}
|
||||
ClientState state() const { return this->state_; }
|
||||
|
||||
/// Called by ESP32BLETracker::register_client() to enable state change notifications.
|
||||
/// The pointer must remain valid for the lifetime of the client (guaranteed since
|
||||
/// ESP32BLETracker is a singleton that outlives all clients).
|
||||
void set_tracker_state_version(uint8_t *version) { this->tracker_state_version_ = version; }
|
||||
ClientState state() const { return state_; }
|
||||
|
||||
// Memory optimized layout
|
||||
uint8_t app_id; // App IDs are small integers assigned sequentially
|
||||
|
||||
protected:
|
||||
/// Set state without IDLE handling - use for direct state transitions.
|
||||
/// Increments the tracker's state version counter to signal that loop()
|
||||
/// should do full processing on the next iteration.
|
||||
void set_state_internal_(ClientState st) {
|
||||
this->state_ = st;
|
||||
// Notify tracker that state changed (tracker_state_version_ is owned by ESP32BLETracker)
|
||||
if (this->tracker_state_version_ != nullptr) {
|
||||
(*this->tracker_state_version_)++;
|
||||
}
|
||||
}
|
||||
|
||||
// Group 1: 1-byte types
|
||||
ClientState state_{ClientState::INIT};
|
||||
// want_disconnect_ is set to true when a disconnect is requested
|
||||
// while the client is connecting. This is used to disconnect the
|
||||
// client as soon as we get the connection id (conn_id_) from the
|
||||
// ESP_GATTC_OPEN_EVT event.
|
||||
bool want_disconnect_{false};
|
||||
|
||||
private:
|
||||
ClientState state_{ClientState::INIT};
|
||||
/// Non-owning pointer to ESP32BLETracker::state_version_. When this client's
|
||||
/// state changes, we increment the tracker's counter to signal that loop()
|
||||
/// should perform full processing. Null if client not registered with tracker.
|
||||
uint8_t *tracker_state_version_{nullptr};
|
||||
// 2 bytes used, 2 bytes padding
|
||||
};
|
||||
|
||||
class ESP32BLETracker : public Component,
|
||||
@@ -416,16 +380,6 @@ class ESP32BLETracker : public Component,
|
||||
// Group 4: 1-byte types (enums, uint8_t, bool)
|
||||
uint8_t app_id_{0};
|
||||
uint8_t scan_start_fail_count_{0};
|
||||
/// Version counter for loop() fast-path optimization. Incremented when:
|
||||
/// - Scanner state changes (via set_scanner_state_())
|
||||
/// - Any registered client's state changes (clients hold pointer to this counter)
|
||||
/// Owned by this class; clients receive non-owning pointer via register_client().
|
||||
/// When loop() sees state_version_ == last_processed_version_, it skips expensive
|
||||
/// client state counting and takes the fast path (just timeout check + return).
|
||||
uint8_t state_version_{0};
|
||||
/// Last state_version_ value when loop() did full processing. Compared against
|
||||
/// state_version_ to detect if any state changed since last iteration.
|
||||
uint8_t last_processed_version_{0};
|
||||
ScannerState scanner_state_{ScannerState::IDLE};
|
||||
bool scan_continuous_;
|
||||
bool scan_active_;
|
||||
@@ -442,8 +396,6 @@ class ESP32BLETracker : public Component,
|
||||
EXCEEDED_WAIT, // Timeout exceeded, waiting one loop before reboot
|
||||
};
|
||||
uint32_t scan_start_time_{0};
|
||||
/// Precomputed timeout value: scan_duration_ * 2000
|
||||
uint32_t scan_timeout_ms_{0};
|
||||
ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ extern "C" {
|
||||
#include "preferences.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
|
||||
namespace esphome::esp8266 {
|
||||
|
||||
@@ -143,16 +142,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
return false;
|
||||
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
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();
|
||||
}
|
||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
||||
uint32_t *buffer = buffer_alloc.get();
|
||||
memset(buffer, 0, buffer_size * sizeof(uint32_t));
|
||||
|
||||
memcpy(buffer, data, len);
|
||||
@@ -167,16 +158,8 @@ class ESP8266PreferenceBackend : public ESPPreferenceBackend {
|
||||
return false;
|
||||
|
||||
const size_t buffer_size = static_cast<size_t>(this->length_words) + 1;
|
||||
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();
|
||||
}
|
||||
SmallBufferWithHeapFallback<PREF_BUFFER_WORDS, uint32_t> buffer_alloc(buffer_size);
|
||||
uint32_t *buffer = buffer_alloc.get();
|
||||
|
||||
bool ret = this->in_flash ? load_from_flash(this->offset, buffer, buffer_size)
|
||||
: load_from_rtc(this->offset, buffer, buffer_size);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core import CoroPriority, coroutine_with_priority
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
json_ns = cg.esphome_ns.namespace("json")
|
||||
@@ -12,11 +12,6 @@ CONFIG_SCHEMA = cv.All(
|
||||
|
||||
@coroutine_with_priority(CoroPriority.BUS)
|
||||
async def to_code(config):
|
||||
if CORE.is_esp32:
|
||||
from esphome.components.esp32 import add_idf_component
|
||||
|
||||
add_idf_component(name="bblanchon/arduinojson", ref="7.4.2")
|
||||
else:
|
||||
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
|
||||
cg.add_library("bblanchon/ArduinoJson", "7.4.2")
|
||||
cg.add_define("USE_JSON")
|
||||
cg.add_global(json_ns.using)
|
||||
|
||||
@@ -382,11 +382,4 @@ async def component_to_code(config):
|
||||
"custom_options.sys_config#h", _BK7231N_SYS_CONFIG_OPTIONS
|
||||
)
|
||||
|
||||
# Disable LWIP statistics to save RAM - not needed in production
|
||||
# Must explicitly disable all sub-stats to avoid redefinition warnings
|
||||
cg.add_platformio_option(
|
||||
"custom_options.lwip",
|
||||
["LWIP_STATS=0", "MEM_STATS=0", "MEMP_STATS=0"],
|
||||
)
|
||||
|
||||
await cg.register_component(var, config)
|
||||
|
||||
@@ -166,8 +166,8 @@ class LibreTinyPreferences : public ESPPreferences {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Most preferences are small, use stack buffer with heap fallback for large ones
|
||||
SmallBufferWithHeapFallback<256> stored_data(kv.value_len);
|
||||
// Allocate buffer on heap to avoid stack allocation for large data
|
||||
auto stored_data = std::make_unique<uint8_t[]>(kv.value_len);
|
||||
fdb_blob_make(&this->blob, stored_data.get(), kv.value_len);
|
||||
size_t actual_len = fdb_kv_get_blob(db, key_str, &this->blob);
|
||||
if (actual_len != kv.value_len) {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "logger.h"
|
||||
#include <cinttypes>
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#include <memory> // For unique_ptr
|
||||
#endif
|
||||
|
||||
#include "esphome/core/application.h"
|
||||
#include "esphome/core/hal.h"
|
||||
@@ -196,8 +199,7 @@ inline uint8_t Logger::level_for(const char *tag) {
|
||||
|
||||
Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) {
|
||||
// add 1 to buffer size for null terminator
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];
|
||||
this->tx_buffer_ = new char[this->tx_buffer_size_ + 1]; // NOLINT
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
this->main_task_ = xTaskGetCurrentTaskHandle();
|
||||
#elif defined(USE_ZEPHYR)
|
||||
@@ -210,14 +212,11 @@ Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate
|
||||
void Logger::init_log_buffer(size_t total_buffer_size) {
|
||||
#ifdef USE_HOST
|
||||
// Host uses slot count instead of byte size
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->log_buffer_ = new logger::TaskLogBufferHost(total_buffer_size);
|
||||
this->log_buffer_ = esphome::make_unique<logger::TaskLogBufferHost>(total_buffer_size);
|
||||
#elif defined(USE_ESP32)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->log_buffer_ = new logger::TaskLogBuffer(total_buffer_size);
|
||||
this->log_buffer_ = esphome::make_unique<logger::TaskLogBuffer>(total_buffer_size);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-owning-memory) - allocated once, never freed
|
||||
this->log_buffer_ = new logger::TaskLogBufferLibreTiny(total_buffer_size);
|
||||
this->log_buffer_ = esphome::make_unique<logger::TaskLogBufferLibreTiny>(total_buffer_size);
|
||||
#endif
|
||||
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
|
||||
@@ -412,11 +412,11 @@ class Logger : public Component {
|
||||
#endif
|
||||
#ifdef USE_ESPHOME_TASK_LOG_BUFFER
|
||||
#ifdef USE_HOST
|
||||
logger::TaskLogBufferHost *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
std::unique_ptr<logger::TaskLogBufferHost> log_buffer_; // Will be initialized with init_log_buffer
|
||||
#elif defined(USE_ESP32)
|
||||
logger::TaskLogBuffer *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
std::unique_ptr<logger::TaskLogBuffer> log_buffer_; // Will be initialized with init_log_buffer
|
||||
#elif defined(USE_LIBRETINY)
|
||||
logger::TaskLogBufferLibreTiny *log_buffer_{nullptr}; // Allocated once, never freed
|
||||
std::unique_ptr<logger::TaskLogBufferLibreTiny> log_buffer_; // Will be initialized with init_log_buffer
|
||||
#endif
|
||||
#endif
|
||||
|
||||
|
||||
@@ -24,14 +24,13 @@ static void register_esp32(MDNSComponent *comp, StaticVector<MDNSService, MDNS_S
|
||||
mdns_instance_name_set(hostname);
|
||||
|
||||
for (const auto &service : services) {
|
||||
// Stack buffer for up to 16 txt records, heap fallback for more
|
||||
SmallBufferWithHeapFallback<16, mdns_txt_item_t> txt_records(service.txt_records.size());
|
||||
auto txt_records = std::make_unique<mdns_txt_item_t[]>(service.txt_records.size());
|
||||
for (size_t i = 0; i < service.txt_records.size(); i++) {
|
||||
const auto &record = service.txt_records[i];
|
||||
// key and value are either compile-time string literals in flash or pointers to dynamic_txt_values_
|
||||
// Both remain valid for the lifetime of this function, and ESP-IDF makes internal copies
|
||||
txt_records.get()[i].key = MDNS_STR_ARG(record.key);
|
||||
txt_records.get()[i].value = MDNS_STR_ARG(record.value);
|
||||
txt_records[i].key = MDNS_STR_ARG(record.key);
|
||||
txt_records[i].value = MDNS_STR_ARG(record.value);
|
||||
}
|
||||
uint16_t port = const_cast<TemplatableValue<uint16_t> &>(service.port).value();
|
||||
err = mdns_service_add(nullptr, MDNS_STR_ARG(service.service_type), MDNS_STR_ARG(service.proto), port,
|
||||
|
||||
@@ -9,7 +9,6 @@ from esphome.const import (
|
||||
CONF_ABOVE,
|
||||
CONF_ACCURACY_DECIMALS,
|
||||
CONF_ALPHA,
|
||||
CONF_BASELINE,
|
||||
CONF_BELOW,
|
||||
CONF_CALIBRATION,
|
||||
CONF_DEVICE_CLASS,
|
||||
@@ -39,6 +38,7 @@ from esphome.const import (
|
||||
CONF_TIMEOUT,
|
||||
CONF_TO,
|
||||
CONF_TRIGGER_ID,
|
||||
CONF_TYPE,
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
CONF_VALUE,
|
||||
CONF_WEB_SERVER,
|
||||
@@ -107,7 +107,7 @@ from esphome.const import (
|
||||
)
|
||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||
from esphome.core.entity_helpers import entity_duplicate_validator, setup_entity
|
||||
from esphome.cpp_generator import MockObj, MockObjClass
|
||||
from esphome.cpp_generator import MockObjClass
|
||||
from esphome.util import Registry
|
||||
|
||||
CODEOWNERS = ["@esphome/core"]
|
||||
@@ -574,56 +574,38 @@ async def lambda_filter_to_code(config, filter_id):
|
||||
return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
|
||||
|
||||
|
||||
def validate_delta_value(value):
|
||||
if isinstance(value, str) and value.endswith("%"):
|
||||
# Check it's a well-formed percentage, but return the string as-is
|
||||
try:
|
||||
cv.positive_float(value[:-1])
|
||||
return value
|
||||
except cv.Invalid as exc:
|
||||
raise cv.Invalid("Malformed delta % value") from exc
|
||||
return cv.positive_float(value)
|
||||
|
||||
|
||||
# This ideally would be done with `cv.maybe_simple_value` but it doesn't seem to respect the default for min_value.
|
||||
DELTA_SCHEMA = cv.Any(
|
||||
cv.All(
|
||||
{
|
||||
# Ideally this would be 'default=float("inf")' but it doesn't translate well to C++
|
||||
cv.Optional(CONF_MAX_VALUE): validate_delta_value,
|
||||
cv.Optional(CONF_MIN_VALUE, default="0.0"): validate_delta_value,
|
||||
cv.Optional(CONF_BASELINE): cv.templatable(cv.float_),
|
||||
},
|
||||
cv.has_at_least_one_key(CONF_MAX_VALUE, CONF_MIN_VALUE),
|
||||
),
|
||||
validate_delta_value,
|
||||
DELTA_SCHEMA = cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_VALUE): cv.positive_float,
|
||||
cv.Optional(CONF_TYPE, default="absolute"): cv.one_of(
|
||||
"absolute", "percentage", lower=True
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _get_delta(value):
|
||||
if isinstance(value, str):
|
||||
assert value.endswith("%")
|
||||
return 0.0, float(value[:-1])
|
||||
return value, 0.0
|
||||
def validate_delta(config):
|
||||
try:
|
||||
value = cv.positive_float(config)
|
||||
return DELTA_SCHEMA({CONF_VALUE: value, CONF_TYPE: "absolute"})
|
||||
except cv.Invalid:
|
||||
pass
|
||||
try:
|
||||
value = cv.percentage(config)
|
||||
return DELTA_SCHEMA({CONF_VALUE: value, CONF_TYPE: "percentage"})
|
||||
except cv.Invalid:
|
||||
pass
|
||||
raise cv.Invalid("Delta filter requires a positive number or percentage value.")
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("delta", DeltaFilter, DELTA_SCHEMA)
|
||||
@FILTER_REGISTRY.register("delta", DeltaFilter, cv.Any(DELTA_SCHEMA, validate_delta))
|
||||
async def delta_filter_to_code(config, filter_id):
|
||||
# The config could be just the min_value, or it could be a dict.
|
||||
max = MockObj("std::numeric_limits<float>::infinity()"), 0
|
||||
if isinstance(config, dict):
|
||||
min = _get_delta(config[CONF_MIN_VALUE])
|
||||
if CONF_MAX_VALUE in config:
|
||||
max = _get_delta(config[CONF_MAX_VALUE])
|
||||
else:
|
||||
min = _get_delta(config)
|
||||
var = cg.new_Pvariable(filter_id, *min, *max)
|
||||
if isinstance(config, dict) and (baseline_lambda := config.get(CONF_BASELINE)):
|
||||
baseline = await cg.process_lambda(
|
||||
baseline_lambda, [(float, "x")], return_type=float
|
||||
)
|
||||
cg.add(var.set_baseline(baseline))
|
||||
return var
|
||||
percentage = config[CONF_TYPE] == "percentage"
|
||||
return cg.new_Pvariable(
|
||||
filter_id,
|
||||
config[CONF_VALUE],
|
||||
percentage,
|
||||
)
|
||||
|
||||
|
||||
@FILTER_REGISTRY.register("or", OrFilter, validate_filters)
|
||||
|
||||
@@ -291,27 +291,22 @@ optional<float> ThrottleWithPriorityFilter::new_value(float value) {
|
||||
}
|
||||
|
||||
// DeltaFilter
|
||||
DeltaFilter::DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1)
|
||||
: min_a0_(min_a0), min_a1_(min_a1), max_a0_(max_a0), max_a1_(max_a1) {}
|
||||
|
||||
void DeltaFilter::set_baseline(float (*fn)(float)) { this->baseline_ = fn; }
|
||||
|
||||
DeltaFilter::DeltaFilter(float delta, bool percentage_mode)
|
||||
: delta_(delta), current_delta_(delta), last_value_(NAN), percentage_mode_(percentage_mode) {}
|
||||
optional<float> DeltaFilter::new_value(float value) {
|
||||
// Always yield the first value.
|
||||
if (std::isnan(this->last_value_)) {
|
||||
this->last_value_ = value;
|
||||
return value;
|
||||
if (std::isnan(value)) {
|
||||
if (std::isnan(this->last_value_)) {
|
||||
return {};
|
||||
} else {
|
||||
return this->last_value_ = value;
|
||||
}
|
||||
}
|
||||
// calculate min and max using the linear equation
|
||||
float ref = this->baseline_(this->last_value_);
|
||||
float min = fabsf(this->min_a0_ + ref * this->min_a1_);
|
||||
float max = fabsf(this->max_a0_ + ref * this->max_a1_);
|
||||
float delta = fabsf(value - ref);
|
||||
// if there is no reference, e.g. for the first value, just accept this one,
|
||||
// otherwise accept only if within range.
|
||||
if (delta > min && delta <= max) {
|
||||
this->last_value_ = value;
|
||||
return value;
|
||||
float diff = fabsf(value - this->last_value_);
|
||||
if (std::isnan(this->last_value_) || (diff > 0.0f && diff >= this->current_delta_)) {
|
||||
if (this->percentage_mode_) {
|
||||
this->current_delta_ = fabsf(value * this->delta_);
|
||||
}
|
||||
return this->last_value_ = value;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -452,21 +452,15 @@ class HeartbeatFilter : public Filter, public Component {
|
||||
|
||||
class DeltaFilter : public Filter {
|
||||
public:
|
||||
explicit DeltaFilter(float min_a0, float min_a1, float max_a0, float max_a1);
|
||||
|
||||
void set_baseline(float (*fn)(float));
|
||||
explicit DeltaFilter(float delta, bool percentage_mode);
|
||||
|
||||
optional<float> new_value(float value) override;
|
||||
|
||||
protected:
|
||||
// These values represent linear equations for the min and max values but in practice only one of a0 and a1 will be
|
||||
// non-zero Each limit is calculated as fabs(a0 + value * a1)
|
||||
|
||||
float min_a0_, min_a1_, max_a0_, max_a1_;
|
||||
// default baseline is the previous value
|
||||
float (*baseline_)(float) = [](float last_value) { return last_value; };
|
||||
|
||||
float delta_;
|
||||
float current_delta_;
|
||||
float last_value_{NAN};
|
||||
bool percentage_mode_;
|
||||
};
|
||||
|
||||
class OrFilter : public Filter {
|
||||
|
||||
@@ -487,7 +487,7 @@ void AsyncEventSource::deferrable_send_state(void *source, const char *event_typ
|
||||
AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *request,
|
||||
esphome::web_server_idf::AsyncEventSource *server,
|
||||
esphome::web_server::WebServer *ws)
|
||||
: server_(server), web_server_(ws), entities_iterator_(ws, server) {
|
||||
: server_(server), web_server_(ws), entities_iterator_(new esphome::web_server::ListEntitiesIterator(ws, server)) {
|
||||
httpd_req_t *req = *request;
|
||||
|
||||
httpd_resp_set_status(req, HTTPD_200);
|
||||
@@ -531,12 +531,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
|
||||
}
|
||||
#endif
|
||||
|
||||
this->entities_iterator_.begin(ws->include_internal_);
|
||||
this->entities_iterator_->begin(ws->include_internal_);
|
||||
|
||||
// just dump them all up-front and take advantage of the deferred queue
|
||||
// on second thought that takes too long, but leaving the commented code here for debug purposes
|
||||
// while(!this->entities_iterator_.completed()) {
|
||||
// this->entities_iterator_.advance();
|
||||
// while(!this->entities_iterator_->completed()) {
|
||||
// this->entities_iterator_->advance();
|
||||
//}
|
||||
}
|
||||
|
||||
@@ -634,8 +634,8 @@ void AsyncEventSourceResponse::process_buffer_() {
|
||||
void AsyncEventSourceResponse::loop() {
|
||||
process_buffer_();
|
||||
process_deferred_queue_();
|
||||
if (!this->entities_iterator_.completed())
|
||||
this->entities_iterator_.advance();
|
||||
if (!this->entities_iterator_->completed())
|
||||
this->entities_iterator_->advance();
|
||||
}
|
||||
|
||||
bool AsyncEventSourceResponse::try_send_nodefer(const char *message, const char *event, uint32_t id,
|
||||
@@ -781,7 +781,7 @@ void AsyncEventSourceResponse::deferrable_send_state(void *source, const char *e
|
||||
message_generator_t *message_generator) {
|
||||
// allow all json "details_all" to go through before publishing bare state events, this avoids unnamed entries showing
|
||||
// up in the web GUI and reduces event load during initial connect
|
||||
if (!this->entities_iterator_.completed() && 0 != strcmp(event_type, "state_detail_all"))
|
||||
if (!entities_iterator_->completed() && 0 != strcmp(event_type, "state_detail_all"))
|
||||
return;
|
||||
|
||||
if (source == nullptr)
|
||||
|
||||
@@ -13,14 +13,11 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#ifdef USE_WEBSERVER
|
||||
#include "esphome/components/web_server/list_entities.h"
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
#ifdef USE_WEBSERVER
|
||||
namespace web_server {
|
||||
class WebServer;
|
||||
class ListEntitiesIterator;
|
||||
}; // namespace web_server
|
||||
#endif
|
||||
namespace web_server_idf {
|
||||
@@ -287,7 +284,7 @@ class AsyncEventSourceResponse {
|
||||
std::atomic<int> fd_{};
|
||||
std::vector<DeferredEvent> deferred_queue_;
|
||||
esphome::web_server::WebServer *web_server_;
|
||||
esphome::web_server::ListEntitiesIterator entities_iterator_;
|
||||
std::unique_ptr<esphome::web_server::ListEntitiesIterator> entities_iterator_;
|
||||
std::string event_buffer_{""};
|
||||
size_t event_bytes_sent_;
|
||||
uint16_t consecutive_send_failures_{0};
|
||||
|
||||
@@ -920,16 +920,7 @@ bssid_t WiFiComponent::wifi_bssid() {
|
||||
}
|
||||
return bssid;
|
||||
}
|
||||
std::string WiFiComponent::wifi_ssid() {
|
||||
struct station_config conf {};
|
||||
if (!wifi_station_get_config(&conf)) {
|
||||
return "";
|
||||
}
|
||||
// conf.ssid is uint8[32], not null-terminated if full
|
||||
auto *ssid_s = reinterpret_cast<const char *>(conf.ssid);
|
||||
size_t len = strnlen(ssid_s, sizeof(conf.ssid));
|
||||
return {ssid_s, len};
|
||||
}
|
||||
std::string WiFiComponent::wifi_ssid() { return WiFi.SSID().c_str(); }
|
||||
const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer) {
|
||||
struct station_config conf {};
|
||||
if (!wifi_station_get_config(&conf)) {
|
||||
@@ -943,24 +934,16 @@ const char *WiFiComponent::wifi_ssid_to(std::span<char, SSID_BUFFER_SIZE> buffer
|
||||
return buffer.data();
|
||||
}
|
||||
int8_t WiFiComponent::wifi_rssi() {
|
||||
if (wifi_station_get_connect_status() != STATION_GOT_IP)
|
||||
if (WiFi.status() != WL_CONNECTED)
|
||||
return WIFI_RSSI_DISCONNECTED;
|
||||
sint8 rssi = wifi_station_get_rssi();
|
||||
int8_t rssi = WiFi.RSSI();
|
||||
// Values >= 31 are error codes per NONOS SDK API, not valid RSSI readings
|
||||
return rssi >= 31 ? WIFI_RSSI_DISCONNECTED : rssi;
|
||||
}
|
||||
int32_t WiFiComponent::get_wifi_channel() { return wifi_get_channel(); }
|
||||
network::IPAddress WiFiComponent::wifi_subnet_mask_() {
|
||||
struct ip_info ip {};
|
||||
wifi_get_ip_info(STATION_IF, &ip);
|
||||
return network::IPAddress(&ip.netmask);
|
||||
}
|
||||
network::IPAddress WiFiComponent::wifi_gateway_ip_() {
|
||||
struct ip_info ip {};
|
||||
wifi_get_ip_info(STATION_IF, &ip);
|
||||
return network::IPAddress(&ip.gw);
|
||||
}
|
||||
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return network::IPAddress(dns_getserver(num)); }
|
||||
int32_t WiFiComponent::get_wifi_channel() { return WiFi.channel(); }
|
||||
network::IPAddress WiFiComponent::wifi_subnet_mask_() { return {(const ip_addr_t *) WiFi.subnetMask()}; }
|
||||
network::IPAddress WiFiComponent::wifi_gateway_ip_() { return {(const ip_addr_t *) WiFi.gatewayIP()}; }
|
||||
network::IPAddress WiFiComponent::wifi_dns_ip_(int num) { return {(const ip_addr_t *) WiFi.dnsIP(num)}; }
|
||||
void WiFiComponent::wifi_loop_() {}
|
||||
|
||||
} // namespace esphome::wifi
|
||||
|
||||
@@ -827,17 +827,16 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
|
||||
}
|
||||
|
||||
uint16_t number = it.number;
|
||||
scan_result_.init(number);
|
||||
auto records = std::make_unique<wifi_ap_record_t[]>(number);
|
||||
err = esp_wifi_scan_get_ap_records(&number, records.get());
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_records failed: %s", esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
|
||||
// Process one record at a time to avoid large buffer allocation
|
||||
wifi_ap_record_t record;
|
||||
for (uint16_t i = 0; i < number; i++) {
|
||||
err = esp_wifi_scan_get_ap_record(&record);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGW(TAG, "esp_wifi_scan_get_ap_record failed: %s", esp_err_to_name(err));
|
||||
esp_wifi_clear_ap_list(); // Free remaining records not yet retrieved
|
||||
break;
|
||||
}
|
||||
scan_result_.init(number);
|
||||
for (int i = 0; i < number; i++) {
|
||||
auto &record = records[i];
|
||||
bssid_t bssid;
|
||||
std::copy(record.bssid, record.bssid + 6, bssid.begin());
|
||||
std::string ssid(reinterpret_cast<const char *>(record.ssid));
|
||||
|
||||
@@ -1379,7 +1379,6 @@ KEY_FRAMEWORK_VERSION = "framework_version"
|
||||
KEY_NAME = "name"
|
||||
KEY_VARIANT = "variant"
|
||||
KEY_PAST_SAFE_MODE = "past_safe_mode"
|
||||
KEY_NATIVE_IDF = "native_idf"
|
||||
|
||||
# Entity categories
|
||||
ENTITY_CATEGORY_NONE = ""
|
||||
|
||||
@@ -17,7 +17,6 @@ from esphome.const import (
|
||||
CONF_WEB_SERVER,
|
||||
CONF_WIFI,
|
||||
KEY_CORE,
|
||||
KEY_NATIVE_IDF,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_BK72XX,
|
||||
@@ -764,9 +763,6 @@ class EsphomeCore:
|
||||
|
||||
@property
|
||||
def firmware_bin(self) -> Path:
|
||||
# Check if using native ESP-IDF build (--native-idf)
|
||||
if self.data.get(KEY_NATIVE_IDF, False):
|
||||
return self.relative_build_path("build", f"{self.name}.bin")
|
||||
if self.is_libretiny:
|
||||
return self.relative_pioenvs_path(self.name, "firmware.uf2")
|
||||
return self.relative_pioenvs_path(self.name, "firmware.bin")
|
||||
|
||||
@@ -47,21 +47,18 @@ struct ComponentPriorityOverride {
|
||||
};
|
||||
|
||||
// Error messages for failed components
|
||||
// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead
|
||||
// This is never freed as error messages persist for the lifetime of the device
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::vector<ComponentErrorMessage> *component_error_messages = nullptr;
|
||||
std::unique_ptr<std::vector<ComponentErrorMessage>> component_error_messages;
|
||||
// Setup priority overrides - freed after setup completes
|
||||
// Using raw pointer instead of unique_ptr to avoid global constructor/destructor overhead
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
std::vector<ComponentPriorityOverride> *setup_priority_overrides = nullptr;
|
||||
std::unique_ptr<std::vector<ComponentPriorityOverride>> setup_priority_overrides;
|
||||
|
||||
// Helper to store error messages - reduces duplication between deprecated and new API
|
||||
// Remove before 2026.6.0 when deprecated const char* API is removed
|
||||
void store_component_error_message(const Component *component, const char *message, bool is_flash_ptr) {
|
||||
// Lazy allocate the error messages vector if needed
|
||||
if (!component_error_messages) {
|
||||
component_error_messages = new std::vector<ComponentErrorMessage>();
|
||||
component_error_messages = std::make_unique<std::vector<ComponentErrorMessage>>();
|
||||
}
|
||||
// Check if this component already has an error message
|
||||
for (auto &entry : *component_error_messages) {
|
||||
@@ -470,7 +467,7 @@ float Component::get_actual_setup_priority() const {
|
||||
void Component::set_setup_priority(float priority) {
|
||||
// Lazy allocate the vector if needed
|
||||
if (!setup_priority_overrides) {
|
||||
setup_priority_overrides = new std::vector<ComponentPriorityOverride>();
|
||||
setup_priority_overrides = std::make_unique<std::vector<ComponentPriorityOverride>>();
|
||||
// Reserve some space to avoid reallocations (most configs have < 10 overrides)
|
||||
setup_priority_overrides->reserve(10);
|
||||
}
|
||||
@@ -556,8 +553,7 @@ WarnIfComponentBlockingGuard::~WarnIfComponentBlockingGuard() {}
|
||||
|
||||
void clear_setup_priority_overrides() {
|
||||
// Free the setup priority map completely
|
||||
delete setup_priority_overrides;
|
||||
setup_priority_overrides = nullptr;
|
||||
setup_priority_overrides.reset();
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
@@ -572,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);
|
||||
@@ -1351,30 +1347,16 @@ template<typename... X> class LazyCallbackManager;
|
||||
*
|
||||
* Memory overhead comparison (32-bit systems):
|
||||
* - CallbackManager: 12 bytes (empty std::vector)
|
||||
* - LazyCallbackManager: 4 bytes (nullptr pointer)
|
||||
*
|
||||
* Uses plain pointer instead of unique_ptr to avoid template instantiation overhead.
|
||||
* The class is explicitly non-copyable/non-movable for Rule of Five compliance.
|
||||
* - LazyCallbackManager: 4 bytes (nullptr unique_ptr)
|
||||
*
|
||||
* @tparam Ts The arguments for the callbacks, wrapped in void().
|
||||
*/
|
||||
template<typename... Ts> class LazyCallbackManager<void(Ts...)> {
|
||||
public:
|
||||
LazyCallbackManager() = default;
|
||||
/// Destructor - clean up allocated CallbackManager if any.
|
||||
/// In practice this never runs (entities live for device lifetime) but included for correctness.
|
||||
~LazyCallbackManager() { delete this->callbacks_; }
|
||||
|
||||
// Non-copyable and non-movable (entities are never copied or moved)
|
||||
LazyCallbackManager(const LazyCallbackManager &) = delete;
|
||||
LazyCallbackManager &operator=(const LazyCallbackManager &) = delete;
|
||||
LazyCallbackManager(LazyCallbackManager &&) = delete;
|
||||
LazyCallbackManager &operator=(LazyCallbackManager &&) = delete;
|
||||
|
||||
/// Add a callback to the list. Allocates the underlying CallbackManager on first use.
|
||||
void add(std::function<void(Ts...)> &&callback) {
|
||||
if (!this->callbacks_) {
|
||||
this->callbacks_ = new CallbackManager<void(Ts...)>();
|
||||
this->callbacks_ = make_unique<CallbackManager<void(Ts...)>>();
|
||||
}
|
||||
this->callbacks_->add(std::move(callback));
|
||||
}
|
||||
@@ -1396,7 +1378,7 @@ template<typename... Ts> class LazyCallbackManager<void(Ts...)> {
|
||||
void operator()(Ts... args) { this->call(args...); }
|
||||
|
||||
protected:
|
||||
CallbackManager<void(Ts...)> *callbacks_{nullptr};
|
||||
std::unique_ptr<CallbackManager<void(Ts...)>> callbacks_;
|
||||
};
|
||||
|
||||
/// Helper class to deduplicate items in a series of values.
|
||||
|
||||
@@ -67,7 +67,7 @@ std::string ESPTime::strftime(const char *format) {
|
||||
|
||||
std::string ESPTime::strftime(const std::string &format) { return this->strftime(format.c_str()); }
|
||||
|
||||
bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time) {
|
||||
bool ESPTime::strptime(const std::string &time_to_parse, ESPTime &esp_time) {
|
||||
uint16_t year;
|
||||
uint8_t month;
|
||||
uint8_t day;
|
||||
@@ -75,41 +75,40 @@ bool ESPTime::strptime(const char *time_to_parse, size_t len, ESPTime &esp_time)
|
||||
uint8_t minute;
|
||||
uint8_t second;
|
||||
int num;
|
||||
const int ilen = static_cast<int>(len);
|
||||
|
||||
if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
|
||||
&hour, // NOLINT
|
||||
&minute, // NOLINT
|
||||
&second, &num) == 6 && // NOLINT
|
||||
num == ilen) {
|
||||
if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu:%02hhu %n", &year, &month, &day, // NOLINT
|
||||
&hour, // NOLINT
|
||||
&minute, // NOLINT
|
||||
&second, &num) == 6 && // NOLINT
|
||||
num == static_cast<int>(time_to_parse.size())) {
|
||||
esp_time.year = year;
|
||||
esp_time.month = month;
|
||||
esp_time.day_of_month = day;
|
||||
esp_time.hour = hour;
|
||||
esp_time.minute = minute;
|
||||
esp_time.second = second;
|
||||
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
|
||||
&hour, // NOLINT
|
||||
&minute, &num) == 5 && // NOLINT
|
||||
num == ilen) {
|
||||
} else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %02hhu:%02hhu %n", &year, &month, &day, // NOLINT
|
||||
&hour, // NOLINT
|
||||
&minute, &num) == 5 && // NOLINT
|
||||
num == static_cast<int>(time_to_parse.size())) {
|
||||
esp_time.year = year;
|
||||
esp_time.month = month;
|
||||
esp_time.day_of_month = day;
|
||||
esp_time.hour = hour;
|
||||
esp_time.minute = minute;
|
||||
esp_time.second = 0;
|
||||
} else if (sscanf(time_to_parse, "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
|
||||
num == ilen) {
|
||||
} else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu:%02hhu %n", &hour, &minute, &second, &num) == 3 && // NOLINT
|
||||
num == static_cast<int>(time_to_parse.size())) {
|
||||
esp_time.hour = hour;
|
||||
esp_time.minute = minute;
|
||||
esp_time.second = second;
|
||||
} else if (sscanf(time_to_parse, "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
|
||||
num == ilen) {
|
||||
} else if (sscanf(time_to_parse.c_str(), "%02hhu:%02hhu %n", &hour, &minute, &num) == 2 && // NOLINT
|
||||
num == static_cast<int>(time_to_parse.size())) {
|
||||
esp_time.hour = hour;
|
||||
esp_time.minute = minute;
|
||||
esp_time.second = 0;
|
||||
} else if (sscanf(time_to_parse, "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
|
||||
num == ilen) {
|
||||
} else if (sscanf(time_to_parse.c_str(), "%04hu-%02hhu-%02hhu %n", &year, &month, &day, &num) == 3 && // NOLINT
|
||||
num == static_cast<int>(time_to_parse.size())) {
|
||||
esp_time.year = year;
|
||||
esp_time.month = month;
|
||||
esp_time.day_of_month = day;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <span>
|
||||
#include <string>
|
||||
@@ -81,20 +80,11 @@ struct ESPTime {
|
||||
}
|
||||
|
||||
/** Convert a string to ESPTime struct as specified by the format argument.
|
||||
* @param time_to_parse c string formatted like this: 2020-08-25 05:30:00.
|
||||
* @param len length of the string (not including null terminator if present)
|
||||
* @param time_to_parse null-terminated c string formatet like this: 2020-08-25 05:30:00.
|
||||
* @param esp_time an instance of a ESPTime struct
|
||||
* @return the success state of the parsing
|
||||
* @return the success sate of the parsing
|
||||
*/
|
||||
static bool strptime(const char *time_to_parse, size_t len, ESPTime &esp_time);
|
||||
/// @copydoc strptime(const char *, size_t, ESPTime &)
|
||||
static bool strptime(const char *time_to_parse, ESPTime &esp_time) {
|
||||
return strptime(time_to_parse, strlen(time_to_parse), esp_time);
|
||||
}
|
||||
/// @copydoc strptime(const char *, size_t, ESPTime &)
|
||||
static bool strptime(const std::string &time_to_parse, ESPTime &esp_time) {
|
||||
return strptime(time_to_parse.c_str(), time_to_parse.size(), esp_time);
|
||||
}
|
||||
static bool strptime(const std::string &time_to_parse, ESPTime &esp_time);
|
||||
|
||||
/// Convert a C tm struct instance with a C unix epoch timestamp to an ESPTime instance.
|
||||
static ESPTime from_c_tm(struct tm *c_tm, time_t c_time);
|
||||
|
||||
@@ -1,229 +0,0 @@
|
||||
"""ESP-IDF direct build API for ESPHome."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
from esphome.components.esp32.const import KEY_ESP32, KEY_FLASH_SIZE
|
||||
from esphome.const import CONF_COMPILE_PROCESS_LIMIT, CONF_ESPHOME
|
||||
from esphome.core import CORE, EsphomeError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_idf_path() -> Path | None:
|
||||
"""Get IDF_PATH from environment or common locations."""
|
||||
# Check environment variable first
|
||||
if "IDF_PATH" in os.environ:
|
||||
path = Path(os.environ["IDF_PATH"])
|
||||
if path.is_dir():
|
||||
return path
|
||||
|
||||
# Check common installation locations
|
||||
common_paths = [
|
||||
Path.home() / "esp" / "esp-idf",
|
||||
Path.home() / ".espressif" / "esp-idf",
|
||||
Path("/opt/esp-idf"),
|
||||
]
|
||||
|
||||
for path in common_paths:
|
||||
if path.is_dir() and (path / "tools" / "idf.py").is_file():
|
||||
return path
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _get_idf_env() -> dict[str, str]:
|
||||
"""Get environment variables needed for ESP-IDF build.
|
||||
|
||||
Requires the user to have sourced export.sh before running esphome.
|
||||
"""
|
||||
env = os.environ.copy()
|
||||
|
||||
idf_path = _get_idf_path()
|
||||
if idf_path is None:
|
||||
raise EsphomeError(
|
||||
"ESP-IDF not found. Please install ESP-IDF and source export.sh:\n"
|
||||
" git clone -b v5.3.2 --recursive https://github.com/espressif/esp-idf.git ~/esp-idf\n"
|
||||
" cd ~/esp-idf && ./install.sh\n"
|
||||
" source ~/esp-idf/export.sh\n"
|
||||
"See: https://docs.espressif.com/projects/esp-idf/en/latest/esp32/get-started/"
|
||||
)
|
||||
|
||||
env["IDF_PATH"] = str(idf_path)
|
||||
return env
|
||||
|
||||
|
||||
def run_idf_py(
|
||||
*args, cwd: Path | None = None, capture_output: bool = False
|
||||
) -> int | str:
|
||||
"""Run idf.py with the given arguments."""
|
||||
idf_path = _get_idf_path()
|
||||
if idf_path is None:
|
||||
raise EsphomeError("ESP-IDF not found")
|
||||
|
||||
env = _get_idf_env()
|
||||
idf_py = idf_path / "tools" / "idf.py"
|
||||
|
||||
cmd = ["python", str(idf_py)] + list(args)
|
||||
|
||||
if cwd is None:
|
||||
cwd = CORE.build_path
|
||||
|
||||
_LOGGER.debug("Running: %s", " ".join(cmd))
|
||||
_LOGGER.debug(" in directory: %s", cwd)
|
||||
|
||||
if capture_output:
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
if result.returncode != 0:
|
||||
_LOGGER.error("idf.py failed:\n%s", result.stderr)
|
||||
return result.stdout
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
check=False,
|
||||
)
|
||||
return result.returncode
|
||||
|
||||
|
||||
def run_reconfigure() -> int:
|
||||
"""Run cmake reconfigure only (no build)."""
|
||||
return run_idf_py("reconfigure")
|
||||
|
||||
|
||||
def run_compile(config, verbose: bool) -> int:
|
||||
"""Compile the ESP-IDF project.
|
||||
|
||||
Uses two-phase configure to auto-discover available components:
|
||||
1. If no previous build, configure with minimal REQUIRES to discover components
|
||||
2. Regenerate CMakeLists.txt with discovered components
|
||||
3. Run full build
|
||||
"""
|
||||
from esphome.build_gen.espidf import has_discovered_components, write_project
|
||||
|
||||
# Check if we need to do discovery phase
|
||||
if not has_discovered_components():
|
||||
_LOGGER.info("Discovering available ESP-IDF components...")
|
||||
write_project(minimal=True)
|
||||
rc = run_reconfigure()
|
||||
if rc != 0:
|
||||
_LOGGER.error("Component discovery failed")
|
||||
return rc
|
||||
_LOGGER.info("Regenerating CMakeLists.txt with discovered components...")
|
||||
write_project(minimal=False)
|
||||
|
||||
# Build
|
||||
args = ["build"]
|
||||
|
||||
if verbose:
|
||||
args.append("-v")
|
||||
|
||||
# Add parallel job limit if configured
|
||||
if CONF_COMPILE_PROCESS_LIMIT in config.get(CONF_ESPHOME, {}):
|
||||
limit = config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]
|
||||
args.extend(["-j", str(limit)])
|
||||
|
||||
# Set the sdkconfig file
|
||||
sdkconfig_path = CORE.relative_build_path(f"sdkconfig.{CORE.name}")
|
||||
if sdkconfig_path.is_file():
|
||||
args.extend(["-D", f"SDKCONFIG={sdkconfig_path}"])
|
||||
|
||||
return run_idf_py(*args)
|
||||
|
||||
|
||||
def get_firmware_path() -> Path:
|
||||
"""Get the path to the compiled firmware binary."""
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
return build_dir / f"{CORE.name}.bin"
|
||||
|
||||
|
||||
def get_factory_firmware_path() -> Path:
|
||||
"""Get the path to the factory firmware (with bootloader)."""
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
return build_dir / f"{CORE.name}.factory.bin"
|
||||
|
||||
|
||||
def create_factory_bin() -> bool:
|
||||
"""Create factory.bin by merging bootloader, partition table, and app."""
|
||||
build_dir = CORE.relative_build_path("build")
|
||||
flasher_args_path = build_dir / "flasher_args.json"
|
||||
|
||||
if not flasher_args_path.is_file():
|
||||
_LOGGER.warning("flasher_args.json not found, cannot create factory.bin")
|
||||
return False
|
||||
|
||||
try:
|
||||
with open(flasher_args_path, encoding="utf-8") as f:
|
||||
flash_data = json.load(f)
|
||||
except (json.JSONDecodeError, OSError) as e:
|
||||
_LOGGER.error("Failed to read flasher_args.json: %s", e)
|
||||
return False
|
||||
|
||||
# Get flash size from config
|
||||
flash_size = CORE.data[KEY_ESP32][KEY_FLASH_SIZE]
|
||||
|
||||
# Build esptool merge command
|
||||
sections = []
|
||||
for addr, fname in sorted(
|
||||
flash_data.get("flash_files", {}).items(), key=lambda kv: int(kv[0], 16)
|
||||
):
|
||||
file_path = build_dir / fname
|
||||
if file_path.is_file():
|
||||
sections.extend([addr, str(file_path)])
|
||||
else:
|
||||
_LOGGER.warning("Flash file not found: %s", file_path)
|
||||
|
||||
if not sections:
|
||||
_LOGGER.warning("No flash sections found")
|
||||
return False
|
||||
|
||||
output_path = get_factory_firmware_path()
|
||||
chip = flash_data.get("extra_esptool_args", {}).get("chip", "esp32")
|
||||
|
||||
cmd = [
|
||||
"python",
|
||||
"-m",
|
||||
"esptool",
|
||||
"--chip",
|
||||
chip,
|
||||
"merge_bin",
|
||||
"--flash_size",
|
||||
flash_size,
|
||||
"--output",
|
||||
str(output_path),
|
||||
] + sections
|
||||
|
||||
_LOGGER.info("Creating factory.bin...")
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||
|
||||
if result.returncode != 0:
|
||||
_LOGGER.error("Failed to create factory.bin: %s", result.stderr)
|
||||
return False
|
||||
|
||||
_LOGGER.info("Created: %s", output_path)
|
||||
return True
|
||||
|
||||
|
||||
def create_ota_bin() -> bool:
|
||||
"""Copy the firmware to .ota.bin for ESPHome OTA compatibility."""
|
||||
firmware_path = get_firmware_path()
|
||||
ota_path = firmware_path.with_suffix(".ota.bin")
|
||||
|
||||
if not firmware_path.is_file():
|
||||
_LOGGER.warning("Firmware not found: %s", firmware_path)
|
||||
return False
|
||||
|
||||
shutil.copy(firmware_path, ota_path)
|
||||
_LOGGER.info("Created: %s", ota_path)
|
||||
return True
|
||||
@@ -1,6 +1,4 @@
|
||||
dependencies:
|
||||
bblanchon/arduinojson:
|
||||
version: "7.4.2"
|
||||
espressif/esp-tflite-micro:
|
||||
version: 1.3.3~1
|
||||
espressif/esp32-camera:
|
||||
|
||||
@@ -34,6 +34,7 @@ build_flags =
|
||||
[common]
|
||||
; Base dependencies for all environments
|
||||
lib_deps_base =
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
wjtje/qr-code-generator-library@1.7.0 ; qr_code
|
||||
functionpointer/arduino-MLX90393@1.0.2 ; mlx90393
|
||||
pavlodn/HaierProtocol@0.9.31 ; haier
|
||||
@@ -110,7 +111,6 @@ platform_packages =
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
${common:arduino.lib_deps}
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
ESP8266WiFi ; wifi (Arduino built-in)
|
||||
Update ; ota (Arduino built-in)
|
||||
ESP32Async/ESPAsyncTCP@2.0.0 ; async_tcp
|
||||
@@ -201,7 +201,6 @@ platform_packages =
|
||||
framework = arduino
|
||||
lib_deps =
|
||||
${common:arduino.lib_deps}
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
|
||||
build_flags =
|
||||
${common:arduino.build_flags}
|
||||
@@ -217,7 +216,6 @@ platform = libretiny@1.9.2
|
||||
framework = arduino
|
||||
lib_compat_mode = soft
|
||||
lib_deps =
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
ESP32Async/ESPAsyncWebServer@3.7.8 ; web_server_base
|
||||
droscy/esp_wireguard@0.4.2 ; wireguard
|
||||
build_flags =
|
||||
@@ -241,7 +239,6 @@ build_flags =
|
||||
-DUSE_NRF52
|
||||
lib_deps =
|
||||
${common.lib_deps_base}
|
||||
bblanchon/ArduinoJson@7.4.2 ; json
|
||||
|
||||
; All the actual environments are defined below.
|
||||
|
||||
|
||||
@@ -732,26 +732,6 @@ def lint_no_heap_allocating_helpers(fname, match):
|
||||
)
|
||||
|
||||
|
||||
@lint_re_check(
|
||||
# Match sprintf/vsprintf but not snprintf/vsnprintf
|
||||
# [^\w] ensures we don't match the safe variants
|
||||
r"[^\w](v?sprintf)\s*\(" + CPP_RE_EOL,
|
||||
include=cpp_include,
|
||||
)
|
||||
def lint_no_sprintf(fname, match):
|
||||
func = match.group(1)
|
||||
safe_func = func.replace("sprintf", "snprintf")
|
||||
return (
|
||||
f"{highlight(func + '()')} is not allowed in ESPHome. It has no buffer size limit "
|
||||
f"and can cause buffer overflows.\n"
|
||||
f"Please use one of these alternatives:\n"
|
||||
f" - {highlight(safe_func + '(buf, sizeof(buf), fmt, ...)')} for general formatting\n"
|
||||
f" - {highlight('buf_append_printf(buf, sizeof(buf), pos, fmt, ...)')} for "
|
||||
f"offset-based formatting (also stores format strings in flash on ESP8266)\n"
|
||||
f"(If strictly necessary, add `// NOLINT` to the end of the line)"
|
||||
)
|
||||
|
||||
|
||||
@lint_content_find_check(
|
||||
"ESP_LOG",
|
||||
include=["*.h", "*.tcc"],
|
||||
|
||||
@@ -121,8 +121,6 @@ sensor:
|
||||
min_value: -10.0
|
||||
- debounce: 0.1s
|
||||
- delta: 5.0
|
||||
- delta:
|
||||
max_value: 2%
|
||||
- exponential_moving_average:
|
||||
alpha: 0.1
|
||||
send_every: 15
|
||||
|
||||
@@ -1,180 +0,0 @@
|
||||
esphome:
|
||||
name: test-delta-filters
|
||||
|
||||
host:
|
||||
api:
|
||||
batch_delay: 0ms # Disable batching to receive all state updates
|
||||
logger:
|
||||
level: DEBUG
|
||||
|
||||
sensor:
|
||||
- platform: template
|
||||
name: "Source Sensor 1"
|
||||
id: source_sensor_1
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 2"
|
||||
id: source_sensor_2
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 3"
|
||||
id: source_sensor_3
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: template
|
||||
name: "Source Sensor 4"
|
||||
id: source_sensor_4
|
||||
accuracy_decimals: 1
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_1
|
||||
name: "Filter Min"
|
||||
id: filter_min
|
||||
filters:
|
||||
- delta:
|
||||
min_value: 10
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_2
|
||||
name: "Filter Max"
|
||||
id: filter_max
|
||||
filters:
|
||||
- delta:
|
||||
max_value: 10
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_3
|
||||
id: test_3_baseline
|
||||
filters:
|
||||
- median:
|
||||
window_size: 6
|
||||
send_every: 1
|
||||
send_first_at: 1
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_3
|
||||
name: "Filter Baseline Max"
|
||||
id: filter_baseline_max
|
||||
filters:
|
||||
- delta:
|
||||
max_value: 10
|
||||
baseline: !lambda return id(test_3_baseline).state;
|
||||
|
||||
- platform: copy
|
||||
source_id: source_sensor_4
|
||||
name: "Filter Zero Delta"
|
||||
id: filter_zero_delta
|
||||
filters:
|
||||
- delta: 0
|
||||
|
||||
script:
|
||||
- id: test_filter_min
|
||||
then:
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_1
|
||||
state: 1.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_1
|
||||
state: 5.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_1
|
||||
state: 12.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_1
|
||||
state: 8.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_1
|
||||
state: -2.0
|
||||
|
||||
- id: test_filter_max
|
||||
then:
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: 1.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: 5.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: 40.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: 10.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_2
|
||||
state: -40.0 # Filtered out
|
||||
|
||||
- id: test_filter_baseline_max
|
||||
then:
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 1.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 2.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 3.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 40.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 20.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_3
|
||||
state: 20.0
|
||||
|
||||
- id: test_filter_zero_delta
|
||||
then:
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_4
|
||||
state: 1.0
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_4
|
||||
state: 1.0 # Filtered out
|
||||
- delay: 20ms
|
||||
- sensor.template.publish:
|
||||
id: source_sensor_4
|
||||
state: 2.0
|
||||
|
||||
button:
|
||||
- platform: template
|
||||
name: "Test Filter Min"
|
||||
id: btn_filter_min
|
||||
on_press:
|
||||
- script.execute: test_filter_min
|
||||
|
||||
- platform: template
|
||||
name: "Test Filter Max"
|
||||
id: btn_filter_max
|
||||
on_press:
|
||||
- script.execute: test_filter_max
|
||||
|
||||
- platform: template
|
||||
name: "Test Filter Baseline Max"
|
||||
id: btn_filter_baseline_max
|
||||
on_press:
|
||||
- script.execute: test_filter_baseline_max
|
||||
|
||||
- platform: template
|
||||
name: "Test Filter Zero Delta"
|
||||
id: btn_filter_zero_delta
|
||||
on_press:
|
||||
- script.execute: test_filter_zero_delta
|
||||
@@ -1,163 +0,0 @@
|
||||
"""Test sensor DeltaFilter functionality."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from aioesphomeapi import ButtonInfo, EntityState, SensorState
|
||||
import pytest
|
||||
|
||||
from .state_utils import InitialStateHelper, build_key_to_entity_mapping
|
||||
from .types import APIClientConnectedFactory, RunCompiledFunction
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_sensor_filters_delta(
|
||||
yaml_config: str,
|
||||
run_compiled: RunCompiledFunction,
|
||||
api_client_connected: APIClientConnectedFactory,
|
||||
) -> None:
|
||||
loop = asyncio.get_running_loop()
|
||||
|
||||
sensor_values: dict[str, list[float]] = {
|
||||
"filter_min": [],
|
||||
"filter_max": [],
|
||||
"filter_baseline_max": [],
|
||||
"filter_zero_delta": [],
|
||||
}
|
||||
|
||||
filter_min_done = loop.create_future()
|
||||
filter_max_done = loop.create_future()
|
||||
filter_baseline_max_done = loop.create_future()
|
||||
filter_zero_delta_done = loop.create_future()
|
||||
|
||||
def on_state(state: EntityState) -> None:
|
||||
if not isinstance(state, SensorState) or state.missing_state:
|
||||
return
|
||||
|
||||
sensor_name = key_to_sensor.get(state.key)
|
||||
if sensor_name not in sensor_values:
|
||||
return
|
||||
|
||||
sensor_values[sensor_name].append(state.state)
|
||||
|
||||
# Check completion conditions
|
||||
if (
|
||||
sensor_name == "filter_min"
|
||||
and len(sensor_values[sensor_name]) == 3
|
||||
and not filter_min_done.done()
|
||||
):
|
||||
filter_min_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "filter_max"
|
||||
and len(sensor_values[sensor_name]) == 3
|
||||
and not filter_max_done.done()
|
||||
):
|
||||
filter_max_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "filter_baseline_max"
|
||||
and len(sensor_values[sensor_name]) == 4
|
||||
and not filter_baseline_max_done.done()
|
||||
):
|
||||
filter_baseline_max_done.set_result(True)
|
||||
elif (
|
||||
sensor_name == "filter_zero_delta"
|
||||
and len(sensor_values[sensor_name]) == 2
|
||||
and not filter_zero_delta_done.done()
|
||||
):
|
||||
filter_zero_delta_done.set_result(True)
|
||||
|
||||
async with (
|
||||
run_compiled(yaml_config),
|
||||
api_client_connected() as client,
|
||||
):
|
||||
# Get entities and build key mapping
|
||||
entities, _ = await client.list_entities_services()
|
||||
key_to_sensor = build_key_to_entity_mapping(
|
||||
entities,
|
||||
{
|
||||
"filter_min": "Filter Min",
|
||||
"filter_max": "Filter Max",
|
||||
"filter_baseline_max": "Filter Baseline Max",
|
||||
"filter_zero_delta": "Filter Zero Delta",
|
||||
},
|
||||
)
|
||||
|
||||
# Set up initial state helper with all entities
|
||||
initial_state_helper = InitialStateHelper(entities)
|
||||
|
||||
# Subscribe to state changes with wrapper
|
||||
client.subscribe_states(initial_state_helper.on_state_wrapper(on_state))
|
||||
|
||||
# Wait for initial states
|
||||
await initial_state_helper.wait_for_initial_states()
|
||||
|
||||
# Find all buttons
|
||||
button_name_map = {
|
||||
"Test Filter Min": "filter_min",
|
||||
"Test Filter Max": "filter_max",
|
||||
"Test Filter Baseline Max": "filter_baseline_max",
|
||||
"Test Filter Zero Delta": "filter_zero_delta",
|
||||
}
|
||||
buttons = {}
|
||||
for entity in entities:
|
||||
if isinstance(entity, ButtonInfo) and entity.name in button_name_map:
|
||||
buttons[button_name_map[entity.name]] = entity.key
|
||||
|
||||
assert len(buttons) == 4, f"Expected 3 buttons, found {len(buttons)}"
|
||||
|
||||
# Test 1: Min
|
||||
sensor_values["filter_min"].clear()
|
||||
client.button_command(buttons["filter_min"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_min_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 1 timed out. Values: {sensor_values['filter_min']}")
|
||||
|
||||
expected = [1.0, 12.0, -2.0]
|
||||
assert sensor_values["filter_min"] == pytest.approx(expected), (
|
||||
f"Test 1 failed: expected {expected}, got {sensor_values['filter_min']}"
|
||||
)
|
||||
|
||||
# Test 2: Max
|
||||
sensor_values["filter_max"].clear()
|
||||
client.button_command(buttons["filter_max"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_max_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(f"Test 2 timed out. Values: {sensor_values['filter_max']}")
|
||||
|
||||
expected = [1.0, 5.0, 10.0]
|
||||
assert sensor_values["filter_max"] == pytest.approx(expected), (
|
||||
f"Test 2 failed: expected {expected}, got {sensor_values['filter_max']}"
|
||||
)
|
||||
|
||||
# Test 3: Baseline Max
|
||||
sensor_values["filter_baseline_max"].clear()
|
||||
client.button_command(buttons["filter_baseline_max"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_baseline_max_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 3 timed out. Values: {sensor_values['filter_baseline_max']}"
|
||||
)
|
||||
|
||||
expected = [1.0, 2.0, 3.0, 20.0]
|
||||
assert sensor_values["filter_baseline_max"] == pytest.approx(expected), (
|
||||
f"Test 3 failed: expected {expected}, got {sensor_values['filter_baseline_max']}"
|
||||
)
|
||||
|
||||
# Test 4: Zero Delta
|
||||
sensor_values["filter_zero_delta"].clear()
|
||||
client.button_command(buttons["filter_zero_delta"])
|
||||
try:
|
||||
await asyncio.wait_for(filter_zero_delta_done, timeout=2.0)
|
||||
except TimeoutError:
|
||||
pytest.fail(
|
||||
f"Test 4 timed out. Values: {sensor_values['filter_zero_delta']}"
|
||||
)
|
||||
|
||||
expected = [1.0, 2.0]
|
||||
assert sensor_values["filter_zero_delta"] == pytest.approx(expected), (
|
||||
f"Test 4 failed: expected {expected}, got {sensor_values['filter_zero_delta']}"
|
||||
)
|
||||
Reference in New Issue
Block a user