1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-18 07:45:56 +00:00

Compare commits

..

1 Commits

Author SHA1 Message Date
J. Nick Koston
085aeeb8d5 Merge branch 'dev' into light_loop 2025-11-12 22:10:59 -06:00
109 changed files with 790 additions and 3118 deletions

View File

@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@014f16e7ab1402f30e7c3329d33797e7948572db # v4.31.3
uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
with:
category: "/language:${{matrix.language}}"

View File

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

View File

@@ -741,13 +741,6 @@ def command_vscode(args: ArgsProtocol) -> int | None:
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
# Set memory analysis options in config
if args.analyze_memory:
config.setdefault(CONF_ESPHOME, {})["analyze_memory"] = True
if args.memory_report:
config.setdefault(CONF_ESPHOME, {})["memory_report_file"] = args.memory_report
exit_code = write_cpp(config)
if exit_code != 0:
return exit_code
@@ -1209,17 +1202,6 @@ def parse_args(argv):
help="Only generate source code, do not compile.",
action="store_true",
)
parser_compile.add_argument(
"--analyze-memory",
help="Analyze and display memory usage by component after compilation.",
action="store_true",
)
parser_compile.add_argument(
"--memory-report",
help="Save memory analysis report to a file (supports .json or .txt).",
type=str,
metavar="FILE",
)
parser_upload = subparsers.add_parser(
"upload",

View File

@@ -1,7 +1,6 @@
"""CLI interface for memory analysis with report generation."""
from collections import defaultdict
import json
import sys
from . import (
@@ -16,11 +15,6 @@ from . import (
class MemoryAnalyzerCLI(MemoryAnalyzer):
"""Memory analyzer with CLI-specific report generation."""
# Symbol size threshold for detailed analysis
SYMBOL_SIZE_THRESHOLD: int = (
100 # Show symbols larger than this in detailed analysis
)
# Column width constants
COL_COMPONENT: int = 29
COL_FLASH_TEXT: int = 14
@@ -197,21 +191,14 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%"
)
# All core symbols above threshold
# Top 15 largest core symbols
lines.append("")
lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:")
sorted_core_symbols = sorted(
self._esphome_core_symbols, key=lambda x: x[2], reverse=True
)
large_core_symbols = [
(symbol, demangled, size)
for symbol, demangled, size in sorted_core_symbols
if size > self.SYMBOL_SIZE_THRESHOLD
]
lines.append(
f"{_COMPONENT_CORE} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_core_symbols)} symbols):"
)
for i, (symbol, demangled, size) in enumerate(large_core_symbols):
for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]):
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
lines.append("=" * self.TABLE_WIDTH)
@@ -281,15 +268,13 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
lines.append(f"Total size: {comp_mem.flash_total:,} B")
lines.append("")
# Show all symbols above threshold for better visibility
# Show all symbols > 100 bytes for better visibility
large_symbols = [
(sym, dem, size)
for sym, dem, size in sorted_symbols
if size > self.SYMBOL_SIZE_THRESHOLD
(sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
]
lines.append(
f"{comp_name} Symbols > {self.SYMBOL_SIZE_THRESHOLD} B ({len(large_symbols)} symbols):"
f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):"
)
for i, (symbol, demangled, size) in enumerate(large_symbols):
lines.append(f"{i + 1}. {demangled} ({size:,} B)")
@@ -298,28 +283,6 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
return "\n".join(lines)
def to_json(self) -> str:
"""Export analysis results as JSON."""
data = {
"components": {
name: {
"text": mem.text_size,
"rodata": mem.rodata_size,
"data": mem.data_size,
"bss": mem.bss_size,
"flash_total": mem.flash_total,
"ram_total": mem.ram_total,
"symbol_count": mem.symbol_count,
}
for name, mem in self.components.items()
},
"totals": {
"flash": sum(c.flash_total for c in self.components.values()),
"ram": sum(c.ram_total for c in self.components.values()),
},
}
return json.dumps(data, indent=2)
def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
"""Dump uncategorized symbols for analysis."""
# Sort by size descending

View File

@@ -51,14 +51,13 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
return true;
}
protected:
virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, size_t... S>
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}
@@ -96,14 +95,13 @@ template<typename... Ts> class UserServiceDynamic : public UserServiceDescriptor
return false;
if (req.args.size() != sizeof...(Ts))
return false;
this->execute_(req.args, std::make_index_sequence<sizeof...(Ts)>{});
this->execute_(req.args, typename gens<sizeof...(Ts)>::type());
return true;
}
protected:
virtual void execute(Ts... x) = 0;
template<typename ArgsContainer, size_t... S>
void execute_(const ArgsContainer &args, std::index_sequence<S...> type) {
template<typename ArgsContainer, int... S> void execute_(const ArgsContainer &args, seq<S...> type) {
this->execute((get_execute_arg_value<Ts>(args[S]))...);
}

View File

@@ -1,8 +1,8 @@
#include "bh1750.h"
#include "esphome/core/log.h"
#include "esphome/core/application.h"
namespace esphome::bh1750 {
namespace esphome {
namespace bh1750 {
static const char *const TAG = "bh1750.sensor";
@@ -13,31 +13,6 @@ static const uint8_t BH1750_COMMAND_ONE_TIME_L = 0b00100011;
static const uint8_t BH1750_COMMAND_ONE_TIME_H = 0b00100000;
static const uint8_t BH1750_COMMAND_ONE_TIME_H2 = 0b00100001;
static constexpr uint32_t MEASUREMENT_TIMEOUT_MS = 2000;
static constexpr float HIGH_LIGHT_THRESHOLD_LX = 7000.0f;
// Measurement time constants (datasheet values)
static constexpr uint16_t MTREG_DEFAULT = 69;
static constexpr uint16_t MTREG_MIN = 31;
static constexpr uint16_t MTREG_MAX = 254;
static constexpr uint16_t MEAS_TIME_L_MS = 24; // L-resolution max measurement time @ mtreg=69
static constexpr uint16_t MEAS_TIME_H_MS = 180; // H/H2-resolution max measurement time @ mtreg=69
// Conversion constants (datasheet formulas)
static constexpr float RESOLUTION_DIVISOR = 1.2f; // counts to lux conversion divisor
static constexpr float MODE_H2_DIVISOR = 2.0f; // H2 mode has 2x higher resolution
// MTreg calculation constants
static constexpr int COUNTS_TARGET = 50000; // Target counts for optimal range (avoid saturation)
static constexpr int COUNTS_NUMERATOR = 10;
static constexpr int COUNTS_DENOMINATOR = 12;
// MTreg register bit manipulation constants
static constexpr uint8_t MTREG_HI_SHIFT = 5; // High 3 bits start at bit 5
static constexpr uint8_t MTREG_HI_MASK = 0b111; // 3-bit mask for high bits
static constexpr uint8_t MTREG_LO_SHIFT = 0; // Low 5 bits start at bit 0
static constexpr uint8_t MTREG_LO_MASK = 0b11111; // 5-bit mask for low bits
/*
bh1750 properties:
@@ -68,7 +43,74 @@ void BH1750Sensor::setup() {
this->mark_failed();
return;
}
this->state_ = IDLE;
}
void BH1750Sensor::read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f) {
// turn on (after one-shot sensor automatically powers down)
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Power on failed");
f(NAN);
return;
}
if (active_mtreg_ != mtreg) {
// set mtreg
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> 5) & 0b111);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> 0) & 0b11111);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Set measurement time failed");
active_mtreg_ = 0;
f(NAN);
return;
}
active_mtreg_ = mtreg;
}
uint8_t cmd;
uint16_t meas_time;
switch (mode) {
case BH1750_MODE_L:
cmd = BH1750_COMMAND_ONE_TIME_L;
meas_time = 24 * mtreg / 69;
break;
case BH1750_MODE_H:
cmd = BH1750_COMMAND_ONE_TIME_H;
meas_time = 180 * mtreg / 69;
break;
case BH1750_MODE_H2:
cmd = BH1750_COMMAND_ONE_TIME_H2;
meas_time = 180 * mtreg / 69;
break;
default:
f(NAN);
return;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Start measurement failed");
f(NAN);
return;
}
// probably not needed, but adjust for rounding
meas_time++;
this->set_timeout("read", meas_time, [this, mode, mtreg, f]() {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Read data failed");
f(NAN);
return;
}
raw_value = i2c::i2ctohs(raw_value);
float lx = float(raw_value) / 1.2f;
lx *= 69.0f / mtreg;
if (mode == BH1750_MODE_H2)
lx /= 2.0f;
f(lx);
});
}
void BH1750Sensor::dump_config() {
@@ -82,188 +124,45 @@ void BH1750Sensor::dump_config() {
}
void BH1750Sensor::update() {
const uint32_t now = millis();
// Start coarse measurement to determine optimal mode/mtreg
if (this->state_ != IDLE) {
// Safety timeout: reset if stuck
if (now - this->measurement_start_time_ > MEASUREMENT_TIMEOUT_MS) {
ESP_LOGW(TAG, "Measurement timeout, resetting state");
this->state_ = IDLE;
} else {
ESP_LOGW(TAG, "Previous measurement not complete, skipping update");
// first do a quick measurement in L-mode with full range
// to find right range
this->read_lx_(BH1750_MODE_L, 31, [this](float val) {
if (std::isnan(val)) {
this->status_set_warning();
this->publish_state(NAN);
return;
}
}
if (!this->start_measurement_(BH1750_MODE_L, MTREG_MIN, now)) {
this->status_set_warning();
this->publish_state(NAN);
return;
}
this->state_ = WAITING_COARSE_MEASUREMENT;
this->enable_loop(); // Enable loop while measurement in progress
}
void BH1750Sensor::loop() {
const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) {
case IDLE:
// Disable loop when idle to save cycles
this->disable_loop();
break;
case WAITING_COARSE_MEASUREMENT:
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
this->state_ = READING_COARSE_RESULT;
}
break;
case READING_COARSE_RESULT: {
float lx;
if (!this->read_measurement_(lx)) {
this->fail_and_reset_();
break;
}
this->process_coarse_result_(lx);
// Start fine measurement with optimal settings
if (!this->start_measurement_(this->fine_mode_, this->fine_mtreg_, now)) {
this->fail_and_reset_();
break;
}
this->state_ = WAITING_FINE_MEASUREMENT;
break;
BH1750Mode use_mode;
uint8_t use_mtreg;
if (val <= 7000) {
use_mode = BH1750_MODE_H2;
use_mtreg = 254;
} else {
use_mode = BH1750_MODE_H;
// lx = counts / 1.2 * (69 / mtreg)
// -> mtreg = counts / 1.2 * (69 / lx)
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
// -> mtreg = 50000*(10/12)*(69/lx)
int ideal_mtreg = 50000 * 10 * 69 / (12 * (int) val);
use_mtreg = std::min(254, std::max(31, ideal_mtreg));
}
ESP_LOGV(TAG, "L result: %f -> Calculated mode=%d, mtreg=%d", val, (int) use_mode, use_mtreg);
case WAITING_FINE_MEASUREMENT:
if (now - this->measurement_start_time_ >= this->measurement_duration_) {
this->state_ = READING_FINE_RESULT;
this->read_lx_(use_mode, use_mtreg, [this](float val) {
if (std::isnan(val)) {
this->status_set_warning();
this->publish_state(NAN);
return;
}
break;
case READING_FINE_RESULT: {
float lx;
if (!this->read_measurement_(lx)) {
this->fail_and_reset_();
break;
}
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), lx);
ESP_LOGD(TAG, "'%s': Illuminance=%.1flx", this->get_name().c_str(), val);
this->status_clear_warning();
this->publish_state(lx);
this->state_ = IDLE;
break;
}
}
}
bool BH1750Sensor::start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now) {
// Power on
uint8_t turn_on = BH1750_COMMAND_POWER_ON;
if (this->write(&turn_on, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Power on failed");
return false;
}
// Set MTreg if changed
if (this->active_mtreg_ != mtreg) {
uint8_t mtreg_hi = BH1750_COMMAND_MT_REG_HI | ((mtreg >> MTREG_HI_SHIFT) & MTREG_HI_MASK);
uint8_t mtreg_lo = BH1750_COMMAND_MT_REG_LO | ((mtreg >> MTREG_LO_SHIFT) & MTREG_LO_MASK);
if (this->write(&mtreg_hi, 1) != i2c::ERROR_OK || this->write(&mtreg_lo, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Set measurement time failed");
this->active_mtreg_ = 0;
return false;
}
this->active_mtreg_ = mtreg;
}
// Start measurement
uint8_t cmd;
uint16_t meas_time;
switch (mode) {
case BH1750_MODE_L:
cmd = BH1750_COMMAND_ONE_TIME_L;
meas_time = MEAS_TIME_L_MS * mtreg / MTREG_DEFAULT;
break;
case BH1750_MODE_H:
cmd = BH1750_COMMAND_ONE_TIME_H;
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
break;
case BH1750_MODE_H2:
cmd = BH1750_COMMAND_ONE_TIME_H2;
meas_time = MEAS_TIME_H_MS * mtreg / MTREG_DEFAULT;
break;
default:
return false;
}
if (this->write(&cmd, 1) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Start measurement failed");
return false;
}
// Store current measurement parameters
this->current_mode_ = mode;
this->current_mtreg_ = mtreg;
this->measurement_start_time_ = now;
this->measurement_duration_ = meas_time + 1; // Add 1ms for safety
return true;
}
bool BH1750Sensor::read_measurement_(float &lx_out) {
uint16_t raw_value;
if (this->read(reinterpret_cast<uint8_t *>(&raw_value), 2) != i2c::ERROR_OK) {
ESP_LOGW(TAG, "Read data failed");
return false;
}
raw_value = i2c::i2ctohs(raw_value);
float lx = float(raw_value) / RESOLUTION_DIVISOR;
lx *= float(MTREG_DEFAULT) / this->current_mtreg_;
if (this->current_mode_ == BH1750_MODE_H2) {
lx /= MODE_H2_DIVISOR;
}
lx_out = lx;
return true;
}
void BH1750Sensor::process_coarse_result_(float lx) {
if (std::isnan(lx)) {
// Use defaults if coarse measurement failed
this->fine_mode_ = BH1750_MODE_H2;
this->fine_mtreg_ = MTREG_MAX;
return;
}
if (lx <= HIGH_LIGHT_THRESHOLD_LX) {
this->fine_mode_ = BH1750_MODE_H2;
this->fine_mtreg_ = MTREG_MAX;
} else {
this->fine_mode_ = BH1750_MODE_H;
// lx = counts / 1.2 * (69 / mtreg)
// -> mtreg = counts / 1.2 * (69 / lx)
// calculate for counts=50000 (allow some range to not saturate, but maximize mtreg)
// -> mtreg = 50000*(10/12)*(69/lx)
int ideal_mtreg = COUNTS_TARGET * COUNTS_NUMERATOR * MTREG_DEFAULT / (COUNTS_DENOMINATOR * (int) lx);
this->fine_mtreg_ = std::min((int) MTREG_MAX, std::max((int) MTREG_MIN, ideal_mtreg));
}
ESP_LOGV(TAG, "L result: %.1f -> Calculated mode=%d, mtreg=%d", lx, (int) this->fine_mode_, this->fine_mtreg_);
}
void BH1750Sensor::fail_and_reset_() {
this->status_set_warning();
this->publish_state(NAN);
this->state_ = IDLE;
this->publish_state(val);
});
});
}
float BH1750Sensor::get_setup_priority() const { return setup_priority::DATA; }
} // namespace esphome::bh1750
} // namespace bh1750
} // namespace esphome

View File

@@ -4,9 +4,10 @@
#include "esphome/components/sensor/sensor.h"
#include "esphome/components/i2c/i2c.h"
namespace esphome::bh1750 {
namespace esphome {
namespace bh1750 {
enum BH1750Mode : uint8_t {
enum BH1750Mode {
BH1750_MODE_L,
BH1750_MODE_H,
BH1750_MODE_H2,
@@ -20,36 +21,13 @@ class BH1750Sensor : public sensor::Sensor, public PollingComponent, public i2c:
void setup() override;
void dump_config() override;
void update() override;
void loop() override;
float get_setup_priority() const override;
protected:
// State machine states
enum State : uint8_t {
IDLE,
WAITING_COARSE_MEASUREMENT,
READING_COARSE_RESULT,
WAITING_FINE_MEASUREMENT,
READING_FINE_RESULT,
};
void read_lx_(BH1750Mode mode, uint8_t mtreg, const std::function<void(float)> &f);
// 4-byte aligned members
uint32_t measurement_start_time_{0};
uint32_t measurement_duration_{0};
// 1-byte members grouped together to minimize padding
State state_{IDLE};
BH1750Mode current_mode_{BH1750_MODE_L};
uint8_t current_mtreg_{31};
BH1750Mode fine_mode_{BH1750_MODE_H2};
uint8_t fine_mtreg_{254};
uint8_t active_mtreg_{0};
// Helper methods
bool start_measurement_(BH1750Mode mode, uint8_t mtreg, uint32_t now);
bool read_measurement_(float &lx_out);
void process_coarse_result_(float lx);
void fail_and_reset_();
};
} // namespace esphome::bh1750
} // namespace bh1750
} // namespace esphome

View File

@@ -1,11 +1,12 @@
#include "automation.h"
#include "esphome/core/log.h"
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
static const char *const TAG = "binary_sensor.automation";
void MultiClickTrigger::on_state_(bool state) {
void binary_sensor::MultiClickTrigger::on_state_(bool state) {
// Handle duplicate events
if (state == this->last_state_) {
return;
@@ -66,7 +67,7 @@ void MultiClickTrigger::on_state_(bool state) {
*this->at_index_ = *this->at_index_ + 1;
}
void MultiClickTrigger::schedule_cooldown_() {
void binary_sensor::MultiClickTrigger::schedule_cooldown_() {
ESP_LOGV(TAG, "Multi Click: Invalid length of press, starting cooldown of %" PRIu32 " ms", this->invalid_cooldown_);
this->is_in_cooldown_ = true;
this->set_timeout("cooldown", this->invalid_cooldown_, [this]() {
@@ -78,7 +79,7 @@ void MultiClickTrigger::schedule_cooldown_() {
this->cancel_timeout("is_valid");
this->cancel_timeout("is_not_valid");
}
void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
void binary_sensor::MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
if (min_length == 0) {
this->is_valid_ = true;
return;
@@ -89,19 +90,19 @@ void MultiClickTrigger::schedule_is_valid_(uint32_t min_length) {
this->is_valid_ = true;
});
}
void MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
void binary_sensor::MultiClickTrigger::schedule_is_not_valid_(uint32_t max_length) {
this->set_timeout("is_not_valid", max_length, [this]() {
ESP_LOGV(TAG, "Multi Click: You waited too long to %s.", this->parent_->state ? "RELEASE" : "PRESS");
this->is_valid_ = false;
this->schedule_cooldown_();
});
}
void MultiClickTrigger::cancel() {
void binary_sensor::MultiClickTrigger::cancel() {
ESP_LOGV(TAG, "Multi Click: Sequence explicitly cancelled.");
this->is_valid_ = false;
this->schedule_cooldown_();
}
void MultiClickTrigger::trigger_() {
void binary_sensor::MultiClickTrigger::trigger_() {
ESP_LOGV(TAG, "Multi Click: Hooray, multi click is valid. Triggering!");
this->at_index_.reset();
this->cancel_timeout("trigger");
@@ -117,4 +118,5 @@ bool match_interval(uint32_t min_length, uint32_t max_length, uint32_t length) {
return length >= min_length && length <= max_length;
}
}
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -9,7 +9,8 @@
#include "esphome/core/helpers.h"
#include "esphome/components/binary_sensor/binary_sensor.h"
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
struct MultiClickTriggerEvent {
bool state;
@@ -171,4 +172,5 @@ template<typename... Ts> class BinarySensorInvalidateAction : public Action<Ts..
BinarySensor *sensor_;
};
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -3,7 +3,9 @@
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
static const char *const TAG = "binary_sensor";
@@ -61,4 +63,6 @@ void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
}
bool BinarySensor::is_status_binary_sensor() const { return false; }
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -6,7 +6,9 @@
#include <initializer_list>
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
class BinarySensor;
void log_binary_sensor(const char *tag, const char *prefix, const char *type, BinarySensor *obj);
@@ -68,4 +70,5 @@ class BinarySensorInitiallyOff : public BinarySensor {
bool has_state() const override { return true; }
};
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -2,7 +2,9 @@
#include "binary_sensor.h"
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
static const char *const TAG = "sensor.filter";
@@ -130,4 +132,6 @@ optional<bool> SettleFilter::new_value(bool value) {
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -4,7 +4,9 @@
#include "esphome/core/component.h"
#include "esphome/core/helpers.h"
namespace esphome::binary_sensor {
namespace esphome {
namespace binary_sensor {
class BinarySensor;
@@ -137,4 +139,6 @@ class SettleFilter : public Filter, public Component {
bool steady_{true};
};
} // namespace esphome::binary_sensor
} // namespace binary_sensor
} // namespace esphome

View File

@@ -122,19 +122,16 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
void play_complex(const Ts &...x) override {
this->num_running_++;
this->var_ = std::make_tuple(x...);
bool result;
std::vector<uint8_t> value;
if (this->len_ >= 0) {
// Static mode: write directly from flash pointer
result = this->write(this->value_.data, this->len_);
// Static mode: copy from flash to vector
value.assign(this->value_.data, this->value_.data + this->len_);
} else {
// Template mode: call function and write the vector
std::vector<uint8_t> value = this->value_.func(x...);
result = this->write(value);
// Template mode: call function
value = this->value_.func(x...);
}
// on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
if (!result)
if (!write(value))
this->play_next_(x...);
}
@@ -147,15 +144,15 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
* errors.
*/
// initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
bool write(const uint8_t *data, size_t len) {
bool write(const std::vector<uint8_t> &value) {
if (this->node_state != espbt::ClientState::ESTABLISHED) {
esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
return false;
}
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", len, format_hex_pretty(data, len).c_str());
esp_err_t err =
esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_, len,
const_cast<uint8_t *>(data), this->write_type_, ESP_GATT_AUTH_REQ_NONE);
esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
this->write_type_, ESP_GATT_AUTH_REQ_NONE);
if (err != ESP_OK) {
esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
return false;
@@ -163,8 +160,6 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
return true;
}
bool write(const std::vector<uint8_t> &value) { return this->write(value.data(), value.size()); }
void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
esp_ble_gattc_cb_param_t *param) override {
switch (event) {

View File

@@ -4,7 +4,8 @@
#include "esphome/core/automation.h"
#include "cover.h"
namespace esphome::cover {
namespace esphome {
namespace cover {
template<typename... Ts> class OpenAction : public Action<Ts...> {
public:
@@ -130,4 +131,5 @@ class CoverClosedTrigger : public Trigger<> {
}
};
} // namespace esphome::cover
} // namespace cover
} // namespace esphome

View File

@@ -6,7 +6,8 @@
#include "esphome/core/log.h"
namespace esphome::cover {
namespace esphome {
namespace cover {
static const char *const TAG = "cover";
@@ -211,4 +212,5 @@ void CoverRestoreState::apply(Cover *cover) {
cover->publish_state();
}
} // namespace esphome::cover
} // namespace cover
} // namespace esphome

View File

@@ -7,7 +7,8 @@
#include "cover_traits.h"
namespace esphome::cover {
namespace esphome {
namespace cover {
const extern float COVER_OPEN;
const extern float COVER_CLOSED;
@@ -156,4 +157,5 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
ESPPreferenceObject rtc_;
};
} // namespace esphome::cover
} // namespace cover
} // namespace esphome

View File

@@ -1,6 +1,7 @@
#pragma once
namespace esphome::cover {
namespace esphome {
namespace cover {
class CoverTraits {
public:
@@ -25,4 +26,5 @@ class CoverTraits {
bool supports_stop_{false};
};
} // namespace esphome::cover
} // namespace cover
} // namespace esphome

View File

@@ -3,10 +3,10 @@
namespace esphome {
namespace dashboard_import {
static const char *g_package_import_url = ""; // NOLINT
static std::string g_package_import_url; // NOLINT
const char *get_package_import_url() { return g_package_import_url; }
void set_package_import_url(const char *url) { g_package_import_url = url; }
const std::string &get_package_import_url() { return g_package_import_url; }
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
} // namespace dashboard_import
} // namespace esphome

View File

@@ -1,10 +1,12 @@
#pragma once
#include <string>
namespace esphome {
namespace dashboard_import {
const char *get_package_import_url();
void set_package_import_url(const char *url);
const std::string &get_package_import_url();
void set_package_import_url(std::string url);
} // namespace dashboard_import
} // namespace esphome

View File

@@ -381,9 +381,8 @@ PLATFORM_VERSION_LOOKUP = {
}
def _check_versions(config):
config = config.copy()
value = config[CONF_FRAMEWORK]
def _check_versions(value):
value = value.copy()
if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP:
if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value:
@@ -448,7 +447,7 @@ def _check_versions(config):
"If there are connectivity or build issues please remove the manual version."
)
return config
return value
def _parse_platform_version(value):
@@ -498,8 +497,6 @@ def final_validate(config):
from esphome.components.psram import DOMAIN as PSRAM_DOMAIN
errs = []
conf_fw = config[CONF_FRAMEWORK]
advanced = conf_fw[CONF_ADVANCED]
full_config = fv.full_config.get()
if pio_options := full_config[CONF_ESPHOME].get(CONF_PLATFORMIO_OPTIONS):
pio_flash_size_key = "board_upload.flash_size"
@@ -516,14 +513,22 @@ def final_validate(config):
f"Please specify {CONF_FLASH_SIZE} within esp32 configuration only"
)
)
if config[CONF_VARIANT] != VARIANT_ESP32 and advanced[CONF_IGNORE_EFUSE_MAC_CRC]:
if (
config[CONF_VARIANT] != VARIANT_ESP32
and CONF_ADVANCED in (conf_fw := config[CONF_FRAMEWORK])
and CONF_IGNORE_EFUSE_MAC_CRC in conf_fw[CONF_ADVANCED]
):
errs.append(
cv.Invalid(
f"'{CONF_IGNORE_EFUSE_MAC_CRC}' is not supported on {config[CONF_VARIANT]}",
path=[CONF_FRAMEWORK, CONF_ADVANCED, CONF_IGNORE_EFUSE_MAC_CRC],
)
)
if advanced[CONF_EXECUTE_FROM_PSRAM]:
if (
config.get(CONF_FRAMEWORK, {})
.get(CONF_ADVANCED, {})
.get(CONF_EXECUTE_FROM_PSRAM)
):
if config[CONF_VARIANT] != VARIANT_ESP32S3:
errs.append(
cv.Invalid(
@@ -539,17 +544,6 @@ def final_validate(config):
)
)
if (
config[CONF_FLASH_SIZE] == "32MB"
and "ota" in full_config
and not advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES]
):
errs.append(
cv.Invalid(
f"OTA with 32MB flash requires '{CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES}' to be set in the '{CONF_ADVANCED}' section of the esp32 configuration",
path=[CONF_FLASH_SIZE],
)
)
if errs:
raise cv.MultipleInvalid(errs)
@@ -604,74 +598,89 @@ def _validate_idf_component(config: ConfigType) -> ConfigType:
FRAMEWORK_ESP_IDF = "esp-idf"
FRAMEWORK_ARDUINO = "arduino"
FRAMEWORK_SCHEMA = cv.Schema(
{
cv.Optional(CONF_TYPE): cv.one_of(FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO),
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(CONF_RELEASE): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.string_strict,
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
cv.string_strict: cv.string_strict
},
cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of(
*LOG_LEVELS_IDF, upper=True
),
cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
{
cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of(
*ASSERTION_LEVELS, upper=True
),
cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of(
*COMPILER_OPTIMIZATIONS, upper=True
),
cv.Optional(
CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES, default=False
): cv.boolean,
cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC, default=False): cv.boolean,
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
# it will handle disabling DHCP server when AP is not configured.
# Default to false (disabled) when WiFi is not used.
cv.OnlyWithout(
CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False
): cv.boolean,
cv.Optional(CONF_ENABLE_LWIP_MDNS_QUERIES, default=True): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
): cv.boolean,
cv.Optional(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_VFS_SUPPORT_SELECT, default=True): cv.boolean,
cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM, default=False): cv.boolean,
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
min=8192, max=32768
),
}
),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.git_ref,
cv.Optional(CONF_REF): cv.string,
cv.Optional(CONF_PATH): cv.string,
cv.Optional(CONF_REFRESH): cv.All(cv.string, cv.source_refresh),
}
),
_validate_idf_component,
)
),
}
FRAMEWORK_SCHEMA = cv.All(
cv.Schema(
{
cv.Optional(CONF_TYPE, default=FRAMEWORK_ARDUINO): cv.one_of(
FRAMEWORK_ESP_IDF, FRAMEWORK_ARDUINO
),
cv.Optional(CONF_VERSION, default="recommended"): cv.string_strict,
cv.Optional(CONF_RELEASE): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.string_strict,
cv.Optional(CONF_PLATFORM_VERSION): _parse_platform_version,
cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
cv.string_strict: cv.string_strict
},
cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of(
*LOG_LEVELS_IDF, upper=True
),
cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
{
cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of(
*ASSERTION_LEVELS, upper=True
),
cv.Optional(CONF_COMPILER_OPTIMIZATION, default="SIZE"): cv.one_of(
*COMPILER_OPTIMIZATIONS, upper=True
),
cv.Optional(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES): cv.boolean,
cv.Optional(CONF_ENABLE_LWIP_ASSERT, default=True): cv.boolean,
cv.Optional(
CONF_IGNORE_EFUSE_CUSTOM_MAC, default=False
): cv.boolean,
cv.Optional(CONF_IGNORE_EFUSE_MAC_CRC): cv.boolean,
# DHCP server is needed for WiFi AP mode. When WiFi component is used,
# it will handle disabling DHCP server when AP is not configured.
# Default to false (disabled) when WiFi is not used.
cv.OnlyWithout(
CONF_ENABLE_LWIP_DHCP_SERVER, "wifi", default=False
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_MDNS_QUERIES, default=True
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_BRIDGE_INTERFACE, default=False
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, default=True
): cv.boolean,
cv.Optional(
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, default=True
): cv.boolean,
cv.Optional(
CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True
): cv.boolean,
cv.Optional(
CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True
): cv.boolean,
cv.Optional(
CONF_DISABLE_VFS_SUPPORT_SELECT, default=True
): cv.boolean,
cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
cv.Optional(CONF_LOOP_TASK_STACK_SIZE, default=8192): cv.int_range(
min=8192, max=32768
),
}
),
cv.Optional(CONF_COMPONENTS, default=[]): cv.ensure_list(
cv.All(
cv.Schema(
{
cv.Required(CONF_NAME): cv.string_strict,
cv.Optional(CONF_SOURCE): cv.git_ref,
cv.Optional(CONF_REF): cv.string,
cv.Optional(CONF_PATH): cv.string,
cv.Optional(CONF_REFRESH): cv.All(
cv.string, cv.source_refresh
),
}
),
_validate_idf_component,
)
),
}
),
_check_versions,
)
@@ -734,11 +743,11 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
def _set_default_framework(config):
config = config.copy()
if CONF_FRAMEWORK not in config:
config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({})
if CONF_TYPE not in config[CONF_FRAMEWORK]:
config = config.copy()
variant = config[CONF_VARIANT]
config[CONF_FRAMEWORK] = FRAMEWORK_SCHEMA({})
if variant in ARDUINO_ALLOWED_VARIANTS:
config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO
_show_framework_migration_message(
@@ -778,7 +787,6 @@ CONFIG_SCHEMA = cv.All(
),
_detect_variant,
_set_default_framework,
_check_versions,
set_core_data,
cv.has_at_least_one_key(CONF_BOARD, CONF_VARIANT),
)
@@ -797,7 +805,9 @@ def _configure_lwip_max_sockets(conf: dict) -> None:
from esphome.components.socket import KEY_SOCKET_CONSUMERS
# Check if user manually specified CONFIG_LWIP_MAX_SOCKETS
user_max_sockets = conf[CONF_SDKCONFIG_OPTIONS].get("CONFIG_LWIP_MAX_SOCKETS")
user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get(
"CONFIG_LWIP_MAX_SOCKETS"
)
socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {})
total_sockets = sum(socket_consumers.values())
@@ -967,18 +977,23 @@ async def to_code(config):
# WiFi component handles its own optimization when AP mode is not used
# When using Arduino with Ethernet, DHCP server functions must be available
# for the Network library to compile, even if not actively used
if advanced.get(CONF_ENABLE_LWIP_DHCP_SERVER) is False and not (
conf[CONF_TYPE] == FRAMEWORK_ARDUINO and "ethernet" in CORE.loaded_integrations
if (
CONF_ENABLE_LWIP_DHCP_SERVER in advanced
and not advanced[CONF_ENABLE_LWIP_DHCP_SERVER]
and not (
conf[CONF_TYPE] == FRAMEWORK_ARDUINO
and "ethernet" in CORE.loaded_integrations
)
):
add_idf_sdkconfig_option("CONFIG_LWIP_DHCPS", False)
if not advanced[CONF_ENABLE_LWIP_MDNS_QUERIES]:
if not advanced.get(CONF_ENABLE_LWIP_MDNS_QUERIES, True):
add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
if not advanced[CONF_ENABLE_LWIP_BRIDGE_INTERFACE]:
if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
_configure_lwip_max_sockets(conf)
if advanced[CONF_EXECUTE_FROM_PSRAM]:
if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
@@ -989,22 +1004,23 @@ async def to_code(config):
# - select() on 4 sockets: ~190μs (Arduino/core locking) vs ~235μs (ESP-IDF default)
# - Up to 200% slower under load when all operations queue through tcpip_thread
# Enabling this makes ESP-IDF socket performance match Arduino framework.
if advanced[CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING]:
if advanced.get(CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING, True):
add_idf_sdkconfig_option("CONFIG_LWIP_TCPIP_CORE_LOCKING", True)
if advanced[CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY]:
if advanced.get(CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY, True):
add_idf_sdkconfig_option("CONFIG_LWIP_CHECK_THREAD_SAFETY", True)
# Disable placing libc locks in IRAM to save RAM
# This is safe for ESPHome since no IRAM ISRs (interrupts that run while cache is disabled)
# use libc lock APIs. Saves approximately 1.3KB (1,356 bytes) of IRAM.
if advanced[CONF_DISABLE_LIBC_LOCKS_IN_IRAM]:
if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True):
add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
# Disable VFS support for termios (terminal I/O functions)
# ESPHome doesn't use termios functions on ESP32 (only used in host UART driver).
# Saves approximately 1.8KB of flash when disabled (default).
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_TERMIOS", not advanced[CONF_DISABLE_VFS_SUPPORT_TERMIOS]
"CONFIG_VFS_SUPPORT_TERMIOS",
not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True),
)
# Disable VFS support for select() with file descriptors
@@ -1018,7 +1034,8 @@ async def to_code(config):
else:
# No component needs it - allow user to control (default: disabled)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_SELECT", not advanced[CONF_DISABLE_VFS_SUPPORT_SELECT]
"CONFIG_VFS_SUPPORT_SELECT",
not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True),
)
# Disable VFS support for directory functions (opendir, readdir, mkdir, etc.)
@@ -1031,7 +1048,8 @@ async def to_code(config):
else:
# No component needs it - allow user to control (default: disabled)
add_idf_sdkconfig_option(
"CONFIG_VFS_SUPPORT_DIR", not advanced[CONF_DISABLE_VFS_SUPPORT_DIR]
"CONFIG_VFS_SUPPORT_DIR",
not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True),
)
cg.add_platformio_option("board_build.partitions", "partitions.csv")
@@ -1045,7 +1063,7 @@ async def to_code(config):
add_idf_sdkconfig_option(flag, assertion_level == key)
add_idf_sdkconfig_option("CONFIG_COMPILER_OPTIMIZATION_DEFAULT", False)
compiler_optimization = advanced[CONF_COMPILER_OPTIMIZATION]
compiler_optimization = advanced.get(CONF_COMPILER_OPTIMIZATION)
for key, flag in COMPILER_OPTIMIZATIONS.items():
add_idf_sdkconfig_option(flag, compiler_optimization == key)
@@ -1054,20 +1072,18 @@ async def to_code(config):
conf[CONF_ADVANCED][CONF_ENABLE_LWIP_ASSERT],
)
if advanced[CONF_IGNORE_EFUSE_MAC_CRC]:
if advanced.get(CONF_IGNORE_EFUSE_MAC_CRC):
add_idf_sdkconfig_option("CONFIG_ESP_MAC_IGNORE_MAC_CRC_ERROR", True)
add_idf_sdkconfig_option("CONFIG_ESP_PHY_CALIBRATION_AND_DATA_STORAGE", False)
if advanced[CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES]:
if advanced.get(CONF_ENABLE_IDF_EXPERIMENTAL_FEATURES):
_LOGGER.warning(
"Using experimental features in ESP-IDF may result in unexpected failures."
)
add_idf_sdkconfig_option("CONFIG_IDF_EXPERIMENTAL_FEATURES", True)
if config[CONF_FLASH_SIZE] == "32MB":
add_idf_sdkconfig_option(
"CONFIG_BOOTLOADER_CACHE_32BIT_ADDR_QUAD_FLASH", True
)
cg.add_define("ESPHOME_LOOP_TASK_STACK_SIZE", advanced[CONF_LOOP_TASK_STACK_SIZE])
cg.add_define(
"ESPHOME_LOOP_TASK_STACK_SIZE", advanced.get(CONF_LOOP_TASK_STACK_SIZE)
)
cg.add_define(
"USE_ESP_IDF_VERSION_CODE",

View File

@@ -96,10 +96,6 @@ void ESP32BLE::advertising_set_service_data(const std::vector<uint8_t> &data) {
}
void ESP32BLE::advertising_set_manufacturer_data(const std::vector<uint8_t> &data) {
this->advertising_set_manufacturer_data(std::span<const uint8_t>(data));
}
void ESP32BLE::advertising_set_manufacturer_data(std::span<const uint8_t> data) {
this->advertising_init_();
this->advertising_->set_manufacturer_data(data);
this->advertising_start();

View File

@@ -118,7 +118,6 @@ class ESP32BLE : public Component {
void advertising_start();
void advertising_set_service_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
void advertising_set_manufacturer_data(std::span<const uint8_t> data);
void advertising_set_appearance(uint16_t appearance) { this->appearance_ = appearance; }
void advertising_set_service_data_and_name(std::span<const uint8_t> data, bool include_name);
void advertising_add_service_uuid(ESPBTUUID uuid);

View File

@@ -59,10 +59,6 @@ void BLEAdvertising::set_service_data(const std::vector<uint8_t> &data) {
}
void BLEAdvertising::set_manufacturer_data(const std::vector<uint8_t> &data) {
this->set_manufacturer_data(std::span<const uint8_t>(data));
}
void BLEAdvertising::set_manufacturer_data(std::span<const uint8_t> data) {
delete[] this->advertising_data_.p_manufacturer_data;
this->advertising_data_.p_manufacturer_data = nullptr;
this->advertising_data_.manufacturer_len = data.size();

View File

@@ -37,7 +37,6 @@ class BLEAdvertising {
void set_scan_response(bool scan_response) { this->scan_response_ = scan_response; }
void set_min_preferred_interval(uint16_t interval) { this->advertising_data_.min_interval = interval; }
void set_manufacturer_data(const std::vector<uint8_t> &data);
void set_manufacturer_data(std::span<const uint8_t> data);
void set_appearance(uint16_t appearance) { this->advertising_data_.appearance = appearance; }
void set_service_data(const std::vector<uint8_t> &data);
void set_service_data(std::span<const uint8_t> data);

View File

@@ -1,6 +1,5 @@
#include "esp32_ble_beacon.h"
#include "esphome/core/log.h"
#include "esphome/core/helpers.h"
#ifdef USE_ESP32

View File

@@ -15,10 +15,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;
@@ -30,10 +27,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger = // NOLINT(cppcoreguidelines-owning-memory)
new Trigger<std::vector<uint8_t>, uint16_t>();
descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
// Convert span to vector for trigger - copy is necessary because:
// 1. Trigger stores the data for use in automation actions that execute later
// 2. The span is only valid during this callback (points to temporary BLE stack data)
// 3. User lambdas in automations need persistent data they can access asynchronously
// Convert span to vector for trigger
on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
});
return on_write_trigger;

View File

@@ -383,7 +383,6 @@ async def to_code(config):
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
if CONF_MANUAL_IP in config:
cg.add_define("USE_ETHERNET_MANUAL_IP")
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
# Add compile-time define for PHY types with specific code

View File

@@ -553,14 +553,11 @@ void EthernetComponent::start_connect_() {
}
esp_netif_ip_info_t info;
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
info.ip = this->manual_ip_->static_ip;
info.gw = this->manual_ip_->gateway;
info.netmask = this->manual_ip_->subnet;
} else
#endif
{
} else {
info.ip.addr = 0;
info.gw.addr = 0;
info.netmask.addr = 0;
@@ -581,7 +578,6 @@ void EthernetComponent::start_connect_() {
err = esp_netif_set_ip_info(this->eth_netif_, &info);
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
@@ -594,9 +590,7 @@ void EthernetComponent::start_connect_() {
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
} else
#endif
{
} else {
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
@@ -694,9 +688,7 @@ void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->cl
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
#endif
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
#ifdef USE_ETHERNET_MANUAL_IP
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
#endif
// set_use_address() is guaranteed to be called during component setup by Python code generation,
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.

View File

@@ -82,9 +82,7 @@ class EthernetComponent : public Component {
void add_phy_register(PHYRegister register_value);
#endif
void set_type(EthernetType type);
#ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip);
#endif
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
network::IPAddresses get_ip_addresses();
@@ -139,9 +137,7 @@ class EthernetComponent : public Component {
uint8_t mdc_pin_{23};
uint8_t mdio_pin_{18};
#endif
#ifdef USE_ETHERNET_MANUAL_IP
optional<ManualIP> manual_ip_{};
#endif
uint32_t connect_begin_;
// Group all uint8_t types together (enums and bools)

View File

@@ -36,6 +36,7 @@ from esphome.const import (
CONF_WEIGHT,
)
from esphome.core import CORE, HexInt
from esphome.helpers import cpp_string_escape
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
@@ -49,6 +50,7 @@ font_ns = cg.esphome_ns.namespace("font")
Font = font_ns.class_("Font")
Glyph = font_ns.class_("Glyph")
GlyphData = font_ns.struct("GlyphData")
CONF_BPP = "bpp"
CONF_EXTRAS = "extras"
@@ -461,7 +463,7 @@ FONT_SCHEMA = cv.Schema(
)
),
cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8),
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(Glyph),
cv.GenerateID(CONF_RAW_GLYPH_ID): cv.declare_id(GlyphData),
},
)
@@ -486,8 +488,6 @@ class GlyphInfo:
def glyph_to_glyphinfo(glyph, font, size, bpp):
# Convert to 32 bit unicode codepoint
glyph = ord(glyph)
scale = 256 // (1 << bpp)
if not font.is_scalable:
sizes = [pt_to_px(x.size) for x in font.available_sizes]
@@ -583,15 +583,22 @@ async def to_code(config):
# Create the glyph table that points to data in the above array.
glyph_initializer = [
[
x.glyph,
prog_arr + (y - len(x.bitmap_data)),
x.advance,
x.offset_x,
x.offset_y,
x.width,
x.height,
]
cg.StructInitializer(
GlyphData,
(
"a_char",
cg.RawExpression(f"(const uint8_t *){cpp_string_escape(x.glyph)}"),
),
(
"data",
cg.RawExpression(f"{str(prog_arr)} + {str(y - len(x.bitmap_data))}"),
),
("advance", x.advance),
("offset_x", x.offset_x),
("offset_y", x.offset_y),
("width", x.width),
("height", x.height),
)
for (x, y) in zip(
glyph_args, list(accumulate([len(x.bitmap_data) for x in glyph_args]))
)

View File

@@ -6,245 +6,133 @@
namespace esphome {
namespace font {
static const char *const TAG = "font";
#ifdef USE_LVGL_FONT
const uint8_t *Font::get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
auto *fe = (Font *) font->dsc;
const auto *gd = fe->get_glyph_data_(unicode_letter);
if (gd == nullptr) {
return nullptr;
const uint8_t *Glyph::get_char() const { return this->glyph_data_->a_char; }
// Compare the char at the string position with this char.
// Return true if this char is less than or equal the other.
bool Glyph::compare_to(const uint8_t *str) const {
// 1 -> this->char_
// 2 -> str
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return true;
if (str[i] == '\0')
return false;
if (this->glyph_data_->a_char[i] > str[i])
return false;
if (this->glyph_data_->a_char[i] < str[i])
return true;
}
return gd->data;
// this should not happen
return false;
}
int Glyph::match_length(const uint8_t *str) const {
for (uint32_t i = 0;; i++) {
if (this->glyph_data_->a_char[i] == '\0')
return i;
if (str[i] != this->glyph_data_->a_char[i])
return 0;
}
// this should not happen
return 0;
}
void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const {
*x1 = this->glyph_data_->offset_x;
*y1 = this->glyph_data_->offset_y;
*width = this->glyph_data_->width;
*height = this->glyph_data_->height;
}
bool Font::get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
auto *fe = (Font *) font->dsc;
const auto *gd = fe->get_glyph_data_(unicode_letter);
if (gd == nullptr) {
return false;
}
dsc->adv_w = gd->advance;
dsc->ofs_x = gd->offset_x;
dsc->ofs_y = fe->height_ - gd->height - gd->offset_y - fe->lv_font_.base_line;
dsc->box_w = gd->width;
dsc->box_h = gd->height;
dsc->is_placeholder = 0;
dsc->bpp = fe->get_bpp();
return true;
}
const Glyph *Font::get_glyph_data_(uint32_t unicode_letter) {
if (unicode_letter == this->last_letter_ && this->last_letter_ != 0)
return this->last_data_;
auto *glyph = this->find_glyph(unicode_letter);
if (glyph == nullptr) {
return nullptr;
}
this->last_data_ = glyph;
this->last_letter_ = unicode_letter;
return glyph;
}
#endif
/**
* Attempt to extract a 32 bit Unicode codepoint from a UTF-8 string.
* If successful, return the codepoint and set the length to the number of bytes read.
* If the end of the string has been reached and a valid codepoint has not been found, return 0 and set the length to
* 0.
*
* @param utf8_str The input string
* @param length Pointer to length storage
* @return The extracted code point
*/
static uint32_t extract_unicode_codepoint(const char *utf8_str, size_t *length) {
// Safely cast to uint8_t* for correct bitwise operations on bytes
const uint8_t *current = reinterpret_cast<const uint8_t *>(utf8_str);
uint32_t code_point = 0;
uint8_t c1 = *current++;
// check for end of string
if (c1 == 0) {
*length = 0;
return 0;
}
// --- 1-Byte Sequence: 0xxxxxxx (ASCII) ---
if (c1 < 0x80) {
// Valid ASCII byte.
code_point = c1;
// Optimization: No need to check for continuation bytes.
}
// --- 2-Byte Sequence: 110xxxxx 10xxxxxx ---
else if ((c1 & 0xE0) == 0xC0) {
uint8_t c2 = *current++;
// Error Check 1: Check if c2 is a valid continuation byte (10xxxxxx)
if ((c2 & 0xC0) != 0x80) {
*length = 0;
return 0;
}
code_point = (c1 & 0x1F) << 6;
code_point |= (c2 & 0x3F);
// Error Check 2: Overlong check (2-byte must be > 0x7F)
if (code_point <= 0x7F) {
*length = 0;
return 0;
}
}
// --- 3-Byte Sequence: 1110xxxx 10xxxxxx 10xxxxxx ---
else if ((c1 & 0xF0) == 0xE0) {
uint8_t c2 = *current++;
uint8_t c3 = *current++;
// Error Check 1: Check continuation bytes
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80)) {
*length = 0;
return 0;
}
code_point = (c1 & 0x0F) << 12;
code_point |= (c2 & 0x3F) << 6;
code_point |= (c3 & 0x3F);
// Error Check 2: Overlong check (3-byte must be > 0x7FF)
// Also check for surrogates (0xD800-0xDFFF)
if (code_point <= 0x7FF || (code_point >= 0xD800 && code_point <= 0xDFFF)) {
*length = 0;
return 0;
}
}
// --- 4-Byte Sequence: 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx ---
else if ((c1 & 0xF8) == 0xF0) {
uint8_t c2 = *current++;
uint8_t c3 = *current++;
uint8_t c4 = *current++;
// Error Check 1: Check continuation bytes
if (((c2 & 0xC0) != 0x80) || ((c3 & 0xC0) != 0x80) || ((c4 & 0xC0) != 0x80)) {
*length = 0;
return 0;
}
code_point = (c1 & 0x07) << 18;
code_point |= (c2 & 0x3F) << 12;
code_point |= (c3 & 0x3F) << 6;
code_point |= (c4 & 0x3F);
// Error Check 2: Overlong check (4-byte must be > 0xFFFF)
// Also check for valid Unicode range (must be <= 0x10FFFF)
if (code_point <= 0xFFFF || code_point > 0x10FFFF) {
*length = 0;
return 0;
}
}
// --- Invalid leading byte (e.g., 10xxxxxx or 11111xxx) ---
else {
*length = 0;
return 0;
}
*length = current - reinterpret_cast<const uint8_t *>(utf8_str);
return code_point;
}
Font::Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
Font::Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
uint8_t bpp)
: glyphs_(ConstVector(data, data_nr)),
baseline_(baseline),
: baseline_(baseline),
height_(height),
descender_(descender),
linegap_(height - baseline - descender),
xheight_(xheight),
capheight_(capheight),
bpp_(bpp) {
#ifdef USE_LVGL_FONT
this->lv_font_.dsc = this;
this->lv_font_.line_height = this->get_height();
this->lv_font_.base_line = this->lv_font_.line_height - this->get_baseline();
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
this->lv_font_.underline_position = -1;
this->lv_font_.underline_thickness = 1;
#endif
glyphs_.reserve(data_nr);
for (int i = 0; i < data_nr; ++i)
glyphs_.emplace_back(&data[i]);
}
const Glyph *Font::find_glyph(uint32_t codepoint) const {
int Font::match_next_glyph(const uint8_t *str, int *match_length) {
int lo = 0;
int hi = this->glyphs_.size() - 1;
while (lo != hi) {
int mid = (lo + hi + 1) / 2;
if (this->glyphs_[mid].is_less_or_equal(codepoint)) {
if (this->glyphs_[mid].compare_to(str)) {
lo = mid;
} else {
hi = mid - 1;
}
}
auto *result = &this->glyphs_[lo];
if (result->code_point == codepoint)
return result;
return nullptr;
*match_length = this->glyphs_[lo].match_length(str);
if (*match_length <= 0)
return -1;
return lo;
}
#ifdef USE_DISPLAY
void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) {
*baseline = this->baseline_;
*height = this->height_;
int i = 0;
int min_x = 0;
bool has_char = false;
int x = 0;
for (;;) {
size_t length;
auto code_point = extract_unicode_codepoint(str, &length);
if (length == 0)
break;
str += length;
auto *glyph = this->find_glyph(code_point);
if (glyph == nullptr) {
while (str[i] != '\0') {
int match_length;
int glyph_n = this->match_next_glyph((const uint8_t *) str + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
if (!this->glyphs_.empty())
x += this->glyphs_[0].advance;
if (!this->get_glyphs().empty())
x += this->get_glyphs()[0].glyph_data_->advance;
i++;
continue;
}
const Glyph &glyph = this->glyphs_[glyph_n];
if (!has_char) {
min_x = glyph->offset_x;
min_x = glyph.glyph_data_->offset_x;
} else {
min_x = std::min(min_x, x + glyph->offset_x);
min_x = std::min(min_x, x + glyph.glyph_data_->offset_x);
}
x += glyph->advance;
x += glyph.glyph_data_->advance;
i += match_length;
has_char = true;
}
*x_offset = min_x;
*width = x - min_x;
}
void Font::print(int x_start, int y_start, display::Display *display, Color color, const char *text, Color background) {
int i = 0;
int x_at = x_start;
for (;;) {
size_t length;
auto code_point = extract_unicode_codepoint(text, &length);
if (length == 0)
break;
text += length;
auto *glyph = this->find_glyph(code_point);
if (glyph == nullptr) {
int scan_x1, scan_y1, scan_width, scan_height;
while (text[i] != '\0') {
int match_length;
int glyph_n = this->match_next_glyph((const uint8_t *) text + i, &match_length);
if (glyph_n < 0) {
// Unknown char, skip
ESP_LOGW(TAG, "Codepoint 0x%08" PRIx32 " not found in font", code_point);
if (!this->glyphs_.empty()) {
uint8_t glyph_width = this->glyphs_[0].advance;
display->rectangle(x_at, y_start, glyph_width, this->height_, color);
ESP_LOGW(TAG, "Encountered character without representation in font: '%c'", text[i]);
if (!this->get_glyphs().empty()) {
uint8_t glyph_width = this->get_glyphs()[0].glyph_data_->advance;
display->filled_rectangle(x_at, y_start, glyph_width, this->height_, color);
x_at += glyph_width;
}
i++;
continue;
}
const uint8_t *data = glyph->data;
const int max_x = x_at + glyph->offset_x + glyph->width;
const int max_y = y_start + glyph->offset_y + glyph->height;
const Glyph &glyph = this->get_glyphs()[glyph_n];
glyph.scan_area(&scan_x1, &scan_y1, &scan_width, &scan_height);
const uint8_t *data = glyph.glyph_data_->data;
const int max_x = x_at + scan_x1 + scan_width;
const int max_y = y_start + scan_y1 + scan_height;
uint8_t bitmask = 0;
uint8_t pixel_data = 0;
@@ -257,10 +145,10 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
auto b_g = (float) background.g;
auto b_b = (float) background.b;
auto b_w = (float) background.w;
for (int glyph_y = y_start + glyph->offset_y; glyph_y != max_y; glyph_y++) {
for (int glyph_x = x_at + glyph->offset_x; glyph_x != max_x; glyph_x++) {
for (int glyph_y = y_start + scan_y1; glyph_y != max_y; glyph_y++) {
for (int glyph_x = x_at + scan_x1; glyph_x != max_x; glyph_x++) {
uint8_t pixel = 0;
for (uint8_t bit_num = 0; bit_num != this->bpp_; bit_num++) {
for (int bit_num = 0; bit_num != this->bpp_; bit_num++) {
if (bitmask == 0) {
pixel_data = progmem_read_byte(data++);
bitmask = 0x80;
@@ -280,9 +168,12 @@ void Font::print(int x_start, int y_start, display::Display *display, Color colo
}
}
}
x_at += glyph->advance;
x_at += glyph.glyph_data_->advance;
i += match_length;
}
}
#endif
} // namespace font
} // namespace esphome

View File

@@ -6,30 +6,14 @@
#ifdef USE_DISPLAY
#include "esphome/components/display/display.h"
#endif
#ifdef USE_LVGL_FONT
#include <lvgl.h>
#endif
namespace esphome {
namespace font {
class Font;
class Glyph {
public:
constexpr Glyph(uint32_t code_point, const uint8_t *data, int advance, int offset_x, int offset_y, int width,
int height)
: code_point(code_point),
data(data),
advance(advance),
offset_x(offset_x),
offset_y(offset_y),
width(width),
height(height) {}
bool is_less_or_equal(uint32_t other) const { return this->code_point <= other; }
const uint32_t code_point;
struct GlyphData {
const uint8_t *a_char;
const uint8_t *data;
int advance;
int offset_x;
@@ -38,6 +22,26 @@ class Glyph {
int height;
};
class Glyph {
public:
Glyph(const GlyphData *data) : glyph_data_(data) {}
const uint8_t *get_char() const;
bool compare_to(const uint8_t *str) const;
int match_length(const uint8_t *str) const;
void scan_area(int *x1, int *y1, int *width, int *height) const;
const GlyphData *get_glyph_data() const { return this->glyph_data_; }
protected:
friend Font;
const GlyphData *glyph_data_;
};
class Font
#ifdef USE_DISPLAY
: public display::BaseFont
@@ -46,8 +50,8 @@ class Font
public:
/** Construct the font with the given glyphs.
*
* @param data A list of glyphs, must be sorted lexicographically.
* @param data_nr The number of glyphs
* @param data A vector of glyphs, must be sorted lexicographically.
* @param data_nr The number of glyphs in data.
* @param baseline The y-offset from the top of the text to the baseline.
* @param height The y-offset from the top of the text to the bottom.
* @param descender The y-offset from the baseline to the lowest stroke in the font (e.g. from letters like g or p).
@@ -55,10 +59,10 @@ class Font
* @param capheight The height of capital letters, usually measured at the "X" glyph.
* @param bpp The bits per pixel used for this font. Used to read data out of the glyph bitmaps.
*/
Font(const Glyph *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
Font(const GlyphData *data, int data_nr, int baseline, int height, int descender, int xheight, int capheight,
uint8_t bpp = 1);
const Glyph *find_glyph(uint32_t codepoint) const;
int match_next_glyph(const uint8_t *str, int *match_length);
#ifdef USE_DISPLAY
void print(int x_start, int y_start, display::Display *display, Color color, const char *text,
@@ -73,14 +77,11 @@ class Font
inline int get_xheight() { return this->xheight_; }
inline int get_capheight() { return this->capheight_; }
inline int get_bpp() { return this->bpp_; }
#ifdef USE_LVGL_FONT
const lv_font_t *get_lv_font() const { return &this->lv_font_; }
#endif
const ConstVector<Glyph> &get_glyphs() const { return glyphs_; }
const std::vector<Glyph, RAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; }
protected:
ConstVector<Glyph> glyphs_;
std::vector<Glyph, RAMAllocator<Glyph>> glyphs_;
int baseline_;
int height_;
int descender_;
@@ -88,14 +89,6 @@ class Font
int xheight_;
int capheight_;
uint8_t bpp_; // bits per pixel
#ifdef USE_LVGL_FONT
lv_font_t lv_font_{};
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter);
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next);
const Glyph *get_glyph_data_(uint32_t unicode_letter);
uint32_t last_letter_{};
const Glyph *last_data_{};
#endif
};
} // namespace font

View File

@@ -107,7 +107,7 @@ void IDFI2CBus::dump_config() {
if (s.second) {
ESP_LOGCONFIG(TAG, "Found device at address 0x%02X", s.first);
} else {
ESP_LOGCONFIG(TAG, "Unknown error at address 0x%02X", s.first);
ESP_LOGE(TAG, "Unknown error at address 0x%02X", s.first);
}
}
}

View File

@@ -31,83 +31,35 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_SIGNAL,
unit_of_measurement=UNIT_CENTIMETER,
),
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_SIGNAL,
unit_of_measurement=UNIT_CENTIMETER,
),
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_MOTION_SENSOR,
unit_of_measurement=UNIT_PERCENT,
),
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_FLASH,
unit_of_measurement=UNIT_PERCENT,
),
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
device_class=DEVICE_CLASS_ILLUMINANCE,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_LIGHTBULB,
),
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_SIGNAL,
unit_of_measurement=UNIT_CENTIMETER,
),
@@ -121,13 +73,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
],
icon=ICON_MOTION_SENSOR,
unit_of_measurement=UNIT_PERCENT,
@@ -135,13 +81,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
],
icon=ICON_FLASH,
unit_of_measurement=UNIT_PERCENT,

View File

@@ -31,84 +31,36 @@ CONFIG_SCHEMA = cv.Schema(
cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_SIGNAL,
unit_of_measurement=UNIT_CENTIMETER,
),
cv.Optional(CONF_LIGHT): sensor.sensor_schema(
device_class=DEVICE_CLASS_ILLUMINANCE,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_LIGHTBULB,
unit_of_measurement=UNIT_EMPTY, # No standard unit for this light sensor
),
cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_SIGNAL,
unit_of_measurement=UNIT_CENTIMETER,
),
cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_MOTION_SENSOR,
unit_of_measurement=UNIT_PERCENT,
),
cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
device_class=DEVICE_CLASS_DISTANCE,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_SIGNAL,
unit_of_measurement=UNIT_CENTIMETER,
),
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
],
filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
icon=ICON_FLASH,
unit_of_measurement=UNIT_PERCENT,
),
@@ -122,13 +74,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
],
icon=ICON_MOTION_SENSOR,
unit_of_measurement=UNIT_PERCENT,
@@ -136,13 +82,7 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
filters=[
{
"timeout": {
"timeout": cv.TimePeriod(milliseconds=1000),
"value": "last",
}
},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)},
{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
],
icon=ICON_FLASH,
unit_of_measurement=UNIT_PERCENT,

View File

@@ -52,10 +52,8 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
}
static const LogString *color_mode_to_human(ColorMode color_mode) {
if (color_mode == ColorMode::ON_OFF)
return LOG_STR("On/Off");
if (color_mode == ColorMode::BRIGHTNESS)
return LOG_STR("Brightness");
if (color_mode == ColorMode::UNKNOWN)
return LOG_STR("Unknown");
if (color_mode == ColorMode::WHITE)
return LOG_STR("White");
if (color_mode == ColorMode::COLOR_TEMPERATURE)
@@ -70,7 +68,7 @@ static const LogString *color_mode_to_human(ColorMode color_mode) {
return LOG_STR("RGB + cold/warm white");
if (color_mode == ColorMode::RGB_COLOR_TEMPERATURE)
return LOG_STR("RGB + color temperature");
return LOG_STR("Unknown");
return LOG_STR("");
}
// Helper to log percentage values

View File

@@ -365,10 +365,8 @@ async def to_code(config):
if CORE.is_esp32:
if config[CONF_HARDWARE_UART] == USB_CDC:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_CDC", True)
cg.add_define("USE_LOGGER_UART_SELECTION_USB_CDC")
elif config[CONF_HARDWARE_UART] == USB_SERIAL_JTAG:
add_idf_sdkconfig_option("CONFIG_ESP_CONSOLE_USB_SERIAL_JTAG", True)
cg.add_define("USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG")
try:
uart_selection(USB_SERIAL_JTAG)
cg.add_define("USE_LOGGER_USB_SERIAL_JTAG")

View File

@@ -65,9 +65,7 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch
uint16_t buffer_at = 0; // Initialize buffer position
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, console_buffer, &buffer_at,
MAX_CONSOLE_LOG_MSG_SIZE);
// Add newline if platform needs it (ESP32 doesn't add via write_msg_)
this->add_newline_to_buffer_if_needed_(console_buffer, &buffer_at, MAX_CONSOLE_LOG_MSG_SIZE);
this->write_msg_(console_buffer, buffer_at);
this->write_msg_(console_buffer);
}
// Reset the recursion guard for this task
@@ -133,19 +131,18 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas
// Save the offset before calling format_log_to_buffer_with_terminator_
// since it will increment tx_buffer_at_ to the end of the formatted string
uint16_t msg_start = this->tx_buffer_at_;
uint32_t msg_start = this->tx_buffer_at_;
this->format_log_to_buffer_with_terminator_(level, tag, line, this->tx_buffer_, args, this->tx_buffer_,
&this->tx_buffer_at_, this->tx_buffer_size_);
uint16_t msg_length =
// Write to console and send callback starting at the msg_start
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_ + msg_start);
}
size_t msg_length =
this->tx_buffer_at_ - msg_start; // Don't subtract 1 - tx_buffer_at_ is already at the null terminator position
// Callbacks get message first (before console write)
this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start, msg_length);
// Write to console starting at the msg_start
this->write_tx_buffer_to_console_(msg_start, &msg_length);
global_recursion_guard_ = false;
}
#endif // USE_STORE_LOG_STR_IN_FLASH
@@ -212,7 +209,9 @@ void Logger::process_messages_() {
// This ensures all log messages appear on the console in a clean, serialized manner
// Note: Messages may appear slightly out of order due to async processing, but
// this is preferred over corrupted/interleaved console output
this->write_tx_buffer_to_console_();
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_);
}
}
} else {
// No messages to process, disable loop if appropriate

View File

@@ -71,17 +71,6 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128;
// "0x" + 2 hex digits per byte + '\0'
static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1;
// Platform-specific: does write_msg_ add its own newline?
// false: Caller must add newline to buffer before calling write_msg_ (ESP32, ESP8266, LibreTiny)
// Allows single write call with newline included for efficiency
// true: write_msg_ adds newline itself via puts()/println() (other platforms)
// Newline should NOT be added to buffer
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_LIBRETINY)
static constexpr bool WRITE_MSG_ADDS_NEWLINE = false;
#else
static constexpr bool WRITE_MSG_ADDS_NEWLINE = true;
#endif
#if defined(USE_ESP32) || defined(USE_ESP8266) || defined(USE_RP2040) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR)
/** Enum for logging UART selection
*
@@ -184,7 +173,7 @@ class Logger : public Component {
protected:
void process_messages_();
void write_msg_(const char *msg, size_t len);
void write_msg_(const char *msg);
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
// It's the caller's responsibility to initialize buffer_at (typically to 0)
@@ -211,35 +200,6 @@ class Logger : public Component {
}
}
// Helper to add newline to buffer for platforms that need it
// Modifies buffer_at to include the newline
inline void HOT add_newline_to_buffer_if_needed_(char *buffer, uint16_t *buffer_at, uint16_t buffer_size) {
if constexpr (!WRITE_MSG_ADDS_NEWLINE) {
// Add newline - don't need to maintain null termination
// write_msg_ now always receives explicit length, so we can safely overwrite the null terminator
// This is safe because:
// 1. Callbacks already received the message (before we add newline)
// 2. write_msg_ receives the length explicitly (doesn't need null terminator)
if (*buffer_at < buffer_size) {
buffer[(*buffer_at)++] = '\n';
} else if (buffer_size > 0) {
// Buffer was full - replace last char with newline to ensure it's visible
buffer[buffer_size - 1] = '\n';
*buffer_at = buffer_size;
}
}
}
// Helper to write tx_buffer_ to console if logging is enabled
// INTERNAL USE ONLY - offset > 0 requires length parameter to be non-null
inline void HOT write_tx_buffer_to_console_(uint16_t offset = 0, uint16_t *length = nullptr) {
if (this->baud_rate_ > 0) {
uint16_t *len_ptr = length ? length : &this->tx_buffer_at_;
this->add_newline_to_buffer_if_needed_(this->tx_buffer_ + offset, len_ptr, this->tx_buffer_size_ - offset);
this->write_msg_(this->tx_buffer_ + offset, *len_ptr);
}
}
// Helper to format and send a log message to both console and callbacks
inline void HOT log_message_to_buffer_and_send_(uint8_t level, const char *tag, int line, const char *format,
va_list args) {
@@ -248,11 +208,10 @@ class Logger : public Component {
this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, this->tx_buffer_, &this->tx_buffer_at_,
this->tx_buffer_size_);
// Callbacks get message WITHOUT newline (for API/MQTT/syslog)
if (this->baud_rate_ > 0) {
this->write_msg_(this->tx_buffer_); // If logging is enabled, write to console
}
this->log_callback_.call(level, tag, this->tx_buffer_, this->tx_buffer_at_);
// Console gets message WITH newline (if platform needs it)
this->write_tx_buffer_to_console_();
}
// Write the body of the log message to the buffer
@@ -466,9 +425,7 @@ class Logger : public Component {
}
// Update buffer_at with the formatted length (handle truncation)
// When vsnprintf truncates (ret >= remaining), it writes (remaining - 1) chars + null terminator
// When it doesn't truncate (ret < remaining), it writes ret chars + null terminator
uint16_t formatted_len = (ret >= remaining) ? (remaining - 1) : ret;
uint16_t formatted_len = (ret >= remaining) ? remaining : ret;
*buffer_at += formatted_len;
// Remove all trailing newlines right after formatting

View File

@@ -121,23 +121,25 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Length is now always passed explicitly - no strlen() fallback needed
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
// USB CDC/JTAG - single write including newline (already in buffer)
// Use fwrite to stdout which goes through VFS to USB console
//
// Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG).
// They are ONLY defined when the user explicitly selects USB as the logger output in their config.
// This is compile-time selection, not runtime detection - if USB is configured, it's always used.
// There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility
// to configure correctly for their hardware. This approach eliminates runtime overhead.
fwrite(msg, 1, len, stdout);
void HOT Logger::write_msg_(const char *msg) {
if (
#if defined(USE_LOGGER_USB_CDC) && !defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC
#elif defined(USE_LOGGER_USB_SERIAL_JTAG) && !defined(USE_LOGGER_USB_CDC)
this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
#elif defined(USE_LOGGER_USB_CDC) && defined(USE_LOGGER_USB_SERIAL_JTAG)
this->uart_ == UART_SELECTION_USB_CDC || this->uart_ == UART_SELECTION_USB_SERIAL_JTAG
#else
// Regular UART - single write including newline (already in buffer)
uart_write_bytes(this->uart_num_, msg, len);
/* DISABLES CODE */ (false) // NOLINT
#endif
) {
puts(msg);
} else {
// Use tx_buffer_at_ if msg points to tx_buffer_, otherwise fall back to strlen
size_t len = (msg == this->tx_buffer_) ? this->tx_buffer_at_ : strlen(msg);
uart_write_bytes(this->uart_num_, msg, len);
uart_write_bytes(this->uart_num_, "\n", 1);
}
}
const LogString *Logger::get_uart_selection_() {

View File

@@ -33,10 +33,7 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg, size_t len) {
// Single write with newline already in buffer (added by caller)
this->hw_serial_->write(msg, len);
}
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {

View File

@@ -3,7 +3,7 @@
namespace esphome::logger {
void HOT Logger::write_msg_(const char *msg, size_t) {
void HOT Logger::write_msg_(const char *msg) {
time_t rawtime;
struct tm *timeinfo;
char buffer[80];

View File

@@ -49,7 +49,7 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg, size_t len) { this->hw_serial_->write(msg, len); }
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {

View File

@@ -27,7 +27,7 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg, size_t) { this->hw_serial_->println(msg); }
void HOT Logger::write_msg_(const char *msg) { this->hw_serial_->println(msg); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {

View File

@@ -62,7 +62,7 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg, size_t) {
void HOT Logger::write_msg_(const char *msg) {
#ifdef CONFIG_PRINTK
printk("%s\n", msg);
#endif

View File

@@ -52,7 +52,15 @@ from .schemas import (
from .styles import add_top_layer, styles_to_code, theme_to_code
from .touchscreens import touchscreen_schema, touchscreens_to_code
from .trigger import add_on_boot_triggers, generate_triggers
from .types import IdleTrigger, PlainTrigger, lv_font_t, lv_group_t, lv_style_t, lvgl_ns
from .types import (
FontEngine,
IdleTrigger,
PlainTrigger,
lv_font_t,
lv_group_t,
lv_style_t,
lvgl_ns,
)
from .widgets import (
LvScrActType,
Widget,
@@ -236,6 +244,7 @@ async def to_code(configs):
cg.add_global(lvgl_ns.using)
for font in helpers.esphome_fonts_used:
await cg.get_variable(font)
cg.new_Pvariable(ID(f"{font}_engine", True, type=FontEngine), MockObj(font))
default_font = config_0[df.CONF_DEFAULT_FONT]
if not lvalid.is_lv_font(default_font):
add_define(
@@ -247,8 +256,7 @@ async def to_code(configs):
type=lv_font_t.operator("ptr").operator("const"),
)
cg.new_variable(
globfont_id,
MockObj(await lvalid.lv_font.process(default_font), "->").get_lv_font(),
globfont_id, MockObj(await lvalid.lv_font.process(default_font))
)
add_define("LV_FONT_DEFAULT", df.DEFAULT_ESPHOME_FONT)
else:

View File

@@ -0,0 +1,76 @@
#include "lvgl_esphome.h"
#ifdef USE_LVGL_FONT
namespace esphome {
namespace lvgl {
static const uint8_t *get_glyph_bitmap(const lv_font_t *font, uint32_t unicode_letter) {
auto *fe = (FontEngine *) font->dsc;
const auto *gd = fe->get_glyph_data(unicode_letter);
if (gd == nullptr)
return nullptr;
// esph_log_d(TAG, "Returning bitmap @ %X", (uint32_t)gd->data);
return gd->data;
}
static bool get_glyph_dsc_cb(const lv_font_t *font, lv_font_glyph_dsc_t *dsc, uint32_t unicode_letter, uint32_t next) {
auto *fe = (FontEngine *) font->dsc;
const auto *gd = fe->get_glyph_data(unicode_letter);
if (gd == nullptr)
return false;
dsc->adv_w = gd->advance;
dsc->ofs_x = gd->offset_x;
dsc->ofs_y = fe->height - gd->height - gd->offset_y - fe->baseline;
dsc->box_w = gd->width;
dsc->box_h = gd->height;
dsc->is_placeholder = 0;
dsc->bpp = fe->bpp;
return true;
}
FontEngine::FontEngine(font::Font *esp_font) : font_(esp_font) {
this->bpp = esp_font->get_bpp();
this->lv_font_.dsc = this;
this->lv_font_.line_height = this->height = esp_font->get_height();
this->lv_font_.base_line = this->baseline = this->lv_font_.line_height - esp_font->get_baseline();
this->lv_font_.get_glyph_dsc = get_glyph_dsc_cb;
this->lv_font_.get_glyph_bitmap = get_glyph_bitmap;
this->lv_font_.subpx = LV_FONT_SUBPX_NONE;
this->lv_font_.underline_position = -1;
this->lv_font_.underline_thickness = 1;
}
const lv_font_t *FontEngine::get_lv_font() { return &this->lv_font_; }
const font::GlyphData *FontEngine::get_glyph_data(uint32_t unicode_letter) {
if (unicode_letter == last_letter_)
return this->last_data_;
uint8_t unicode[5];
memset(unicode, 0, sizeof unicode);
if (unicode_letter > 0xFFFF) {
unicode[0] = 0xF0 + ((unicode_letter >> 18) & 0x7);
unicode[1] = 0x80 + ((unicode_letter >> 12) & 0x3F);
unicode[2] = 0x80 + ((unicode_letter >> 6) & 0x3F);
unicode[3] = 0x80 + (unicode_letter & 0x3F);
} else if (unicode_letter > 0x7FF) {
unicode[0] = 0xE0 + ((unicode_letter >> 12) & 0xF);
unicode[1] = 0x80 + ((unicode_letter >> 6) & 0x3F);
unicode[2] = 0x80 + (unicode_letter & 0x3F);
} else if (unicode_letter > 0x7F) {
unicode[0] = 0xC0 + ((unicode_letter >> 6) & 0x1F);
unicode[1] = 0x80 + (unicode_letter & 0x3F);
} else {
unicode[0] = unicode_letter;
}
int match_length;
int glyph_n = this->font_->match_next_glyph(unicode, &match_length);
if (glyph_n < 0)
return nullptr;
this->last_data_ = this->font_->get_glyphs()[glyph_n].get_glyph_data();
this->last_letter_ = unicode_letter;
return this->last_data_;
}
} // namespace lvgl
} // namespace esphome
#endif // USES_LVGL_FONT

View File

@@ -493,7 +493,6 @@ class LvFont(LValidator):
return LV_FONTS
if is_lv_font(value):
return lv_builtin_font(value)
add_lv_use("font")
fontval = cv.use_id(Font)(value)
esphome_fonts_used.add(fontval)
return requires_component("font")(fontval)
@@ -503,9 +502,7 @@ class LvFont(LValidator):
async def process(self, value, args=()):
if is_lv_font(value):
return literal(f"&lv_font_{value}")
if isinstance(value, str):
return literal(f"{value}")
return await super().process(value, args)
return literal(f"{value}_engine->get_lv_font()")
lv_font = LvFont()

View File

@@ -50,14 +50,6 @@ static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BIT
static const display::ColorBitness LV_BITNESS = display::ColorBitness::COLOR_BITNESS_332;
#endif // LV_COLOR_DEPTH
#ifdef USE_LVGL_FONT
inline void lv_obj_set_style_text_font(lv_obj_t *obj, const font::Font *font, lv_style_selector_t part) {
lv_obj_set_style_text_font(obj, font->get_lv_font(), part);
}
inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
lv_style_set_text_font(style, font->get_lv_font());
}
#endif
#ifdef USE_LVGL_IMAGE
// Shortcut / overload, so that the source of an image can easily be updated
// from within a lambda.
@@ -142,6 +134,24 @@ template<typename... Ts> class ObjUpdateAction : public Action<Ts...> {
protected:
std::function<void(Ts...)> lamb_;
};
#ifdef USE_LVGL_FONT
class FontEngine {
public:
FontEngine(font::Font *esp_font);
const lv_font_t *get_lv_font();
const font::GlyphData *get_glyph_data(uint32_t unicode_letter);
uint16_t baseline{};
uint16_t height{};
uint8_t bpp{};
protected:
font::Font *font_{};
uint32_t last_letter_{};
const font::GlyphData *last_data_{};
lv_font_t lv_font_{};
};
#endif // USE_LVGL_FONT
#ifdef USE_LVGL_ANIMIMG
void lv_animimg_stop(lv_obj_t *obj);
#endif // USE_LVGL_ANIMIMG

View File

@@ -45,6 +45,7 @@ lv_coord_t = cg.global_ns.namespace("lv_coord_t")
lv_event_code_t = cg.global_ns.enum("lv_event_code_t")
lv_indev_type_t = cg.global_ns.enum("lv_indev_type_t")
lv_key_t = cg.global_ns.enum("lv_key_t")
FontEngine = lvgl_ns.class_("FontEngine")
PlainTrigger = esphome_ns.class_("Trigger<>", automation.Trigger.template())
DrawEndTrigger = esphome_ns.class_(
"Trigger<uint32_t, uint32_t>", automation.Trigger.template(cg.uint32, cg.uint32)

View File

@@ -56,7 +56,7 @@ void MCP23016::pin_mode(uint8_t pin, gpio::Flags flags) {
this->update_reg_(pin, false, iodir);
}
}
float MCP23016::get_setup_priority() const { return setup_priority::IO; }
float MCP23016::get_setup_priority() const { return setup_priority::HARDWARE; }
bool MCP23016::read_reg_(uint8_t reg, uint8_t *value) {
if (this->is_failed())
return false;

View File

@@ -135,7 +135,8 @@ void MDNSComponent::compile_records_(StaticVector<MDNSService, MDNS_SERVICE_COUN
#ifdef USE_DASHBOARD_IMPORT
MDNS_STATIC_CONST_CHAR(TXT_PACKAGE_IMPORT_URL, "package_import_url");
txt_records.push_back({MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url())});
txt_records.push_back(
{MDNS_STR(TXT_PACKAGE_IMPORT_URL), MDNS_STR(dashboard_import::get_package_import_url().c_str())});
#endif
}
#endif // USE_API

View File

@@ -1,7 +1,8 @@
#include "automation.h"
#include "esphome/core/log.h"
namespace esphome::number {
namespace esphome {
namespace number {
static const char *const TAG = "number.automation";
@@ -51,4 +52,5 @@ void ValueRangeTrigger::on_state_(float state) {
this->rtc_.save(&in_range);
}
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -4,7 +4,8 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
namespace esphome::number {
namespace esphome {
namespace number {
class NumberStateTrigger : public Trigger<float> {
public:
@@ -90,4 +91,5 @@ template<typename... Ts> class NumberInRangeCondition : public Condition<Ts...>
float max_{NAN};
};
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -3,7 +3,8 @@
#include "esphome/core/controller_registry.h"
#include "esphome/core/log.h"
namespace esphome::number {
namespace esphome {
namespace number {
static const char *const TAG = "number";
@@ -42,4 +43,5 @@ void Number::add_on_state_callback(std::function<void(float)> &&callback) {
this->state_callback_.add(std::move(callback));
}
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -6,7 +6,8 @@
#include "number_call.h"
#include "number_traits.h"
namespace esphome::number {
namespace esphome {
namespace number {
class Number;
void log_number(const char *tag, const char *prefix, const char *type, Number *obj);
@@ -52,4 +53,5 @@ class Number : public EntityBase {
CallbackManager<void(float)> state_callback_;
};
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -2,7 +2,8 @@
#include "number.h"
#include "esphome/core/log.h"
namespace esphome::number {
namespace esphome {
namespace number {
static const char *const TAG = "number";
@@ -124,4 +125,5 @@ void NumberCall::perform() {
this->parent_->control(target_value);
}
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -4,7 +4,8 @@
#include "esphome/core/log.h"
#include "number_traits.h"
namespace esphome::number {
namespace esphome {
namespace number {
class Number;
@@ -43,4 +44,5 @@ class NumberCall {
bool cycle_;
};
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -1,8 +1,10 @@
#include "esphome/core/log.h"
#include "number_traits.h"
namespace esphome::number {
namespace esphome {
namespace number {
static const char *const TAG = "number";
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -3,7 +3,8 @@
#include "esphome/core/entity_base.h"
#include "esphome/core/helpers.h"
namespace esphome::number {
namespace esphome {
namespace number {
enum NumberMode : uint8_t {
NUMBER_MODE_AUTO = 0,
@@ -34,4 +35,5 @@ class NumberTraits : public EntityBase_DeviceClass, public EntityBase_UnitOfMeas
NumberMode mode_{NUMBER_MODE_AUTO};
};
} // namespace esphome::number
} // namespace number
} // namespace esphome

View File

@@ -46,14 +46,14 @@ template<typename... Ts> class Script : public ScriptLogger, public Trigger<Ts..
// execute this script using a tuple that contains the arguments
void execute_tuple(const std::tuple<Ts...> &tuple) {
this->execute_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
this->execute_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
}
// Internal function to give scripts readable names.
void set_name(const LogString *name) { name_ = name; }
protected:
template<size_t... S> void execute_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
template<int... S> void execute_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
this->execute(std::get<S>(tuple)...);
}
@@ -157,7 +157,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
const size_t queue_capacity = static_cast<size_t>(this->max_runs_ - 1);
auto tuple_ptr = std::move(this->var_queue_[this->queue_front_]);
this->queue_front_ = (this->queue_front_ + 1) % queue_capacity;
this->trigger_tuple_(*tuple_ptr, std::make_index_sequence<sizeof...(Ts)>{});
this->trigger_tuple_(*tuple_ptr, typename gens<sizeof...(Ts)>::type());
}
}
@@ -174,7 +174,7 @@ template<typename... Ts> class QueueingScript : public Script<Ts...>, public Com
}
}
template<size_t... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
template<int... S> void trigger_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
this->trigger(std::get<S>(tuple)...);
}
@@ -305,7 +305,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
while (!this->param_queue_.empty()) {
auto &params = this->param_queue_.front();
this->play_next_tuple_(params, std::make_index_sequence<sizeof...(Ts)>{});
this->play_next_tuple_(params, typename gens<sizeof...(Ts)>::type());
this->param_queue_.pop_front();
}
// Queue is now empty - disable loop until next play_complex
@@ -321,7 +321,7 @@ template<class C, typename... Ts> class ScriptWaitAction : public Action<Ts...>,
}
protected:
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
this->play_next_(std::get<S>(tuple)...);
}

View File

@@ -77,21 +77,23 @@ class Select : public EntityBase {
void add_on_state_callback(std::function<void(std::string, size_t)> &&callback);
/** Set the value of the select by index, this is an optional virtual method.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control().
* Override this to work directly with indices and avoid string conversions.
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
protected:
friend class SelectCall;
size_t active_index_{0};
/** Set the value of the select by index, this is an optional virtual method.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.
* Overriding this index-based version is PREFERRED as it avoids string conversions.
*
* This method is called by the SelectCall when the index is already known.
* Default implementation converts to string and calls control(const std::string&).
*
* @param index The index as validated by the SelectCall.
*/
virtual void control(size_t index) { this->control(this->option_at(index)); }
/** Set the value of the select, this is a virtual method that each select integration can implement.
*
* IMPORTANT: At least ONE of the two control() methods must be overridden by derived classes.

View File

@@ -74,9 +74,9 @@ StateClass Sensor::get_state_class() {
void Sensor::publish_state(float state) {
this->raw_state = state;
// Call raw callbacks (before filters)
this->callbacks_.call_first(this->raw_count_, state);
if (this->raw_callback_) {
this->raw_callback_->call(state);
}
ESP_LOGV(TAG, "'%s': Received new state %f", this->name_.c_str(), state);
@@ -87,12 +87,12 @@ void Sensor::publish_state(float state) {
}
}
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) {
this->callbacks_.add_second(std::move(callback));
}
void Sensor::add_on_state_callback(std::function<void(float)> &&callback) { this->callback_.add(std::move(callback)); }
void Sensor::add_on_raw_state_callback(std::function<void(float)> &&callback) {
this->callbacks_.add_first(std::move(callback), &this->raw_count_);
if (!this->raw_callback_) {
this->raw_callback_ = make_unique<CallbackManager<void(float)>>();
}
this->raw_callback_->add(std::move(callback));
}
void Sensor::add_filter(Filter *filter) {
@@ -132,10 +132,7 @@ void Sensor::internal_send_state_to_frontend(float state) {
this->state = state;
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals());
// Call filtered callbacks (after filters)
this->callbacks_.call_second(this->raw_count_, state);
this->callback_.call(state);
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_sensor_update(this);
#endif

View File

@@ -124,7 +124,8 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
void internal_send_state_to_frontend(float state);
protected:
PartitionedCallbackManager<void(float)> callbacks_;
std::unique_ptr<CallbackManager<void(float)>> raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(float)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.
@@ -139,8 +140,6 @@ class Sensor : public EntityBase, public EntityBase_DeviceClass, public EntityBa
uint8_t force_update : 1;
uint8_t reserved : 5; // Reserved for future use
} sensor_flags_{};
uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector)
};
} // namespace sensor

View File

@@ -1,14 +1,9 @@
import logging
import esphome.codegen as cg
from esphome.components import time as time_
from esphome.config_helpers import merge_config
import esphome.config_validation as cv
from esphome.const import (
CONF_ID,
CONF_PLATFORM,
CONF_SERVERS,
CONF_TIME,
PLATFORM_BK72XX,
PLATFORM_ESP32,
PLATFORM_ESP8266,
@@ -17,74 +12,13 @@ from esphome.const import (
PLATFORM_RTL87XX,
)
from esphome.core import CORE
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ["network"]
CONF_SNTP = "sntp"
sntp_ns = cg.esphome_ns.namespace("sntp")
SNTPComponent = sntp_ns.class_("SNTPComponent", time_.RealTimeClock)
DEFAULT_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"]
def _sntp_final_validate(config: ConfigType) -> None:
"""Merge multiple SNTP instances into one, similar to OTA merging behavior."""
full_conf = fv.full_config.get()
time_confs = full_conf.get(CONF_TIME, [])
sntp_configs: list[ConfigType] = []
other_time_configs: list[ConfigType] = []
for time_conf in time_confs:
if time_conf.get(CONF_PLATFORM) == CONF_SNTP:
sntp_configs.append(time_conf)
else:
other_time_configs.append(time_conf)
if len(sntp_configs) <= 1:
return
# Merge all SNTP configs into the first one
merged = sntp_configs[0]
for sntp_conf in sntp_configs[1:]:
# Validate that IDs are consistent if manually specified
if merged[CONF_ID].is_manual and sntp_conf[CONF_ID].is_manual:
raise cv.Invalid(
f"Found multiple SNTP configurations but {CONF_ID} is inconsistent"
)
merged = merge_config(merged, sntp_conf)
# Deduplicate servers while preserving order
servers = merged[CONF_SERVERS]
unique_servers = list(dict.fromkeys(servers))
# Warn if we're dropping servers due to 3-server limit
if len(unique_servers) > 3:
dropped = unique_servers[3:]
unique_servers = unique_servers[:3]
_LOGGER.warning(
"SNTP supports maximum 3 servers. Dropped excess server(s): %s",
dropped,
)
merged[CONF_SERVERS] = unique_servers
_LOGGER.warning(
"Found and merged %d SNTP time configurations into one instance",
len(sntp_configs),
)
# Replace time configs with merged SNTP + other time platforms
other_time_configs.append(merged)
full_conf[CONF_TIME] = other_time_configs
fv.full_config.set(full_conf)
CONFIG_SCHEMA = cv.All(
time_.TIME_SCHEMA.extend(
{
@@ -106,8 +40,6 @@ CONFIG_SCHEMA = cv.All(
),
)
FINAL_VALIDATE_SCHEMA = _sntp_final_validate
async def to_code(config):
servers = config[CONF_SERVERS]

View File

@@ -137,11 +137,7 @@ async def to_code(config):
cg.add(var.set_arming_night_time(config[CONF_ARMING_NIGHT_TIME]))
supports_arm_night = True
if sensors := config.get(CONF_BINARY_SENSORS, []):
# Initialize FixedVector with the exact number of sensors
cg.add(var.init_sensors(len(sensors)))
for sensor in sensors:
for sensor in config.get(CONF_BINARY_SENSORS, []):
bs = await cg.get_variable(sensor[CONF_INPUT])
flags = BinarySensorFlags[FLAG_NORMAL]

View File

@@ -20,13 +20,10 @@ void TemplateAlarmControlPanel::add_sensor(binary_sensor::BinarySensor *sensor,
// Save the flags and type. Assign a store index for the per sensor data type.
SensorDataStore sd;
sd.last_chime_state = false;
AlarmSensor alarm_sensor;
alarm_sensor.sensor = sensor;
alarm_sensor.info.flags = flags;
alarm_sensor.info.type = type;
alarm_sensor.info.store_index = this->next_store_index_++;
this->sensors_.push_back(alarm_sensor);
this->sensor_map_[sensor].flags = flags;
this->sensor_map_[sensor].type = type;
this->sensor_data_.push_back(sd);
this->sensor_map_[sensor].store_index = this->next_store_index_++;
};
static const LogString *sensor_type_to_string(AlarmSensorType type) {
@@ -48,7 +45,7 @@ void TemplateAlarmControlPanel::dump_config() {
ESP_LOGCONFIG(TAG,
"TemplateAlarmControlPanel:\n"
" Current State: %s\n"
" Number of Codes: %zu\n"
" Number of Codes: %u\n"
" Requires Code To Arm: %s\n"
" Arming Away Time: %" PRIu32 "s\n"
" Arming Home Time: %" PRIu32 "s\n"
@@ -61,8 +58,7 @@ void TemplateAlarmControlPanel::dump_config() {
(this->arming_home_time_ / 1000), (this->arming_night_time_ / 1000), (this->pending_time_ / 1000),
(this->trigger_time_ / 1000), this->get_supported_features());
#ifdef USE_BINARY_SENSOR
for (const auto &alarm_sensor : this->sensors_) {
const uint16_t flags = alarm_sensor.info.flags;
for (auto const &[sensor, info] : this->sensor_map_) {
ESP_LOGCONFIG(TAG,
" Binary Sensor:\n"
" Name: %s\n"
@@ -71,10 +67,11 @@ void TemplateAlarmControlPanel::dump_config() {
" Armed night bypass: %s\n"
" Auto bypass: %s\n"
" Chime mode: %s",
alarm_sensor.sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(alarm_sensor.info.type)),
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
TRUEFALSE(flags & BINARY_SENSOR_MODE_BYPASS_AUTO), TRUEFALSE(flags & BINARY_SENSOR_MODE_CHIME));
sensor->get_name().c_str(), LOG_STR_ARG(sensor_type_to_string(info.type)),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_HOME),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_ARMED_NIGHT),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO),
TRUEFALSE(info.flags & BINARY_SENSOR_MODE_CHIME));
}
#endif
}
@@ -124,9 +121,7 @@ void TemplateAlarmControlPanel::loop() {
#ifdef USE_BINARY_SENSOR
// Test all of the sensors regardless of the alarm panel state
for (const auto &alarm_sensor : this->sensors_) {
const auto &info = alarm_sensor.info;
auto *sensor = alarm_sensor.sensor;
for (auto const &[sensor, info] : this->sensor_map_) {
// Check for chime zones
if (info.flags & BINARY_SENSOR_MODE_CHIME) {
// Look for the transition from closed to open
@@ -247,11 +242,11 @@ void TemplateAlarmControlPanel::arm_(optional<std::string> code, alarm_control_p
void TemplateAlarmControlPanel::bypass_before_arming() {
#ifdef USE_BINARY_SENSOR
for (const auto &alarm_sensor : this->sensors_) {
for (auto const &[sensor, info] : this->sensor_map_) {
// Check for faulted bypass_auto sensors and remove them from monitoring
if ((alarm_sensor.info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (alarm_sensor.sensor->state)) {
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", alarm_sensor.sensor->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(alarm_sensor.info.store_index);
if ((info.flags & BINARY_SENSOR_MODE_BYPASS_AUTO) && (sensor->state)) {
ESP_LOGW(TAG, "'%s' is faulted and will be automatically bypassed", sensor->get_name().c_str());
this->bypassed_sensor_indicies_.push_back(info.store_index);
}
}
#endif

View File

@@ -1,12 +1,11 @@
#pragma once
#include <cinttypes>
#include <vector>
#include <map>
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include "esphome/components/alarm_control_panel/alarm_control_panel.h"
@@ -50,13 +49,6 @@ struct SensorInfo {
uint8_t store_index;
};
#ifdef USE_BINARY_SENSOR
struct AlarmSensor {
binary_sensor::BinarySensor *sensor;
SensorInfo info;
};
#endif
class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControlPanel, public Component {
public:
TemplateAlarmControlPanel();
@@ -71,12 +63,6 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
void bypass_before_arming();
#ifdef USE_BINARY_SENSOR
/** Initialize the sensors vector with the specified capacity.
*
* @param capacity The number of sensors to allocate space for.
*/
void init_sensors(size_t capacity) { this->sensors_.init(capacity); }
/** Add a binary_sensor to the alarm_panel.
*
* @param sensor The BinarySensor instance.
@@ -136,8 +122,8 @@ class TemplateAlarmControlPanel final : public alarm_control_panel::AlarmControl
protected:
void control(const alarm_control_panel::AlarmControlPanelCall &call) override;
#ifdef USE_BINARY_SENSOR
// List of binary sensors with their alarm-specific info
FixedVector<AlarmSensor> sensors_;
// This maps a binary sensor to its alarm specific info
std::map<binary_sensor::BinarySensor *, SensorInfo> sensor_map_;
// a list of automatically bypassed sensors
std::vector<uint8_t> bypassed_sensor_indicies_;
#endif

View File

@@ -26,9 +26,9 @@ void log_text_sensor(const char *tag, const char *prefix, const char *type, Text
void TextSensor::publish_state(const std::string &state) {
this->raw_state = state;
// Call raw callbacks (before filters)
this->callbacks_.call_first(this->raw_count_, state);
if (this->raw_callback_) {
this->raw_callback_->call(state);
}
ESP_LOGV(TAG, "'%s': Received new state %s", this->name_.c_str(), state.c_str());
@@ -70,11 +70,13 @@ void TextSensor::clear_filters() {
}
void TextSensor::add_on_state_callback(std::function<void(std::string)> callback) {
this->callbacks_.add_second(std::move(callback));
this->callback_.add(std::move(callback));
}
void TextSensor::add_on_raw_state_callback(std::function<void(std::string)> callback) {
this->callbacks_.add_first(std::move(callback), &this->raw_count_);
if (!this->raw_callback_) {
this->raw_callback_ = make_unique<CallbackManager<void(std::string)>>();
}
this->raw_callback_->add(std::move(callback));
}
std::string TextSensor::get_state() const { return this->state; }
@@ -83,10 +85,7 @@ void TextSensor::internal_send_state_to_frontend(const std::string &state) {
this->state = state;
this->set_has_state(true);
ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), state.c_str());
// Call filtered callbacks (after filters)
this->callbacks_.call_second(this->raw_count_, state);
this->callback_.call(state);
#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_text_sensor_update(this);
#endif

View File

@@ -58,11 +58,11 @@ class TextSensor : public EntityBase, public EntityBase_DeviceClass {
void internal_send_state_to_frontend(const std::string &state);
protected:
PartitionedCallbackManager<void(std::string)> callbacks_;
std::unique_ptr<CallbackManager<void(std::string)>>
raw_callback_; ///< Storage for raw state callbacks (lazy allocated).
CallbackManager<void(std::string)> callback_; ///< Storage for filtered state callbacks.
Filter *filter_list_{nullptr}; ///< Store all active filters.
uint8_t raw_count_{0}; ///< Number of raw callbacks (partition point in callbacks_ vector)
};
} // namespace text_sensor

View File

@@ -1,4 +1,3 @@
from logging import getLogger
import math
import re
@@ -36,8 +35,6 @@ from esphome.core import CORE, ID
import esphome.final_validate as fv
from esphome.yaml_util import make_data_base
_LOGGER = getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
uart_ns = cg.esphome_ns.namespace("uart")
UARTComponent = uart_ns.class_("UARTComponent")
@@ -133,21 +130,6 @@ def validate_host_config(config):
return config
def validate_rx_buffer_size(config):
if CORE.is_esp32:
# ESP32 UART hardware FIFO is 128 bytes (LP UART is 16 bytes, but we use 128 as safe minimum)
# rx_buffer_size must be greater than the hardware FIFO length
min_buffer_size = 128
if config[CONF_RX_BUFFER_SIZE] <= min_buffer_size:
_LOGGER.warning(
"UART rx_buffer_size (%d bytes) is too small and must be greater than the hardware "
"FIFO size (%d bytes). The buffer size will be automatically adjusted at runtime.",
config[CONF_RX_BUFFER_SIZE],
min_buffer_size,
)
return config
def _uart_declare_type(value):
if CORE.is_esp8266:
return cv.declare_id(ESP8266UartComponent)(value)
@@ -265,7 +247,6 @@ CONFIG_SCHEMA = cv.All(
).extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_TX_PIN, CONF_RX_PIN, CONF_PORT),
validate_host_config,
validate_rx_buffer_size,
)

View File

@@ -56,19 +56,11 @@ uint32_t ESP8266UartComponent::get_config() {
}
void ESP8266UartComponent::setup() {
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
if (!pin) {
return;
}
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
pin->setup();
}
};
setup_pin_if_needed(this->rx_pin_);
if (this->rx_pin_ != this->tx_pin_) {
setup_pin_if_needed(this->tx_pin_);
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
// Use Arduino HardwareSerial UARTs if all used pins match the ones

View File

@@ -91,16 +91,6 @@ void IDFUARTComponent::setup() {
this->uart_num_ = static_cast<uart_port_t>(next_uart_num++);
this->lock_ = xSemaphoreCreateMutex();
#if (SOC_UART_LP_NUM >= 1)
size_t fifo_len = ((this->uart_num_ < SOC_UART_HP_NUM) ? SOC_UART_FIFO_LEN : SOC_LP_UART_FIFO_LEN);
#else
size_t fifo_len = SOC_UART_FIFO_LEN;
#endif
if (this->rx_buffer_size_ <= fifo_len) {
ESP_LOGW(TAG, "rx_buffer_size is too small, must be greater than %zu", fifo_len);
this->rx_buffer_size_ = fifo_len * 2;
}
xSemaphoreTake(this->lock_, portMAX_DELAY);
this->load_settings(false);
@@ -133,19 +123,11 @@ void IDFUARTComponent::load_settings(bool dump_config) {
return;
}
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
if (!pin) {
return;
}
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
pin->setup();
}
};
setup_pin_if_needed(this->rx_pin_);
if (this->rx_pin_ != this->tx_pin_) {
setup_pin_if_needed(this->tx_pin_);
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
int8_t tx = this->tx_pin_ != nullptr ? this->tx_pin_->get_pin() : -1;
@@ -255,12 +237,8 @@ void IDFUARTComponent::set_rx_timeout(size_t rx_timeout) {
void IDFUARTComponent::write_array(const uint8_t *data, size_t len) {
xSemaphoreTake(this->lock_, portMAX_DELAY);
int32_t write_len = uart_write_bytes(this->uart_num_, data, len);
uart_write_bytes(this->uart_num_, data, len);
xSemaphoreGive(this->lock_);
if (write_len != (int32_t) len) {
ESP_LOGW(TAG, "uart_write_bytes failed: %d != %zu", write_len, len);
this->mark_failed();
}
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_TX, data[i]);
@@ -289,7 +267,6 @@ bool IDFUARTComponent::peek_byte(uint8_t *data) {
bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
size_t length_to_read = len;
int32_t read_len = 0;
if (!this->check_read_timeout_(len))
return false;
xSemaphoreTake(this->lock_, portMAX_DELAY);
@@ -300,31 +277,25 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) {
this->has_peek_ = false;
}
if (length_to_read > 0)
read_len = uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
uart_read_bytes(this->uart_num_, data, length_to_read, 20 / portTICK_PERIOD_MS);
xSemaphoreGive(this->lock_);
#ifdef USE_UART_DEBUGGER
for (size_t i = 0; i < len; i++) {
this->debug_callback_.call(UART_DIRECTION_RX, data[i]);
}
#endif
return read_len == (int32_t) length_to_read;
return true;
}
int IDFUARTComponent::available() {
size_t available = 0;
esp_err_t err;
size_t available;
xSemaphoreTake(this->lock_, portMAX_DELAY);
err = uart_get_buffered_data_len(this->uart_num_, &available);
uart_get_buffered_data_len(this->uart_num_, &available);
if (this->has_peek_)
available++;
xSemaphoreGive(this->lock_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "uart_get_buffered_data_len failed: %s", esp_err_to_name(err));
this->mark_failed();
}
if (this->has_peek_) {
available++;
}
return available;
}

View File

@@ -53,7 +53,7 @@ void LibreTinyUARTComponent::setup() {
auto shouldFallbackToSoftwareSerial = [&]() -> bool {
auto hasFlags = [](InternalGPIOPin *pin, const gpio::Flags mask) -> bool {
return pin && (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE;
return pin && pin->get_flags() & mask != gpio::Flags::FLAG_NONE;
};
if (hasFlags(this->tx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN) ||
hasFlags(this->rx_pin_, gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN)) {

View File

@@ -52,19 +52,11 @@ uint16_t RP2040UartComponent::get_config() {
}
void RP2040UartComponent::setup() {
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
if (!pin) {
return;
}
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
pin->setup();
}
};
setup_pin_if_needed(this->rx_pin_);
if (this->rx_pin_ != this->tx_pin_) {
setup_pin_if_needed(this->tx_pin_);
if (this->rx_pin_) {
this->rx_pin_->setup();
}
if (this->tx_pin_ && this->rx_pin_ != this->tx_pin_) {
this->tx_pin_->setup();
}
uint16_t config = get_config();

View File

@@ -1,17 +1,10 @@
import logging
import esphome.codegen as cg
from esphome.components.esp32 import add_idf_component
from esphome.components.ota import BASE_OTA_SCHEMA, OTAComponent, ota_to_code
from esphome.config_helpers import merge_config
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER
from esphome.const import CONF_ID
from esphome.core import CORE, coroutine_with_priority
from esphome.coroutine import CoroPriority
import esphome.final_validate as fv
from esphome.types import ConfigType
_LOGGER = logging.getLogger(__name__)
CODEOWNERS = ["@esphome/core"]
DEPENDENCIES = ["network", "web_server_base"]
@@ -19,53 +12,6 @@ DEPENDENCIES = ["network", "web_server_base"]
web_server_ns = cg.esphome_ns.namespace("web_server")
WebServerOTAComponent = web_server_ns.class_("WebServerOTAComponent", OTAComponent)
def _web_server_ota_final_validate(config: ConfigType) -> None:
"""Merge multiple web_server OTA instances into one.
Multiple web_server OTA instances register duplicate HTTP handlers for /update,
causing undefined behavior. Merge them into a single instance.
"""
full_conf = fv.full_config.get()
ota_confs = full_conf.get(CONF_OTA, [])
web_server_ota_configs: list[ConfigType] = []
other_ota_configs: list[ConfigType] = []
for ota_conf in ota_confs:
if ota_conf.get(CONF_PLATFORM) == CONF_WEB_SERVER:
web_server_ota_configs.append(ota_conf)
else:
other_ota_configs.append(ota_conf)
if len(web_server_ota_configs) <= 1:
return
# Merge all web_server OTA configs into the first one
merged = web_server_ota_configs[0]
for ota_conf in web_server_ota_configs[1:]:
# Validate that IDs are consistent if manually specified
if (
merged[CONF_ID].is_manual
and ota_conf[CONF_ID].is_manual
and merged[CONF_ID] != ota_conf[CONF_ID]
):
raise cv.Invalid(
f"Found multiple web_server OTA configurations but {CONF_ID} is inconsistent"
)
merged = merge_config(merged, ota_conf)
_LOGGER.warning(
"Found and merged %d web_server OTA configurations into one instance",
len(web_server_ota_configs),
)
# Replace OTA configs with merged web_server + other OTA platforms
other_ota_configs.append(merged)
full_conf[CONF_OTA] = other_ota_configs
fv.full_config.set(full_conf)
CONFIG_SCHEMA = (
cv.Schema(
{
@@ -76,8 +22,6 @@ CONFIG_SCHEMA = (
.extend(cv.COMPONENT_SCHEMA)
)
FINAL_VALIDATE_SCHEMA = _web_server_ota_final_validate
@coroutine_with_priority(CoroPriority.WEB_SERVER_OTA)
async def to_code(config):

View File

@@ -353,9 +353,8 @@ void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
void AsyncResponseStream::print(float value) {
// Use stack buffer to avoid temporary string allocation
// Size: sign (1) + digits (10) + decimal (1) + precision (6) + exponent (5) + null (1) = 24, use 32 for safety
constexpr size_t float_buf_size = 32;
char buf[float_buf_size];
int len = snprintf(buf, float_buf_size, "%f", value);
char buf[32];
int len = snprintf(buf, sizeof(buf), "%f", value);
this->content_.append(buf, len);
}
@@ -490,18 +489,10 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest *
void AsyncEventSourceResponse::destroy(void *ptr) {
auto *rsp = static_cast<AsyncEventSourceResponse *>(ptr);
int fd = rsp->fd_.exchange(0); // Atomically get and clear fd
if (fd > 0) {
ESP_LOGD(TAG, "Event source connection closed (fd: %d)", fd);
// Immediately shut down the socket to prevent lwIP from delivering more data
// This prevents "recv_tcp: recv for wrong pcb!" assertions when the TCP stack
// tries to deliver queued data after the session is marked as dead
// See: https://github.com/esphome/esphome/issues/11936
shutdown(fd, SHUT_RDWR);
// Note: We don't close() the socket - httpd owns it and will close it
}
// Session will be cleaned up in the main loop to avoid race conditions
ESP_LOGD(TAG, "Event source connection closed (fd: %d)", rsp->fd_.load());
// Mark as dead by setting fd to 0 - will be cleaned up in the main loop
rsp->fd_.store(0);
// Note: We don't delete or remove from set here to avoid race conditions
}
// helper for allowing only unique entries in the queue

View File

@@ -12,6 +12,7 @@ from esphome.components.network import (
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.config_validation import only_with_esp_idf
from esphome.const import (
CONF_AP,
CONF_BSSID,
@@ -351,7 +352,7 @@ CONFIG_SCHEMA = cv.All(
single=True
),
cv.Optional(CONF_USE_PSRAM): cv.All(
cv.only_on_esp32, cv.requires_component("psram"), cv.boolean
only_with_esp_idf, cv.requires_component("psram"), cv.boolean
),
}
),

View File

@@ -1414,7 +1414,6 @@ void WiFiComponent::log_and_adjust_priority_for_failed_connect_() {
(old_priority > std::numeric_limits<int8_t>::min()) ? (old_priority - 1) : std::numeric_limits<int8_t>::min();
this->set_sta_priority(failed_bssid.value(), new_priority);
}
char bssid_s[18];
format_mac_addr_upper(failed_bssid.value().data(), bssid_s);
ESP_LOGD(TAG, "Failed " LOG_SECRET("'%s'") " " LOG_SECRET("(%s)") ", priority %d → %d", ssid.c_str(), bssid_s,

View File

@@ -710,15 +710,6 @@ class EsphomeCore:
def relative_piolibdeps_path(self, *path: str | Path) -> Path:
return self.relative_build_path(".piolibdeps", *path)
@property
def platformio_cache_dir(self) -> str:
"""Get the PlatformIO cache directory path."""
# Check if running in Docker/HA addon with custom cache dir
if (cache_dir := os.environ.get("PLATFORMIO_CACHE_DIR")) and cache_dir.strip():
return cache_dir
# Default PlatformIO cache location
return os.path.expanduser("~/.platformio/.cache")
@property
def firmware_bin(self) -> Path:
if self.is_libretiny:

View File

@@ -11,26 +11,10 @@
namespace esphome {
// C++20 std::index_sequence is now used for tuple unpacking
// Legacy seq<>/gens<> pattern deprecated but kept for backwards compatibility
// https://stackoverflow.com/questions/7858817/unpacking-a-tuple-to-call-a-matching-function-pointer/7858971#7858971
// Remove before 2026.6.0
// NOLINTBEGIN(readability-identifier-naming)
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
template<int...> struct ESPDEPRECATED("Use std::index_sequence instead. Removed in 2026.6.0", "2025.12.0") seq {};
template<int N, int... S>
struct ESPDEPRECATED("Use std::make_index_sequence instead. Removed in 2026.6.0", "2025.12.0") gens
: gens<N - 1, N - 1, S...> {};
template<int... S> struct gens<0, S...> { using type = seq<S...>; };
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
// NOLINTEND(readability-identifier-naming)
template<int...> struct seq {}; // NOLINT
template<int N, int... S> struct gens : gens<N - 1, N - 1, S...> {}; // NOLINT
template<int... S> struct gens<0, S...> { using type = seq<S...>; }; // NOLINT
#define TEMPLATABLE_VALUE_(type, name) \
protected: \
@@ -168,11 +152,11 @@ template<typename... Ts> class Condition {
/// Call check with a tuple of values as parameter.
bool check_tuple(const std::tuple<Ts...> &tuple) {
return this->check_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
return this->check_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
}
protected:
template<size_t... S> bool check_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
template<int... S> bool check_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
return this->check(std::get<S>(tuple)...);
}
};
@@ -247,11 +231,11 @@ template<typename... Ts> class Action {
}
}
}
template<size_t... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
template<int... S> void play_next_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
this->play_next_(std::get<S>(tuple)...);
}
void play_next_tuple_(const std::tuple<Ts...> &tuple) {
this->play_next_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
this->play_next_tuple_(tuple, typename gens<sizeof...(Ts)>::type());
}
virtual void stop() {}
@@ -293,9 +277,7 @@ template<typename... Ts> class ActionList {
if (this->actions_begin_ != nullptr)
this->actions_begin_->play_complex(x...);
}
void play_tuple(const std::tuple<Ts...> &tuple) {
this->play_tuple_(tuple, std::make_index_sequence<sizeof...(Ts)>{});
}
void play_tuple(const std::tuple<Ts...> &tuple) { this->play_tuple_(tuple, typename gens<sizeof...(Ts)>::type()); }
void stop() {
if (this->actions_begin_ != nullptr)
this->actions_begin_->stop_complex();
@@ -316,7 +298,7 @@ template<typename... Ts> class ActionList {
}
protected:
template<size_t... S> void play_tuple_(const std::tuple<Ts...> &tuple, std::index_sequence<S...> /*unused*/) {
template<int... S> void play_tuple_(const std::tuple<Ts...> &tuple, seq<S...> /*unused*/) {
this->play(std::get<S>(tuple)...);
}

View File

@@ -178,6 +178,7 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
TEMPLATABLE_VALUE(uint32_t, delay)
void play_complex(const Ts &...x) override {
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
this->num_running_++;
// If num_running_ > 1, we have multiple instances running in parallel
@@ -186,22 +187,9 @@ template<typename... Ts> class DelayAction : public Action<Ts...>, public Compon
// WARNING: This can accumulate delays if scripts are triggered faster than they complete!
// Users should set max_runs on parallel scripts to limit concurrent executions.
// Issue #10264: This is a workaround for parallel script delays interfering with each other.
// Optimization: For no-argument delays (most common case), use direct lambda
// instead of std::bind to avoid bind overhead (~16 bytes heap + faster execution)
if constexpr (sizeof...(Ts) == 0) {
App.scheduler.set_timer_common_(
this, Scheduler::SchedulerItem::TIMEOUT,
/* is_static_string= */ true, "delay", this->delay_.value(), [this]() { this->play_next_(); },
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
} else {
// For delays with arguments, use std::bind to preserve argument values
// Arguments must be copied because original references may be invalid after delay
auto f = std::bind(&DelayAction<Ts...>::play_next_, this, x...);
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT,
/* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f),
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
}
App.scheduler.set_timer_common_(this, Scheduler::SchedulerItem::TIMEOUT,
/* is_static_string= */ true, "delay", this->delay_.value(x...), std::move(f),
/* is_retry= */ false, /* skip_cancel= */ this->num_running_ > 1);
}
float get_setup_priority() const override { return setup_priority::HARDWARE; }

View File

@@ -216,7 +216,6 @@
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 3, 2)
#define USE_ETHERNET
#define USE_ETHERNET_KSZ8081
#define USE_ETHERNET_MANUAL_IP
#endif
#ifdef USE_ESP_IDF

View File

@@ -74,12 +74,6 @@ void EntityBase::set_object_id(const char *object_id) {
this->calc_object_id_();
}
void EntityBase::set_name_and_object_id(const char *name, const char *object_id) {
this->set_name(name);
this->object_id_c_str_ = object_id;
this->calc_object_id_();
}
// Calculate Object ID Hash from Entity Name
void EntityBase::calc_object_id_() {
this->object_id_hash_ =

View File

@@ -41,9 +41,6 @@ class EntityBase {
std::string get_object_id() const;
void set_object_id(const char *object_id);
// Set both name and object_id in one call (reduces generated code size)
void set_name_and_object_id(const char *name, const char *object_id);
// Get the unique Object ID of this Entity
uint32_t get_object_id_hash();

View File

@@ -84,6 +84,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
# Get device name for object ID calculation
device_name = device_id_obj.id
add(var.set_name(config[CONF_NAME]))
# Calculate base object_id using the same logic as C++
# This must match the C++ behavior in esphome/core/entity_base.cpp
base_object_id = get_base_entity_object_id(
@@ -95,8 +97,8 @@ async def setup_entity(var: MockObj, config: ConfigType, platform: str) -> None:
"Entity has empty name, using '%s' as object_id base", base_object_id
)
# Set both name and object_id in one call to reduce generated code size
add(var.set_name_and_object_id(config[CONF_NAME], base_object_id))
# Set the object ID
add(var.set_object_id(base_object_id))
_LOGGER.debug(
"Setting object_id '%s' for entity '%s' on platform '%s'",
base_object_id,

View File

@@ -414,8 +414,10 @@ int8_t step_to_accuracy_decimals(float step) {
return str.length() - dot_pos - 1;
}
// Store BASE64 characters as array - automatically placed in flash/ROM on embedded platforms
static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
// Use C-style string constant to store in ROM instead of RAM (saves 24 bytes)
static constexpr const char *BASE64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
// Helper function to find the index of a base64 character in the lookup table.
// Returns the character's position (0-63) if found, or 0 if not found.
@@ -425,8 +427,8 @@ static const char BASE64_CHARS[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr
// stops processing at the first invalid character due to the is_base64() check in its
// while loop condition, making this edge case harmless in practice.
static inline uint8_t base64_find_char(char c) {
const void *ptr = memchr(BASE64_CHARS, c, sizeof(BASE64_CHARS));
return ptr ? (static_cast<const char *>(ptr) - BASE64_CHARS) : 0;
const char *pos = strchr(BASE64_CHARS, c);
return pos ? (pos - BASE64_CHARS) : 0;
}
static inline bool is_base64(char c) { return (isalnum(c) || (c == '+') || (c == '/')); }

View File

@@ -111,23 +111,6 @@ template<> constexpr int64_t byteswap(int64_t n) { return __builtin_bswap64(n);
/// @name Container utilities
///@{
/// Lightweight read-only view over a const array stored in RODATA (will typically be in flash memory)
/// Avoids copying data from flash to RAM by keeping a pointer to the flash data.
/// Similar to std::span but with minimal overhead for embedded systems.
template<typename T> class ConstVector {
public:
constexpr ConstVector(const T *data, size_t size) : data_(data), size_(size) {}
const constexpr T &operator[](size_t i) const { return data_[i]; }
constexpr size_t size() const { return size_; }
constexpr bool empty() const { return size_ == 0; }
protected:
const T *data_;
size_t size_;
};
/// Minimal static vector - saves memory by avoiding std::vector overhead
template<typename T, size_t N> class StaticVector {
public:
@@ -162,9 +145,6 @@ template<typename T, size_t N> class StaticVector {
size_t size() const { return count_; }
bool empty() const { return count_ == 0; }
// Direct access to size counter for efficient in-place construction
size_t &count() { return count_; }
T &operator[](size_t i) { return data_[i]; }
const T &operator[](size_t i) const { return data_[i]; }
@@ -889,73 +869,6 @@ template<typename... Ts> class CallbackManager<void(Ts...)> {
std::vector<std::function<void(Ts...)>> callbacks_;
};
template<typename... X> class PartitionedCallbackManager;
/** Helper class for callbacks partitioned into two sections.
*
* Uses a single vector partitioned into two sections: [first_0, ..., first_m-1, second_0, ..., second_n-1]
* The partition point is tracked externally by the caller (typically stored in the entity class for optimal alignment).
*
* Memory efficient: Only stores a single pointer (4 bytes on 32-bit platforms, 8 bytes on 64-bit platforms).
* The partition count lives in the entity class where it can be packed with other small fields to avoid padding waste.
*
* Design rationale: The asymmetric API (add_first takes first_count*, while call_first/call_second take it by value)
* is intentional - add_first must increment the count, while call methods only read it. This avoids storing first_count
* internally, saving memory per instance.
*
* @tparam Ts The arguments for the callbacks, wrapped in void().
*/
template<typename... Ts> class PartitionedCallbackManager<void(Ts...)> {
public:
/// Add a callback to the first partition.
void add_first(std::function<void(Ts...)> &&callback, uint8_t *first_count) {
if (!this->callbacks_) {
this->callbacks_ = make_unique<std::vector<std::function<void(Ts...)>>>();
}
// Add to first partition: append then rotate into position
this->callbacks_->push_back(std::move(callback));
// Avoid potential underflow: rewrite comparison to not subtract from size()
if (*first_count + 1 < this->callbacks_->size()) {
// Use std::rotate to maintain registration order in second partition
std::rotate(this->callbacks_->begin() + *first_count, this->callbacks_->end() - 1, this->callbacks_->end());
}
(*first_count)++;
}
/// Add a callback to the second partition.
void add_second(std::function<void(Ts...)> &&callback) {
if (!this->callbacks_) {
this->callbacks_ = make_unique<std::vector<std::function<void(Ts...)>>>();
}
// Add to second partition: just append (already at end after first partition)
this->callbacks_->push_back(std::move(callback));
}
/// Call all callbacks in the first partition.
void call_first(uint8_t first_count, Ts... args) {
if (this->callbacks_) {
for (size_t i = 0; i < first_count; i++) {
(*this->callbacks_)[i](args...);
}
}
}
/// Call all callbacks in the second partition.
void call_second(uint8_t first_count, Ts... args) {
if (this->callbacks_) {
for (size_t i = first_count; i < this->callbacks_->size(); i++) {
(*this->callbacks_)[i](args...);
}
}
}
protected:
/// Partitioned callback storage: [first_0, ..., first_m-1, second_0, ..., second_n-1]
std::unique_ptr<std::vector<std::function<void(Ts...)>>> callbacks_;
};
/// Helper class to deduplicate items in a series of values.
template<typename T> class Deduplicator {
public:

View File

@@ -609,12 +609,13 @@ uint64_t Scheduler::millis_64_(uint32_t now) {
if (now < last && (last - now) > HALF_MAX_UINT32) {
this->millis_major_++;
major++;
this->last_millis_ = now;
#ifdef ESPHOME_DEBUG_SCHEDULER
ESP_LOGD(TAG, "Detected true 32-bit rollover at %" PRIu32 "ms (was %" PRIu32 ")", now, last);
#endif /* ESPHOME_DEBUG_SCHEDULER */
} else if (now > last) {
// Only update if time moved forward
}
// Only update if time moved forward
if (now > last) {
this->last_millis_ = now;
}

View File

@@ -94,9 +94,10 @@ class Scheduler {
} name_;
uint32_t interval;
// Split time to handle millis() rollover. The scheduler combines the 32-bit millis()
// with a 16-bit rollover counter to create a 48-bit time space (stored as 64-bit
// for compatibility). With 49.7 days per 32-bit rollover, the 16-bit counter
// supports 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// with a 16-bit rollover counter to create a 48-bit time space (using 32+16 bits).
// This is intentionally limited to 48 bits, not stored as a full 64-bit value.
// With 49.7 days per 32-bit rollover, the 16-bit counter supports
// 49.7 days × 65536 = ~8900 years. This ensures correct scheduling
// even when devices run for months. Split into two fields for better memory
// alignment on 32-bit systems.
uint32_t next_execution_low_; // Lower 32 bits of execution time (millis value)

View File

@@ -19,21 +19,11 @@ from esphome.core import (
TimePeriodNanoseconds,
TimePeriodSeconds,
)
from esphome.coroutine import CoroPriority, coroutine_with_priority
from esphome.helpers import cpp_string_escape, indent_all_but_first_and_last
from esphome.types import Expression, SafeExpType, TemplateArgsType
from esphome.util import OrderedDict
from esphome.yaml_util import ESPHomeDataBase
# Keys for lambda deduplication storage in CORE.data
_KEY_LAMBDA_DEDUP = "lambda_dedup"
_KEY_LAMBDA_DEDUP_DECLARATIONS = "lambda_dedup_declarations"
# Regex patterns for static variable detection (compiled once)
_RE_CPP_SINGLE_LINE_COMMENT = re.compile(r"//.*?$", re.MULTILINE)
_RE_CPP_MULTI_LINE_COMMENT = re.compile(r"/\*.*?\*/", re.DOTALL)
_RE_STATIC_VARIABLE = re.compile(r"\bstatic\s+(?!cast|assert|pointer_cast)\w+\s+\w+")
class RawExpression(Expression):
__slots__ = ("text",)
@@ -198,7 +188,7 @@ class LambdaExpression(Expression):
def __init__(
self, parts, parameters, capture: str = "=", return_type=None, source=None
) -> None:
):
self.parts = parts
if not isinstance(parameters, ParameterListExpression):
parameters = ParameterListExpression(*parameters)
@@ -207,21 +197,16 @@ class LambdaExpression(Expression):
self.capture = capture
self.return_type = safe_exp(return_type) if return_type is not None else None
def format_body(self) -> str:
"""Format the lambda body with source directive and content."""
body = ""
if self.source is not None:
body += f"{self.source.as_line_directive}\n"
body += self.content
return body
def __str__(self) -> str:
def __str__(self):
# Stateless lambdas (empty capture) implicitly convert to function pointers
# when assigned to function pointer types - no unary + needed
cpp = f"[{self.capture}]({self.parameters})"
if self.return_type is not None:
cpp += f" -> {self.return_type}"
cpp += f" {{\n{self.format_body()}\n}}"
cpp += " {\n"
if self.source is not None:
cpp += f"{self.source.as_line_directive}\n"
cpp += f"{self.content}\n}}"
return indent_all_but_first_and_last(cpp)
@property
@@ -229,37 +214,6 @@ class LambdaExpression(Expression):
return "".join(str(part) for part in self.parts)
class SharedFunctionLambdaExpression(LambdaExpression):
"""A lambda expression that references a shared deduplicated function.
This class wraps a function pointer but maintains the LambdaExpression
interface so calling code works unchanged.
"""
__slots__ = ("_func_name",)
def __init__(
self,
func_name: str,
parameters: TemplateArgsType,
return_type: SafeExpType | None = None,
) -> None:
# Initialize parent with empty parts since we're just a function reference
super().__init__(
[], parameters, capture="", return_type=return_type, source=None
)
self._func_name = func_name
def __str__(self) -> str:
# Just return the function name - it's already a function pointer
return self._func_name
@property
def content(self) -> str:
# No content, just a function reference
return ""
# pylint: disable=abstract-method
class Literal(Expression, metaclass=abc.ABCMeta):
__slots__ = ()
@@ -629,25 +583,6 @@ def add_global(expression: SafeExpType | Statement, prepend: bool = False):
CORE.add_global(expression, prepend)
@coroutine_with_priority(CoroPriority.FINAL)
async def flush_lambda_dedup_declarations() -> None:
"""Flush all deferred lambda deduplication declarations to global scope.
This is a coroutine that runs with FINAL priority (after all components)
to ensure all referenced variables are declared before the shared
lambda functions that use them.
"""
if _KEY_LAMBDA_DEDUP_DECLARATIONS not in CORE.data:
return
declarations = CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS]
for func_declaration in declarations:
add_global(RawStatement(func_declaration))
# Clear the list so we don't add them again
CORE.data[_KEY_LAMBDA_DEDUP_DECLARATIONS] = []
def add_library(name: str, version: str | None, repository: str | None = None):
"""Add a library to the codegen library storage.
@@ -721,93 +656,6 @@ async def get_variable_with_full_id(id_: ID) -> tuple[ID, "MockObj"]:
return await CORE.get_variable_with_full_id(id_)
def _has_static_variables(code: str) -> bool:
"""Check if code contains static variable definitions.
Static variables in lambdas should not be deduplicated because each lambda
instance should have its own static variable state.
Args:
code: The lambda body code to check
Returns:
True if code contains static variable definitions
"""
# Remove C++ comments to avoid false positives
# Remove single-line comments (// ...)
code_no_comments = _RE_CPP_SINGLE_LINE_COMMENT.sub("", code)
# Remove multi-line comments (/* ... */)
code_no_comments = _RE_CPP_MULTI_LINE_COMMENT.sub("", code_no_comments)
# Match: static <type> <identifier>
# But not: static_cast, static_assert, static_pointer_cast
return bool(_RE_STATIC_VARIABLE.search(code_no_comments))
def _get_shared_lambda_name(lambda_expr: LambdaExpression) -> str | None:
"""Get the shared function name for a lambda expression.
If an identical lambda was already generated, returns the existing shared
function name. Otherwise, creates a new shared function and returns its name.
Lambdas with static variables are not deduplicated to preserve their
independent state.
Args:
lambda_expr: The lambda expression to deduplicate
Returns:
The name of the shared function for this lambda (either existing or newly created),
or None if the lambda should not be deduplicated (e.g., contains static variables)
"""
# Create a unique key from the lambda content, parameters, and return type
content = lambda_expr.content
# Don't deduplicate lambdas with static variables - each instance needs its own state
if _has_static_variables(content):
return None
param_str = str(lambda_expr.parameters)
return_str = (
str(lambda_expr.return_type) if lambda_expr.return_type is not None else "void"
)
# Use tuple of (content, params, return_type) as key
lambda_key = (content, param_str, return_str)
# Initialize deduplication storage in CORE.data if not exists
if _KEY_LAMBDA_DEDUP not in CORE.data:
CORE.data[_KEY_LAMBDA_DEDUP] = {}
# Register the flush job to run after all components (FINAL priority)
# This ensures all variables are declared before shared lambda functions
CORE.add_job(flush_lambda_dedup_declarations)
lambda_cache = CORE.data[_KEY_LAMBDA_DEDUP]
# Check if we've seen this lambda before
if lambda_key in lambda_cache:
# Return name of existing shared function
return lambda_cache[lambda_key]
# First occurrence - create a shared function
# Use the cache size as the function number
func_name = f"shared_lambda_{len(lambda_cache)}"
# Build the function declaration using lambda's body formatting
func_declaration = (
f"{return_str} {func_name}({param_str}) {{\n{lambda_expr.format_body()}\n}}"
)
# Store the declaration to be added later (after all variable declarations)
# We can't add it immediately because it might reference variables not yet declared
CORE.data.setdefault(_KEY_LAMBDA_DEDUP_DECLARATIONS, []).append(func_declaration)
# Store in cache
lambda_cache[lambda_key] = func_name
# Return the function name (this is the first occurrence, but we still generate shared function)
return func_name
async def process_lambda(
value: Lambda,
parameters: TemplateArgsType,
@@ -865,19 +713,6 @@ async def process_lambda(
location.line += value.content_offset
else:
location = None
# Lambda deduplication: Only deduplicate stateless lambdas (empty capture).
# Stateful lambdas cannot be shared as they capture different contexts.
# Lambdas with static variables are also not deduplicated to preserve independent state.
if capture == "":
lambda_expr = LambdaExpression(
parts, parameters, capture, return_type, location
)
func_name = _get_shared_lambda_name(lambda_expr)
if func_name is not None:
# Return a shared function reference instead of inline lambda
return SharedFunctionLambdaExpression(func_name, parameters, return_type)
return LambdaExpression(parts, parameters, capture, return_type, location)

View File

@@ -145,16 +145,7 @@ def run_compile(config, verbose):
args = []
if CONF_COMPILE_PROCESS_LIMIT in config[CONF_ESPHOME]:
args += [f"-j{config[CONF_ESPHOME][CONF_COMPILE_PROCESS_LIMIT]}"]
result = run_platformio_cli_run(config, verbose, *args)
# Run memory analysis if enabled
if config.get(CONF_ESPHOME, {}).get("analyze_memory", False):
try:
analyze_memory_usage(config)
except Exception as e:
_LOGGER.warning("Failed to analyze memory usage: %s", e)
return result
return run_platformio_cli_run(config, verbose, *args)
def _run_idedata(config):
@@ -403,74 +394,3 @@ class IDEData:
if path.endswith(".exe")
else f"{path[:-3]}readelf"
)
def analyze_memory_usage(config: dict[str, Any]) -> None:
"""Analyze memory usage by component after compilation."""
# Lazy import to avoid overhead when not needed
from esphome.analyze_memory.cli import MemoryAnalyzerCLI
from esphome.analyze_memory.helpers import get_esphome_components
idedata = get_idedata(config)
# Get paths to tools
elf_path = idedata.firmware_elf_path
objdump_path = idedata.objdump_path
readelf_path = idedata.readelf_path
# Debug logging
_LOGGER.debug("ELF path from idedata: %s", elf_path)
# Check if file exists
if not Path(elf_path).exists():
# Try alternate path
alt_path = Path(CORE.relative_build_path(".pioenvs", CORE.name, "firmware.elf"))
if alt_path.exists():
elf_path = str(alt_path)
_LOGGER.debug("Using alternate ELF path: %s", elf_path)
else:
_LOGGER.warning("ELF file not found at %s or %s", elf_path, alt_path)
return
# Extract external components from config
external_components = set()
# Get the list of built-in ESPHome components
builtin_components = get_esphome_components()
# Special non-component keys that appear in configs
NON_COMPONENT_KEYS = {
CONF_ESPHOME,
"substitutions",
"packages",
"globals",
"<<",
}
# Check all top-level keys in config
for key in config:
if key not in builtin_components and key not in NON_COMPONENT_KEYS:
# This is an external component
external_components.add(key)
_LOGGER.debug("Detected external components: %s", external_components)
# Create analyzer and run analysis
analyzer = MemoryAnalyzerCLI(
elf_path, objdump_path, readelf_path, external_components
)
analyzer.analyze()
# Generate and print report
report = analyzer.generate_report()
_LOGGER.info("\n%s", report)
# Optionally save to file
if config.get(CONF_ESPHOME, {}).get("memory_report_file"):
report_file = Path(config[CONF_ESPHOME]["memory_report_file"])
if report_file.suffix == ".json":
report_file.write_text(analyzer.to_json())
_LOGGER.info("Memory report saved to %s", report_file)
else:
report_file.write_text(report)
_LOGGER.info("Memory report saved to %s", report_file)

View File

@@ -1,6 +1,6 @@
pylint==4.0.3
pylint==4.0.2
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.14.5 # also change in .pre-commit-config.yaml when updating
ruff==0.14.4 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.1 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -1,18 +1,6 @@
"""Tests for the web_server OTA platform."""
from __future__ import annotations
from collections.abc import Callable
import logging
from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.web_server.ota import _web_server_ota_final_validate
from esphome.const import CONF_ID, CONF_OTA, CONF_PLATFORM, CONF_WEB_SERVER
from esphome.core import ID
import esphome.final_validate as fv
def test_web_server_ota_generated(generate_main: Callable[[str], str]) -> None:
@@ -112,144 +100,3 @@ def test_web_server_ota_esp8266(generate_main: Callable[[str], str]) -> None:
# Check web server OTA component is present
assert "WebServerOTAComponent" in main_cpp
assert "web_server::WebServerOTAComponent" in main_cpp
@pytest.mark.parametrize(
("ota_configs", "expected_count", "warning_expected"),
[
pytest.param(
[
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web", is_manual=False),
}
],
1,
False,
id="single_instance_no_merge",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_1", is_manual=False),
},
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_2", is_manual=False),
},
],
1,
True,
id="two_instances_merged",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_1", is_manual=False),
},
{
CONF_PLATFORM: "esphome",
CONF_ID: ID("ota_esphome", is_manual=False),
},
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_2", is_manual=False),
},
],
2,
True,
id="mixed_platforms_web_server_merged",
),
],
)
def test_web_server_ota_instance_merging(
ota_configs: list[dict[str, Any]],
expected_count: int,
warning_expected: bool,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test web_server OTA instance merging behavior."""
full_conf = {CONF_OTA: ota_configs.copy()}
token = fv.full_config.set(full_conf)
try:
with caplog.at_level(logging.WARNING):
_web_server_ota_final_validate({})
updated_conf = fv.full_config.get()
# Verify total number of OTA platforms
assert len(updated_conf[CONF_OTA]) == expected_count
# Verify warning
if warning_expected:
assert any(
"Found and merged" in record.message
and "web_server OTA" in record.message
for record in caplog.records
), "Expected merge warning not found in log"
else:
assert len(caplog.records) == 0, "Unexpected warnings logged"
finally:
fv.full_config.reset(token)
def test_web_server_ota_consistent_manual_ids(
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test that consistent manual IDs can be merged successfully."""
ota_configs = [
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web", is_manual=True),
},
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web", is_manual=True),
},
]
full_conf = {CONF_OTA: ota_configs}
token = fv.full_config.set(full_conf)
try:
with caplog.at_level(logging.WARNING):
_web_server_ota_final_validate({})
updated_conf = fv.full_config.get()
assert len(updated_conf[CONF_OTA]) == 1
assert updated_conf[CONF_OTA][0][CONF_ID].id == "ota_web"
assert any(
"Found and merged" in record.message and "web_server OTA" in record.message
for record in caplog.records
)
finally:
fv.full_config.reset(token)
def test_web_server_ota_inconsistent_manual_ids() -> None:
"""Test that inconsistent manual IDs raise an error."""
ota_configs = [
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_1", is_manual=True),
},
{
CONF_PLATFORM: CONF_WEB_SERVER,
CONF_ID: ID("ota_web_2", is_manual=True),
},
]
full_conf = {CONF_OTA: ota_configs}
token = fv.full_config.set(full_conf)
try:
with pytest.raises(
cv.Invalid,
match="Found multiple web_server OTA configurations but id is inconsistent",
):
_web_server_ota_final_validate({})
finally:
fv.full_config.reset(token)

View File

@@ -1 +0,0 @@
"""Tests for SNTP component."""

View File

@@ -1,22 +0,0 @@
esphome:
name: sntp-test
esp32:
board: esp32dev
framework:
type: esp-idf
wifi:
ssid: "testssid"
password: "testpassword"
# Test multiple SNTP instances that should be merged
time:
- platform: sntp
servers:
- 192.168.1.1
- pool.ntp.org
- platform: sntp
servers:
- pool.ntp.org
- 192.168.1.2

View File

@@ -1,238 +0,0 @@
"""Tests for SNTP time configuration validation."""
from __future__ import annotations
import logging
from typing import Any
import pytest
from esphome import config_validation as cv
from esphome.components.sntp.time import CONF_SNTP, _sntp_final_validate
from esphome.const import CONF_ID, CONF_PLATFORM, CONF_SERVERS, CONF_TIME
from esphome.core import ID
import esphome.final_validate as fv
@pytest.mark.parametrize(
("time_configs", "expected_count", "expected_servers", "warning_messages"),
[
pytest.param(
[
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time", is_manual=False),
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
}
],
1,
["192.168.1.1", "pool.ntp.org"],
[],
id="single_instance_no_merge",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=False),
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=False),
CONF_SERVERS: ["192.168.1.2"],
},
],
1,
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
["Found and merged 2 SNTP time configurations into one instance"],
id="two_instances_merged",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=False),
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=False),
CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"],
},
],
1,
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
["Found and merged 2 SNTP time configurations into one instance"],
id="deduplication_preserves_order",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=False),
CONF_SERVERS: ["192.168.1.1", "pool.ntp.org"],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=False),
CONF_SERVERS: ["192.168.1.2", "pool2.ntp.org"],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_3", is_manual=False),
CONF_SERVERS: ["pool3.ntp.org"],
},
],
1,
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
[
"SNTP supports maximum 3 servers. Dropped excess server(s): ['pool2.ntp.org', 'pool3.ntp.org']",
"Found and merged 3 SNTP time configurations into one instance",
],
id="three_instances_drops_excess_servers",
),
pytest.param(
[
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=False),
CONF_SERVERS: [
"192.168.1.1",
"pool.ntp.org",
"pool.ntp.org",
"192.168.1.1",
],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=False),
CONF_SERVERS: ["pool.ntp.org", "192.168.1.2"],
},
],
1,
["192.168.1.1", "pool.ntp.org", "192.168.1.2"],
["Found and merged 2 SNTP time configurations into one instance"],
id="deduplication_multiple_duplicates",
),
],
)
def test_sntp_instance_merging(
time_configs: list[dict[str, Any]],
expected_count: int,
expected_servers: list[str],
warning_messages: list[str],
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test SNTP instance merging behavior."""
# Create a mock full config with time configs
full_conf = {CONF_TIME: time_configs.copy()}
# Set the context var
token = fv.full_config.set(full_conf)
try:
with caplog.at_level(logging.WARNING):
_sntp_final_validate({})
# Get the updated config
updated_conf = fv.full_config.get()
# Check if merging occurred
if len(time_configs) > 1:
# Verify only one SNTP instance remains
sntp_instances = [
tc
for tc in updated_conf[CONF_TIME]
if tc.get(CONF_PLATFORM) == CONF_SNTP
]
assert len(sntp_instances) == expected_count
# Verify server list
assert sntp_instances[0][CONF_SERVERS] == expected_servers
# Verify warnings
for expected_msg in warning_messages:
assert any(
expected_msg in record.message for record in caplog.records
), f"Expected warning message '{expected_msg}' not found in log"
else:
# Single instance should not trigger merging or warnings
assert len(caplog.records) == 0
# Config should be unchanged
assert updated_conf[CONF_TIME] == time_configs
finally:
fv.full_config.reset(token)
def test_sntp_inconsistent_manual_ids() -> None:
"""Test that inconsistent manual IDs raise an error."""
# Create configs with manual IDs that are inconsistent
time_configs = [
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=True),
CONF_SERVERS: ["192.168.1.1"],
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=True),
CONF_SERVERS: ["192.168.1.2"],
},
]
full_conf = {CONF_TIME: time_configs}
token = fv.full_config.set(full_conf)
try:
with pytest.raises(
cv.Invalid,
match="Found multiple SNTP configurations but id is inconsistent",
):
_sntp_final_validate({})
finally:
fv.full_config.reset(token)
def test_sntp_with_other_time_platforms(caplog: pytest.LogCaptureFixture) -> None:
"""Test that SNTP merging doesn't affect other time platforms."""
time_configs = [
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_1", is_manual=False),
CONF_SERVERS: ["192.168.1.1"],
},
{
CONF_PLATFORM: "homeassistant",
CONF_ID: ID("homeassistant_time", is_manual=False),
},
{
CONF_PLATFORM: CONF_SNTP,
CONF_ID: ID("sntp_time_2", is_manual=False),
CONF_SERVERS: ["192.168.1.2"],
},
]
full_conf = {CONF_TIME: time_configs.copy()}
token = fv.full_config.set(full_conf)
try:
with caplog.at_level(logging.WARNING):
_sntp_final_validate({})
updated_conf = fv.full_config.get()
# Should have 2 time platforms: 1 merged SNTP + 1 homeassistant
assert len(updated_conf[CONF_TIME]) == 2
# Find the platforms
platforms = {tc[CONF_PLATFORM] for tc in updated_conf[CONF_TIME]}
assert platforms == {CONF_SNTP, "homeassistant"}
# Verify SNTP was merged
sntp_instances = [
tc for tc in updated_conf[CONF_TIME] if tc[CONF_PLATFORM] == CONF_SNTP
]
assert len(sntp_instances) == 1
assert sntp_instances[0][CONF_SERVERS] == ["192.168.1.1", "192.168.1.2"]
finally:
fv.full_config.reset(token)

View File

@@ -1,6 +1,4 @@
"""Tests for the text component."""
from esphome.core import CORE
"""Tests for the binary sensor component."""
def test_text_is_setup(generate_main):
@@ -58,22 +56,15 @@ def test_text_config_value_mode_set(generate_main):
assert "it_3->traits.set_mode(text::TEXT_MODE_PASSWORD);" in main_cpp
def test_text_config_lambda_is_set(generate_main) -> None:
def test_text_config_lamda_is_set(generate_main):
"""
Test if lambda is set for lambda mode (optimized with stateless lambda and deduplication)
Test if lambda is set for lambda mode (optimized with stateless lambda)
"""
# Given
# When
main_cpp = generate_main("tests/component_tests/text/test_text.yaml")
# Get both global and main sections to find the shared lambda definition
full_cpp = CORE.cpp_global_section + main_cpp
# Then
# Lambda is deduplicated into a shared function (reference in main section)
assert "it_4->set_template(shared_lambda_" in main_cpp
# Lambda body should be in the code somewhere
assert 'return std::string{"Hello"};' in full_cpp
# Verify the shared lambda function is defined (in global section)
assert "esphome::optional<std::string> shared_lambda_" in full_cpp
assert "it_4->set_template([]() -> esphome::optional<std::string> {" in main_cpp
assert 'return std::string{"Hello"};' in main_cpp

View File

@@ -1,27 +0,0 @@
esp32:
variant: esp32p4
flash_size: 32MB
cpu_frequency: 400MHz
framework:
type: esp-idf
advanced:
enable_idf_experimental_features: yes
ota:
platform: esphome
wifi:
ssid: MySSID
password: password1
esp32_hosted:
variant: ESP32C6
slot: 1
active_high: true
reset_pin: GPIO15
cmd_pin: GPIO13
clk_pin: GPIO12
d0_pin: GPIO11
d1_pin: GPIO10
d2_pin: GPIO9
d3_pin: GPIO8

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